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.
This commit is contained in:
Benjamin Dauvergne 2020-10-30 09:57:15 +01:00
parent 5bf49c33ba
commit 4ba0534d01
2 changed files with 199 additions and 36 deletions

View File

@ -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,19 @@ 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'[\'-]', ' ', objet).strip()
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 +216,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 +265,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)

View File

@ -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 hors sol 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 TR 12345',
'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 TR 12345',
'mel': MEL,
'numcli': NUMCLI,
'saisie': 'T',
'url_notification': NOTIF_URL,
'url_redirect': REDIRECT_URL,
}