diff --git a/eopayment/__init__.py b/eopayment/__init__.py index fbd0e55..f2f1239 100644 --- a/eopayment/__init__.py +++ b/eopayment/__init__.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- +import datetime import importlib import logging +import pytz + from .common import (URL, HTML, FORM, RECEIVED, ACCEPTED, PAID, DENIED, 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 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: # encode all input params to unicode kwargs[param] = force_text(kwargs[param]) diff --git a/eopayment/paybox.py b/eopayment/paybox.py index ab77f83..4c34731 100644 --- a/eopayment/paybox.py +++ b/eopayment/paybox.py @@ -279,8 +279,9 @@ class Payment(PaymentCommon): warnings.warn("callback option is deprecated, " "use automatic_return_url", DeprecationWarning) automatic_return_url = self.callback - if self.capture_day: - d['PBX_DIFF'] = self.capture_day.zfill(2) + capture_day = capture_day = kwargs.get('capture_day', self.capture_day) + if capture_day: + d['PBX_DIFF'] = capture_day.zfill(2) d['PBX_AUTOSEULE'] = PAYMENT_MODES[self.capture_mode] if automatic_return_url: d['PBX_REPONDRE_A'] = force_text(automatic_return_url) diff --git a/eopayment/sips2.py b/eopayment/sips2.py index fe4de5a..440546c 100644 --- a/eopayment/sips2.py +++ b/eopayment/sips2.py @@ -181,8 +181,8 @@ class Payment(PaymentCommon): data['amount'] = force_text(int(Decimal(amount) * 100)) if email: data['billingContact.email'] = email - if 'captureDay' in kwargs: - data['captureDay'] == kwargs.get('captureDay') + if 'capture_day' in kwargs: + data['captureDay'] = kwargs.get('capture_day') normal_return_url = self.normal_return_url if next_url and not normal_return_url: warnings.warn("passing next_url to request() is deprecated, " diff --git a/eopayment/systempayv2.py b/eopayment/systempayv2.py index 201cb2c..31731fd 100644 --- a/eopayment/systempayv2.py +++ b/eopayment/systempayv2.py @@ -82,6 +82,9 @@ PARAMETERS = [ choices=('SILENT', 'INTERACTIVE')), Parameter('vads_amount', 'n', 9, max_length=12, needed=True), 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'), # defaut currency = EURO, norme ISO4217 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', 'vads_order_info2', 'vads_order_info3', - 'vads_payment_cards', 'vads_payment_config'): + 'vads_payment_cards', 'vads_payment_config', 'capture_day'): parameter = PARAMETER_MAP[name] x = {'name': name, 'caption': parameter.description or name, @@ -332,6 +335,9 @@ class Payment(PaymentCommon): fields[name] = parameter.default() else: fields[name] = parameter.default + capture_day = fields.pop('capture_day') + if capture_day: + fields['vads_capture_delay'] = capture_day check_vads(fields) fields[SIGNATURE] = force_text(self.signature(fields)) self.logger.debug('%s request contains fields: %s', __name__, fields) diff --git a/tests/test_base_payment.py b/tests/test_base_payment.py new file mode 100644 index 0000000..7ea551e --- /dev/null +++ b/tests/test_base_payment.py @@ -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) diff --git a/tests/test_paybox.py b/tests/test_paybox.py index 7045026..9f38090 100644 --- a/tests/test_paybox.py +++ b/tests/test_paybox.py @@ -105,6 +105,37 @@ class PayboxTests(TestCase): self.assertIn('PBX_DIFF', form_params) 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): params = BACKEND_PARAMS.copy() time = '2018-08-21T10:26:32+02:00' diff --git a/tests/test_sips2.py b/tests/test_sips2.py index 65e63f1..563c75b 100644 --- a/tests/test_sips2.py +++ b/tests/test_sips2.py @@ -19,6 +19,11 @@ def test_build_request(): data = [f for f in form.fields if f['name'] == 'Data'] 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(): payment = eopayment.Payment('sips2', {'capture_mode': u'VALIDATION'}) assert payment.backend.get_data()['captureMode'] == 'VALIDATION' diff --git a/tests/test_systempayv2.py b/tests/test_systempayv2.py index bf00b6d..1df7282 100644 --- a/tests/test_systempayv2.py +++ b/tests/test_systempayv2.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- +from datetime import datetime, timedelta import pytest from six.moves.urllib import parse as urlparse +import eopayment from eopayment.systempayv2 import Payment, VADS_CUST_FIRST_NAME, \ VADS_CUST_LAST_NAME, PAID from eopayment import ResponseError @@ -14,6 +16,13 @@ PARAMS = { '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(): p = Payment(PARAMS) data = {'amount': 15.24, 'orderid': '654321', @@ -49,3 +58,43 @@ def test_systempayv2(): # bad response with pytest.raises(ResponseError, match='missing signature, vads_ctx_mode or vads_auth_result'): 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' diff --git a/tox.ini b/tox.ini index 676a49b..622788c 100644 --- a/tox.ini +++ b/tox.ini @@ -16,5 +16,6 @@ commands = usedevelop = True deps = coverage pytest + pytest-freezegun py2: pytest-cov mock