From 18eef0578f055ebc3fb862481fcbe5d7b66e3356 Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Tue, 9 Oct 2018 16:43:41 +0200 Subject: [PATCH] paybox: implement transaction validation and cancelling (#26960) --- eopayment/paybox.py | 50 +++++++++++++++++++++++++++++ tests/test_paybox.py | 75 ++++++++++++++++++++++++++++++++++++++++++++ tox.ini | 1 + 3 files changed, 126 insertions(+) diff --git a/eopayment/paybox.py b/eopayment/paybox.py index 705f1fe..d5cd6ce 100644 --- a/eopayment/paybox.py +++ b/eopayment/paybox.py @@ -6,6 +6,7 @@ import datetime import logging import hashlib import hmac +import requests from decimal import Decimal, ROUND_DOWN from Crypto.Signature import PKCS1_v1_5 from Crypto.PublicKey import RSA @@ -99,6 +100,19 @@ URLS = { 'https://tpeweb1.paybox.com/cgi/MYchoix_pagepaiement.cgi', } +PAYBOX_DIRECT_URLS = { + 'test': 'https://preprod-ppps.paybox.com/PPPS.php', + 'prod': 'https://ppps.paybox.com/PPPS.php', + 'backup': 'https://ppps1.paybox.com/PPPS.php' +} + +PAYBOX_DIRECT_CANCEL_OPERATION = '00005' +PAYBOX_DIRECT_VALIDATE_OPERATION = '00002' + +PAYBOX_DIRECT_VERSION_NUMBER = '00104' + +PAYBOX_DIRECT_SUCCESS_RESPONSE_CODE = '00000' + def sign(data, key): '''Take a list of tuple key, value and sign it by building a string to @@ -174,6 +188,12 @@ class Payment(PaymentCommon): 'validation': lambda x: isinstance(x, basestring) and x.isdigit() and len(x) == 7, }, + { + 'name': 'cle', + 'caption': _('Site key'), + 'required': False, + 'validation': lambda x: isinstance(x, basestring), + }, { 'name': 'rang', 'caption': _('Numéro de rang'), @@ -311,3 +331,33 @@ class Payment(PaymentCommon): bank_data=d, result=result, bank_status=bank_status) + + def perform(self, amount, bank_data, operation): + logger = logging.getLogger(__name__) + url = PAYBOX_DIRECT_URLS[self.platform] + params = {'VERSION': PAYBOX_DIRECT_VERSION_NUMBER, + 'TYPE': operation, + 'SITE': force_text(self.site), + 'RANG': self.rang.strip(), + 'CLE': force_text(self.cle), + 'NUMQUESTION': bank_data['numero_transaction'][0].zfill(10), + 'MONTANT': (amount * Decimal(100)).to_integral_value(ROUND_DOWN), + 'DEVISE': force_text(self.devise), + 'NUMTRANS': bank_data['numero_transaction'][0], # paybox transaction number + 'NUMAPPEL': bank_data['numero_appel'][0], + 'REFERENCE': bank_data['reference'][0], + 'DATEQ': datetime.datetime.now().strftime('%d%m%Y%H%M%S'), + } + response = requests.post(url, params) + response.raise_for_status() + logger.debug('received %r', response.content) + data = dict(urlparse.parse_qsl(response.content, True, True)) + if data.get('CODEREPONSE') != PAYBOX_DIRECT_SUCCESS_RESPONSE_CODE: + raise ResponseError(data.get('COMMENTAIRE')) + return data + + def validate(self, amount, bank_data, **kwargs): + return self.perform(amount, bank_data, PAYBOX_DIRECT_VALIDATE_OPERATION) + + def cancel(self, amount, bank_data, **kwargs): + return self.perform(amount, bank_data, PAYBOX_DIRECT_CANCEL_OPERATION) diff --git a/tests/test_paybox.py b/tests/test_paybox.py index 93d3c32..a0b8d9a 100644 --- a/tests/test_paybox.py +++ b/tests/test_paybox.py @@ -4,6 +4,7 @@ import codecs from unittest import TestCase from decimal import Decimal import base64 +import mock from six.moves.urllib import parse as urllib from xml.etree import ElementTree as ET @@ -116,6 +117,80 @@ class PayboxTests(TestCase): with self.assertRaisesRegexp(eopayment.ResponseError, 'missing erreur or reference'): backend.response('foo=bar') + def test_perform_operations(self): + operations = {'validate': '00002', 'cancel': '00005'} + for operation_name, operation_code in operations.items(): + params = BACKEND_PARAMS.copy() + params['cle'] = 'cancelling_key' + backend = eopayment.Payment('paybox', params) + bank_data = {'numero_transaction': ['13957441'], + 'numero_appel': ['30310733'], + 'reference': ['830657461681'] + } + backend_raw_response = """NUMTRANS=0013989865&NUMAPPEL=0030378572&NUMQUESTION=0013989862&SITE=1999888&RANG=32&AUTORISATION=XXXXXX&CODEREPONSE=00000&COMMENTAIRE=Demande traitée avec succès&REFABONNE=&PORTEUR=""" + backend_expected_response = {"CODEREPONSE": "00000", + "RANG": "32", + "AUTORISATION": "XXXXXX", + "NUMTRANS": "0013989865", + "PORTEUR": "", + "COMMENTAIRE": "Demande traitée avec succès", + "SITE": "1999888", + "NUMAPPEL": "0030378572", + "REFABONNE": "", + "NUMQUESTION": "0013989862"} + + with mock.patch('eopayment.paybox.requests.post') as requests_post: + response = mock.Mock(status_code=200, content=backend_raw_response) + requests_post.return_value = response + backend_response = getattr(backend, operation_name)(Decimal('10'), bank_data) + self.assertEqual(requests_post.call_args[0][0], 'https://preprod-ppps.paybox.com/PPPS.php') + params_sent = requests_post.call_args[0][1] + # make sure the date parameter is present + assert 'DATEQ' in params_sent + # don't care about its value + params_sent.pop('DATEQ') + expected_params = {'CLE': 'cancelling_key', + 'VERSION': '00104', + 'TYPE': operation_code, + 'MONTANT': Decimal('1000'), + 'NUMAPPEL': '30310733', + 'NUMTRANS': '13957441', + 'NUMQUESTION': '0013957441', + 'REFERENCE': '830657461681', + 'RANG': backend.backend.rang, + 'SITE': backend.backend.site, + 'DEVISE': backend.backend.devise + } + self.assertEqual(params_sent, expected_params) + self.assertEqual(backend_response, backend_expected_response) + + params['platform'] = 'prod' + backend = eopayment.Payment('paybox', params) + with mock.patch('eopayment.paybox.requests.post') as requests_post: + response = mock.Mock(status_code=200, content=backend_raw_response) + requests_post.return_value = response + getattr(backend, operation_name)(Decimal('10'), bank_data) + self.assertEqual(requests_post.call_args[0][0], 'https://ppps.paybox.com/PPPS.php') + + with mock.patch('eopayment.paybox.requests.post') as requests_post: + error_response = """CODEREPONSE=00015&COMMENTAIRE=PAYBOX : Transaction non trouvée""" + response = mock.Mock(status_code=200, content=error_response) + requests_post.return_value = response + self.assertRaisesRegexp(eopayment.ResponseError, 'Transaction non trouvée', getattr(backend, operation_name), + Decimal('10'), bank_data) + + + def test_validate_payment(self): + params = BACKEND_PARAMS.copy() + params['cle'] = 'cancelling_key' + backend = eopayment.Payment('paybox', params) + bank_data = {'numero_transaction': ['13957441'], + 'numero_appel': ['30310733'], + 'reference': ['830657461681'] + } + backend_raw_response = """NUMTRANS=0013989865&NUMAPPEL=0030378572&NUMQUESTION=0013989862&SITE=1999888&RANG=32&AUTORISATION=XXXXXX&CODEREPONSE=00000&COMMENTAIRE=Demande traitée avec succès&REFABONNE=&PORTEUR=""" + + def test_rsa_signature_validation(self): pkey = '''-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUgYufHuheMztK1LhQSG6xsOzb diff --git a/tox.ini b/tox.ini index 7a7f70e..676a49b 100644 --- a/tox.ini +++ b/tox.ini @@ -17,3 +17,4 @@ usedevelop = True deps = coverage pytest py2: pytest-cov + mock