From a7333255293d1fc178bef58f42ffb517640de525 Mon Sep 17 00:00:00 2001 From: Pablo Moreno Date: Sun, 12 Jan 2025 22:40:46 +0100 Subject: [PATCH] feat: changed TPV behaviour --- config/settings/base.py | 1 - config/urls.py | 2 +- shop/admin.py | 11 + {tpv => shop}/exceptions.py | 0 shop/migrations/0001_initial.py | 5 +- ...der_code_order_shipping_status_and_more.py | 344 ++++++++++++++++++ shop/models.py | 91 +++++ {tpv => shop}/redsys.py | 140 ++++--- shop/settings.py | 274 ++++++++++++++ shop/tests.py | 3 - {tpv => shop}/tests/test_redsys.py | 169 ++++----- shop/urls.py | 6 +- shop/utils.py | 125 +++++++ shop/views.py | 22 ++ tpv/__init__.py | 0 tpv/admin.py | 13 - tpv/api/__init__.py | 0 tpv/api/v1/__init__.py | 0 tpv/api/v1/routers.py | 0 tpv/api/v1/serializers.py | 0 tpv/api/v1/viewsets.py | 0 tpv/apps.py | 6 - tpv/forms.py | 14 - tpv/migrations/0001_initial.py | 101 ----- tpv/migrations/__init__.py | 0 tpv/models.py | 72 ---- tpv/settings.py | 272 -------------- tpv/signals.py | 4 - tpv/templates/tpv/base.html | 38 -- tpv/templates/tpv/cart_item.html | 15 - tpv/templates/tpv/form.html | 20 - tpv/templates/tpv/header.html | 3 - tpv/templates/tpv/order_created.html | 102 ------ tpv/tests.py | 3 - tpv/tests/__init__.py | 0 tpv/urls.py | 20 - tpv/utils.py | 133 ------- tpv/views.py | 99 ----- .../components/products/wishlist_button.html | 6 +- web/templates/web/list_products.html | 3 + web/urls.py | 5 +- web/views/components.py | 4 +- web/views/web.py | 10 +- 43 files changed, 1036 insertions(+), 1100 deletions(-) rename {tpv => shop}/exceptions.py (100%) create mode 100644 shop/migrations/0002_shopsettings_order_code_order_shipping_status_and_more.py rename {tpv => shop}/redsys.py (51%) create mode 100644 shop/settings.py delete mode 100644 shop/tests.py rename {tpv => shop}/tests/test_redsys.py (54%) delete mode 100644 tpv/__init__.py delete mode 100644 tpv/admin.py delete mode 100644 tpv/api/__init__.py delete mode 100644 tpv/api/v1/__init__.py delete mode 100644 tpv/api/v1/routers.py delete mode 100644 tpv/api/v1/serializers.py delete mode 100644 tpv/api/v1/viewsets.py delete mode 100644 tpv/apps.py delete mode 100644 tpv/forms.py delete mode 100644 tpv/migrations/0001_initial.py delete mode 100644 tpv/migrations/__init__.py delete mode 100644 tpv/models.py delete mode 100644 tpv/settings.py delete mode 100644 tpv/signals.py delete mode 100644 tpv/templates/tpv/base.html delete mode 100644 tpv/templates/tpv/cart_item.html delete mode 100644 tpv/templates/tpv/form.html delete mode 100644 tpv/templates/tpv/header.html delete mode 100644 tpv/templates/tpv/order_created.html delete mode 100644 tpv/tests.py delete mode 100644 tpv/tests/__init__.py delete mode 100644 tpv/urls.py delete mode 100644 tpv/utils.py delete mode 100644 tpv/views.py diff --git a/config/settings/base.py b/config/settings/base.py index 7e3c03b..8ff8bbf 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -39,7 +39,6 @@ PROJECT_APPS = [ "files", "shop", "theme", - "tpv", "users", "web", ] diff --git a/config/urls.py b/config/urls.py index 540c741..729307d 100644 --- a/config/urls.py +++ b/config/urls.py @@ -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: diff --git a/shop/admin.py b/shop/admin.py index 9c3d472..3081224 100644 --- a/shop/admin.py +++ b/shop/admin.py @@ -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", + ) diff --git a/tpv/exceptions.py b/shop/exceptions.py similarity index 100% rename from tpv/exceptions.py rename to shop/exceptions.py diff --git a/shop/migrations/0001_initial.py b/shop/migrations/0001_initial.py index 708d53f..43f3d65 100644 --- a/shop/migrations/0001_initial.py +++ b/shop/migrations/0001_initial.py @@ -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 diff --git a/shop/migrations/0002_shopsettings_order_code_order_shipping_status_and_more.py b/shop/migrations/0002_shopsettings_order_code_order_shipping_status_and_more.py new file mode 100644 index 0000000..d171eab --- /dev/null +++ b/shop/migrations/0002_shopsettings_order_code_order_shipping_status_and_more.py @@ -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", + }, + ), + ] diff --git a/shop/models.py b/shop/models.py index 71112a5..16f4550 100644 --- a/shop/models.py +++ b/shop/models.py @@ -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") diff --git a/tpv/redsys.py b/shop/redsys.py similarity index 51% rename from tpv/redsys.py rename to shop/redsys.py index 0db1a4b..0a7da85 100644 --- a/tpv/redsys.py +++ b/shop/redsys.py @@ -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, - pan=pan, - expiry_date=expiry_date, - cvv2=cvv2, - transaction_type=transaction_type, - ) + merchant_parameters = self._get_rest_encoded_merchant_parameters_for_order( + order=order, + pan=pan, + expiry_date=expiry_date, + cvv2=cvv2, + 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", "") diff --git a/shop/settings.py b/shop/settings.py new file mode 100644 index 0000000..31b1b1d --- /dev/null +++ b/shop/settings.py @@ -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"), +} diff --git a/shop/tests.py b/shop/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/shop/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/tpv/tests/test_redsys.py b/shop/tests/test_redsys.py similarity index 54% rename from tpv/tests/test_redsys.py rename to shop/tests/test_redsys.py index 516bd1c..72a1537 100644 --- a/tpv/tests/test_redsys.py +++ b/shop/tests/test_redsys.py @@ -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() diff --git a/shop/urls.py b/shop/urls.py index d73d9c9..4b8a6f8 100644 --- a/shop/urls.py +++ b/shop/urls.py @@ -1,6 +1,10 @@ from django.urls import path +from shop.views import webhook + app_name = "shop" -urlpatterns = [] +urlpatterns = [ + path("order//webhook/", webhook, name="webhook"), +] diff --git a/shop/utils.py b/shop/utils.py index cc88759..3e86b6b 100644 --- a/shop/utils.py +++ b/shop/utils.py @@ -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() diff --git a/shop/views.py b/shop/views.py index e69de29..da9aba0 100644 --- a/shop/views.py +++ b/shop/views.py @@ -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) diff --git a/tpv/__init__.py b/tpv/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tpv/admin.py b/tpv/admin.py deleted file mode 100644 index a531a98..0000000 --- a/tpv/admin.py +++ /dev/null @@ -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", - ) diff --git a/tpv/api/__init__.py b/tpv/api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tpv/api/v1/__init__.py b/tpv/api/v1/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tpv/api/v1/routers.py b/tpv/api/v1/routers.py deleted file mode 100644 index e69de29..0000000 diff --git a/tpv/api/v1/serializers.py b/tpv/api/v1/serializers.py deleted file mode 100644 index e69de29..0000000 diff --git a/tpv/api/v1/viewsets.py b/tpv/api/v1/viewsets.py deleted file mode 100644 index e69de29..0000000 diff --git a/tpv/apps.py b/tpv/apps.py deleted file mode 100644 index f1171a4..0000000 --- a/tpv/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class TpvConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "tpv" diff --git a/tpv/forms.py b/tpv/forms.py deleted file mode 100644 index 0796af9..0000000 --- a/tpv/forms.py +++ /dev/null @@ -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" - } - ), - ) diff --git a/tpv/migrations/0001_initial.py b/tpv/migrations/0001_initial.py deleted file mode 100644 index f8ebca3..0000000 --- a/tpv/migrations/0001_initial.py +++ /dev/null @@ -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", - }, - ), - ] diff --git a/tpv/migrations/__init__.py b/tpv/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tpv/models.py b/tpv/models.py deleted file mode 100644 index 4edb3a7..0000000 --- a/tpv/models.py +++ /dev/null @@ -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") diff --git a/tpv/settings.py b/tpv/settings.py deleted file mode 100644 index b5a2f26..0000000 --- a/tpv/settings.py +++ /dev/null @@ -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"), -} diff --git a/tpv/signals.py b/tpv/signals.py deleted file mode 100644 index 4e952a7..0000000 --- a/tpv/signals.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.dispatch import Signal - -redsys_payment_accepted = Signal() -redsys_payment_rejected = Signal() diff --git a/tpv/templates/tpv/base.html b/tpv/templates/tpv/base.html deleted file mode 100644 index 15733e9..0000000 --- a/tpv/templates/tpv/base.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - Pago de facturas - - - - - - - - -{% include 'tpv/header.html' %} - -{% block body %} -{% endblock %} - - - \ No newline at end of file diff --git a/tpv/templates/tpv/cart_item.html b/tpv/templates/tpv/cart_item.html deleted file mode 100644 index 108c24f..0000000 --- a/tpv/templates/tpv/cart_item.html +++ /dev/null @@ -1,15 +0,0 @@ -
-
-
-

Factura #{{ item.id }}

-

- {{ item.name }} - {{ item.description }} -

-
-
-
- {{ item.amount_to_pay }} € -
-
-
-
diff --git a/tpv/templates/tpv/form.html b/tpv/templates/tpv/form.html deleted file mode 100644 index f12ec33..0000000 --- a/tpv/templates/tpv/form.html +++ /dev/null @@ -1,20 +0,0 @@ -
- {% if order.status == 'PEN' %} -
- {% csrf_token %} - - - - - {% if order.contact_email != '' %} - - {% else %} - - {% endif %} -
-{% endif %} -
diff --git a/tpv/templates/tpv/header.html b/tpv/templates/tpv/header.html deleted file mode 100644 index d6a2c9d..0000000 --- a/tpv/templates/tpv/header.html +++ /dev/null @@ -1,3 +0,0 @@ -
- -
diff --git a/tpv/templates/tpv/order_created.html b/tpv/templates/tpv/order_created.html deleted file mode 100644 index b85a5df..0000000 --- a/tpv/templates/tpv/order_created.html +++ /dev/null @@ -1,102 +0,0 @@ -{% extends 'tpv/base.html' %} - -{% block body %} - -
- -
-

Resumen de pedido

- - {% if order.status == 'PAI' %} - Pagado - {% elif order.status == 'PEN' %} - Pendiente de pago - {% else %} - Error de pago - {% endif %} - -
- -
-
- - {% for item in order.metadata.items %} - {% include 'tpv/cart_item.html' %} - {% endfor %} - -
- -
- {% if order.status == 'PEN' %} - - -
-
-
-
-
- {% csrf_token %} - - {% for field in form %} - - {{ field }} - - {% endfor %} - - -
- -
-
-
-
- - {% endif %} - -
-
-

Subtotal

-

{{ order.amount }} €

-
- -
-
-

Total

-
-

{{ order.amount }} €

-

Impuestos incluidos

-
-
- - {% if order.status == 'PEN' %} -
- {% csrf_token %} - - - - - {% if order.contact_email != '' %} - - {% else %} - - {% endif %} -
- {% endif %} -
- - -
-
- -
-{% endblock %} diff --git a/tpv/tests.py b/tpv/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/tpv/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/tpv/tests/__init__.py b/tpv/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tpv/urls.py b/tpv/urls.py deleted file mode 100644 index 1f7ae6c..0000000 --- a/tpv/urls.py +++ /dev/null @@ -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//", transaction_created, name="order_created"), - path("transaction//webhook/", webhook, name="webhook"), - path("transaction//ok/", payment_accepted, name="ok"), - path("transaction//ko/", payment_rejected, name="ko"), - path("transaction//form/", payment_form, name="payment_form"), -] diff --git a/tpv/utils.py b/tpv/utils.py deleted file mode 100644 index 4030f82..0000000 --- a/tpv/utils.py +++ /dev/null @@ -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)) diff --git a/tpv/views.py b/tpv/views.py deleted file mode 100644 index c422769..0000000 --- a/tpv/views.py +++ /dev/null @@ -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() diff --git a/web/templates/components/products/wishlist_button.html b/web/templates/components/products/wishlist_button.html index 816f448..b1b4cff 100644 --- a/web/templates/components/products/wishlist_button.html +++ b/web/templates/components/products/wishlist_button.html @@ -3,8 +3,7 @@
{% csrf_token %} -
@@ -12,8 +11,7 @@
{% csrf_token %} -
diff --git a/web/templates/web/list_products.html b/web/templates/web/list_products.html index 94171f7..e194afa 100644 --- a/web/templates/web/list_products.html +++ b/web/templates/web/list_products.html @@ -1,5 +1,7 @@ {% load i18n %} {% load static %} +{{ page }} +
    {% for product in page %}
  • @@ -8,6 +10,7 @@ {% endfor %}
+ {% if has_previous_page %}