allow arbitrary date for deferred payment (#26992)

This commit is contained in:
Emmanuel Cazenave 2018-10-12 15:35:23 +02:00
parent d383315b34
commit 980ab967ba
9 changed files with 195 additions and 6 deletions

View File

@ -1,7 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime
import importlib import importlib
import logging import logging
import pytz
from .common import (URL, HTML, FORM, RECEIVED, ACCEPTED, PAID, DENIED, from .common import (URL, HTML, FORM, RECEIVED, ACCEPTED, PAID, DENIED,
CANCELED, CANCELLED, ERROR, WAITING, ResponseError, force_text) CANCELED, CANCELLED, ERROR, WAITING, ResponseError, force_text)
@ -102,7 +105,34 @@ class Payment(object):
- the third is the URL or the HTML form to contact the payment - the third is the URL or the HTML form to contact the payment
server, which must be sent to the customer browser. server, which must be sent to the customer browser.
''' '''
logger.debug(u'%r' % kwargs) logger.debug(u'%r' % kwargs)
if 'capture_date' in kwargs:
capture_date = kwargs.pop('capture_date')
delay_param = False
for parameter in self.backend.description['parameters']:
if parameter['name'] == 'capture_day':
delay_param = True
break
if not delay_param:
raise ValueError('capture_date is not supported by the backend.')
if not isinstance(capture_date, datetime.date):
raise ValueError('capture_date should be a datetime.date object.')
# backend timezone should come from some backend configuration
backend_tz = pytz.timezone('Europe/Paris')
utc_tz = pytz.timezone('Etc/UTC')
backend_trans_date = utc_tz.localize(
datetime.datetime.utcnow()).astimezone(backend_tz)
capture_day = (capture_date - backend_trans_date.date()).days
if capture_day <= 0:
raise ValueError("capture_date needs to be superior to the transaction date.")
kwargs['capture_day'] = capture_day
for param in kwargs: for param in kwargs:
# encode all input params to unicode # encode all input params to unicode
kwargs[param] = force_text(kwargs[param]) kwargs[param] = force_text(kwargs[param])

View File

@ -279,8 +279,9 @@ class Payment(PaymentCommon):
warnings.warn("callback option is deprecated, " warnings.warn("callback option is deprecated, "
"use automatic_return_url", DeprecationWarning) "use automatic_return_url", DeprecationWarning)
automatic_return_url = self.callback automatic_return_url = self.callback
if self.capture_day: capture_day = capture_day = kwargs.get('capture_day', self.capture_day)
d['PBX_DIFF'] = self.capture_day.zfill(2) if capture_day:
d['PBX_DIFF'] = capture_day.zfill(2)
d['PBX_AUTOSEULE'] = PAYMENT_MODES[self.capture_mode] d['PBX_AUTOSEULE'] = PAYMENT_MODES[self.capture_mode]
if automatic_return_url: if automatic_return_url:
d['PBX_REPONDRE_A'] = force_text(automatic_return_url) d['PBX_REPONDRE_A'] = force_text(automatic_return_url)

View File

@ -181,8 +181,8 @@ class Payment(PaymentCommon):
data['amount'] = force_text(int(Decimal(amount) * 100)) data['amount'] = force_text(int(Decimal(amount) * 100))
if email: if email:
data['billingContact.email'] = email data['billingContact.email'] = email
if 'captureDay' in kwargs: if 'capture_day' in kwargs:
data['captureDay'] == kwargs.get('captureDay') data['captureDay'] = kwargs.get('capture_day')
normal_return_url = self.normal_return_url normal_return_url = self.normal_return_url
if next_url and not normal_return_url: if next_url and not normal_return_url:
warnings.warn("passing next_url to request() is deprecated, " warnings.warn("passing next_url to request() is deprecated, "

View File

@ -82,6 +82,9 @@ PARAMETERS = [
choices=('SILENT', 'INTERACTIVE')), choices=('SILENT', 'INTERACTIVE')),
Parameter('vads_amount', 'n', 9, max_length=12, needed=True), Parameter('vads_amount', 'n', 9, max_length=12, needed=True),
Parameter('vads_capture_delay', 'n', 6, max_length=3, default=''), Parameter('vads_capture_delay', 'n', 6, max_length=3, default=''),
# Same as 'vads_capture_delay' but matches other backend naming for
# deferred payment
Parameter('capture_day', 'n', 6, max_length=3, default=''),
Parameter('vads_contrib', 'ans', 31, max_length=255, default='eopayment'), Parameter('vads_contrib', 'ans', 31, max_length=255, default='eopayment'),
# defaut currency = EURO, norme ISO4217 # defaut currency = EURO, norme ISO4217
Parameter('vads_currency', 'n', 10, length=3, default='978', needed=True), Parameter('vads_currency', 'n', 10, length=3, default='978', needed=True),
@ -248,7 +251,7 @@ class Payment(PaymentCommon):
for name in ('vads_ctx_mode', VADS_SITE_ID, 'vads_order_info', for name in ('vads_ctx_mode', VADS_SITE_ID, 'vads_order_info',
'vads_order_info2', 'vads_order_info3', 'vads_order_info2', 'vads_order_info3',
'vads_payment_cards', 'vads_payment_config'): 'vads_payment_cards', 'vads_payment_config', 'capture_day'):
parameter = PARAMETER_MAP[name] parameter = PARAMETER_MAP[name]
x = {'name': name, x = {'name': name,
'caption': parameter.description or name, 'caption': parameter.description or name,
@ -332,6 +335,9 @@ class Payment(PaymentCommon):
fields[name] = parameter.default() fields[name] = parameter.default()
else: else:
fields[name] = parameter.default fields[name] = parameter.default
capture_day = fields.pop('capture_day')
if capture_day:
fields['vads_capture_delay'] = capture_day
check_vads(fields) check_vads(fields)
fields[SIGNATURE] = force_text(self.signature(fields)) fields[SIGNATURE] = force_text(self.signature(fields))
self.logger.debug('%s request contains fields: %s', __name__, fields) self.logger.debug('%s request contains fields: %s', __name__, fields)

View File

@ -0,0 +1,66 @@
from datetime import date, datetime, timedelta
import mock
import pytest
def do_mock_backend(monkeypatch):
class MockBackend(object):
request = mock.Mock()
description = {
'parameters': [
{
'name': 'capture_day',
}
]
}
def get_backend(*args, **kwargs):
def backend(*args, **kwargs):
return MockBackend
return backend
import eopayment
monkeypatch.setattr(eopayment, 'get_backend', get_backend)
return MockBackend, eopayment.Payment('kind', None)
def test_deferred_payment(monkeypatch):
mock_backend, payment = do_mock_backend(monkeypatch)
capture_date = (datetime.now().date() + timedelta(days=3))
payment.request(amount=12.2, capture_date=capture_date)
mock_backend.request.assert_called_with(12.2, **{'capture_day': u'3'})
# capture date can't be inferior to the transaction date
capture_date = (datetime.now().date() - timedelta(days=3))
with pytest.raises(
ValueError, match='capture_date needs to be superior to the transaction date.'):
payment.request(amount=12.2, capture_date=capture_date)
# capture date should be a date object
capture_date = 'not a date'
with pytest.raises(ValueError, match='capture_date should be a datetime.date object.'):
payment.request(amount=12.2, capture_date=capture_date)
# using capture date on a backend that does not support it raise an error
capture_date = (datetime.now().date() + timedelta(days=3))
mock_backend.description['parameters'] = []
with pytest.raises(ValueError, match='capture_date is not supported by the backend.'):
payment.request(amount=12.2, capture_date=capture_date)
def test_paris_timezone(freezer, monkeypatch):
freezer.move_to('2018-10-02 23:50:00')
_, payment = do_mock_backend(monkeypatch)
capture_date = date(year=2018, month=10, day=3)
with pytest.raises(
ValueError, match='capture_date needs to be superior to the transaction date'):
# utcnow will return 2018-10-02 23:50:00,
# converted to Europe/Paris it is already 2018-10-03
# so 2018-10-03 for capture_date is invalid
payment.request(amount=12.2, capture_date=capture_date)

View File

@ -105,6 +105,37 @@ class PayboxTests(TestCase):
self.assertIn('PBX_DIFF', form_params) self.assertIn('PBX_DIFF', form_params)
self.assertEqual(form_params['PBX_DIFF'], '07') self.assertEqual(form_params['PBX_DIFF'], '07')
# capture_day can be used as a request argument
params = BACKEND_PARAMS.copy()
backend = eopayment.Payment('paybox', params)
transaction_id, kind, what = backend.request(
Decimal(amount), email=email, orderid=order_id,
transaction_id=transaction, time=time, capture_day=2)
root = ET.fromstring(str(what))
form_params = dict(((
node.attrib['name'], node.attrib['value']) for node in root
if node.attrib['type'] == 'hidden'))
self.assertIn('PBX_DIFF', form_params)
self.assertEqual(form_params['PBX_DIFF'], '02')
# capture_day passed as a request argument
# overrides capture_day from backend params
params = BACKEND_PARAMS.copy()
params['capture_day'] = '7'
backend = eopayment.Payment('paybox', params)
transaction_id, kind, what = backend.request(
Decimal(amount), email=email, orderid=order_id,
transaction_id=transaction, time=time, capture_day=2)
root = ET.fromstring(str(what))
form_params = dict(((
node.attrib['name'], node.attrib['value']) for node in root
if node.attrib['type'] == 'hidden'))
self.assertIn('PBX_DIFF', form_params)
self.assertEqual(form_params['PBX_DIFF'], '02')
def test_request_with_authorization_only(self): def test_request_with_authorization_only(self):
params = BACKEND_PARAMS.copy() params = BACKEND_PARAMS.copy()
time = '2018-08-21T10:26:32+02:00' time = '2018-08-21T10:26:32+02:00'

View File

@ -19,6 +19,11 @@ def test_build_request():
data = [f for f in form.fields if f['name'] == 'Data'] data = [f for f in form.fields if f['name'] == 'Data']
assert 'statementReference=foobar' in data[0]['value'] assert 'statementReference=foobar' in data[0]['value']
transaction, f, form = backend.request(amount=u'12', info1='foobar', capture_day=u'1')
data = [f for f in form.fields if f['name'] == 'Data']
assert 'captureDay=1' in data[0]['value']
def test_options(): def test_options():
payment = eopayment.Payment('sips2', {'capture_mode': u'VALIDATION'}) payment = eopayment.Payment('sips2', {'capture_mode': u'VALIDATION'})
assert payment.backend.get_data()['captureMode'] == 'VALIDATION' assert payment.backend.get_data()['captureMode'] == 'VALIDATION'

View File

@ -1,8 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from datetime import datetime, timedelta
import pytest import pytest
from six.moves.urllib import parse as urlparse from six.moves.urllib import parse as urlparse
import eopayment
from eopayment.systempayv2 import Payment, VADS_CUST_FIRST_NAME, \ from eopayment.systempayv2 import Payment, VADS_CUST_FIRST_NAME, \
VADS_CUST_LAST_NAME, PAID VADS_CUST_LAST_NAME, PAID
from eopayment import ResponseError from eopayment import ResponseError
@ -14,6 +16,13 @@ PARAMS = {
'vads_trans_date': u'20090501193530', 'vads_trans_date': u'20090501193530',
} }
def get_field(form, field_name):
for field in form.fields:
if field['name'] == field_name:
return field
def test_systempayv2(): def test_systempayv2():
p = Payment(PARAMS) p = Payment(PARAMS)
data = {'amount': 15.24, 'orderid': '654321', data = {'amount': 15.24, 'orderid': '654321',
@ -49,3 +58,43 @@ def test_systempayv2():
# bad response # bad response
with pytest.raises(ResponseError, match='missing signature, vads_ctx_mode or vads_auth_result'): with pytest.raises(ResponseError, match='missing signature, vads_ctx_mode or vads_auth_result'):
p.response('foo=bar') p.response('foo=bar')
def test_systempayv2_deferred_payment():
default_params = {
'secret_test': u'1122334455667788',
'vads_site_id': u'12345678',
'vads_ctx_mode': u'TEST',
}
default_data = {
'amount': 15.24, 'orderid': '654321', 'first_name': u'John',
'last_name': u'Doe'
}
# default vads_capture_delay used
params = default_params.copy()
params['vads_capture_delay'] = 1
backend = eopayment.Payment('systempayv2', params)
data = default_data.copy()
transaction_id, f, form = backend.request(**data)
assert get_field(form, 'vads_capture_delay')['value'] == '1'
# vads_capture_delay can used in request and
# override default vads_capture_delay
params = default_params.copy()
params['vads_capture_delay'] = 1
p = eopayment.Payment('systempayv2', params)
data = default_data.copy()
data['vads_capture_delay'] = '3'
transaction_id, f, form = p.request(**data)
assert get_field(form, 'vads_capture_delay')['value'] == '3'
# capture_date can be used for deferred_payment
params = default_params.copy()
params['vads_capture_delay'] = 1
p = eopayment.Payment('systempayv2', params)
data = default_data.copy()
data['capture_date'] = (datetime.now().date() + timedelta(days=4))
transaction_id, f, form = p.request(**data)
assert get_field(form, 'vads_capture_delay')['value'] == '4'

View File

@ -16,5 +16,6 @@ commands =
usedevelop = True usedevelop = True
deps = coverage deps = coverage
pytest pytest
pytest-freezegun
py2: pytest-cov py2: pytest-cov
mock mock