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..eaeaeb3
--- /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, PaymentException)
+
+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(PaymentException):
+ 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 not (Decimal('0') < montant <= Decimal('10000000')):
+ raise ValueError('MONTANT > 100000 euros or < 0')
+ 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..0076bc6
--- /dev/null
+++ b/tests/test_payfip_ws.py
@@ -0,0 +1,251 @@
+# 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'
+
+
+# freeze time to fix EXER field to 2019
+@pytest.fixture(autouse=True)
+def freezer(freezer):
+ freezer.move_to('2019-12-12')
+
+
+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