2011-04-22 17:21:16 +02:00
|
|
|
|
# -*- coding: utf-8 -*-
|
2020-04-10 11:10:15 +02:00
|
|
|
|
# eopayment - online payment library
|
|
|
|
|
# Copyright (C) 2011-2020 Entr'ouvert
|
|
|
|
|
#
|
|
|
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
|
|
|
# under the terms of the GNU Affero General Public License as published
|
|
|
|
|
# by the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
# (at your option) any later version.
|
|
|
|
|
#
|
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
|
#
|
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
2011-04-22 17:21:16 +02:00
|
|
|
|
|
2020-09-15 14:54:38 +02:00
|
|
|
|
import base64
|
2020-04-03 17:47:04 +02:00
|
|
|
|
import pytz
|
2011-04-22 17:21:16 +02:00
|
|
|
|
import datetime as dt
|
|
|
|
|
import hashlib
|
2020-09-15 14:54:38 +02:00
|
|
|
|
import hmac
|
2020-10-10 12:36:57 +02:00
|
|
|
|
import random
|
|
|
|
|
import re
|
2011-04-22 17:21:16 +02:00
|
|
|
|
import string
|
2020-10-10 12:36:57 +02:00
|
|
|
|
import six
|
2018-03-26 09:56:16 +02:00
|
|
|
|
from six.moves.urllib import parse as urlparse
|
2016-02-16 10:51:41 +01:00
|
|
|
|
import warnings
|
2012-05-25 15:42:04 +02:00
|
|
|
|
from gettext import gettext as _
|
2011-04-22 17:21:16 +02:00
|
|
|
|
|
2020-10-11 09:43:07 +02:00
|
|
|
|
from .common import (PaymentCommon, PaymentResponse, PAID, DENIED, CANCELLED,
|
|
|
|
|
ERROR, FORM, Form, ResponseError, force_text, force_byte)
|
|
|
|
|
from .cb import translate_cb_error_code
|
2011-04-22 17:21:16 +02:00
|
|
|
|
|
|
|
|
|
__all__ = ['Payment']
|
|
|
|
|
|
2011-04-26 12:41:19 +02:00
|
|
|
|
VADS_TRANS_DATE = 'vads_trans_date'
|
2012-01-30 17:47:47 +01:00
|
|
|
|
VADS_AUTH_NUMBER = 'vads_auth_number'
|
2012-02-17 17:49:51 +01:00
|
|
|
|
VADS_AUTH_RESULT = 'vads_auth_result'
|
|
|
|
|
VADS_RESULT = 'vads_result'
|
|
|
|
|
VADS_EXTRA_RESULT = 'vads_extra_result'
|
2012-05-25 15:42:04 +02:00
|
|
|
|
VADS_CUST_EMAIL = 'vads_cust_email'
|
2012-09-24 15:43:34 +02:00
|
|
|
|
VADS_CUST_NAME = 'vads_cust_name'
|
|
|
|
|
VADS_CUST_PHONE = 'vads_cust_phone'
|
|
|
|
|
VADS_CUST_INFO1 = 'vads_order_info'
|
|
|
|
|
VADS_CUST_INFO2 = 'vads_order_info2'
|
|
|
|
|
VADS_CUST_INFO3 = 'vads_order_info3'
|
2016-07-05 13:22:11 +02:00
|
|
|
|
VADS_CUST_FIRST_NAME = 'vads_cust_first_name'
|
|
|
|
|
VADS_CUST_LAST_NAME = 'vads_cust_last_name'
|
2012-05-25 15:42:04 +02:00
|
|
|
|
VADS_URL_RETURN = 'vads_url_return'
|
|
|
|
|
VADS_AMOUNT = 'vads_amount'
|
|
|
|
|
VADS_SITE_ID = 'vads_site_id'
|
|
|
|
|
VADS_TRANS_ID = 'vads_trans_id'
|
2012-02-17 17:49:51 +01:00
|
|
|
|
SIGNATURE = 'signature'
|
2014-03-30 19:27:48 +02:00
|
|
|
|
VADS_CTX_MODE = 'vads_ctx_mode'
|
2020-04-03 17:47:04 +02:00
|
|
|
|
VADS_EFFECTIVE_CREATION_DATE = 'vads_effective_creation_date'
|
2020-10-27 15:51:29 +01:00
|
|
|
|
VADS_EOPAYMENT_TRANS_ID = 'vads_ext_info_eopayment_trans_id'
|
2011-04-22 17:21:16 +02:00
|
|
|
|
|
2012-07-10 18:04:22 +02:00
|
|
|
|
|
2011-04-22 17:21:16 +02:00
|
|
|
|
def isonow():
|
2015-07-22 00:28:39 +02:00
|
|
|
|
return dt.datetime.utcnow().isoformat('T').replace('-', '') \
|
|
|
|
|
.replace('T', '').replace(':', '')[:14]
|
2012-07-10 18:04:22 +02:00
|
|
|
|
|
2011-04-22 17:21:16 +02:00
|
|
|
|
|
2020-04-03 17:47:04 +02:00
|
|
|
|
def parse_utc(value):
|
|
|
|
|
try:
|
|
|
|
|
naive_dt = dt.datetime.strptime(value, '%Y%m%d%H%M%S')
|
|
|
|
|
except ValueError:
|
|
|
|
|
return None
|
|
|
|
|
return pytz.utc.localize(naive_dt)
|
|
|
|
|
|
|
|
|
|
|
2011-04-22 17:21:16 +02:00
|
|
|
|
class Parameter:
|
2011-05-27 15:55:26 +02:00
|
|
|
|
def __init__(self, name, ptype, code, max_length=None, length=None,
|
2015-07-22 00:28:39 +02:00
|
|
|
|
needed=False, default=None, choices=None, description=None,
|
|
|
|
|
help_text=None):
|
2011-04-22 17:21:16 +02:00
|
|
|
|
self.name = name
|
|
|
|
|
self.ptype = ptype
|
|
|
|
|
self.code = code
|
|
|
|
|
self.max_length = max_length
|
|
|
|
|
self.length = length
|
|
|
|
|
self.needed = needed
|
|
|
|
|
self.default = default
|
|
|
|
|
self.choices = choices
|
2012-05-25 15:42:04 +02:00
|
|
|
|
self.description = description
|
|
|
|
|
self.help_text = help_text
|
2011-04-22 17:21:16 +02:00
|
|
|
|
|
|
|
|
|
def check_value(self, value):
|
2016-10-12 10:59:58 +02:00
|
|
|
|
if self.length and len(value) != self.length:
|
2011-04-22 17:21:16 +02:00
|
|
|
|
return False
|
2016-10-12 10:59:58 +02:00
|
|
|
|
if self.max_length and len(value) > self.max_length:
|
2011-04-22 17:21:16 +02:00
|
|
|
|
return False
|
2016-10-12 10:59:58 +02:00
|
|
|
|
if self.choices and value not in self.choices:
|
2011-04-22 17:21:16 +02:00
|
|
|
|
return False
|
|
|
|
|
if value == '':
|
|
|
|
|
return True
|
2016-10-12 10:59:58 +02:00
|
|
|
|
value = value.replace('.', '')
|
2011-04-22 17:21:16 +02:00
|
|
|
|
if self.ptype == 'n':
|
|
|
|
|
return value.isdigit()
|
|
|
|
|
elif self.ptype == 'an':
|
|
|
|
|
return value.isalnum()
|
|
|
|
|
elif self.ptype == 'an-':
|
2012-07-10 18:04:22 +02:00
|
|
|
|
return value.replace('-', '').isalnum()
|
2011-04-22 17:21:16 +02:00
|
|
|
|
elif self.ptype == 'an;':
|
2012-07-10 18:04:22 +02:00
|
|
|
|
return value.replace(';', '').isalnum()
|
2011-04-22 17:21:16 +02:00
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PARAMETERS = [
|
2015-07-22 00:28:39 +02:00
|
|
|
|
# amount as euro cents
|
|
|
|
|
Parameter('vads_action_mode', None, 47, needed=True, default='INTERACTIVE',
|
|
|
|
|
choices=('SILENT', 'INTERACTIVE')),
|
|
|
|
|
Parameter('vads_amount', 'n', 9, max_length=12, needed=True),
|
|
|
|
|
Parameter('vads_capture_delay', 'n', 6, max_length=3, default=''),
|
2018-10-12 15:35:23 +02:00
|
|
|
|
# Same as 'vads_capture_delay' but matches other backend naming for
|
|
|
|
|
# deferred payment
|
|
|
|
|
Parameter('capture_day', 'n', 6, max_length=3, default=''),
|
2015-07-22 00:28:39 +02:00
|
|
|
|
Parameter('vads_contrib', 'ans', 31, max_length=255, default='eopayment'),
|
|
|
|
|
# defaut currency = EURO, norme ISO4217
|
2016-06-16 09:00:08 +02:00
|
|
|
|
Parameter('vads_currency', 'n', 10, length=3, default='978', needed=True),
|
2015-07-22 00:28:39 +02:00
|
|
|
|
Parameter('vads_cust_address', 'an', 19, max_length=255),
|
|
|
|
|
# code ISO 3166
|
|
|
|
|
Parameter('vads_cust_country', 'a', 22, length=2, default='FR'),
|
|
|
|
|
Parameter('vads_cust_email', 'an@', 15, max_length=127),
|
|
|
|
|
Parameter('vads_cust_id', 'an', 16, max_length=63),
|
|
|
|
|
Parameter('vads_cust_name', 'ans', 18, max_length=127),
|
|
|
|
|
Parameter('vads_cust_phone', 'an', 23, max_length=63),
|
|
|
|
|
Parameter('vads_cust_title', 'an', 17, max_length=63),
|
|
|
|
|
Parameter('vads_cust_city', 'an', 21, max_length=63),
|
|
|
|
|
Parameter('vads_cust_zip', 'an', 20, max_length=63),
|
|
|
|
|
Parameter('vads_ctx_mode', 'a', 11, needed=True, choices=('TEST',
|
|
|
|
|
'PRODUCTION'),
|
|
|
|
|
default='TEST'),
|
|
|
|
|
# ISO 639 code
|
|
|
|
|
Parameter('vads_language', 'a', 12, length=2, default='fr'),
|
|
|
|
|
Parameter('vads_order_id', 'an-', 13, max_length=32),
|
|
|
|
|
Parameter('vads_order_info', 'an', 14, max_length=255,
|
|
|
|
|
description=_(u"Complément d'information 1")),
|
|
|
|
|
Parameter('vads_order_info2', 'an', 14, max_length=255,
|
|
|
|
|
description=_(u"Complément d'information 2")),
|
|
|
|
|
Parameter('vads_order_info3', 'an', 14, max_length=255,
|
|
|
|
|
description=_(u"Complément d'information 3")),
|
|
|
|
|
Parameter('vads_page_action', None, 46, needed=True, default='PAYMENT',
|
|
|
|
|
choices=('PAYMENT',)),
|
|
|
|
|
Parameter('vads_payment_cards', 'an;', 8, max_length=127, default='',
|
|
|
|
|
description=_(u'Liste des cartes de paiement acceptées'),
|
|
|
|
|
help_text=_(u'vide ou des valeurs sépareés par un point-virgule '
|
|
|
|
|
'parmi AMEX, AURORE-MULTI, BUYSTER, CB, COFINOGA, '
|
|
|
|
|
'E-CARTEBLEUE, MASTERCARD, JCB, MAESTRO, ONEY, '
|
|
|
|
|
'ONEY_SANDBOX, PAYPAL, PAYPAL_SB, PAYSAFECARD, '
|
|
|
|
|
'VISA')),
|
|
|
|
|
# must be SINGLE or MULTI with parameters
|
2018-03-26 09:56:16 +02:00
|
|
|
|
Parameter('vads_payment_config', '', 7, default='SINGLE',
|
2015-07-22 00:28:39 +02:00
|
|
|
|
choices=('SINGLE', 'MULTI'), needed=True),
|
|
|
|
|
Parameter('vads_return_mode', None, 48, default='GET',
|
|
|
|
|
choices=('', 'NONE', 'POST', 'GET')),
|
|
|
|
|
Parameter('signature', 'an', None, length=40),
|
2018-03-26 09:56:16 +02:00
|
|
|
|
Parameter('vads_site_id', 'n', 2, length=8, needed=True,
|
2015-07-22 00:28:39 +02:00
|
|
|
|
description=_(u'Identifiant de la boutique')),
|
|
|
|
|
Parameter('vads_theme_config', 'ans', 32, max_length=255),
|
2018-03-26 09:56:16 +02:00
|
|
|
|
Parameter(VADS_TRANS_DATE, 'n', 4, length=14, needed=True,
|
2015-07-22 00:28:39 +02:00
|
|
|
|
default=isonow),
|
2020-10-13 00:29:22 +02:00
|
|
|
|
# https://paiement.systempay.fr/doc/fr-FR/form-payment/reference/vads-trans-id.html
|
|
|
|
|
Parameter('vads_trans_id', 'an', 3, length=6, needed=True),
|
2018-10-02 12:25:04 +02:00
|
|
|
|
Parameter('vads_validation_mode', 'n', 5, max_length=1, choices=('', '0', '1'),
|
2015-07-22 00:28:39 +02:00
|
|
|
|
default=''),
|
2018-03-26 09:56:16 +02:00
|
|
|
|
Parameter('vads_version', 'an', 1, default='V2', needed=True,
|
2015-07-22 00:28:39 +02:00
|
|
|
|
choices=('V2',)),
|
2020-01-16 14:30:40 +01:00
|
|
|
|
Parameter('vads_url_success', 'ans', 24, max_length=1024),
|
2015-07-22 00:28:39 +02:00
|
|
|
|
Parameter('vads_url_referral', 'ans', 26, max_length=127),
|
2020-01-16 14:30:40 +01:00
|
|
|
|
Parameter('vads_url_refused', 'ans', 25, max_length=1024),
|
|
|
|
|
Parameter('vads_url_cancel', 'ans', 27, max_length=1024),
|
|
|
|
|
Parameter('vads_url_error', 'ans', 29, max_length=1024),
|
|
|
|
|
Parameter('vads_url_return', 'ans', 28, max_length=1024),
|
2015-07-22 00:28:39 +02:00
|
|
|
|
Parameter('vads_user_info', 'ans', 61, max_length=255),
|
|
|
|
|
Parameter('vads_contracts', 'ans', 62, max_length=255),
|
2016-11-03 10:44:12 +01:00
|
|
|
|
Parameter(VADS_CUST_FIRST_NAME, 'ans', 104, max_length=63),
|
|
|
|
|
Parameter(VADS_CUST_LAST_NAME, 'ans', 104, max_length=63),
|
2011-04-22 17:21:16 +02:00
|
|
|
|
]
|
2012-07-10 18:04:22 +02:00
|
|
|
|
PARAMETER_MAP = dict(((parameter.name,
|
|
|
|
|
parameter) for parameter in PARAMETERS))
|
2011-04-22 17:21:16 +02:00
|
|
|
|
|
2020-10-27 15:51:29 +01:00
|
|
|
|
|
2011-04-22 17:21:16 +02:00
|
|
|
|
def add_vads(kwargs):
|
2012-07-10 18:04:22 +02:00
|
|
|
|
new_vargs = {}
|
2018-03-26 09:56:16 +02:00
|
|
|
|
for k, v in kwargs.items():
|
2011-04-22 17:21:16 +02:00
|
|
|
|
if k.startswith('vads_'):
|
|
|
|
|
new_vargs[k] = v
|
|
|
|
|
else:
|
2012-07-10 18:04:22 +02:00
|
|
|
|
new_vargs['vads_' + k] = v
|
2011-04-22 17:21:16 +02:00
|
|
|
|
return new_vargs
|
|
|
|
|
|
2012-07-10 18:04:22 +02:00
|
|
|
|
|
2012-05-25 15:42:04 +02:00
|
|
|
|
def check_vads(kwargs, exclude=[]):
|
|
|
|
|
for parameter in PARAMETERS:
|
|
|
|
|
name = parameter.name
|
|
|
|
|
if name not in kwargs and name not in exclude and parameter.needed:
|
|
|
|
|
raise ValueError('parameter %s must be defined' % name)
|
|
|
|
|
if name in kwargs and not parameter.check_value(kwargs[name]):
|
|
|
|
|
raise ValueError('parameter %s value %s is not of the type %s' % (
|
|
|
|
|
name, kwargs[name],
|
|
|
|
|
parameter.ptype))
|
|
|
|
|
|
2012-07-10 18:04:22 +02:00
|
|
|
|
|
2011-04-22 17:21:16 +02:00
|
|
|
|
class Payment(PaymentCommon):
|
2012-07-10 18:04:22 +02:00
|
|
|
|
'''
|
2012-05-25 15:42:04 +02:00
|
|
|
|
Produce request for and verify response from the SystemPay payment
|
|
|
|
|
gateway.
|
|
|
|
|
|
2012-07-10 18:04:22 +02:00
|
|
|
|
>>> gw =Payment(dict(secret_test='xxx', secret_production='yyyy',
|
|
|
|
|
site_id=123, ctx_mode='PRODUCTION'))
|
2012-05-25 15:42:04 +02:00
|
|
|
|
>>> print gw.request(100)
|
|
|
|
|
('20120525093304_188620',
|
|
|
|
|
'https://paiement.systempay.fr/vads-payment/?vads_url_return=http%3A%2F%2Furl.de.retour%2Fretour.php&vads_cust_country=FR&vads_site_id=&vads_payment_config=SINGLE&vads_trans_id=188620&vads_action_mode=INTERACTIVE&vads_contrib=eopayment&vads_page_action=PAYMENT&vads_trans_date=20120525093304&vads_ctx_mode=TEST&vads_validation_mode=&vads_version=V2&vads_payment_cards=&signature=5d412498ab523627ec5730a09118f75afa602af5&vads_language=fr&vads_capture_delay=&vads_currency=978&vads_amount=100&vads_return_mode=NONE',
|
|
|
|
|
{'vads_url_return': 'http://url.de.retour/retour.php',
|
|
|
|
|
'vads_cust_country': 'FR', 'vads_site_id': '',
|
|
|
|
|
'vads_payment_config': 'SINGLE', 'vads_trans_id': '188620',
|
|
|
|
|
'vads_action_mode': 'INTERACTIVE', 'vads_contrib': 'eopayment',
|
|
|
|
|
'vads_page_action': 'PAYMENT', 'vads_trans_date': '20120525093304',
|
|
|
|
|
'vads_ctx_mode': 'TEST', 'vads_validation_mode': '',
|
|
|
|
|
'vads_version': 'V2', 'vads_payment_cards': '', 'signature':
|
|
|
|
|
'5d412498ab523627ec5730a09118f75afa602af5', 'vads_language': 'fr',
|
2016-06-16 09:00:08 +02:00
|
|
|
|
'vads_capture_delay': '', 'vads_currency': '978', 'vads_amount': 100,
|
2012-05-25 15:42:04 +02:00
|
|
|
|
'vads_return_mode': 'NONE'})
|
2011-04-22 17:21:16 +02:00
|
|
|
|
|
|
|
|
|
'''
|
2020-10-27 15:51:29 +01:00
|
|
|
|
has_free_transaction_id = True
|
2015-11-03 23:16:40 +01:00
|
|
|
|
service_url = "https://paiement.systempay.fr/vads-payment/"
|
2020-09-28 15:40:18 +02:00
|
|
|
|
signature_algo = 'sha1'
|
2015-11-03 23:16:40 +01:00
|
|
|
|
|
2012-05-25 15:42:04 +02:00
|
|
|
|
description = {
|
2012-07-10 18:04:22 +02:00
|
|
|
|
'caption': 'SystemPay, système de paiment du groupe BPCE',
|
|
|
|
|
'parameters': [
|
2016-02-16 10:51:41 +01:00
|
|
|
|
{
|
|
|
|
|
'name': 'normal_return_url',
|
|
|
|
|
'caption': _('Normal return URL'),
|
|
|
|
|
'default': '',
|
|
|
|
|
'required': True,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
'name': 'automatic_return_url',
|
|
|
|
|
'caption': _('Automatic return URL (ignored, must be set in Payzen/SystemPay backoffice)'),
|
|
|
|
|
'required': False,
|
|
|
|
|
},
|
2012-07-10 18:04:22 +02:00
|
|
|
|
{'name': 'service_url',
|
2015-11-03 23:16:40 +01:00
|
|
|
|
'default': service_url,
|
2012-07-10 18:04:22 +02:00
|
|
|
|
'caption': _(u'URL du service de paiment'),
|
|
|
|
|
'help_text': _(u'ne pas modifier si vous ne savez pas'),
|
|
|
|
|
'validation': lambda x: x.startswith('http'),
|
|
|
|
|
'required': True, },
|
|
|
|
|
{'name': 'secret_test',
|
|
|
|
|
'caption': _(u'Secret pour la configuration de TEST'),
|
2020-09-15 09:57:41 +02:00
|
|
|
|
'validation': lambda value: str.isalnum(value),
|
2012-07-10 18:04:22 +02:00
|
|
|
|
'required': True, },
|
|
|
|
|
{'name': 'secret_production',
|
|
|
|
|
'caption': _(u'Secret pour la configuration de PRODUCTION'),
|
2020-09-15 09:57:41 +02:00
|
|
|
|
'validation': lambda value: str.isalnum(value), },
|
2020-09-15 14:54:38 +02:00
|
|
|
|
{'name': 'signature_algo',
|
|
|
|
|
'caption': _(u'Algorithme de signature'),
|
2020-09-28 15:40:18 +02:00
|
|
|
|
'default': 'sha1',
|
2020-09-15 14:54:38 +02:00
|
|
|
|
'choices': (
|
|
|
|
|
('sha1', 'SHA-1'),
|
|
|
|
|
('hmac_sha256', 'HMAC-SHA-256'),
|
|
|
|
|
)},
|
2019-05-10 17:03:03 +02:00
|
|
|
|
{
|
|
|
|
|
'name': 'manual_validation',
|
|
|
|
|
'caption': 'Validation manuelle',
|
|
|
|
|
'type': bool,
|
|
|
|
|
'default': False,
|
|
|
|
|
'scope': 'transaction'
|
|
|
|
|
}
|
2012-07-10 18:04:22 +02:00
|
|
|
|
]
|
2012-05-25 15:42:04 +02:00
|
|
|
|
}
|
|
|
|
|
|
2012-07-10 18:04:22 +02:00
|
|
|
|
for name in ('vads_ctx_mode', VADS_SITE_ID, 'vads_order_info',
|
|
|
|
|
'vads_order_info2', 'vads_order_info3',
|
2018-10-12 15:35:23 +02:00
|
|
|
|
'vads_payment_cards', 'vads_payment_config', 'capture_day'):
|
2012-05-25 15:42:04 +02:00
|
|
|
|
parameter = PARAMETER_MAP[name]
|
2020-05-19 11:44:14 +02:00
|
|
|
|
|
|
|
|
|
def check_value(parameter):
|
|
|
|
|
def validate(value):
|
|
|
|
|
return parameter.check_value(value)
|
|
|
|
|
return validate
|
|
|
|
|
|
2012-07-10 18:04:22 +02:00
|
|
|
|
x = {'name': name,
|
|
|
|
|
'caption': parameter.description or name,
|
2020-05-19 11:44:14 +02:00
|
|
|
|
'validation': check_value(parameter),
|
2012-07-10 18:04:22 +02:00
|
|
|
|
'default': parameter.default,
|
|
|
|
|
'required': parameter.needed,
|
|
|
|
|
'help_text': parameter.help_text,
|
|
|
|
|
'max_length': parameter.max_length}
|
2012-05-25 15:42:04 +02:00
|
|
|
|
description['parameters'].append(x)
|
|
|
|
|
|
2015-11-03 23:16:40 +01:00
|
|
|
|
def __init__(self, options, logger=None):
|
2016-02-18 16:05:58 +01:00
|
|
|
|
super(Payment, self).__init__(options, logger=logger)
|
2011-04-22 17:21:16 +02:00
|
|
|
|
options = add_vads(options)
|
|
|
|
|
self.options = options
|
|
|
|
|
|
2020-10-10 12:36:57 +02:00
|
|
|
|
def make_vads_trans_id(self):
|
|
|
|
|
# vads_trans_id must be 6 alphanumeric characters,
|
|
|
|
|
# trans_id starting with 9 are reserved for the systempay backoffice
|
|
|
|
|
# https://paiement.systempay.fr/doc/fr-FR/form-payment/reference/vads-trans-id.html
|
|
|
|
|
gen = random.SystemRandom()
|
|
|
|
|
if six.PY3:
|
|
|
|
|
alphabet = string.ascii_letters + string.digits
|
|
|
|
|
else:
|
|
|
|
|
alphabet = string.letters + string.digits
|
|
|
|
|
first_letter_alphabet = alphabet.replace('9', '')
|
|
|
|
|
vads_trans_id = (
|
|
|
|
|
gen.choice(first_letter_alphabet)
|
|
|
|
|
+ ''.join(gen.choice(alphabet) for i in range(5))
|
|
|
|
|
)
|
|
|
|
|
return vads_trans_id
|
2019-05-10 17:03:03 +02:00
|
|
|
|
|
2016-07-05 13:22:11 +02:00
|
|
|
|
def request(self, amount, name=None, first_name=None, last_name=None,
|
|
|
|
|
address=None, email=None, phone=None, orderid=None, info1=None,
|
2020-04-10 11:10:15 +02:00
|
|
|
|
info2=None, info3=None, next_url=None, manual_validation=None,
|
2020-10-27 15:51:29 +01:00
|
|
|
|
transaction_id=None, **kwargs):
|
2011-04-22 17:21:16 +02:00
|
|
|
|
'''
|
2012-06-03 10:30:48 +02:00
|
|
|
|
Create the URL string to send a request to SystemPay
|
2011-04-22 17:21:16 +02:00
|
|
|
|
'''
|
2015-07-22 00:28:39 +02:00
|
|
|
|
self.logger.debug('%s amount %s name %s address %s email %s phone %s '
|
|
|
|
|
'next_url %s info1 %s info2 %s info3 %s kwargs: %s',
|
|
|
|
|
__name__, amount, name, address, email, phone, info1,
|
|
|
|
|
info2, info3, next_url, kwargs)
|
2012-05-29 17:43:51 +02:00
|
|
|
|
# amount unit is cents
|
2012-09-24 15:43:34 +02:00
|
|
|
|
amount = '%.0f' % (100 * amount)
|
2018-03-26 09:56:16 +02:00
|
|
|
|
kwargs.update(add_vads({'amount': force_text(amount)}))
|
|
|
|
|
if int(amount) < 0:
|
2012-05-25 15:42:04 +02:00
|
|
|
|
raise ValueError('amount must be an integer >= 0')
|
2016-02-16 10:51:41 +01:00
|
|
|
|
normal_return_url = self.normal_return_url
|
2011-04-22 17:21:16 +02:00
|
|
|
|
if next_url:
|
2016-02-16 10:51:41 +01:00
|
|
|
|
warnings.warn("passing next_url to request() is deprecated, "
|
|
|
|
|
"set normal_return_url in options", DeprecationWarning)
|
|
|
|
|
normal_return_url = next_url
|
|
|
|
|
if normal_return_url:
|
2018-03-26 09:56:16 +02:00
|
|
|
|
kwargs[VADS_URL_RETURN] = force_text(normal_return_url)
|
2012-09-24 15:43:34 +02:00
|
|
|
|
if name is not None:
|
2018-03-26 09:56:16 +02:00
|
|
|
|
kwargs['vads_cust_name'] = force_text(name)
|
2016-07-05 13:22:11 +02:00
|
|
|
|
if first_name is not None:
|
2018-03-26 09:56:16 +02:00
|
|
|
|
kwargs[VADS_CUST_FIRST_NAME] = force_text(first_name)
|
2016-07-05 13:22:11 +02:00
|
|
|
|
if last_name is not None:
|
2018-03-26 09:56:16 +02:00
|
|
|
|
kwargs[VADS_CUST_LAST_NAME] = force_text(last_name)
|
2016-07-05 13:22:11 +02:00
|
|
|
|
|
2012-06-03 10:30:48 +02:00
|
|
|
|
if address is not None:
|
2018-03-26 09:56:16 +02:00
|
|
|
|
kwargs['vads_cust_address'] = force_text(address)
|
2012-06-03 10:30:48 +02:00
|
|
|
|
if email is not None:
|
2018-03-26 09:56:16 +02:00
|
|
|
|
kwargs['vads_cust_email'] = force_text(email)
|
2012-09-24 15:43:34 +02:00
|
|
|
|
if phone is not None:
|
2018-03-26 09:56:16 +02:00
|
|
|
|
kwargs['vads_cust_phone'] = force_text(phone)
|
2012-09-24 15:43:34 +02:00
|
|
|
|
if info1 is not None:
|
2018-03-26 09:56:16 +02:00
|
|
|
|
kwargs['vads_order_info'] = force_text(info1)
|
2012-09-24 15:43:34 +02:00
|
|
|
|
if info2 is not None:
|
2018-03-26 09:56:16 +02:00
|
|
|
|
kwargs['vads_order_info2'] = force_text(info2)
|
2012-09-24 15:43:34 +02:00
|
|
|
|
if info3 is not None:
|
2018-03-26 09:56:16 +02:00
|
|
|
|
kwargs['vads_order_info3'] = force_text(info3)
|
2016-02-08 18:40:45 +01:00
|
|
|
|
if orderid is not None:
|
|
|
|
|
# check orderid format first
|
|
|
|
|
name = 'vads_order_id'
|
2018-03-26 09:56:16 +02:00
|
|
|
|
orderid = force_text(orderid)
|
2016-02-08 18:40:45 +01:00
|
|
|
|
ptype = 'an-'
|
|
|
|
|
p = Parameter(name, ptype, 13, max_length=32)
|
|
|
|
|
if not p.check_value(orderid):
|
2020-04-10 11:10:15 +02:00
|
|
|
|
raise ValueError(
|
|
|
|
|
'%s value %s is not of the type %s' % (name, orderid, ptype))
|
2016-02-08 18:40:45 +01:00
|
|
|
|
kwargs[name] = orderid
|
2011-04-22 17:21:16 +02:00
|
|
|
|
|
2020-10-10 12:36:57 +02:00
|
|
|
|
vads_trans_id = self.make_vads_trans_id()
|
|
|
|
|
assert re.match(r'^[0-9a-zA-Z]{6}$', vads_trans_id)
|
|
|
|
|
|
|
|
|
|
kwargs[VADS_TRANS_ID] = vads_trans_id
|
2011-04-22 17:21:16 +02:00
|
|
|
|
fields = kwargs
|
|
|
|
|
for parameter in PARAMETERS:
|
|
|
|
|
name = parameter.name
|
|
|
|
|
# import default parameters from configuration
|
|
|
|
|
if name not in fields \
|
|
|
|
|
and name in self.options:
|
2018-03-26 09:56:16 +02:00
|
|
|
|
fields[name] = force_text(self.options[name])
|
2011-04-22 17:21:16 +02:00
|
|
|
|
# import default parameters from module
|
|
|
|
|
if name not in fields and parameter.default is not None:
|
|
|
|
|
if callable(parameter.default):
|
|
|
|
|
fields[name] = parameter.default()
|
|
|
|
|
else:
|
|
|
|
|
fields[name] = parameter.default
|
2018-10-12 15:35:23 +02:00
|
|
|
|
capture_day = fields.pop('capture_day')
|
|
|
|
|
if capture_day:
|
|
|
|
|
fields['vads_capture_delay'] = capture_day
|
2019-05-10 17:03:03 +02:00
|
|
|
|
if manual_validation:
|
|
|
|
|
fields['vads_validation_mode'] = '1'
|
2012-05-25 15:42:04 +02:00
|
|
|
|
check_vads(fields)
|
2020-10-27 15:51:29 +01:00
|
|
|
|
if transaction_id:
|
|
|
|
|
fields[VADS_EOPAYMENT_TRANS_ID] = transaction_id
|
|
|
|
|
else:
|
|
|
|
|
transaction_id = '%s_%s' % (fields[VADS_TRANS_DATE], vads_trans_id)
|
2018-03-26 09:56:16 +02:00
|
|
|
|
fields[SIGNATURE] = force_text(self.signature(fields))
|
2012-05-25 15:42:04 +02:00
|
|
|
|
self.logger.debug('%s request contains fields: %s', __name__, fields)
|
|
|
|
|
self.logger.debug('%s transaction id: %s', __name__, transaction_id)
|
2015-07-22 00:39:25 +02:00
|
|
|
|
form = Form(
|
2016-06-16 14:59:35 +02:00
|
|
|
|
url=self.service_url,
|
|
|
|
|
method='POST',
|
|
|
|
|
fields=[
|
|
|
|
|
{
|
|
|
|
|
'type': u'hidden',
|
|
|
|
|
'name': force_text(field_name),
|
|
|
|
|
'value': force_text(field_value),
|
|
|
|
|
}
|
2018-03-26 09:56:16 +02:00
|
|
|
|
for field_name, field_value in fields.items()])
|
2015-07-22 00:39:25 +02:00
|
|
|
|
return transaction_id, FORM, form
|
2011-04-22 17:21:16 +02:00
|
|
|
|
|
2020-10-11 09:43:07 +02:00
|
|
|
|
RESULT_MAP = {
|
|
|
|
|
'00': {'message': 'Paiement réalisé avec succés.', 'result': PAID},
|
|
|
|
|
'02': {'message': 'Le commerçant doit contacter la banque du porteur.'},
|
|
|
|
|
'05': {'message': 'Paiement refusé.', 'result': DENIED},
|
|
|
|
|
'17': {'message': 'Annulation client.', 'result': CANCELLED},
|
|
|
|
|
'30': {'message': 'Erreur de format.'},
|
|
|
|
|
'96': {'message': 'Erreur technique lors du paiement.'},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EXTRA_RESULT_MAP = {
|
|
|
|
|
'': {'message': 'Pas de contrôle effectué.'},
|
|
|
|
|
'00': {'message': 'Tous les contrôles se sont déroulés avec succés.'},
|
|
|
|
|
'02': {'message': 'La carte a dépassé l\'encours autorisé.'},
|
|
|
|
|
'03': {'message': 'La carte appartient à la liste grise du commerçant.'},
|
|
|
|
|
'04': {'messaǵe': 'Le pays d\'émission de la carte appartient à la liste grise du '
|
|
|
|
|
'commerçant ou le pays d\'émission de la carte n\'appartient pas à la '
|
|
|
|
|
'liste blanche du commerçant.'},
|
|
|
|
|
'05': {'message': 'L’adresse IP appartient à la liste grise du marchand.'},
|
|
|
|
|
'06': {'message': 'Le code bin appartient à la liste grise du marchand.'},
|
|
|
|
|
'07': {'message': 'Détection d’une e-carte bleue.'},
|
|
|
|
|
'08': {'message': 'Détection d’une carte commerciale nationale.'},
|
|
|
|
|
'09': {'message': 'Détection d’une carte commerciale étrangère.'},
|
|
|
|
|
'14': {'message': 'Détection d’une carte à autorisation systématique.'},
|
|
|
|
|
'30': {'message': 'Le pays de l’adresse IP appartient à la liste grise.'},
|
|
|
|
|
'99': {'message': 'Problème technique recontré par le serveur lors du traitement '
|
|
|
|
|
'd\'un des contrôles locauxi.'},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def make_eopayment_result(cls, fields):
|
|
|
|
|
# https://paiement.systempay.fr/doc/fr-FR/payment-file/oneclick-payment/vads-result.html
|
|
|
|
|
# https://paiement.systempay.fr/doc/fr-FR/payment-file/oneclick-payment/vads-auth-result.html
|
|
|
|
|
# https://paiement.systempay.fr/doc/fr-FR/payment-file/oneclick-payment/vads-extra-result.html
|
|
|
|
|
vads_result = fields.get(VADS_RESULT)
|
|
|
|
|
vads_auth_result = fields.get(VADS_AUTH_RESULT)
|
|
|
|
|
vads_extra_result = fields.get(VADS_EXTRA_RESULT)
|
|
|
|
|
|
|
|
|
|
# map to human messages and update return
|
|
|
|
|
vads_result_message = cls.RESULT_MAP.get(vads_result, {}).get('message')
|
|
|
|
|
if vads_result_message:
|
|
|
|
|
fields[VADS_RESULT + '_message'] = vads_result_message
|
|
|
|
|
|
|
|
|
|
vads_extra_result_message = cls.EXTRA_RESULT_MAP.get(vads_extra_result, {}).get('message')
|
|
|
|
|
if vads_extra_result_message:
|
|
|
|
|
fields[VADS_EXTRA_RESULT + '_message'] = vads_extra_result_message
|
|
|
|
|
|
|
|
|
|
vads_auth_result_message, auth_eopayment_result = translate_cb_error_code(vads_auth_result)
|
|
|
|
|
if vads_auth_result_message:
|
|
|
|
|
fields[VADS_AUTH_RESULT + '_message'] = vads_auth_result_message
|
|
|
|
|
|
|
|
|
|
# now build eopayment resume
|
|
|
|
|
if vads_result is None:
|
|
|
|
|
return ERROR, 'absence de champ vads_result'
|
|
|
|
|
if vads_result_message is None:
|
|
|
|
|
return ERROR, 'valeur vads_result inconnue'
|
|
|
|
|
|
|
|
|
|
result = cls.RESULT_MAP[vads_result].get('result', ERROR)
|
|
|
|
|
message = vads_result_message
|
|
|
|
|
if vads_auth_result_message and (vads_result != '00' or vads_result != vads_auth_result):
|
|
|
|
|
message += ' ' + vads_auth_result_message
|
|
|
|
|
if vads_result in ('00', '05', '30') and vads_extra_result_message and vads_extra_result != '':
|
|
|
|
|
message += ' ' + vads_extra_result_message
|
|
|
|
|
if result == ERROR and auth_eopayment_result not in (PAID, ERROR, None):
|
|
|
|
|
result = auth_eopayment_result
|
|
|
|
|
return result, message
|
|
|
|
|
|
2015-07-16 14:12:15 +02:00
|
|
|
|
def response(self, query_string, **kwargs):
|
2012-05-29 14:49:26 +02:00
|
|
|
|
fields = urlparse.parse_qs(query_string, True)
|
2016-03-09 22:31:40 +01:00
|
|
|
|
if not set(fields) >= set([SIGNATURE, VADS_CTX_MODE, VADS_AUTH_RESULT]):
|
2018-08-22 23:14:53 +02:00
|
|
|
|
raise ResponseError('missing %s, %s or %s' % (SIGNATURE, VADS_CTX_MODE,
|
|
|
|
|
VADS_AUTH_RESULT))
|
2018-03-26 09:56:16 +02:00
|
|
|
|
for key, value in fields.items():
|
2012-05-29 14:49:26 +02:00
|
|
|
|
fields[key] = value[0]
|
2011-04-22 17:21:16 +02:00
|
|
|
|
copy = fields.copy()
|
2012-05-25 15:42:04 +02:00
|
|
|
|
signature = self.signature(fields)
|
2020-10-11 09:43:07 +02:00
|
|
|
|
result, message = self.make_eopayment_result(copy)
|
|
|
|
|
self.logger.debug('checking systempay response on: %r', copy)
|
2012-02-17 17:49:51 +01:00
|
|
|
|
signature_result = signature == fields[SIGNATURE]
|
|
|
|
|
if not signature_result:
|
2020-10-11 09:43:07 +02:00
|
|
|
|
self.logger.debug('signature check: %s <!> %s', signature,
|
|
|
|
|
fields[SIGNATURE])
|
|
|
|
|
|
|
|
|
|
if not signature_result:
|
|
|
|
|
message += ' signature invalide.'
|
2012-05-29 16:54:49 +02:00
|
|
|
|
|
2014-03-30 19:27:48 +02:00
|
|
|
|
test = fields[VADS_CTX_MODE] == 'TEST'
|
2020-10-27 15:51:29 +01:00
|
|
|
|
if VADS_EOPAYMENT_TRANS_ID in fields:
|
|
|
|
|
transaction_id = fields[VADS_EOPAYMENT_TRANS_ID]
|
|
|
|
|
else:
|
|
|
|
|
transaction_id = '%s_%s' % (copy[VADS_TRANS_DATE], copy[VADS_TRANS_ID])
|
2012-01-30 17:47:47 +01:00
|
|
|
|
# the VADS_AUTH_NUMBER is the number to match payment in bank logs
|
2012-02-17 17:49:51 +01:00
|
|
|
|
copy[self.BANK_ID] = copy.get(VADS_AUTH_NUMBER, '')
|
2020-04-03 17:47:04 +02:00
|
|
|
|
transaction_date = None
|
|
|
|
|
if VADS_EFFECTIVE_CREATION_DATE in fields:
|
|
|
|
|
transaction_date = parse_utc(fields[VADS_EFFECTIVE_CREATION_DATE])
|
2012-02-17 17:49:51 +01:00
|
|
|
|
response = PaymentResponse(
|
2015-07-22 00:28:39 +02:00
|
|
|
|
result=result,
|
|
|
|
|
signed=signature_result,
|
|
|
|
|
bank_data=copy,
|
|
|
|
|
order_id=transaction_id,
|
2020-10-27 15:51:29 +01:00
|
|
|
|
transaction_id=transaction_id,
|
2020-10-11 09:43:07 +02:00
|
|
|
|
bank_status=message,
|
2020-04-03 17:47:04 +02:00
|
|
|
|
transaction_date=transaction_date,
|
2015-07-22 00:28:39 +02:00
|
|
|
|
test=test)
|
2012-02-17 17:49:51 +01:00
|
|
|
|
return response
|
2011-04-22 17:21:16 +02:00
|
|
|
|
|
2020-09-15 14:54:38 +02:00
|
|
|
|
def sha1_sign(self, secret, signed_data):
|
|
|
|
|
return hashlib.sha1(signed_data).hexdigest()
|
|
|
|
|
|
|
|
|
|
def hmac_sha256_sign(self, secret, signed_data):
|
|
|
|
|
digest = hmac.HMAC(secret, digestmod=hashlib.sha256, msg=signed_data).digest()
|
|
|
|
|
return base64.b64encode(digest)
|
|
|
|
|
|
2012-05-25 15:42:04 +02:00
|
|
|
|
def signature(self, fields):
|
2012-07-10 18:04:22 +02:00
|
|
|
|
self.logger.debug('got fields %s to sign' % fields)
|
2015-07-22 00:28:39 +02:00
|
|
|
|
ordered_keys = sorted(
|
|
|
|
|
[key for key in fields.keys() if key.startswith('vads_')])
|
2012-05-25 15:42:04 +02:00
|
|
|
|
self.logger.debug('ordered keys %s' % ordered_keys)
|
2016-10-12 10:59:58 +02:00
|
|
|
|
ordered_fields = [force_byte(fields[key]) for key in ordered_keys]
|
2020-09-15 14:54:38 +02:00
|
|
|
|
secret = force_byte(getattr(self, 'secret_%s' % fields['vads_ctx_mode'].lower()))
|
2018-03-26 09:56:16 +02:00
|
|
|
|
signed_data = b'+'.join(ordered_fields)
|
2020-09-15 14:54:38 +02:00
|
|
|
|
signed_data = b'%s+%s' % (signed_data, secret)
|
2016-10-12 10:59:58 +02:00
|
|
|
|
self.logger.debug(u'generating signature on «%s»', signed_data)
|
2020-09-15 14:54:38 +02:00
|
|
|
|
sign_method = getattr(self, '%s_sign' % self.signature_algo)
|
|
|
|
|
sign = sign_method(secret, signed_data)
|
2016-10-12 10:59:58 +02:00
|
|
|
|
self.logger.debug(u'signature «%s»', sign)
|
2020-09-15 14:54:38 +02:00
|
|
|
|
return force_text(sign)
|