diff --git a/config/api/v1/urls.py b/config/api/v1/urls.py index 31f406b..fdf8282 100644 --- a/config/api/v1/urls.py +++ b/config/api/v1/urls.py @@ -5,6 +5,8 @@ from config.api.v1.views import APISchema urlpatterns = [ path("schema/", APISchema.as_view(), name="schema"), - path("swagger/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), + path( + "swagger/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui" + ), path("redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), ] diff --git a/config/api/v1/views.py b/config/api/v1/views.py index c6819ce..0ca967f 100644 --- a/config/api/v1/views.py +++ b/config/api/v1/views.py @@ -3,4 +3,3 @@ from drf_spectacular.views import SpectacularAPIView class APISchema(SpectacularAPIView): api_version = "v1" - diff --git a/config/settings/base.py b/config/settings/base.py index 5da7898..697007b 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -1,9 +1,9 @@ import datetime from config.settings.environ import * # noqa -APP_NAME = 'template' -DESCRIPTION = '' -VERSION = '0.1.0' +APP_NAME = "TPV" +DESCRIPTION = "" +VERSION = "0.1.0" DJANGO_APPS = [ @@ -26,7 +26,7 @@ THIRD_PARTY_APPS = [ ] PROJECT_APPS = [ - + "tpv", ] INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + PROJECT_APPS @@ -176,3 +176,14 @@ SPECTACULAR_SETTINGS = { CELERY_BROKER_URL = env.str("CELERY_BROKER_URL", default="redis://172.17.0.1:6379/0") CELERY_TIME_ZONE = TIME_ZONE CELERY_ALWAYS_EAGER = DEBUG + +# Redsys +# https://pagosonline.redsys.es/conexion-redireccion.html + +REDSYS_SHARED_SECRET = env.str( + "REDSYS_SHARED_SECRET", "sq7HjrUOBfKmC576ILgskD5srU870gJ7" +) +REDSYS_MERCHANT_CODE = env.str("REDSYS_MERCHANT_CODE", "999008881") +REDSYS_TERMINAL = env.str("REDSYS_TERMINAL", "001") +REDSYS_CURRENCY_CODE = env.str("REDSYS_CURRENCY_CODE", "978") +REDSYS_TPV_DOMAIN = env.str("REDSYS_TPV_DOMAIN", "") diff --git a/config/urls.py b/config/urls.py index 28b7917..a72fc89 100644 --- a/config/urls.py +++ b/config/urls.py @@ -6,4 +6,5 @@ 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")), ] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..7522288 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +DJANGO_SETTINGS_MODULE = config.settings.develop +addopts = --ignore=src diff --git a/requirements.txt b/requirements.txt index ddbb97b..e00f58e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ celery==5.2.7 -django==5.0.1 +django==5.0.3 django-cors-headers==3.13.0 +django-cryptography==1.1 django-extensions==3.2.0 django-filter==23.1 django-environ==0.10.0 @@ -13,6 +14,7 @@ drf-spectacular==0.26.1 ipython==8.16.1 Pillow==10.0.1 psycopg2-binary==2.9.5 +pyDes==2.0.1 redis==4.5.4 requests==2.31.0 uvicorn==0.21.1 diff --git a/tpv/__init__.py b/tpv/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tpv/admin.py b/tpv/admin.py new file mode 100644 index 0000000..09315d9 --- /dev/null +++ b/tpv/admin.py @@ -0,0 +1,12 @@ +from django.contrib import admin +from tpv.models import PaymentTPV +from unfold.admin import ModelAdmin + + +# Register your models here. +@admin.register(PaymentTPV) +class PaymentTPVAdmin(ModelAdmin): + list_display = ( + "hash", + "amount", + ) diff --git a/tpv/apps.py b/tpv/apps.py new file mode 100644 index 0000000..f1171a4 --- /dev/null +++ b/tpv/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TpvConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "tpv" diff --git a/tpv/exceptions.py b/tpv/exceptions.py new file mode 100644 index 0000000..ab5b9c3 --- /dev/null +++ b/tpv/exceptions.py @@ -0,0 +1,6 @@ +class RedsysValidationException(Exception): + pass + + +class RedsysPaymentException(Exception): + pass diff --git a/tpv/forms.py b/tpv/forms.py new file mode 100644 index 0000000..0796af9 --- /dev/null +++ b/tpv/forms.py @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..3a320e1 --- /dev/null +++ b/tpv/migrations/0001_initial.py @@ -0,0 +1,99 @@ +# Generated by Django 5.0.3 on 2024-03-21 22:43 + +import tpv.models +import uuid +from decimal import Decimal +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="PaymentTPV", + 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 new file mode 100644 index 0000000..e69de29 diff --git a/tpv/models.py b/tpv/models.py new file mode 100644 index 0000000..bd991b7 --- /dev/null +++ b/tpv/models.py @@ -0,0 +1,75 @@ +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 PaymentTPV(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, + }], + } + """ + + def regenerate_uuid(self): + self.hash = uuid4() + self.save() + + @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/redsys.py b/tpv/redsys.py new file mode 100644 index 0000000..06efbe8 --- /dev/null +++ b/tpv/redsys.py @@ -0,0 +1,105 @@ +import base64 +import json + +from django.conf import settings +from django.urls import reverse +from tpv.models import PaymentTPV +from tpv.utils import compute_signature + + +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 + + +class RedsysClient: + DEBUG_ENVIRONMENT_URL = "https://sis-t.redsys.es:25443/sis/realizarPago" + PROD_ENVIRONMENT_URL = "" + + def get_target_url(self): + if settings.DEBUG: + return self.DEBUG_ENVIRONMENT_URL + return self.PROD_ENVIRONMENT_URL + + def get_merchant_code(self) -> str: + return settings.REDSYS_MERCHANT_CODE + + def get_terminal(self) -> str: + return settings.REDSYS_TERMINAL + + def get_currency_code(self) -> str: + return settings.REDSYS_CURRENCY_CODE + + def get_merchant_url_ok_for_order(self, order: PaymentTPV) -> str: + path = reverse("tpv:ok", kwargs={"order": order.hash}) + return f"{settings.REDSYS_TPV_DOMAIN}{path}" + + def get_merchant_url_ko_for_order(self, order: PaymentTPV) -> str: + path = reverse("tpv:ko", kwargs={"order": order.hash}) + return f"{settings.REDSYS_TPV_DOMAIN}{path}" + + def get_webhook_url_for_order(self, order: PaymentTPV) -> str: + path = reverse("tpv:webhook", kwargs={"order": order.hash}) + return f"{settings.REDSYS_TPV_DOMAIN}{path}" + + def get_shared_secret(self) -> str: + return settings.REDSYS_SHARED_SECRET + + def get_signature(self, hash, payload: str): + key = self.get_shared_secret() + return compute_signature(str(hash), payload, key).decode() + + def get_merchant_parameters_for_order( + self, order: PaymentTPV, transaction_type: int = TransactionTypes.AUTHORIZATION + ) -> dict: + merchant_code = self.get_merchant_code() + + return { + "DS_MERCHANT_AMOUNT": str(order.amount_integer), + "DS_MERCHANT_CURRENCY": self.get_currency_code(), + "DS_MERCHANT_MERCHANTCODE": merchant_code, + "DS_MERCHANT_MERCHANTURL": self.get_webhook_url_for_order(order), # Webhook + "DS_MERCHANT_ORDER": order.hash.hex, + "DS_MERCHANT_TERMINAL": self.get_terminal(), + "DS_MERCHANT_TRANSACTIONTYPE": transaction_type, + "DS_MERCHANT_URLKO": self.get_merchant_url_ko_for_order( + order + ), # Página informativa al usuario - Pago erróneo + "DS_MERCHANT_URLOK": self.get_merchant_url_ok_for_order( + order + ), # Página informativa al usuario - Pago confirmado + } + + def get_encoded_merchant_parameters_for_order( + self, order: PaymentTPV, transaction_type: int = TransactionTypes.AUTHORIZATION + ) -> str: + body = self.get_merchant_parameters_for_order(order, transaction_type) + stringified_body = json.dumps(body) + return base64.b64encode(stringified_body.encode()).decode("utf-8") + + def get_body_for_order( + self, order: PaymentTPV, transaction_type: int = TransactionTypes.AUTHORIZATION + ) -> dict: + merchant_parameters = self.get_encoded_merchant_parameters_for_order( + order, transaction_type + ) + signature = self.get_signature(order.hash.hex, merchant_parameters) + + return { + "Ds_MerchantParameters": merchant_parameters, + "Ds_SignatureVersion": "HMAC_SHA256_V1", + "Ds_Signature": signature, + } diff --git a/tpv/settings.py b/tpv/settings.py new file mode 100644 index 0000000..0b8dbda --- /dev/null +++ b/tpv/settings.py @@ -0,0 +1,254 @@ +from django.utils.text import gettext_lazy as _ + + +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 new file mode 100644 index 0000000..b968008 --- /dev/null +++ b/tpv/signals.py @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..15733e9 --- /dev/null +++ b/tpv/templates/tpv/base.html @@ -0,0 +1,38 @@ + + +
+ ++ {{ item.name }} - {{ item.description }} +
+Subtotal
+{{ order.amount }} €
+Total
+{{ order.amount }} €
+Impuestos incluidos
+