From 782cf09d6897565d3dfae0ab90aab0e7cb8da932 Mon Sep 17 00:00:00 2001 From: Diego J Date: Mon, 13 Mar 2017 14:56:14 +0100 Subject: [PATCH 01/72] Fixing get_type_help bug --- djangovirtualpos/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index 78ccea3..d855923 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -252,7 +252,7 @@ def erase(self): #################################################################### ## Obtiene el texto de ayuda del tipo del TPV def get_type_help(self): - return VPOS_TYPES[self.type] + return dict(VPOS_TYPES)[self.type] #################################################################### ## Devuelve el TPV específico From 01b54c3037132b6be1ba475edad8ba0d6d63de7e Mon Sep 17 00:00:00 2001 From: Diego J Date: Tue, 14 Mar 2017 12:40:12 +0100 Subject: [PATCH 02/72] Release 1.4 --- CHANGES.md | 7 +++++ .../migrations/0005_auto_20170314_1057.py | 28 +++++++++++++++++++ .../0006_vpospaymentoperation_environment.py | 19 +++++++++++++ setup.py | 2 +- 4 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 djangovirtualpos/migrations/0005_auto_20170314_1057.py create mode 100644 djangovirtualpos/migrations/0006_vpospaymentoperation_environment.py diff --git a/CHANGES.md b/CHANGES.md index 956b438..9332e9f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,3 +16,10 @@ Minor changes in README.md. ## 1.2 - Add new permission view_virtualpointofsale to ease management. - Add method specificit_vpos in VirtualPointOfSale that returns the specific model object according to the VPOS type. + +## 1.3 +- Fixing get_type_help_bug + +## 1.4 +- Adding environment to VPOSPaymentOperation +- Changing labels in models \ No newline at end of file diff --git a/djangovirtualpos/migrations/0005_auto_20170314_1057.py b/djangovirtualpos/migrations/0005_auto_20170314_1057.py new file mode 100644 index 0000000..2e81c48 --- /dev/null +++ b/djangovirtualpos/migrations/0005_auto_20170314_1057.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangovirtualpos', '0004_auto_20170313_1401'), + ] + + operations = [ + migrations.AlterModelOptions( + name='virtualpointofsale', + options={'ordering': ['name'], 'verbose_name': 'virtual point of sale', 'verbose_name_plural': 'virtual points of sale', 'permissions': (('view_virtualpointofsale', 'View Virtual Points of Sale'),)}, + ), + migrations.AlterField( + model_name='virtualpointofsale', + name='distributor_cif', + field=models.CharField(help_text='C.I.F. del distribuidor.', max_length=150, verbose_name='CIF del distribuidor', blank=True), + ), + migrations.AlterField( + model_name='virtualpointofsale', + name='distributor_name', + field=models.CharField(help_text='Raz\xf3n social del distribuidor.', max_length=512, verbose_name='Raz\xf3n social del distribuidor', blank=True), + ), + ] diff --git a/djangovirtualpos/migrations/0006_vpospaymentoperation_environment.py b/djangovirtualpos/migrations/0006_vpospaymentoperation_environment.py new file mode 100644 index 0000000..5f6db21 --- /dev/null +++ b/djangovirtualpos/migrations/0006_vpospaymentoperation_environment.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangovirtualpos', '0005_auto_20170314_1057'), + ] + + operations = [ + migrations.AddField( + model_name='vpospaymentoperation', + name='environment', + field=models.CharField(default='', max_length=255, verbose_name='Entorno del TPV', blank=True, choices=[('testing', 'Pruebas'), ('production', 'Producci\xf3n')]), + ), + ] diff --git a/setup.py b/setup.py index 32183df..ec854f8 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ setup( name="django-virtual-pos", - version="1.2", + version="1.4", install_requires=[ "django", "beautifulsoup4", From 17da90933b00497f615a5b3f6ba1c57a5c8eeb58 Mon Sep 17 00:00:00 2001 From: Diego J Date: Tue, 14 Mar 2017 12:53:35 +0100 Subject: [PATCH 03/72] Add enviroment to VPOSPaymentOperation --- djangovirtualpos/models.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index d855923..5f32720 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -82,6 +82,12 @@ def get_delegated_class(virtualpos_type): ("failed", _(u"Failed")), ) +#################################################################### +## Tipos de estado del TPV +VIRTUALPOS_STATE_TYPES = ( + ("testing", "Pruebas"), + ("production", "Producción") +) #################################################################### ## Operación de pago de TPV @@ -114,7 +120,7 @@ class VPOSPaymentOperation(models.Model): type = models.CharField(max_length=16, choices=VPOS_TYPES, default="", verbose_name="Tipo de TPV") virtual_point_of_sale = models.ForeignKey("VirtualPointOfSale", parent_link=True, related_name="+", null=False) - + environment = models.CharField(max_length=255, choices=VIRTUALPOS_STATE_TYPES, default="", blank=True, verbose_name="Entorno del TPV") @property def vpos(self): @@ -137,14 +143,6 @@ def save(self, *args, **kwargs): super(VPOSPaymentOperation, self).save(*args, **kwargs) -#################################################################### -## Tipos de estado del TPV -VIRTUALPOS_STATE_TYPES = ( - ("testing", "Pruebas"), - ("production", "Producción") -) - - #################################################################### #################################################################### @@ -180,13 +178,13 @@ class VirtualPointOfSale(models.Model): ## Nombre del distribuidor del plan distributor_name = models.CharField(null=False, blank=True, max_length=512, - verbose_name="Razón social del distribuidor de entradas", - help_text="Razón social del distribuidor de entradas.") + verbose_name="Razón social del distribuidor", + help_text="Razón social del distribuidor.") ## CIF del organizador del plan distributor_cif = models.CharField(null=False, blank=True, max_length=150, - verbose_name="CIF del distribuidor de entradas", - help_text="C.I.F. del distribuidor de entradas.") + verbose_name="CIF del distribuidor", + help_text="C.I.F. del distribuidor.") ## Estado del TPV: por si es de pruebas o de producción environment = models.CharField(max_length=16, null=False, blank=False, choices=VIRTUALPOS_STATE_TYPES, From fa3c3fd8d74ae29e533d9e096685c38feffa5d20 Mon Sep 17 00:00:00 2001 From: Diego J Date: Tue, 14 Mar 2017 12:54:38 +0100 Subject: [PATCH 04/72] Release 1.5 --- CHANGES.md | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9332e9f..ca62a61 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,6 +20,6 @@ Minor changes in README.md. ## 1.3 - Fixing get_type_help_bug -## 1.4 +## 1.5 - Adding environment to VPOSPaymentOperation -- Changing labels in models \ No newline at end of file +- Changing labels in models diff --git a/setup.py b/setup.py index ec854f8..87148fa 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ setup( name="django-virtual-pos", - version="1.4", + version="1.5", install_requires=[ "django", "beautifulsoup4", From bc7d697defdb5a78c837b0f31f73055c10847518 Mon Sep 17 00:00:00 2001 From: Diego J Date: Tue, 14 Mar 2017 13:04:30 +0100 Subject: [PATCH 05/72] Environment must be assigned in the VPOSPaymentOperation --- djangovirtualpos/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index 5f32720..d472f18 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -333,7 +333,8 @@ def configurePayment(self, amount, description, url_ok, url_nok, sale_code): # (se guarda cuando se tenga el número de operación) self.operation = VPOSPaymentOperation( amount=amount, description=description, url_ok=url_ok, url_nok=url_nok, - sale_code=sale_code, status="pending", virtual_point_of_sale=self, type=self.type + sale_code=sale_code, status="pending", + virtual_point_of_sale=self, type=self.type, environment=self.environment ) # Configuración específica (requiere que exista self.operation) From 5f44a2aa00b3f2d450b29cc9839502ae43119623 Mon Sep 17 00:00:00 2001 From: Diego J Date: Tue, 14 Mar 2017 13:05:25 +0100 Subject: [PATCH 06/72] New version (1.6) that fixes the problems with VPOSPaymentOperation --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 87148fa..06fa8e8 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ setup( name="django-virtual-pos", - version="1.5", + version="1.6", install_requires=[ "django", "beautifulsoup4", From 285329269a920dc449eaebce083cc1b23a35c85a Mon Sep 17 00:00:00 2001 From: Diego J Date: Fri, 17 Mar 2017 18:10:33 +0100 Subject: [PATCH 07/72] Allow reverse relation VPOSPaymentOperation -> VirtualPointOfSale --- CHANGES.md | 3 +++ djangovirtualpos/models.py | 2 +- setup.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ca62a61..33bfc6f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,3 +23,6 @@ Minor changes in README.md. ## 1.5 - Adding environment to VPOSPaymentOperation - Changing labels in models + +## 1.6.1 +- Allow reverse relation VPOSPaymentOperation -> VirtualPointOfSale diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index d472f18..b5b50e0 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -119,7 +119,7 @@ class VPOSPaymentOperation(models.Model): last_update_datetime = models.DateTimeField(verbose_name="Fecha de última actualización del objeto") type = models.CharField(max_length=16, choices=VPOS_TYPES, default="", verbose_name="Tipo de TPV") - virtual_point_of_sale = models.ForeignKey("VirtualPointOfSale", parent_link=True, related_name="+", null=False) + virtual_point_of_sale = models.ForeignKey("VirtualPointOfSale", parent_link=True, related_name="payment_operations", null=False) environment = models.CharField(max_length=255, choices=VIRTUALPOS_STATE_TYPES, default="", blank=True, verbose_name="Entorno del TPV") @property diff --git a/setup.py b/setup.py index 06fa8e8..3cab7ac 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ setup( name="django-virtual-pos", - version="1.6", + version="1.6.1", install_requires=[ "django", "beautifulsoup4", From 0e9a6d369f79fdb12e7fe3b0f3f668d555cdd2c4 Mon Sep 17 00:00:00 2001 From: Diego J Date: Mon, 20 Mar 2017 17:28:08 +0100 Subject: [PATCH 08/72] Fixing bug of calling a non-existant class --- djangovirtualpos/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index b5b50e0..0407a66 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -1161,7 +1161,7 @@ def _receiveConfirmationSOAP(request): operation = VPOSPaymentOperation.objects.get(operation_number=ds_order) operation.confirmation_data = {"GET": "", "POST": xml_content} operation.confirmation_code = ds_order - operation.response_code = TpvRedsys._format_ds_response_code(ds_response) + operation.response_code = VPOSRedsys._format_ds_response_code(ds_response) operation.save() dlprint("Operation {0} actualizada en _receiveConfirmationSOAP()".format(operation.operation_number)) vpos = operation.virtual_point_of_sale @@ -1371,7 +1371,7 @@ def _format_ds_response_code(ds_response): if len(ds_response) == 4 and ds_response.isdigit() and ds_response[:2] == "00": message = u"Transacción autorizada para pagos y preautorizaciones." else: - message = TpvRedsys.DS_RESPONSE_CODES.get(ds_response) + message = VPOSRedsys.DS_RESPONSE_CODES.get(ds_response) out = u"{0}. {1}".format(ds_response, message) From d1f114e946c467fd4f7f2c0316bfed2a20e39e13 Mon Sep 17 00:00:00 2001 From: Diego J Date: Mon, 20 Mar 2017 17:28:48 +0100 Subject: [PATCH 09/72] New version in setup.py (1.6.2) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3cab7ac..e2478dc 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ setup( name="django-virtual-pos", - version="1.6.1", + version="1.6.2", install_requires=[ "django", "beautifulsoup4", From cfa3048c00ceb963103b30912d4d5f7e987f7123 Mon Sep 17 00:00:00 2001 From: Diego J Date: Mon, 20 Mar 2017 17:44:09 +0100 Subject: [PATCH 10/72] New version in setup.py (1.6.3) --- djangovirtualpos/models.py | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index 0407a66..66cb25b 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -1158,6 +1158,9 @@ def _receiveConfirmationSOAP(request): # Almacén de operaciones try: ds_order = root.xpath("//Message/Request/Ds_Order/text()")[0] + ds_authorisationcode = root.xpath("//Message/Request/Ds_AuthorisationCode/text()")[0] + ds_response = root.xpath("//Message/Request/Ds_Response/text()")[0] + operation = VPOSPaymentOperation.objects.get(operation_number=ds_order) operation.confirmation_data = {"GET": "", "POST": xml_content} operation.confirmation_code = ds_order diff --git a/setup.py b/setup.py index e2478dc..e485f83 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ setup( name="django-virtual-pos", - version="1.6.2", + version="1.6.3", install_requires=[ "django", "beautifulsoup4", From 7988f808e281e9da134cf26b98178b3fd6e98d2c Mon Sep 17 00:00:00 2001 From: Diego J Date: Wed, 29 Mar 2017 14:41:17 +0200 Subject: [PATCH 11/72] Forgotten add migrations --- .../migrations/0007_auto_20170320_1757.py | 24 +++++++++++++++++++ .../migrations/0008_auto_20170321_1437.py | 19 +++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 djangovirtualpos/migrations/0007_auto_20170320_1757.py create mode 100644 djangovirtualpos/migrations/0008_auto_20170321_1437.py diff --git a/djangovirtualpos/migrations/0007_auto_20170320_1757.py b/djangovirtualpos/migrations/0007_auto_20170320_1757.py new file mode 100644 index 0000000..dd57cec --- /dev/null +++ b/djangovirtualpos/migrations/0007_auto_20170320_1757.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangovirtualpos', '0006_vpospaymentoperation_environment'), + ] + + operations = [ + migrations.AlterField( + model_name='vpospaymentoperation', + name='sale_code', + field=models.CharField(help_text='C\xf3digo de la venta seg\xfan la aplicaci\xf3n.', unique=True, max_length=255, verbose_name='C\xf3digo de la venta'), + ), + migrations.AlterField( + model_name='vpospaymentoperation', + name='virtual_point_of_sale', + field=models.ForeignKey(parent_link=True, related_name='payment_operations', to='djangovirtualpos.VirtualPointOfSale'), + ), + ] diff --git a/djangovirtualpos/migrations/0008_auto_20170321_1437.py b/djangovirtualpos/migrations/0008_auto_20170321_1437.py new file mode 100644 index 0000000..5f008bd --- /dev/null +++ b/djangovirtualpos/migrations/0008_auto_20170321_1437.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangovirtualpos', '0007_auto_20170320_1757'), + ] + + operations = [ + migrations.AlterField( + model_name='vpospaymentoperation', + name='sale_code', + field=models.CharField(help_text='C\xf3digo de la venta seg\xfan la aplicaci\xf3n.', max_length=512, verbose_name='C\xf3digo de la venta'), + ), + ] From af50e68cca2aef816290ae5c9a9309a72e5682f1 Mon Sep 17 00:00:00 2001 From: Diego J Date: Wed, 29 Mar 2017 14:42:24 +0200 Subject: [PATCH 12/72] 1.6.4 release --- CHANGES.md | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 33bfc6f..4ad0f80 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,3 +26,6 @@ Minor changes in README.md. ## 1.6.1 - Allow reverse relation VPOSPaymentOperation -> VirtualPointOfSale + +## 1.6.4 +- Include migrations. diff --git a/setup.py b/setup.py index e485f83..bbc7be3 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ setup( name="django-virtual-pos", - version="1.6.3", + version="1.6.4", install_requires=[ "django", "beautifulsoup4", From 19138700dd2b9b0a62de033bb282ecef6aaee869 Mon Sep 17 00:00:00 2001 From: Jose Miguel Lopez Date: Tue, 4 Apr 2017 13:48:04 +0200 Subject: [PATCH 13/72] Add method to allow partial refund and full refund, specific to Redsys TPV Platform. --- CHANGES.md | 54 ++++++-- djangovirtualpos/models.py | 272 ++++++++++++++++++++++++++++++++++++- setup.py | 1 + 3 files changed, 310 insertions(+), 17 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4ad0f80..7a129bb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,42 @@ Django module that abstracts the flow of several virtual points of sale includin # Releases +## 1.6.5 +- Add method to allow partial refund and full refund, specific to Redsys TPV Platform. +- New model to register refund operations. +- Add refund view example. + + +## 1.6.4 +- Include migrations. + + +## 1.6.4 +- Include migrations. + + +## 1.6.1 +- Allow reverse relation VPOSPaymentOperation -> VirtualPointOfSale + + +## 1.5 +- Adding environment to VPOSPaymentOperation +- Changing labels in models + + +## 1.3 +- Fixing get_type_help_bug + + +## 1.2 +- Add new permission view_virtualpointofsale to ease management. +- Add method specificit_vpos in VirtualPointOfSale that returns the specific model object according to the VPOS type. + + +## 1.1 +Minor changes in README.md. + + ## 1.0 Features - Integration with PayPal Checkout. - Integration with the following Spanish bank virtual POS: @@ -10,22 +46,12 @@ Django module that abstracts the flow of several virtual points of sale includin - [Santander Elavon](https://www.santanderelavon.com/) - [CECA](http://www.cajasdeahorros.es/). -## 1.1 -Minor changes in README.md. -## 1.2 -- Add new permission view_virtualpointofsale to ease management. -- Add method specificit_vpos in VirtualPointOfSale that returns the specific model object according to the VPOS type. -## 1.3 -- Fixing get_type_help_bug -## 1.5 -- Adding environment to VPOSPaymentOperation -- Changing labels in models -## 1.6.1 -- Allow reverse relation VPOSPaymentOperation -> VirtualPointOfSale -## 1.6.4 -- Include migrations. + + + + diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index 66cb25b..0634619 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -8,13 +8,16 @@ ########################################### # Sistema de depuración + from bs4 import BeautifulSoup from debug import dlprint +from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.conf import settings from django.core.validators import MinLengthValidator, MaxLengthValidator, RegexValidator +from django.db.models import Sum from django.http import HttpResponse from django.utils import timezone @@ -34,6 +37,10 @@ from djangovirtualpos.util import dictlist, localize_datetime from django.utils.translation import ugettext_lazy as _ +import requests +from bs4 import BeautifulSoup + + VPOS_TYPES = ( ("ceca", _("TPV Virtual - Confederación Española de Cajas de Ahorros (CECA)")), ("paypal", _("Paypal")), @@ -80,8 +87,17 @@ def get_delegated_class(virtualpos_type): ("pending", _(u"Pending")), ("completed", _(u"Completed")), ("failed", _(u"Failed")), + ("partially_refunded", _(u"Partially Refunded")), + ("completely_refunded", _(u"Completely Refunded")), +) + +VPOS_REFUND_STATUS_CHOICES = ( + ("pending", _(u"Pending")), + ("completed", _(u"Completed")), + ("failed", _(u"Failed")), ) + #################################################################### ## Tipos de estado del TPV VIRTUALPOS_STATE_TYPES = ( @@ -126,6 +142,26 @@ class VPOSPaymentOperation(models.Model): def vpos(self): return self.virtual_point_of_sale + @property + def total_amount_refunded(self): + return self.refund_operations.filter(status='completed').aggregate(Sum('amount'))['amount__sum'] + + # Comprueba si un pago ha sido totalmente debuelto y cambia el estado en coherencias. + def compute_payment_refunded_status(self): + + if self.total_amount_refunded == self.amount: + self.status = "completely_refunded" + + elif self.total_amount_refunded < self.amount: + dlprint('Devolución parcial de pago.') + self.status = "partially_refunded" + + elif self.total_amount_refunded > self.amount: + raise ValueError(u'ERROR. Este caso es imposible, no se puede reembolsar una cantidad superior al pago.') + + self.save() + + ## Guarda el objeto en BD, en realidad lo único que hace es actualizar los datetimes def save(self, *args, **kwargs): """ @@ -149,11 +185,17 @@ def save(self, *args, **kwargs): # Excepción para indicar que la operación charge ha devuelto una respuesta incorrecta o de fallo class VPOSCantCharge(Exception): pass +# Excepción para indicar que no se ha implementado una operación para un tipo de TPV en particular. +class VPOSOperationDontImplemented(Exception): pass + +# Cuando se produce un error al realizar una operación en concreto. +class VPOSOperationException(Exception): pass + #################################################################### ## Clase que contiene las operaciones de pago de forma genérica ## actúa de fachada de forma que el resto del software no conozca -## +## class VirtualPointOfSale(models.Model): """ Clases que actúa como clase base para la relación de especialización. @@ -444,6 +486,8 @@ def charge(self): # Devolvemos el cargo return response + + #################################################################### ## Paso 3.3b1. Error en verificación. ## No se ha podido recuperar la instancia de TPV de la respuesta del @@ -472,6 +516,97 @@ def responseNok(self, extended_status=""): return self.delegated.responseNok() + #################################################################### + ## Paso R. (Refund) Configura el TPV en modo devolución + ## TODO: Se implementa solo para Redsys + def refund(self, operation_sale_code, refund_amount, description): + """ + 1. Realiza configuración necesaria para realiza una devolución. + + Antes de ello realiza las comprobaciones necesarias. + + @param operation_sale_code: Código del pago que pretendemos reembolsar. + @param refund_amount: Cantidad del pago que reembolsamos + @param description: Descripción del motivo por el cual se realiza la devolución. + """ + + try: + # Cargamos la operación sobre la que vamos a realizar la devolución. + payment_operation = VPOSPaymentOperation.objects.get(sale_code=operation_sale_code) + except ObjectDoesNotExist: + raise Exception(u"No se puede cargar una operación anterior con el código {0}".format(operation_sale_code)) + + if (not self.has_total_refunds) and (not self.has_partial_refunds): + raise Exception(u"El TPV no admite devoluciones, ni totales, ni parciales") + + if refund_amount > payment_operation.amount: + raise Exception(u"Imposible reembolsar una cantidad superior a la del pago") + + if (refund_amount < payment_operation.amount) and (not self.has_partial_refunds): + raise Exception(u"Configuración del TPV no permite realizar devoluciones parciales") + + if (refund_amount == payment_operation.amount) and (not self.has_total_refunds): + raise Exception(u"Configuración del TPV no permite realizar devoluciones totales") + + # Creamos la operación, marcandola como pendiente. + self.operation = VPOSRefundOperation(amount=refund_amount, + description=description, + operation_number=payment_operation.operation_number, + status='pending', + payment=payment_operation) + + self.operation.save() + + # Llamamos al delegado que implementa la funcionalidad en particular. + refund_response = self.delegated.refund(refund_amount, description) + + if refund_response: + refund_status = 'completed' + else: + refund_status = 'failed' + + self.operation.status = refund_status + self.operation.save() + + # Calcula el nuevo estado del pago, en función de la suma de las devoluciones, + # (pudiendolo marcas como "completely_refunded" o "partially_refunded"). + payment_operation.compute_payment_refunded_status() + + return refund_response + + +######################################################################################################################## +class VPOSRefundOperation(models.Model): + """ + Entidad que gestiona las devoluciones de pagos realizados. + Las devoluciones pueden ser totales o parciales, por tanto un "pago" tiene una relación uno a muchos con "devoluciones". + """ + amount = models.DecimalField(max_digits=6, decimal_places=2, null=False, blank=False, verbose_name=u"Cantidad de la devolución") + description = models.CharField(max_length=512, null=False, blank=False, verbose_name=u"Descripción de la devolución") + + operation_number = models.CharField(max_length=255, null=False, blank=False, verbose_name=u"Número de operación") + status = models.CharField(max_length=64, choices=VPOS_REFUND_STATUS_CHOICES, null=False, blank=False, verbose_name=u"Estado de la devolución") + creation_datetime = models.DateTimeField(verbose_name="Fecha de creación del objeto") + last_update_datetime = models.DateTimeField(verbose_name="Fecha de última actualización del objeto") + payment = models.ForeignKey(VPOSPaymentOperation, on_delete=models.PROTECT, related_name="refund_operations") + + + ## Guarda el objeto en BD, en realidad lo único que hace es actualizar los datetimes + def save(self, *args, **kwargs): + """ + Guarda el objeto en BD, en realidad lo único que hace es actualizar los datetimes. + El datetime de actualización se actualiza siempre, el de creación sólo al guardar de nuevas. + """ + # Datetime con el momento actual en UTC + now_datetime = datetime.datetime.now() + # Si no se ha guardado aún, el datetime de creación es la fecha actual + if not self.id: + self.creation_datetime = localize_datetime(now_datetime) + # El datetime de actualización es la fecha actual + self.last_update_datetime = localize_datetime(now_datetime) + # Llamada al constructor del padre + super(VPOSRefundOperation, self).save(*args, **kwargs) + ######################################################################################################################## ######################################################################################################################## @@ -767,6 +902,12 @@ def responseNok(self, **kwargs): dlprint("responseNok") return HttpResponse("") + #################################################################### + ## Paso R. (Refund) Configura el TPV en modo devolución + ## TODO: No implementado + def refund(self, refund_amount, description): + raise VPOSOperationDontImplemented(u"No se ha implementado la operación de devolución particular para CECA.") + #################################################################### ## Generador de firma para el envío def _sending_signature(self): @@ -1205,7 +1346,6 @@ def _receiveConfirmationSOAP(request): def verifyConfirmation(self): firma_calculada = self._verification_signature() dlprint("Firma calculada " + firma_calculada) - dlprint("Firma recibida " + self.firma) # Traducir caracteres de la firma recibida '-' y '_' al alfabeto base64 @@ -1292,6 +1432,118 @@ def responseNok(self, **kwargs): # que la operación ha sido negativa, pasamos una respuesta vacia return HttpResponse("") + def refund(self, refund_amount, description): + + """ + :param payment_operation: Operación de pago asociada a la devolución. + :param refund_amount: Cantidad de la devolución. + :param description: Motivo o comentario de la devolución. + :return: True | False según se complete la operación con éxito. + """ + + # Modificamos el tipo de operación para indicar que la transacción + # es de tipo devolución automática. + # URL de pago según el entorno + self.url = self.REDSYS_URL[self.parent.environment] + + # IMPORTANTE: Este es el código de operación para hacer devoluciones. + self.transaction_type = 3 + + # Formato para Importe: según redsys, ha de tener un formato de entero positivo, con las dos últimas posiciones + # ocupadas por los decimales + self.importe = "{0:.2f}".format(float(refund_amount)).replace(".", "") + + # Idioma de la pasarela, por defecto es español, tomamos + # el idioma actual y le asignamos éste + self.idioma = self.IDIOMAS["es"] + lang = translation.get_language() + if lang in self.IDIOMAS: + self.idioma = self.IDIOMAS[lang] + + order_data = { + # Indica el importe de la venta + "DS_MERCHANT_AMOUNT": self.importe, + + # Indica el número de operacion + "DS_MERCHANT_ORDER": self.parent.operation.operation_number, + + # Código FUC asignado al comercio + "DS_MERCHANT_MERCHANTCODE": self.merchant_code, + + # Indica el tipo de moneda a usar + "DS_MERCHANT_CURRENCY": self.tipo_moneda, + + # Indica que tipo de transacción se utiliza + "DS_MERCHANT_TRANSACTIONTYPE": self.transaction_type, + + # Indica el terminal + "DS_MERCHANT_TERMINAL": self.terminal_id, + + # Obligatorio si se tiene confirmación online. + "DS_MERCHANT_MERCHANTURL": self.merchant_response_url, + + # URL a la que se redirige al usuario en caso de que la venta haya sido satisfactoria + "DS_MERCHANT_URLOK": self.parent.operation.payment.url_ok, + + # URL a la que se redirige al usuario en caso de que la venta NO haya sido satisfactoria + "DS_MERCHANT_URLKO": self.parent.operation.payment.url_nok, + + # Se mostrará al titular en la pantalla de confirmación de la compra + "DS_MERCHANT_PRODUCTDESCRIPTION": description, + + # Indica el valor del idioma + "DS_MERCHANT_CONSUMERLANGUAGE": self.idioma, + + # Representa la suma total de los importes de las cuotas + "DS_MERCHANT_SUMTOTAL": self.importe, + } + + json_order_data = json.dumps(order_data) + packed_order_data = base64.b64encode(json_order_data) + + data = { + "Ds_SignatureVersion": "HMAC_SHA256_V1", + "Ds_MerchantParameters": packed_order_data, + "Ds_Signature": self._redsys_hmac_sha256_signature(packed_order_data) + } + + headers = {'enctype': 'application/x-www-form-urlencoded'} + + # Realizamos petición POST con los datos de la operación y las cabeceras necesarias. + refund_html_request = requests.post(self.url, data=data, headers=headers) + + # En caso de tener una respuesta 200 + if refund_html_request.status_code == 200: + + # Iniciamos un objeto BeautifulSoup (para poder leer los elementos del DOM del HTML recibido). + html = BeautifulSoup(refund_html_request.text, "html.parser") + + # Buscamos elementos significativos del DOM que nos indiquen si la operación se ha realizado correctamente o no. + refund_message_error = html.find('text', {'lngid': 'noSePuedeRealizarOperacion'}) + refund_message_ok = html.find('text', {'lngid': 'operacionAceptada'}) + + # Cuando en el DOM del documento HTML aparece un mensaje de error. + if refund_message_error: + dlprint(refund_message_error) + dlprint(u'Error realizando la operación') + status = False + + # Cuando en el DOM del documento HTML aparece un mensaje de ok. + elif refund_message_ok: + dlprint(u'Operación realizada correctamente') + dlprint(refund_message_error) + status = True + + # No aparece mensaje de error ni de ok + else: + raise VPOSOperationException("La resupuesta HTML con la pantalla de devolución no muestra mensaje informado de forma expícita, si la operación se produce con éxito error, (revisar método 'VPOSRedsys.refund').") + + # Respuesta HTTP diferente a 200 + else: + status = False + + return status + #################################################################### ## Generador de firma de mensajes def _redsys_hmac_sha256_signature(self, data): @@ -1672,6 +1924,13 @@ def responseNok(self, **kwargs): return redirect(reverse("payment_cancel_url", kwargs={"sale_code": self.parent.operation.sale_code})) + #################################################################### + ## Paso R. (Refund) Configura el TPV en modo devolución + ## TODO: No implementado + def refund(self, refund_amount, description): + raise VPOSOperationDontImplemented(u"No se ha implementado la operación de devolución particular para Paypal.") + + ######################################################################################################################## ######################################################################################################################## ################################################# TPV Santander Elavon ################################################# @@ -2078,6 +2337,13 @@ def responseNok(self, **kwargs): """.format(self.parent.operation.url_nok)) + #################################################################### + ## Paso R. (Refund) Configura el TPV en modo devolución + ## TODO: No implementado + def refund(self, refund_amount, description): + raise VPOSOperationDontImplemented(u"No se ha implementado la operación de devolución particular para Santander-Elavon.") + + #################################################################### ## Generador de firma para el envío POST al servicio "Redirect" def _post_signature(self): @@ -2168,4 +2434,4 @@ def _verification_signature(self): dlprint(u"FIRMA2 datos: {0}".format(signature2)) dlprint(u"FIRMA2 hash: {0}".format(firma2)) - return firma2 + return firma2 \ No newline at end of file diff --git a/setup.py b/setup.py index bbc7be3..4014e9b 100755 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ "lxml", "pycrypto", "pytz", + "requests" ], author="intelligenia S.L.", author_email="diego@intelligenia.es", From 4e0561d87460c8d2473c9b640b626e826f708f75 Mon Sep 17 00:00:00 2001 From: Jose Miguel Lopez Date: Tue, 4 Apr 2017 14:15:44 +0200 Subject: [PATCH 14/72] Improving documentation --- README.md | 35 +++++++++++++++++++++++ manual/vpos_types/REDSYS.md | 57 ++++++++++++++++++++++++++----------- setup.py | 2 +- 3 files changed, 77 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index fa6abc4..c665fa5 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Easy integration with PayPal. - [lxml](https://pypi.python.org/pypi/lxml) - [pycrypto](https://pypi.python.org/pypi/pycrypto) - [Pytz](https://pypi.python.org/pypi/pytz) +- [Requests](https://pypi.python.org/pypi/requests) Type: @@ -200,6 +201,40 @@ def payment_cancel(request, sale_code): return render(request, '', {'context': context, 'payment': payment}) ```` +### Refund view + +````python +def refund(request, tpv, payment_code, amount, ): + """ + :param request: + :param tpv: TPV Id + :param payment_code: Payment code + :param amount: Refund Amount (Example 10.89). + :param description: Description of refund cause. + :return: + """ + + amount = Decimal(amount) + + try: + # Checking if the Point of Sale exists + tpv = VirtualPointOfSale.get(id=tpv) + # Checking if the Payment exists + payment = Payment.objects.get(code=payment_code, state="paid") + + except Payment.DoesNotExist as e: + return http_bad_request_response_json_error(message=u"Does not exist payment with code {0}".format(payment_code)) + + refund_status = tpv.refund(payment_code, amount, description) + + if refund_status: + message = u"Refund successful" + else: + message = u"Refund with erros" + + return http_response_json_ok(message) +```` + # Authors - Mario Barchéin marioREMOVETHIS@REMOVETHISintelligenia.com diff --git a/manual/vpos_types/REDSYS.md b/manual/vpos_types/REDSYS.md index aad1248..e69fec5 100644 --- a/manual/vpos_types/REDSYS.md +++ b/manual/vpos_types/REDSYS.md @@ -57,22 +57,22 @@ > Estos serán los valores que nos devuelve la pasarela de pago - **Ds_Date:** Fecha de la transacción - **Ds_Hour:** Hora de la transacción - **Ds_Amount:** Importe de la venta, mismo valor que en la petición - **Ds_Currency:** Tipo de moneda - **Ds_Order:** Número de operación, mismo valor que en la petición - **Ds_MerchantCode:** Indica el código FUC del comercio, mismo valor que en la petición - **Ds_Terminal:** Indica la terminal, , mismo valor que en la petición - **Ds_Signature:** Firma enviada por RedSys, que más tarde compararemos con la generada por el comercio - **Ds_Response:** Código que indica el tipo de transacción - **Ds_MerchantData:** Información opcional enviada por el comercio en el formulario de pago - **Ds_SecurePayment:** Indica: 0, si el pago es NO seguro; 1, si el pago es seguro - **Ds_TransactionType:** Tipo de operación que se envió en el formulario de pago - **Card_Country:** País de emisión de la tarjeta con la que se ha intentado realizar el pago - **AuthorisationCode:** Código alfanumerico de autorización asignado a la aprobación de la transacción - **ConsumerLanguage:** El valor 0 indicará que no se ha determinado el idioma - **Card_type:** Valores posibles: C - Crédito; D - Débito + - **Ds_Date:** Fecha de la transacción + - **Ds_Hour:** Hora de la transacción + - **Ds_Amount:** Importe de la venta, mismo valor que en la petición + - **Ds_Currency:** Tipo de moneda + - **Ds_Order:** Número de operación, mismo valor que en la petición + - **Ds_MerchantCode:** Indica el código FUC del comercio, mismo valor que en la petición + - **Ds_Terminal:** Indica la terminal, , mismo valor que en la petición + - **Ds_Signature:** Firma enviada por RedSys, que más tarde compararemos con la generada por el comercio + - **Ds_Response:** Código que indica el tipo de transacción + - **Ds_MerchantData:** Información opcional enviada por el comercio en el formulario de pago + - **Ds_SecurePayment:** Indica: 0, si el pago es NO seguro; 1, si el pago es seguro + - **Ds_TransactionType:** Tipo de operación que se envió en el formulario de pago + - **Card_Country:** País de emisión de la tarjeta con la que se ha intentado realizar el pago + - **AuthorisationCode:** Código alfanumerico de autorización asignado a la aprobación de la transacción + - **ConsumerLanguage:** El valor 0 indicará que no se ha determinado el idioma + - **Card_type:** Valores posibles: C - Crédito; D - Débito --- @@ -98,3 +98,28 @@ ``` > En el TPV de REDSYS no habrá que enviar una respuesta como en CECA, de todas maneras nosotros enviamos una cadena vacia + + +##### Devolución o reembolso de un pago. + +```python + def refund(self, operation_sale_code, refund_amount, description): +``` + +> Para ello se prepara una petición http con los siguientes campos: + + - **Ds_Merchant_Amount:** Indica el importe de la devolución + - **Ds_Merchant_Currency:** Indica el tipo de moneda a usar + - **Ds_Merchant_Order:** Indica el número de operacion, (que debe coincidir con el número de operación del pago realizado). + - **Ds_Merchant_ProductDescription:** Descripción de la devolución. + - **Ds_Merchant_MerchantCode:** Código FUC asignado al comercio + - **Ds_Merchant_UrlOK:** URL a la que se redirige al usuario en caso de operación OK (no es muy importante dado que al ser un proceso en backend no se llega a producir tal redicrección). + - **Ds_Merchant_UrlKO:** URL a la que se redirige al usuario en caso de que la operación NO haya sido satisfactoria, (misma circunstancia que la anterior) + - **Ds_Merchant_MerchantURL:** Obligatorio si se tiene confirmación online. + - **Ds_Merchant_ConsumerLanguage:** Indica el valor del idioma + - **Ds_Merchant_MerchantSignature:** Indica la firma generada por el comercio + - **Ds_Merchant_Terminal:** Indica el terminal + - **Ds_Merchant_SumTotal:** Representa la suma total de la devolución + - **Ds_Merchant_TransactionType:** Indica que tipo de transacción se utiliza, (para devoluciones usar **3**). + + \ No newline at end of file diff --git a/setup.py b/setup.py index 4014e9b..aae06d3 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ setup( name="django-virtual-pos", - version="1.6.4", + version="1.6.5", install_requires=[ "django", "beautifulsoup4", From 4602151244cbaafd358147d1d6932e828d8fbe06 Mon Sep 17 00:00:00 2001 From: Jose Miguel Lopez Date: Tue, 4 Apr 2017 14:18:19 +0200 Subject: [PATCH 15/72] Register VPOSRefundOperation in admin zone --- djangovirtualpos/admin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/djangovirtualpos/admin.py b/djangovirtualpos/admin.py index bc843fc..db09f1d 100644 --- a/djangovirtualpos/admin.py +++ b/djangovirtualpos/admin.py @@ -1,9 +1,10 @@ # coding=utf-8 from django.contrib import admin -from djangovirtualpos.models import VirtualPointOfSale, VPOSCeca, VPOSRedsys, VPOSSantanderElavon, VPOSPaypal +from djangovirtualpos.models import VirtualPointOfSale, VPOSRefundOperation, VPOSCeca, VPOSRedsys, VPOSSantanderElavon, VPOSPaypal admin.site.register(VirtualPointOfSale) +admin.site.register(VPOSRefundOperation) admin.site.register(VPOSCeca) admin.site.register(VPOSRedsys) admin.site.register(VPOSPaypal) From a608364cf1da90ced5170104569c3b49425ce3d2 Mon Sep 17 00:00:00 2001 From: Jose Miguel Lopez Date: Tue, 4 Apr 2017 14:41:23 +0200 Subject: [PATCH 16/72] Description param in refund example view --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c665fa5..6ceeeac 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ def payment_cancel(request, sale_code): ### Refund view ````python -def refund(request, tpv, payment_code, amount, ): +def refund(request, tpv, payment_code, amount, description): """ :param request: :param tpv: TPV Id From 2093a9dbb93c8791083fda39df6bcc44227b85f8 Mon Sep 17 00:00:00 2001 From: Jose Miguel Lopez Date: Tue, 4 Apr 2017 14:50:02 +0200 Subject: [PATCH 17/72] Add more comments about refund method --- djangovirtualpos/models.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index 0634619..5c7f991 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -521,9 +521,11 @@ def responseNok(self, extended_status=""): ## TODO: Se implementa solo para Redsys def refund(self, operation_sale_code, refund_amount, description): """ - 1. Realiza configuración necesaria para realiza una devolución. - - Antes de ello realiza las comprobaciones necesarias. + 1. Realiza las comprobaciones necesarias, para determinar si la operación es permitida, (en caso contrario se lanzan las correspondientes excepciones). + 2. Crea un objeto VPOSRefundOperation (con estado pendiente). + 3. Llama al delegado, que implementa las particularidades para la comunicación con el TPV concreto. + 4. Actualiza el estado del pago, según se encuentra 'parcialmente devuelto' o 'totalmente devuelto'. + 5. Actualiza el estado de la devolución a 'completada' o 'fallada'. @param operation_sale_code: Código del pago que pretendemos reembolsar. @param refund_amount: Cantidad del pago que reembolsamos From 6da30ed0b1f2e2a9a4bdf7cc78aded0f43b57c3a Mon Sep 17 00:00:00 2001 From: Jose Miguel Lopez Date: Tue, 4 Apr 2017 15:01:01 +0200 Subject: [PATCH 18/72] Add more comments about refund method --- djangovirtualpos/models.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index 5c7f991..c87619a 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -1437,6 +1437,18 @@ def responseNok(self, **kwargs): def refund(self, refund_amount, description): """ + Implementación particular del mátodo de devolución para el TPV de Redsys. + Se ocupa de preparar un mensaje http con los parámetros adecuados. + Realizar la comunicación con los parámetros dados y la codificación necesaria. + Interpretar la respuesta HTML, buscando etiquetas DOM que informen si la operación se realiza correctamente o con error. + + NOTA IMPORTANTE: La busqueda de etiquetas en el arbol DOM es sensible a posibles cambios en la plataforma Redsys, por lo tanto en caso + de no encontrar ninguna etiqueta de las posibles esperadas (noSePuedeRealizarOperacion o operacionAceptada), se lanza una excepción del tipo + 'VPOSOperationException'. + + Es responsibilidad del programador gestionar adecuadamente esta excepción desde la vista y en caso que se produzca, + avisar a los desarrolladores responsables del módulo 'DjangoVirtualPost' para su actualización. + :param payment_operation: Operación de pago asociada a la devolución. :param refund_amount: Cantidad de la devolución. :param description: Motivo o comentario de la devolución. From 9474911860339cf07602533fe457fea240fd7c35 Mon Sep 17 00:00:00 2001 From: Jose Miguel Lopez Date: Tue, 4 Apr 2017 15:01:41 +0200 Subject: [PATCH 19/72] Add more comments about refund method --- djangovirtualpos/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index c87619a..fcdfedf 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -521,7 +521,8 @@ def responseNok(self, extended_status=""): ## TODO: Se implementa solo para Redsys def refund(self, operation_sale_code, refund_amount, description): """ - 1. Realiza las comprobaciones necesarias, para determinar si la operación es permitida, (en caso contrario se lanzan las correspondientes excepciones). + 1. Realiza las comprobaciones necesarias, para determinar si la operación es permitida, + (en caso contrario se lanzan las correspondientes excepciones). 2. Crea un objeto VPOSRefundOperation (con estado pendiente). 3. Llama al delegado, que implementa las particularidades para la comunicación con el TPV concreto. 4. Actualiza el estado del pago, según se encuentra 'parcialmente devuelto' o 'totalmente devuelto'. From 2781fbcf3bc6e5179921bad85cd12414078618a3 Mon Sep 17 00:00:00 2001 From: Jose Miguel Lopez Date: Tue, 4 Apr 2017 15:03:17 +0200 Subject: [PATCH 20/72] Add more comments about refund method --- djangovirtualpos/models.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index fcdfedf..badc561 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -1441,14 +1441,16 @@ def refund(self, refund_amount, description): Implementación particular del mátodo de devolución para el TPV de Redsys. Se ocupa de preparar un mensaje http con los parámetros adecuados. Realizar la comunicación con los parámetros dados y la codificación necesaria. - Interpretar la respuesta HTML, buscando etiquetas DOM que informen si la operación se realiza correctamente o con error. + Interpretar la respuesta HTML, buscando etiquetas DOM que informen si la operación + se realiza correctamente o con error. - NOTA IMPORTANTE: La busqueda de etiquetas en el arbol DOM es sensible a posibles cambios en la plataforma Redsys, por lo tanto en caso - de no encontrar ninguna etiqueta de las posibles esperadas (noSePuedeRealizarOperacion o operacionAceptada), se lanza una excepción del tipo - 'VPOSOperationException'. + NOTA IMPORTANTE: La busqueda de etiquetas en el arbol DOM es sensible a posibles cambios en la plataforma Redsys, + por lo tanto en caso de no encontrar ninguna etiqueta de las posibles esperadas + (noSePuedeRealizarOperacion o operacionAceptada), se lanza una excepción del tipo 'VPOSOperationException'. - Es responsibilidad del programador gestionar adecuadamente esta excepción desde la vista y en caso que se produzca, - avisar a los desarrolladores responsables del módulo 'DjangoVirtualPost' para su actualización. + Es responsibilidad del programador gestionar adecuadamente esta excepción desde la vista + y en caso que se produzca, avisar a los desarrolladores responsables del módulo 'DjangoVirtualPost' + para su actualización. :param payment_operation: Operación de pago asociada a la devolución. :param refund_amount: Cantidad de la devolución. From 8ee500fe396ae7924915121f4d2f09b96c725d97 Mon Sep 17 00:00:00 2001 From: Jose Miguel Lopez Date: Tue, 4 Apr 2017 16:39:18 +0200 Subject: [PATCH 21/72] Forgotten add migrations --- .../migrations/0009_vpsrefundoperation.py | 39 +++++++++++++++++++ .../migrations/0010_auto_20170329_1227.py | 24 ++++++++++++ .../migrations/0011_auto_20170404_1424.py | 21 ++++++++++ 3 files changed, 84 insertions(+) create mode 100644 djangovirtualpos/migrations/0009_vpsrefundoperation.py create mode 100644 djangovirtualpos/migrations/0010_auto_20170329_1227.py create mode 100644 djangovirtualpos/migrations/0011_auto_20170404_1424.py diff --git a/djangovirtualpos/migrations/0009_vpsrefundoperation.py b/djangovirtualpos/migrations/0009_vpsrefundoperation.py new file mode 100644 index 0000000..954f019 --- /dev/null +++ b/djangovirtualpos/migrations/0009_vpsrefundoperation.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-03-28 16:08 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangovirtualpos', '0008_auto_20170321_1437'), + ] + + operations = [ + migrations.CreateModel( + name='VPOSRefundOperation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='Cantidad de la devoluci\xf3n')), + ('description', models.CharField(max_length=512, verbose_name='Descripci\xf3n de la devoluci\xf3n')), + ('operation_number', models.CharField(max_length=255, verbose_name='N\xfamero de operaci\xf3n')), + ('confirmation_code', models.CharField(max_length=255, null=True, verbose_name='C\xf3digo de confirmaci\xf3n enviado por el banco.')), + ('status', models.CharField(choices=[('completed', 'Completed'), ('failed', 'Failed')], max_length=64, verbose_name='Estado de la devoluci\xf3n')), + ('creation_datetime', models.DateTimeField(verbose_name='Fecha de creaci\xf3n del objeto')), + ('last_update_datetime', models.DateTimeField(verbose_name='Fecha de \xfaltima actualizaci\xf3n del objeto')), + ], + ), + migrations.AlterField( + model_name='vpospaymentoperation', + name='status', + field=models.CharField(choices=[('pending', 'Pending'), ('completed', 'Completed'), ('failed', 'Failed'), ('partially_refunded', 'Partially Refunded'), ('completely_refunded', 'Completely Refunded')], max_length=64, verbose_name='Estado del pago'), + ), + migrations.AddField( + model_name='vposrefundoperation', + name='payment', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='refund_operation', to='djangovirtualpos.VPOSPaymentOperation'), + ), + ] diff --git a/djangovirtualpos/migrations/0010_auto_20170329_1227.py b/djangovirtualpos/migrations/0010_auto_20170329_1227.py new file mode 100644 index 0000000..a693513 --- /dev/null +++ b/djangovirtualpos/migrations/0010_auto_20170329_1227.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-03-29 12:27 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangovirtualpos', '0009_vpsrefundoperation'), + ] + + operations = [ + migrations.RemoveField( + model_name='vposrefundoperation', + name='confirmation_code', + ), + migrations.AlterField( + model_name='vposrefundoperation', + name='status', + field=models.CharField(choices=[('pending', 'Pending'), ('completed', 'Completed'), ('failed', 'Failed')], max_length=64, verbose_name='Estado de la devoluci\xf3n'), + ), + ] diff --git a/djangovirtualpos/migrations/0011_auto_20170404_1424.py b/djangovirtualpos/migrations/0011_auto_20170404_1424.py new file mode 100644 index 0000000..d4f4c6f --- /dev/null +++ b/djangovirtualpos/migrations/0011_auto_20170404_1424.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-04 14:24 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangovirtualpos', '0010_auto_20170329_1227'), + ] + + operations = [ + migrations.AlterField( + model_name='vposrefundoperation', + name='payment', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='refund_operations', to='djangovirtualpos.VPOSPaymentOperation'), + ), + ] From f767c25944ff4d9ba68768998c03d1abe26d5769 Mon Sep 17 00:00:00 2001 From: Diego J Date: Tue, 9 May 2017 17:51:28 +0200 Subject: [PATCH 22/72] Move set_payment_attributes.js --- .../djangovirtualpos/payment}/set_payment_attributes.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename static/{django-virtual-post => js/djangovirtualpos/payment}/set_payment_attributes.js (100%) diff --git a/static/django-virtual-post/set_payment_attributes.js b/static/js/djangovirtualpos/payment/set_payment_attributes.js similarity index 100% rename from static/django-virtual-post/set_payment_attributes.js rename to static/js/djangovirtualpos/payment/set_payment_attributes.js From 2c078095f6770518d60c9dbca06cb68015127d8e Mon Sep 17 00:00:00 2001 From: Diego J Date: Wed, 10 May 2017 16:13:08 +0200 Subject: [PATCH 23/72] Set payment attributes JS --- .../payment/set_payment_attributes.js | 33 ++++++------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/static/js/djangovirtualpos/payment/set_payment_attributes.js b/static/js/djangovirtualpos/payment/set_payment_attributes.js index de6cb7c..205a387 100644 --- a/static/js/djangovirtualpos/payment/set_payment_attributes.js +++ b/static/js/djangovirtualpos/payment/set_payment_attributes.js @@ -1,10 +1,9 @@ $(document).ready(function(){ - /** Muestra un mensaje de error */ - + /** Show error message */ function show_error(message){ - console.log(message); + console.error(message); if(PNotify){ $(function(){ @@ -20,9 +19,7 @@ $(document).ready(function(){ } } - - /********************************************************************/ - /* Pago compra */ + /* Payment */ $(".pay_button").click(function(event){ $(".pay_button").attr("disabled", true); @@ -30,17 +27,15 @@ $(document).ready(function(){ var $this = $(this); - var url = vpos_constants.get("SET_PAYMENT_ATTRIBUTES_URL"); + var url = vpos_constants["SET_PAYMENT_ATTRIBUTES_URL"]; var $form = $(this).parents("form"); var post = { - payment_code: vpos_constants.get("PAYMENT_CODE"), - url_ok: vpos_constants.get("URL_OK"), - url_nok: vpos_constants.get("URL_NOK"), - tpv_id: $(this).attr("id").replace("tpv_button_","") - + payment_code: vpos_constants["PAYMENT_CODE"], + url_ok: vpos_constants["URL_OK"], + url_nok: vpos_constants["URL_NOK"], + vpos_id: $(this).data("id") }; - // Evitar que se pueda pulsar más de una vez sobre el botón if(this.clicked){ event.preventDefault(); @@ -56,16 +51,13 @@ $(document).ready(function(){ var $input; if("status" in data && data["status"]=="error"){ - show_error("No se ha podido generar el número de operación"); + show_error("Unable to create an operation number"); return false; } var formdata = data['data']; - // Para cada atributo generado por el servidor devuelto, - // lo asignamos. - // Esto incluye el número de operación y la firma - // y cualquier otro atributo generado. + // Assignment of each attribute generated by server $form.attr({ "action": data['action'], "method": data['method'] @@ -81,9 +73,6 @@ $(document).ready(function(){ $form.append($input); } - // Enviamos el formulario - - // No enviamos formulario "por el momento". $form.submit(); return false; @@ -91,6 +80,4 @@ $(document).ready(function(){ return false; }); - /********************************************************************/ - }); From 4eeebe2ac4d11b81de5dab6383a7fee399a701c6 Mon Sep 17 00:00:00 2001 From: Diego J Date: Wed, 10 May 2017 16:21:32 +0200 Subject: [PATCH 24/72] Confirm payment URL and view --- djangovirtualpos/urls.py | 14 ++++++++++ djangovirtualpos/views.py | 59 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 djangovirtualpos/urls.py create mode 100644 djangovirtualpos/views.py diff --git a/djangovirtualpos/urls.py b/djangovirtualpos/urls.py new file mode 100644 index 0000000..315a873 --- /dev/null +++ b/djangovirtualpos/urls.py @@ -0,0 +1,14 @@ + +from __future__ import unicode_literals + +from django.conf import settings +from django.conf.urls import url, include +from django.conf.urls.static import static +from django.contrib import admin + + +from views import confirm_payment + +urlpatterns = [ + url(r'^confirm/$', confirm_payment, name="confirm_payment") +] diff --git a/djangovirtualpos/views.py b/djangovirtualpos/views.py new file mode 100644 index 0000000..68adb19 --- /dev/null +++ b/djangovirtualpos/views.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +from django.core.exceptions import ObjectDoesNotExist +from django.db import transaction +from django.http import Http404, HttpResponseRedirect +from django.shortcuts import render, get_object_or_404 +from django.urls import reverse +from django.views.decorators.csrf import csrf_exempt +from djangovirtualpos.models import VirtualPointOfSale, VPOSCantCharge + + +from django.http import JsonResponse + +# Confirm sale +def confirm_payment(request, virtualpos_type, sale_model): + """ + This view will be called by the bank. + """ + + # Checking if the Point of Sale exists + virtual_pos = VirtualPointOfSale.receiveConfirmation(request, virtualpos_type=virtualpos_type) + + if not virtual_pos: + # The VPOS does not exist, inform the bank with a cancel + # response if needed + return VirtualPointOfSale.staticResponseNok(virtualpos_type) + + # Verify if bank confirmation is indeed from the bank + verified = virtual_pos.verifyConfirmation() + operation_number = virtual_pos.operation.operation_number + + with transaction.atomic(): + try: + # Getting your payment object from operation number + payment = sale_model.objects.get(operation_number=operation_number, status="pending") + except ObjectDoesNotExist: + return virtual_pos.responseNok("not_exists") + + if verified: + # Charge the money and answer the bank confirmation + try: + response = virtual_pos.charge() + # Implement the online_confirm method in your payment + # this method will mark this payment as paid and will + # store the payment date and time. + payment.online_confirm() + except VPOSCantCharge as e: + return virtual_pos.responseNok(extended_status=e) + except Exception as e: + return virtual_pos.responseNok("cant_charge") + + else: + # Payment could not be verified + # signature is not right + response = virtual_pos.responseNok("verification_error") + + return response \ No newline at end of file From a55fc14d1248833d2c93c5d013d9c45b3a34ae21 Mon Sep 17 00:00:00 2001 From: Diego J Date: Wed, 10 May 2017 16:55:11 +0200 Subject: [PATCH 25/72] Generic views --- djangovirtualpos/views.py | 66 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/djangovirtualpos/views.py b/djangovirtualpos/views.py index 68adb19..fa306e4 100644 --- a/djangovirtualpos/views.py +++ b/djangovirtualpos/views.py @@ -13,7 +13,73 @@ from django.http import JsonResponse + +def set_payment_attributes(request, sale_model, sale_ok_url, sale_nok_url): + """ + Set payment attributes for the form that makes the first call to the VPOS. + :param request: HttpRequest. + :param sale_model: Sale model. Must have "status" and "operation_number" attributes and "online_confirm" method. + :param sale_ok_url: Name of the URL used to redirect client when sale is successful. + :param sale_nok_url: Name of the URL used to redirect client when sale is not successful. + :return: HttpResponse. + """ + if request.method == 'GET': + return JsonResponse({"message":u"Method not valid."}) + + # Getting the VPOS and the Sale + try: + # Getting the VirtualPointOfSale object + virtual_point_of_sale = VirtualPointOfSale.get(id=request.POST["vpos_id"], is_erased=False) + # Getting Sale object + payment_code = request.POST["payment_code"] + sale = sale_model.objects.get(code=payment_code, status="pending") + sale.virtual_point_of_sale = virtual_point_of_sale + sale.save() + + except Sale.DoesNotExist as e: + return JsonResponse({"message":u"La orden de pago no ha sido previamente creada."}, status=404) + + except VirtualPointOfSale.DoesNotExist: + return JsonResponse({"message": u"VirtualPOS does NOT exist"}, status=404) + + virtual_point_of_sale.configurePayment( + # Payment amount + amount=sale.final_price, + # Payment description + description=sale.description, + # Sale code + sale_code=sale.code, + # Return URLs + url_ok=request.build_absolute_uri(reverse(sale_ok_url, kwargs={"sale_code": sale.code})), + url_nok=request.build_absolute_uri(reverse(sale_nok_url, kwargs={"sale_code": sale.code})), + ) + + # Operation number assignment. This operation number depends on the + # Virtual VPOS selected, it can be letters and numbers or numbers + # or even match with a specific pattern depending on the + # Virtual VPOS selected, remember. + try: + # Operation number generation and assignement + operation_number = virtual_point_of_sale.setupPayment() + # Update operation number of sale + sale.operation_number = operation_number + sale_model.objects.filter(id=sale.id).update(operation_number=operation_number) + except Exception as e: + return JsonResponse({"message": u"Error generating operation number {0}".format(e)}, status=500) + print("dsfadsfdsafdsa") + + # Payment form data + form_data = virtual_point_of_sale.getPaymentFormData() + + # Debug message + form_data["message"] = "Payment {0} updated. Returning payment attributes.".format(payment_code) + + # Return JSON response + return JsonResponse(form_data) + + # Confirm sale +@csrf_exempt def confirm_payment(request, virtualpos_type, sale_model): """ This view will be called by the bank. From fe34ee950faf191958aafea3d8c2eba0071ca4ae Mon Sep 17 00:00:00 2001 From: Diego J Date: Thu, 11 May 2017 14:49:49 +0200 Subject: [PATCH 26/72] Move set_payment_attributes to its correct path --- .../static}/js/djangovirtualpos/payment/set_payment_attributes.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {static => djangovirtualpos/static}/js/djangovirtualpos/payment/set_payment_attributes.js (100%) diff --git a/static/js/djangovirtualpos/payment/set_payment_attributes.js b/djangovirtualpos/static/js/djangovirtualpos/payment/set_payment_attributes.js similarity index 100% rename from static/js/djangovirtualpos/payment/set_payment_attributes.js rename to djangovirtualpos/static/js/djangovirtualpos/payment/set_payment_attributes.js From a082ffa75022ff916c51b3baaace73cd49de70e6 Mon Sep 17 00:00:00 2001 From: Diego J Date: Thu, 11 May 2017 15:50:07 +0200 Subject: [PATCH 27/72] Set payment attributes templatetag --- .../djangovirtualpos_js.html | 11 +++++++++ djangovirtualpos/templatetags/__init__.py | 0 .../templatetags/djangovirtualpos_js.py | 23 +++++++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 djangovirtualpos/templates/djangovirtualpos/templatetags/djangovirtualpos_js/djangovirtualpos_js.html create mode 100644 djangovirtualpos/templatetags/__init__.py create mode 100644 djangovirtualpos/templatetags/djangovirtualpos_js.py diff --git a/djangovirtualpos/templates/djangovirtualpos/templatetags/djangovirtualpos_js/djangovirtualpos_js.html b/djangovirtualpos/templates/djangovirtualpos/templatetags/djangovirtualpos_js/djangovirtualpos_js.html new file mode 100644 index 0000000..762c204 --- /dev/null +++ b/djangovirtualpos/templates/djangovirtualpos/templatetags/djangovirtualpos_js/djangovirtualpos_js.html @@ -0,0 +1,11 @@ +{% load staticfiles %} + + + + \ No newline at end of file diff --git a/djangovirtualpos/templatetags/__init__.py b/djangovirtualpos/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/djangovirtualpos/templatetags/djangovirtualpos_js.py b/djangovirtualpos/templatetags/djangovirtualpos_js.py new file mode 100644 index 0000000..5d3fb96 --- /dev/null +++ b/djangovirtualpos/templatetags/djangovirtualpos_js.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals +from django import template +from django.utils import timezone +from django.template import loader, Context + +register = template.Library() + + +@register.simple_tag +def djangovirtualpos_set_payment_attributes_js(set_payment_attributes_url, sale_code, url_ok, url_nok): + t = loader.get_template("djangovirtualpos/templatetags/djangovirtualpos_js/djangovirtualpos_js.html") + replacements = { + "url_set_payment_attributes": set_payment_attributes_url, + "sale_code": sale_code, + "url_ok": url_ok, + "url_nok": url_nok + } + try: + return t.render(Context(replacements)) + except TypeError: + return t.render(replacements) \ No newline at end of file From 02d644d57e995a4e0bb6b74b1ea7ae1ab3e60035 Mon Sep 17 00:00:00 2001 From: Diego J Date: Thu, 11 May 2017 15:55:26 +0200 Subject: [PATCH 28/72] Set payment attributes templatetag --- .../js/djangovirtualpos/payment/set_payment_attributes.js | 5 +++++ djangovirtualpos/templatetags/djangovirtualpos_js.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/djangovirtualpos/static/js/djangovirtualpos/payment/set_payment_attributes.js b/djangovirtualpos/static/js/djangovirtualpos/payment/set_payment_attributes.js index 205a387..cff8a81 100644 --- a/djangovirtualpos/static/js/djangovirtualpos/payment/set_payment_attributes.js +++ b/djangovirtualpos/static/js/djangovirtualpos/payment/set_payment_attributes.js @@ -1,3 +1,8 @@ +if(!$){ + console.error("django-virtual-pos needs jQuery"); + return; +} + $(document).ready(function(){ /** Show error message */ diff --git a/djangovirtualpos/templatetags/djangovirtualpos_js.py b/djangovirtualpos/templatetags/djangovirtualpos_js.py index 5d3fb96..d47ac9b 100644 --- a/djangovirtualpos/templatetags/djangovirtualpos_js.py +++ b/djangovirtualpos/templatetags/djangovirtualpos_js.py @@ -9,7 +9,7 @@ @register.simple_tag -def djangovirtualpos_set_payment_attributes_js(set_payment_attributes_url, sale_code, url_ok, url_nok): +def include_djangovirtualpos_set_payment_attributes_js(set_payment_attributes_url, sale_code, url_ok, url_nok): t = loader.get_template("djangovirtualpos/templatetags/djangovirtualpos_js/djangovirtualpos_js.html") replacements = { "url_set_payment_attributes": set_payment_attributes_url, From 71a5c261f6ce627c82bdbd4fc87f8a420658df34 Mon Sep 17 00:00:00 2001 From: Diego J Date: Thu, 11 May 2017 15:57:27 +0200 Subject: [PATCH 29/72] Set payment attributes templatetag --- .../js/djangovirtualpos/payment/set_payment_attributes.js | 5 ----- .../djangovirtualpos_js/djangovirtualpos_js.html | 3 +++ 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/djangovirtualpos/static/js/djangovirtualpos/payment/set_payment_attributes.js b/djangovirtualpos/static/js/djangovirtualpos/payment/set_payment_attributes.js index cff8a81..205a387 100644 --- a/djangovirtualpos/static/js/djangovirtualpos/payment/set_payment_attributes.js +++ b/djangovirtualpos/static/js/djangovirtualpos/payment/set_payment_attributes.js @@ -1,8 +1,3 @@ -if(!$){ - console.error("django-virtual-pos needs jQuery"); - return; -} - $(document).ready(function(){ /** Show error message */ diff --git a/djangovirtualpos/templates/djangovirtualpos/templatetags/djangovirtualpos_js/djangovirtualpos_js.html b/djangovirtualpos/templates/djangovirtualpos/templatetags/djangovirtualpos_js/djangovirtualpos_js.html index 762c204..cf33cd0 100644 --- a/djangovirtualpos/templates/djangovirtualpos/templatetags/djangovirtualpos_js/djangovirtualpos_js.html +++ b/djangovirtualpos/templates/djangovirtualpos/templatetags/djangovirtualpos_js/djangovirtualpos_js.html @@ -6,6 +6,9 @@ vpos_constants["PAYMENT_CODE"] = "{{sale_code}}"; vpos_constants["URL_OK"] = "{{url_ok}}"; vpos_constants["URL_NOK"] = "{{url_nok}}"; + if(!$){ + console.error("django-virtual-pos needs jQuery and it hasn't found it"); + } \ No newline at end of file From b2185e311ed981fa592fc607ab76df59af774349 Mon Sep 17 00:00:00 2001 From: Diego J Date: Thu, 11 May 2017 16:12:59 +0200 Subject: [PATCH 30/72] Fixing bad exception in view --- djangovirtualpos/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangovirtualpos/views.py b/djangovirtualpos/views.py index fa306e4..8c2c8fd 100644 --- a/djangovirtualpos/views.py +++ b/djangovirtualpos/views.py @@ -36,7 +36,7 @@ def set_payment_attributes(request, sale_model, sale_ok_url, sale_nok_url): sale.virtual_point_of_sale = virtual_point_of_sale sale.save() - except Sale.DoesNotExist as e: + except ObjectDoesNotExist as e: return JsonResponse({"message":u"La orden de pago no ha sido previamente creada."}, status=404) except VirtualPointOfSale.DoesNotExist: From 48ad9fd657d1fffb21267bbbdf82618a9d342584 Mon Sep 17 00:00:00 2001 From: Diego J Date: Thu, 11 May 2017 16:31:44 +0200 Subject: [PATCH 31/72] Preparing for new pypi version --- CHANGES.md | 4 ++++ README.md | 8 +++++++- setup.py | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7a129bb..2a4048b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,10 @@ Django module that abstracts the flow of several virtual points of sale includin # Releases +## 1.6.6 +- Simplify integration. +- Add example integration. + ## 1.6.5 - Add method to allow partial refund and full refund, specific to Redsys TPV Platform. - New model to register refund operations. diff --git a/README.md b/README.md index 6ceeeac..5c33f2f 100644 --- a/README.md +++ b/README.md @@ -86,12 +86,15 @@ You will need to implement this skeleton view using your own **Payment** model. This model has must have at least the following attributes: - **code**: sale code given by our system. - **operation_number**: bank operation number. - - **status**: status of the payment: "paid", "pending" or "canceled". + - **status**: status of the payment: "paid", "pending" (**pending** is mandatory) or "canceled". - **amount**: amount to be charged. And the following methods: - **online_confirm**: mark the payment as paid. +## Integration examples +- [djshop](https://github.com/diegojromerolopez/djshop) + ## Needed views ### Sale summary view @@ -124,6 +127,9 @@ def payment_confirmation(request, virtualpos_type): """ This view will be called by the bank. """ + # Directly call to confirm_payment view + + # Or implement the following actions # Checking if the Point of Sale exists virtual_pos = VirtualPointOfSale.receiveConfirmation(request, virtualpos_type=virtualpos_type) diff --git a/setup.py b/setup.py index aae06d3..f8edbad 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ setup( name="django-virtual-pos", - version="1.6.5", + version="1.6.6", install_requires=[ "django", "beautifulsoup4", From 758d7d7c8c3c44cc18b66fef5a938021c478f847 Mon Sep 17 00:00:00 2001 From: Diego J Date: Wed, 17 May 2017 14:38:22 +0200 Subject: [PATCH 32/72] Fixing regression --- djangovirtualpos/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangovirtualpos/views.py b/djangovirtualpos/views.py index 8c2c8fd..ba32ce7 100644 --- a/djangovirtualpos/views.py +++ b/djangovirtualpos/views.py @@ -44,7 +44,7 @@ def set_payment_attributes(request, sale_model, sale_ok_url, sale_nok_url): virtual_point_of_sale.configurePayment( # Payment amount - amount=sale.final_price, + amount=sale.amount, # Payment description description=sale.description, # Sale code From 49237fb5a4ea410660263d06aaaa1b5fb6c83137 Mon Sep 17 00:00:00 2001 From: Diego J Date: Tue, 23 May 2017 15:08:24 +0200 Subject: [PATCH 33/72] Payment by reference --- djangovirtualpos/models.py | 31 +++++++++++++++++++++++++++---- djangovirtualpos/views.py | 12 ++++++++---- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index badc561..e60c041 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -358,7 +358,7 @@ def configurePayment(self, amount, description, url_ok, url_nok, sale_code): """ if type(amount) == int or type(amount) == Decimal: amount = float(amount) - if type(amount) != float or amount <= 0.0: + if type(amount) != float or amount < 0.0: raise ValueError(u"La cantidad debe ser un flotante positivo") if sale_code is None or sale_code == "": raise ValueError(u"El código de venta no puede estar vacío") @@ -424,10 +424,10 @@ def setupPayment(self): ## Paso 1.3. Obtiene los datos de pago ## Este método será el que genere los campos del formulario de pago ## que se rellenarán desde el cliente (por Javascript) - def getPaymentFormData(self): + def getPaymentFormData(self, *args, **kwargs): if self.operation.operation_number is None: raise Exception(u"No se ha generado el número de operación, ¿ha llamado a vpos.setupPayment antes?") - data = self.delegated.getPaymentFormData() + data = self.delegated.getPaymentFormData(*args, **kwargs) data["type"] = self.type return data @@ -1056,6 +1056,8 @@ class VPOSRedsys(VirtualPointOfSale): "9999": u"Operación que ha sido redirigida al emisor a autenticar.", } + ALLOW_PAYMENT_BY_REFERENCE = True + # El TPV de RedSys consta de dos entornos en funcionamiento, uno para pruebas y otro para producción REDSYS_URL = { "production": "https://sis.redsys.es/sis/realizarPago", @@ -1120,6 +1122,8 @@ def configurePayment(self, **kwargs): # Formato para Importe: según redsys, ha de tener un formato de entero positivo, con las dos últimas posiciones # ocupadas por los decimales self.importe = "{0:.2f}".format(float(self.parent.operation.amount)).replace(".", "") + if self.importe == "000": + self.importe = "0" # Idioma de la pasarela, por defecto es español, tomamos # el idioma actual y le asignamos éste @@ -1159,7 +1163,7 @@ def setupPayment(self, operation_number=None, code_len=12): ## Paso 1.3. Obtiene los datos de pago ## Este método será el que genere los campos del formulario de pago ## que se rellenarán desde el cliente (por Javascript) - def getPaymentFormData(self): + def getPaymentFormData(self, reference_number=False): order_data = { # Indica el importe de la venta "DS_MERCHANT_AMOUNT": self.importe, @@ -1198,6 +1202,20 @@ def getPaymentFormData(self): "DS_MERCHANT_SUMTOTAL": self.importe, } + # En caso de que tenga referencia + if reference_number: + # Puede ser una petición de referencia + if reference_number.lower() == "request": + order_data["DS_MERCHANT_IDENTIFIER"] = "REQUIRED" + order_data["DS_MERCHANT_MERCHANTURL"] += "?request_reference=1" + # o en cambio puede ser el envío de una referencia obtenida antes + else: + order_data["DS_MERCHANT_IDENTIFIER"] = reference_number + + print("order data") + print(order_data) + print("END order data") + json_order_data = json.dumps(order_data) packed_order_data = base64.b64encode(json_order_data) @@ -1242,6 +1260,8 @@ def _receiveConfirmationHTTPPOST(request): # Almacén de operaciones try: operation_data = json.loads(base64.b64decode(request.POST.get("Ds_MerchantParameters"))) + dlprint(operation_data) + # Operation number operation_number = operation_data.get("Ds_Order") operation = VPOSPaymentOperation.objects.get(operation_number=operation_number) operation.confirmation_data = {"GET": request.GET.dict(), "POST": request.POST.dict()} @@ -1261,6 +1281,9 @@ def _receiveConfirmationHTTPPOST(request): # Iniciamos los valores recibidos en el delegado + # Datos de la operación al completo + vpos.delegated.ds_merchantparameters = operation_data + ## Datos que llegan por POST # Firma enviada por RedSys, que más tarde compararemos con la generada por el comercio vpos.delegated.firma = request.POST.get("Ds_Signature") diff --git a/djangovirtualpos/views.py b/djangovirtualpos/views.py index ba32ce7..6cb1258 100644 --- a/djangovirtualpos/views.py +++ b/djangovirtualpos/views.py @@ -14,7 +14,7 @@ from django.http import JsonResponse -def set_payment_attributes(request, sale_model, sale_ok_url, sale_nok_url): +def set_payment_attributes(request, sale_model, sale_ok_url, sale_nok_url, reference_number=False): """ Set payment attributes for the form that makes the first call to the VPOS. :param request: HttpRequest. @@ -66,10 +66,14 @@ def set_payment_attributes(request, sale_model, sale_ok_url, sale_nok_url): sale_model.objects.filter(id=sale.id).update(operation_number=operation_number) except Exception as e: return JsonResponse({"message": u"Error generating operation number {0}".format(e)}, status=500) - print("dsfadsfdsafdsa") # Payment form data - form_data = virtual_point_of_sale.getPaymentFormData() + if hasattr(reference_number, "lower") and reference_number.lower() == "request": + form_data = virtual_point_of_sale.getPaymentFormData(reference_number="request") + elif reference_number: + form_data = virtual_point_of_sale.getPaymentFormData(reference_number=reference_number) + else: + form_data = virtual_point_of_sale.getPaymentFormData(reference_number=False) # Debug message form_data["message"] = "Payment {0} updated. Returning payment attributes.".format(payment_code) @@ -84,7 +88,6 @@ def confirm_payment(request, virtualpos_type, sale_model): """ This view will be called by the bank. """ - # Checking if the Point of Sale exists virtual_pos = VirtualPointOfSale.receiveConfirmation(request, virtualpos_type=virtualpos_type) @@ -111,6 +114,7 @@ def confirm_payment(request, virtualpos_type, sale_model): # Implement the online_confirm method in your payment # this method will mark this payment as paid and will # store the payment date and time. + payment.virtual_pos = virtual_pos payment.online_confirm() except VPOSCantCharge as e: return virtual_pos.responseNok(extended_status=e) From cbccab8cc521eb90c7281ac1730afb882d8f4b3c Mon Sep 17 00:00:00 2001 From: Diego J Date: Tue, 23 May 2017 15:17:27 +0200 Subject: [PATCH 34/72] Payment by reference --- djangovirtualpos/models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index e60c041..5e3244d 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -1207,15 +1207,14 @@ def getPaymentFormData(self, reference_number=False): # Puede ser una petición de referencia if reference_number.lower() == "request": order_data["DS_MERCHANT_IDENTIFIER"] = "REQUIRED" - order_data["DS_MERCHANT_MERCHANTURL"] += "?request_reference=1" + if "?" in order_data["DS_MERCHANT_MERCHANTURL"]: + order_data["DS_MERCHANT_MERCHANTURL"] += "&request_reference=1" + else: + order_data["DS_MERCHANT_MERCHANTURL"] += "?request_reference=1" # o en cambio puede ser el envío de una referencia obtenida antes else: order_data["DS_MERCHANT_IDENTIFIER"] = reference_number - print("order data") - print(order_data) - print("END order data") - json_order_data = json.dumps(order_data) packed_order_data = base64.b64encode(json_order_data) @@ -1282,6 +1281,7 @@ def _receiveConfirmationHTTPPOST(request): # Iniciamos los valores recibidos en el delegado # Datos de la operación al completo + # Usado para recuperar los datos la referencia vpos.delegated.ds_merchantparameters = operation_data ## Datos que llegan por POST From e8e200e43fb5194f9f3604a613473d42ac56cfaf Mon Sep 17 00:00:00 2001 From: Diego J Date: Thu, 25 May 2017 15:55:55 +0200 Subject: [PATCH 35/72] REDSYS integration with SOAP confirmation method with reference number --- djangovirtualpos/models.py | 9 +++++++++ .../djangovirtualpos/payment/set_payment_attributes.js | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index 5e3244d..c655df3 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -1363,6 +1363,15 @@ def _receiveConfirmationSOAP(request): # Código que indica el tipo de transacción vpos.delegated.ds_response = root.xpath("//Message/Request/Ds_Response/text()")[0] + # Usado para recuperar los datos la referencia + vpos.delegated.ds_merchantparameters = {} + try: + vpos.delegated.ds_merchantparameters["Ds_Merchant_Identifier"] = root.xpath("//Message/Request/Ds_Merchant_Identifier/text()")[0] + vpos.delegated.ds_merchantparameters["Ds_ExpiryDate"] = root.xpath("//Message/Request/Ds_ExpiryDate/text()")[0] + # Aquí la idea es incluir más parámetros que nos puedan servir en el llamador de este módulo + except IndexError: + pass + return vpos.delegated #################################################################### diff --git a/djangovirtualpos/static/js/djangovirtualpos/payment/set_payment_attributes.js b/djangovirtualpos/static/js/djangovirtualpos/payment/set_payment_attributes.js index 205a387..21e4c59 100644 --- a/djangovirtualpos/static/js/djangovirtualpos/payment/set_payment_attributes.js +++ b/djangovirtualpos/static/js/djangovirtualpos/payment/set_payment_attributes.js @@ -36,6 +36,11 @@ $(document).ready(function(){ vpos_id: $(this).data("id") }; + // Reference number + if($this.data("reference_number")){ + post["reference_number"] = $this.data("reference_number"); + } + // Evitar que se pueda pulsar más de una vez sobre el botón if(this.clicked){ event.preventDefault(); From bfca6d60b34743f0ca1b5f93b02214bab9641ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20J=2E=20Barch=C3=A9in=20Molina?= Date: Fri, 9 Feb 2018 12:14:43 +0100 Subject: [PATCH 36/72] Add DS_ERROR_CODE logging, default message for unknown Ds_Response, allow SOAP responses with empty Ds_AuthorisationCode [backported from 'intellitickets' project] --- djangovirtualpos/models.py | 500 ++++++++++++++++++++++++++++++++++++- setup.cfg | 0 setup.py | 10 +- 3 files changed, 502 insertions(+), 8 deletions(-) mode change 100755 => 100644 setup.cfg diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index c655df3..75435f9 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -1056,6 +1056,461 @@ class VPOSRedsys(VirtualPointOfSale): "9999": u"Operación que ha sido redirigida al emisor a autenticar.", } + # Códigos de error SISxxxx + DS_ERROR_CODES = { + 'SIS0001': u'Error en la generación de HTML', + 'SIS0002': u'Error al generar el XML de la clase de datos', + 'SIS0003': u'Error al crear el gestor de mensajes price', + 'SIS0004': u'Error al montar el mensaje para pago móvil', + 'SIS0005': u'Error al desmontar la respuesta de un pago móvil', + 'SIS0006': u'Error al provocar un ROLLBACK de una transacción', + 'SIS0007': u'Error al desmontar XML', + 'SIS0008': u'Error falta Ds_Merchant_MerchantCode ', + 'SIS0009': u'Error de formato en Ds_Merchant_MerchantCode', + 'SIS0010': u'Error falta Ds_Merchant_Terminal', + 'SIS0011': u'Error de formato en Ds_Merchant_Terminal', + 'SIS0012': u'Error, no se pudo crear el componente de conexión con Stratus', + 'SIS0013': u'Error, no se pudo cerrar el componente de conexión con Stratus', + 'SIS0014': u'Error de formato en Ds_Merchant_Order', + 'SIS0015': u'Error falta Ds_Merchant_Currency', + 'SIS0016': u'Error de formato en Ds_Merchant_Currency', + 'SIS0017': u'Error no se admiten operaciones en pesetas -- DEPRECATED !!!!', + 'SIS0018': u'Error falta Ds_Merchant_Amount', + 'SIS0019': u'Error de formato en Ds_Merchant_Amount', + 'SIS0020': u'Error falta Ds_Merchant_MerchantSignature', + 'SIS0021': u'Error la Ds_Merchant_MerchantSignature viene vacía', + 'SIS0022': u'Error de formato en Ds_Merchant_TransactionType', + 'SIS0023': u'Error Ds_Merchant_TransactionType desconocido. Pago Adicional: Si no se permite pago Adicional (porque el comercio no es de la Entidad o no hay pago adicional en métodos de pago -> SIS0023 Transation type invalido)', + 'SIS0024': u'Error Ds_Merchant_ConsumerLanguage tiene mas de 3 posiciones', + 'SIS0025': u'Error de formato en Ds_Merchant_ConsumerLanguage', + 'SIS0026': u'Error No existe el comercio / terminal enviado en TZF', + 'SIS0027': u'Error Moneda enviada por el comercio es diferente a la de la TZF', + 'SIS0028': u'Error Comercio / terminal está dado de baja', + 'SIS0029': u'Error al montar el mensaje para pago con tarjeta', + 'SIS0030': u'Error en un pago con tarjeta ha llegado un tipo de operación que no es ni pago ni preautorización', + 'SIS0031': u'Método de pago no definido', + 'SIS0032': u'Error al montar el mensaje para una devolución', + 'SIS0033': u'Error en un pago con móvil ha llegado un tipo de operación que no es ni pago ni preautorización', + 'SIS0034': u'Error de acceso a la base de datos', + 'SIS0035': u'Error al recuperar los datos de la sesión desde un XML', + 'SIS0036': u'Error al tomar los datos para Pago Móvil desde el XML', + 'SIS0037': u'El número de teléfono no es válido', + 'SIS0038': u'Error en java (errores varios)', + 'SIS0039': u'Error al tomar los datos para Pago Tarjeta desde el XML', + 'SIS0040': u'Error el comercio / terminal no tiene ningún método de pago asignado', + 'SIS0041': u'Error en el cálculo de la HASH de datos del comercio.', + 'SIS0042': u'La firma enviada no es correcta', + 'SIS0043': u'Error al realizar la notificación on-line', + 'SIS0044': u'Error al tomar los datos para Pago Finanet desde el XML', + 'SIS0045': u'Error al montar el mensaje para pago Finanet', + 'SIS0046': u'El bin de la tarjeta no está dado de alta en FINANET', + 'SIS0047': u'Error al montar el mensaje para preautorización móvil', + 'SIS0048': u'Error al montar el mensaje para preautorización tarjeta', + 'SIS0049': u'Error al montar un mensaje de anulación', + 'SIS0050': u'Error al montar un mensaje de repetición de anulación', + 'SIS0051': u'Error número de pedido repetido', + 'SIS0052': u'Error al montar el mensaje para una confirmación', + 'SIS0053': u'Error al montar el mensaje para una preautenticación por referencia', + 'SIS0054': u'Error no existe operación sobre la que realizar la devolución', + 'SIS0055': u'Error existe más de un pago con el mismo número de pedido', + 'SIS0056': u'La operación sobre la que se desea devolver no está autorizada', + 'SIS0057': u'El importe a devolver supera el permitido', + 'SIS0058': u'Inconsistencia de datos, en la validación de una confirmación ', + 'SIS0059': u'Error no existe operación sobre la que realizar la confirmación', + 'SIS0060': u'Ya existe una confirmación asociada a la preautorización', + 'SIS0061': u'La preautorización sobre la que se desea confirmar no está autorizada', + 'SIS0062': u'El importe a confirmar supera el permitido', + 'SIS0063': u'Error. Número de tarjeta no disponible', + 'SIS0064': u'Error. Número de posiciones de la tarjeta incorrecto', + 'SIS0065': u'Error. El número de tarjeta no es numérico', + 'SIS0066': u'Error. Mes de caducidad no disponible', + 'SIS0067': u'Error. El mes de la caducidad no es numérico', + 'SIS0068': u'Error. El mes de la caducidad no es válido', + 'SIS0069': u'Error. Año de caducidad no disponible', + 'SIS0070': u'Error. El Año de la caducidad no es numérico', + 'SIS0071': u'Tarjeta caducada', + 'SIS0072': u'Operación no anulable', + 'SIS0073': u'Error al analizar la respuesta de una anulación', + 'SIS0074': u'Error falta Ds_Merchant_Order', + 'SIS0075': u'Error el Ds_Merchant_Order tiene menos de 4 posiciones o más de 12 (Para algunas operativas el límite es 10 en lugar de 12)', + 'SIS0076': u'Error el Ds_Merchant_Order no tiene las cuatro primeras posiciones numéricas', + 'SIS0077': u'Error de formato en Ds_Merchant_Order', + 'SIS0078': u'Método de pago no disponible', + 'SIS0079': u'Error en realizar pago tarjeta', + 'SIS0080': u'Error al tomar los datos para Pago tarjeta desde el XML', + 'SIS0081': u'La sesión es nueva, se han perdido los datos almacenados', + 'SIS0082': u'Error procesando operaciones pendientes en el arranque', + 'SIS0083': u'El sistema no está arrancado (Se está arrancado)', + 'SIS0084': u'El valor de Ds_Merchant_Conciliation es nulo', + 'SIS0085': u'El valor de Ds_Merchant_Conciliation no es numérico', + 'SIS0086': u'El valor de Ds_Merchant_Conciliation no ocupa 6 posiciones', + 'SIS0087': u'El valor de Ds_Merchant_Session es nulo', + 'SIS0088': u'El valor de Ds_Merchant_Session no es numérico', + 'SIS0089': u'El valor de caducidad no ocupa 4 posiciones', + 'SIS0090': u'El valor del ciers representado de BBVA es nulo', + 'SIS0091': u'El valor del ciers representado de BBVA no es numérico', + 'SIS0092': u'El valor de caducidad es nulo', + 'SIS0093': u'Tarjeta no encontrada en la tabla de rangos', + 'SIS0094': u'La tarjeta no fue autenticada como 3D Secure', + 'SIS0095': u'Error al intentar validar la tarjeta como 3DSecure', + 'SIS0096': u'El formato utilizado para los datos 3DSecure es incorrecto', + 'SIS0097': u'Valor del campo Ds_Merchant_CComercio no válido', + 'SIS0098': u'Valor del campo Ds_Merchant_CVentana no válido', + 'SIS0099': u'Error al desmontar los datos para Pago 3D Secure desde el XML', + 'SIS0100': u'Error al desmontar los datos para PagoPIN desde el XML', + 'SIS0101': u'Error al desmontar los datos para PantallaPIN desde el XML', + 'SIS0102': u'Error No se recibió el resultado de la autenticación', + 'SIS0103': u'Error Mandando SisMpiTransactionRequestMessage al Merchant Plugin', + 'SIS0104': u'Error calculando el bloque de PIN', + 'SIS0105': u'Error, la referencia es nula o vacía', + 'SIS0106': u'Error al montar los datos para RSisPantallaSPAUCAF.xsl', + 'SIS0107': u'Error al desmontar los datos para PantallaSPAUCAF desde el XML', + 'SIS0108': u'Error al desmontar los datos para pagoSPAUCAF desde el XML', + 'SIS0109': u'Error El número de tarjeta no se corresponde con el seleccionado originalmente ', + 'SIS0110': u'Error La fecha de caducidad de la tarjeta no se corresponde con el seleccionado originalmente', + 'SIS0111': u'Error El campo Ucaf_Authentication_Data no tiene la longitud requerida', + 'SIS0112': u'Error El tipo de transacción especificado en Ds_Merchant_Transaction_Type no está permitido', + 'SIS0113': u'Excepción producida en el servlet de operaciones', + 'SIS0114': u'Error, se ha llamado con un GET al servlet de operaciones', + 'SIS0115': u'Error no existe operación sobre la que realizar el pago de la cuota', + 'SIS0116': u'La operación sobre la que se desea pagar una cuota no es una operación válida', + 'SIS0117': u'La operación sobre la que se desea pagar una cuota no está autorizada', + 'SIS0118': u'Se ha excedido el importe total de las cuotas', + 'SIS0119': u'Valor del campo Ds_Merchant_DateFrecuency no válido', + 'SIS0120': u'Valor del campo Ds_Merchant_ChargeExpiryDate no válido', + 'SIS0121': u'Valor del campo Ds_Merchant_SumTotal no válido', + 'SIS0122': u'Error en formato numérico. Antiguo Valor del campo Ds_Merchant_DateFrecuency o no Ds_Merchant_SumTotal tiene formato incorrecto', + 'SIS0123': u'Se ha excedido la fecha tope para realizar transacciones', + 'SIS0124': u'No ha transcurrido la frecuencia mínima en un pago recurrente sucesivo', + 'SIS0125': u'Error en código java validando cuota', + 'SIS0126': u'Error la operación no se puede marcar como pendiente', + 'SIS0127': u'Error la generando datos Url OK CANCEL', + 'SIS0128': u'Error se quiere generar una anulación sin p2', + 'SIS0129': u'Error, se ha detectado un intento masivo de peticiones desde la ip', + 'SIS0130': u'Error al regenerar el mensaje', + 'SIS0131': u'Error en la firma de los datos del SAS', + 'SIS0132': u'La fecha de Confirmación de Autorización no puede superar en más de 7 días a la de Preautorización.', + 'SIS0133': u'La fecha de Confirmación de Autenticación no puede superar en más de 45 días a la de Autenticación Previa.', + 'SIS0134': u'El valor del Ds_MerchantCiers enviado por BBVA no es válido', + 'SIS0135': u'Error generando un nuevo valor para el IDETRA', + 'SIS0136': u'Error al montar el mensaje de notificación', + 'SIS0137': u'Error al intentar validar la tarjeta como 3DSecure NACIONAL', + 'SIS0138': u'Error debido a que existe una Regla del ficheros de reglas que evita que se produzca la Autorización', + 'SIS0139': u'Error el pago recurrente inicial está duplicado', + 'SIS0140': u'Error al interpretar la respuesta de Stratus para una preautenticación por referencia', + 'SIS0141': u'Error formato no correcto para 3DSecure', + 'SIS0142': u'Tiempo excedido para el pago', + 'SIS0143': u'No viene el campo laOpcion en el formulario enviado', + 'SIS0144': u'El campo laOpcion recibido del formulario tiene un valor desconocido para el servlet', + 'SIS0145': u'Error al montar el mensaje para P2P', + 'SIS0146': u'Transacción P2P no reconocida', + 'SIS0147': u'Error al tomar los datos para Pago P2P desde el XML', + 'SIS0148': u'Método de pago no disponible o no válido para P2P', + 'SIS0149': u'Error al obtener la referencia para operación P2P', + 'SIS0150': u'Error al obtener la clave para operación P2P', + 'SIS0151': u'Error al generar un objeto desde el XML', + 'SIS0152': u'Error en operación P2P. Se carece de datos', + 'SIS0153': u'Error, el número de días de operación P2P no es correcto', + 'SIS0154': u'Error el mail o el teléfono de T2 son obligatorios (operación P2P)', + 'SIS0155': u'Error obteniendo datos de operación P2P', + 'SIS0156': u'Error la operación no es P2P Tipo 3', + 'SIS0157': u'Error no se encuentra la operación P2P original', + 'SIS0158': u'Error, la operación P2P original no está en el estado correcto', + 'SIS0159': u'Error, la clave de control de operación P2P no es válida ', + 'SIS0160': u'Error al tomar los datos para una operación P2P tipo 3', + 'SIS0161': u'Error en el envío de notificación P2P', + 'SIS0162': u'Error tarjeta de carga micropago no tiene pool asociado', + 'SIS0163': u'Error tarjeta de carga micropago no autenticable', + 'SIS0164': u'Error la recarga para micropagos sólo permite euros', + 'SIS0165': u'Error la T1 de la consulta no coincide con la de la operación P2P original', + 'SIS0166': u'Error el nombre del titular de T1 es obligatorio', + 'SIS0167': u'Error la operación está bloqueada por superar el número de intentos fallidosde introducción del código por parte de T2', + 'SIS0168': u'No existe terminal AMEX asociada', + 'SIS0169': u'Valor PUCE Ds_Merchant_MatchingData no válido', + 'SIS0170': u'Valor PUCE Ds_Acquirer_Identifier no válido', + 'SIS0171': u'Valor PUCE Ds_Merchant_Csb no válido', + 'SIS0172': u'Valor PUCE Ds_Merchant_MerchantCode no válido', + 'SIS0173': u'Valor PUCE Ds_Merchant_UrlOK no válido', + 'SIS0174': u'Error calculando el resultado PUCE', + 'SIS0175': u'Error al montar el mensaje PUCE', + 'SIS0176': u'Error al tratar el mensaje de petición P2P procedente de Stratus.', + 'SIS0177': u'Error al descomponer el mensaje de Envío de fondos en una operación P2P iniciada por Stratus.', + 'SIS0178': u'Error al montar el XML con los datos de envío para una operación P2P', + 'SIS0179': u'Error P2P Móvil, el teléfono no tiene asociada tarjeta', + 'SIS0180': u'El telecode es nulo o vacía para operación P2P', + 'SIS0181': u'Error al montar el XML con los datos recibidos', + 'SIS0182': u'Error al montar el mensaje PRICE / Error al tratar el mensaje de petición Cobro de Recibo', + 'SIS0183': u'Error al montar el XML de respuesta', + 'SIS0184': u'Error al tratar el XML de Recibo', + 'SIS0186': u'Error en entrada Banco Sabadell. Faltan datos', + 'SIS0187': u'Error al montar el mensaje de respuesta a Stratus (Error Formato)', + 'SIS0188': u'Error al desmontar el mensaje price en una petición P2P procedente de Stratus', + 'SIS0190': u'Error al intentar mandar el mensaje SMS', + 'SIS0191': u'Error, El mail del beneficiario no coincide con el indicado en la recepción P2P', + 'SIS0192': u'Error, La clave de mail del beneficiario no es correcta en la recepción P2P', + 'SIS0193': u'Error comprobando monedas para DCC', + 'SIS0194': u'Error problemas con la aplicación del cambio y el mostrado al titular', + 'SIS0195': u'Error en pago PIN. No llegan los datos', + 'SIS0196': u'Error las tarjetas de operación P2P no son del mismo procesador', + 'SIS0197': u'Error al obtener los datos de cesta de la compra en operación tipo pasarela', + 'SIS0198': u'Error el importe supera el límite permitido para el comercio', + 'SIS0199': u'Error el número de operaciones supera el límite permitido para el comercio', + 'SIS0200': u'Error el importe acumulado supera el límite permitido para el comercio', + 'SIS0201': u'Se ha producido un error inesperado al realizar la conexión con el VDS', + 'SIS0202': u'Se ha producido un error en el envío del mensaje', + 'SIS0203': u'No existe ningún método definido para el envío del mensaje', + 'SIS0204': u'No se ha definido una URL válida para el envío de mensajes', + 'SIS0205': u'Error al generar la firma, es posible que el mensaje no sea válido o esté incompleto', + 'SIS0206': u'No existe una clave asociada al BID especificado', + 'SIS0207': u'La consulta no ha devuelto ningún resultado', + 'SIS0208': u'La operación devuelta por el SIS no coincide con la petición', + 'SIS0209': u'No se han definido parámetros para realizar la consulta', + 'SIS0210': u'Error al validar el mensaje, faltan datos: BID', + 'SIS0211': u'Error en la validación de la firma ', + 'SIS0212': u'La respuesta recibida no se corresponde con la petición. Referencias de mensaje distintas', + 'SIS0213': u'Errores devueltos por el VDS', + 'SIS0214': u'El comercio no permite devoluciones. Se requiere usar firma ampliada.', + 'SIS0215': u'Operación no permitida para TPV’s virtuales de esta entidad.', + 'SIS0216': u'Error Ds_Merchant_CVV2 tiene más de 3 posiciones', + 'SIS0217': u'Error de formato en Ds_Merchant_CVV2', + 'SIS0218': u'El comercio no permite operaciones seguras por entrada XML', + 'SIS0219': u'Error el número de operaciones de la tarjeta supera el límite permitido para el comercio', + 'SIS0220': u'Error el importe acumulado de la tarjeta supera el límite permitido para el comercio', + 'SIS0221': u'Error el CVV2 es obligatorio', + 'SIS0222': u'Ya existe una anulación asociada a la preautorización', + 'SIS0223': u'La preautorización que se desea anular no está autorizada', + 'SIS0224': u'El comercio no permite anulaciones por no tener firma ampliada', + 'SIS0225': u'Error no existe operación sobre la que realizar la anulación', + 'SIS0226': u'Inconsistencia de datos, en la validación de una anulación', + 'SIS0227': u'Valor del campo Ds_Merchant_TransactionDate no válido', + 'SIS0228': u'Sólo se puede hacer pago aplazado con tarjeta de crédito On-us', + 'SIS0229': u'No existe el código de pago aplazado solicitado', + 'SIS0230': u'El comercio no permite pago fraccionado', + 'SIS0231': u'No hay forma de pago aplicable para el cliente', + 'SIS0232': u'Error. Forma de pago no disponible', + 'SIS0233': u'Error. Forma de pago desconocida', + 'SIS0234': u'Error. Nombre del titular de la cuenta no disponible', + 'SIS0235': u'Error. Campo Sis_Numero_Entidad no disponible', + 'SIS0236': u'Error. El campo Sis_Numero_Entidad no tiene la longitud requerida', + 'SIS0237': u'Error. El campo Sis_Numero_Entidad no es numérico', + 'SIS0238': u'Error. Campo Sis_Numero_Oficina no disponible', + 'SIS0239': u'Error. El campo Sis_Numero_Oficina no tiene la longitud requerida', + 'SIS0240': u'Error. El campo Sis_Numero_Oficina no es numérico', + 'SIS0241': u'Error. Campo Sis_Numero_DC no disponible', + 'SIS0242': u'Error. El campo Sis_Numero_DC no tiene la longitud requerida', + 'SIS0243': u'Error. El campo Sis_Numero_DC no es numérico', + 'SIS0244': u'Error. Campo Sis_Numero_Cuenta no disponible', + 'SIS0245': u'Error. El campo Sis_Numero_Cuenta no tiene la longitud requerida', + 'SIS0246': u'Error. El campo Sis_Numero_Cuenta no es numérico', + 'SIS0247': u'Dígito de Control de Cuenta Cliente no válido', + 'SIS0248': u'El comercio no permite pago por domiciliación', + 'SIS0249': u'Error al realizar pago por domiciliación', + 'SIS0250': u'Error al tomar los datos del XML para realizar Pago por Transferencia', + 'SIS0251': u'El comercio no permite pago por transferencia', + 'SIS0252': u'El comercio no permite el envío de tarjeta', + 'SIS0253': u'Tarjeta no cumple check-digit', + 'SIS0254': u'El número de operaciones de la IP supera el límite permitido por el comercio', + 'SIS0255': u'El importe acumulado por la IP supera el límite permitido por el comercio', + 'SIS0256': u'El comercio no puede realizar preautorizaciones', + 'SIS0257': u'Esta tarjeta no permite operativa de preautorizaciones', + 'SIS0258': u'Inconsistencia de datos, en la validación de una confirmación', + 'SIS0259': u'No existe la operación original para notificar o consultar', + 'SIS0260': u'Entrada incorrecta al SIS', + 'SIS0261': u'Operación detenida por superar el control de restricciones en la entrada al SIS', + 'SIS0262': u'Moneda no permitida para operación de transferencia o domiciliación ', + 'SIS0263': u'Error calculando datos para procesar operación en su banca online', + 'SIS0264': u'Error procesando datos de respuesta recibidos desde su banca online', + 'SIS0265': u'Error de firma en los datos recibidos desde su banca online', + 'SIS0266': u'No se pueden recuperar los datos de la operación recibida desde su banca online', + 'SIS0267': u'La operación no se puede procesar por no existir Código Cuenta Cliente', + 'SIS0268': u'La operación no se puede procesar por este canal', + 'SIS0269': u'No se pueden realizar devoluciones de operaciones de domiciliación no descargadas', + 'SIS0270': u'El comercio no puede realizar preautorizaciones en diferido', + 'SIS0271': u'Error realizando pago-autenticación por WebService', + 'SIS0272': u'La operación a autorizar por WebService no se puede encontrar', + 'SIS0273': u'La operación a autorizar por WebService está en un estado incorrecto', + 'SIS0274': u'Tipo de operación desconocida o no permitida por esta entrada al SIS', + 'SIS0275': u'Error Premio: Premio sin IdPremio', + 'SIS0276': u'Error Premio: Unidades del Premio a redimir no numéricas.', + 'SIS0277': u'Error Premio: Error general en el proceso.', + 'SIS0278': u'Error Premio: Error en el proceso de consulta de premios', + 'SIS0279': u'Error Premio: El comercio no tiene activada la operativa de fidelización', + 'SIS0280': u'Reglas V3.0 : excepción por regla con Nivel de gestión usuario Interno.', + 'SIS0281': u'Reglas V3.0 : excepción por regla con Nivel de gestión usuario Entidad.', + 'SIS0282': u'Reglas V3.0 : excepción por regla con Nivel de gestión usuario Comercio/MultiComercio de una entidad.', + 'SIS0283': u'Reglas V3.0 : excepción por regla con Nivel de gestión usuario Comercio-Terminal.', + 'SIS0284': u'Pago Adicional: error no existe operación sobre la que realizar el PagoAdicional', + 'SIS0285': u'Pago Adicional: error tiene más de una operación sobre la que realizar el Pago Adicional', + 'SIS0286': u'Pago Adicional: La operación sobre la que se quiere hacer la operación adicional no está Aceptada', + 'SIS0287': u'Pago Adicional: la Operación ha sobrepasado el importe para el Pago Adicional.', + 'SIS0288': u'Pago Adicional: No se puede realizar otro pago Adicional. Se ha superado el número de pagos adicionales permitidos sobre la operación.', + 'SIS0289': u'Pago Adicional: El importe del pago Adicional supera el máximo días permitido.', + 'SIS0290': u'Control de Fraude: Bloqueo por control de Seguridad', + 'SIS0291': u'Control de Fraude: Bloqueo por lista Negra control de IP', + 'SIS0292': u'Control de Fraude: Bloqueo por lista Negra control de Tarjeta', + 'SIS0293': u'Control de Fraude: Bloqueo por Lista negra evaluación de Regla', + 'SIS0294': u'Tarjetas Privadas BBVA: La tarjeta no es Privada de BBVA (uno-e). No seadmite el envío de DS_MERCHANT_PAY_TYPE.', + 'SIS0295': u'Error de duplicidad de operación. Se puede intentar de nuevo', + 'SIS0296': u'Error al validar los datos de la Operación de Tarjeta en Archivo Inicial', + 'SIS0297': u'Número de operaciones sucesivas de Tarjeta en Archivo superado', + 'SIS0298': u'El comercio no permite realizar operaciones de Tarjeta en Archivo', + 'SIS0299': u'Error en la llamada a PayPal', + 'SIS0300': u'Error en los datos recibidos de PayPal', + 'SIS0301': u'Error en pago con PayPal', + 'SIS0302': u'Moneda no válida para pago con PayPal', + 'SIS0303': u'Esquema de la entidad es 4B', + 'SIS0304': u'No se permite pago fraccionado si la tarjeta no es de FINCONSUM', + 'SIS0305': u'No se permite pago fraccionado FINCONSUM en moneda diferente de euro', + 'SIS0306': u'Valor de Ds_Merchant_PrepaidCard no válido', + 'SIS0307': u'Operativa de tarjeta regalo no permitida', + 'SIS0308': u'Tiempo límite para recarga de tarjeta regalo superado', + 'SIS0309': u'Error faltan datos adicionales para realizar la recarga de tarjeta prepago', + 'SIS0310': u'Valor de Ds_Merchant_Prepaid_Expiry no válido', + 'SIS0311': u'Error al montar el mensaje para consulta de comisión en recarga de tarjeta prepago ', + 'SIS0312': u'Error en petición StartCheckoutSession con V.me', + 'SIS0313': u'Petición de compra mediante V.me no permitida', + 'SIS0314': u'Error en pago V.me', + 'SIS0315': u'Error analizando petición de autorización de V.me', + 'SIS0316': u'Error en petición de autorización de V.me', + 'SIS0317': u'Error montando respuesta a autorización de V.me', + 'SIS0318': u'Error en retorno del pago desde V.me', + 'SIS0319': u'El comercio no pertenece al grupo especificado en Ds_Merchant_Group', + 'SIS0321': u'El identificador indicado en Ds_Merchant_Identifier no está asociado al comercio', + 'SIS0322': u'Error de formato en Ds_Merchant_Group', + 'SIS0323': u'Para tipo de operación F es necesario el campo Ds_Merchant_Customer_Mobile o Ds_Merchant_Customer_Mail', + 'SIS0324': u'Para tipo de operación F. Imposible enviar link al titular', + 'SIS0325': u'Se ha pedido no mostrar pantallas pero no se ha enviado ningún identificador de tarjeta', + 'SIS0326': u'Se han enviado datos de tarjeta en fase primera de un pago con dos fases', + 'SIS0327': u'No se ha enviado ni móvil ni email en fase primera de un pago con dos fases', + 'SIS0328': u'Token de pago en dos fases inválido', + 'SIS0329': u'No se puede recuperar el registro en la tabla temporal de pago en dos fases', + 'SIS0330': u'Fechas incorrectas de pago dos fases', + 'SIS0331': u'La operación no tiene un estado válido o no existe.', + 'SIS0332': u'El importe de la operación original y de la devolución debe ser idéntico', + 'SIS0333': u'Error en una petición a MasterPass Wallet', + 'SIS0334': u'Bloqueo regla operativa grupos definidos por la entidad', + 'SIS0335': u'Ds_Merchant_Recharge_Commission no válido', + 'SIS0336': u'Error realizando petición de redirección a Oasys', + 'SIS0337': u'Error calculando datos de firma para redirección a Oasys', + 'SIS0338': u'No se encuentra la operación Oasys en la BD', + 'SIS0339': u'El comercio no dispone de pago Oasys', + 'SIS0340': u'Respuesta recibida desde Oasys no válida', + 'SIS0341': u'Error en la firma recibida desde Oasys', + 'SIS0342': u'El comercio no permite realizar operaciones de pago de tributos', + 'SIS0343': u'El parámetro Ds_Merchant_Tax_Reference falta o es incorrecto', + 'SIS0344': u'El usuario ha elegido aplazar el pago, pero no ha aceptado las condiciones de las cuotas', + 'SIS0345': u'El usuario ha elegido un número de plazos incorrecto', + 'SIS0346': u'Error de formato en parámetro DS_MERCHANT_PAY_TYPE', + 'SIS0347': u'El comercio no está configurado para realizar la consulta de BIN.', + 'SIS0348': u'El BIN indicado en la consulta no se reconoce', + 'SIS0349': u'Los datos de importe y DCC enviados no coinciden con los registrados en SIS', + 'SIS0350': u'No hay datos DCC registrados en SIS para este número de pedido', + 'SIS0351': u'Autenticación prepago incorrecta', + 'SIS0352': u'El tipo de firma del comercio no permite esta operativa', + 'SIS0353': u'El comercio no tiene definida una clave 3DES válida', + 'SIS0354': u'Error descifrando petición al SIS', + 'SIS0355': u'El comercio-terminal enviado en los datos cifrados no coincide con el enviado en la petición', + 'SIS0356': u'Existen datos de entrada para control de fraude y el comercio no tiene activo control de fraude', + 'SIS0357': u'Error en parametros enviados. El comercio tiene activo control de fraude y no existe campo ds_merchant_merchantscf', + 'SIS0358': u'La entidad no dispone de pago Oasys', + 'SIS0370': u'Error en formato Scf_Merchant_Nif. Longitud máxima 16', + 'SIS0371': u'Error en formato Scf_Merchant_Name. Longitud máxima 30', + 'SIS0372': u'Error en formato Scf_Merchant_First_Name. Longitud máxima 30 ', + 'SIS0373': u'Error en formato Scf_Merchant_Last_Name. Longitud máxima 30', + 'SIS0374': u'Error en formato Scf_Merchant_User. Longitud máxima 45', + 'SIS0375': u'Error en formato Scf_Affinity_Card. Valores posibles \'S\' o \'N\'. Longitud máxima 1', + 'SIS0376': u'Error en formato Scf_Payment_Financed. Valores posibles \'S\' o \'N\'. Longitud máxima 1', + 'SIS0377': u'Error en formato Scf_Ticket_Departure_Point. Longitud máxima 30', + 'SIS0378': u'Error en formato Scf_Ticket_Destination. Longitud máxima 30', + 'SIS0379': u'Error en formato Scf_Ticket_Departure_Date. Debe tener formato yyyyMMddHHmmss.', + 'SIS0380': u'Error en formato Scf_Ticket_Num_Passengers. Longitud máxima 1.', + 'SIS0381': u'Error en formato Scf_Passenger_Dni. Longitud máxima 16.', + 'SIS0382': u'Error en formato Scf_Passenger_Name. Longitud máxima 30.', + 'SIS0383': u'Error en formato Scf_Passenger_First_Name. Longitud máxima 30.', + 'SIS0384': u'Error en formato Scf_Passenger_Last_Name. Longitud máxima 30.', + 'SIS0385': u'Error en formato Scf_Passenger_Check_Luggage. Valores posibles \'S\' o \'N\'. Longitud máxima 1.', + 'SIS0386': u'Error en formato Scf_Passenger_Special_luggage. Valores posibles \'S\' o \'N\'. Longitud máxima 1.', + 'SIS0387': u'Error en formato Scf_Passenger_Insurance_Trip. Valores posibles \'S\' o \'N\'. Longitud máxima 1.', + 'SIS0388': u'Error en formato Scf_Passenger_Type_Trip. Valores posibles \'N\' o \'I\'. Longitud máxima 1.', + 'SIS0389': u'Error en formato Scf_Passenger_Pet. Valores posibles \'S\' o \'N\'. Longitud máxima 1.', + 'SIS0390': u'Error en formato Scf_Order_Channel. Valores posibles \'M\'(móvil), \'P\'(PC) o \'T\'(Tablet)', + 'SIS0391': u'Error en formato Scf_Order_Total_Products. Debe tener formato numérico y longitud máxima de 3.', + 'SIS0392': u'Error en formato Scf_Order_Different_Products. Debe tener formato numérico y longitud máxima de 3.', + 'SIS0393': u'Error en formato Scf_Order_Amount. Debe tener formato numérico y longitud máxima de 19.', + 'SIS0394': u'Error en formato Scf_Order_Max_Amount. Debe tener formato numérico y longitud máxima de 19.', + 'SIS0395': u'Error en formato Scf_Order_Coupon. Valores posibles \'S\' o \'N\'', + 'SIS0396': u'Error en formato Scf_Order_Show_Type. Debe longitud máxima de 30.', + 'SIS0397': u'Error en formato Scf_Wallet_Identifier', + 'SIS0398': u'Error en formato Scf_Wallet_Client_Identifier', + 'SIS0399': u'Error en formato Scf_Merchant_Ip_Address', + 'SIS0400': u'Error en formato Scf_Merchant_Proxy', + 'SIS0401': u'Error en formato Ds_Merchant_Mail_Phone_Number. Debe ser numérico y de longitud máxima 19', + 'SIS0402': u'Error en llamada a SafetyPay para solicitar token url', + 'SIS0403': u'Error en proceso de solicitud de token url a SafetyPay', + 'SIS0404': u'Error en una petición a SafetyPay', + 'SIS0405': u'Solicitud de token url denegada', + 'SIS0406': u'El sector del comercio no está permitido para realizar un pago de premio de apuesta', + 'SIS0407': u'El importe de la operación supera el máximo permitido para realizar un pago de premio de apuesta', + 'SIS0408': u'La tarjeta debe de haber operado durante el último año para poder realizar un pago de premio de apuesta', + 'SIS0409': u'La tarjeta debe ser una Visa o MasterCard nacional para realizar un pago de premio de apuesta', + 'SIS0410': u'Bloqueo por Operación con Tarjeta Privada del Cajamar, en comercio que no es de Cajamar', + 'SIS0411': u'No existe el comercio en la tabla de datos adicionales de RSI Directo', + 'SIS0412': u'La firma enviada por RSI Directo no es correcta', + 'SIS0413': u'La operación ha sido denegada por Lynx', + 'SIS0414': u'El plan de ventas no es correcto', + 'SIS0415': u'El tipo de producto no es correcto', + 'SIS0416': u'Importe no permitido en devolución ', + 'SIS0417': u'Fecha de devolución no permitida', + 'SIS0418': u'No existe plan de ventas vigente', + 'SIS0419': u'Tipo de cuenta no permitida', + 'SIS0420': u'El comercio no dispone de formas de pago para esta operación', + 'SIS0421': u'Tarjeta no permitida. No es producto Agro', + 'SIS0422': u'Faltan datos para operación Agro', + 'SIS0423': u'CNPJ del comercio incorrecto', + 'SIS0424': u'No se ha encontrado el establecimiento', + 'SIS0425': u'No se ha encontrado la tarjeta', + 'SIS0426': u'Enrutamiento no valido para comercio Corte Ingles.', + 'SIS0427': u'La conexión con CECA no ha sido posible para el comercio Corte Ingles.', + 'SIS0428': u'Operación debito no segura', + 'SIS0429': u'Error en la versión enviada por el comercio (Ds_SignatureVersion)', + 'SIS0430': u'Error al decodificar el parámetro Ds_MerchantParameters', + 'SIS0431': u'Error del objeto JSON que se envía codificado en el parámetro Ds_MerchantParameters', + 'SIS0432': u'Error FUC del comercio erróneo', + 'SIS0433': u'Error Terminal del comercio erróneo', + 'SIS0434': u'Error ausencia de número de pedido en la op. del comercio', + 'SIS0435': u'Error en el cálculo de la firma', + 'SIS0436': u'Error en la construcción del elemento padre ', + 'SIS0437': u'Error en la construcción del elemento ', + 'SIS0438': u'Error en la construcción del elemento ', + 'SIS0439': u'Error en la construcción del elemento ', + 'SIS0440': u'Error al crear pantalla MyBank', + 'SIS0441': u'Error no tenemos bancos para Mybank', + 'SIS0442': u'Error al realizar el pago Mybank', + 'SIS0443': u'No se permite pago en terminales ONEY con tarjetas ajenas', + 'SIS0445': u'Error gestionando referencias con Stratus', + 'SIS0444': u'Se está intentando acceder usando firmas antiguas y el comercio está configurado como HMAC SHA256', + 'SIS0446': u'Para terminales Oney es obligatorio indicar la forma de pago', + 'SIS0447': u'Error, se está utilizando una referencia que se generó con un adquirente distinto al adquirente que la utiliza.', + 'SIS0448': u'Error, la tarjeta de la operación es DINERS y el comercio no tiene el método de pago "Pago DINERS"', + 'SIS0449': u'Error, el tipo de pago de la operación es Tradicional(A), la tarjeta de la operación no es DINERS ni JCB ni AMEX y el comercio tiene el método de pago "Prohibir Pago A"', + 'SIS0450': u'Error, el tipo de pago de la operación es Tradicional(A), la tarjeta de la operación es AMEX y el comercio tiene los métodos de pago "Pago Amex y Prohibir Pago A AMEX"', + 'SIS0451': u'Error, la operación es Host to Host con tipo de pago Tradicional(A), la tarjeta de la operación no es DINERS ni JCB ni AMEX y el comercio tiene el método de pago "Prohibir Pago A"', + 'SIS0452': u'Error, la tarjeta de la operación es 4B y el comercio no tiene el método de pago "Tarjeta 4B"', + 'SIS0453': u'Error, la tarjeta de la operación es JCB y el comercio no tiene el método de pago "Pago JCB"', + 'SIS0454': u'Error, la tarjeta de la operación es AMEX y el comercio no tiene el método de pago "Pago Amex"', + 'SIS0455': u'Error, el comercio no tiene el método de pago "Tarjetas Propias" y la tarjeta no está registrada como propia. ', + 'SIS0456': u'Error, se aplica el método de pago "Verified By Visa" con Respuesta [VEReq, VERes] = U y el comercio no tiene los métodos de pago "Pago U y Pago U Nacional"', + 'SIS0457': u'Error, se aplica el método de pago "MasterCard SecureCode" con Respuesta [VEReq, VERes] = N con tarjeta MasterCard Comercial y el comercio no tiene el método de pago "MasterCard Comercial"', + 'SIS0458': u'Error, se aplica el método de pago "MasterCard SecureCode" con Respuesta [VEReq, VERes] = U con tarjeta MasterCard Comercial y el comercio no tiene el método de pago "MasterCard Comercial"', + 'SIS0459': u'Error, se aplica el método de pago "JCB Secure" con Respuesta [VEReq, VERes]= U y el comercio no tiene el método de pago "Pago JCB"', + 'SIS0460': u'Error, se aplica el método de pago "AMEX SafeKey" con Respuesta [VEReq, VERes] = N y el comercio no tiene el método de pago "Pago AMEX"', + 'SIS0461': u'Error, se aplica el método de pago "AMEX SafeKey" con Respuesta [VEReq, VERes] = U y el comercio no tiene el método de pago "Pago AMEX"', + 'SIS0462': u'Error, se aplica el método de pago "Verified By Visa","MasterCard SecureCode","JCB Secure" o "AMEX SafeKey" y la operación es Host To Host', + 'SIS0463': u'Error, se selecciona un método de pago que no está entre los permitidos por el SIS para ser ejecutado', + 'SIS0464': u'Error, el resultado de la autenticación 3DSecure es "NO_3DSECURE" con tarjeta MasterCard Comercial y el comercio no tiene el método de pago "MasterCard Comercial"', + 'SIS0465': u'Error, el resultado de la autenticación 3DSecure es "NO_3DSECURE", la tarjeta no es Visa, ni Amex, ni JCB, ni Master y el comercio no tiene el método de pago "Tradicional Mundial" ', + } + ALLOW_PAYMENT_BY_REFERENCE = True # El TPV de RedSys consta de dos entornos en funcionamiento, uno para pruebas y otro para producción @@ -1265,8 +1720,17 @@ def _receiveConfirmationHTTPPOST(request): operation = VPOSPaymentOperation.objects.get(operation_number=operation_number) operation.confirmation_data = {"GET": request.GET.dict(), "POST": request.POST.dict()} operation.confirmation_code = operation_number + + ds_errorcode = operation_data.get("Ds_ErrorCode") + if ds_errorcode: + errormsg = u' // ' + VPOSRedsys._format_ds_error_code(operation_data.get("Ds_ErrorCode")) + else: + errormsg = u'' + + operation.response_code = VPOSRedsys._format_ds_response_code(operation_data.get("Ds_Response")) + errormsg operation.save() dlprint("Operation {0} actualizada en _receiveConfirmationHTTPPOST()".format(operation.operation_number)) + dlprint(u"Ds_Response={0} Ds_ErrorCode={1}".format(operation_data.get("Ds_Response"), operation_data.get("Ds_ErrorCode"))) vpos = operation.virtual_point_of_sale except VPOSPaymentOperation.DoesNotExist: @@ -1325,16 +1789,30 @@ def _receiveConfirmationSOAP(request): # Almacén de operaciones try: ds_order = root.xpath("//Message/Request/Ds_Order/text()")[0] - ds_authorisationcode = root.xpath("//Message/Request/Ds_AuthorisationCode/text()")[0] ds_response = root.xpath("//Message/Request/Ds_Response/text()")[0] + try: + ds_authorisationcode = root.xpath("//Message/Request/Ds_AuthorisationCode/text()")[0] + except IndexError: + dlprint(u"Ds_Order {0} sin Ds_AuthorisationCode (Ds_response={1})".format(ds_order, ds_response)) + ds_authorisationcode = "" + + try: + ds_errorcode = root.xpath("//Message/Request/Ds_ErrorCode/text()")[0] + errormsg = u' // ' + VPOSRedsys._format_ds_error_code(ds_errorcode) + except IndexError: + ds_errorcode = None + errormsg = u'' + operation = VPOSPaymentOperation.objects.get(operation_number=ds_order) operation.confirmation_data = {"GET": "", "POST": xml_content} operation.confirmation_code = ds_order - operation.response_code = VPOSRedsys._format_ds_response_code(ds_response) + operation.response_code = VPOSRedsys._format_ds_response_code(ds_response) + errormsg operation.save() dlprint("Operation {0} actualizada en _receiveConfirmationSOAP()".format(operation.operation_number)) + dlprint(u"Ds_Response={0} Ds_ErrorCode={1}".format(ds_response, ds_errorcode)) vpos = operation.virtual_point_of_sale + except VPOSPaymentOperation.DoesNotExist: # Si no existe la operación, están intentando # cargar una operación inexistente @@ -1675,13 +2153,29 @@ def _format_ds_response_code(ds_response): if len(ds_response) == 4 and ds_response.isdigit() and ds_response[:2] == "00": message = u"Transacción autorizada para pagos y preautorizaciones." else: - message = VPOSRedsys.DS_RESPONSE_CODES.get(ds_response) + message = VPOSRedsys.DS_RESPONSE_CODES.get(ds_response, u"código de respuesta Ds_Response desconocido") out = u"{0}. {1}".format(ds_response, message) return out + @staticmethod + def _format_ds_error_code(ds_errorcode): + """ + Formatea el mensaje asociado a un Ds_ErrorCode + :param ds_errorcode: str código Ds_ErrorCode + :return: unicode mensaje formateado + """ + + if not ds_errorcode: + return '' + + message = VPOSRedsys.DS_ERROR_CODES.get(ds_errorcode, u'Código de respuesta Ds_ErrorCode desconocido') + out = u"{0}. {1}".format(ds_errorcode, message) + + return out + ######################################################################################################################## ######################################################################################################################## ###################################################### TPV PayPal ###################################################### diff --git a/setup.cfg b/setup.cfg old mode 100755 new mode 100644 diff --git a/setup.py b/setup.py index f8edbad..3fd9522 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ setup( name="django-virtual-pos", - version="1.6.6", + version="1.6.7", install_requires=[ "django", "beautifulsoup4", @@ -54,9 +54,9 @@ "pytz", "requests" ], - author="intelligenia S.L.", - author_email="diego@intelligenia.es", - description="django-virtual-post is a module that abstracts the flow of paying in several virtual points of sale.", + author="intelligenia", + author_email="mario@intelligenia.es", + description="django-virtual-pos is a module that abstracts the flow of paying in several online payment platforms.", long_description=long_description, classifiers=[ 'Development Status :: 5 - Production/Stable', @@ -64,7 +64,7 @@ 'License :: OSI Approved :: MIT License', ], license="MIT", - keywords="virtual point-of-sale puchases online payments", + keywords=["virtual", "point-of-sale", "puchases", "online", "payments"], url='https://github.com/intelligenia/django-virtual-pos', packages=find_packages('.'), data_files=data_files, From cff4933f5837b4259462f4d3f4dca4c929a158bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Fri, 9 Feb 2018 13:53:31 +0100 Subject: [PATCH 37/72] Bitpay payment integration, main connection logic --- .../migrations/0012_auto_20180209_1222.py | 40 ++++ djangovirtualpos/models.py | 186 +++++++++++++++++- 2 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 djangovirtualpos/migrations/0012_auto_20180209_1222.py diff --git a/djangovirtualpos/migrations/0012_auto_20180209_1222.py b/djangovirtualpos/migrations/0012_auto_20180209_1222.py new file mode 100644 index 0000000..de0d79a --- /dev/null +++ b/djangovirtualpos/migrations/0012_auto_20180209_1222.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2018-02-09 12:22 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangovirtualpos', '0011_auto_20170404_1424'), + ] + + operations = [ + migrations.CreateModel( + name='VPOSBitpay', + fields=[ + ('parent', models.OneToOneField(db_column='vpos_id', on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='+', serialize=False, to='djangovirtualpos.VirtualPointOfSale')), + ('testing_api_key', models.CharField(blank=True, max_length=512, null=True, verbose_name='API Key de Bitpay para entorno de test')), + ('production_api_key', models.CharField(max_length=512, verbose_name='API Key de Bitpay para entorno de produccion')), + ('currency', models.CharField(choices=[('EUR', 'Euro'), ('USD', 'Dolares'), ('BTC', 'Bitcoin')], default='EUR', max_length=3, verbose_name='Moneda (EUR, USD, BTC)')), + ('transaction_speed', models.CharField(choices=[('high', 'Alta'), ('medium', 'Media'), ('low', 'Baja')], default='medium', max_length=10, verbose_name='Velocidad de la operaci\xf3n')), + ('notification_url', models.URLField(verbose_name='Url notificaciones actualizaci\xf3n estados (https)')), + ('operation_number_prefix', models.CharField(blank=True, max_length=20, null=True, verbose_name='Prefijo del n\xfamero de operaci\xf3n')), + ('api_version', models.CharField(default='1.2', max_length=3, verbose_name='Version')), + ], + bases=('djangovirtualpos.virtualpointofsale',), + ), + migrations.AlterField( + model_name='virtualpointofsale', + name='type', + field=models.CharField(choices=[('ceca', 'TPV Virtual - Confederaci\xf3n Espa\xf1ola de Cajas de Ahorros (CECA)'), ('paypal', 'Paypal'), ('redsys', 'TPV Redsys'), ('santanderelavon', 'TPV Santander Elavon'), ('bitpay', 'TPV Bitpay')], default='', max_length=16, verbose_name='Tipo de TPV'), + ), + migrations.AlterField( + model_name='vpospaymentoperation', + name='type', + field=models.CharField(choices=[('ceca', 'TPV Virtual - Confederaci\xf3n Espa\xf1ola de Cajas de Ahorros (CECA)'), ('paypal', 'Paypal'), ('redsys', 'TPV Redsys'), ('santanderelavon', 'TPV Santander Elavon'), ('bitpay', 'TPV Bitpay')], default='', max_length=16, verbose_name='Tipo de TPV'), + ), + ] diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index 75435f9..2680a86 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -46,6 +46,7 @@ ("paypal", _("Paypal")), ("redsys", _("TPV Redsys")), ("santanderelavon", _("TPV Santander Elavon")), + ("bitpay", _("TPV Bitpay")), ) ## Relación entre tipos de TPVs y clases delegadas @@ -54,6 +55,7 @@ "redsys": "VPOSRedsys", "paypal": "VPOSPaypal", "santanderelavon": "VPOSSantanderElavon", + "bitpay": "VPOSBitpay", } @@ -2977,4 +2979,186 @@ def _verification_signature(self): dlprint(u"FIRMA2 datos: {0}".format(signature2)) dlprint(u"FIRMA2 hash: {0}".format(firma2)) - return firma2 \ No newline at end of file + return firma2 + +class VPOSBitpay(VirtualPointOfSale): + """ + Pago con criptomoneda usando la plataforma bitpay.com + Siguiendo la documentación: https://bitpay.com/api + """ + + CURRENCIES = ( + ('EUR', 'Euro'), + ('USD', 'Dolares'), + ('BTC', 'Bitcoin'), + ) + + # Velocidad de la operación en función de la fortaleza de la confirmación en blockchain. + TRANSACTION_SPEED = ( + ('high', 'Alta'), # Se supone confirma en el momento que se ejecuta. + ('medium', 'Media'), # Se supone confirmada una vez se verifica 1 bloque. (~10 min) + ('low', 'Baja'), # Se supone confirmada una vez se verifican 6 bloques (~1 hora) + ) + + # Relación con el padre (TPV). + # Al poner el signo "+" como "related_name" evitamos que desde el padre + # se pueda seguir la relación hasta aquí (ya que cada uno de las clases + # que heredan de ella estará en una tabla y sería un lío). + parent = models.OneToOneField(VirtualPointOfSale, parent_link=True, related_name="+", null=False, db_column="vpos_id") + testing_api_key = models.CharField(max_length=512, null=True, blank=True, verbose_name="API Key de Bitpay para entorno de test") + production_api_key = models.CharField(max_length=512, null=False, blank=False, verbose_name="API Key de Bitpay para entorno de produccion") + currency = models.CharField(max_length=3, choices=CURRENCIES, default='EUR', null=False, blank=False, verbose_name="Moneda (EUR, USD, BTC)") + transaction_speed = models.CharField(max_length=10, choices=TRANSACTION_SPEED, default='medium', null=False, blank=False, verbose_name="Velocidad de la operación") + notification_url = models.URLField(verbose_name="Url notificaciones actualización estados (https)", null=False, blank=False) + + # Prefijo usado para identicar al servidor desde el que se realiza la petición, en caso de usar TPV-Proxy. + operation_number_prefix = models.CharField(max_length=20, null=True, blank=True, verbose_name="Prefijo del número de operación") + api_version = models.CharField(max_length=3, null=False, blank=False, verbose_name="Version", default="1.2") + + + bitpay_url = { + "production": { + "api": "https://bitpay.com/api/", + "create_invoice": "https://bitpay.com/api/invoice", + "payment": "https://bitpay.com/invoice/" + }, + "testing": { + "api": "https://test.bitpay.com/api/", + "create_invoice": "https://test.bitpay.com/api/invoice", + "payment": "https://test.bitpay.com/invoice/" + } + } + + def configurePayment(self, **kwargs): + + self.api_key = self.testing_api_key + + if self.parent.environment == "production": + self.api_key = self.production_api_key + + self.importe = self.parent.operation.amount + + def setupPayment(self, operation_number=None, code_len=40): + """ + Inicializa el pago + Obtiene el número de operación, que para el caso de BitPay será el id dado + :param operation_number: + :param code_len: + :return: + """ + + dlprint("BitPay.setupPayment") + if operation_number: + self.bitpay_id = operation_number + dlprint("Rescato el operation number para esta venta {0}".format(self.bitpay_id)) + return self.bitpay_id + + params = { + 'price': self.importe, + 'currency': self.currency, + 'redirectURL': self.parent.operation.url_ok, + 'itemDesc': self.parent.operation.description, + 'notificationURL': self.notification_url, + # Campos libres para el programador, puedes introducir cualquier información útil. + # En nuestro caso el prefijo de la operación, que ayuda a TPV proxy a identificar el servidor + # desde donde se ha ejecutado la operación. + 'posData': '{"operation_number_prefix": "' + str(self.operation_number_prefix) + '"}', + 'fullNotifications': True + } + + # URL de pago según el entorno + url = self.bitpay_url[self.parent.environment]["create_invoice"] + + post = json.dumps(params) + req = urllib2.Request(url) + base64string = base64.encodestring(self.api_key).replace('\n', '') + req.add_header("Authorization", "Basic %s" % base64string) + req.add_header("Content-Type", "application/json") + req.add_header("Content-Length", len(post)) + + json_response = urllib2.urlopen(req, post) + response = json.load(json_response) + + dlprint(u"Parametros que enviamos a Bitpay para crear la operación") + dlprint(params) + + dlprint(u"Respuesta de Bitpay") + dlprint(response) + + if not response.get("id"): + raise ValueError(u"ERROR. La respuesta no contiene id de invoice.") + + self.bitpay_id = response.get("id") + + return self.bitpay_id + + def getPaymentFormData(self): + """ + Generar formulario (en este caso prepara un submit a la página de bitpay). + """ + + url = self.bitpay_url[self.parent.environment]["payment"] + data = {"id": self.bitpay_id} + + form_data = { + "data": data, + "action": url, + "method": "get" + } + return form_data + + @staticmethod + def receiveConfirmation(request, **kwargs): + + confirmation_body_param = json.loads(request.body) + + # Almacén de operaciones + try: + operation = VPOSPaymentOperation.objects.get(operation_number=confirmation_body_param.get("id")) + operation.confirmation_data = {"GET": request.GET.dict(), "POST": request.POST.dict(), "BODY": confirmation_body_param} + operation.save() + + dlprint("Operation {0} actualizada en receiveConfirmation()".format(operation.operation_number)) + vpos = operation.virtual_point_of_sale + except VPOSPaymentOperation.DoesNotExist: + # Si no existe la operación, están intentando + # cargar una operación inexistente + return False + + # Iniciamos el delegado y la operación + vpos._init_delegated() + vpos.operation = operation + + vpos.delegated.bitpay_id = operation.confirmation_data["BODY"].get("id") + vpos.delegated.status = operation.confirmation_data["BODY"].get("status") + + dlprint(u"Lo que recibimos de BitPay: ") + dlprint(operation.confirmation_data["BODY"]) + return vpos.delegated + + def verifyConfirmation(self): + # Comprueba si el envío es correcto + # Para esto, comprobamos si hay alguna operación que tenga el mismo + # número de operación + operation = VPOSPaymentOperation.objects.filter(operation_number=self.bitpay_id) + + if operation: + # En caso de recibir, un estado confirmado () + # NOTA: Bitpay tiene los siguientes posibles estados: + # new, paid, confirmed, complete, expired, invalid. + if self.status == "confirmed": + dlprint(u"La operación es confirmada") + return True + + return False + + def charge(self): + dlprint(u"Marca la operacion como pagada") + return HttpResponse("OK") + + def responseNok(self, extended_status=""): + dlprint("responseNok") + return HttpResponse("NOK") + + def refund(self, refund_amount, description): + raise VPOSOperationDontImplemented(u"No se ha implementado la operación de devolución particular.") \ No newline at end of file From 4fd8b79870b239ab0b681cc4fdab38cebe7f8939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Fri, 9 Feb 2018 14:00:48 +0100 Subject: [PATCH 38/72] Update changelog with new release --- CHANGES.md | 8 +++++++- setup.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2a4048b..ce79f1c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,8 +1,14 @@ # django-virtual-pos -Django module that abstracts the flow of several virtual points of sale including PayPal. +Django module that abstracts the flow of several virtual points of sale. # Releases +## 1.6.8 +- Integration with [BitPay](https://bitpay.com/) + +## 1.6.7 +- Add DS_ERROR_CODE logging default message for unknown Ds_Response, allow SOAP responses with empty Ds_AuthorisationCode + ## 1.6.6 - Simplify integration. - Add example integration. diff --git a/setup.py b/setup.py index 3fd9522..8c69337 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ setup( name="django-virtual-pos", - version="1.6.7", + version="1.6.8", install_requires=[ "django", "beautifulsoup4", From 31231afea71b3fd9213b39cf1bb32e10b2a9e843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Fri, 9 Feb 2018 14:30:37 +0100 Subject: [PATCH 39/72] Add Bitpay config model to Django Admin panel --- djangovirtualpos/admin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/djangovirtualpos/admin.py b/djangovirtualpos/admin.py index db09f1d..ed17141 100644 --- a/djangovirtualpos/admin.py +++ b/djangovirtualpos/admin.py @@ -1,7 +1,7 @@ # coding=utf-8 from django.contrib import admin -from djangovirtualpos.models import VirtualPointOfSale, VPOSRefundOperation, VPOSCeca, VPOSRedsys, VPOSSantanderElavon, VPOSPaypal +from djangovirtualpos.models import VirtualPointOfSale, VPOSRefundOperation, VPOSCeca, VPOSRedsys, VPOSSantanderElavon, VPOSPaypal, VPOSBitpay admin.site.register(VirtualPointOfSale) admin.site.register(VPOSRefundOperation) @@ -9,5 +9,6 @@ admin.site.register(VPOSRedsys) admin.site.register(VPOSPaypal) admin.site.register(VPOSSantanderElavon) +admin.site.register(VPOSBitpay) From c6d41cf389449209e054f348ef4f43d2b7d563ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Fri, 9 Feb 2018 14:38:19 +0100 Subject: [PATCH 40/72] Erase version field in VPOBitpay --- djangovirtualpos/migrations/0012_auto_20180209_1222.py | 1 - djangovirtualpos/models.py | 1 - 2 files changed, 2 deletions(-) diff --git a/djangovirtualpos/migrations/0012_auto_20180209_1222.py b/djangovirtualpos/migrations/0012_auto_20180209_1222.py index de0d79a..1fd6ddf 100644 --- a/djangovirtualpos/migrations/0012_auto_20180209_1222.py +++ b/djangovirtualpos/migrations/0012_auto_20180209_1222.py @@ -23,7 +23,6 @@ class Migration(migrations.Migration): ('transaction_speed', models.CharField(choices=[('high', 'Alta'), ('medium', 'Media'), ('low', 'Baja')], default='medium', max_length=10, verbose_name='Velocidad de la operaci\xf3n')), ('notification_url', models.URLField(verbose_name='Url notificaciones actualizaci\xf3n estados (https)')), ('operation_number_prefix', models.CharField(blank=True, max_length=20, null=True, verbose_name='Prefijo del n\xfamero de operaci\xf3n')), - ('api_version', models.CharField(default='1.2', max_length=3, verbose_name='Version')), ], bases=('djangovirtualpos.virtualpointofsale',), ), diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index 2680a86..f27856f 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -3013,7 +3013,6 @@ class VPOSBitpay(VirtualPointOfSale): # Prefijo usado para identicar al servidor desde el que se realiza la petición, en caso de usar TPV-Proxy. operation_number_prefix = models.CharField(max_length=20, null=True, blank=True, verbose_name="Prefijo del número de operación") - api_version = models.CharField(max_length=3, null=False, blank=False, verbose_name="Version", default="1.2") bitpay_url = { From 26d547e9d2e2561496ca471a154b6fbc469d3e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Fri, 9 Feb 2018 14:47:13 +0100 Subject: [PATCH 41/72] minor change --- djangovirtualpos/migrations/0012_auto_20180209_1222.py | 2 +- djangovirtualpos/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/djangovirtualpos/migrations/0012_auto_20180209_1222.py b/djangovirtualpos/migrations/0012_auto_20180209_1222.py index 1fd6ddf..77890ed 100644 --- a/djangovirtualpos/migrations/0012_auto_20180209_1222.py +++ b/djangovirtualpos/migrations/0012_auto_20180209_1222.py @@ -18,7 +18,7 @@ class Migration(migrations.Migration): fields=[ ('parent', models.OneToOneField(db_column='vpos_id', on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='+', serialize=False, to='djangovirtualpos.VirtualPointOfSale')), ('testing_api_key', models.CharField(blank=True, max_length=512, null=True, verbose_name='API Key de Bitpay para entorno de test')), - ('production_api_key', models.CharField(max_length=512, verbose_name='API Key de Bitpay para entorno de produccion')), + ('production_api_key', models.CharField(max_length=512, verbose_name='API Key de Bitpay para entorno de producci\xf3n')), ('currency', models.CharField(choices=[('EUR', 'Euro'), ('USD', 'Dolares'), ('BTC', 'Bitcoin')], default='EUR', max_length=3, verbose_name='Moneda (EUR, USD, BTC)')), ('transaction_speed', models.CharField(choices=[('high', 'Alta'), ('medium', 'Media'), ('low', 'Baja')], default='medium', max_length=10, verbose_name='Velocidad de la operaci\xf3n')), ('notification_url', models.URLField(verbose_name='Url notificaciones actualizaci\xf3n estados (https)')), diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index f27856f..885ddcc 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -3006,7 +3006,7 @@ class VPOSBitpay(VirtualPointOfSale): # que heredan de ella estará en una tabla y sería un lío). parent = models.OneToOneField(VirtualPointOfSale, parent_link=True, related_name="+", null=False, db_column="vpos_id") testing_api_key = models.CharField(max_length=512, null=True, blank=True, verbose_name="API Key de Bitpay para entorno de test") - production_api_key = models.CharField(max_length=512, null=False, blank=False, verbose_name="API Key de Bitpay para entorno de produccion") + production_api_key = models.CharField(max_length=512, null=False, blank=False, verbose_name="API Key de Bitpay para entorno de producción") currency = models.CharField(max_length=3, choices=CURRENCIES, default='EUR', null=False, blank=False, verbose_name="Moneda (EUR, USD, BTC)") transaction_speed = models.CharField(max_length=10, choices=TRANSACTION_SPEED, default='medium', null=False, blank=False, verbose_name="Velocidad de la operación") notification_url = models.URLField(verbose_name="Url notificaciones actualización estados (https)", null=False, blank=False) From 4e27fc6a6a8aa763c0b1dd1ff0320ef7d040c181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Mon, 12 Feb 2018 10:22:07 +0100 Subject: [PATCH 42/72] add rst readme file --- README.rst | 170 +++++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 3 +- 2 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 README.rst diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..d484de9 --- /dev/null +++ b/README.rst @@ -0,0 +1,170 @@ +django-virtual-pos +================== + +Django module that abstracts the flow of several virtual points of sale +including PayPal + +What’s this? +============ + +This module abstracts the use of the most used virtual points of sale in +Spain. + +License +======= + +`MIT LICENSE`_. + +Implemented payment methods +=========================== + +PayPal +------ + +Easy integration with PayPal. + +Spanish Virtual Points of Sale +------------------------------ + +Ceca +~~~~ + +`CECA`_ is the Spanish confederation of savings banks. + +RedSyS +~~~~~~ + +`RedSyS`_ gives payment services to several Spanish banks like CaixaBank +or Caja Rural. + +Santander Elavon +~~~~~~~~~~~~~~~~ + +`Santander Elavon`_ is one of the payment methods of the Spanish bank +Santander. + +Requirements and Installation +============================= + +Requirements +------------ + +- Python 2.7 (Python 3 not tested, contributors wanted!) +- `Django`_ +- `BeautifulSoup4`_ +- `lxml`_ +- `pycrypto`_ +- `Pytz`_ +- `Requests`_ + +Type: + +.. code:: sh + + $ pip install django beautifulsoup4 lxml pycrypto pytz + +Installation +------------ + +From PyPi +~~~~~~~~~ + +.. code:: sh + + $ pip install django-virtual-pos + +From master branch +~~~~~~~~~~~~~~~~~~ + +Master branch will allways contain a working version of this module. + +.. code:: sh + + $ pip install git+git://github.com/intelligenia/django-virtual-pos.git + +settings.py +~~~~~~~~~~~ + +Add the application djangovirtualpos to your settings.py: + +.. code:: python + + INSTALLED_APPS = ( + # ... + "djangovirtualpos", + ) + +Use +=== + +See this `manual`_ (currently only in Spanish). + +Needed models +------------- + +You will need to implement this skeleton view using your own **Payment** +model. + +This model has must have at least the following attributes: - **code**: +sale code given by our system. - **operation_number**: bank operation +number. - **status**: status of the payment: “paid”, “pending” +(**pending** is mandatory) or “canceled”. - **amount**: amount to be +charged. + +And the following methods: - **online_confirm**: mark the payment as +paid. + +Integration examples +-------------------- + +- `djshop`_ + +Needed views +------------ + +Sale summary view +~~~~~~~~~~~~~~~~~ + +.. code:: python + + def payment_summary(request, payment_id): + """ + Load a Payment object and show a summary of its contents to the user. + """ + + payment = get_object_or_404(Payment, id=payment_id, status="pending") + replacements = { + "payment": payment, + # ... + } + return render(request, '', replacements) + +Note that this payment summary view should load a JS file called +**set_payment_attributes.js**. + +This file is needed to set initial payment attributes according to which +bank have the user selected. + +Payment_confirm view +~~~~~~~~~~~~~~~~~~~~ + +\````python @csrf_exempt def payment_confirmation(request, +virtualpos_type): “”" This view will be called by the bank. “”" # +Directly call to confirm_payment view + +:: + + # Or implement th + +.. _MIT LICENSE: LICENSE +.. _CECA: http://www.cajasdeahorros.es/ +.. _RedSyS: http://www.redsys.es/ +.. _Santander Elavon: https://www.santanderelavon.com/ +.. _Django: https://pypi.python.org/pypi/django +.. _BeautifulSoup4: https://pypi.python.org/pypi/beautifulsoup4 +.. _lxml: https://pypi.python.org/pypi/lxml +.. _pycrypto: https://pypi.python.org/pypi/pycrypto +.. _Pytz: https://pypi.python.org/pypi/pytz +.. _Requests: https://pypi.python.org/pypi/requests +.. _manual: manual/COMMON.md +.. _djshop: https://github.com/diegojromerolopez/djshop diff --git a/setup.py b/setup.py index 8c69337..cb28ecb 100755 --- a/setup.py +++ b/setup.py @@ -28,8 +28,7 @@ from setuptools import setup, find_packages try: - import pypandoc - long_description = pypandoc.convert('README.md', 'rst') + long_description = open('README.rst').read() except(IOError, ImportError): long_description = open('README.md').read() From 4999196631f0a7a88808d06ef46925191f942a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Mon, 12 Feb 2018 12:00:20 +0100 Subject: [PATCH 43/72] add bitpay documentation --- README.md | 7 ++- README.rst | 24 +++++--- manual/COMMON.md | 1 + manual/vpos_types/BITPAY.md | 116 ++++++++++++++++++++++++++++++++++++ 4 files changed, 138 insertions(+), 10 deletions(-) create mode 100644 manual/vpos_types/BITPAY.md diff --git a/README.md b/README.md index 5c33f2f..a174fa9 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,15 @@ This module abstracts the use of the most used virtual points of sale in Spain. ## PayPal -Easy integration with PayPal. +### Paypal +[Paypal](https://www.paypal.com/) paypal payment available. + +### Bitpay +[Bitpay](bitpay.com) bitcoin payments, from wallet to checkout ## Spanish Virtual Points of Sale + ### Ceca [CECA](http://www.cajasdeahorros.es/) is the Spanish confederation of savings banks. diff --git a/README.rst b/README.rst index d484de9..ceeec40 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,17 @@ Implemented payment methods PayPal ------ -Easy integration with PayPal. +.. _paypal-1: + +Paypal +~~~~~~ + +`Paypal`_ paypal payment available. + +Bitpay +~~~~~~ + +`Bitpay`_ bitcoin payments, from wallet to checkout Spanish Virtual Points of Sale ------------------------------ @@ -148,15 +158,11 @@ bank have the user selected. Payment_confirm view ~~~~~~~~~~~~~~~~~~~~ -\````python @csrf_exempt def payment_confirmation(request, -virtualpos_type): “”" This view will be called by the bank. “”" # -Directly call to confirm_payment view - -:: - - # Or implement th +\````python @csrf_exempt def payment_confirmation(request, virtualpos .. _MIT LICENSE: LICENSE +.. _Paypal: https://www.paypal.com/ +.. _Bitpay: bitpay.com .. _CECA: http://www.cajasdeahorros.es/ .. _RedSyS: http://www.redsys.es/ .. _Santander Elavon: https://www.santanderelavon.com/ @@ -167,4 +173,4 @@ Directly call to confirm_payment view .. _Pytz: https://pypi.python.org/pypi/pytz .. _Requests: https://pypi.python.org/pypi/requests .. _manual: manual/COMMON.md -.. _djshop: https://github.com/diegojromerolopez/djshop +.. _djshop: https://github.com/diegojromerolopez/djshop \ No newline at end of file diff --git a/manual/COMMON.md b/manual/COMMON.md index 3c14f4f..c659f5e 100644 --- a/manual/COMMON.md +++ b/manual/COMMON.md @@ -145,4 +145,5 @@ Especificaciones particulares de cada TPV - [CECA](manual/vpos_types/CECA.md) - [PAYPAL](manual/vpos_types/PAYPAL.md) - [REDSYS](manual/vpos_types/REDSYS.md) +- [BITPAY](manual/vpos_types/BITPAY.md) - Santander Enlavon está pendiente. diff --git a/manual/vpos_types/BITPAY.md b/manual/vpos_types/BITPAY.md new file mode 100644 index 0000000..bfe7a01 --- /dev/null +++ b/manual/vpos_types/BITPAY.md @@ -0,0 +1,116 @@ +# Biptay + +##### Configuración de la operación + +```python + def setupPayment(self, code_len=40): +``` + +Realiza una petición a Bitpay, ``Create an Invoice``. + +Los parámetros que se deben incorporar pare crear una orden de pago en bitpay son los siguientes (* son obligatórios). + + +| Name | Description | +|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| price* | This is the amount that is required to be collected from the buyer. Note, if this is specified in a currency other than BTC, the price will be converted into BTC at market exchange rates to determine the amount collected from the buyer. | +| currency* | This is the currency code set for the price setting. The pricing currencies currently supported are USD, EUR, BTC, and all of the codes listed on this page: ​https://bitpay.com/bitcoin­exchange­rates | +| posData | A passthrough variable provided by the merchant and designed to be used by the merchant to correlate the invoice with an order or other object in their system. Maximum string length is 100 characters.,This passthrough variable can be a JSON­encoded string, for example,posData: ‘ ``{ “ref” : 711454, “affiliate” : “spring112” }`` ‘ | +| notificationURL | A URL to send status update messages to your server (this must be an https URL, unencrypted http URLs or any other type of URL is not supported).,Bitpay.com will send a POST request with a JSON encoding of the invoice to this URL when the invoice status changes. | +| transactionSpeed | default value: set in your ​https://bitpay.com/order­settings​, the default value set in your merchant dashboard is “medium”. - **high**: An invoice is considered to be "confirmed" immediately upon receipt of payment., - **medium**: An invoice is considered to be "confirmed" after 1 blockconfirmation (~10 minutes). - **low**: An invoice is considered to be "confirmed" after 6 block confirmations (~1 hour). | +| fullNotifications | Notifications will be sent on every status change. | +| notificationEmail | Bitpay will send an email to this email address when the invoice status changes. | +| redirectURL | This is the URL for a return link that is displayed on the receipt, to return the shopper back to your website after a successful purchase. This could be a page specific to the order, or to their account. | + +> En nuetra implementación particular incorporamos los siguientes campos: + +```python + +params = { + 'price': self.importe, + 'currency': self.currency, + 'redirectURL': self.parent.operation.url_ok, + 'itemDesc': self.parent.operation.description, + 'notificationURL': self.notification_url, + # Campos libres para el programador, puedes introducir cualquier información útil. + # En nuestro caso el prefijo de la operación, que ayuda a TPV proxy a identificar el servidor + # desde donde se ha ejecutado la operación. + 'posData': json.dumps({"operation_number_prefix": self.operation_number_prefix}) + 'fullNotifications': True +} + +``` + +>Tales parámetros se envían como ``JSON`` con el verbo http ``POST``. + +>Como resultado de esta petición obtenemos una respuesta JSON como la siguiente: + +```json +{ + u'status':u'new', + u'btcPaid':u'0.000000', + u'invoiceTime':1518093126310, + u'buyerFields':{ }, + u'currentTime':1518093126390, + u'url': u'https://bitpay.com/invoice?id=X7VytgMABGuv5Vo4xPsRhb', + u'price':5, + u'btcDue':u'0.000889', + u'btcPrice':u'0.000721', + u'currency':u'EUR', + u'rate':6938.79, + u'paymentSubtotals':{ + u'BTC':72100 + }, + u'paymentTotals':{ + u'BTC':88900 + }, + u'expirationTime':1518094026310, + u'id':u'X7VytgMABGuv5Vo4xPsRhb', + u'exceptionStatus':False +} +``` + +>De esta petición capturamos el ``id`` para almacenarlo como identificador de la operación. + +--- + +##### Generación de los datos de pago que se enviarán a la pasarela + +```python + def getPaymentFormData(self): +``` + +>Dada la URL principal de bitpay (entorno test o estable) y el identificador de la operación, construimos un formulario ``GET`` con referencia a la url de pago de bitpay. + +>Al hacer **submit** de este formulario el usuario es redirigido a la plataforma de Bitpay y cuando termine la operación vuelve a la aplicación, (a la url de vuelta establecida). + +--- + +##### Obtención de los datos de pago enviados por la pasarela + +```python + def receiveConfirmation(request): +``` + +>Cuando se produce un cambio de estado en la operación de pago realizada anteriormente, el servidor de bitpay hace una petición POST a la url ``confirm/``, indicando el nuevo estado. Este método tiene la responsabilidad de identificar la operación dedo el ``id`` y capturar el ``status``. + +--- + + +##### Verificación de la operación + +```python + def verifyConfirmation(self): +``` + +>Tiene la responsabilidad de verificar que el nuevo estado comunicado en ``receiveConfirmation`` se corresponde con **"confirmed"**, ello quiere decir que el pago ha sido escrito correctamente en blockchain. +--- + +##### Confirmación de la operación + +```python + def charge(self): +``` + +En Bitpay no es importante la respuesta de la url ``confirm/``, siempre y cuando sea un 200, por tanto nosotros enviamos el string **"OK"** + From c91901e91cc904300c333827425a8b7b13d5170d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Mon, 12 Feb 2018 12:02:20 +0100 Subject: [PATCH 44/72] add bitpay documentation --- README.md | 2 -- README.rst | 8 ++------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a174fa9..771497e 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,6 @@ This module abstracts the use of the most used virtual points of sale in Spain. # Implemented payment methods -## PayPal - ### Paypal [Paypal](https://www.paypal.com/) paypal payment available. diff --git a/README.rst b/README.rst index ceeec40..de0c45b 100644 --- a/README.rst +++ b/README.rst @@ -18,11 +18,6 @@ License Implemented payment methods =========================== -PayPal ------- - -.. _paypal-1: - Paypal ~~~~~~ @@ -158,7 +153,8 @@ bank have the user selected. Payment_confirm view ~~~~~~~~~~~~~~~~~~~~ -\````python @csrf_exempt def payment_confirmation(request, virtualpos +\````python @csrf_exempt def payment_confirmation(request, +virtualpos_type): “” .. _MIT LICENSE: LICENSE .. _Paypal: https://www.paypal.com/ From 8f4d9f7c79f998e79b0a2c7ef3a4c96a956e9455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Mon, 12 Feb 2018 12:04:27 +0100 Subject: [PATCH 45/72] add bitpay documentation --- README.md | 2 +- README.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 771497e..a2da39c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ This module abstracts the use of the most used virtual points of sale in Spain. [Paypal](https://www.paypal.com/) paypal payment available. ### Bitpay -[Bitpay](bitpay.com) bitcoin payments, from wallet to checkout +[Bitpay](http://bitpay.com) bitcoin payments, from wallet to checkout ## Spanish Virtual Points of Sale diff --git a/README.rst b/README.rst index de0c45b..0c9855e 100644 --- a/README.rst +++ b/README.rst @@ -154,11 +154,11 @@ Payment_confirm view ~~~~~~~~~~~~~~~~~~~~ \````python @csrf_exempt def payment_confirmation(request, -virtualpos_type): “” +virtualpos_typ .. _MIT LICENSE: LICENSE .. _Paypal: https://www.paypal.com/ -.. _Bitpay: bitpay.com +.. _Bitpay: http://bitpay.com .. _CECA: http://www.cajasdeahorros.es/ .. _RedSyS: http://www.redsys.es/ .. _Santander Elavon: https://www.santanderelavon.com/ From 5593469c38cedf261dce842172abe75e65027f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Mon, 12 Feb 2018 12:07:20 +0100 Subject: [PATCH 46/72] add bitpay documentation --- manual/vpos_types/BITPAY.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/manual/vpos_types/BITPAY.md b/manual/vpos_types/BITPAY.md index bfe7a01..b5af3a4 100644 --- a/manual/vpos_types/BITPAY.md +++ b/manual/vpos_types/BITPAY.md @@ -47,26 +47,26 @@ params = { ```json { - u'status':u'new', - u'btcPaid':u'0.000000', - u'invoiceTime':1518093126310, - u'buyerFields':{ }, - u'currentTime':1518093126390, - u'url': u'https://bitpay.com/invoice?id=X7VytgMABGuv5Vo4xPsRhb', - u'price':5, - u'btcDue':u'0.000889', - u'btcPrice':u'0.000721', - u'currency':u'EUR', - u'rate':6938.79, - u'paymentSubtotals':{ - u'BTC':72100 + "status":"new", + "btcPaid":"0.000000", + "invoiceTime":1518093126310, + "buyerFields":{ }, + "currentTime":1518093126390, + "url":"https://bitpay.com/invoice?id=X7VytgMABGuv5Vo4xPsRhb", + "price":5, + "btcDue":"0.000889", + "btcPrice":"0.000721", + "currency":"EUR", + "rate":6938.79, + "paymentSubtotals":{ + "BTC":72100 }, - u'paymentTotals':{ - u'BTC':88900 + "paymentTotals":{ + "BTC":88900 }, - u'expirationTime':1518094026310, - u'id':u'X7VytgMABGuv5Vo4xPsRhb', - u'exceptionStatus':False + "expirationTime":1518094026310, + "id":"X7VytgMABGuv5Vo4xPsRhb", + "exceptionStatus": false } ``` From d888a0ab67fb0b49c009b68b0f993afd41c318a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Mon, 12 Feb 2018 12:36:25 +0100 Subject: [PATCH 47/72] minors changes --- setup.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index cb28ecb..e25abee 100755 --- a/setup.py +++ b/setup.py @@ -26,11 +26,13 @@ import os from setuptools import setup, find_packages +from os import path -try: - long_description = open('README.rst').read() -except(IOError, ImportError): - long_description = open('README.md').read() +current_path = path.abspath(path.dirname(__file__)) + +# Get the long description from the README file +with open(path.join(current_path, 'README.rst')) as f: + long_description = f.read() data_files = [] for dirpath, dirnames, filenames in os.walk('.'): From a0e07bb24f0a42df2b2878c0221ffacd80da4d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Mon, 19 Feb 2018 14:31:49 +0100 Subject: [PATCH 48/72] Paid mark operation as confirmed --- djangovirtualpos/models.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index 885ddcc..d2738e1 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -3139,13 +3139,13 @@ def verifyConfirmation(self): # Comprueba si el envío es correcto # Para esto, comprobamos si hay alguna operación que tenga el mismo # número de operación - operation = VPOSPaymentOperation.objects.filter(operation_number=self.bitpay_id) + operation = VPOSPaymentOperation.objects.filter(operation_number=self.bitpay_id, status='pending') if operation: # En caso de recibir, un estado confirmado () # NOTA: Bitpay tiene los siguientes posibles estados: # new, paid, confirmed, complete, expired, invalid. - if self.status == "confirmed": + if self.status == "paid": dlprint(u"La operación es confirmada") return True diff --git a/setup.py b/setup.py index e25abee..fff4e6d 100755 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ setup( name="django-virtual-pos", - version="1.6.8", + version="1.6.8.3", install_requires=[ "django", "beautifulsoup4", From 80d3cefd3f7e21d35e715c506a7d83f1bbe43438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Tue, 3 Apr 2018 11:43:42 +0200 Subject: [PATCH 49/72] redsys preauth policy --- djangovirtualpos/models.py | 645 ++++++++++++++++++++++++++----------- 1 file changed, 462 insertions(+), 183 deletions(-) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index d2738e1..768d22d 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -188,11 +188,13 @@ def save(self, *args, **kwargs): class VPOSCantCharge(Exception): pass # Excepción para indicar que no se ha implementado una operación para un tipo de TPV en particular. -class VPOSOperationDontImplemented(Exception): pass +class VPOSOperationNotImplemented(Exception): pass # Cuando se produce un error al realizar una operación en concreto. class VPOSOperationException(Exception): pass +# La operacióm ya fue confirmada anteriormente mediante otra notificación recibida +class VPOSOperationAlreadyConfirmed(Exception): pass #################################################################### ## Clase que contiene las operaciones de pago de forma genérica @@ -911,7 +913,7 @@ def responseNok(self, **kwargs): ## Paso R. (Refund) Configura el TPV en modo devolución ## TODO: No implementado def refund(self, refund_amount, description): - raise VPOSOperationDontImplemented(u"No se ha implementado la operación de devolución particular para CECA.") + raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular para CECA.") #################################################################### ## Generador de firma para el envío @@ -999,6 +1001,9 @@ class VPOSRedsys(VirtualPointOfSale): # Número de terminal que le asignará su banco terminal_id = models.CharField(max_length=3, null=False, blank=False, verbose_name="TerminalID") + # Habilita mecanismo de preautorización + confirmación o anulación. + enable_preauth_policy = models.BooleanField(default=False, verbose_name=u"Habilitar política de preautorización") + # Clave de cifrado SHA-256 para el entorno de prueba encryption_key_testing_sha256 = models.CharField(max_length=64, null=True, default=None, verbose_name="Encryption Key SHA-256 para el entorno de pruebas") @@ -1532,8 +1537,12 @@ class VPOSRedsys(VirtualPointOfSale): cifrado = "SHA1" # Tipo de moneda usada en la operación, en este caso sera Euros tipo_moneda = "978" - # Indica qué tipo de transacción se utiliza, en este caso usamos 0-Autorización - transaction_type = "0" + + # Indica qué tipo de transacción se utiliza, en función del parámetro enable_preauth-policy puede ser: + # 0 - Autorización + # 1 - Preautorización + transaction_type = None + # Idioma por defecto a usar. Español idioma = "001" @@ -1576,6 +1585,15 @@ def configurePayment(self, **kwargs): # URL de pago según el entorno self.url = self.REDSYS_URL[self.parent.environment] + # Configurar el tipo de transacción se utiliza, en función del parámetro enable_preauth-policy. + # La autorización tienen el código 0. + self.transaction_type = "0" + + # La pre-autorización tiene el código 1 + if self.enable_preauth_policy: + dlprint(u"Configuracion TPV en modo Pre-Autorizacion") + self.transaction_type = "1" + # Formato para Importe: según redsys, ha de tener un formato de entero positivo, con las dos últimas posiciones # ocupadas por los decimales self.importe = "{0:.2f}".format(float(self.parent.operation.amount)).replace(".", "") @@ -1720,6 +1738,10 @@ def _receiveConfirmationHTTPPOST(request): # Operation number operation_number = operation_data.get("Ds_Order") operation = VPOSPaymentOperation.objects.get(operation_number=operation_number) + + if operation.status != "pending": + raise VPOSOperationAlreadyConfirmed("Operación ya confirmada") + operation.confirmation_data = {"GET": request.GET.dict(), "POST": request.POST.dict()} operation.confirmation_code = operation_number @@ -1747,7 +1769,7 @@ def _receiveConfirmationHTTPPOST(request): # Iniciamos los valores recibidos en el delegado # Datos de la operación al completo - # Usado para recuperar los datos la referencia + # Usado para recuperar los datos la referencia vpos.delegated.ds_merchantparameters = operation_data ## Datos que llegan por POST @@ -1807,6 +1829,10 @@ def _receiveConfirmationSOAP(request): errormsg = u'' operation = VPOSPaymentOperation.objects.get(operation_number=ds_order) + + if operation.status != "pending": + raise VPOSOperationAlreadyConfirmed("Operación ya confirmada") + operation.confirmation_data = {"GET": "", "POST": xml_content} operation.confirmation_code = ds_order operation.response_code = VPOSRedsys._format_ds_response_code(ds_response) + errormsg @@ -1842,6 +1868,10 @@ def _receiveConfirmationSOAP(request): # Código que indica el tipo de transacción vpos.delegated.ds_response = root.xpath("//Message/Request/Ds_Response/text()")[0] + ds_transaction_type = root.xpath("//Message/Request/Ds_TransactionType/text()")[0] + + if not (vpos.delegated.enable_preauth_policy and ds_transaction_type == "1"): + raise VPOSOperationException("Configuración de política de confirmación no coincide con respuesta Redsys") # Usado para recuperar los datos la referencia vpos.delegated.ds_merchantparameters = {} @@ -1878,10 +1908,11 @@ def verifyConfirmation(self): # Comprobar que el resultado se corresponde a un pago autorizado # por RedSys. Los pagos autorizados son todos los Ds_Response entre # 0000 y 0099 [manual TPV Virtual SIS v1.0, pág. 31] - if len(self.ds_response) != 4 or self.ds_response.isdigit() == False or self.ds_response[:2] != "00": - dlprint(u"Transacción no autorizada por RedSys. Ds_Response es {0} (no está entre 0000-0099)".format( - self.ds_response)) - return False + if len(self.ds_response) != 4 or not self.ds_response.isdigit(): + if self.ds_response[:2] != "00" or self.ds_response[:2] != "99": + dlprint(u"Transacción no autorizada por RedSys. Ds_Response es {0} (no está entre 0000-0099)".format( + self.ds_response)) + return False # Todo OK return True @@ -1891,7 +1922,19 @@ def verifyConfirmation(self): ## comunicamos con la pasarela de pago para que marque la operación ## como pagada. Sólo se usa en CECA def charge(self): - if self.soap_request: + # En caso de tener habilitada la preautorización + # no nos importa el tipo de confirmación. + if self.enable_preauth_policy: + # Cuando se tiene habilitada política de preautorización. + dlprint("Confirmar medinate política de preautorizacion") + if self._confirm_preauthorization(): + return HttpResponse("OK") + else: + self.responseNok() + + # En otro caso la confirmación continua haciendose como antes. + # Sin cambiar nada. + elif self.soap_request: dlprint("responseOk SOAP") # Respuesta a notificación HTTP SOAP response = 'OK' @@ -1910,18 +1953,26 @@ def charge(self): dlprint("RESPUESTA SOAP:" + out) return HttpResponse(out, "text/xml") + else: - dlprint(u"responseOk HTTP POST (respuesta vacía)") + dlprint(u"responseOk HTTP POST") # Respuesta a notificación HTTP POST # En RedSys no se exige una respuesta, por parte del comercio, para verificar # la operación, pasamos una respuesta vacia - return HttpResponse("") + return HttpResponse("OK") #################################################################### ## Paso 3.3b. Si ha habido un error en el pago, se ha de dar una ## respuesta negativa a la pasarela bancaria. def responseNok(self, **kwargs): - if self.soap_request: + + if self.enable_preauth_policy: + # Cuando se tiene habilitada política de preautorización. + dlprint("Enviar mensaje para cancelar una preautorizacion") + self._cancel_preauthorization() + return HttpResponse("") + + elif self.soap_request: dlprint("responseOk SOAP") # Respuesta a notificación HTTP SOAP response = 'KO' @@ -1940,6 +1991,7 @@ def responseNok(self, **kwargs): dlprint("RESPUESTA SOAP:" + out) return HttpResponse(out, "text/xml") + else: dlprint(u"responseNok HTTP POST (respuesta vacía)") # Respuesta a notificación HTTP POST @@ -1964,7 +2016,6 @@ def refund(self, refund_amount, description): y en caso que se produzca, avisar a los desarrolladores responsables del módulo 'DjangoVirtualPost' para su actualización. - :param payment_operation: Operación de pago asociada a la devolución. :param refund_amount: Cantidad de la devolución. :param description: Motivo o comentario de la devolución. :return: True | False según se complete la operación con éxito. @@ -1972,7 +2023,8 @@ def refund(self, refund_amount, description): # Modificamos el tipo de operación para indicar que la transacción # es de tipo devolución automática. - # URL de pago según el entorno + # URL de pago según el entorno. + self.url = self.REDSYS_URL[self.parent.environment] # IMPORTANTE: Este es el código de operación para hacer devoluciones. @@ -2065,7 +2117,233 @@ def refund(self, refund_amount, description): # No aparece mensaje de error ni de ok else: - raise VPOSOperationException("La resupuesta HTML con la pantalla de devolución no muestra mensaje informado de forma expícita, si la operación se produce con éxito error, (revisar método 'VPOSRedsys.refund').") + raise VPOSOperationException("La resupuesta HTML con la pantalla de devolución " + "no muestra mensaje informado de forma expícita," + "si la operación se produce con éxito error, (revisar método 'VPOSRedsys.refund').") + + # Respuesta HTTP diferente a 200 + else: + status = False + + return status + + def _confirm_preauthorization(self): + """ + Realiza petición HTTP POST con los parámetros adecuados para + confirmar una operación de pre-autorización. + NOTA: La respuesta de esta petición es un HTML, aplicamos scraping + para asegurarnos que corresponde a una pantalla de éxito. + NOTA2: Si el HTML anterior no proporciona información de éxito o error. Lanza una excepción. + :return: status: Bool + """ + + # URL de pago según el entorno + self.url = self.REDSYS_URL[self.parent.environment] + + # IMPORTANTE: Este es el código de operación para hacer confirmación de preautorizacon. + self.transaction_type = 2 + + # Idioma de la pasarela, por defecto es español, tomamos + # el idioma actual y le asignamos éste + self.idioma = self.IDIOMAS["es"] + lang = translation.get_language() + if lang in self.IDIOMAS: + self.idioma = self.IDIOMAS[lang] + + self.importe = "{0:.2f}".format(float(self.parent.operation.amount)).replace(".", "") + if self.importe == "000": + self.importe = "0" + + order_data = { + # Indica el importe de la venta + "DS_MERCHANT_AMOUNT": self.importe, + + # Indica el número de operacion + "DS_MERCHANT_ORDER": self.parent.operation.operation_number, + + # Código FUC asignado al comercio + "DS_MERCHANT_MERCHANTCODE": self.merchant_code, + + # Indica el tipo de moneda a usar + "DS_MERCHANT_CURRENCY": self.tipo_moneda, + + # Indica que tipo de transacción se utiliza + "DS_MERCHANT_TRANSACTIONTYPE": self.transaction_type, + + # Indica el terminal + "DS_MERCHANT_TERMINAL": self.terminal_id, + + # Obligatorio si se tiene confirmación online. + "DS_MERCHANT_MERCHANTURL": self.merchant_response_url, + + # URL a la que se redirige al usuario en caso de que la venta haya sido satisfactoria + "DS_MERCHANT_URLOK": self.parent.operation.url_ok, + + # URL a la que se redirige al usuario en caso de que la venta NO haya sido satisfactoria + "DS_MERCHANT_URLKO": self.parent.operation.url_nok, + + # Se mostrará al titular en la pantalla de confirmación de la compra + "DS_MERCHANT_PRODUCTDESCRIPTION": self.parent.operation.description, + + # Indica el valor del idioma + "DS_MERCHANT_CONSUMERLANGUAGE": self.idioma, + + # Representa la suma total de los importes de las cuotas + "DS_MERCHANT_SUMTOTAL": self.importe, + } + + json_order_data = json.dumps(order_data) + packed_order_data = base64.b64encode(json_order_data) + + data = { + "Ds_SignatureVersion": "HMAC_SHA256_V1", + "Ds_MerchantParameters": packed_order_data, + "Ds_Signature": self._redsys_hmac_sha256_signature(packed_order_data) + } + + headers = {'enctype': 'application/x-www-form-urlencoded'} + + # Realizamos petición POST con los datos de la operación y las cabeceras necesarias. + confirmpreauth_html_request = requests.post(self.url, data=data, headers=headers) + + if confirmpreauth_html_request.status_code == 200: + + # Iniciamos un objeto BeautifulSoup (para poder leer los elementos del DOM del HTML recibido). + html = BeautifulSoup(confirmpreauth_html_request.text, "html.parser") + + # Buscamos elementos significativos del DOM que nos indiquen si la operación se ha realizado correctamente o no. + confirmpreauth_message_error = html.find('text', {'lngid': 'noSePuedeRealizarOperacion'}) + confirmpreauth_message_ok = html.find('text', {'lngid': 'operacionAceptada'}) + + # Cuando en el DOM del documento HTML aparece un mensaje de error. + if confirmpreauth_message_error: + dlprint(confirmpreauth_message_error) + dlprint(u'Error realizando la operación') + status = False + + # Cuando en el DOM del documento HTML aparece un mensaje de ok. + elif confirmpreauth_message_ok: + dlprint(u'Operación realizada correctamente') + dlprint(confirmpreauth_message_ok) + status = True + + # No aparece mensaje de error ni de ok + else: + raise VPOSOperationException( + "La resupuesta HTML con la pantalla de confirmación no muestra mensaje informado de forma expícita," + " si la operación se produce con éxito/error, (revisar método 'VPOSRedsys._confirm_preauthorization').") + + # Respuesta HTTP diferente a 200 + else: + status = False + + return status + + def _cancel_preauthorization(self): + """ + Realiza petición HTTP POST con los parámetros adecuados para + anular una operación de pre-autorización. + NOTA: La respuesta de esta petición es un HTML, aplicamos scraping + para asegurarnos que corresponde a una pantalla de éxito. + NOTA2: Si el HTML anterior no proporciona información de éxito o error. Lanza una excepción. + :return: status: Bool + """ + + # URL de pago según el entorno + self.url = self.REDSYS_URL[self.parent.environment] + + # IMPORTANTE: Este es el código de operación para hacer cancelación de preautorizacon. + self.transaction_type = 9 + + # Idioma de la pasarela, por defecto es español, tomamos + # el idioma actual y le asignamos éste + self.idioma = self.IDIOMAS["es"] + lang = translation.get_language() + if lang in self.IDIOMAS: + self.idioma = self.IDIOMAS[lang] + + self.importe = "{0:.2f}".format(float(self.parent.operation.amount)).replace(".", "") + if self.importe == "000": + self.importe = "0" + + order_data = { + # Indica el importe de la venta + "DS_MERCHANT_AMOUNT": self.importe, + + # Indica el número de operacion + "DS_MERCHANT_ORDER": self.parent.operation.operation_number, + + # Código FUC asignado al comercio + "DS_MERCHANT_MERCHANTCODE": self.merchant_code, + + # Indica el tipo de moneda a usar + "DS_MERCHANT_CURRENCY": self.tipo_moneda, + + # Indica que tipo de transacción se utiliza + "DS_MERCHANT_TRANSACTIONTYPE": self.transaction_type, + + # Indica el terminal + "DS_MERCHANT_TERMINAL": self.terminal_id, + + # Obligatorio si se tiene confirmación online. + "DS_MERCHANT_MERCHANTURL": self.merchant_response_url, + + # URL a la que se redirige al usuario en caso de que la venta haya sido satisfactoria + "DS_MERCHANT_URLOK": self.parent.operation.url_ok, + + # URL a la que se redirige al usuario en caso de que la venta NO haya sido satisfactoria + "DS_MERCHANT_URLKO": self.parent.operation.url_nok, + + # Se mostrará al titular en la pantalla de confirmación de la compra + "DS_MERCHANT_PRODUCTDESCRIPTION": self.parent.operation.description, + + # Indica el valor del idioma + "DS_MERCHANT_CONSUMERLANGUAGE": self.idioma, + + # Representa la suma total de los importes de las cuotas + "DS_MERCHANT_SUMTOTAL": self.importe + } + + json_order_data = json.dumps(order_data) + packed_order_data = base64.b64encode(json_order_data) + + data = { + "Ds_SignatureVersion": "HMAC_SHA256_V1", + "Ds_MerchantParameters": packed_order_data, + "Ds_Signature": self._redsys_hmac_sha256_signature(packed_order_data) + } + + headers = {'enctype': 'application/x-www-form-urlencoded'} + + # Realizamos petición POST con los datos de la operación y las cabeceras necesarias. + confirmpreauth_html_request = requests.post(self.url, data=data, headers=headers) + + if confirmpreauth_html_request.status_code == 200: + + # Iniciamos un objeto BeautifulSoup (para poder leer los elementos del DOM del HTML recibido). + html = BeautifulSoup(confirmpreauth_html_request.text, "html.parser") + + # Buscamos elementos significativos del DOM que nos indiquen si la operación se ha realizado correctamente o no. + confirmpreauth_message_error = html.find('text', {'lngid': 'noSePuedeRealizarOperacion'}) + confirmpreauth_message_ok = html.find('text', {'lngid': 'operacionAceptada'}) + + # Cuando en el DOM del documento HTML aparece un mensaje de error. + if confirmpreauth_message_error: + dlprint(confirmpreauth_message_error) + dlprint(u'Error realizando la operación') + status = False + + # Cuando en el DOM del documento HTML aparece un mensaje de ok. + elif confirmpreauth_message_ok: + dlprint(u'Operación realizada correctamente') + dlprint(confirmpreauth_message_ok) + status = True + + # No aparece mensaje de error ni de ok + else: + raise VPOSOperationException( + "La resupuesta HTML con la pantalla de cancelación no muestra mensaje informado de forma expícita," + " si la operación se produce con éxito/error, (revisar método 'VPOSRedsys._cancel_preauthorization').") # Respuesta HTTP diferente a 200 else: @@ -2473,7 +2751,7 @@ def responseNok(self, **kwargs): ## Paso R. (Refund) Configura el TPV en modo devolución ## TODO: No implementado def refund(self, refund_amount, description): - raise VPOSOperationDontImplemented(u"No se ha implementado la operación de devolución particular para Paypal.") + raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular para Paypal.") ######################################################################################################################## @@ -2886,7 +3164,7 @@ def responseNok(self, **kwargs): ## Paso R. (Refund) Configura el TPV en modo devolución ## TODO: No implementado def refund(self, refund_amount, description): - raise VPOSOperationDontImplemented(u"No se ha implementado la operación de devolución particular para Santander-Elavon.") + raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular para Santander-Elavon.") #################################################################### @@ -2982,63 +3260,63 @@ def _verification_signature(self): return firma2 class VPOSBitpay(VirtualPointOfSale): - """ + """ Pago con criptomoneda usando la plataforma bitpay.com Siguiendo la documentación: https://bitpay.com/api """ - CURRENCIES = ( - ('EUR', 'Euro'), - ('USD', 'Dolares'), - ('BTC', 'Bitcoin'), - ) - - # Velocidad de la operación en función de la fortaleza de la confirmación en blockchain. - TRANSACTION_SPEED = ( - ('high', 'Alta'), # Se supone confirma en el momento que se ejecuta. - ('medium', 'Media'), # Se supone confirmada una vez se verifica 1 bloque. (~10 min) - ('low', 'Baja'), # Se supone confirmada una vez se verifican 6 bloques (~1 hora) - ) - - # Relación con el padre (TPV). - # Al poner el signo "+" como "related_name" evitamos que desde el padre - # se pueda seguir la relación hasta aquí (ya que cada uno de las clases - # que heredan de ella estará en una tabla y sería un lío). - parent = models.OneToOneField(VirtualPointOfSale, parent_link=True, related_name="+", null=False, db_column="vpos_id") - testing_api_key = models.CharField(max_length=512, null=True, blank=True, verbose_name="API Key de Bitpay para entorno de test") - production_api_key = models.CharField(max_length=512, null=False, blank=False, verbose_name="API Key de Bitpay para entorno de producción") - currency = models.CharField(max_length=3, choices=CURRENCIES, default='EUR', null=False, blank=False, verbose_name="Moneda (EUR, USD, BTC)") - transaction_speed = models.CharField(max_length=10, choices=TRANSACTION_SPEED, default='medium', null=False, blank=False, verbose_name="Velocidad de la operación") - notification_url = models.URLField(verbose_name="Url notificaciones actualización estados (https)", null=False, blank=False) - - # Prefijo usado para identicar al servidor desde el que se realiza la petición, en caso de usar TPV-Proxy. - operation_number_prefix = models.CharField(max_length=20, null=True, blank=True, verbose_name="Prefijo del número de operación") - - - bitpay_url = { - "production": { - "api": "https://bitpay.com/api/", - "create_invoice": "https://bitpay.com/api/invoice", - "payment": "https://bitpay.com/invoice/" - }, - "testing": { - "api": "https://test.bitpay.com/api/", - "create_invoice": "https://test.bitpay.com/api/invoice", - "payment": "https://test.bitpay.com/invoice/" - } - } - - def configurePayment(self, **kwargs): - - self.api_key = self.testing_api_key - - if self.parent.environment == "production": - self.api_key = self.production_api_key - - self.importe = self.parent.operation.amount - - def setupPayment(self, operation_number=None, code_len=40): - """ + CURRENCIES = ( + ('EUR', 'Euro'), + ('USD', 'Dolares'), + ('BTC', 'Bitcoin'), + ) + + # Velocidad de la operación en función de la fortaleza de la confirmación en blockchain. + TRANSACTION_SPEED = ( + ('high', 'Alta'), # Se supone confirma en el momento que se ejecuta. + ('medium', 'Media'), # Se supone confirmada una vez se verifica 1 bloque. (~10 min) + ('low', 'Baja'), # Se supone confirmada una vez se verifican 6 bloques (~1 hora) + ) + + # Relación con el padre (TPV). + # Al poner el signo "+" como "related_name" evitamos que desde el padre + # se pueda seguir la relación hasta aquí (ya que cada uno de las clases + # que heredan de ella estará en una tabla y sería un lío). + parent = models.OneToOneField(VirtualPointOfSale, parent_link=True, related_name="+", null=False, db_column="vpos_id") + testing_api_key = models.CharField(max_length=512, null=True, blank=True, verbose_name="API Key de Bitpay para entorno de test") + production_api_key = models.CharField(max_length=512, null=False, blank=False, verbose_name="API Key de Bitpay para entorno de producción") + currency = models.CharField(max_length=3, choices=CURRENCIES, default='EUR', null=False, blank=False, verbose_name="Moneda (EUR, USD, BTC)") + transaction_speed = models.CharField(max_length=10, choices=TRANSACTION_SPEED, default='medium', null=False, blank=False, verbose_name="Velocidad de la operación") + notification_url = models.URLField(verbose_name="Url notificaciones actualización estados (https)", null=False, blank=False) + + # Prefijo usado para identicar al servidor desde el que se realiza la petición, en caso de usar TPV-Proxy. + operation_number_prefix = models.CharField(max_length=20, null=True, blank=True, verbose_name="Prefijo del número de operación") + + + bitpay_url = { + "production": { + "api": "https://bitpay.com/api/", + "create_invoice": "https://bitpay.com/api/invoice", + "payment": "https://bitpay.com/invoice/" + }, + "testing": { + "api": "https://test.bitpay.com/api/", + "create_invoice": "https://test.bitpay.com/api/invoice", + "payment": "https://test.bitpay.com/invoice/" + } + } + + def configurePayment(self, **kwargs): + + self.api_key = self.testing_api_key + + if self.parent.environment == "production": + self.api_key = self.production_api_key + + self.importe = self.parent.operation.amount + + def setupPayment(self, operation_number=None, code_len=40): + """ Inicializa el pago Obtiene el número de operación, que para el caso de BitPay será el id dado :param operation_number: @@ -3046,118 +3324,119 @@ def setupPayment(self, operation_number=None, code_len=40): :return: """ - dlprint("BitPay.setupPayment") - if operation_number: - self.bitpay_id = operation_number - dlprint("Rescato el operation number para esta venta {0}".format(self.bitpay_id)) - return self.bitpay_id - - params = { - 'price': self.importe, - 'currency': self.currency, - 'redirectURL': self.parent.operation.url_ok, - 'itemDesc': self.parent.operation.description, - 'notificationURL': self.notification_url, - # Campos libres para el programador, puedes introducir cualquier información útil. - # En nuestro caso el prefijo de la operación, que ayuda a TPV proxy a identificar el servidor - # desde donde se ha ejecutado la operación. - 'posData': '{"operation_number_prefix": "' + str(self.operation_number_prefix) + '"}', - 'fullNotifications': True - } - - # URL de pago según el entorno - url = self.bitpay_url[self.parent.environment]["create_invoice"] - - post = json.dumps(params) - req = urllib2.Request(url) - base64string = base64.encodestring(self.api_key).replace('\n', '') - req.add_header("Authorization", "Basic %s" % base64string) - req.add_header("Content-Type", "application/json") - req.add_header("Content-Length", len(post)) - - json_response = urllib2.urlopen(req, post) - response = json.load(json_response) - - dlprint(u"Parametros que enviamos a Bitpay para crear la operación") - dlprint(params) - - dlprint(u"Respuesta de Bitpay") - dlprint(response) - - if not response.get("id"): - raise ValueError(u"ERROR. La respuesta no contiene id de invoice.") - - self.bitpay_id = response.get("id") - - return self.bitpay_id - - def getPaymentFormData(self): - """ + dlprint("BitPay.setupPayment") + if operation_number: + self.bitpay_id = operation_number + dlprint("Rescato el operation number para esta venta {0}".format(self.bitpay_id)) + return self.bitpay_id + + params = { + 'price': self.importe, + 'currency': self.currency, + 'redirectURL': self.parent.operation.url_ok, + 'itemDesc': self.parent.operation.description, + 'notificationURL': self.notification_url, + # Campos libres para el programador, puedes introducir cualquier información útil. + # En nuestro caso el prefijo de la operación, que ayuda a TPV proxy a identificar el servidor + # desde donde se ha ejecutado la operación. + 'posData': '{"operation_number_prefix": "' + str(self.operation_number_prefix) + '"}', + 'fullNotifications': True + } + + # URL de pago según el entorno + url = self.bitpay_url[self.parent.environment]["create_invoice"] + + post = json.dumps(params) + req = urllib2.Request(url) + base64string = base64.encodestring(self.api_key).replace('\n', '') + req.add_header("Authorization", "Basic %s" % base64string) + req.add_header("Content-Type", "application/json") + req.add_header("Content-Length", len(post)) + + json_response = urllib2.urlopen(req, post) + response = json.load(json_response) + + dlprint(u"Parametros que enviamos a Bitpay para crear la operación") + dlprint(params) + + dlprint(u"Respuesta de Bitpay") + dlprint(response) + + if not response.get("id"): + raise ValueError(u"ERROR. La respuesta no contiene id de invoice.") + + self.bitpay_id = response.get("id") + + return self.bitpay_id + + def getPaymentFormData(self): + """ Generar formulario (en este caso prepara un submit a la página de bitpay). """ - url = self.bitpay_url[self.parent.environment]["payment"] - data = {"id": self.bitpay_id} - - form_data = { - "data": data, - "action": url, - "method": "get" - } - return form_data - - @staticmethod - def receiveConfirmation(request, **kwargs): - - confirmation_body_param = json.loads(request.body) - - # Almacén de operaciones - try: - operation = VPOSPaymentOperation.objects.get(operation_number=confirmation_body_param.get("id")) - operation.confirmation_data = {"GET": request.GET.dict(), "POST": request.POST.dict(), "BODY": confirmation_body_param} - operation.save() - - dlprint("Operation {0} actualizada en receiveConfirmation()".format(operation.operation_number)) - vpos = operation.virtual_point_of_sale - except VPOSPaymentOperation.DoesNotExist: - # Si no existe la operación, están intentando - # cargar una operación inexistente - return False - - # Iniciamos el delegado y la operación - vpos._init_delegated() - vpos.operation = operation - - vpos.delegated.bitpay_id = operation.confirmation_data["BODY"].get("id") - vpos.delegated.status = operation.confirmation_data["BODY"].get("status") - - dlprint(u"Lo que recibimos de BitPay: ") - dlprint(operation.confirmation_data["BODY"]) - return vpos.delegated - - def verifyConfirmation(self): - # Comprueba si el envío es correcto - # Para esto, comprobamos si hay alguna operación que tenga el mismo - # número de operación - operation = VPOSPaymentOperation.objects.filter(operation_number=self.bitpay_id, status='pending') - - if operation: - # En caso de recibir, un estado confirmado () - # NOTA: Bitpay tiene los siguientes posibles estados: - # new, paid, confirmed, complete, expired, invalid. - if self.status == "paid": - dlprint(u"La operación es confirmada") - return True - - return False - - def charge(self): - dlprint(u"Marca la operacion como pagada") - return HttpResponse("OK") - - def responseNok(self, extended_status=""): - dlprint("responseNok") - return HttpResponse("NOK") - - def refund(self, refund_amount, description): - raise VPOSOperationDontImplemented(u"No se ha implementado la operación de devolución particular.") \ No newline at end of file + url = self.bitpay_url[self.parent.environment]["payment"] + data = {"id": self.bitpay_id} + + form_data = { + "data": data, + "action": url, + "method": "get" + } + return form_data + + @staticmethod + def receiveConfirmation(request, **kwargs): + + confirmation_body_param = json.loads(request.body) + + # Almacén de operaciones + try: + operation = VPOSPaymentOperation.objects.get(operation_number=confirmation_body_param.get("id")) + operation.confirmation_data = {"GET": request.GET.dict(), "POST": request.POST.dict(), "BODY": confirmation_body_param} + operation.save() + + dlprint("Operation {0} actualizada en receiveConfirmation()".format(operation.operation_number)) + vpos = operation.virtual_point_of_sale + except VPOSPaymentOperation.DoesNotExist: + # Si no existe la operación, están intentando + # cargar una operación inexistente + return False + + # Iniciamos el delegado y la operación + vpos._init_delegated() + vpos.operation = operation + + vpos.delegated.bitpay_id = operation.confirmation_data["BODY"].get("id") + vpos.delegated.status = operation.confirmation_data["BODY"].get("status") + + dlprint(u"Lo que recibimos de BitPay: ") + dlprint(operation.confirmation_data["BODY"]) + return vpos.delegated + + def verifyConfirmation(self): + # Comprueba si el envío es correcto + # Para esto, comprobamos si hay alguna operación que tenga el mismo + # número de operación + operation = VPOSPaymentOperation.objects.filter(operation_number=self.bitpay_id, status='pending') + + if operation: + # En caso de recibir, un estado confirmado () + # NOTA: Bitpay tiene los siguientes posibles estados: + # new, paid, confirmed, complete, expired, invalid. + if self.status == "paid": + dlprint(u"La operación es confirmada") + return True + + return False + + def charge(self): + dlprint(u"Marca la operacion como pagada") + return HttpResponse("OK") + + def responseNok(self, extended_status=""): + dlprint("responseNok") + return HttpResponse("NOK") + + def refund(self, refund_amount, description): + raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular.") + From 67383b8c0ca397b8711b664ad09cd7b1ac78ca51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Tue, 3 Apr 2018 11:44:59 +0200 Subject: [PATCH 50/72] change package version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fff4e6d..0af0cbe 100755 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ setup( name="django-virtual-pos", - version="1.6.8.3", + version="1.6.9", install_requires=[ "django", "beautifulsoup4", From 20feda4c9050a053792fbed41f459e3cd8c94e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Tue, 3 Apr 2018 12:07:33 +0200 Subject: [PATCH 51/72] improve bitpay error manage --- .../0013_vposredsys_enable_preauth_policy.py | 20 +++++++++++++++++++ djangovirtualpos/models.py | 6 ++++++ 2 files changed, 26 insertions(+) create mode 100644 djangovirtualpos/migrations/0013_vposredsys_enable_preauth_policy.py diff --git a/djangovirtualpos/migrations/0013_vposredsys_enable_preauth_policy.py b/djangovirtualpos/migrations/0013_vposredsys_enable_preauth_policy.py new file mode 100644 index 0000000..643a1a9 --- /dev/null +++ b/djangovirtualpos/migrations/0013_vposredsys_enable_preauth_policy.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2018-04-03 09:53 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangovirtualpos', '0012_auto_20180209_1222'), + ] + + operations = [ + migrations.AddField( + model_name='vposredsys', + name='enable_preauth_policy', + field=models.BooleanField(default=False, verbose_name='Habilitar pol\xedtica de preautorizaci\xf3n'), + ), + ] diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index 768d22d..100a82c 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -3362,6 +3362,12 @@ def setupPayment(self, operation_number=None, code_len=40): dlprint(u"Respuesta de Bitpay") dlprint(response) + if response.get("error"): + error = response.get("error") + message = error.get("message") + error_type = error.get("type") + raise ValueError(u"ERROR. {0} - {1}".format(message, error_type)) + if not response.get("id"): raise ValueError(u"ERROR. La respuesta no contiene id de invoice.") From 6aa22574c1df858d3ee3cb1be7c21d8b4894bef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Tue, 3 Apr 2018 13:02:59 +0200 Subject: [PATCH 52/72] refactor preauth selector from checkbox to choice --- .../migrations/0014_auto_20180403_1057.py | 24 +++++++++++++++ djangovirtualpos/models.py | 29 +++++++++++-------- 2 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 djangovirtualpos/migrations/0014_auto_20180403_1057.py diff --git a/djangovirtualpos/migrations/0014_auto_20180403_1057.py b/djangovirtualpos/migrations/0014_auto_20180403_1057.py new file mode 100644 index 0000000..fc9e111 --- /dev/null +++ b/djangovirtualpos/migrations/0014_auto_20180403_1057.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2018-04-03 10:57 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangovirtualpos', '0013_vposredsys_enable_preauth_policy'), + ] + + operations = [ + migrations.RemoveField( + model_name='vposredsys', + name='enable_preauth_policy', + ), + migrations.AddField( + model_name='vposredsys', + name='operative_type', + field=models.CharField(choices=[('authorization', 'autorizaci\xf3n'), ('pre-authorization', 'pre-autorizaci\xf3n')], default='authorization', max_length=512, verbose_name='Tipo de operativa'), + ), + ] diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index 100a82c..03a6d1f 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -982,6 +982,15 @@ def _verification_signature(self): ######################################################################################################################## ######################################################################################################################## +AUTHORIZATION_TYPE = "authorization" +PREAUTHORIZATION_TYPE = "pre-authorization" + +OPERATIVE_TYPES = ( + (AUTHORIZATION_TYPE, u"autorización"), + (PREAUTHORIZATION_TYPE, u"pre-autorización"), +) + + class VPOSRedsys(VirtualPointOfSale): """Información de configuración del TPV Virtual Redsys""" ## Todo TPV tiene una relación con los datos generales del TPV @@ -1002,7 +1011,8 @@ class VPOSRedsys(VirtualPointOfSale): terminal_id = models.CharField(max_length=3, null=False, blank=False, verbose_name="TerminalID") # Habilita mecanismo de preautorización + confirmación o anulación. - enable_preauth_policy = models.BooleanField(default=False, verbose_name=u"Habilitar política de preautorización") + + operative_type = models.CharField(max_length=512, choices=OPERATIVE_TYPES, default=AUTHORIZATION_TYPE, verbose_name=u"Tipo de operativa") # Clave de cifrado SHA-256 para el entorno de prueba encryption_key_testing_sha256 = models.CharField(max_length=64, null=True, default=None, @@ -1586,13 +1596,12 @@ def configurePayment(self, **kwargs): self.url = self.REDSYS_URL[self.parent.environment] # Configurar el tipo de transacción se utiliza, en función del parámetro enable_preauth-policy. - # La autorización tienen el código 0. - self.transaction_type = "0" - - # La pre-autorización tiene el código 1 - if self.enable_preauth_policy: + if self.operative_type == PREAUTHORIZATION_TYPE: dlprint(u"Configuracion TPV en modo Pre-Autorizacion") self.transaction_type = "1" + elif self.operative_type == AUTHORIZATION_TYPE: + dlprint(u"Configuracion TPV en modo Autorizacion") + self.transaction_type = "0" # Formato para Importe: según redsys, ha de tener un formato de entero positivo, con las dos últimas posiciones # ocupadas por los decimales @@ -1868,10 +1877,6 @@ def _receiveConfirmationSOAP(request): # Código que indica el tipo de transacción vpos.delegated.ds_response = root.xpath("//Message/Request/Ds_Response/text()")[0] - ds_transaction_type = root.xpath("//Message/Request/Ds_TransactionType/text()")[0] - - if not (vpos.delegated.enable_preauth_policy and ds_transaction_type == "1"): - raise VPOSOperationException("Configuración de política de confirmación no coincide con respuesta Redsys") # Usado para recuperar los datos la referencia vpos.delegated.ds_merchantparameters = {} @@ -1924,7 +1929,7 @@ def verifyConfirmation(self): def charge(self): # En caso de tener habilitada la preautorización # no nos importa el tipo de confirmación. - if self.enable_preauth_policy: + if self.operative_type == PREAUTHORIZATION_TYPE: # Cuando se tiene habilitada política de preautorización. dlprint("Confirmar medinate política de preautorizacion") if self._confirm_preauthorization(): @@ -1966,7 +1971,7 @@ def charge(self): ## respuesta negativa a la pasarela bancaria. def responseNok(self, **kwargs): - if self.enable_preauth_policy: + if self.operative_type == PREAUTHORIZATION_TYPE: # Cuando se tiene habilitada política de preautorización. dlprint("Enviar mensaje para cancelar una preautorizacion") self._cancel_preauthorization() From 5d0c2e74029cddf168a8d3b9353b73c596cd8708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Tue, 3 Apr 2018 14:21:40 +0200 Subject: [PATCH 53/72] change response redsys charge --- djangovirtualpos/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index 03a6d1f..0403474 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -1960,11 +1960,11 @@ def charge(self): return HttpResponse(out, "text/xml") else: - dlprint(u"responseOk HTTP POST") + dlprint(u"responseOk HTTP POST (respuesta vacía)") # Respuesta a notificación HTTP POST # En RedSys no se exige una respuesta, por parte del comercio, para verificar # la operación, pasamos una respuesta vacia - return HttpResponse("OK") + return HttpResponse("") #################################################################### ## Paso 3.3b. Si ha habido un error en el pago, se ha de dar una From 9a7d1dab2bd79dbe60d35258bdeec965724f8645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Tue, 3 Apr 2018 15:59:54 +0200 Subject: [PATCH 54/72] format readme file --- README.rst | 192 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 159 insertions(+), 33 deletions(-) diff --git a/README.rst b/README.rst index 0c9855e..e2fdd4f 100644 --- a/README.rst +++ b/README.rst @@ -1,66 +1,65 @@ -django-virtual-pos -================== +Django Virtual Pos +====== Django module that abstracts the flow of several virtual points of sale including PayPal What’s this? -============ +------------ This module abstracts the use of the most used virtual points of sale in Spain. License -======= +------------ `MIT LICENSE`_. Implemented payment methods -=========================== +------------ Paypal -~~~~~~ +------------ `Paypal`_ paypal payment available. Bitpay -~~~~~~ +------------ `Bitpay`_ bitcoin payments, from wallet to checkout Spanish Virtual Points of Sale ------------------------------- +------------ + +- Ceca -Ceca -~~~~ `CECA`_ is the Spanish confederation of savings banks. -RedSyS -~~~~~~ +- RedSyS `RedSyS`_ gives payment services to several Spanish banks like CaixaBank or Caja Rural. Santander Elavon -~~~~~~~~~~~~~~~~ +------------ `Santander Elavon`_ is one of the payment methods of the Spanish bank Santander. Requirements and Installation -============================= +====== Requirements ------------ - Python 2.7 (Python 3 not tested, contributors wanted!) -- `Django`_ -- `BeautifulSoup4`_ -- `lxml`_ -- `pycrypto`_ -- `Pytz`_ -- `Requests`_ +- ``Django`` +- ``BeautifulSoup4`` +- ``lxml`` +- ``pycrypto`` +- ``Pytz`` +- ``Requests`` Type: @@ -72,14 +71,14 @@ Installation ------------ From PyPi -~~~~~~~~~ +------------ .. code:: sh $ pip install django-virtual-pos From master branch -~~~~~~~~~~~~~~~~~~ +------------ Master branch will allways contain a working version of this module. @@ -88,7 +87,7 @@ Master branch will allways contain a working version of this module. $ pip install git+git://github.com/intelligenia/django-virtual-pos.git settings.py -~~~~~~~~~~~ +------------ Add the application djangovirtualpos to your settings.py: @@ -100,12 +99,12 @@ Add the application djangovirtualpos to your settings.py: ) Use -=== +====== -See this `manual`_ (currently only in Spanish). +See this ``manual`` (currently only in Spanish). Needed models -------------- +------------ You will need to implement this skeleton view using your own **Payment** model. @@ -120,15 +119,15 @@ And the following methods: - **online_confirm**: mark the payment as paid. Integration examples --------------------- +------------ -- `djshop`_ +- ``djshop`` Needed views ------------ Sale summary view -~~~~~~~~~~~~~~~~~ +------------ .. code:: python @@ -151,10 +150,137 @@ This file is needed to set initial payment attributes according to which bank have the user selected. Payment_confirm view -~~~~~~~~~~~~~~~~~~~~ +------------ + +.. code:: python + @csrf_exempt + def payment_confirmation(request, virtualpos_type): + """ + This view will be called by the bank. + """ + # Directly call to confirm_payment view + + # Or implement the following actions + + # Checking if the Point of Sale exists + virtual_pos = VirtualPointOfSale.receiveConfirmation(request, virtualpos_type=virtualpos_type) + + if not virtual_pos: + # The VPOS does not exist, inform the bank with a cancel + # response if needed + return VirtualPointOfSale.staticResponseNok(virtualpos_type) + + # Verify if bank confirmation is indeed from the bank + verified = virtual_pos.verifyConfirmation() + operation_number = virtual_pos.operation.operation_number + + with transaction.atomic(): + try: + # Getting your payment object from operation number + payment = Payment.objects.get(operation_number=operation_number, status="pending") + except Payment.DoesNotExist: + return virtual_pos.responseNok("not_exists") + + if verified: + # Charge the money and answer the bank confirmation + try: + response = virtual_pos.charge() + # Implement the online_confirm method in your payment + # this method will mark this payment as paid and will + # store the payment date and time. + payment.online_confirm() + except VPOSCantCharge as e: + return virtual_pos.responseNok(extended_status=e) + except Exception as e: + return virtual_pos.responseNok("cant_charge") + + else: + # Payment could not be verified + # signature is not right + response = virtual_pos.responseNok("verification_error") + + return response + +Payment ok view +------------ + +.. code:: python + def payment_ok(request, sale_code): + """ + Informs the user that the payment has been made successfully + :param payment_code: Payment code. + :param request: request. + """ + + # Load your Payment model given its code + payment = get_object_or_404(Payment, code=sale_code, status="paid") + + context = {'pay_status': "Done", "request": request} + return render(request, '', {'context': context, 'payment': payment}) + +Payment cancel view +------------ + +.. code:: python + + def payment_cancel(request, sale_code): + """ + Informs the user that the payment has been canceled + :param payment_code: Payment code. + :param request: request. + """ + + # Load your Payment model given its code + payment = get_object_or_404(Payment, code=sale_code, status="pending") + # Mark this payment as canceled + payment.cancel() + + context = {'pay_status': "Done", "request": request} + return render(request, '', {'context': context, 'payment': payment}) + + +Refund view +------------ + +.. code:: python + def refund(request, tpv, payment_code, amount, description): + """ + :param request: + :param tpv: TPV Id + :param payment_code: Payment code + :param amount: Refund Amount (Example 10.89). + :param description: Description of refund cause. + :return: + """ + + amount = Decimal(amount) + + try: + # Checking if the Point of Sale exists + tpv = VirtualPointOfSale.get(id=tpv) + # Checking if the Payment exists + payment = Payment.objects.get(code=payment_code, state="paid") + + except Payment.DoesNotExist as e: + return http_bad_request_response_json_error(message=u"Does not exist payment with code {0}".format(payment_code)) + + refund_status = tpv.refund(payment_code, amount, description) + + if refund_status: + message = u"Refund successful" + else: + message = u"Refund with erros" + + return http_response_json_ok(message) + +Authors +====== + +- Mario Barchéin marioREMOVETHIS@REMOVETHISintelligenia.com +- Diego J. Romero diegoREMOVETHIS@REMOVETHISintelligenia.com + +Remove REMOVETHIS to contact the authors. -\````python @csrf_exempt def payment_confirmation(request, -virtualpos_typ .. _MIT LICENSE: LICENSE .. _Paypal: https://www.paypal.com/ @@ -169,4 +295,4 @@ virtualpos_typ .. _Pytz: https://pypi.python.org/pypi/pytz .. _Requests: https://pypi.python.org/pypi/requests .. _manual: manual/COMMON.md -.. _djshop: https://github.com/diegojromerolopez/djshop \ No newline at end of file +.. _djshop: https://github.com/diegojromerolopez/djshop From 8fffd87c8151872750a235d09826d50d9594294f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Tue, 3 Apr 2018 16:01:07 +0200 Subject: [PATCH 55/72] format readme file --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index e2fdd4f..5265570 100644 --- a/README.rst +++ b/README.rst @@ -153,6 +153,7 @@ Payment_confirm view ------------ .. code:: python + @csrf_exempt def payment_confirmation(request, virtualpos_type): """ @@ -205,6 +206,7 @@ Payment ok view ------------ .. code:: python + def payment_ok(request, sale_code): """ Informs the user that the payment has been made successfully @@ -243,6 +245,7 @@ Refund view ------------ .. code:: python + def refund(request, tpv, payment_code, amount, description): """ :param request: From 74e0e09a7af331753fc264b51a8675c4d290a14e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Wed, 4 Apr 2018 11:25:21 +0200 Subject: [PATCH 56/72] add exception VPOSOperationAlreadyConfirmed Bitpay --- djangovirtualpos/models.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index 0403474..0d9f3f5 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -1749,7 +1749,7 @@ def _receiveConfirmationHTTPPOST(request): operation = VPOSPaymentOperation.objects.get(operation_number=operation_number) if operation.status != "pending": - raise VPOSOperationAlreadyConfirmed("Operación ya confirmada") + raise VPOSOperationAlreadyConfirmed(u"Operación ya confirmada") operation.confirmation_data = {"GET": request.GET.dict(), "POST": request.POST.dict()} operation.confirmation_code = operation_number @@ -1840,7 +1840,7 @@ def _receiveConfirmationSOAP(request): operation = VPOSPaymentOperation.objects.get(operation_number=ds_order) if operation.status != "pending": - raise VPOSOperationAlreadyConfirmed("Operación ya confirmada") + raise VPOSOperationAlreadyConfirmed(u"Operación ya confirmada") operation.confirmation_data = {"GET": "", "POST": xml_content} operation.confirmation_code = ds_order @@ -3403,6 +3403,10 @@ def receiveConfirmation(request, **kwargs): # Almacén de operaciones try: operation = VPOSPaymentOperation.objects.get(operation_number=confirmation_body_param.get("id")) + + if operation.status != "pending": + raise VPOSOperationAlreadyConfirmed(u"Operación ya confirmada") + operation.confirmation_data = {"GET": request.GET.dict(), "POST": request.POST.dict(), "BODY": confirmation_body_param} operation.save() @@ -3450,4 +3454,3 @@ def responseNok(self, extended_status=""): def refund(self, refund_amount, description): raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular.") - From 9841cf2c4b9f366e22128a852110a3e0550b45d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Fri, 6 Apr 2018 15:02:00 +0200 Subject: [PATCH 57/72] fix README text format --- README.rst | 51 ++++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/README.rst b/README.rst index 5265570..e700dab 100644 --- a/README.rst +++ b/README.rst @@ -1,35 +1,35 @@ Django Virtual Pos -====== +================== Django module that abstracts the flow of several virtual points of sale including PayPal What’s this? ------------- +------------------------- This module abstracts the use of the most used virtual points of sale in Spain. License ------------- +------------------------- `MIT LICENSE`_. Implemented payment methods ------------- +---------------------------- Paypal ------------- +------------------------- `Paypal`_ paypal payment available. Bitpay ------------- +------------------------- `Bitpay`_ bitcoin payments, from wallet to checkout Spanish Virtual Points of Sale ------------- +------------------------------ - Ceca @@ -42,16 +42,16 @@ Spanish Virtual Points of Sale or Caja Rural. Santander Elavon ------------- +---------------- `Santander Elavon`_ is one of the payment methods of the Spanish bank Santander. Requirements and Installation -====== +============================== Requirements ------------- +---------------- - Python 2.7 (Python 3 not tested, contributors wanted!) - ``Django`` @@ -68,17 +68,18 @@ Type: $ pip install django beautifulsoup4 lxml pycrypto pytz Installation ------------- +---------------- + From PyPi ------------- +---------------- .. code:: sh $ pip install django-virtual-pos From master branch ------------- +------------------- Master branch will allways contain a working version of this module. @@ -87,7 +88,7 @@ Master branch will allways contain a working version of this module. $ pip install git+git://github.com/intelligenia/django-virtual-pos.git settings.py ------------- +------------- Add the application djangovirtualpos to your settings.py: @@ -99,12 +100,12 @@ Add the application djangovirtualpos to your settings.py: ) Use -====== +---- See this ``manual`` (currently only in Spanish). Needed models ------------- +------------- You will need to implement this skeleton view using your own **Payment** model. @@ -119,15 +120,15 @@ And the following methods: - **online_confirm**: mark the payment as paid. Integration examples ------------- +----------------------- - ``djshop`` Needed views ------------- +-------------- Sale summary view ------------- +------------------ .. code:: python @@ -150,7 +151,7 @@ This file is needed to set initial payment attributes according to which bank have the user selected. Payment_confirm view ------------- +------------------------- .. code:: python @@ -203,7 +204,7 @@ Payment_confirm view return response Payment ok view ------------- +------------------------- .. code:: python @@ -221,7 +222,7 @@ Payment ok view return render(request, '', {'context': context, 'payment': payment}) Payment cancel view ------------- +-------------------- .. code:: python @@ -242,7 +243,7 @@ Payment cancel view Refund view ------------- +----------- .. code:: python @@ -277,7 +278,7 @@ Refund view return http_response_json_ok(message) Authors -====== +=============== - Mario Barchéin marioREMOVETHIS@REMOVETHISintelligenia.com - Diego J. Romero diegoREMOVETHIS@REMOVETHISintelligenia.com @@ -298,4 +299,4 @@ Remove REMOVETHIS to contact the authors. .. _Pytz: https://pypi.python.org/pypi/pytz .. _Requests: https://pypi.python.org/pypi/requests .. _manual: manual/COMMON.md -.. _djshop: https://github.com/diegojromerolopez/djshop +.. _djshop: https://github.com/diegojromerolopez/djshop \ No newline at end of file From d3e9dc21051c7fbd8ca6a606281b6b383e6a955a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20J=2E=20Barch=C3=A9in=20Molina?= Date: Fri, 6 Apr 2018 15:05:44 +0200 Subject: [PATCH 58/72] new setup.py based on https://github.com/kennethreitz/setup.py --- MANIFEST.in | 2 + djangovirtualpos/__version__.py | 3 + setup.cfg | 11 +-- setup.py | 137 ++++++++++++++++++++++++-------- 4 files changed, 114 insertions(+), 39 deletions(-) create mode 100644 MANIFEST.in create mode 100644 djangovirtualpos/__version__.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..042d30e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +recursive-exclude venv * +recursive-exclude dist * diff --git a/djangovirtualpos/__version__.py b/djangovirtualpos/__version__.py new file mode 100644 index 0000000..81571ea --- /dev/null +++ b/djangovirtualpos/__version__.py @@ -0,0 +1,3 @@ +VERSION = (1, 6, 9) + +__version__ = '.'.join(map(str, VERSION)) \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 861a9f5..79da4d7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,6 @@ -[egg_info] -tag_build = -tag_date = 0 -tag_svn_revision = 0 - +[bdist_wheel] +# This flag says to generate wheels that support both Python 2 and Python +# 3. If your code will not run unchanged on both Python 2 and 3, you will +# need to generate separate wheels for each Python version that you +# support. +universal = 1 \ No newline at end of file diff --git a/setup.py b/setup.py index 0af0cbe..d790fed 100755 --- a/setup.py +++ b/setup.py @@ -24,50 +24,119 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from setuptools import setup, Command, find_packages +from shutil import rmtree import os -from setuptools import setup, find_packages -from os import path +import sys +import io -current_path = path.abspath(path.dirname(__file__)) +# Package meta-data. +NAME = "django-virtual-pos" +DESCRIPTION = "django-virtual-pos is a module that abstracts the flow of paying in several online payment platforms." +URL = 'https://github.com/intelligenia/django-virtual-pos' +EMAIL = 'mario@intelligenia.com' +AUTHOR = 'intelligenia' +REQUIRES_PYTHON = '>=2.7.0' +VERSION = None +KEYWORDS = ["virtual", "point-of-sale", "puchases", "online", "payments"] -# Get the long description from the README file -with open(path.join(current_path, 'README.rst')) as f: - long_description = f.read() +# Directory with the package +PACKAGE = "djangovirtualpos" + +# What packages are required for this module to be executed? +REQUIRED = [ + "django", + "beautifulsoup4", + "lxml", + "pycrypto", + "pytz", + "requests" +] + +# The rest you shouldn't have to touch too much :) +# ------------------------------------------------ +# Except, perhaps the License and Trove Classifiers! +# If you do change the License, remember to change the Trove Classifier for that! + +here = os.path.abspath(os.path.dirname(__file__)) + +# Import the README and use it as the long-description. +# Note: this will only work if 'README.rst' is present in your MANIFEST.in file! +with io.open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: + long_description = '\n' + f.read() + +# Load the package's __version__.py module as a dictionary. +about = {} +if not VERSION: + with open(os.path.join(here, PACKAGE, '__version__.py')) as f: + exec (f.read(), about) +else: + about['__version__'] = VERSION + + +class UploadCommand(Command): + """Support setup.py upload.""" + + description = 'Build and publish the package.' + user_options = [] + + @staticmethod + def status(s): + """Prints things in bold.""" + print('\033[1m{0}\033[0m'.format(s)) + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + try: + self.status('Removing previous builds…') + rmtree(os.path.join(here, 'dist')) + except OSError: + pass + + self.status('Building Source and Wheel (universal) distribution…') + os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) + + self.status('Uploading the package to PyPi via Twine…') + os.system('twine upload dist/*') + + self.status('Pushing git tags…') + os.system('git tag v{0}'.format(about['__version__'])) + os.system('git push --tags') + + sys.exit() -data_files = [] -for dirpath, dirnames, filenames in os.walk('.'): - for i, dirname in enumerate(dirnames): - if dirname.startswith('.'): - del dirnames[i] - if '__init__.py' in filenames: - continue - elif filenames: - data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]]) setup( - name="django-virtual-pos", - version="1.6.9", - install_requires=[ - "django", - "beautifulsoup4", - "lxml", - "pycrypto", - "pytz", - "requests" - ], - author="intelligenia", - author_email="mario@intelligenia.es", - description="django-virtual-pos is a module that abstracts the flow of paying in several online payment platforms.", + name=NAME, + version=about['__version__'], + description=DESCRIPTION, long_description=long_description, + author=AUTHOR, + author_email=EMAIL, + python_requires=REQUIRES_PYTHON, + url=URL, + packages=find_packages(exclude=('tests',)), + # If your package is a single module, use this instead of 'packages': + # py_modules=['mypackage'], + + # entry_points={ + # 'console_scripts': ['mycli=mymodule:cli'], + # }, + install_requires=REQUIRED, + include_package_data=True, + license="MIT", classifiers=[ 'Development Status :: 5 - Production/Stable', 'Framework :: Django', 'License :: OSI Approved :: MIT License', ], - license="MIT", - keywords=["virtual", "point-of-sale", "puchases", "online", "payments"], - url='https://github.com/intelligenia/django-virtual-pos', - packages=find_packages('.'), - data_files=data_files, - include_package_data=True, + keywords=KEYWORDS, + cmdclass={ + 'upload': UploadCommand, + }, ) From 20f0fb93fffa1c12a0a12a87b0b5a45ca29f14d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20L=C3=B3pez=20P=C3=A9rez?= Date: Mon, 9 Apr 2018 12:21:38 +0200 Subject: [PATCH 59/72] debug messages --- djangovirtualpos/__version__.py | 2 +- djangovirtualpos/models.py | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/djangovirtualpos/__version__.py b/djangovirtualpos/__version__.py index 81571ea..18c4ebd 100644 --- a/djangovirtualpos/__version__.py +++ b/djangovirtualpos/__version__.py @@ -1,3 +1,3 @@ -VERSION = (1, 6, 9) +VERSION = (1, 6, 9, 2) __version__ = '.'.join(map(str, VERSION)) \ No newline at end of file diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index 0d9f3f5..040c3d2 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -1935,7 +1935,7 @@ def charge(self): if self._confirm_preauthorization(): return HttpResponse("OK") else: - self.responseNok() + return self.responseNok() # En otro caso la confirmación continua haciendose como antes. # Sin cambiar nada. @@ -2133,6 +2133,7 @@ def refund(self, refund_amount, description): return status def _confirm_preauthorization(self): + """ Realiza petición HTTP POST con los parámetros adecuados para confirmar una operación de pre-autorización. @@ -2142,6 +2143,8 @@ def _confirm_preauthorization(self): :return: status: Bool """ + dlprint("Entra en confirmacion de pre-autorizacion") + # URL de pago según el entorno self.url = self.REDSYS_URL[self.parent.environment] @@ -2200,6 +2203,8 @@ def _confirm_preauthorization(self): json_order_data = json.dumps(order_data) packed_order_data = base64.b64encode(json_order_data) + dlprint(json_order_data) + data = { "Ds_SignatureVersion": "HMAC_SHA256_V1", "Ds_MerchantParameters": packed_order_data, @@ -2211,6 +2216,8 @@ def _confirm_preauthorization(self): # Realizamos petición POST con los datos de la operación y las cabeceras necesarias. confirmpreauth_html_request = requests.post(self.url, data=data, headers=headers) + dlprint(confirmpreauth_html_request.text()) + if confirmpreauth_html_request.status_code == 200: # Iniciamos un objeto BeautifulSoup (para poder leer los elementos del DOM del HTML recibido). @@ -2254,6 +2261,8 @@ def _cancel_preauthorization(self): :return: status: Bool """ + dlprint("Entra en cancelacion de pre-autorizacion") + # URL de pago según el entorno self.url = self.REDSYS_URL[self.parent.environment] @@ -2310,6 +2319,9 @@ def _cancel_preauthorization(self): } json_order_data = json.dumps(order_data) + + dlprint(json_order_data) + packed_order_data = base64.b64encode(json_order_data) data = { From f648fa85359dcb79baf7bf1ce7ac553dda3244af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20J=2E=20Barch=C3=A9in=20Molina?= Date: Mon, 9 Apr 2018 12:25:59 +0200 Subject: [PATCH 60/72] gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ad76a82..c632427 100644 --- a/.gitignore +++ b/.gitignore @@ -95,4 +95,6 @@ ENV/ .DS_Store # Backup files -*~ \ No newline at end of file +*~ + +MANIFEST From d7d8c5c606dc3ee979cb6d2c7ac9da7becd89860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel?= Date: Mon, 9 Apr 2018 16:55:29 +0200 Subject: [PATCH 61/72] Update models.py --- djangovirtualpos/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index 040c3d2..e25e71a 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -2216,9 +2216,9 @@ def _confirm_preauthorization(self): # Realizamos petición POST con los datos de la operación y las cabeceras necesarias. confirmpreauth_html_request = requests.post(self.url, data=data, headers=headers) - dlprint(confirmpreauth_html_request.text()) - if confirmpreauth_html_request.status_code == 200: + + dlprint("_confirm_preauthorization status_code 200") # Iniciamos un objeto BeautifulSoup (para poder leer los elementos del DOM del HTML recibido). html = BeautifulSoup(confirmpreauth_html_request.text, "html.parser") From bb4d888314f75b6f85d2200c1c58beedb0a7cf9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel?= Date: Mon, 9 Apr 2018 16:55:56 +0200 Subject: [PATCH 62/72] Update __version__.py --- djangovirtualpos/__version__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/djangovirtualpos/__version__.py b/djangovirtualpos/__version__.py index 18c4ebd..6c1549a 100644 --- a/djangovirtualpos/__version__.py +++ b/djangovirtualpos/__version__.py @@ -1,3 +1,3 @@ -VERSION = (1, 6, 9, 2) +VERSION = (1, 6, 9, 3) -__version__ = '.'.join(map(str, VERSION)) \ No newline at end of file +__version__ = '.'.join(map(str, VERSION)) From b1a7f462c25486cd8a72980ebd412b6224b55668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel?= Date: Tue, 10 Apr 2018 14:43:42 +0200 Subject: [PATCH 63/72] Update models.py --- djangovirtualpos/models.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index e25e71a..66bb49a 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -1914,12 +1914,14 @@ def verifyConfirmation(self): # por RedSys. Los pagos autorizados son todos los Ds_Response entre # 0000 y 0099 [manual TPV Virtual SIS v1.0, pág. 31] if len(self.ds_response) != 4 or not self.ds_response.isdigit(): - if self.ds_response[:2] != "00" or self.ds_response[:2] != "99": - dlprint(u"Transacción no autorizada por RedSys. Ds_Response es {0} (no está entre 0000-0099)".format( - self.ds_response)) - return False - - # Todo OK + dlprint(u"Transacción no autorizada por RedSys. Ds_Response es {0} (no está entre 0000-0099)".format( + self.ds_response)) + return False + elif self.ds_response[:2] != "00": + dlprint(u"Transacción no autorizada por RedSys. Ds_Response es {0} (no está entre 0000-0099)".format( + self.ds_response)) + return False + return True #################################################################### @@ -2499,7 +2501,7 @@ class VPOSPaypal(VirtualPointOfSale): } Cancel_url = { "production": "http://" + settings.ALLOWED_HOSTS[0] + "/es/payment/cancel/", - "testing": "http://" + settings.ALLOWED_HOSTS[0] + "/es/payment/cancel/" + "testing": "http://" + settings.ALLOWEDcom_HOSTS[0] + "/es/payment/cancel/" } paypal_url = { "production": { From 9451906bee7ca5cd72513225094c1638730fce19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel?= Date: Tue, 10 Apr 2018 14:44:02 +0200 Subject: [PATCH 64/72] Update __version__.py --- djangovirtualpos/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangovirtualpos/__version__.py b/djangovirtualpos/__version__.py index 6c1549a..d34a7bb 100644 --- a/djangovirtualpos/__version__.py +++ b/djangovirtualpos/__version__.py @@ -1,3 +1,3 @@ -VERSION = (1, 6, 9, 3) +VERSION = (1, 6, 9, 4) __version__ = '.'.join(map(str, VERSION)) From f542a91a006916b107459265b7615990518a71fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel?= Date: Tue, 10 Apr 2018 14:46:37 +0200 Subject: [PATCH 65/72] Update models.py --- djangovirtualpos/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index 66bb49a..b690ae0 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -2501,7 +2501,7 @@ class VPOSPaypal(VirtualPointOfSale): } Cancel_url = { "production": "http://" + settings.ALLOWED_HOSTS[0] + "/es/payment/cancel/", - "testing": "http://" + settings.ALLOWEDcom_HOSTS[0] + "/es/payment/cancel/" + "testing": "http://" + settings.ALLOWED_HOSTS[0] + "/es/payment/cancel/" } paypal_url = { "production": { From 431acd16ac2231ceaca189c718b1dffed45557b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20J=2E=20Barch=C3=A9in=20Molina?= Date: Tue, 2 Oct 2018 17:00:39 +0200 Subject: [PATCH 66/72] Managing refund async confirmations for Redsys --- CHANGES.md | 2 + djangovirtualpos/__version__.py | 2 +- djangovirtualpos/models.py | 227 ++++++++++++++++++++++++++------ 3 files changed, 192 insertions(+), 39 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ce79f1c..68abf28 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ Django module that abstracts the flow of several virtual points of sale. # Releases +## 1.6.10 +- Managing refund async confirmations for Redsys ## 1.6.8 - Integration with [BitPay](https://bitpay.com/) diff --git a/djangovirtualpos/__version__.py b/djangovirtualpos/__version__.py index d34a7bb..885d21e 100644 --- a/djangovirtualpos/__version__.py +++ b/djangovirtualpos/__version__.py @@ -1,3 +1,3 @@ -VERSION = (1, 6, 9, 4) +VERSION = (1, 6, 10) __version__ = '.'.join(map(str, VERSION)) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index b690ae0..a749b0f 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -521,7 +521,7 @@ def responseNok(self, extended_status=""): return self.delegated.responseNok() #################################################################### - ## Paso R. (Refund) Configura el TPV en modo devolución + ## Paso R1 (Refund) Configura el TPV en modo devolución y ejecuta la operación ## TODO: Se implementa solo para Redsys def refund(self, operation_sale_code, refund_amount, description): """ @@ -565,7 +565,7 @@ def refund(self, operation_sale_code, refund_amount, description): self.operation.save() # Llamamos al delegado que implementa la funcionalidad en particular. - refund_response = self.delegated.refund(refund_amount, description) + refund_response = self.delegated.refund(operation_sale_code, refund_amount, description) if refund_response: refund_status = 'completed' @@ -582,6 +582,20 @@ def refund(self, operation_sale_code, refund_amount, description): return refund_response + #################################################################### + ## Paso R2.a. Respuesta positiva a confirmación asíncrona de refund + def refund_response_ok(self, extended_status=""): + dlprint("vpos.refund_response_ok") + return self.delegated.refund_response_ok() + + + #################################################################### + ## Paso R2.b. Respuesta negativa a confirmación asíncrona de refund + def refund_response_nok(self, extended_status=""): + dlprint("vpos.refund_response_nok") + return self.delegated.refund_response_nok() + + ######################################################################################################################## class VPOSRefundOperation(models.Model): """ @@ -598,6 +612,11 @@ class VPOSRefundOperation(models.Model): payment = models.ForeignKey(VPOSPaymentOperation, on_delete=models.PROTECT, related_name="refund_operations") + @property + def virtual_point_of_sale(self): + return self.payment.virtual_point_of_sale + + ## Guarda el objeto en BD, en realidad lo único que hace es actualizar los datetimes def save(self, *args, **kwargs): """ @@ -910,9 +929,19 @@ def responseNok(self, **kwargs): return HttpResponse("") #################################################################### - ## Paso R. (Refund) Configura el TPV en modo devolución + ## Paso R1. (Refund) Configura el TPV en modo devolución ## TODO: No implementado - def refund(self, refund_amount, description): + def refund(self, operation_sale_code, refund_amount, description): + raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular para CECA.") + + #################################################################### + ## Paso R2.a. Respuesta positiva a confirmación asíncrona de refund + def refund_response_ok(self, extended_status=""): + raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular para CECA.") + + #################################################################### + ## Paso R2.b. Respuesta negativa a confirmación asíncrona de refund + def refund_response_nok(self, extended_status=""): raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular para CECA.") #################################################################### @@ -1744,34 +1773,49 @@ def _receiveConfirmationHTTPPOST(request): try: operation_data = json.loads(base64.b64decode(request.POST.get("Ds_MerchantParameters"))) dlprint(operation_data) + # Operation number operation_number = operation_data.get("Ds_Order") - operation = VPOSPaymentOperation.objects.get(operation_number=operation_number) - if operation.status != "pending": - raise VPOSOperationAlreadyConfirmed(u"Operación ya confirmada") - - operation.confirmation_data = {"GET": request.GET.dict(), "POST": request.POST.dict()} - operation.confirmation_code = operation_number + ds_transactiontype = operation_data.get("Ds_TransactionType") + if ds_transactiontype == "3": + # Operación de reembolso + operation = VPOSRefundOperation.objects.get(operation_number=operation_number) - ds_errorcode = operation_data.get("Ds_ErrorCode") - if ds_errorcode: - errormsg = u' // ' + VPOSRedsys._format_ds_error_code(operation_data.get("Ds_ErrorCode")) else: - errormsg = u'' + # Operación de confirmación de venta + operation = VPOSPaymentOperation.objects.get(operation_number=operation_number) - operation.response_code = VPOSRedsys._format_ds_response_code(operation_data.get("Ds_Response")) + errormsg - operation.save() - dlprint("Operation {0} actualizada en _receiveConfirmationHTTPPOST()".format(operation.operation_number)) - dlprint(u"Ds_Response={0} Ds_ErrorCode={1}".format(operation_data.get("Ds_Response"), operation_data.get("Ds_ErrorCode"))) + # Comprobar que no se trata de una operación de confirmación de compra anteriormente confirmada + if operation.status != "pending": + raise VPOSOperationAlreadyConfirmed(u"Operación ya confirmada") + + operation.confirmation_data = {"GET": request.GET.dict(), "POST": request.POST.dict()} + operation.confirmation_code = operation_number + + ds_errorcode = operation_data.get("Ds_ErrorCode") + if ds_errorcode: + errormsg = u' // ' + VPOSRedsys._format_ds_error_code(operation_data.get("Ds_ErrorCode")) + else: + errormsg = u'' + + operation.response_code = VPOSRedsys._format_ds_response_code(operation_data.get("Ds_Response")) + errormsg + operation.save() + dlprint("Operation {0} actualizada en _receiveConfirmationHTTPPOST()".format(operation.operation_number)) + dlprint(u"Ds_Response={0} Ds_ErrorCode={1}".format(operation_data.get("Ds_Response"), operation_data.get("Ds_ErrorCode"))) - vpos = operation.virtual_point_of_sale except VPOSPaymentOperation.DoesNotExist: # Si no existe la operación, están intentando # cargar una operación inexistente return False + except VPOSRefundOperation.DoesNotExist: + # Si no existe la operación, están intentando + # cargar una operación inexistente + return False + # Iniciamos el delegado y la operación, esto es fundamental para luego calcular la firma + vpos = operation.virtual_point_of_sale vpos._init_delegated() vpos.operation = operation @@ -1823,6 +1867,7 @@ def _receiveConfirmationSOAP(request): try: ds_order = root.xpath("//Message/Request/Ds_Order/text()")[0] ds_response = root.xpath("//Message/Request/Ds_Response/text()")[0] + ds_transactiontype = root.xpath("//Message/Request/Ds_TransactionType/text()")[0] try: ds_authorisationcode = root.xpath("//Message/Request/Ds_AuthorisationCode/text()")[0] @@ -1837,26 +1882,36 @@ def _receiveConfirmationSOAP(request): ds_errorcode = None errormsg = u'' - operation = VPOSPaymentOperation.objects.get(operation_number=ds_order) + if ds_transactiontype == "3": + # Operación de reembolso + operation = VPOSRefundOperation.objects.get(operation_number=ds_order) + else: + # Operación de confirmación de venta + operation = VPOSPaymentOperation.objects.get(operation_number=ds_order) - if operation.status != "pending": - raise VPOSOperationAlreadyConfirmed(u"Operación ya confirmada") + if operation.status != "pending": + raise VPOSOperationAlreadyConfirmed(u"Operación ya confirmada") - operation.confirmation_data = {"GET": "", "POST": xml_content} - operation.confirmation_code = ds_order - operation.response_code = VPOSRedsys._format_ds_response_code(ds_response) + errormsg - operation.save() - dlprint("Operation {0} actualizada en _receiveConfirmationSOAP()".format(operation.operation_number)) - dlprint(u"Ds_Response={0} Ds_ErrorCode={1}".format(ds_response, ds_errorcode)) - vpos = operation.virtual_point_of_sale + operation.confirmation_data = {"GET": "", "POST": xml_content} + operation.confirmation_code = ds_order + operation.response_code = VPOSRedsys._format_ds_response_code(ds_response) + errormsg + operation.save() + dlprint("Operation {0} actualizada en _receiveConfirmationSOAP()".format(operation.operation_number)) + dlprint(u"Ds_Response={0} Ds_ErrorCode={1}".format(ds_response, ds_errorcode)) except VPOSPaymentOperation.DoesNotExist: # Si no existe la operación, están intentando # cargar una operación inexistente return False + except VPOSRefundOperation.DoesNotExist: + # Si no existe la operación, están intentando + # cargar una operación inexistente + return False + # Iniciamos el delegado y la operación, esto es fundamental # para luego calcular la firma + vpos = operation.virtual_point_of_sale vpos._init_delegated() vpos.operation = operation @@ -1933,7 +1988,7 @@ def charge(self): # no nos importa el tipo de confirmación. if self.operative_type == PREAUTHORIZATION_TYPE: # Cuando se tiene habilitada política de preautorización. - dlprint("Confirmar medinate política de preautorizacion") + dlprint("Confirmar mediante política de preautorizacion") if self._confirm_preauthorization(): return HttpResponse("OK") else: @@ -1980,7 +2035,7 @@ def responseNok(self, **kwargs): return HttpResponse("") elif self.soap_request: - dlprint("responseOk SOAP") + dlprint("responseNok SOAP") # Respuesta a notificación HTTP SOAP response = 'KO' @@ -2006,7 +2061,9 @@ def responseNok(self, **kwargs): # que la operación ha sido negativa, pasamos una respuesta vacia return HttpResponse("") - def refund(self, refund_amount, description): + #################################################################### + ## Paso R1 (Refund) Configura el TPV en modo devolución y ejecuta la operación + def refund(self, operation_sale_code, refund_amount, description): """ Implementación particular del mátodo de devolución para el TPV de Redsys. @@ -2125,8 +2182,8 @@ def refund(self, refund_amount, description): # No aparece mensaje de error ni de ok else: raise VPOSOperationException("La resupuesta HTML con la pantalla de devolución " - "no muestra mensaje informado de forma expícita," - "si la operación se produce con éxito error, (revisar método 'VPOSRedsys.refund').") + "no muestra mensaje informado de forma expícita " + "si la operación se produce con éxito o error. Revisar método 'VPOSRedsys.refund'.") # Respuesta HTTP diferente a 200 else: @@ -2134,6 +2191,69 @@ def refund(self, refund_amount, description): return status + #################################################################### + ## Paso R2.a. Respuesta positiva a confirmación asíncrona de refund + def refund_response_ok(self, extended_status=""): + if self.soap_request: + dlprint("refund_response_ok SOAP") + # Respuesta a notificación HTTP SOAP + response = 'OK' + + dlprint("FIRMAR RESPUESTA {response} CON CLAVE DE CIFRADO {key}".format(response=response, + key=self.encryption_key)) + signature = self._redsys_hmac_sha256_signature(response) + + message = "{response}{signature}".format(response=response, + signature=signature) + dlprint("MENSAJE RESPUESTA CON FIRMA {0}".format(message)) + + # El siguiente mensaje NO debe tener espacios en blanco ni saltos de línea entre las marcas XML + out = "{0}" + out = out.format(cgi.escape(message)) + dlprint("RESPUESTA SOAP:" + out) + + return HttpResponse(out, "text/xml") + + else: + dlprint(u"refund_response_ok HTTP POST (respuesta vacía)") + # Respuesta a notificación HTTP POST + # En RedSys no se exige una respuesta, por parte del comercio, para verificar + # la operación, pasamos una respuesta vacia + return HttpResponse("") + + + #################################################################### + ## Paso R2.b. Respuesta negativa a confirmación asíncrona de refund + def refund_response_nok(self, extended_status=""): + + if self.soap_request: + dlprint("refund_response_nok SOAP") + # Respuesta a notificación HTTP SOAP + response = 'KO' + + dlprint("FIRMAR RESPUESTA {response} CON CLAVE DE CIFRADO {key}".format(response=response, + key=self.encryption_key)) + signature = self._redsys_hmac_sha256_signature(response) + + message = "{response}{signature}".format(response=response, + signature=signature) + dlprint("MENSAJE RESPUESTA CON FIRMA {0}".format(message)) + + # El siguiente mensaje NO debe tener espacios en blanco ni saltos de línea entre las marcas XML + out = "{0}" + out = out.format(cgi.escape(message)) + dlprint("RESPUESTA SOAP:" + out) + + return HttpResponse(out, "text/xml") + + else: + dlprint(u"refund_response_nok HTTP POST (respuesta vacía)") + # Respuesta a notificación HTTP POST + # En RedSys no se exige una respuesta, por parte del comercio, para verificar + # que la operación ha sido negativa, pasamos una respuesta vacia + return HttpResponse("") + + def _confirm_preauthorization(self): """ @@ -2769,7 +2889,17 @@ def responseNok(self, **kwargs): #################################################################### ## Paso R. (Refund) Configura el TPV en modo devolución ## TODO: No implementado - def refund(self, refund_amount, description): + def refund(self, operation_sale_code, refund_amount, description): + raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular para Paypal.") + + #################################################################### + ## Paso R2.a. Respuesta positiva a confirmación asíncrona de refund + def refund_response_ok(self, extended_status=""): + raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular para Paypal.") + + #################################################################### + ## Paso R2.b. Respuesta negativa a confirmación asíncrona de refund + def refund_response_nok(self, extended_status=""): raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular para Paypal.") @@ -3182,9 +3312,18 @@ def responseNok(self, **kwargs): #################################################################### ## Paso R. (Refund) Configura el TPV en modo devolución ## TODO: No implementado - def refund(self, refund_amount, description): + def refund(self, operation_sale_code, refund_amount, description): raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular para Santander-Elavon.") + #################################################################### + ## Paso R2.a. Respuesta positiva a confirmación asíncrona de refund + def refund_response_ok(self, extended_status=""): + raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular para Santader-Elavon.") + + #################################################################### + ## Paso R2.b. Respuesta negativa a confirmación asíncrona de refund + def refund_response_nok(self, extended_status=""): + raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular para Santender-Elavon.") #################################################################### ## Generador de firma para el envío POST al servicio "Redirect" @@ -3466,5 +3605,17 @@ def responseNok(self, extended_status=""): dlprint("responseNok") return HttpResponse("NOK") - def refund(self, refund_amount, description): - raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular.") + #################################################################### + ## Paso R1 (Refund) Configura el TPV en modo devolución y ejecuta la operación + def refund(self, operation_sale_code, refund_amount, description): + raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular para Bitpay.") + + #################################################################### + ## Paso R2.a. Respuesta positiva a confirmación asíncrona de refund + def refund_response_ok(self, extended_status=""): + raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular para Bitpay.") + + #################################################################### + ## Paso R2.b. Respuesta negativa a confirmación asíncrona de refund + def refund_response_nok(self, extended_status=""): + raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular para Bitpay.") From 1391a6e0e75158c07cfd9692228c328e27c14e3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20J=2E=20Barch=C3=A9in=20Molina?= Date: Wed, 18 Mar 2020 12:37:27 +0100 Subject: [PATCH 67/72] Fix refund operations where there are more than one VPOSPaymentOperation per sale_code --- CHANGES.md | 3 +++ djangovirtualpos/__version__.py | 2 +- djangovirtualpos/models.py | 5 +++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 68abf28..7b3633d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,9 @@ Django module that abstracts the flow of several virtual points of sale. # Releases +## 1.6.11 +- On refund operations, select the 'completed' VPOSPaymentOperation matching the sale_code if there are more than one + ## 1.6.10 - Managing refund async confirmations for Redsys diff --git a/djangovirtualpos/__version__.py b/djangovirtualpos/__version__.py index 885d21e..e204c73 100644 --- a/djangovirtualpos/__version__.py +++ b/djangovirtualpos/__version__.py @@ -1,3 +1,3 @@ -VERSION = (1, 6, 10) +VERSION = (1, 6, 11) __version__ = '.'.join(map(str, VERSION)) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index a749b0f..d175003 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -539,9 +539,10 @@ def refund(self, operation_sale_code, refund_amount, description): try: # Cargamos la operación sobre la que vamos a realizar la devolución. - payment_operation = VPOSPaymentOperation.objects.get(sale_code=operation_sale_code) + payment_operation = VPOSPaymentOperation.objects.get(sale_code=operation_sale_code, status='completed') except ObjectDoesNotExist: - raise Exception(u"No se puede cargar una operación anterior con el código {0}".format(operation_sale_code)) + raise Exception(u"No se puede cargar una operación anterior completada con el código" + u" {0}".format(operation_sale_code)) if (not self.has_total_refunds) and (not self.has_partial_refunds): raise Exception(u"El TPV no admite devoluciones, ni totales, ni parciales") From a55e1df34d0cbb48a0c7656d938df6ccf20a3bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20J=2E=20Barch=C3=A9in=20Molina?= Date: Mon, 20 Apr 2020 12:47:02 +0200 Subject: [PATCH 68/72] fix Redsys message parsing for signature calculation in SOAP mode --- djangovirtualpos/models.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index d175003..79924f2 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -1919,11 +1919,17 @@ def _receiveConfirmationSOAP(request): ## Iniciamos los valores recibidos en el delegado # Contenido completo de ..., necesario posteriormente para cálculo de firma - soap_request = etree.tostring(root.xpath("//Message/Request")[0]) + #soap_request = etree.tostring(root.xpath("//Message/Request")[0]) # corrige autocierre de etuqueta y entrecomillado de atributos. Para la comprobación de la firma, # la etiqueta debe tener apertura y cierre y el atributo va entre comilla simple - soap_request = soap_request.replace("", "", 1).replace('"', - "'") + # soap_request = soap_request\ + # .replace("", "", 1)\ + # .replace('"',"'") + + regex = r"" + matches = re.search(regex, xml_content, re.MULTILINE) + soap_request = matches.group(0) + vpos.delegated.soap_request = soap_request dlprint(u"Request:" + vpos.delegated.soap_request) From 57066ee7a900b02b070dfc71f9fc9b95cf06d4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20J=2E=20Barch=C3=A9in=20Molina?= Date: Mon, 20 Apr 2020 12:48:32 +0200 Subject: [PATCH 69/72] version bump --- djangovirtualpos/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangovirtualpos/__version__.py b/djangovirtualpos/__version__.py index e204c73..60d5f56 100644 --- a/djangovirtualpos/__version__.py +++ b/djangovirtualpos/__version__.py @@ -1,3 +1,3 @@ -VERSION = (1, 6, 11) +VERSION = (1, 6, 12) __version__ = '.'.join(map(str, VERSION)) From 9ec899af3473481d4729f37772f351eee6f17602 Mon Sep 17 00:00:00 2001 From: kezoyu Date: Tue, 23 Jul 2024 14:23:44 +0200 Subject: [PATCH 70/72] Pago por referencia (#18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Pagos con referencia, no está el titular presente, conexión host to host (REST) --------- Co-authored-by: José Francisco Fernández Fernández --- djangovirtualpos/models.py | 250 +++++++++++++++++++++++++++++-------- djangovirtualpos/views.py | 30 +++-- 2 files changed, 219 insertions(+), 61 deletions(-) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index 79924f2..d8586b6 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -40,7 +40,6 @@ import requests from bs4 import BeautifulSoup - VPOS_TYPES = ( ("ceca", _("TPV Virtual - Confederación Española de Cajas de Ahorros (CECA)")), ("paypal", _("Paypal")), @@ -99,7 +98,6 @@ def get_delegated_class(virtualpos_type): ("failed", _(u"Failed")), ) - #################################################################### ## Tipos de estado del TPV VIRTUALPOS_STATE_TYPES = ( @@ -107,6 +105,7 @@ def get_delegated_class(virtualpos_type): ("production", "Producción") ) + #################################################################### ## Operación de pago de TPV class VPOSPaymentOperation(models.Model): @@ -137,8 +136,10 @@ class VPOSPaymentOperation(models.Model): last_update_datetime = models.DateTimeField(verbose_name="Fecha de última actualización del objeto") type = models.CharField(max_length=16, choices=VPOS_TYPES, default="", verbose_name="Tipo de TPV") - virtual_point_of_sale = models.ForeignKey("VirtualPointOfSale", parent_link=True, related_name="payment_operations", null=False) - environment = models.CharField(max_length=255, choices=VIRTUALPOS_STATE_TYPES, default="", blank=True, verbose_name="Entorno del TPV") + virtual_point_of_sale = models.ForeignKey("VirtualPointOfSale", parent_link=True, related_name="payment_operations", + null=False) + environment = models.CharField(max_length=255, choices=VIRTUALPOS_STATE_TYPES, default="", blank=True, + verbose_name="Entorno del TPV") @property def vpos(self): @@ -163,7 +164,6 @@ def compute_payment_refunded_status(self): self.save() - ## Guarda el objeto en BD, en realidad lo único que hace es actualizar los datetimes def save(self, *args, **kwargs): """ @@ -187,15 +187,19 @@ def save(self, *args, **kwargs): # Excepción para indicar que la operación charge ha devuelto una respuesta incorrecta o de fallo class VPOSCantCharge(Exception): pass + # Excepción para indicar que no se ha implementado una operación para un tipo de TPV en particular. class VPOSOperationNotImplemented(Exception): pass + # Cuando se produce un error al realizar una operación en concreto. class VPOSOperationException(Exception): pass + # La operacióm ya fue confirmada anteriormente mediante otra notificación recibida class VPOSOperationAlreadyConfirmed(Exception): pass + #################################################################### ## Clase que contiene las operaciones de pago de forma genérica ## actúa de fachada de forma que el resto del software no conozca @@ -476,22 +480,22 @@ def verifyConfirmation(self): ## y otros tienen una verificación y una respuesta con "OK". ## En cualquier caso, es necesario que la aplicación llame a este ## método para terminar correctamente el proceso. - def charge(self): + def charge(self, **kwargs): # Bloquear otras transacciones VPOSPaymentOperation.objects.select_for_update().filter(id=self.operation.id) # Realizamos el cargo - response = self.delegated.charge() - # Cambiamos el estado de la operación - self.operation.status = "completed" + response = self.delegated.charge(**kwargs) + + if response: + # Cambiamos el estado de la operación + self.operation.status = "completed" + dlprint("Operation {0} actualizada en charge()".format(self.operation.operation_number)) self.operation.save() - dlprint("Operation {0} actualizada en charge()".format(self.operation.operation_number)) # Devolvemos el cargo return response - - #################################################################### ## Paso 3.3b1. Error en verificación. ## No se ha podido recuperar la instancia de TPV de la respuesta del @@ -582,14 +586,12 @@ def refund(self, operation_sale_code, refund_amount, description): return refund_response - #################################################################### ## Paso R2.a. Respuesta positiva a confirmación asíncrona de refund def refund_response_ok(self, extended_status=""): dlprint("vpos.refund_response_ok") return self.delegated.refund_response_ok() - #################################################################### ## Paso R2.b. Respuesta negativa a confirmación asíncrona de refund def refund_response_nok(self, extended_status=""): @@ -603,21 +605,22 @@ class VPOSRefundOperation(models.Model): Entidad que gestiona las devoluciones de pagos realizados. Las devoluciones pueden ser totales o parciales, por tanto un "pago" tiene una relación uno a muchos con "devoluciones". """ - amount = models.DecimalField(max_digits=6, decimal_places=2, null=False, blank=False, verbose_name=u"Cantidad de la devolución") - description = models.CharField(max_length=512, null=False, blank=False, verbose_name=u"Descripción de la devolución") + amount = models.DecimalField(max_digits=6, decimal_places=2, null=False, blank=False, + verbose_name=u"Cantidad de la devolución") + description = models.CharField(max_length=512, null=False, blank=False, + verbose_name=u"Descripción de la devolución") operation_number = models.CharField(max_length=255, null=False, blank=False, verbose_name=u"Número de operación") - status = models.CharField(max_length=64, choices=VPOS_REFUND_STATUS_CHOICES, null=False, blank=False, verbose_name=u"Estado de la devolución") + status = models.CharField(max_length=64, choices=VPOS_REFUND_STATUS_CHOICES, null=False, blank=False, + verbose_name=u"Estado de la devolución") creation_datetime = models.DateTimeField(verbose_name="Fecha de creación del objeto") last_update_datetime = models.DateTimeField(verbose_name="Fecha de última actualización del objeto") payment = models.ForeignKey(VPOSPaymentOperation, on_delete=models.PROTECT, related_name="refund_operations") - @property def virtual_point_of_sale(self): return self.payment.virtual_point_of_sale - ## Guarda el objeto en BD, en realidad lo único que hace es actualizar los datetimes def save(self, *args, **kwargs): """ @@ -651,7 +654,8 @@ class VPOSCeca(VirtualPointOfSale): # Al poner el signo "+" como "related_name" evitamos que desde el padre # se pueda seguir la relación hasta aquí (ya que cada uno de las clases # que heredan de ella estará en una tabla y sería un lío). - parent = models.OneToOneField(VirtualPointOfSale, parent_link=True, related_name="+", null=False, db_column="vpos_id") + parent = models.OneToOneField(VirtualPointOfSale, parent_link=True, related_name="+", null=False, + db_column="vpos_id") # Identifica al comercio, será facilitado por la caja en el proceso de alta merchant_id = models.CharField(max_length=9, null=False, blank=False, verbose_name="MerchantID", @@ -1024,7 +1028,8 @@ def _verification_signature(self): class VPOSRedsys(VirtualPointOfSale): """Información de configuración del TPV Virtual Redsys""" ## Todo TPV tiene una relación con los datos generales del TPV - parent = models.OneToOneField(VirtualPointOfSale, parent_link=True, related_name="+", null=False, db_column="vpos_id") + parent = models.OneToOneField(VirtualPointOfSale, parent_link=True, related_name="+", null=False, + db_column="vpos_id") # Expresión regular usada en la identificación del servidor regex_number = re.compile("^\d*$") @@ -1042,7 +1047,8 @@ class VPOSRedsys(VirtualPointOfSale): # Habilita mecanismo de preautorización + confirmación o anulación. - operative_type = models.CharField(max_length=512, choices=OPERATIVE_TYPES, default=AUTHORIZATION_TYPE, verbose_name=u"Tipo de operativa") + operative_type = models.CharField(max_length=512, choices=OPERATIVE_TYPES, default=AUTHORIZATION_TYPE, + verbose_name=u"Tipo de operativa") # Clave de cifrado SHA-256 para el entorno de prueba encryption_key_testing_sha256 = models.CharField(max_length=64, null=True, default=None, @@ -1566,6 +1572,11 @@ class VPOSRedsys(VirtualPointOfSale): "testing": "https://sis-t.redsys.es:25443/sis/realizarPago" } + REDSYS_REST_URL = { + "production": "https://sis.redsys.es/sis/rest/trataPeticionREST", + "testing": "https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST" + } + # Idiomas soportados por RedSys IDIOMAS = {"es": "001", "en": "002", "ca": "003", "fr": "004", "de": "005", "pt": "009", "it": "007"} @@ -1715,7 +1726,7 @@ def getPaymentFormData(self, reference_number=False): # Representa la suma total de los importes de las cuotas "DS_MERCHANT_SUMTOTAL": self.importe, } - + url = self.url # En caso de que tenga referencia if reference_number: # Puede ser una petición de referencia @@ -1727,7 +1738,15 @@ def getPaymentFormData(self, reference_number=False): order_data["DS_MERCHANT_MERCHANTURL"] += "?request_reference=1" # o en cambio puede ser el envío de una referencia obtenida antes else: - order_data["DS_MERCHANT_IDENTIFIER"] = reference_number + # Pagos con referencia, no está el titular presente, conexión host to host (REST) + order_data.update({ + "DS_MERCHANT_DIRECTPAYMENT": True, + "DS_MERCHANT_EXCEP_SCA": 'MIT', + "DS_MERCHANT_IDENTIFIER": reference_number + }) + del order_data["DS_MERCHANT_URLOK"] + del order_data["DS_MERCHANT_URLKO"] + url = self.REDSYS_REST_URL[self.parent.environment] json_order_data = json.dumps(order_data) packed_order_data = base64.b64encode(json_order_data) @@ -1740,7 +1759,7 @@ def getPaymentFormData(self, reference_number=False): form_data = { "data": data, - "action": self.url, + "action": url, "enctype": "application/x-www-form-urlencoded", "method": "post" } @@ -1796,14 +1815,17 @@ def _receiveConfirmationHTTPPOST(request): ds_errorcode = operation_data.get("Ds_ErrorCode") if ds_errorcode: - errormsg = u' // ' + VPOSRedsys._format_ds_error_code(operation_data.get("Ds_ErrorCode")) + errormsg = u' // ' + VPOSRedsys._format_ds_error_code(operation_data.get("Ds_ErrorCode")) else: errormsg = u'' - operation.response_code = VPOSRedsys._format_ds_response_code(operation_data.get("Ds_Response")) + errormsg + operation.response_code = VPOSRedsys._format_ds_response_code( + operation_data.get("Ds_Response")) + errormsg operation.save() - dlprint("Operation {0} actualizada en _receiveConfirmationHTTPPOST()".format(operation.operation_number)) - dlprint(u"Ds_Response={0} Ds_ErrorCode={1}".format(operation_data.get("Ds_Response"), operation_data.get("Ds_ErrorCode"))) + dlprint( + "Operation {0} actualizada en _receiveConfirmationHTTPPOST()".format(operation.operation_number)) + dlprint(u"Ds_Response={0} Ds_ErrorCode={1}".format(operation_data.get("Ds_Response"), + operation_data.get("Ds_ErrorCode"))) except VPOSPaymentOperation.DoesNotExist: # Si no existe la operación, están intentando @@ -1842,6 +1864,7 @@ def _receiveConfirmationHTTPPOST(request): return vpos.delegated + #################################################################### ## Paso 3.1.b Procesar notificación SOAP @staticmethod @@ -1919,7 +1942,7 @@ def _receiveConfirmationSOAP(request): ## Iniciamos los valores recibidos en el delegado # Contenido completo de ..., necesario posteriormente para cálculo de firma - #soap_request = etree.tostring(root.xpath("//Message/Request")[0]) + # soap_request = etree.tostring(root.xpath("//Message/Request")[0]) # corrige autocierre de etuqueta y entrecomillado de atributos. Para la comprobación de la firma, # la etiqueta debe tener apertura y cierre y el atributo va entre comilla simple # soap_request = soap_request\ @@ -1943,14 +1966,108 @@ def _receiveConfirmationSOAP(request): # Usado para recuperar los datos la referencia vpos.delegated.ds_merchantparameters = {} try: - vpos.delegated.ds_merchantparameters["Ds_Merchant_Identifier"] = root.xpath("//Message/Request/Ds_Merchant_Identifier/text()")[0] - vpos.delegated.ds_merchantparameters["Ds_ExpiryDate"] = root.xpath("//Message/Request/Ds_ExpiryDate/text()")[0] + vpos.delegated.ds_merchantparameters["Ds_Merchant_Identifier"] = \ + root.xpath("//Message/Request/Ds_Merchant_Identifier/text()")[0] + vpos.delegated.ds_merchantparameters["Ds_ExpiryDate"] = \ + root.xpath("//Message/Request/Ds_ExpiryDate/text()")[0] # Aquí la idea es incluir más parámetros que nos puedan servir en el llamador de este módulo except IndexError: pass return vpos.delegated + ## Paso 3.1.c Procesar notificación REST + @staticmethod + def _receiveConfirmationREST(request, operation_number): + dlprint(u"Notificación Redsys REST:") + dlprint(request) + + if 'errorCode' in request: + # Operación de confirmación de venta + operation = VPOSPaymentOperation.objects.get(operation_number=operation_number) + operation.response_code = u' // ' + VPOSRedsys._format_ds_error_code(request.get("errorCode")) + operation.save() + dlprint("Operation {0} actualizada en _receiveConfirmationREST()".format(operation.operation_number)) + dlprint(u"errorCode={0}".format(request.get("errorCode"))) + return False + + # Almacén de operaciones + try: + operation_data = json.loads(base64.b64decode(request.get("Ds_MerchantParameters"))) + dlprint(operation_data) + + # Operation number + operation_number = operation_data.get("Ds_Order") + + ds_transactiontype = operation_data.get("Ds_TransactionType") + if ds_transactiontype == "3": + # Operación de reembolso + operation = VPOSRefundOperation.objects.get(operation_number=operation_number) + + else: + # Operación de confirmación de venta + operation = VPOSPaymentOperation.objects.get(operation_number=operation_number) + + # Comprobar que no se trata de una operación de confirmación de compra anteriormente confirmada + print(u"Operation: {}".format(operation.operation_number)) + print(u"Operation status: {}".format(operation.status)) + if operation.status != "pending": + raise VPOSOperationAlreadyConfirmed(u"Operación ya confirmada") + + operation.confirmation_data = request + operation.confirmation_code = operation_number + + ds_errorcode = operation_data.get("Ds_ErrorCode") + if ds_errorcode: + errormsg = u' // ' + VPOSRedsys._format_ds_error_code(operation_data.get("Ds_ErrorCode")) + else: + errormsg = u'' + + operation.response_code = VPOSRedsys._format_ds_response_code( + operation_data.get("Ds_Response")) + errormsg + operation.save() + dlprint( + "Operation {0} actualizada en _receiveConfirmationREST()".format(operation.operation_number)) + dlprint(u"Ds_Response={0} Ds_ErrorCode={1}".format(operation_data.get("Ds_Response"), + operation_data.get("Ds_ErrorCode"))) + + except VPOSPaymentOperation.DoesNotExist: + # Si no existe la operación, están intentando + # cargar una operación inexistente + return False + + except VPOSRefundOperation.DoesNotExist: + # Si no existe la operación, están intentando + # cargar una operación inexistente + return False + + # Iniciamos el delegado y la operación, esto es fundamental para luego calcular la firma + vpos = operation.virtual_point_of_sale + vpos._init_delegated() + vpos.operation = operation + + # Iniciamos los valores recibidos en el delegado + + # Datos de la operación al completo + # Usado para recuperar los datos la referencia + vpos.delegated.ds_merchantparameters = operation_data + + ## Datos que llegan por REST + # Firma enviada por RedSys, que más tarde compararemos con la generada por el comercio + vpos.delegated.firma = request.get("Ds_Signature") + + # Versión del método de firma utilizado + vpos.delegated.signature_version = request.get("Ds_SignatureVersion") + + # Parámetros de la operación (en base64 + JSON) + vpos.delegated.merchant_parameters = request.get("Ds_MerchantParameters") + + ## Datos decodificados de Ds_MerchantParameters + # Respuesta de la pasarela de pagos. Indica si la operación se autoriza o no + vpos.delegated.ds_response = operation_data.get("Ds_Response") + + return vpos.delegated + #################################################################### ## Paso 3.2. Verifica que los datos enviados desde ## la pasarela de pago identifiquen a una operación de compra y un @@ -1983,16 +2100,18 @@ def verifyConfirmation(self): dlprint(u"Transacción no autorizada por RedSys. Ds_Response es {0} (no está entre 0000-0099)".format( self.ds_response)) return False - + return True #################################################################### ## Paso 3.3a. Realiza el cobro y genera la respuesta a la pasarela y ## comunicamos con la pasarela de pago para que marque la operación ## como pagada. Sólo se usa en CECA - def charge(self): + def charge(self, operation=None, reference_number=None): # En caso de tener habilitada la preautorización # no nos importa el tipo de confirmación. + dlprint("tipo de operativa {0}".format(self.operative_type)) + dlprint("SOAP request {0}".format(self.soap_request)) if self.operative_type == PREAUTHORIZATION_TYPE: # Cuando se tiene habilitada política de preautorización. dlprint("Confirmar mediante política de preautorizacion") @@ -2022,6 +2141,21 @@ def charge(self): dlprint("RESPUESTA SOAP:" + out) return HttpResponse(out, "text/xml") + # Pagos con referencia, no está el titular presente, conexión host to host (REST) + elif operation and reference_number: + dlprint("Pago con referencia".format(reference_number)) + # URL de pago según el entorno + form_data = self.getPaymentFormData(reference_number) + # peticion REST + r = requests.post(form_data["action"], data=form_data["data"]) + # El pago se confirma por REST + virtual_pos = self._receiveConfirmationREST(r.json(), operation) + # El pago se verifica por REST + if virtual_pos and virtual_pos.verifyConfirmation(): + dlprint(u"responseOK REST") + return virtual_pos + dlprint(u"responseKO REST") + return False else: dlprint(u"responseOk HTTP POST (respuesta vacía)") @@ -2228,7 +2362,6 @@ def refund_response_ok(self, extended_status=""): # la operación, pasamos una respuesta vacia return HttpResponse("") - #################################################################### ## Paso R2.b. Respuesta negativa a confirmación asíncrona de refund def refund_response_nok(self, extended_status=""): @@ -2260,7 +2393,6 @@ def refund_response_nok(self, extended_status=""): # que la operación ha sido negativa, pasamos una respuesta vacia return HttpResponse("") - def _confirm_preauthorization(self): """ @@ -2346,7 +2478,7 @@ def _confirm_preauthorization(self): confirmpreauth_html_request = requests.post(self.url, data=data, headers=headers) if confirmpreauth_html_request.status_code == 200: - + dlprint("_confirm_preauthorization status_code 200") # Iniciamos un objeto BeautifulSoup (para poder leer los elementos del DOM del HTML recibido). @@ -2585,7 +2717,6 @@ def _format_ds_response_code(ds_response): return out - @staticmethod def _format_ds_error_code(ds_errorcode): """ @@ -2602,6 +2733,7 @@ def _format_ds_error_code(ds_errorcode): return out + ######################################################################################################################## ######################################################################################################################## ###################################################### TPV PayPal ###################################################### @@ -2611,7 +2743,8 @@ def _format_ds_error_code(ds_errorcode): class VPOSPaypal(VirtualPointOfSale): """Información de configuración del TPV Virtual PayPal """ ## Todo TPV tiene una relación con los datos generales del TPV - parent = models.OneToOneField(VirtualPointOfSale, parent_link=True, related_name="+", null=False, db_column="vpos_id") + parent = models.OneToOneField(VirtualPointOfSale, parent_link=True, related_name="+", null=False, + db_column="vpos_id") # nombre de usuario para la API de Paypal API_username = models.CharField(max_length=60, null=False, blank=False, verbose_name="API_username") @@ -2892,7 +3025,6 @@ def responseNok(self, **kwargs): # que la operación ha sido negativa, redireccionamos a la url de cancelación return redirect(reverse("payment_cancel_url", kwargs={"sale_code": self.parent.operation.sale_code})) - #################################################################### ## Paso R. (Refund) Configura el TPV en modo devolución ## TODO: No implementado @@ -2928,7 +3060,8 @@ class VPOSSantanderElavon(VirtualPointOfSale): # Al poner el signo "+" como "related_name" evitamos que desde el padre # se pueda seguir la relación hasta aquí (ya que cada uno de las clases # que heredan de ella estará en una tabla y sería un lío). - parent = models.OneToOneField(VirtualPointOfSale, parent_link=True, related_name="+", null=False, db_column="vpos_id") + parent = models.OneToOneField(VirtualPointOfSale, parent_link=True, related_name="+", null=False, + db_column="vpos_id") # Identifica al comercio, será facilitado por la caja en el proceso de alta merchant_id = models.CharField(max_length=50, null=False, blank=False, verbose_name="MerchantID", @@ -3320,17 +3453,20 @@ def responseNok(self, **kwargs): ## Paso R. (Refund) Configura el TPV en modo devolución ## TODO: No implementado def refund(self, operation_sale_code, refund_amount, description): - raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular para Santander-Elavon.") + raise VPOSOperationNotImplemented( + u"No se ha implementado la operación de devolución particular para Santander-Elavon.") #################################################################### ## Paso R2.a. Respuesta positiva a confirmación asíncrona de refund def refund_response_ok(self, extended_status=""): - raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular para Santader-Elavon.") + raise VPOSOperationNotImplemented( + u"No se ha implementado la operación de devolución particular para Santader-Elavon.") #################################################################### ## Paso R2.b. Respuesta negativa a confirmación asíncrona de refund def refund_response_nok(self, extended_status=""): - raise VPOSOperationNotImplemented(u"No se ha implementado la operación de devolución particular para Santender-Elavon.") + raise VPOSOperationNotImplemented( + u"No se ha implementado la operación de devolución particular para Santender-Elavon.") #################################################################### ## Generador de firma para el envío POST al servicio "Redirect" @@ -3424,6 +3560,7 @@ def _verification_signature(self): return firma2 + class VPOSBitpay(VirtualPointOfSale): """ Pago con criptomoneda usando la plataforma bitpay.com @@ -3447,16 +3584,22 @@ class VPOSBitpay(VirtualPointOfSale): # Al poner el signo "+" como "related_name" evitamos que desde el padre # se pueda seguir la relación hasta aquí (ya que cada uno de las clases # que heredan de ella estará en una tabla y sería un lío). - parent = models.OneToOneField(VirtualPointOfSale, parent_link=True, related_name="+", null=False, db_column="vpos_id") - testing_api_key = models.CharField(max_length=512, null=True, blank=True, verbose_name="API Key de Bitpay para entorno de test") - production_api_key = models.CharField(max_length=512, null=False, blank=False, verbose_name="API Key de Bitpay para entorno de producción") - currency = models.CharField(max_length=3, choices=CURRENCIES, default='EUR', null=False, blank=False, verbose_name="Moneda (EUR, USD, BTC)") - transaction_speed = models.CharField(max_length=10, choices=TRANSACTION_SPEED, default='medium', null=False, blank=False, verbose_name="Velocidad de la operación") - notification_url = models.URLField(verbose_name="Url notificaciones actualización estados (https)", null=False, blank=False) + parent = models.OneToOneField(VirtualPointOfSale, parent_link=True, related_name="+", null=False, + db_column="vpos_id") + testing_api_key = models.CharField(max_length=512, null=True, blank=True, + verbose_name="API Key de Bitpay para entorno de test") + production_api_key = models.CharField(max_length=512, null=False, blank=False, + verbose_name="API Key de Bitpay para entorno de producción") + currency = models.CharField(max_length=3, choices=CURRENCIES, default='EUR', null=False, blank=False, + verbose_name="Moneda (EUR, USD, BTC)") + transaction_speed = models.CharField(max_length=10, choices=TRANSACTION_SPEED, default='medium', null=False, + blank=False, verbose_name="Velocidad de la operación") + notification_url = models.URLField(verbose_name="Url notificaciones actualización estados (https)", null=False, + blank=False) # Prefijo usado para identicar al servidor desde el que se realiza la petición, en caso de usar TPV-Proxy. - operation_number_prefix = models.CharField(max_length=20, null=True, blank=True, verbose_name="Prefijo del número de operación") - + operation_number_prefix = models.CharField(max_length=20, null=True, blank=True, + verbose_name="Prefijo del número de operación") bitpay_url = { "production": { @@ -3567,7 +3710,8 @@ def receiveConfirmation(request, **kwargs): if operation.status != "pending": raise VPOSOperationAlreadyConfirmed(u"Operación ya confirmada") - operation.confirmation_data = {"GET": request.GET.dict(), "POST": request.POST.dict(), "BODY": confirmation_body_param} + operation.confirmation_data = {"GET": request.GET.dict(), "POST": request.POST.dict(), + "BODY": confirmation_body_param} operation.save() dlprint("Operation {0} actualizada en receiveConfirmation()".format(operation.operation_number)) diff --git a/djangovirtualpos/views.py b/djangovirtualpos/views.py index 6cb1258..8189968 100644 --- a/djangovirtualpos/views.py +++ b/djangovirtualpos/views.py @@ -8,7 +8,7 @@ from django.shortcuts import render, get_object_or_404 from django.urls import reverse from django.views.decorators.csrf import csrf_exempt -from djangovirtualpos.models import VirtualPointOfSale, VPOSCantCharge +from djangovirtualpos.models import VirtualPointOfSale, VPOSCantCharge, VPOSRedsys from django.http import JsonResponse @@ -109,21 +109,35 @@ def confirm_payment(request, virtualpos_type, sale_model): if verified: # Charge the money and answer the bank confirmation - try: + # try: response = virtual_pos.charge() # Implement the online_confirm method in your payment # this method will mark this payment as paid and will # store the payment date and time. payment.virtual_pos = virtual_pos - payment.online_confirm() - except VPOSCantCharge as e: - return virtual_pos.responseNok(extended_status=e) - except Exception as e: - return virtual_pos.responseNok("cant_charge") + + # Para el pago por referencia de Redsys + reference_number = expiration_date = None + if hasattr(virtual_pos, "delegated") and type(virtual_pos.delegated) == VPOSRedsys: + print virtual_pos.delegated + print virtual_pos.delegated.ds_merchantparameters + reference_number = virtual_pos.delegated.ds_merchantparameters.get("Ds_Merchant_Identifier") + expiration_date = virtual_pos.delegated.ds_merchantparameters.get("Ds_ExpiryDate") + if reference_number: + print(u"Online Confirm: Reference number") + print(reference_number) + payment.online_confirm(reference=reference_number, expiration_date=expiration_date) + else: + print(u"Online Confirm: No Reference number") + payment.online_confirm() + # except VPOSCantCharge as e: + # return virtual_pos.responseNok(extended_status=e) + # except Exception as e: + # return virtual_pos.responseNok("cant_charge") else: # Payment could not be verified # signature is not right response = virtual_pos.responseNok("verification_error") - return response \ No newline at end of file + return response From 240bc2e3c48c7bdde4a6ed5592270d74704baf3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20J=2E=20Barch=C3=A9in=20Molina?= Date: Tue, 23 Jul 2024 14:52:59 +0200 Subject: [PATCH 71/72] bump version and enable extra debug in _confirm_preauthorization --- .gitignore | 1 + djangovirtualpos/__version__.py | 2 +- djangovirtualpos/models.py | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index c632427..7ff8873 100644 --- a/.gitignore +++ b/.gitignore @@ -98,3 +98,4 @@ ENV/ *~ MANIFEST +.pypirc diff --git a/djangovirtualpos/__version__.py b/djangovirtualpos/__version__.py index 60d5f56..3bf6f31 100644 --- a/djangovirtualpos/__version__.py +++ b/djangovirtualpos/__version__.py @@ -1,3 +1,3 @@ -VERSION = (1, 6, 12) +VERSION = (1, 7, 0) __version__ = '.'.join(map(str, VERSION)) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index d8586b6..3f4f70f 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -2480,11 +2480,13 @@ def _confirm_preauthorization(self): if confirmpreauth_html_request.status_code == 200: dlprint("_confirm_preauthorization status_code 200") + dlprint("_confirm_preauthorization response_body: {0}".format(confirmpreauth_html_request.text)) # Iniciamos un objeto BeautifulSoup (para poder leer los elementos del DOM del HTML recibido). html = BeautifulSoup(confirmpreauth_html_request.text, "html.parser") - # Buscamos elementos significativos del DOM que nos indiquen si la operación se ha realizado correctamente o no. + # Buscamos elementos significativos del DOM que nos indiquen si la operación se ha realizado correctamente + # o no. confirmpreauth_message_error = html.find('text', {'lngid': 'noSePuedeRealizarOperacion'}) confirmpreauth_message_ok = html.find('text', {'lngid': 'operacionAceptada'}) From 583abd4ed988738bcf6bd6a024478048160fe3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20J=2E=20Barch=C3=A9in=20Molina?= Date: Tue, 23 Jul 2024 15:33:50 +0200 Subject: [PATCH 72/72] fixing RedSys preauth response. Version 1.7.1 --- djangovirtualpos/__version__.py | 2 +- djangovirtualpos/models.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/djangovirtualpos/__version__.py b/djangovirtualpos/__version__.py index 3bf6f31..ebb9970 100644 --- a/djangovirtualpos/__version__.py +++ b/djangovirtualpos/__version__.py @@ -1,3 +1,3 @@ -VERSION = (1, 7, 0) +VERSION = (1, 7, 1) __version__ = '.'.join(map(str, VERSION)) diff --git a/djangovirtualpos/models.py b/djangovirtualpos/models.py index 3f4f70f..f8052b0 100644 --- a/djangovirtualpos/models.py +++ b/djangovirtualpos/models.py @@ -2487,8 +2487,14 @@ def _confirm_preauthorization(self): # Buscamos elementos significativos del DOM que nos indiquen si la operación se ha realizado correctamente # o no. - confirmpreauth_message_error = html.find('text', {'lngid': 'noSePuedeRealizarOperacion'}) - confirmpreauth_message_ok = html.find('text', {'lngid': 'operacionAceptada'}) + confirmpreauth_message_error = ( + html.find('text', {'lngid': 'noSePuedeRealizarOperacion'}) + or html.find(lngid='noSePuedeRealizarOperacion') + ) + confirmpreauth_message_ok = ( + html.find('text', {'lngid': 'operacionAceptada'}) + or html.find(lngid='operacionAceptada') + ) # Cuando en el DOM del documento HTML aparece un mensaje de error. if confirmpreauth_message_error: