feat: changed TPV behaviour

This commit is contained in:
Pablo Moreno
2025-01-12 22:40:46 +01:00
parent fc0cdad246
commit a733325529
43 changed files with 1036 additions and 1100 deletions
-1
View File
@@ -39,7 +39,6 @@ PROJECT_APPS = [
"files",
"shop",
"theme",
"tpv",
"users",
"web",
]
+1 -1
View File
@@ -9,8 +9,8 @@ urlpatterns = [
path("admin/", admin.site.urls),
path("api/v1/", include("config.api.v1.urls")),
path("watchman/", include("watchman.urls")),
path("tpv/", include("tpv.urls", namespace="tpv")),
path("auth/", include("users.urls", namespace="users")),
path("tpv/", include("shop.urls", namespace="shop")),
]
if settings.DEBUG:
+11
View File
@@ -14,6 +14,7 @@ from shop.models import (
ProductPrice,
Provider,
ShippingMethod,
ShopSettings,
Tag,
Tax,
)
@@ -155,3 +156,13 @@ class ProductCategoryAdmin(ModelAdmin):
"hidden",
"show_in_navbar",
)
@admin.register(ShopSettings)
class ShopSettingsAdmin(ModelAdmin):
list_display = (
"id",
"merchant_code",
"currency_code",
"terminal",
)
+3 -2
View File
@@ -1,9 +1,10 @@
# Generated by Django 5.1.3 on 2025-01-03 11:17
import uuid
from decimal import Decimal
import django.db.models.deletion
import django.utils.timezone
import uuid
from decimal import Decimal
from django.conf import settings
from django.db import migrations, models
@@ -0,0 +1,344 @@
# Generated by Django 5.1.4 on 2025-01-12 13:03
import uuid
from decimal import Decimal
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import shop.models
class Migration(migrations.Migration):
dependencies = [
("shop", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="ShopSettings",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"merchant_code",
models.CharField(
max_length=9, verbose_name="Identificación de comercio"
),
),
(
"currency_code",
models.CharField(
choices=[
("8", "LEK ALL"),
("12", "ALGERIAN DINAR DZD"),
("24", "ANGOLA KWANZA AOK"),
("30", "PROBANDO DESA MON"),
("31", "AZERBAIJANIAN MANAT AZM"),
("32", "ARGENTINE AUSTRAL ARP"),
("36", "AUSTRALIAN DOLLAR AUD"),
("44", "BAHAMIAN DOLLAR BSD"),
("48", "BAHRAINI DINAR BHD"),
("50", "TAKA BDT"),
("51", "ARMENIAN DRAM AMD"),
("52", "BARBADOS DOLLAR BBD"),
("60", "BERMUDAN DOLLAR BMD"),
("64", "NGULTRUM BTN"),
("68", "BOLIVIAN PESO BOP"),
("70", "DINAR BAD"),
("72", "PULA BWP"),
("76", "CRUZEIRO BRC"),
("84", "BELIZE DOLLAR 084"),
("90", "SOLOMON ISLANDS DOLL SBD"),
("96", "BRUNEI DOLLAR BND"),
("100", "LEV BGL"),
("104", "KYAT BUK"),
("108", "BURUNDI FRANC BIF"),
("112", "BELARUSSIAN RUBLE BYB"),
("116", "RIEL KHR"),
("124", "CANADIAN DOLLAR CAD"),
("132", "CAPE VERDE ESCUDO CVE"),
("136", "CAYMAN ISLANDS DOLLA KYD"),
("144", "SRI LANKA RUPEE LKR"),
("152", "CHILEAN PESO CLP"),
("156", "YUAN RENMINBI CNY"),
("157", "CHINESE RENMIMBI CNH"),
("158", "CHINESE RENMINBI CNX"),
("170", "COLOMBIAN PESO COP"),
("174", "COMOROS FRANC KMF"),
("180", "ZAIRE ZRZ"),
("188", "COSTA RICA COLON CRC"),
("191", "CROATIAN KUNA HRK"),
("192", "CUBAN PESO CUP"),
("196", "CYPRUS POUND CYP"),
("200", "KORUNA CSK"),
("203", "CZECH KORUNA CZK"),
("208", "DANISH KRONE DKK"),
("214", "DOMINICAN PESO DOP"),
("218", "SUCRE ECS"),
("222", "EL SALVADOR COLON SVC"),
("226", "EKWELE GQE"),
("230", "ETHIOPIAN BIRR ETB"),
("232", "ERITREAN NAKTAN ERN"),
("233", "ESTONIAN KROON EEK"),
("238", "FALKLAND ISLANDS FKP"),
("242", "FIJI DOLLAR FJD"),
("262", "DJIBOUTI FRANC DJF"),
("268", "GEORGIAN LARI GEL"),
("270", "DALASI GMD"),
("278", "MARK DER DDR DDM"),
("288", "GHANA CEDI GHC"),
("292", "GIBRALTAR POUND GIP"),
("320", "QUETZAL GTQ"),
("324", "SYLI GNS"),
("328", "GUYANA DOLLAR GYD"),
("332", "GOURDE HTG"),
("340", "LEMPIRA HNL"),
("344", "HONG KONG DOLLAR HKD"),
("348", "FORINT HUF"),
("352", "ICELAND KRONA ISK"),
("356", "INDIAN RUPEE INR"),
("360", "RUPIAH IDR"),
("364", "IRANIAL RIAL IRR"),
("365", "IRANIAN AIRLINE RATE IRA"),
("368", "IRAQI DINAR IQD"),
("376", "ISRAEL SHEKEL ILS"),
("388", "JAMAICAN DOLLAR JMD"),
("392", "YEN JPY"),
("398", "TENGE KZT"),
("400", "JORDANIAN DINAR JOD"),
("404", "KENYAN SHILLING KES"),
("408", "NORTH KOREAN WON KPW"),
("410", "KOREAN WON KRW"),
("414", "KUWAITI DINAR KWD"),
("417", "KYRGYZSTAN SON KGS"),
("418", "KIP LAK"),
("422", "LEBANESE POUND LBP"),
("426", "LESOTHO LOTI LSM"),
("428", "LATVIAN LAT LVL"),
("430", "LIBERIAN DOLLAR LRD"),
("434", "LIBYAN DINAR LYD"),
("440", "LITHUANIAN LITAS LTL"),
("446", "PATACA MOP"),
("450", "MALAGASY FRANC MGF"),
("454", "MALAWI KWACHA MWK"),
("458", "MALASYAN RINGGIT MYR"),
("462", "MALDIVE RUPEE MVR"),
("466", "MALI MLF"),
("470", "MALTESE LIRA MTL"),
("478", "OUGUIYA MRO"),
("480", "MAURITIUS RUPEE MUR"),
("484", "MEXICAN PESO MXP"),
("496", "TUGRIK MNT"),
("498", "MOLDOVIAN LEU MDL"),
("504", "MORROCAN DIRHAM MAD"),
("508", "METICAL MZM"),
("512", "RIAL OMANI OMR"),
("516", "NAMIBIAN DOLLAR NAD"),
("524", "NEPALESE RUPEE NPR"),
("532", "NETHERLANDS ANTILLIA ANG"),
("533", "ARUBA AWG"),
("536", "YUGOSLAVIAN NEW DIAN NTZ"),
("548", "VANUATU VATU VUV"),
("554", "NEW ZEALAND DOLLAR NZD"),
("556", "NAIRA 566"),
("558", "CORDOBA NIC"),
("566", "NAIRA NGN"),
("578", "NORWEGIAN KRONE NOK"),
("582", "PACIFIC ISLAND PCI"),
("586", "PAKISTAN RUPEE PKR"),
("590", "BALBOA PAB"),
("598", "KINA PGK"),
("600", "GUARANI PYG"),
("604", "PERU INTI PEI"),
("608", "PHILIPPINE PESO PHP"),
("616", "ZLOTY PLZ"),
("624", "GUINEA"),
("626", "TIMOR ESCUDO TPE"),
("634", "QATARI RIAL QAR"),
("642", "LEU ROL"),
("643", "RUSSIAN ROUBLE RUB"),
("646", "RWANDA FRANC RWF"),
("654", "ST.HELENA POUND SHP"),
("678", "DOBRA STD"),
("682", "SAUDI RIYAL SAR"),
("690", "SEYCHELLES RUPEE SCR"),
("694", "LEONE SLL"),
("702", "SINGAPORE DOLLAR SGD"),
("703", "SLOVAK KORUNA SKK"),
("704", "DONG VND"),
("705", "SLOVENIAN TOLAR SIT"),
("706", "SOMALI SHILLING SOS"),
("710", "RAND ZAR"),
("716", "ZIMBABWE DOLLAR ZWD"),
("720", "YEMENI DINAR YDD"),
("728", "SOUTH SUDANESE POUND SSP"),
("736", "SUDANESE POUND SDP"),
("737", "SUDAN AIRLINES SDA"),
("740", "SURINAM GUILDER SRG"),
("748", "LILANGENI SZL"),
("752", "SWEDISH KRONA SEK"),
("756", "SWISS FRANC CHF"),
("760", "SYRIAN POUND SYP"),
("762", "TAJIK RUBLE TJR"),
("764", "BAHT THB"),
("776", "PA'ANGA TOP"),
("780", "TRINIDAD Y TOBAGO DO TTD"),
("784", "UAE DIRHAM AED"),
("788", "TUNISIAN DINAR TND"),
("792", "TURKISH LIRA TRL"),
("793", "PSEUDO TURKISH LIRA PTL"),
("795", "MANAT TMM"),
("800", "UGANDA SHILLING UGS"),
("804", "KARBOVANET UAK"),
("807", "MACEDONIAN DENAR MKD"),
("810", "RUSSIAN ROUBLE RUR"),
("818", "EGYPTIAN POUND EGP"),
("826", "POUND STERLING GBP"),
("834", "TANZANIAN SHILLING TZS"),
("840", "DOLAR U.S.A. USD"),
("858", "URUGUAYAN PESO UYP"),
("860", "UZBEKISTAN SUM UZS"),
("862", "BOLIVAR VEB"),
("882", "TALA WST"),
("886", "YEMINI RIAL YER"),
("890", "NEW YUGOSLAVIAN DOLL YUD"),
("891", "NEW DINAR YUG"),
("894", "KWACHA ZMK"),
("901", "NEW TAIWAN DOLLAR TWD"),
("934", "NEW MANAT TMT"),
("936", "GHANA CEDI GHS"),
("941", "DINAR SERBIO RSD"),
("943", "MOZAMBIQUE METICAL MZN"),
("944", "AZERBAIJANIAN MANAT AZN"),
("946", "NEW LEU RON"),
("949", "TURKISH LIRA TRY"),
("950", "CFA FRANC BEAC XAF"),
("951", "EAST CARIBBEAN DOLLA XCD"),
("952", "CFA FRANC BCEAO XOF"),
("953", "CFP FRANC XPF"),
("954", "E.C.U. EUROPEAN CUR XEU"),
("967", "KWACHA ZMW"),
("968", "SURINAME DOLLAR SRD"),
("969", "ARIARY MGA"),
("971", "AFGHANISTAN AFGHANI AFN"),
("972", "TAJIKISTAN SOMONI TJS"),
("973", "KWANZA ANGOLA AOA"),
("974", "BELARUSSIAN RUBLE BYR"),
("975", "NEW LEV BGN"),
("976", "FRANCO DEL CONGO CDF"),
("977", "BOSNIAN MARKA BAM"),
("978", "EURO EUR"),
("980", "HRYVNIA UAH"),
("981", "GEORGIAN LARI GEL"),
("985", "NEW POLISH ZLOTY PLN"),
("986", "BRAZILIAN REAL BRL"),
("991", "RAND FINANCIER ZAL"),
],
default="978",
max_length=4,
verbose_name="Código de moneda",
),
),
("terminal", models.CharField(max_length=8, verbose_name="Terminal")),
(
"shared_secret",
models.CharField(max_length=100, verbose_name="Clave de Redsys"),
),
],
options={
"verbose_name": "ajustes de la tienda",
"verbose_name_plural": "ajustes de la tienda",
},
),
migrations.AddField(
model_name="order",
name="code",
field=models.CharField(
default=shop.models.create_order_code, max_length=20, unique=True
),
),
migrations.AddField(
model_name="order",
name="shipping_status",
field=models.CharField(
default="NOT", max_length=3, verbose_name="estado de envío"
),
),
migrations.AddField(
model_name="product",
name="is_shipping_method",
field=models.BooleanField(default=False, verbose_name="es forma de envío"),
),
migrations.CreateModel(
name="Payment",
fields=[
(
"hash",
models.UUIDField(
default=uuid.uuid4,
primary_key=True,
serialize=False,
verbose_name="Hash",
),
),
(
"amount",
models.DecimalField(
decimal_places=2,
default=Decimal("0"),
max_digits=8,
verbose_name="Cantidad",
),
),
(
"creation_date",
models.DateTimeField(
auto_now_add=True, verbose_name="Fecha de creación"
),
),
("metadata", models.JSONField(default=dict, verbose_name="Metadata")),
(
"method",
models.CharField(
choices=[("REDSYS", "Redsys")],
default="REDSYS",
max_length=6,
verbose_name="método de pago",
),
),
(
"order",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="shop.order",
verbose_name="pedido",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
verbose_name="cliente",
),
),
],
options={
"verbose_name": "Pago",
"verbose_name_plural": "Pagos",
},
),
]
+91
View File
@@ -8,6 +8,9 @@ from django.utils import timezone
from django.utils.text import gettext_lazy as _
from django.utils.text import slugify
from config.models import SingletonModel
from shop.settings import CURRENCY_CODES
User = get_user_model()
@@ -118,6 +121,9 @@ class Product(TimestampedModel):
default="", blank=True, null=True, max_length=128, verbose_name=_("Slug")
)
hidden = models.BooleanField(default=False, db_index=True, verbose_name=_("oculto"))
is_shipping_method = models.BooleanField(
default=False, verbose_name=_("es forma de envío")
)
def __str__(self):
return self.name
@@ -328,30 +334,55 @@ class OrderLine(TimestampedModel):
verbose_name_plural = _("líneas de pedido")
def create_order_code(*args, **kwargs):
return (
timezone.now()
.isoformat()
.replace("-", "")
.replace("T", "")
.replace(":", "")
.replace(".", "")[:-5]
)
class Order(TimestampedModel):
class Statuses(models.TextChoices):
STATUS_PENDING = "PEN", _("pendiente de pago")
STATUS_PAID = "PAI", _("pagado")
STATUS_RETURNED = "RTN", _("devuelto")
STATUS_ERROR = "ERR", _("Error")
class ShippingStatuses(models.TextChoices):
STATUS_NOT_READY = "NOT", _("no preparado")
STATUS_READY = "RDY", _("preparado")
STATUS_TO_BE_SENT = "TBS", _("listo para ser enviado")
STATUS_SENT = "SNT", _("enviado")
STATUS_DELIVERED = "DLV", _("entregado")
STATUS_FINISHED = "FIN", _("finalizado")
STATUS_CANCELED = "CAN", _("cancelado")
STATUS_REQUESTED_RETURN = "RQT", _("devolución solicitada")
STATUS_RETURNED = "RTN", _("devuelto")
uuid = models.UUIDField(default=uuid4, verbose_name=_("UUID"), db_index=True)
code = models.CharField(default=create_order_code, max_length=20, unique=True)
status = models.CharField(
max_length=3, default=Statuses.STATUS_PENDING, verbose_name=_("estado")
)
shipping_status = models.CharField(
max_length=3,
default=ShippingStatuses.STATUS_NOT_READY,
verbose_name=_("estado de envío"),
)
base_total = models.DecimalField(
default=Decimal("0"),
max_digits=13,
decimal_places=2,
verbose_name=_("base imponible"),
)
total = models.DecimalField(
default=Decimal("0"), max_digits=13, decimal_places=2, verbose_name=_("total")
)
@@ -536,3 +567,63 @@ class WishlistedProduct(TimestampedModel):
"user",
"product",
)
class Payment(models.Model):
class MethodChoices(models.TextChoices):
REDSYS = "REDSYS"
hash = models.UUIDField(verbose_name=_("Hash"), default=uuid4, primary_key=True)
amount = models.DecimalField(
max_digits=8,
decimal_places=2,
default=Decimal("0"),
verbose_name=_("Cantidad"),
)
order = models.ForeignKey(
"shop.Order",
on_delete=models.PROTECT,
verbose_name=_("pedido"),
related_name="payments",
)
user = models.ForeignKey(
User, blank=True, null=True, on_delete=models.PROTECT, verbose_name=_("cliente")
)
creation_date = models.DateTimeField(
auto_now_add=True, verbose_name=_("Fecha de creación")
)
metadata = models.JSONField(default=dict, verbose_name=_("Metadata"))
method = models.CharField(
max_length=6,
choices=MethodChoices.choices,
default=MethodChoices.REDSYS,
verbose_name=_("método de pago"),
)
class Meta:
verbose_name = _("Pago")
verbose_name_plural = _("Pagos")
class ShopSettings(SingletonModel):
merchant_code = models.CharField(
max_length=9,
blank=False,
null=False,
verbose_name=_("Identificación de comercio"),
)
currency_code = models.CharField(
max_length=4,
default="978",
choices=CURRENCY_CODES,
verbose_name=_("Código de moneda"),
) # 978 == EURO
terminal = models.CharField(max_length=8, verbose_name=_("Terminal"))
shared_secret = models.CharField(max_length=100, verbose_name=_("Clave de Redsys"))
class Meta:
verbose_name = _("ajustes de la tienda")
verbose_name_plural = _("ajustes de la tienda")
+62 -72
View File
@@ -1,24 +1,25 @@
import base64
import json
from decimal import Decimal
import requests
from django.conf import settings
from django.urls import reverse
from tpv.exceptions import RedsysPaymentException
from tpv.models import PaymentTransaction
from tpv.settings import TransactionTypes
from tpv.utils import compute_signature, decode_b64_dict
from shop.exceptions import RedsysPaymentException
from shop.models import Order, Payment
from shop.settings import TransactionTypes
from shop.utils import compute_signature, decode_b64_dict
class RedsysClient:
DEBUG_ENVIRONMENT_URL = "https://sis-t.redsys.es:25443/sis/realizarPago"
PROD_ENVIRONMENT_URL = ""
PROD_ENVIRONMENT_URL = "https://sis.redsys.es/sis/realizarPago"
REST_DEBUG_ENVIRONMENT_URL = (
"https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST"
)
REST_PROD_ENVIRONMENT_URL = ""
REST_PROD_ENVIRONMENT_URL = "https://sis.redsys.es/sis/rest/trataPeticionREST"
def get_target_url(self):
if settings.DEBUG:
@@ -39,20 +40,19 @@ class RedsysClient:
def get_currency_code(self) -> str:
return settings.REDSYS_CURRENCY_CODE
def get_merchant_url_ok_for_transaction(
self, transaction: PaymentTransaction
) -> str:
path = reverse("tpv:ok", kwargs={"transaction": transaction.hash})
def get_merchant_url_ok_for_order(self, order: Order) -> str:
path = reverse("web:order", kwargs={"uuid": order.uuid})
return f"{settings.REDSYS_TPV_DOMAIN}{path}"
def get_merchant_url_ko_for_transaction(
self, transaction: PaymentTransaction
) -> str:
path = reverse("tpv:ko", kwargs={"transaction": transaction.hash})
def to_integer(self, value: Decimal):
return int(value * 100)
def get_merchant_url_ko_for_order(self, order: Order) -> str:
path = reverse("web:order", kwargs={"uuid": order.uuid})
return f"{settings.REDSYS_TPV_DOMAIN}{path}"
def get_webhook_url_for_transaction(self, transaction: PaymentTransaction) -> str:
path = reverse("tpv:webhook", kwargs={"transaction": transaction.hash})
def get_webhook_url_for_order(self, order: Order) -> str:
path = reverse("shop:webhook", kwargs={"uuid": order.uuid})
return f"{settings.REDSYS_TPV_DOMAIN}{path}"
def get_shared_secret(self) -> str:
@@ -62,49 +62,45 @@ class RedsysClient:
key = self.get_shared_secret()
return compute_signature(str(hash), payload, key).decode()
def _get_merchant_parameters_for_transaction(
def _get_merchant_parameters_for_order(
self,
transaction: PaymentTransaction,
transaction_type: int = TransactionTypes.AUTHORIZATION,
order: Order,
order_type: int = TransactionTypes.AUTHORIZATION,
) -> dict:
merchant_code = self.get_merchant_code()
return {
"DS_MERCHANT_AMOUNT": str(transaction.amount_integer),
"DS_MERCHANT_AMOUNT": str(self.to_integer(order.total)),
"DS_MERCHANT_CURRENCY": self.get_currency_code(),
"DS_MERCHANT_MERCHANTCODE": merchant_code,
# Webhook
"DS_MERCHANT_MERCHANTURL": self.get_webhook_url_for_transaction(
transaction
),
"DS_MERCHANT_ORDER": transaction.hash.hex,
"DS_MERCHANT_MERCHANTURL": self.get_webhook_url_for_order(order),
"DS_MERCHANT_ORDER": order.code,
"DS_MERCHANT_TERMINAL": self.get_terminal(),
"DS_MERCHANT_TRANSACTIONTYPE": transaction_type,
"DS_MERCHANT_TRANSACTIONTYPE": order_type,
# Página informativa al usuario - Pago erróneo
"DS_MERCHANT_URLKO": self.get_merchant_url_ko_for_transaction(transaction),
"DS_MERCHANT_URLKO": self.get_merchant_url_ko_for_order(order),
# Página informativa al usuario - Pago confirmado
"DS_MERCHANT_URLOK": self.get_merchant_url_ok_for_transaction(transaction),
"DS_MERCHANT_URLOK": self.get_merchant_url_ok_for_order(order),
}
def _get_encoded_merchant_parameters_for_transaction(
def _get_encoded_merchant_parameters_for_order(
self,
transaction: PaymentTransaction,
transaction_type: int = TransactionTypes.AUTHORIZATION,
order: Order,
order_type: int = TransactionTypes.AUTHORIZATION,
) -> str:
body = self._get_merchant_parameters_for_transaction(
transaction, transaction_type
)
body = self._get_merchant_parameters_for_order(order, order_type)
return self._encode_body(body)
def get_body_for_transaction(
def get_body_for_order(
self,
transaction: PaymentTransaction,
transaction_type: int = TransactionTypes.AUTHORIZATION,
order: Order,
order_type: int = TransactionTypes.AUTHORIZATION,
) -> dict:
merchant_parameters = self._get_encoded_merchant_parameters_for_transaction(
transaction, transaction_type
merchant_parameters = self._get_encoded_merchant_parameters_for_order(
order, order_type
)
signature = self.get_signature(transaction.hash.hex, merchant_parameters)
signature = self.get_signature(order.code, merchant_parameters)
return {
"Ds_MerchantParameters": merchant_parameters,
@@ -116,63 +112,61 @@ class RedsysClient:
stringified_body = json.dumps(value)
return base64.b64encode(stringified_body.encode()).decode("utf-8")
def _get_rest_merchant_parameters_for_transaction(
def _get_rest_merchant_parameters_for_order(
self,
transaction: PaymentTransaction,
order: Order,
pan: str,
expiry_date: str,
cvv2: str,
transaction_type: int = TransactionTypes.AUTHORIZATION,
order_type: int = TransactionTypes.AUTHORIZATION,
) -> dict:
merchant_code = self.get_merchant_code()
return {
"DS_MERCHANT_AMOUNT": str(transaction.amount_integer),
"DS_MERCHANT_AMOUNT": str(self.to_integer(order.total)),
"DS_MERCHANT_CURRENCY": self.get_currency_code(),
"DS_MERCHANT_CVV2": cvv2,
"DS_MERCHANT_EXPIRYDATE": expiry_date,
"DS_MERCHANT_MERCHANTCODE": merchant_code,
"DS_MERCHANT_ORDER": transaction.hash.hex,
"DS_MERCHANT_ORDER": order.uuid.hex,
"DS_MERCHANT_PAN": pan,
"DS_MERCHANT_TERMINAL": "1",
"DS_MERCHANT_TRANSACTIONTYPE": transaction_type,
"DS_MERCHANT_TRANSACTIONTYPE": order_type,
}
def _get_rest_encoded_merchant_parameters_for_transaction(
def _get_rest_encoded_merchant_parameters_for_order(
self,
transaction: PaymentTransaction,
order: Order,
pan: str,
expiry_date: str,
cvv2: str,
transaction_type: int = TransactionTypes.AUTHORIZATION,
order_type: int = TransactionTypes.AUTHORIZATION,
) -> str:
body = self._get_rest_merchant_parameters_for_transaction(
transaction,
body = self._get_rest_merchant_parameters_for_order(
order,
pan=pan,
expiry_date=expiry_date,
cvv2=cvv2,
transaction_type=transaction_type,
order_type=order_type,
)
return self._encode_body(body)
def _get_rest_body_for_transaction(
def _get_rest_body_for_order(
self,
transaction: PaymentTransaction,
order: Order,
pan: str,
expiry_date: str,
cvv2: str,
transaction_type: int = TransactionTypes.AUTHORIZATION,
order_type: int = TransactionTypes.AUTHORIZATION,
) -> dict:
merchant_parameters = (
self._get_rest_encoded_merchant_parameters_for_transaction(
transaction=transaction,
merchant_parameters = self._get_rest_encoded_merchant_parameters_for_order(
order=order,
pan=pan,
expiry_date=expiry_date,
cvv2=cvv2,
transaction_type=transaction_type,
order_type=order_type,
)
)
signature = self.get_signature(transaction.hash.hex, merchant_parameters)
signature = self.get_signature(order.code, merchant_parameters)
return {
"Ds_MerchantParameters": merchant_parameters,
@@ -180,31 +174,27 @@ class RedsysClient:
"Ds_Signature": signature,
}
def make_request_for_transaction(
def make_request_for_order(
self,
transaction: PaymentTransaction,
order: Order,
pan: str,
expiry_date: str,
cvv2: str,
transaction_type: int = TransactionTypes.AUTHORIZATION,
order_type: int = TransactionTypes.AUTHORIZATION,
):
body = self._get_rest_body_for_transaction(
transaction=transaction,
body = self._get_rest_body_for_order(
order=order,
pan=pan,
expiry_date=expiry_date,
cvv2=cvv2,
transaction_type=transaction_type,
order_type=order_type,
)
url = self.get_rest_target_url()
response = requests.post(url, body)
return response
def pay_transaction_rest(
self, transaction: PaymentTransaction, pan: str, expiry_date: str, cvv2: str
):
response = self.make_request_for_transaction(
transaction, pan, expiry_date, cvv2
)
def pay_order_rest(self, order: Order, pan: str, expiry_date: str, cvv2: str):
response = self.make_request_for_order(order, pan, expiry_date, cvv2)
data = response.json()
error_code: str = data.get("errorCode", "")
+274
View File
@@ -0,0 +1,274 @@
from django.utils.text import gettext_lazy as _
class TransactionTypes:
AUTHORIZATION = 0
PREAUTHORIZATION = 1
REPLACEMENT_PREAUTHORIZATION = 11
CONFIRMATION = 2
REFUND = 3
SEPARATED_CONFIRMATION = 8
CANCELED = 9
PAYGOLD = 15
PUCE_AUTHENTICATION = 17
REFUND_WITHOUT_ORIGINAL = 34
BET_PRICE = 37
PAYMENT_CANCELED = 45
REFUND_CANCELED = 46
SEPARATED_CONFIRMATION_CANCELED = 47
LINK_EXPIRATION_CHANGED_FOR_PAYGOLD = 51
CURRENCY_CODES = {
"8": "LEK ALL",
"12": "ALGERIAN DINAR DZD",
"24": "ANGOLA KWANZA AOK",
"30": "PROBANDO DESA MON",
"31": "AZERBAIJANIAN MANAT AZM",
"32": "ARGENTINE AUSTRAL ARP",
"36": "AUSTRALIAN DOLLAR AUD",
"44": "BAHAMIAN DOLLAR BSD",
"48": "BAHRAINI DINAR BHD",
"50": "TAKA BDT",
"51": "ARMENIAN DRAM AMD",
"52": "BARBADOS DOLLAR BBD",
"60": "BERMUDAN DOLLAR BMD",
"64": "NGULTRUM BTN",
"68": "BOLIVIAN PESO BOP",
"70": "DINAR BAD",
"72": "PULA BWP",
"76": "CRUZEIRO BRC",
"84": "BELIZE DOLLAR 084",
"90": "SOLOMON ISLANDS DOLL SBD",
"96": "BRUNEI DOLLAR BND",
"100": "LEV BGL",
"104": "KYAT BUK",
"108": "BURUNDI FRANC BIF",
"112": "BELARUSSIAN RUBLE BYB",
"116": "RIEL KHR",
"124": "CANADIAN DOLLAR CAD",
"132": "CAPE VERDE ESCUDO CVE",
"136": "CAYMAN ISLANDS DOLLA KYD",
"144": "SRI LANKA RUPEE LKR",
"152": "CHILEAN PESO CLP",
"156": "YUAN RENMINBI CNY",
"157": "CHINESE RENMIMBI CNH",
"158": "CHINESE RENMINBI CNX",
"170": "COLOMBIAN PESO COP",
"174": "COMOROS FRANC KMF",
"180": "ZAIRE ZRZ",
"188": "COSTA RICA COLON CRC",
"191": "CROATIAN KUNA HRK",
"192": "CUBAN PESO CUP",
"196": "CYPRUS POUND CYP",
"200": "KORUNA CSK",
"203": "CZECH KORUNA CZK",
"208": "DANISH KRONE DKK",
"214": "DOMINICAN PESO DOP",
"218": "SUCRE ECS",
"222": "EL SALVADOR COLON SVC",
"226": "EKWELE GQE",
"230": "ETHIOPIAN BIRR ETB",
"232": "ERITREAN NAKTAN ERN",
"233": "ESTONIAN KROON EEK",
"238": "FALKLAND ISLANDS FKP",
"242": "FIJI DOLLAR FJD",
"262": "DJIBOUTI FRANC DJF",
"268": "GEORGIAN LARI GEL",
"270": "DALASI GMD",
"278": "MARK DER DDR DDM",
"288": "GHANA CEDI GHC",
"292": "GIBRALTAR POUND GIP",
"320": "QUETZAL GTQ",
"324": "SYLI GNS",
"328": "GUYANA DOLLAR GYD",
"332": "GOURDE HTG",
"340": "LEMPIRA HNL",
"344": "HONG KONG DOLLAR HKD",
"348": "FORINT HUF",
"352": "ICELAND KRONA ISK",
"356": "INDIAN RUPEE INR",
"360": "RUPIAH IDR",
"364": "IRANIAL RIAL IRR",
"365": "IRANIAN AIRLINE RATE IRA",
"368": "IRAQI DINAR IQD",
"376": "ISRAEL SHEKEL ILS",
"388": "JAMAICAN DOLLAR JMD",
"392": "YEN JPY",
"398": "TENGE KZT",
"400": "JORDANIAN DINAR JOD",
"404": "KENYAN SHILLING KES",
"408": "NORTH KOREAN WON KPW",
"410": "KOREAN WON KRW",
"414": "KUWAITI DINAR KWD",
"417": "KYRGYZSTAN SON KGS",
"418": "KIP LAK",
"422": "LEBANESE POUND LBP",
"426": "LESOTHO LOTI LSM",
"428": "LATVIAN LAT LVL",
"430": "LIBERIAN DOLLAR LRD",
"434": "LIBYAN DINAR LYD",
"440": "LITHUANIAN LITAS LTL",
"446": "PATACA MOP",
"450": "MALAGASY FRANC MGF",
"454": "MALAWI KWACHA MWK",
"458": "MALASYAN RINGGIT MYR",
"462": "MALDIVE RUPEE MVR",
"466": "MALI MLF",
"470": "MALTESE LIRA MTL",
"478": "OUGUIYA MRO",
"480": "MAURITIUS RUPEE MUR",
"484": "MEXICAN PESO MXP",
"496": "TUGRIK MNT",
"498": "MOLDOVIAN LEU MDL",
"504": "MORROCAN DIRHAM MAD",
"508": "METICAL MZM",
"512": "RIAL OMANI OMR",
"516": "NAMIBIAN DOLLAR NAD",
"524": "NEPALESE RUPEE NPR",
"532": "NETHERLANDS ANTILLIA ANG",
"533": "ARUBA AWG",
"536": "YUGOSLAVIAN NEW DIAN NTZ",
"548": "VANUATU VATU VUV",
"554": "NEW ZEALAND DOLLAR NZD",
"556": "NAIRA 566",
"558": "CORDOBA NIC",
"566": "NAIRA NGN",
"578": "NORWEGIAN KRONE NOK",
"582": "PACIFIC ISLAND PCI",
"586": "PAKISTAN RUPEE PKR",
"590": "BALBOA PAB",
"598": "KINA PGK",
"600": "GUARANI PYG",
"604": "PERU INTI PEI",
"608": "PHILIPPINE PESO PHP",
"616": "ZLOTY PLZ",
"624": "GUINEA",
"626": "TIMOR ESCUDO TPE",
"634": "QATARI RIAL QAR",
"642": "LEU ROL",
"643": "RUSSIAN ROUBLE RUB",
"646": "RWANDA FRANC RWF",
"654": "ST.HELENA POUND SHP",
"678": "DOBRA STD",
"682": "SAUDI RIYAL SAR",
"690": "SEYCHELLES RUPEE SCR",
"694": "LEONE SLL",
"702": "SINGAPORE DOLLAR SGD",
"703": "SLOVAK KORUNA SKK",
"704": "DONG VND",
"705": "SLOVENIAN TOLAR SIT",
"706": "SOMALI SHILLING SOS",
"710": "RAND ZAR",
"716": "ZIMBABWE DOLLAR ZWD",
"720": "YEMENI DINAR YDD",
"728": "SOUTH SUDANESE POUND SSP",
"736": "SUDANESE POUND SDP",
"737": "SUDAN AIRLINES SDA",
"740": "SURINAM GUILDER SRG",
"748": "LILANGENI SZL",
"752": "SWEDISH KRONA SEK",
"756": "SWISS FRANC CHF",
"760": "SYRIAN POUND SYP",
"762": "TAJIK RUBLE TJR",
"764": "BAHT THB",
"776": "PA'ANGA TOP",
"780": "TRINIDAD Y TOBAGO DO TTD",
"784": "UAE DIRHAM AED",
"788": "TUNISIAN DINAR TND",
"792": "TURKISH LIRA TRL",
"793": "PSEUDO TURKISH LIRA PTL",
"795": "MANAT TMM",
"800": "UGANDA SHILLING UGS",
"804": "KARBOVANET UAK",
"807": "MACEDONIAN DENAR MKD",
"810": "RUSSIAN ROUBLE RUR",
"818": "EGYPTIAN POUND EGP",
"826": "POUND STERLING GBP",
"834": "TANZANIAN SHILLING TZS",
"840": "DOLAR U.S.A. USD",
"858": "URUGUAYAN PESO UYP",
"860": "UZBEKISTAN SUM UZS",
"862": "BOLIVAR VEB",
"882": "TALA WST",
"886": "YEMINI RIAL YER",
"890": "NEW YUGOSLAVIAN DOLL YUD",
"891": "NEW DINAR YUG",
"894": "KWACHA ZMK",
"901": "NEW TAIWAN DOLLAR TWD",
"934": "NEW MANAT TMT",
"936": "GHANA CEDI GHS",
"941": "DINAR SERBIO RSD",
"943": "MOZAMBIQUE METICAL MZN",
"944": "AZERBAIJANIAN MANAT AZN",
"946": "NEW LEU RON",
"949": "TURKISH LIRA TRY",
"950": "CFA FRANC BEAC XAF",
"951": "EAST CARIBBEAN DOLLA XCD",
"952": "CFA FRANC BCEAO XOF",
"953": "CFP FRANC XPF",
"954": "E.C.U. EUROPEAN CUR XEU",
"967": "KWACHA ZMW",
"968": "SURINAME DOLLAR SRD",
"969": "ARIARY MGA",
"971": "AFGHANISTAN AFGHANI AFN",
"972": "TAJIKISTAN SOMONI TJS",
"973": "KWANZA ANGOLA AOA",
"974": "BELARUSSIAN RUBLE BYR",
"975": "NEW LEV BGN",
"976": "FRANCO DEL CONGO CDF",
"977": "BOSNIAN MARKA BAM",
"978": "EURO EUR",
"980": "HRYVNIA UAH",
"981": "GEORGIAN LARI GEL",
"985": "NEW POLISH ZLOTY PLN",
"986": "BRAZILIAN REAL BRL",
"991": "RAND FINANCIER ZAL",
}
REVERSE_CURRENCY_CODES = {v: k for k, v in CURRENCY_CODES.items()}
ERROR_CODES = {
"10000": _("Error no tipificado"),
"0900": _("Transacción autorizada para devoluciones y confirmaciones"),
"0400": _("Transacción autorizada para anulaciones"),
"0101": _("Tarjeta caducada"),
"0102": _("Tarjeta en excepción transitoria o bajo sospecha de fraude"),
"0106": _("Intentos de PIN excedidos"),
"0125": _("Tarjeta no efectiva"),
"0129": _("Código de seguridad (CVV2/CVC2) incorrecto"),
"172": _("Denegada, no repetir."),
"173": _("Denegada, no repetir sin actualizar datos de tarjeta."),
"174": _("Denegada, no repetir antes de 72 horas."),
"0180": _("Tarjeta ajena al servicio"),
"0184": _("Error en la autenticación del titular"),
"0190": _("Denegación del emisor sin especificar motivo"),
"0191": _("Fecha de caducidad errónea"),
"0195": _("Requiere autenticación SCA"),
"0202": _(
"Tarjeta en excepción transitoria o bajo sospecha de fraude con retirada de tarjeta"
),
"0904": _("Comercio no registrado en FUC"),
"0909": _("Error de sistema"),
"0913": _("Pedido repetido"),
"0944": _("Sesión Incorrecta"),
"0950": _("Operación de devolución no permitida"),
"9912": _("Emisor no disponible"),
"0912": _("Emisor no disponible"),
"9064": _("Número de posiciones de la tarjeta incorrecto"),
"9078": _("Tipo de operación no permitida para esa tarjeta"),
"9093": _("Tarjeta no existente"),
"9094": _("Rechazo servidores internacionales"),
"9104": _("Comercio con titular seguro y titular sin clave de compra segura"),
"9218": _("El comercio no permite op. seguras por entrada /operaciones"),
"9253": _("Tarjeta no cumple el check-digit"),
"9256": _("El comercio no puede realizar preautorizaciones"),
"9257": _("Esta tarjeta no permite operativa de preautorizaciones"),
"9261": _(
"Operación detenida por superar el control de restricciones en la entrada al SIS"
),
"9915": _("A petición del usuario se ha cancelado el pago"),
"9997": _("Se está procesando otra transacción en SIS con la misma tarjeta"),
"9998": _("Operación en proceso de solicitud de datos de tarjeta"),
"9999": _("Operación que ha sido redirigida al emisor a autenticar"),
}
-3
View File
@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.
@@ -3,43 +3,85 @@ import json
from decimal import Decimal
from django.conf import settings
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils import timezone
from rest_framework import status
from rest_framework.test import APITestCase
from tpv.models import PaymentTransaction
from tpv.redsys import RedsysClient
from tpv.settings import ERROR_CODES
from tpv.utils import validate_expiry_date
from shop.models import CustomerAddress, Order, Tax
from shop.redsys import RedsysClient
from shop.tests.mixins import CreateProductsMixin
from shop.utils import create_order, create_order_line_for_product, validate_expiry_date
User = get_user_model()
class TestRedsysTPV(APITestCase):
def test_redsys_client(self):
amount_to_pay = Decimal("10.00")
transaction = PaymentTransaction.objects.create(amount=amount_to_pay)
items = [
{
"id": 1,
"name": "Papacolas",
"description": "Las mejores papacolas",
"type": "INV",
"amount_to_pay": str(amount_to_pay),
}
]
transaction.metadata["items"] = items
transaction.save()
client = RedsysClient()
merchant_parameters = client._get_merchant_parameters_for_transaction(
transaction
class TestRedsysTPV(APITestCase, CreateProductsMixin):
def setUp(self):
self.tax = Tax.objects.create(
code="IVA",
value=21,
)
self.customer = get_user_model().objects.create_user(
username="11111111H",
first_name="Darth",
last_name="Maull",
email="darth@maul.com",
password="dathomir",
)
assert merchant_parameters.get("DS_MERCHANT_ORDER") == transaction.hash.hex
self.customer_shipping_address = CustomerAddress.objects.create(
user=self.customer,
address="Dathomir",
address_town="Dathomir",
address_zip="00001",
address_state="Dathomir",
address_phone="900000000",
address_type=CustomerAddress.Types.SHIPPING,
)
self.customer_billing_address = CustomerAddress.objects.create(
user=self.customer,
address="Dathomir",
address_town="Dathomir",
address_zip="00001",
address_state="Dathomir",
address_phone="900000000",
address_type=CustomerAddress.Types.BILLING,
)
self.product = self.create_product()
self.order = self.create_order()
self.order.calculate_total_from_lines()
def create_order(self):
order = create_order(
customer=self.customer,
billing_address=self.customer_billing_address.address,
billing_city=self.customer_billing_address.address_town,
billing_state=self.customer_billing_address.address_state,
billing_zip=self.customer_billing_address.address_zip,
billing_country=self.customer_billing_address.address_country,
)
l1 = create_order_line_for_product(
self.product,
quantity=Decimal("1.0"),
order=order,
)
return order
def test_redsys_client(self):
amount_to_pay = Decimal("12.10")
client = RedsysClient()
merchant_parameters = client._get_merchant_parameters_for_order(
self.order,
)
assert merchant_parameters.get("DS_MERCHANT_ORDER") == self.order.code
assert merchant_parameters.get("DS_MERCHANT_AMOUNT") == str(
int(amount_to_pay) * 100
int(amount_to_pay * 100)
)
assert (
merchant_parameters.get("DS_MERCHANT_TERMINAL") == settings.REDSYS_TERMINAL
@@ -49,34 +91,14 @@ class TestRedsysTPV(APITestCase):
== settings.REDSYS_MERCHANT_CODE
)
client.get_body_for_transaction(transaction)
client.get_body_for_order(self.order)
def test_redsys_webhook(self):
amount_to_pay = Decimal("10.00")
transaction = PaymentTransaction.objects.create(amount=amount_to_pay)
items = [
{
"id": 1,
"name": "Papacolas",
"description": "Las mejores papacolas",
"type": "INV",
"amount_to_pay": str(amount_to_pay),
}
]
transaction.metadata["items"] = items
transaction.save()
response = self.client.get(
reverse("tpv:order_created", kwargs={"transaction": transaction.hash})
)
assert response.status_code == status.HTTP_200_OK
redsys_response_data = {
"Ds_MerchantCode": "999008881",
"Ds_Terminal": "001",
"Ds_Order": transaction.hash.hex,
"Ds_Amount": str(transaction.amount_integer),
"Ds_Order": self.order.code,
"Ds_Amount": str(self.order.total * 100),
"Ds_Currency": "978",
"Ds_Date": "01/01/2024",
"Ds_Hour": "00:00",
@@ -99,48 +121,22 @@ class TestRedsysTPV(APITestCase):
).decode()
response = self.client.post(
reverse("tpv:webhook", kwargs={"transaction": transaction.hash}),
reverse("shop:webhook", kwargs={"uuid": self.order.uuid}),
data={
"Ds_MerchantParameters": b64_merchant_params,
"Ds_Signature": settings.REDSYS_SHARED_SECRET,
"Ds_SignatureVersion": "HMAC_SHA256_V1",
},
)
assert response.status_code == status.HTTP_200_OK
response = self.client.get(
reverse("tpv:ok", kwargs={"transaction": transaction.hash})
)
self.order.refresh_from_db()
assert response.status_code == status.HTTP_200_OK
assert self.order.status == Order.Statuses.STATUS_PAID
def test_redsys_webhook_payment_error(self):
amount_to_pay = Decimal("10.00")
transaction = PaymentTransaction.objects.create(amount=amount_to_pay)
items = [
{
"id": 1,
"name": "Papacolas",
"description": "Las mejores papacolas",
"type": "INV",
"amount_to_pay": str(amount_to_pay),
}
]
transaction.metadata["items"] = items
transaction.save()
response = self.client.get(
reverse("tpv:order_created", kwargs={"transaction": transaction.hash})
)
assert response.status_code == status.HTTP_200_OK
redsys_response_data = {
"Ds_MerchantCode": "999008881",
"Ds_Terminal": "001",
"Ds_Order": transaction.hash.hex,
"Ds_Order": self.order.code,
"Ds_Amount": "1000",
"Ds_Currency": "978",
"Ds_Date": "01/01/2024",
@@ -156,7 +152,7 @@ class TestRedsysTPV(APITestCase):
"Ds_Card_Brand": "1",
"Ds_ProcessedPayMethod": "80",
"Ds_ECI": "05",
"Ds_Response_Description": "OPERACION AUTORIZADA",
"Ds_Response_Description": "ERROR",
}
redsys_response_data_str = json.dumps(redsys_response_data)
@@ -165,7 +161,7 @@ class TestRedsysTPV(APITestCase):
).decode()
response = self.client.post(
reverse("tpv:webhook", kwargs={"transaction": transaction.hash}),
reverse("shop:webhook", kwargs={"uuid": self.order.uuid}),
data={
"Ds_MerchantParameters": b64_merchant_params,
"Ds_Signature": settings.REDSYS_SHARED_SECRET,
@@ -174,14 +170,9 @@ class TestRedsysTPV(APITestCase):
)
assert response.status_code == status.HTTP_409_CONFLICT
transaction.refresh_from_db()
assert str(ERROR_CODES.get("0184")) in transaction.observations
assert transaction.status == PaymentTransaction.StatusChoices.ERROR
self.order.refresh_from_db()
response = self.client.get(
reverse("tpv:ko", kwargs={"transaction": transaction.hash})
)
assert response.status_code == status.HTTP_200_OK
assert self.order.status == Order.Statuses.STATUS_ERROR
def test_expiry_date(self):
now = timezone.now()
+5 -1
View File
@@ -1,6 +1,10 @@
from django.urls import path
from shop.views import webhook
app_name = "shop"
urlpatterns = []
urlpatterns = [
path("order/<str:uuid>/webhook/", webhook, name="webhook"),
]
+125
View File
@@ -1,17 +1,28 @@
import base64
import hashlib
import hmac
import json
from decimal import Decimal
import pyDes
from django.contrib.auth import get_user_model
from django.db import transaction
from django.utils import timezone
from django.utils.text import gettext_lazy as _
from shop.exceptions import RedsysPaymentException, RedsysValidationException
from shop.models import (
Cart,
CustomerAddress,
Order,
OrderLine,
Payment,
Product,
ProductBatch,
ProductPrice,
ShippingMethod,
)
from shop.settings import ERROR_CODES
User = get_user_model()
@@ -148,3 +159,117 @@ def add_shipping_order_line(order, shipping_method):
total=shipping_total,
)
order.calculate_total_from_lines()
def compute_signature(salt, payload, key):
"""
:param salt: order number (Ds_Order or Ds_Merchant_Order)
:param payload: Ds_MerchantParameters
:param key: shared secret (aka key) from the Redsys Administration Module
:return:
"""
b64_key = base64.b64decode(key)
des3 = pyDes.triple_des(
b64_key, mode=pyDes.CBC, IV="\0" * 8, pad="\0", padmode=pyDes.PAD_NORMAL
)
pepper = des3.encrypt(str(salt))
payload_hash = hmac.new(pepper, payload.encode(), hashlib.sha256).digest()
return base64.b64encode(payload_hash)
def validate_payment_for_order(request, order: Order) -> Decimal:
"""
example_response_data = {
'Ds_MerchantCode': '999008881',
'Ds_Terminal': '001',
'Ds_Order': 'a082bd1cda0b488786f23a62e89107c0',
'Ds_Amount': '13532',
'Ds_Currency': '978',
'Ds_Date': '20/03/2024',
'Ds_Hour': '12:21',
'Ds_SecurePayment': '1',
'Ds_Card_Number': '454881******1156',
'Ds_Card_Country': '724',
'Ds_Response': '0000',
'Ds_MerchantData': '',
'Ds_TransactionType': '0',
'Ds_ConsumerLanguage': '1',
'Ds_AuthorisationCode': '182670',
'Ds_Card_Brand': '1',
'Ds_ProcessedPayMethod': '80',
'Ds_ECI': '05',
'Ds_Response_Description': 'OPERACION AUTORIZADA'
}
Raises AssertionError
"""
data = request.POST
merchant_parameters = data.get("Ds_MerchantParameters")
if not merchant_parameters:
raise RedsysValidationException(
_("No se ha recibido ningún valor para Ds_MerchantParameters")
)
merchant_params = decode_b64_string(merchant_parameters)
result = json.loads(merchant_params)
order_code = result.get("Ds_Order")
assert order_code == order.code
status_code = result.get("Ds_Response")
if int(status_code) > 100:
reason = ERROR_CODES.get(status_code, _("Error no tipificado"))
raise RedsysPaymentException(_(f"No se ha realizado el pago. Motivo: {reason}"))
amount = Decimal(result.get("Ds_Amount")) / 100
return amount
def decode_b64_string(value: str):
return base64.b64decode(value).decode()
def decode_b64_dict(value: str):
return json.loads(decode_b64_string(value))
def validate_expiry_date(expiry_date: str):
if len(expiry_date) != 4:
return False
month, year = int(expiry_date[2:4]), int(expiry_date[:2])
if month < 1 or month > 12:
return False
current_year = int(str(timezone.now().year)[2:])
current_month = timezone.now().month
if year < current_year:
return False
if year == current_year and month > current_month:
return False
return True
def pay_order(order: Order, amount_paid: Decimal):
with transaction.atomic() as tx:
payment = Payment.objects.create(
amount=amount_paid,
order=order,
user=order.user,
method=Payment.MethodChoices.REDSYS,
)
if amount_paid >= order.total:
order.status = Order.Statuses.STATUS_PAID
order.save()
+22
View File
@@ -0,0 +1,22 @@
from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.csrf import csrf_exempt
from shop.models import Order
from shop.utils import validate_payment_for_order, pay_order
@csrf_exempt
def webhook(request, uuid):
order = get_object_or_404(Order, uuid=uuid)
try:
amount_paid = validate_payment_for_order(request, order)
pay_order(order, amount_paid)
return HttpResponse(status=200)
except Exception as e:
order.status = Order.Statuses.STATUS_ERROR
order.save()
return HttpResponse(status=409)
View File
-13
View File
@@ -1,13 +0,0 @@
from django.contrib import admin
from unfold.admin import ModelAdmin
from tpv.models import PaymentTransaction
# Register your models here.
@admin.register(PaymentTransaction)
class PaymentTPVAdmin(ModelAdmin):
list_display = (
"hash",
"amount",
)
View File
View File
View File
View File
View File
-6
View File
@@ -1,6 +0,0 @@
from django.apps import AppConfig
class TpvConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "tpv"
-14
View File
@@ -1,14 +0,0 @@
from django import forms
from django.utils.text import gettext_lazy as _
class UpdateEmailForm(forms.Form):
contact_email = forms.EmailField(
label=_("Introduce tu e-mail"),
required=True,
widget=forms.EmailInput(
attrs={
"class": "peer bg-green-50 border border-green-500 text-green-900 light:text-green-400 placeholder-green-700 light:placeholder-green-500 text-sm rounded-lg focus:ring-green-500 focus:border-green-500 block w-full p-2.5 light:bg-gray-700 light:border-green-500"
}
),
)
-101
View File
@@ -1,101 +0,0 @@
# Generated by Django 5.0.6 on 2024-05-21 17:19
import uuid
from decimal import Decimal
from django.db import migrations, models
import tpv.models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="PaymentTransaction",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"hash",
models.UUIDField(
db_index=True, default=uuid.uuid4, verbose_name="Hash"
),
),
(
"status",
models.CharField(
choices=[
("PAI", "Pagado"),
("PEN", "Pendiente"),
("ERR", "Error"),
],
default="PEN",
max_length=3,
verbose_name="Estado",
),
),
(
"amount",
models.DecimalField(
decimal_places=2,
default=Decimal("0"),
max_digits=8,
verbose_name="Cantidad a pagar",
),
),
(
"creation_date",
models.DateTimeField(
auto_now_add=True, verbose_name="Fecha de creación"
),
),
(
"last_modification_date",
models.DateTimeField(
auto_now=True, verbose_name="Fecha de última modificación"
),
),
(
"contact_email",
models.EmailField(
blank=True,
max_length=254,
null=True,
verbose_name="Email de contacto",
),
),
(
"observations",
models.CharField(
blank=True,
default="",
max_length=255,
null=True,
verbose_name="Observaciones",
),
),
(
"metadata",
models.JSONField(
default=tpv.models.set_default_metadata, verbose_name="Metadata"
),
),
],
options={
"verbose_name": "Pago de pedido en Redsys",
"verbose_name_plural": "Pagos de pedidos en Redsys",
},
),
]
View File
-72
View File
@@ -1,72 +0,0 @@
from decimal import Decimal
from uuid import uuid4
from django.db import models
from django.utils.text import gettext_lazy as _
def set_default_metadata():
return {"items": [], "response": None}
class PaymentTransaction(models.Model):
class StatusChoices(models.TextChoices):
PAID = "PAI", _("Pagado")
PENDING = "PEN", _("Pendiente")
ERROR = "ERR", _("Error")
hash = models.UUIDField(verbose_name=_("Hash"), default=uuid4, db_index=True)
status = models.CharField(
max_length=3,
default=StatusChoices.PENDING,
choices=StatusChoices.choices,
verbose_name=_("Estado"),
)
amount = models.DecimalField(
max_digits=8,
decimal_places=2,
default=Decimal("0"),
verbose_name=_("Cantidad a pagar"),
)
creation_date = models.DateTimeField(
auto_now_add=True, verbose_name=_("Fecha de creación")
)
last_modification_date = models.DateTimeField(
auto_now=True, verbose_name=_("Fecha de última modificación")
)
contact_email = models.EmailField(
blank=True, null=True, verbose_name=_("Email de contacto")
)
observations = models.CharField(
max_length=255,
default="",
blank=True,
null=True,
verbose_name=_("Observaciones"),
)
metadata = models.JSONField(
default=set_default_metadata, verbose_name=_("Metadata")
)
"""
Metadata example:
{
"items": [{
"id": 1000,
"type": "type",
"amount_to_pay": 10.00,
"amount_to_pay_int": 1000,
}],
}
"""
@property
def amount_integer(self) -> int:
return int(self.amount * 100)
class Meta:
verbose_name = _("Pago de pedido en Redsys")
verbose_name_plural = _("Pagos de pedidos en Redsys")
-272
View File
@@ -1,272 +0,0 @@
from django.utils.text import gettext_lazy as _
class TransactionTypes:
AUTHORIZATION = 0
PREAUTHORIZATION = 1
REPLACEMENT_PREAUTHORIZATION = 11
CONFIRMATION = 2
REFUND = 3
SEPARATED_CONFIRMATION = 8
CANCELED = 9
PAYGOLD = 15
PUCE_AUTHENTICATION = 17
REFUND_WITHOUT_ORIGINAL = 34
BET_PRICE = 37
PAYMENT_CANCELED = 45
REFUND_CANCELED = 46
SEPARATED_CONFIRMATION_CANCELED = 47
LINK_EXPIRATION_CHANGED_FOR_PAYGOLD = 51
CURRENCY_CODES = {
8: "LEK ALL",
12: "ALGERIAN DINAR DZD",
24: "ANGOLA KWANZA AOK",
30: "PROBANDO DESA MON",
31: "AZERBAIJANIAN MANAT AZM",
32: "ARGENTINE AUSTRAL ARP",
36: "AUSTRALIAN DOLLAR AUD",
44: "BAHAMIAN DOLLAR BSD",
48: "BAHRAINI DINAR BHD",
50: "TAKA BDT",
51: "ARMENIAN DRAM AMD",
52: "BARBADOS DOLLAR BBD",
60: "BERMUDAN DOLLAR BMD",
64: "NGULTRUM BTN",
68: "BOLIVIAN PESO BOP",
70: "DINAR BAD",
72: "PULA BWP",
76: "CRUZEIRO BRC",
84: "BELIZE DOLLAR 084",
90: "SOLOMON ISLANDS DOLL SBD",
96: "BRUNEI DOLLAR BND",
100: "LEV BGL",
104: "KYAT BUK",
108: "BURUNDI FRANC BIF",
112: "BELARUSSIAN RUBLE BYB",
116: "RIEL KHR",
124: "CANADIAN DOLLAR CAD",
132: "CAPE VERDE ESCUDO CVE",
136: "CAYMAN ISLANDS DOLLA KYD",
144: "SRI LANKA RUPEE LKR",
152: "CHILEAN PESO CLP",
156: "YUAN RENMINBI CNY",
157: "CHINESE RENMIMBI CNH",
158: "CHINESE RENMINBI CNX",
170: "COLOMBIAN PESO COP",
174: "COMOROS FRANC KMF",
180: "ZAIRE ZRZ",
188: "COSTA RICA COLON CRC",
191: "CROATIAN KUNA HRK",
192: "CUBAN PESO CUP",
196: "CYPRUS POUND CYP",
200: "KORUNA CSK",
203: "CZECH KORUNA CZK",
208: "DANISH KRONE DKK",
214: "DOMINICAN PESO DOP",
218: "SUCRE ECS",
222: "EL SALVADOR COLON SVC",
226: "EKWELE GQE",
230: "ETHIOPIAN BIRR ETB",
232: "ERITREAN NAKTAN ERN",
233: "ESTONIAN KROON EEK",
238: "FALKLAND ISLANDS FKP",
242: "FIJI DOLLAR FJD",
262: "DJIBOUTI FRANC DJF",
268: "GEORGIAN LARI GEL",
270: "DALASI GMD",
278: "MARK DER DDR DDM",
288: "GHANA CEDI GHC",
292: "GIBRALTAR POUND GIP",
320: "QUETZAL GTQ",
324: "SYLI GNS",
328: "GUYANA DOLLAR GYD",
332: "GOURDE HTG",
340: "LEMPIRA HNL",
344: "HONG KONG DOLLAR HKD",
348: "FORINT HUF",
352: "ICELAND KRONA ISK",
356: "INDIAN RUPEE INR",
360: "RUPIAH IDR",
364: "IRANIAL RIAL IRR",
365: "IRANIAN AIRLINE RATE IRA",
368: "IRAQI DINAR IQD",
376: "ISRAEL SHEKEL ILS",
388: "JAMAICAN DOLLAR JMD",
392: "YEN JPY",
398: "TENGE KZT",
400: "JORDANIAN DINAR JOD",
404: "KENYAN SHILLING KES",
408: "NORTH KOREAN WON KPW",
410: "KOREAN WON KRW",
414: "KUWAITI DINAR KWD",
417: "KYRGYZSTAN SON KGS",
418: "KIP LAK",
422: "LEBANESE POUND LBP",
426: "LESOTHO LOTI LSM",
428: "LATVIAN LAT LVL",
430: "LIBERIAN DOLLAR LRD",
434: "LIBYAN DINAR LYD",
440: "LITHUANIAN LITAS LTL",
446: "PATACA MOP",
450: "MALAGASY FRANC MGF",
454: "MALAWI KWACHA MWK",
458: "MALASYAN RINGGIT MYR",
462: "MALDIVE RUPEE MVR",
466: "MALI MLF",
470: "MALTESE LIRA MTL",
478: "OUGUIYA MRO",
480: "MAURITIUS RUPEE MUR",
484: "MEXICAN PESO MXP",
496: "TUGRIK MNT",
498: "MOLDOVIAN LEU MDL",
504: "MORROCAN DIRHAM MAD",
508: "METICAL MZM",
512: "RIAL OMANI OMR",
516: "NAMIBIAN DOLLAR NAD",
524: "NEPALESE RUPEE NPR",
532: "NETHERLANDS ANTILLIA ANG",
533: "ARUBA AWG",
536: "YUGOSLAVIAN NEW DIAN NTZ",
548: "VANUATU VATU VUV",
554: "NEW ZEALAND DOLLAR NZD",
556: "NAIRA 566",
558: "CORDOBA NIC",
566: "NAIRA NGN",
578: "NORWEGIAN KRONE NOK",
582: "PACIFIC ISLAND PCI",
586: "PAKISTAN RUPEE PKR",
590: "BALBOA PAB",
598: "KINA PGK",
600: "GUARANI PYG",
604: "PERU INTI PEI",
608: "PHILIPPINE PESO PHP",
616: "ZLOTY PLZ",
624: "GUINEA",
626: "TIMOR ESCUDO TPE",
634: "QATARI RIAL QAR",
642: "LEU ROL",
643: "RUSSIAN ROUBLE RUB",
646: "RWANDA FRANC RWF",
654: "ST.HELENA POUND SHP",
678: "DOBRA STD",
682: "SAUDI RIYAL SAR",
690: "SEYCHELLES RUPEE SCR",
694: "LEONE SLL",
702: "SINGAPORE DOLLAR SGD",
703: "SLOVAK KORUNA SKK",
704: "DONG VND",
705: "SLOVENIAN TOLAR SIT",
706: "SOMALI SHILLING SOS",
710: "RAND ZAR",
716: "ZIMBABWE DOLLAR ZWD",
720: "YEMENI DINAR YDD",
728: "SOUTH SUDANESE POUND SSP",
736: "SUDANESE POUND SDP",
737: "SUDAN AIRLINES SDA",
740: "SURINAM GUILDER SRG",
748: "LILANGENI SZL",
752: "SWEDISH KRONA SEK",
756: "SWISS FRANC CHF",
760: "SYRIAN POUND SYP",
762: "TAJIK RUBLE TJR",
764: "BAHT THB",
776: "PA'ANGA TOP",
780: "TRINIDAD Y TOBAGO DO TTD",
784: "UAE DIRHAM AED",
788: "TUNISIAN DINAR TND",
792: "TURKISH LIRA TRL",
793: "PSEUDO TURKISH LIRA PTL",
795: "MANAT TMM",
800: "UGANDA SHILLING UGS",
804: "KARBOVANET UAK",
807: "MACEDONIAN DENAR MKD",
810: "RUSSIAN ROUBLE RUR",
818: "EGYPTIAN POUND EGP",
826: "POUND STERLING GBP",
834: "TANZANIAN SHILLING TZS",
840: "DOLAR U.S.A. USD",
858: "URUGUAYAN PESO UYP",
860: "UZBEKISTAN SUM UZS",
862: "BOLIVAR VEB",
882: "TALA WST",
886: "YEMINI RIAL YER",
890: "NEW YUGOSLAVIAN DOLL YUD",
891: "NEW DINAR YUG",
894: "KWACHA ZMK",
901: "NEW TAIWAN DOLLAR TWD",
934: "NEW MANAT TMT",
936: "GHANA CEDI GHS",
941: "DINAR SERBIO RSD",
943: "MOZAMBIQUE METICAL MZN",
944: "AZERBAIJANIAN MANAT AZN",
946: "NEW LEU RON",
949: "TURKISH LIRA TRY",
950: "CFA FRANC BEAC XAF",
951: "EAST CARIBBEAN DOLLA XCD",
952: "CFA FRANC BCEAO XOF",
953: "CFP FRANC XPF",
954: "E.C.U. EUROPEAN CUR XEU",
967: "KWACHA ZMW",
968: "SURINAME DOLLAR SRD",
969: "ARIARY MGA",
971: "AFGHANISTAN AFGHANI AFN",
972: "TAJIKISTAN SOMONI TJS",
973: "KWANZA ANGOLA AOA",
974: "BELARUSSIAN RUBLE BYR",
975: "NEW LEV BGN",
976: "FRANCO DEL CONGO CDF",
977: "BOSNIAN MARKA BAM",
978: "EURO EUR",
980: "HRYVNIA UAH",
981: "GEORGIAN LARI GEL",
985: "NEW POLISH ZLOTY PLN",
986: "BRAZILIAN REAL BRL",
991: "RAND FINANCIER ZAL",
}
ERROR_CODES = {
"10000": _("Error no tipificado"),
"0900": _("Transacción autorizada para devoluciones y confirmaciones"),
"0400": _("Transacción autorizada para anulaciones"),
"0101": _("Tarjeta caducada"),
"0102": _("Tarjeta en excepción transitoria o bajo sospecha de fraude"),
"0106": _("Intentos de PIN excedidos"),
"0125": _("Tarjeta no efectiva"),
"0129": _("Código de seguridad (CVV2/CVC2) incorrecto"),
"172": _("Denegada, no repetir."),
"173": _("Denegada, no repetir sin actualizar datos de tarjeta."),
"174": _("Denegada, no repetir antes de 72 horas."),
"0180": _("Tarjeta ajena al servicio"),
"0184": _("Error en la autenticación del titular"),
"0190": _("Denegación del emisor sin especificar motivo"),
"0191": _("Fecha de caducidad errónea"),
"0195": _("Requiere autenticación SCA"),
"0202": _(
"Tarjeta en excepción transitoria o bajo sospecha de fraude con retirada de tarjeta"
),
"0904": _("Comercio no registrado en FUC"),
"0909": _("Error de sistema"),
"0913": _("Pedido repetido"),
"0944": _("Sesión Incorrecta"),
"0950": _("Operación de devolución no permitida"),
"9912": _("Emisor no disponible"),
"0912": _("Emisor no disponible"),
"9064": _("Número de posiciones de la tarjeta incorrecto"),
"9078": _("Tipo de operación no permitida para esa tarjeta"),
"9093": _("Tarjeta no existente"),
"9094": _("Rechazo servidores internacionales"),
"9104": _("Comercio con titular seguro y titular sin clave de compra segura"),
"9218": _("El comercio no permite op. seguras por entrada /operaciones"),
"9253": _("Tarjeta no cumple el check-digit"),
"9256": _("El comercio no puede realizar preautorizaciones"),
"9257": _("Esta tarjeta no permite operativa de preautorizaciones"),
"9261": _(
"Operación detenida por superar el control de restricciones en la entrada al SIS"
),
"9915": _("A petición del usuario se ha cancelado el pago"),
"9997": _("Se está procesando otra transacción en SIS con la misma tarjeta"),
"9998": _("Operación en proceso de solicitud de datos de tarjeta"),
"9999": _("Operación que ha sido redirigida al emisor a autenticar"),
}
-4
View File
@@ -1,4 +0,0 @@
from django.dispatch import Signal
redsys_payment_accepted = Signal()
redsys_payment_rejected = Signal()
-38
View File
@@ -1,38 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Pago de facturas</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {}
</script>
<style>
@layer utilities {
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
.top-bar {
background: linear-gradient(50deg, #02B0C4 0%, #A0D720 100%);
padding: 32px;
color: white;
display: flex;
justify-content: center;
}
</style>
</head>
<body>
{% include 'tpv/header.html' %}
{% block body %}
{% endblock %}
</body>
</html>
-15
View File
@@ -1,15 +0,0 @@
<div class="justify-between mb-6 rounded-lg bg-white p-6 shadow-md sm:flex sm:justify-start">
<div class="sm:ml-4 sm:flex sm:w-full sm:justify-between">
<div class="mt-5 sm:mt-0">
<h2 class="text-lg font-bold text-gray-900">Factura #{{ item.id }}</h2>
<p class="mt-1 text-xs text-gray-700">
{{ item.name }} - {{ item.description }}
</p>
</div>
<div class="mt-4 flex justify-between sm:space-y-6 sm:mt-0 sm:block sm:space-x-6">
<div class="flex items-center border-gray-100">
{{ item.amount_to_pay }} €
</div>
</div>
</div>
</div>
-20
View File
@@ -1,20 +0,0 @@
<div>
{% if order.status == 'PEN' %}
<form name="from" action="{{ redsys_target_url }}" method="POST">
{% csrf_token %}
<input type="hidden" name="Ds_SignatureVersion" value="{{ signature_version }}"/>
<input type="hidden" name="Ds_MerchantParameters" value="{{ merchant_parameters }}"/>
<input type="hidden" name="Ds_Signature" value="{{ signature }}"/>
{% if order.contact_email != '' %}
<input
class="mt-6 w-full rounded-md bg-primary-500 py-1.5 font-medium text-primary-50 hover:bg-primary-600 cursor-pointer"
type="submit" value="Ir a pagar">
{% else %}
<input
class="mt-6 w-full rounded-md bg-primary-500 py-1.5 font-medium text-primary-50 hover:bg-primary-600 cursor-pointer"
type="submit" value="Ir a pagar" disabled>
{% endif %}
</form>
{% endif %}
</div>
-3
View File
@@ -1,3 +0,0 @@
<header class="top-bar">
<img src="" alt="">
</header>
-102
View File
@@ -1,102 +0,0 @@
{% extends 'tpv/base.html' %}
{% block body %}
<div class="h-screen bg-gray-100 pt-10">
<header class="mb-10 text-center">
<h1 class="text-2xl font-bold p-4">Resumen de pedido</h1>
{% if order.status == 'PAI' %}
<span
class="bg-green-100 text-green-800 text-large font-large me-2 px-2.5 py-0.5 rounded-full dark:bg-green-400 dark:text-green-900">Pagado</span>
{% elif order.status == 'PEN' %}
<span
class="bg-yellow-100 text-yellow-800 text-large font-large me-2 px-2.5 py-0.5 rounded-full dark:bg-yellow-400 dark:text-yellow-900">Pendiente de pago</span>
{% else %}
<span
class="bg-red-100 text-red-800 text-large font-large me-2 px-2.5 py-0.5 rounded-full dark:bg-red-400 dark:text-red-900">Error de pago</span>
{% endif %}
</header>
<div class="mx-auto max-w-5xl justify-center px-6 md:flex md:space-x-6 xl:px-0">
<div class="rounded-lg md:w-2/3">
{% for item in order.metadata.items %}
{% include 'tpv/cart_item.html' %}
{% endfor %}
</div>
<div class=" md:w-1/3">
{% if order.status == 'PEN' %}
<!-- Formulario de email -->
<section class="mt-6 rounded-lg border bg-white shadow-md md:mt-0 mb-4">
<div class="flex flex-col items-center justify-center mx-auto">
<div
class="w-full rounded-lg md:mt-0 sm:max-w-md xl:p-0">
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
<form class="space-y-4 md:space-y-6" action="{{ action }}" method="POST">
{% csrf_token %}
{% for field in form %}
<label class="font-bold" for="{{ field.auto_id }}">{{ field.label }}</label>
{{ field }}
{% endfor %}
<button type="submit" class="mt-6 w-full rounded-md bg-blue-500 py-1.5 font-medium text-blue-50 hover:bg-blue-600 cursor-pointer">
Enviar
</button>
</form>
</div>
</div>
</div>
</section>
{% endif %}
<section class="mt-6 rounded-lg border bg-white p-6 shadow-md md:mt-0">
<div class="mb-2 flex justify-between">
<p class="text-gray-700">Subtotal</p>
<p class="text-gray-700">{{ order.amount }} €</p>
</div>
<hr class="my-4"/>
<div class="flex justify-between">
<p class="text-lg font-bold">Total</p>
<div class="">
<p class="mb-1 text-lg font-bold">{{ order.amount }} €</p>
<p class="text-sm text-gray-700">Impuestos incluidos</p>
</div>
</div>
{% if order.status == 'PEN' %}
<form name="from" action="{{ redsys_target_url }}" method="POST">
{% csrf_token %}
<input type="hidden" name="Ds_SignatureVersion" value="{{ signature_version }}"/>
<input type="hidden" name="Ds_MerchantParameters" value="{{ merchant_parameters }}"/>
<input type="hidden" name="Ds_Signature" value="{{ signature }}"/>
{% if order.contact_email != '' %}
<input
class="mt-6 w-full rounded-md bg-blue-500 py-1.5 font-medium text-blue-50 hover:bg-blue-600 cursor-pointer"
type="submit" value="Ir a pagar">
{% else %}
<input
class="mt-6 w-full rounded-md bg-blue-500 py-1.5 font-medium text-blue-50 hover:bg-blue-600 cursor-pointer"
type="submit" value="Ir a pagar" disabled>
{% endif %}
</form>
{% endif %}
</section>
</div>
</div>
</div>
{% endblock %}
-3
View File
@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.
View File
-20
View File
@@ -1,20 +0,0 @@
from django.urls import path
from tpv.views import (
payment_accepted,
payment_form,
payment_rejected,
transaction_created,
webhook,
)
app_name = "tpv"
urlpatterns = [
path("transaction/<str:transaction>/", transaction_created, name="order_created"),
path("transaction/<str:transaction>/webhook/", webhook, name="webhook"),
path("transaction/<str:transaction>/ok/", payment_accepted, name="ok"),
path("transaction/<str:transaction>/ko/", payment_rejected, name="ko"),
path("transaction/<str:transaction>/form/", payment_form, name="payment_form"),
]
-133
View File
@@ -1,133 +0,0 @@
import base64
import hashlib
import hmac
import json
import re
from decimal import Decimal
import pyDes
from django.utils import timezone
from django.utils.text import gettext_lazy as _
from tpv.exceptions import RedsysPaymentException, RedsysValidationException
from tpv.models import PaymentTransaction
from tpv.settings import ERROR_CODES
from tpv.signals import redsys_payment_accepted
def compute_signature(salt, payload, key):
"""
:param salt: order number (Ds_Order or Ds_Merchant_Order)
:param payload: Ds_MerchantParameters
:param key: shared secret (aka key) from the Redsys Administration Module
:return:
"""
b64_key = base64.b64decode(key)
des3 = pyDes.triple_des(
b64_key, mode=pyDes.CBC, IV="\0" * 8, pad="\0", padmode=pyDes.PAD_NORMAL
)
pepper = des3.encrypt(str(salt))
payload_hash = hmac.new(pepper, payload.encode(), hashlib.sha256).digest()
return base64.b64encode(payload_hash)
def compare_signatures(signature_1, signature_2):
alphanumeric_characters = re.compile("[^a-zA-Z0-9]")
sig1safe = re.sub(alphanumeric_characters, "", signature_1)
sig2safe = re.sub(alphanumeric_characters, "", signature_2)
return sig1safe == sig2safe
def validate_payment_for_transaction(
request, transaction: PaymentTransaction
) -> Decimal:
"""
example_response_data = {
'Ds_MerchantCode': '999008881',
'Ds_Terminal': '001',
'Ds_Order': 'a082bd1cda0b488786f23a62e89107c0',
'Ds_Amount': '13532',
'Ds_Currency': '978',
'Ds_Date': '20/03/2024',
'Ds_Hour': '12:21',
'Ds_SecurePayment': '1',
'Ds_Card_Number': '454881******1156',
'Ds_Card_Country': '724',
'Ds_Response': '0000',
'Ds_MerchantData': '',
'Ds_TransactionType': '0',
'Ds_ConsumerLanguage': '1',
'Ds_AuthorisationCode': '182670',
'Ds_Card_Brand': '1',
'Ds_ProcessedPayMethod': '80',
'Ds_ECI': '05',
'Ds_Response_Description': 'OPERACION AUTORIZADA'
}
"""
data = request.POST
merchant_parameters = data.get("Ds_MerchantParameters")
if not merchant_parameters:
raise RedsysValidationException(
_("No se ha recibido ningún valor para Ds_MerchantParameters")
)
merchant_params = decode_b64_string(merchant_parameters)
result = json.loads(merchant_params)
transaction_hex = result.get("Ds_Order")
assert transaction_hex == transaction.hash.hex
status_code = result.get("Ds_Response")
if int(status_code) > 100:
reason = ERROR_CODES.get(status_code, _("Error no tipificado"))
raise RedsysPaymentException(_(f"No se ha realizado el pago. Motivo: {reason}"))
amount = Decimal(result.get("Ds_Amount")) / 100
return amount
def validate_expiry_date(expiry_date: str):
if len(expiry_date) != 4:
return False
month, year = int(expiry_date[2:4]), int(expiry_date[:2])
if month < 1 or month > 12:
return False
current_year = int(str(timezone.now().year)[2:])
current_month = timezone.now().month
if year < current_year:
return False
if year == current_year and month > current_month:
return False
return True
def pay_transaction(transaction: PaymentTransaction, amount_paid: Decimal):
transaction.status = PaymentTransaction.StatusChoices.PAID
transaction.save()
redsys_payment_accepted.send_robust(
PaymentTransaction.__class__,
hash=transaction.hash,
amount=amount_paid,
)
def decode_b64_string(value: str):
return base64.b64decode(value).decode()
def decode_b64_dict(value: str):
return json.loads(decode_b64_string(value))
-99
View File
@@ -1,99 +0,0 @@
from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView
from tpv.forms import UpdateEmailForm
from tpv.models import PaymentTransaction
from tpv.redsys import RedsysClient
from tpv.signals import redsys_payment_accepted, redsys_payment_rejected
from tpv.utils import pay_transaction, validate_payment_for_transaction
def payment_accepted(request, transaction):
transaction = get_object_or_404(PaymentTransaction, hash=transaction)
return render(
request,
template_name="tpv/order_created.html",
context={
"order": transaction,
},
)
def payment_rejected(request, transaction):
transaction = get_object_or_404(PaymentTransaction, hash=transaction)
return render(
request,
template_name="tpv/order_created.html",
context={
"order": transaction,
},
)
@csrf_exempt
def webhook(request, transaction):
transaction = get_object_or_404(PaymentTransaction, hash=transaction)
try:
amount_paid = validate_payment_for_transaction(request, transaction)
pay_transaction(transaction, amount_paid)
return HttpResponse(status=200)
except Exception as e:
transaction.status = PaymentTransaction.StatusChoices.ERROR
transaction.observations = str(e)
transaction.save()
redsys_payment_rejected.send_robust(
PaymentTransaction.__class__, hash=transaction.hash
)
return HttpResponse(status=409)
def transaction_created(request, transaction):
transaction = get_object_or_404(PaymentTransaction, hash=transaction)
client = RedsysClient()
parameters = client.get_body_for_transaction(transaction)
return render(
request,
template_name="tpv/order_created.html",
context={
"form": UpdateEmailForm({"contact_email": transaction.contact_email}),
"action": reverse(
"tpv:order_created", kwargs={"transaction": transaction.hash}
),
"order": transaction,
"signature_version": parameters.get("Ds_SignatureVersion"),
"merchant_parameters": parameters.get("Ds_MerchantParameters"),
"signature": parameters.get("Ds_Signature"),
"redsys_target_url": client.get_target_url(),
},
)
class PaymentFormView(TemplateView):
template_name = "tpv/form.html"
def get_context_data(self, transaction):
transaction = get_object_or_404(PaymentTransaction, hash=transaction)
client = RedsysClient()
parameters = client.get_body_for_transaction(transaction)
target_url = client.get_target_url()
return {
"order": transaction,
"signature_version": parameters.get("Ds_SignatureVersion"),
"merchant_parameters": parameters.get("Ds_MerchantParameters"),
"signature": parameters.get("Ds_Signature"),
"redsys_target_url": target_url,
}
payment_form = PaymentFormView.as_view()
@@ -3,8 +3,7 @@
<form action="" hx-post="{% url 'web:delete_from_wishlist' pk=product.pk %}" hx-swap="none">
<input type="hidden" name="product" value="{{ product.pk }}">
{% csrf_token %}
<button
class="flex items-center justify-center rounded-full px-1.5 py-2 text-l font-medium text-gray-900 focus:outline-none bg-white border border-gray-200 hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
<button>
{% include 'components/icons/liked.svg' %}
</button>
</form>
@@ -12,8 +11,7 @@
<form action="" hx-post="{% url 'web:add_to_wishlist' %}" hx-swap="none">
<input type="hidden" name="product" value="{{ product.pk }}">
{% csrf_token %}
<button
class="flex items-center justify-center rounded-full px-1.5 py-2 text-l font-medium text-gray-900 focus:outline-none bg-white border border-gray-200 hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
<button>
{% include 'components/icons/like.svg' %}
</button>
</form>
+3
View File
@@ -1,5 +1,7 @@
{% load i18n %}
{% load static %}
{{ page }}
<ul class="mb-4 grid gap-4 sm:grid-cols-2 md:mb-8 lg:grid-cols-3 xl:grid-cols-4">
{% for product in page %}
<li>
@@ -8,6 +10,7 @@
{% endfor %}
</ul>
{% if has_previous_page %}
<button type="button"
class="rounded-lg border border-gray-200 bg-white px-5 py-2.5 text-sm font-medium text-gray-900
+3 -2
View File
@@ -1,13 +1,14 @@
from django.urls import path
from django.conf.urls.i18n import i18n_patterns
from django.contrib.auth.decorators import login_required
from django.urls import path
from django.utils.text import gettext_lazy as _
from web.views.components import (
cart,
cart_dropdown,
list_products,
wishlist_button,
list_wishlisted_products,
wishlist_button,
)
from web.views.users import login, logout, my_account, register, reset_password
from web.views.web import (
+2 -2
View File
@@ -6,11 +6,11 @@ from django.views.generic import TemplateView
from shop.filters import ProductFilter
from shop.models import (
CartItem,
CustomerAddress,
Product,
ProductPrice,
WishlistedProduct,
CustomerAddress,
ShippingMethod,
WishlistedProduct,
)
from web.mixins import FilteredQuerysetMixin, PaginatedQuerysetMixin
from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME
+5 -5
View File
@@ -2,17 +2,17 @@ from django.http.response import HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect, reverse
from django.utils.text import gettext_lazy as _
from django.views.decorators.http import require_http_methods
from django.views.generic import TemplateView, CreateView
from django.views.generic import CreateView, TemplateView
from shop.models import (
CartItem,
Product,
ProductCategory,
WishlistedProduct,
CustomerAddress,
ShippingMethod,
Order,
OrderLine,
Product,
ProductCategory,
ShippingMethod,
WishlistedProduct,
)
from shop.utils import create_order_from_cart
from web.forms import CreateOrderForm