From f9b6f1a94611315458a4e6288747f25a9813030b Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Thu, 12 Dec 2019 10:50:38 +0100 Subject: [PATCH 1/2] start support for PayFiP Regie web-service (#38405) --- MANIFEST.in | 4 + README.txt | 31 ++ eopayment/payfip_ws.py | 346 ++++++++++++++++++ .../resource/PaiementSecuriseService.wsdl | 140 +++++++ .../resource/PaiementSecuriseService1.xsd | 116 ++++++ .../resource/PaiementSecuriseService2.xsd | 16 + .../resource/PaiementSecuriseService3.xsd | 47 +++ setup.py | 2 + tests/data/payfip-test_get_client_info.json | 1 + ...p-test_get_idop_adresse_mel_incorrect.json | 1 + tests/data/payfip-test_get_idop_ok.json | 1 + .../payfip-test_get_idop_refdet_error.json | 1 + .../payfip-test_get_info_paiement_P1.json | 1 + .../payfip-test_get_info_paiement_P5.json | 1 + .../payfip-test_get_info_paiement_ok.json | 1 + tests/data/payfip-test_payment_cancelled.json | 4 + tests/data/payfip-test_payment_denied.json | 4 + tests/data/payfip-test_payment_ok.json | 4 + tests/test_payfip_ws.py | 245 +++++++++++++ tox.ini | 8 + 20 files changed, 974 insertions(+) create mode 100644 eopayment/payfip_ws.py create mode 100644 eopayment/resource/PaiementSecuriseService.wsdl create mode 100644 eopayment/resource/PaiementSecuriseService1.xsd create mode 100644 eopayment/resource/PaiementSecuriseService2.xsd create mode 100644 eopayment/resource/PaiementSecuriseService3.xsd create mode 100644 tests/data/payfip-test_get_client_info.json create mode 100644 tests/data/payfip-test_get_idop_adresse_mel_incorrect.json create mode 100644 tests/data/payfip-test_get_idop_ok.json create mode 100644 tests/data/payfip-test_get_idop_refdet_error.json create mode 100644 tests/data/payfip-test_get_info_paiement_P1.json create mode 100644 tests/data/payfip-test_get_info_paiement_P5.json create mode 100644 tests/data/payfip-test_get_info_paiement_ok.json create mode 100644 tests/data/payfip-test_payment_cancelled.json create mode 100644 tests/data/payfip-test_payment_denied.json create mode 100644 tests/data/payfip-test_payment_ok.json create mode 100644 tests/test_payfip_ws.py diff --git a/MANIFEST.in b/MANIFEST.in index 9685219..30d6b0b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,3 +5,7 @@ include VERSION include README.txt include eopayment/request include eopayment/response +include eopayment/resource/PaiementSecuriseService.wsdl +include eopayment/resource/PaiementSecuriseService1.xsd +include eopayment/resource/PaiementSecuriseService2.xsd +include eopayment/resource/PaiementSecuriseService3.xsd diff --git a/README.txt b/README.txt index 0315c54..56c8930 100644 --- a/README.txt +++ b/README.txt @@ -37,3 +37,34 @@ fields, in order to allow to match them in payment system backoffice. They are: For other backends, the order and transaction ids, separated by '!' are sent in order id field, so they can be matched in backoffice. + +PayFiP +====== + +You can test your PayFiP regie web-service connection with an integrated CLI utility: + + $ python3 -m eopayment.payfip_ws info-client --help + Usage: payfip_ws.py info-client [OPTIONS] NUMCLI + + Options: + --help Show this message and exit. + + $ python3 -m eopayment.payfip_ws get-idop --help + Usage: payfip_ws.py get-idop [OPTIONS] NUMCLI + + Options: + --saisie [T|X|W] [required] + --exer TEXT [required] + --montant INTEGER [required] + --refdet TEXT [required] + --mel TEXT [required] + --url-notification TEXT [required] + --url-redirect TEXT [required] + --objet TEXT + --help Show this message and exit. + + $ python3 -m eopayment.payfip_ws info-paiement --help + Usage: payfip_ws.py info-paiement [OPTIONS] IDOP + + Options: + --help Show this message and exit. diff --git a/eopayment/payfip_ws.py b/eopayment/payfip_ws.py new file mode 100644 index 0000000..a0a9799 --- /dev/null +++ b/eopayment/payfip_ws.py @@ -0,0 +1,346 @@ +# eopayment - online payment library +# Copyright (C) 2011-2019 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from __future__ import print_function, unicode_literals + +import copy +import datetime +from decimal import Decimal, ROUND_DOWN +import functools +import os +import random +import xml.etree.ElementTree as ET + +from gettext import gettext as _ + +import six +from six.moves.urllib.parse import parse_qs + +import zeep +import zeep.exceptions + +from .systempayv2 import isonow +from .common import (PaymentCommon, PaymentResponse, URL, PAID, DENIED, + CANCELLED, ERROR, ResponseError) + +WSDL_URL = 'https://www.tipi.budget.gouv.fr/tpa/services/mas_securite/contrat_paiement_securise/PaiementSecuriseService?wsdl' # noqa: E501 + +SERVICE_URL = 'https://www.tipi.budget.gouv.fr/tpa/services/securite' # noqa: E501 + +PAYMENT_URL = 'https://www.tipi.budget.gouv.fr/tpa/paiementws.web?idop=%s' + + +def clear_namespace(element): + def helper(element): + if element.tag.startswith('{'): + element.tag = element.tag[element.tag.index('}') + 1:] + for subelement in element: + helper(subelement) + + element = copy.deepcopy(element) + helper(element) + return element + + +class PayFiPError(Exception): + def __init__(self, code, message, origin=None): + self.code = code + self.message = message + self.origin = origin + args = [code, message] + if origin: + args.append(origin) + super(PayFiPError, self).__init__(*args) + + +class PayFiP(object): + '''Encapsulate SOAP web-services of PayFiP''' + + def __init__(self, wsdl_url=None, service_url=None, zeep_client_kwargs=None): + self.client = zeep.Client(wsdl_url or WSDL_URL, **(zeep_client_kwargs or {})) + # distribued WSDL is wrong :/ + self.client.service._binding_options['address'] = service_url or SERVICE_URL + + def fault_to_exception(self, fault): + if fault.message != 'fr.gouv.finances.cp.tpa.webservice.exceptions.FonctionnelleErreur' or fault.detail is None: + return + detail = clear_namespace(fault.detail) + code = detail.find('FonctionnelleErreur/code') + if code is None or not code.text: + return PayFiPError('inconnu', ET.tostring(detail)) + descriptif = detail.find('FonctionnelleErreur/descriptif') + libelle = detail.find('FonctionnelleErreur/libelle') + return PayFiPError( + code=code.text, + message=(descriptif is not None and descriptif.text) + or (libelle is not None and libelle.text) + or '') + + def _perform(self, request_qname, operation, **kwargs): + RequestType = self.client.get_type(request_qname) # noqa: E501 + try: + return getattr(self.client.service, operation)(RequestType(**kwargs)) + except zeep.exceptions.Fault as fault: + raise self.fault_to_exception(fault) or PayFiPError('unknown', fault.message, fault) + except zeep.exceptions.Error as zeep_error: + raise PayFiPError('erreur-soap', str(zeep_error), zeep_error) + + def get_info_client(self, numcli): + return self._perform( + '{http://securite.service.tpa.cp.finances.gouv.fr/reponse}RecupererDetailClientRequest', + 'recupererDetailClient', + numCli=numcli) + + def get_idop(self, numcli, saisie, exer, refdet, montant, mel, url_notification, url_redirect, objet=None): + return self._perform( + '{http://securite.service.tpa.cp.finances.gouv.fr/requete}CreerPaiementSecuriseRequest', + 'creerPaiementSecurise', + numcli=numcli, + saisie=saisie, + exer=exer, + montant=montant, + refdet=refdet, + mel=mel, + urlnotif=url_notification, + urlredirect=url_redirect, + objet=objet) + + def get_info_paiement(self, idop): + return self._perform( + '{http://securite.service.tpa.cp.finances.gouv.fr/reponse}RecupererDetailPaiementSecuriseRequest', + 'recupererDetailPaiementSecurise', + idOp=idop) + + +class Payment(PaymentCommon): + '''Produce requests for and verify response from the TIPI online payment + processor from the French Finance Ministry. + + ''' + + description = { + 'caption': 'TIPI, Titres Payables par Internet', + 'parameters': [ + { + 'name': 'numcli', + 'caption': _(u'Client number'), + 'help_text': _(u'6 digits number provided by DGFIP'), + 'validation': lambda s: str.isdigit(s) and len(s) == 6, + 'required': True, + }, + { + 'name': 'service_url', + 'default': SERVICE_URL, + 'caption': _(u'PayFIP WS service URL'), + 'help_text': _(u'do not modify if you do not know'), + 'validation': lambda x: x.startswith('http'), + }, + { + 'name': 'wsdl_url', + 'default': WSDL_URL, + 'caption': _(u'PayFIP WS WSDL URL'), + 'help_text': _(u'do not modify if you do not know'), + 'validation': lambda x: x.startswith('http'), + }, + { + 'name': 'saisie', + 'caption': _('Payment type'), + 'default': 'T', + 'choices': [ + ('T', _('test')), + ('X', _('activation')), + ('W', _('production')), + ], + }, + { + 'name': 'normal_return_url', + 'caption': _('User return URL'), + 'required': True, + }, + { + 'name': 'automatic_return_url', + 'caption': _('Asynchronous return URL'), + 'required': True, + }, + ], + } + + def __init__(self, *args, **kwargs): + super(Payment, self).__init__(*args, **kwargs) + wsdl_url = self.wsdl_url + # use cached WSDL + if wsdl_url == WSDL_URL: + base_path = os.path.join(os.path.dirname(__file__), 'resource', 'PaiementSecuriseService.wsdl') + wsdl_url = 'file://%s' % base_path + self.payfip = PayFiP(wsdl_url=wsdl_url, service_url=self.service_url) + + def _generate_refdet(self): + return '%s%010d' % (isonow(), random.randint(1, 1000000000)) + + def request(self, amount, email, **kwargs): + try: + montant = Decimal(amount) + # MONTANT must be sent as centimes + montant = montant * Decimal('100') + montant = montant.to_integral_value(ROUND_DOWN) + if Decimal('0') > montant > Decimal('999999'): + raise ValueError('MONTANT > 9999.99 euros') + montant = str(montant) + except ValueError: + raise ValueError( + 'MONTANT invalid format, must be ' + 'a decimal integer with less than 4 digits ' + 'before and 2 digits after the decimal point ' + ', here it is %s' % repr(amount)) + + numcli = self.numcli + urlnotif = self.automatic_return_url + urlredirect = self.normal_return_url + exer = str(datetime.date.today().year) + refdet = kwargs.get('refdet', self._generate_refdet()) + mel = email + if hasattr(mel, 'decode'): + mel = email.decode('ascii') + + try: + if '@' not in mel: + raise ValueError('no @ in MEL') + if not (6 <= len(mel) <= 80): + raise ValueError('len(MEL) is invalid, must be between 6 and 80') + except Exception as e: + raise ValueError('MEL is not a valid email, %r' % mel, e) + + # check saisie + saisie = self.saisie + if saisie not in ('T', 'X', 'W'): + raise ValueError('SAISIE invalid format, %r, must be M, T, X or A' % saisie) + + idop = self.payfip.get_idop(numcli=numcli, saisie=saisie, exer=exer, + refdet=refdet, montant=montant, mel=mel, + url_notification=urlnotif, + url_redirect=urlredirect) + + return str(idop), URL, PAYMENT_URL % idop + + def response(self, query_string, **kwargs): + fields = parse_qs(query_string, True) + idop = (fields.get('idop') or [None])[0] + + if not idop: + raise ResponseError('missing idop parameter in query string') + + try: + response = self.payfip.get_info_paiement(idop) + except PayFiPError as e: + raise ResponseError('invalid return from payfip', e) + + if response.resultrans == 'P': + result = PAID + bank_status = '' + elif response.resultrans == 'R': + result = DENIED + bank_status = 'refused' + elif response.resultrans == 'A': + result = CANCELLED + bank_status = 'cancelled' + else: + result = ERROR + bank_status = 'unknown result code: %r' % response.resultrans + + transaction_id = response.refdet + transaction_id += ' ' + idop + if response.numauto: + transaction_id += ' ' + response.numauto + + return PaymentResponse( + result=result, + bank_status=bank_status, + signed=True, + bank_data={k: response[k] for k in response}, + order_id=idop, + transaction_id=transaction_id, + test=response.saisie == 'T') + + +if __name__ == '__main__': + import click + + def show_payfip_error(func): + @functools.wraps(func) + def f(*args, **kwargs): + try: + return func(*args, **kwargs) + except PayFiPError as e: + click.echo(click.style('PayFiP ERROR : %s "%s"' % (e.code, e.message), fg='red')) + return f + + @click.group() + @click.option('--wsdl-url', default=None) + @click.option('--service-url', default=None) + @click.pass_context + def main(ctx, wsdl_url, service_url): + import logging + logging.basicConfig(level=logging.INFO) + # hide warning from zeep + logging.getLogger('zeep.wsdl.bindings.soap').level = logging.ERROR + + ctx.obj = PayFiP(wsdl_url=wsdl_url, service_url=service_url) + + def numcli(ctx, param, value): + if not isinstance(value, six.string_types) or len(value) != 6 or not value.isdigit(): + raise click.BadParameter('numcli must a 6 digits number') + return value + + @main.command() + @click.argument('numcli', callback=numcli, type=str) + @click.pass_obj + @show_payfip_error + def info_client(payfip, numcli): + response = payfip.get_info_client(numcli) + for key in response: + print('%15s:' % key, response[key]) + + @main.command() + @click.argument('numcli', callback=numcli, type=str) + @click.option('--saisie', type=click.Choice(['T', 'X', 'W']), required=True) + @click.option('--exer', type=str, required=True) + @click.option('--montant', type=int, required=True) + @click.option('--refdet', type=str, required=True) + @click.option('--mel', type=str, required=True) + @click.option('--url-notification', type=str, required=True) + @click.option('--url-redirect', type=str, required=True) + @click.option('--objet', default=None, type=str) + @click.pass_obj + @show_payfip_error + def get_idop(payfip, numcli, saisie, exer, montant, refdet, mel, objet, url_notification, url_redirect): + idop = payfip.get_idop(numcli=numcli, saisie=saisie, exer=exer, + montant=montant, refdet=refdet, mel=mel, + objet=objet, url_notification=url_notification, + url_redirect=url_redirect) + print('idOp:', idop) + print(PAYMENT_URL % idop) + + @main.command() + @click.argument('idop', type=str) + @click.pass_obj + @show_payfip_error + def info_paiement(payfip, idop): + print(payfip.get_info_paiement(idop)) + + main() + + + diff --git a/eopayment/resource/PaiementSecuriseService.wsdl b/eopayment/resource/PaiementSecuriseService.wsdl new file mode 100644 index 0000000..4ed4851 --- /dev/null +++ b/eopayment/resource/PaiementSecuriseService.wsdl @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eopayment/resource/PaiementSecuriseService1.xsd b/eopayment/resource/PaiementSecuriseService1.xsd new file mode 100644 index 0000000..5e2e575 --- /dev/null +++ b/eopayment/resource/PaiementSecuriseService1.xsd @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eopayment/resource/PaiementSecuriseService2.xsd b/eopayment/resource/PaiementSecuriseService2.xsd new file mode 100644 index 0000000..2f13e75 --- /dev/null +++ b/eopayment/resource/PaiementSecuriseService2.xsd @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/eopayment/resource/PaiementSecuriseService3.xsd b/eopayment/resource/PaiementSecuriseService3.xsd new file mode 100644 index 0000000..9efecb7 --- /dev/null +++ b/eopayment/resource/PaiementSecuriseService3.xsd @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/setup.py b/setup.py index 29763a1..5aba5a9 100755 --- a/setup.py +++ b/setup.py @@ -123,6 +123,8 @@ setuptools.setup( 'pytz', 'requests', 'six', + 'click', + 'zeep', ], cmdclass={ 'sdist': eo_sdist, diff --git a/tests/data/payfip-test_get_client_info.json b/tests/data/payfip-test_get_client_info.json new file mode 100644 index 0000000..8e6d0c9 --- /dev/null +++ b/tests/data/payfip-test_get_client_info.json @@ -0,0 +1 @@ +[["\n \n \n \n 090909\n \n \n \n\n", "\n \n \n \n RR COMPOSTEURS INDIVIDUELS\n POUETPOUET\n COLLECTE VALORISATION DECHETS\n 090909\n \n \n \n\n"]] \ No newline at end of file diff --git a/tests/data/payfip-test_get_idop_adresse_mel_incorrect.json b/tests/data/payfip-test_get_idop_adresse_mel_incorrect.json new file mode 100644 index 0000000..4308dfa --- /dev/null +++ b/tests/data/payfip-test_get_idop_adresse_mel_incorrect.json @@ -0,0 +1 @@ +[["\n \n \n \n 2019\n john.doeexample.com\n 9990000001\n 090909\n coucou\n ABCDEF\n T\n https://notif.payfip.example.com/\n https://redirect.payfip.example.com/\n \n \n \n\n", "\n \n \n S:Server\n fr.gouv.finances.cp.tpa.webservice.exceptions.FonctionnelleErreur\n \n \n A2\n \n Adresse mél incorrecte. \n 2\n \n \n \n \n\n"]] \ No newline at end of file diff --git a/tests/data/payfip-test_get_idop_ok.json b/tests/data/payfip-test_get_idop_ok.json new file mode 100644 index 0000000..e57277e --- /dev/null +++ b/tests/data/payfip-test_get_idop_ok.json @@ -0,0 +1 @@ +[["\n \n \n \n 2019\n john.doe@example.com\n 1000\n 090909\n coucou\n ABCDEFGH\n T\n https://notif.payfip.example.com/\n https://redirect.payfip.example.com/\n \n \n \n\n", "\n \n \n \n cc0cb210-1cd4-11ea-8cca-0213ad91a103\n \n \n \n\n"]] \ No newline at end of file diff --git a/tests/data/payfip-test_get_idop_refdet_error.json b/tests/data/payfip-test_get_idop_refdet_error.json new file mode 100644 index 0000000..491e7fb --- /dev/null +++ b/tests/data/payfip-test_get_idop_refdet_error.json @@ -0,0 +1 @@ +[["\n \n \n \n 2019\n john.doe@example.com\n 1000\n 090909\n coucou\n ABCD\n T\n https://notif.payfip.example.com/\n https://redirect.payfip.example.com/\n \n \n \n\n", "\n \n \n S:Server\n fr.gouv.finances.cp.tpa.webservice.exceptions.FonctionnelleErreur\n \n \n R3\n \n Le format du paramètre REFDET n'est pas conforme\n 2\n \n \n \n \n\n"]] \ No newline at end of file diff --git a/tests/data/payfip-test_get_info_paiement_P1.json b/tests/data/payfip-test_get_info_paiement_P1.json new file mode 100644 index 0000000..cc0154e --- /dev/null +++ b/tests/data/payfip-test_get_info_paiement_P1.json @@ -0,0 +1 @@ +[["\n \n \n \n cc0cb210-1cd4-11ea-8cca-0213ad91a103\n \n \n \n\n", "\n \n \n S:Server\n fr.gouv.finances.cp.tpa.webservice.exceptions.FonctionnelleErreur\n \n \n P1\n \n IdOp incorrect.\n 2\n \n \n \n \n\n"]] \ No newline at end of file diff --git a/tests/data/payfip-test_get_info_paiement_P5.json b/tests/data/payfip-test_get_info_paiement_P5.json new file mode 100644 index 0000000..f4682f4 --- /dev/null +++ b/tests/data/payfip-test_get_info_paiement_P5.json @@ -0,0 +1 @@ +[["\n \n \n \n cc0cb210-1cd4-11ea-8cca-0213ad91a103\n \n \n \n\n", "\n \n \n S:Server\n fr.gouv.finances.cp.tpa.webservice.exceptions.FonctionnelleErreur\n \n \n P5\n Résultat de la transaction non connu.2\n \n \n \n \n\n"]] diff --git a/tests/data/payfip-test_get_info_paiement_ok.json b/tests/data/payfip-test_get_info_paiement_ok.json new file mode 100644 index 0000000..afea9e5 --- /dev/null +++ b/tests/data/payfip-test_get_info_paiement_ok.json @@ -0,0 +1 @@ +[["\n \n \n \n cc0cb210-1cd4-11ea-8cca-0213ad91a103\n \n \n \n\n", "\n \n \n \n 12122019201311cc0cb210-1cd4-11ea-8cca-0213ad91a103john.doe@example.com1000112233445566-tip090909coucouEFEFAEFGVT\n \n \n \n\n"]] \ No newline at end of file diff --git a/tests/data/payfip-test_payment_cancelled.json b/tests/data/payfip-test_payment_cancelled.json new file mode 100644 index 0000000..67cf7ee --- /dev/null +++ b/tests/data/payfip-test_payment_cancelled.json @@ -0,0 +1,4 @@ +[ + ["\n \n \n \n 2019\n john.doe@example.com\n 1000\n 090909\n 201912261758460053903194\n T\n https://notif.payfip.example.com/\n https://redirect.payfip.example.com/\n \n \n \n\n", "\n \n \n \n cc0cb210-1cd4-11ea-8cca-0213ad91a103\n \n \n \n\n"], + ["\n \n \n \n cc0cb210-1cd4-11ea-8cca-0213ad91a103\n \n \n \n\n", "\n \n \n \n 12122019201311cc0cb210-1cd4-11ea-8cca-0213ad91a103john.doe@example.com1000090909201912261758460053903194AT\n \n \n \n\n"] +] diff --git a/tests/data/payfip-test_payment_denied.json b/tests/data/payfip-test_payment_denied.json new file mode 100644 index 0000000..d877fa4 --- /dev/null +++ b/tests/data/payfip-test_payment_denied.json @@ -0,0 +1,4 @@ +[ + ["\n \n \n \n 2019\n john.doe@example.com\n 1000\n 090909\n 201912261758460053903194\n T\n https://notif.payfip.example.com/\n https://redirect.payfip.example.com/\n \n \n \n\n", "\n \n \n \n cc0cb210-1cd4-11ea-8cca-0213ad91a103\n \n \n \n\n"], + ["\n \n \n \n cc0cb210-1cd4-11ea-8cca-0213ad91a103\n \n \n \n\n", "\n \n \n \n 12122019201311cc0cb210-1cd4-11ea-8cca-0213ad91a103john.doe@example.com1000090909201912261758460053903194RT\n \n \n \n\n"] +] diff --git a/tests/data/payfip-test_payment_ok.json b/tests/data/payfip-test_payment_ok.json new file mode 100644 index 0000000..75bc07c --- /dev/null +++ b/tests/data/payfip-test_payment_ok.json @@ -0,0 +1,4 @@ +[ + ["\n \n \n \n 2019\n john.doe@example.com\n 1000\n 090909\n 201912261758460053903194\n T\n https://notif.payfip.example.com/\n https://redirect.payfip.example.com/\n \n \n \n\n", "\n \n \n \n cc0cb210-1cd4-11ea-8cca-0213ad91a103\n \n \n \n\n"], + ["\n \n \n \n cc0cb210-1cd4-11ea-8cca-0213ad91a103\n \n \n \n\n", "\n \n \n \n 12122019201311cc0cb210-1cd4-11ea-8cca-0213ad91a103john.doe@example.com1000112233445566-tip090909201912261758460053903194PT\n \n \n \n\n"] +] diff --git a/tests/test_payfip_ws.py b/tests/test_payfip_ws.py new file mode 100644 index 0000000..619c578 --- /dev/null +++ b/tests/test_payfip_ws.py @@ -0,0 +1,245 @@ +# coding: utf-8 +# +# eopayment - online payment library +# Copyright (C) 2011-2019 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from __future__ import print_function, unicode_literals + +import json +import lxml.etree as ET + +import httmock +import pytest + +from zeep.plugins import HistoryPlugin + +import eopayment +from eopayment.payfip_ws import PayFiP, PayFiPError + + +def xmlindent(content): + if hasattr(content, 'encode') or hasattr(content, 'decode'): + content = ET.fromstring(content) + return ET.tostring(content, pretty_print=True).decode('utf-8', 'ignore') + +NUMCLI = '090909' + + +class PayFiPHTTMock(object): + def __init__(self, request): + history_path = 'tests/data/payfip-%s.json' % request.function.__name__ + with open(history_path) as fd: + self.history = json.load(fd) + self.counter = 0 + + @httmock.urlmatch() + def mock(self, url, request): + request_content, response_content = self.history[self.counter] + self.counter += 1 + assert xmlindent(request.body) == request_content + return response_content + + +@pytest.fixture +def payfip(request): + history = HistoryPlugin() + + @httmock.urlmatch() + def raise_on_request(url, request): + # ensure we do not access network + from requests.exceptions import RequestException + raise RequestException('huhu') + + with httmock.HTTMock(raise_on_request): + payfip = PayFiP(wsdl_url='file://eopayment/resource/PaiementSecuriseService.wsdl', + zeep_client_kwargs={'plugins': [history]}) + try: + if 'update_data' not in request.keywords: + with httmock.HTTMock(PayFiPHTTMock(request).mock): + yield payfip + else: + yield payfip + finally: + # add @pytest.mark.update_data to test to update fixtures data + if 'update_data' in request.keywords: + history_path = 'tests/data/payfip-%s.json' % request.function.__name__ + d = [ + (xmlindent(exchange['sent']['envelope']), + xmlindent(exchange['received']['envelope'])) + for exchange in history._buffer + ] + content = json.dumps(d) + with open(history_path, 'wb') as fd: + fd.write(content) + +# pytestmark = pytest.mark.update_data + + +def test_get_client_info(payfip): + result = payfip.get_info_client(NUMCLI) + assert result.numcli == NUMCLI + assert result.libelleN2 == 'POUETPOUET' + +NOTIF_URL = 'https://notif.payfip.example.com/' +REDIRECT_URL = 'https://redirect.payfip.example.com/' + + +def test_get_idop_ok(payfip): + result = payfip.get_idop( + numcli=NUMCLI, + exer='2019', + refdet='ABCDEFGH', + montant='1000', + mel='john.doe@example.com', + objet='coucou', + url_notification=NOTIF_URL, + url_redirect=REDIRECT_URL, + saisie='T') + assert result == 'cc0cb210-1cd4-11ea-8cca-0213ad91a103' + + +def test_get_idop_refdet_error(payfip): + with pytest.raises(PayFiPError, match='.*R3.*Le format.*REFDET.*conforme'): + payfip.get_idop( + numcli=NUMCLI, + exer='2019', + refdet='ABCD', + montant='1000', + mel='john.doe@example.com', + objet='coucou', + url_notification='https://notif.payfip.example.com/', + url_redirect='https://redirect.payfip.example.com/', + saisie='T') + + +def test_get_idop_adresse_mel_incorrect(payfip): + with pytest.raises(PayFiPError, match='.*A2.*Adresse.*incorrecte'): + payfip.get_idop( + numcli=NUMCLI, + exer='2019', + refdet='ABCDEF', + montant='9990000001', + mel='john.doeexample.com', + objet='coucou', + url_notification='https://notif.payfip.example.com/', + url_redirect='https://redirect.payfip.example.com/', + saisie='T') + + +def test_get_info_paiement_ok(payfip): + result = payfip.get_info_paiement('cc0cb210-1cd4-11ea-8cca-0213ad91a103') + assert {k: result[k] for k in result} == { + 'dattrans': '12122019', + 'exer': '20', + 'heurtrans': '1311', + 'idOp': 'cc0cb210-1cd4-11ea-8cca-0213ad91a103', + 'mel': 'john.doe@example.com', + 'montant': '1000', + 'numauto': '112233445566-tip', + 'numcli': NUMCLI, + 'objet': 'coucou', + 'refdet': 'EFEFAEFG', + 'resultrans': 'V', + 'saisie': 'T' + } + + +def test_get_info_paiement_P1(payfip): + # idop par pas encore reçu par la plate-forme ou déjà nettoyé (la nuit) + with pytest.raises(PayFiPError, match='.*P1.*IdOp incorrect.*'): + payfip.get_info_paiement('cc0cb210-1cd4-11ea-8cca-0213ad91a103') + + +def test_get_info_paiement_P5(payfip): + # idop reçu par la plate-forme mais transaction en cours + with pytest.raises(PayFiPError, match='.*P5.*sultat de la transaction non connu.*'): + payfip.get_info_paiement('cc0cb210-1cd4-11ea-8cca-0213ad91a103') + + +def test_payment_ok(request): + payment = eopayment.Payment('payfip_ws', { + 'numcli': '090909', + 'automatic_return_url': NOTIF_URL, + 'normal_return_url': REDIRECT_URL, + }) + + with httmock.HTTMock(PayFiPHTTMock(request).mock): + payment_id, kind, url = payment.request( + amount='10.00', + email='john.doe@example.com', + # make test deterministic + refdet='201912261758460053903194') + + assert payment_id == 'cc0cb210-1cd4-11ea-8cca-0213ad91a103' + assert kind == eopayment.URL + assert url == 'https://www.tipi.budget.gouv.fr/tpa/paiementws.web?idop=cc0cb210-1cd4-11ea-8cca-0213ad91a103' + + response = payment.response('idop=%s' % payment_id) + assert response.result == eopayment.PAID + assert response.bank_status == '' + assert response.order_id == payment_id + assert response.transaction_id == ( + '201912261758460053903194 cc0cb210-1cd4-11ea-8cca-0213ad91a103 112233445566-tip') + + +def test_payment_denied(request): + payment = eopayment.Payment('payfip_ws', { + 'numcli': '090909', + 'automatic_return_url': NOTIF_URL, + 'normal_return_url': REDIRECT_URL, + }) + + with httmock.HTTMock(PayFiPHTTMock(request).mock): + payment_id, kind, url = payment.request( + amount='10.00', + email='john.doe@example.com', + # make test deterministic + refdet='201912261758460053903194') + + assert payment_id == 'cc0cb210-1cd4-11ea-8cca-0213ad91a103' + assert kind == eopayment.URL + assert url == 'https://www.tipi.budget.gouv.fr/tpa/paiementws.web?idop=cc0cb210-1cd4-11ea-8cca-0213ad91a103' + + response = payment.response('idop=%s' % payment_id) + assert response.result == eopayment.DENIED + assert response.bank_status == 'refused' + assert response.order_id == payment_id + assert response.transaction_id == '201912261758460053903194 cc0cb210-1cd4-11ea-8cca-0213ad91a103' + + +def test_payment_cancelled(request): + payment = eopayment.Payment('payfip_ws', { + 'numcli': '090909', + 'automatic_return_url': NOTIF_URL, + 'normal_return_url': REDIRECT_URL, + }) + + with httmock.HTTMock(PayFiPHTTMock(request).mock): + payment_id, kind, url = payment.request( + amount='10.00', + email='john.doe@example.com', + # make test deterministic + refdet='201912261758460053903194') + + assert payment_id == 'cc0cb210-1cd4-11ea-8cca-0213ad91a103' + assert kind == eopayment.URL + assert url == 'https://www.tipi.budget.gouv.fr/tpa/paiementws.web?idop=cc0cb210-1cd4-11ea-8cca-0213ad91a103' + + response = payment.response('idop=%s' % payment_id) + assert response.result == eopayment.CANCELLED + assert response.bank_status == 'cancelled' + assert response.order_id == payment_id + assert response.transaction_id == '201912261758460053903194 cc0cb210-1cd4-11ea-8cca-0213ad91a103' diff --git a/tox.ini b/tox.ini index f38d65d..b49e3c4 100644 --- a/tox.ini +++ b/tox.ini @@ -18,3 +18,11 @@ deps = coverage pytest-freezegun py2: pytest-cov mock + httmock + lxml + +[pytest] +filterwarnings = + ignore:defusedxml.lxml is no longer supported.* +markers = + update_data -- 2.24.0