summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBenjamin Dauvergne <bdauvergne@entrouvert.com>2020-10-14 09:32:07 (GMT)
committerBenjamin Dauvergne <bdauvergne@entrouvert.com>2020-11-03 16:21:52 (GMT)
commit5bf49c33ba2fe0fc6c089e1c0498d5de5e1d31d1 (patch)
treebc71f4d5abfb613ad961816f59a29fd949c2dacd
parent5b5f4c165b6d6a5960ba2cfd8ad5b92fb257737e (diff)
downloadeopayment-5bf49c33ba2fe0fc6c089e1c0498d5de5e1d31d1.zip
eopayment-5bf49c33ba2fe0fc6c089e1c0498d5de5e1d31d1.tar.gz
eopayment-5bf49c33ba2fe0fc6c089e1c0498d5de5e1d31d1.tar.bz2
payfip_ws: implement payment_status (#47670)
The response() method is also refactored around payment_status().
-rw-r--r--eopayment/payfip_ws.py52
-rw-r--r--tests/test_payfip_ws.py278
2 files changed, 239 insertions, 91 deletions
diff --git a/eopayment/payfip_ws.py b/eopayment/payfip_ws.py
index 843ba5b..bef4dc0 100644
--- a/eopayment/payfip_ws.py
+++ b/eopayment/payfip_ws.py
@@ -26,6 +26,8 @@ import xml.etree.ElementTree as ET
from gettext import gettext as _
+import pytz
+
import six
from six.moves.urllib.parse import parse_qs
@@ -34,7 +36,8 @@ import zeep.exceptions
from .systempayv2 import isonow
from .common import (PaymentCommon, PaymentResponse, URL, PAID, DENIED,
- CANCELLED, ERROR, ResponseError, PaymentException)
+ CANCELLED, ERROR, ResponseError, PaymentException,
+ WAITING, EXPIRED)
WSDL_URL = 'https://www.tipi.budget.gouv.fr/tpa/services/mas_securite/contrat_paiement_securise/PaiementSecuriseService?wsdl' # noqa: E501
@@ -229,6 +232,46 @@ class Payment(PaymentCommon):
return str(idop), URL, self.payment_url + '?idop=%s' % idop
+ def payment_status(self, transaction_id, transaction_date=None, **kwargs):
+ # idop are valid for 15 minutes after their generation
+ # between generation and payment, any call to get_info_paiement() will return a PayFiPError with code=P5
+ # before the end of the 15 minutes it can mean the payment is in progress
+ # after the 15 minutes period it means the payment will never happen,
+ # and after one day the code will change for P1, meaning the idop is
+ # now unknown as it as been cleaned by the night cleaning job.
+ #
+ # So in order to interpret the meaning of PayFiP error codes we need
+ # the date of the start of the transaction and add to it some margin
+ # to.
+ idop = transaction_id
+ if transaction_date:
+ if transaction_date.tzinfo: # date is aware
+ now = datetime.datetime.now(tz=pytz.utc)
+ else:
+ now = datetime.datetime.now()
+ delta = now - transaction_date
+ else:
+ delta = datetime.timedelta(seconds=0)
+ # set the threshold between transaction 'in progress' and 'expired' at 20 minutes
+ threshold = datetime.timedelta(seconds=20 * 60)
+
+ try:
+ response = self.payfip.get_info_paiement(idop)
+ except PayFiPError as e:
+ if e.code == 'P1' or (
+ e.code == 'P5' and delta >= threshold):
+ return PaymentResponse(
+ result=EXPIRED,
+ signed=True,
+ order_id=transaction_id)
+ if e.code == 'P5' and delta < threshold:
+ return PaymentResponse(
+ result=WAITING,
+ signed=True,
+ order_id=transaction_id)
+ raise e
+ return self.payfip_response_to_eopayment_response(idop, response)
+
def response(self, query_string, **kwargs):
fields = parse_qs(query_string, True)
idop = (fields.get('idop') or [None])[0]
@@ -236,11 +279,10 @@ class Payment(PaymentCommon):
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)
+ return self.payment_status(idop)
+ @classmethod
+ def payfip_response_to_eopayment_response(cls, idop, response):
if response.resultrans == 'P':
result = PAID
bank_status = 'paid CB'
diff --git a/tests/test_payfip_ws.py b/tests/test_payfip_ws.py
index 607bb97..ca3088c 100644
--- a/tests/test_payfip_ws.py
+++ b/tests/test_payfip_ws.py
@@ -18,9 +18,12 @@
from __future__ import print_function, unicode_literals
+import datetime
import json
import lxml.etree as ET
+import pytz
+
import httmock
import pytest
@@ -42,16 +45,26 @@ NUMCLI = '090909'
@pytest.fixture(autouse=True)
def freezer(freezer):
freezer.move_to('2019-12-12')
+ 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, request):
- history_path = 'tests/data/payfip-%s.json' % request.function.__name__
+ def __init__(self, history_name):
+ history_path = 'tests/data/payfip-%s.json' % history_name
with open(history_path) as fd:
self.history = json.load(fd)
self.counter = 0
- @httmock.urlmatch()
+ @httmock.urlmatch(scheme='https')
def mock(self, url, request):
request_content, response_content = self.history[self.counter]
self.counter += 1
@@ -60,7 +73,22 @@ class PayFiPHTTMock(object):
@pytest.fixture
-def payfip(request):
+def history_name(request):
+ return getattr(request.function, 'history_name', request.function.__name__)
+
+
+@pytest.fixture
+def history(history_name, request):
+ if 'update_data' not in request.keywords:
+ history_mock = PayFiPHTTMock(history_name)
+ with httmock.HTTMock(history_mock.mock):
+ yield history_mock
+ else:
+ yield None
+
+
+@pytest.fixture
+def payfip(history, history_name, request):
history = HistoryPlugin()
@httmock.urlmatch()
@@ -72,24 +100,27 @@ def payfip(request):
with httmock.HTTMock(raise_on_request):
payfip = PayFiP(wsdl_url='./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)
+ yield payfip
+
+ # add @pytest.mark.update_data to test to update fixtures data
+ if 'update_data' in request.keywords:
+ history_path = 'tests/data/payfip-%s.json' % history_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)
+
+
+def set_history_name(name):
+ # decorator to add history_name to test
+ def decorator(func):
+ func.history_name = name
+ return func
+ return decorator
# pytestmark = pytest.mark.update_data
@@ -163,89 +194,164 @@ def test_get_info_paiement_ok(payfip):
}
-def test_get_info_paiement_P1(payfip):
+def test_get_info_paiement_P1(payfip, freezer):
# 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')
+@set_history_name('test_get_info_paiement_P1')
+def test_P1_and_payment_status(payfip, backend, freezer):
+ 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):
+ 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):
+ 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):
+ 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)
+ assert response.result == eopayment.EXPIRED
+
+
+@set_history_name('test_get_info_paiement_P1')
+def test_P1_and_payment_status_utc_naive_now_later(payfip, backend, freezer):
+ now = datetime.datetime.now()
+ freezer.move_to(datetime.timedelta(minutes=25))
+ response = backend.payment_status(transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=now)
+ assert response.result == eopayment.EXPIRED
+
+
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,
- })
+@set_history_name('test_get_info_paiement_P5')
+def test_P5_and_payment_status(payfip, backend, freezer):
+ response = backend.payment_status(transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103')
+ assert response.result == eopayment.WAITING
- 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'
+@set_history_name('test_get_info_paiement_P5')
+def test_P5_and_payment_status_utc_aware_now(payfip, backend, freezer):
+ 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.WAITING
- response = payment.response('idop=%s' % payment_id)
- assert response.result == eopayment.PAID
- assert response.bank_status == 'paid CB'
- assert response.order_id == payment_id
- assert response.transaction_id == (
- '201912261758460053903194 cc0cb210-1cd4-11ea-8cca-0213ad91a103 112233445566-tip')
+@set_history_name('test_get_info_paiement_P5')
+def test_P5_and_payment_status_utc_naive_now(payfip, backend, freezer):
+ now = datetime.datetime.now()
+ response = backend.payment_status(transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=now)
+ assert response.result == eopayment.WAITING
-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')
+@set_history_name('test_get_info_paiement_P5')
+def test_P5_and_payment_status_utc_aware_now_later(payfip, 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)
+ assert response.result == eopayment.EXPIRED
- 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 CB'
- assert response.order_id == payment_id
- assert response.transaction_id == '201912261758460053903194 cc0cb210-1cd4-11ea-8cca-0213ad91a103'
+@set_history_name('test_get_info_paiement_P5')
+def test_P5_and_payment_status_utc_naive_now_later(payfip, backend, freezer):
+ now = datetime.datetime.now()
+ freezer.move_to(datetime.timedelta(minutes=25))
+ response = backend.payment_status(transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=now)
+ assert response.result == eopayment.EXPIRED
-def test_payment_cancelled(request):
- payment = eopayment.Payment('payfip_ws', {
- 'numcli': '090909',
- 'automatic_return_url': NOTIF_URL,
- 'normal_return_url': REDIRECT_URL,
- })
+def test_payment_ok(payfip, backend):
+ payment_id, kind, url = backend.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 = backend.response('idop=%s' % payment_id)
+ assert response.result == eopayment.PAID
+ assert response.bank_status == 'paid CB'
+ assert response.order_id == payment_id
+ assert response.transaction_id == (
+ '201912261758460053903194 cc0cb210-1cd4-11ea-8cca-0213ad91a103 112233445566-tip')
+
+
+@set_history_name('test_payment_ok')
+def test_payment_status_ok(backend, freezer, history):
+ 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):
+ payment_id, kind, url = backend.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 = backend.response('idop=%s' % payment_id)
+ assert response.result == eopayment.DENIED
+ assert response.bank_status == 'refused CB'
+ assert response.order_id == payment_id
+ assert response.transaction_id == '201912261758460053903194 cc0cb210-1cd4-11ea-8cca-0213ad91a103'
+
+
+@set_history_name('test_payment_denied')
+def test_payment_status_denied(backend, freezer, history):
+ 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):
+ payment_id, kind, url = backend.request(
+ amount='10.00',
+ email='john.doe@example.com',
+ # make test deterministic
+ refdet='201912261758460053903194')
- 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 CB'
- assert response.order_id == payment_id
- assert response.transaction_id == '201912261758460053903194 cc0cb210-1cd4-11ea-8cca-0213ad91a103'
+ 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 = backend.response('idop=%s' % payment_id)
+ assert response.result == eopayment.CANCELLED
+ assert response.bank_status == 'cancelled CB'
+ assert response.order_id == payment_id
+ assert response.transaction_id == '201912261758460053903194 cc0cb210-1cd4-11ea-8cca-0213ad91a103'
+
+
+@set_history_name('test_payment_cancelled')
+def test_payment_status_cancelled(backend, freezer, history):
+ 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