From f29bbbef1b0794a257a592567d6bcec743f68cd1 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Fri, 30 Oct 2020 09:57:15 +0100 Subject: [PATCH] payfip_ws: add new request() parameters (#48135) The added parameters are : * subject, to pass the description of a payment, * orderid, to pass the order number for the payment, * transaction_id, to identify a payment with an external identifier, * exer, custom field for PayFiP. --- eopayment/payfip_ws.py | 50 +++++++++-- tests/test_payfip_ws.py | 184 +++++++++++++++++++++++++++++++++------- 2 files changed, 198 insertions(+), 36 deletions(-) diff --git a/eopayment/payfip_ws.py b/eopayment/payfip_ws.py index bef4dc0..8669111 100644 --- a/eopayment/payfip_ws.py +++ b/eopayment/payfip_ws.py @@ -18,10 +18,11 @@ from __future__ import print_function, unicode_literals import copy import datetime -from decimal import Decimal, ROUND_DOWN import functools import os import random +import re +import unicodedata import xml.etree.ElementTree as ET from gettext import gettext as _ @@ -37,7 +38,7 @@ import zeep.exceptions from .systempayv2 import isonow from .common import (PaymentCommon, PaymentResponse, URL, PAID, DENIED, CANCELLED, ERROR, ResponseError, PaymentException, - WAITING, EXPIRED) + WAITING, EXPIRED, force_text) WSDL_URL = 'https://www.tipi.budget.gouv.fr/tpa/services/mas_securite/contrat_paiement_securise/PaiementSecuriseService?wsdl' # noqa: E501 @@ -45,6 +46,8 @@ SERVICE_URL = 'https://www.tipi.budget.gouv.fr/tpa/services/securite' # noqa: E PAYMENT_URL = 'https://www.tipi.budget.gouv.fr/tpa/paiementws.web' +REFDET_RE = re.compile(r'^[A-Za-z0-9]{1,30}$') + def clear_namespace(element): def helper(element): @@ -58,6 +61,18 @@ def clear_namespace(element): return element +def normalize_objet(objet): + '''Make objet a string of 100 chars in alphabet [A-Za-z0-9 ]''' + if not objet: + return objet + + objet = force_text(objet) + objet = unicodedata.normalize('NFKD', objet).encode('ascii', 'ignore').decode() + objet = re.sub(r'[^A-Za-z0-9 ]', '', objet).strip() + objet = re.sub(r'[\s]+', ' ', objet) + return objet[:100] + + class PayFiPError(PaymentException): def __init__(self, code, message, origin=None): self.code = code @@ -200,18 +215,40 @@ class Payment(PaymentCommon): def _generate_refdet(self): return '%s%010d' % (isonow(), random.randint(1, 1000000000)) - def request(self, amount, email, **kwargs): + def request(self, amount, email, refdet=None, exer=None, orderid=None, + subject=None, transaction_id=None, **kwargs): montant = self.clean_amount(amount, max_amount=100000) 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()) + + if not exer: + exer = str(datetime.date.today().year) + + if refdet: + pass + elif transaction_id and REFDET_RE.match(transaction_id): + refdet = transaction_id + elif orderid and REFDET_RE.match(orderid): + refdet = orderid + else: + refdet = self._generate_refdet() + + objet_parts = [] + if orderid and refdet != orderid: + objet_parts.extend(['O', orderid]) + if subject: + if objet_parts: + objet_parts.append('S') + objet_parts.append(subject) + if transaction_id and refdet != transaction_id: + objet_parts.extend(['T', transaction_id]) + objet = normalize_objet(' '.join(objet_parts)) + mel = email if hasattr(mel, 'decode'): mel = email.decode('ascii') - try: if '@' not in mel: raise ValueError('no @ in MEL') @@ -227,6 +264,7 @@ class Payment(PaymentCommon): idop = self.payfip.get_idop(numcli=numcli, saisie=saisie, exer=exer, refdet=refdet, montant=montant, mel=mel, + objet=objet or None, url_notification=urlnotif, url_redirect=urlredirect) diff --git a/tests/test_payfip_ws.py b/tests/test_payfip_ws.py index ca3088c..2f2d501 100644 --- a/tests/test_payfip_ws.py +++ b/tests/test_payfip_ws.py @@ -21,6 +21,7 @@ from __future__ import print_function, unicode_literals import datetime import json import lxml.etree as ET +import mock import pytz @@ -30,7 +31,16 @@ import pytest from zeep.plugins import HistoryPlugin import eopayment -from eopayment.payfip_ws import PayFiP, PayFiPError +from eopayment.payfip_ws import PayFiP, PayFiPError, normalize_objet + + +NUMCLI = '090909' +NOTIF_URL = 'https://notif.payfip.example.com/' +REDIRECT_URL = 'https://redirect.payfip.example.com/' +MEL = 'john.doe@example.com' +EXER = '2019' +REFDET = '201912261758460053903194' +REFDET_GEN = '201912261758460053903195' def xmlindent(content): @@ -38,8 +48,6 @@ def xmlindent(content): 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) @@ -48,15 +56,6 @@ def freezer(freezer): return freezer -@pytest.fixture -def backend(history): - return eopayment.Payment('payfip_ws', { - 'numcli': '090909', - 'automatic_return_url': NOTIF_URL, - 'normal_return_url': REDIRECT_URL, - }) - - class PayFiPHTTMock(object): def __init__(self, history_name): history_path = 'tests/data/payfip-%s.json' % history_name @@ -87,6 +86,24 @@ def history(history_name, request): yield None +@pytest.fixture +def get_idop(): + with mock.patch('eopayment.payfip_ws.PayFiP.get_idop') as get_idop: + get_idop.return_value = 'idop-1234' + yield get_idop + + +@pytest.fixture +def backend(request): + with mock.patch('eopayment.payfip_ws.Payment._generate_refdet') as _generate_refdet: + _generate_refdet.return_value = REFDET_GEN + yield eopayment.Payment('payfip_ws', { + 'numcli': '090909', + 'automatic_return_url': NOTIF_URL, + 'normal_return_url': REDIRECT_URL, + }) + + @pytest.fixture def payfip(history, history_name, request): history = HistoryPlugin() @@ -130,9 +147,6 @@ def test_get_client_info(payfip): 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( @@ -140,7 +154,7 @@ def test_get_idop_ok(payfip): exer='2019', refdet='ABCDEFGH', montant='1000', - mel='john.doe@example.com', + mel=MEL, objet='coucou', url_notification=NOTIF_URL, url_redirect=REDIRECT_URL, @@ -155,7 +169,7 @@ def test_get_idop_refdet_error(payfip): exer='2019', refdet='ABCD', montant='1000', - mel='john.doe@example.com', + mel=MEL, objet='coucou', url_notification='https://notif.payfip.example.com/', url_redirect='https://redirect.payfip.example.com/', @@ -183,7 +197,7 @@ def test_get_info_paiement_ok(payfip): 'exer': '20', 'heurtrans': '1311', 'idOp': 'cc0cb210-1cd4-11ea-8cca-0213ad91a103', - 'mel': 'john.doe@example.com', + 'mel': MEL, 'montant': '1000', 'numauto': '112233445566-tip', 'numcli': NUMCLI, @@ -201,27 +215,27 @@ def test_get_info_paiement_P1(payfip, freezer): @set_history_name('test_get_info_paiement_P1') -def test_P1_and_payment_status(payfip, backend, freezer): +def test_P1_and_payment_status(history, backend): response = backend.payment_status(transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103') assert response.result == eopayment.EXPIRED @set_history_name('test_get_info_paiement_P1') -def test_P1_and_payment_status_utc_aware_now(payfip, backend, freezer): +def test_P1_and_payment_status_utc_aware_now(history, backend): utc_now = datetime.datetime.now(pytz.utc) response = backend.payment_status(transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=utc_now) assert response.result == eopayment.EXPIRED @set_history_name('test_get_info_paiement_P1') -def test_P1_and_payment_status_utc_naive_now(payfip, backend, freezer): +def test_P1_and_payment_status_utc_naive_now(history, backend): now = datetime.datetime.now() response = backend.payment_status(transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=now) assert response.result == eopayment.EXPIRED @set_history_name('test_get_info_paiement_P1') -def test_P1_and_payment_status_utc_aware_now_later(payfip, backend, freezer): +def test_P1_and_payment_status_utc_aware_now_later(history, backend, freezer): utc_now = datetime.datetime.now(pytz.utc) freezer.move_to(datetime.timedelta(minutes=25)) response = backend.payment_status(transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=utc_now) @@ -281,7 +295,7 @@ def test_P5_and_payment_status_utc_naive_now_later(payfip, backend, freezer): def test_payment_ok(payfip, backend): payment_id, kind, url = backend.request( amount='10.00', - email='john.doe@example.com', + email=MEL, # make test deterministic refdet='201912261758460053903194') @@ -298,17 +312,17 @@ def test_payment_ok(payfip, backend): @set_history_name('test_payment_ok') -def test_payment_status_ok(backend, freezer, history): +def test_payment_status_ok(history, backend, freezer): history.counter = 1 # only the response now = datetime.datetime.now() response = backend.payment_status(transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=now) assert response.result == eopayment.PAID -def test_payment_denied(backend): +def test_payment_denied(history, backend): payment_id, kind, url = backend.request( amount='10.00', - email='john.doe@example.com', + email=MEL, # make test deterministic refdet='201912261758460053903194') @@ -324,17 +338,17 @@ def test_payment_denied(backend): @set_history_name('test_payment_denied') -def test_payment_status_denied(backend, freezer, history): +def test_payment_status_denied(history, backend, freezer): history.counter = 1 # only the response now = datetime.datetime.now() response = backend.payment_status(transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=now) assert response.result == eopayment.DENIED -def test_payment_cancelled(backend): +def test_payment_cancelled(history, backend): payment_id, kind, url = backend.request( amount='10.00', - email='john.doe@example.com', + email=MEL, # make test deterministic refdet='201912261758460053903194') @@ -350,8 +364,118 @@ def test_payment_cancelled(backend): @set_history_name('test_payment_cancelled') -def test_payment_status_cancelled(backend, freezer, history): +def test_payment_status_cancelled(history, backend, freezer): history.counter = 1 # only the response now = datetime.datetime.now() response = backend.payment_status(transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=now) assert response.result == eopayment.CANCELLED + + +def test_normalize_objet(): + assert normalize_objet(None) is None + assert ( + normalize_objet('18/09/2020 Établissement attestation hors-sol n#1234') + == '18092020 Etablissement attestation horssol n1234' + ) + + +def test_refdet_exer(get_idop, backend): + payment_id, kind, url = backend.request( + amount='10.00', + email=MEL, + # make test deterministic + exer=EXER, + refdet=REFDET) + + assert payment_id == 'idop-1234' + kwargs = get_idop.call_args[1] + + assert kwargs == { + 'exer': EXER, + 'refdet': REFDET, + 'montant': '1000', + 'objet': None, + 'mel': MEL, + 'numcli': NUMCLI, + 'saisie': 'T', + 'url_notification': NOTIF_URL, + 'url_redirect': REDIRECT_URL, + } + + +def test_transaction_id_orderid_subject(get_idop, backend): + payment_id, kind, url = backend.request( + amount='10.00', + email=MEL, + # make test deterministic + exer=EXER, + transaction_id='TR12345', + orderid='F20190003', + subject='Précompte famille #1234') + + assert payment_id == 'idop-1234' + kwargs = get_idop.call_args[1] + + assert kwargs == { + 'exer': EXER, + 'refdet': 'TR12345', + 'montant': '1000', + 'objet': 'O F20190003 S Precompte famille 1234', + 'mel': MEL, + 'numcli': NUMCLI, + 'saisie': 'T', + 'url_notification': NOTIF_URL, + 'url_redirect': REDIRECT_URL, + } + + +def test_invalid_transaction_id_valid_orderid(get_idop, backend): + payment_id, kind, url = backend.request( + amount='10.00', + email=MEL, + # make test deterministic + exer=EXER, + transaction_id='TR-12345', + orderid='F20190003', + subject='Précompte famille #1234') + + assert payment_id == 'idop-1234' + kwargs = get_idop.call_args[1] + + assert kwargs == { + 'exer': EXER, + 'refdet': 'F20190003', + 'montant': '1000', + 'objet': 'Precompte famille 1234 T TR12345', + 'mel': MEL, + 'numcli': NUMCLI, + 'saisie': 'T', + 'url_notification': NOTIF_URL, + 'url_redirect': REDIRECT_URL, + } + + +def test_invalid_transaction_id_invalid_orderid(get_idop, backend): + payment_id, kind, url = backend.request( + amount='10.00', + email=MEL, + # make test deterministic + exer=EXER, + transaction_id='TR-12345', + orderid='F/20190003', + subject='Précompte famille #1234') + + assert payment_id == 'idop-1234' + kwargs = get_idop.call_args[1] + + assert kwargs == { + 'exer': EXER, + 'refdet': REFDET_GEN, + 'montant': '1000', + 'objet': 'O F20190003 S Precompte famille 1234 T TR12345', + 'mel': MEL, + 'numcli': NUMCLI, + 'saisie': 'T', + 'url_notification': NOTIF_URL, + 'url_redirect': REDIRECT_URL, + } -- 2.29.1