trivial: apply black (#58937)
This commit is contained in:
parent
f350cf3169
commit
3ee72e5336
|
@ -20,15 +20,48 @@ import logging
|
|||
import pytz
|
||||
|
||||
from .common import ( # noqa: F401
|
||||
URL, HTML, FORM, RECEIVED, ACCEPTED, PAID, DENIED,
|
||||
CANCELED, CANCELLED, ERROR, WAITING, EXPIRED, force_text,
|
||||
ResponseError, PaymentException,
|
||||
URL,
|
||||
HTML,
|
||||
FORM,
|
||||
RECEIVED,
|
||||
ACCEPTED,
|
||||
PAID,
|
||||
DENIED,
|
||||
CANCELED,
|
||||
CANCELLED,
|
||||
ERROR,
|
||||
WAITING,
|
||||
EXPIRED,
|
||||
force_text,
|
||||
ResponseError,
|
||||
PaymentException,
|
||||
)
|
||||
|
||||
__all__ = ['Payment', 'URL', 'HTML', 'FORM', 'SIPS', 'SYSTEMPAY',
|
||||
'TIPI', 'DUMMY', 'get_backend', 'RECEIVED', 'ACCEPTED', 'PAID',
|
||||
'DENIED', 'CANCELED', 'CANCELLED', 'ERROR', 'WAITING',
|
||||
'EXPIRED', 'get_backends', 'PAYFIP_WS', 'SAGA', 'KEYWARE', 'MOLLIE']
|
||||
__all__ = [
|
||||
'Payment',
|
||||
'URL',
|
||||
'HTML',
|
||||
'FORM',
|
||||
'SIPS',
|
||||
'SYSTEMPAY',
|
||||
'TIPI',
|
||||
'DUMMY',
|
||||
'get_backend',
|
||||
'RECEIVED',
|
||||
'ACCEPTED',
|
||||
'PAID',
|
||||
'DENIED',
|
||||
'CANCELED',
|
||||
'CANCELLED',
|
||||
'ERROR',
|
||||
'WAITING',
|
||||
'EXPIRED',
|
||||
'get_backends',
|
||||
'PAYFIP_WS',
|
||||
'SAGA',
|
||||
'KEYWARE',
|
||||
'MOLLIE',
|
||||
]
|
||||
|
||||
SIPS = 'sips'
|
||||
SIPS2 = 'sips2'
|
||||
|
@ -51,86 +84,86 @@ def get_backend(kind):
|
|||
module = importlib.import_module('.' + kind, package='eopayment')
|
||||
return module.Payment
|
||||
|
||||
__BACKENDS = [DUMMY, SIPS, SIPS2, SYSTEMPAY, OGONE, PAYBOX, PAYZEN,
|
||||
TIPI, PAYFIP_WS, KEYWARE, MOLLIE, SAGA]
|
||||
|
||||
__BACKENDS = [DUMMY, SIPS, SIPS2, SYSTEMPAY, OGONE, PAYBOX, PAYZEN, TIPI, PAYFIP_WS, KEYWARE, MOLLIE, SAGA]
|
||||
|
||||
|
||||
def get_backends():
|
||||
'''Return a dictionnary mapping existing eopayment backends name to their
|
||||
description.
|
||||
"""Return a dictionnary mapping existing eopayment backends name to their
|
||||
description.
|
||||
|
||||
>>> get_backends()['dummy'].description['caption']
|
||||
'Dummy payment backend'
|
||||
>>> get_backends()['dummy'].description['caption']
|
||||
'Dummy payment backend'
|
||||
|
||||
'''
|
||||
"""
|
||||
return {backend: get_backend(backend) for backend in __BACKENDS}
|
||||
|
||||
|
||||
class Payment:
|
||||
'''
|
||||
Interface to credit card online payment servers of French banks. The
|
||||
only use case supported for now is a unique automatic payment.
|
||||
"""
|
||||
Interface to credit card online payment servers of French banks. The
|
||||
only use case supported for now is a unique automatic payment.
|
||||
|
||||
>>> options = {
|
||||
'numcli': '12345',
|
||||
}
|
||||
>>> p = Payment(kind=TIPI, options=options)
|
||||
>>> transaction_id, kind, data = p.request('10.00', email='bob@example.com')
|
||||
>>> print (transaction_id, kind, data) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
|
||||
('...', 1, 'https://www.payfip.gov.fr/tpa/paiement.web?...')
|
||||
>>> options = {
|
||||
'numcli': '12345',
|
||||
}
|
||||
>>> p = Payment(kind=TIPI, options=options)
|
||||
>>> transaction_id, kind, data = p.request('10.00', email='bob@example.com')
|
||||
>>> print (transaction_id, kind, data) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
|
||||
('...', 1, 'https://www.payfip.gov.fr/tpa/paiement.web?...')
|
||||
|
||||
Supported backend of French banks are:
|
||||
Supported backend of French banks are:
|
||||
|
||||
- TIPI/PayFiP
|
||||
- SIPS 2.0, for BNP, Banque Populaire (before 2010), CCF, HSBC, Crédit
|
||||
Agricole, La Banque Postale, LCL, Société Générale and Crédit du
|
||||
Nord.
|
||||
- SystemPay v2/Payzen for Banque Populaire and Caise d'Epargne (Natixis, after 2010)
|
||||
- Ogone
|
||||
- Paybox
|
||||
- Mollie (Belgium)
|
||||
- Keyware (Belgium)
|
||||
- TIPI/PayFiP
|
||||
- SIPS 2.0, for BNP, Banque Populaire (before 2010), CCF, HSBC, Crédit
|
||||
Agricole, La Banque Postale, LCL, Société Générale and Crédit du
|
||||
Nord.
|
||||
- SystemPay v2/Payzen for Banque Populaire and Caise d'Epargne (Natixis, after 2010)
|
||||
- Ogone
|
||||
- Paybox
|
||||
- Mollie (Belgium)
|
||||
- Keyware (Belgium)
|
||||
|
||||
For SIPs you also need the bank provided middleware especially the two
|
||||
executables, request and response, as the protocol from ATOS/SIPS is not
|
||||
documented. For the other backends the modules are autonomous.
|
||||
For SIPs you also need the bank provided middleware especially the two
|
||||
executables, request and response, as the protocol from ATOS/SIPS is not
|
||||
documented. For the other backends the modules are autonomous.
|
||||
|
||||
Each backend need some configuration parameters to be used, the
|
||||
description of the backend list those parameters. The description
|
||||
dictionary can be used to generate configuration forms.
|
||||
Each backend need some configuration parameters to be used, the
|
||||
description of the backend list those parameters. The description
|
||||
dictionary can be used to generate configuration forms.
|
||||
|
||||
>>> d = get_backend(SPPLUS).description
|
||||
>>> print d['caption']
|
||||
SPPlus payment service of French bank Caisse d'epargne
|
||||
>>> print [p['name'] for p in d['parameters']] # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
|
||||
['cle', ..., 'moyen']
|
||||
>>> print d['parameters'][0]['caption']
|
||||
Secret key, a 40 digits hexadecimal number
|
||||
>>> d = get_backend(SPPLUS).description
|
||||
>>> print d['caption']
|
||||
SPPlus payment service of French bank Caisse d'epargne
|
||||
>>> print [p['name'] for p in d['parameters']] # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
|
||||
['cle', ..., 'moyen']
|
||||
>>> print d['parameters'][0]['caption']
|
||||
Secret key, a 40 digits hexadecimal number
|
||||
|
||||
'''
|
||||
"""
|
||||
|
||||
def __init__(self, kind, options, logger=None):
|
||||
self.kind = kind
|
||||
self.backend = get_backend(kind)(options, logger=logger)
|
||||
|
||||
def request(self, amount, **kwargs):
|
||||
'''Request a payment to the payment backend.
|
||||
"""Request a payment to the payment backend.
|
||||
|
||||
Arguments:
|
||||
amount -- the amount of money to ask
|
||||
email -- the email of the customer (optional)
|
||||
usually redundant with the hardwired settings in the bank
|
||||
configuration panel. At this url you must use the Payment.response
|
||||
method to analyze the bank returned values.
|
||||
Arguments:
|
||||
amount -- the amount of money to ask
|
||||
email -- the email of the customer (optional)
|
||||
usually redundant with the hardwired settings in the bank
|
||||
configuration panel. At this url you must use the Payment.response
|
||||
method to analyze the bank returned values.
|
||||
|
||||
It returns a triple of values, (transaction_id, kind, data):
|
||||
- the first gives a string value to later match the payment with
|
||||
the invoice,
|
||||
- kind gives the type of the third value, payment.URL or
|
||||
payment.HTML or payment.FORM,
|
||||
- the third is the URL or the HTML form to contact the payment
|
||||
server, which must be sent to the customer browser.
|
||||
'''
|
||||
It returns a triple of values, (transaction_id, kind, data):
|
||||
- the first gives a string value to later match the payment with
|
||||
the invoice,
|
||||
- kind gives the type of the third value, payment.URL or
|
||||
payment.HTML or payment.FORM,
|
||||
- the third is the URL or the HTML form to contact the payment
|
||||
server, which must be sent to the customer browser.
|
||||
"""
|
||||
logger.debug('%r' % kwargs)
|
||||
|
||||
if 'capture_date' in kwargs:
|
||||
|
@ -151,8 +184,7 @@ class Payment:
|
|||
# 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)
|
||||
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.")
|
||||
|
@ -162,58 +194,58 @@ class Payment:
|
|||
return self.backend.request(amount, **kwargs)
|
||||
|
||||
def response(self, query_string, **kwargs):
|
||||
'''
|
||||
Process a response from the Bank API. It must be used on the URL
|
||||
where the user browser of the payment server is going to post the
|
||||
result of the payment. Beware it can happen multiple times for the
|
||||
same payment, so you MUST support multiple notification of the same
|
||||
event, i.e. it should be idempotent. For example if you already
|
||||
validated some invoice, receiving a new payment notification for the
|
||||
same invoice should alter this state change.
|
||||
"""
|
||||
Process a response from the Bank API. It must be used on the URL
|
||||
where the user browser of the payment server is going to post the
|
||||
result of the payment. Beware it can happen multiple times for the
|
||||
same payment, so you MUST support multiple notification of the same
|
||||
event, i.e. it should be idempotent. For example if you already
|
||||
validated some invoice, receiving a new payment notification for the
|
||||
same invoice should alter this state change.
|
||||
|
||||
Beware that when notified directly by the bank (and not through the
|
||||
customer browser) no applicative session will exist, so you should
|
||||
not depend on it in your handler.
|
||||
Beware that when notified directly by the bank (and not through the
|
||||
customer browser) no applicative session will exist, so you should
|
||||
not depend on it in your handler.
|
||||
|
||||
Arguments:
|
||||
query_string -- the URL encoded form-data from a GET or a POST
|
||||
Arguments:
|
||||
query_string -- the URL encoded form-data from a GET or a POST
|
||||
|
||||
It returns a quadruplet of values:
|
||||
It returns a quadruplet of values:
|
||||
|
||||
(result, transaction_id, bank_data, return_content)
|
||||
(result, transaction_id, bank_data, return_content)
|
||||
|
||||
- result is a boolean stating whether the transaction worked, use it
|
||||
to decide whether to act on a valid payment,
|
||||
- the transaction_id return the same id than returned by request
|
||||
when requesting for the payment, use it to find the invoice or
|
||||
transaction which is linked to the payment,
|
||||
- bank_data is a dictionnary of the data sent by the bank, it should
|
||||
be logged for security reasons,
|
||||
- return_content, if not None you must return this content as the
|
||||
result of the HTTP request, it's used when the bank is calling
|
||||
your site as a web service.
|
||||
- result is a boolean stating whether the transaction worked, use it
|
||||
to decide whether to act on a valid payment,
|
||||
- the transaction_id return the same id than returned by request
|
||||
when requesting for the payment, use it to find the invoice or
|
||||
transaction which is linked to the payment,
|
||||
- bank_data is a dictionnary of the data sent by the bank, it should
|
||||
be logged for security reasons,
|
||||
- return_content, if not None you must return this content as the
|
||||
result of the HTTP request, it's used when the bank is calling
|
||||
your site as a web service.
|
||||
|
||||
'''
|
||||
"""
|
||||
return self.backend.response(query_string, **kwargs)
|
||||
|
||||
def cancel(self, amount, bank_data, **kwargs):
|
||||
'''
|
||||
Cancel or edit the amount of a transaction sent to the bank.
|
||||
"""
|
||||
Cancel or edit the amount of a transaction sent to the bank.
|
||||
|
||||
Arguments:
|
||||
- amount -- the amount of money to cancel
|
||||
- bank_data -- the transaction dictionary received from the bank
|
||||
'''
|
||||
Arguments:
|
||||
- amount -- the amount of money to cancel
|
||||
- bank_data -- the transaction dictionary received from the bank
|
||||
"""
|
||||
return self.backend.cancel(amount, bank_data, **kwargs)
|
||||
|
||||
def validate(self, amount, bank_data, **kwargs):
|
||||
'''
|
||||
Validate and trigger the transmission of a transaction to the bank.
|
||||
"""
|
||||
Validate and trigger the transmission of a transaction to the bank.
|
||||
|
||||
Arguments:
|
||||
- amount -- the amount of money
|
||||
- bank_data -- the transaction dictionary received from the bank
|
||||
'''
|
||||
Arguments:
|
||||
- amount -- the amount of money
|
||||
- bank_data -- the transaction dictionary received from the bank
|
||||
"""
|
||||
return self.backend.validate(amount, bank_data, **kwargs)
|
||||
|
||||
def get_parameters(self, scope='global'):
|
||||
|
|
|
@ -67,7 +67,7 @@ def main(ctx, backend, debug, option, name):
|
|||
backend = config_backend
|
||||
load = True
|
||||
elif name and backend:
|
||||
load = (config_backend == backend and config_name == name)
|
||||
load = config_backend == backend and config_name == name
|
||||
elif name:
|
||||
load = config_name == name
|
||||
elif backend:
|
||||
|
@ -123,4 +123,5 @@ def response(backend, query_string, param):
|
|||
for line in formatted_value.splitlines(False):
|
||||
print(' ', line)
|
||||
|
||||
|
||||
main()
|
||||
|
|
|
@ -36,8 +36,7 @@ except ImportError:
|
|||
pass
|
||||
|
||||
|
||||
__all__ = ['PaymentCommon', 'URL', 'HTML', 'RANDOM', 'RECEIVED', 'ACCEPTED',
|
||||
'PAID', 'ERROR', 'WAITING']
|
||||
__all__ = ['PaymentCommon', 'URL', 'HTML', 'RANDOM', 'RECEIVED', 'ACCEPTED', 'PAID', 'ERROR', 'WAITING']
|
||||
|
||||
|
||||
RANDOM = random.SystemRandom()
|
||||
|
@ -94,29 +93,38 @@ class ResponseError(PaymentException):
|
|||
|
||||
|
||||
class PaymentResponse:
|
||||
'''Holds a generic view on the result of payment transaction response.
|
||||
"""Holds a generic view on the result of payment transaction response.
|
||||
|
||||
result -- holds the declarative result of the transaction, does not use
|
||||
it to validate the payment in your backoffice, it's just for informing
|
||||
the user that all is well.
|
||||
test -- indicates if the transaction was a test
|
||||
signed -- holds whether the message was signed
|
||||
bank_data -- a dictionnary containing some data depending on the bank,
|
||||
you have to log it for audit purpose.
|
||||
return_content -- when handling a response in a callback endpoint, i.e.
|
||||
a response transmitted directly from the bank to the merchant website,
|
||||
you usually have to confirm good reception of the message by returning a
|
||||
properly formatted response, this is it.
|
||||
bank_status -- if result is False, it contains the reason
|
||||
order_id -- the id given by the merchant in the payment request
|
||||
transaction_id -- the id assigned by the bank to this transaction, it
|
||||
could be the one sent by the merchant in the request, but it is usually
|
||||
an identifier internal to the bank.
|
||||
'''
|
||||
result -- holds the declarative result of the transaction, does not use
|
||||
it to validate the payment in your backoffice, it's just for informing
|
||||
the user that all is well.
|
||||
test -- indicates if the transaction was a test
|
||||
signed -- holds whether the message was signed
|
||||
bank_data -- a dictionnary containing some data depending on the bank,
|
||||
you have to log it for audit purpose.
|
||||
return_content -- when handling a response in a callback endpoint, i.e.
|
||||
a response transmitted directly from the bank to the merchant website,
|
||||
you usually have to confirm good reception of the message by returning a
|
||||
properly formatted response, this is it.
|
||||
bank_status -- if result is False, it contains the reason
|
||||
order_id -- the id given by the merchant in the payment request
|
||||
transaction_id -- the id assigned by the bank to this transaction, it
|
||||
could be the one sent by the merchant in the request, but it is usually
|
||||
an identifier internal to the bank.
|
||||
"""
|
||||
|
||||
def __init__(self, result=None, signed=None, bank_data=dict(),
|
||||
return_content=None, bank_status='', transaction_id='',
|
||||
order_id='', test=False, transaction_date=None):
|
||||
def __init__(
|
||||
self,
|
||||
result=None,
|
||||
signed=None,
|
||||
bank_data=dict(),
|
||||
return_content=None,
|
||||
bank_status='',
|
||||
transaction_id='',
|
||||
order_id='',
|
||||
test=False,
|
||||
transaction_date=None,
|
||||
):
|
||||
self.result = result
|
||||
self.signed = signed
|
||||
self.bank_data = bank_data
|
||||
|
@ -163,11 +171,9 @@ class PaymentCommon:
|
|||
while True:
|
||||
parts = [RANDOM.choice(choices) for x in range(length)]
|
||||
id = ''.join(parts)
|
||||
name = '%s_%s_%s' % (str(date.today()),
|
||||
'-'.join(prefixes), str(id))
|
||||
name = '%s_%s_%s' % (str(date.today()), '-'.join(prefixes), str(id))
|
||||
try:
|
||||
fd = os.open(os.path.join(self.PATH, name),
|
||||
os.O_CREAT | os.O_EXCL)
|
||||
fd = os.open(os.path.join(self.PATH, name), os.O_CREAT | os.O_EXCL)
|
||||
except Exception:
|
||||
raise
|
||||
else:
|
||||
|
@ -179,9 +185,12 @@ class PaymentCommon:
|
|||
try:
|
||||
amount = Decimal(amount)
|
||||
except ValueError:
|
||||
raise ValueError('invalid amount %s: it must be a decimal integer with two digits '
|
||||
'at most after the decimal point', amount)
|
||||
if int(amount) < min_amount or (max_amount and int(amount) > max_amount):
|
||||
raise ValueError(
|
||||
'invalid amount %s: it must be a decimal integer with two digits '
|
||||
'at most after the decimal point',
|
||||
amount,
|
||||
)
|
||||
if int(amount) < min_amount or (max_amount and int(amount) > max_amount):
|
||||
raise ValueError('amount %s is not in range [%s, %s]' % (amount, min_amount, max_amount))
|
||||
if cents:
|
||||
amount *= Decimal('100') # convert to cents
|
||||
|
@ -190,8 +199,7 @@ class PaymentCommon:
|
|||
|
||||
|
||||
class Form:
|
||||
def __init__(self, url, method, fields, encoding='utf-8',
|
||||
submit_name='Submit', submit_value='Submit'):
|
||||
def __init__(self, url, method, fields, encoding='utf-8', submit_name='Submit', submit_value='Submit'):
|
||||
self.url = url
|
||||
self.method = method
|
||||
self.fields = fields
|
||||
|
@ -211,13 +219,14 @@ class Form:
|
|||
def escape(self, s):
|
||||
return html.escape(force_text(s, self.encoding))
|
||||
|
||||
|
||||
def __str__(self):
|
||||
s = '<form method="%s" action="%s">' % (self.method, self.url)
|
||||
for field in self.fields:
|
||||
s += '\n <input type="%s" name="%s" value="%s"/>' % (self.escape(field['type']),
|
||||
self.escape(field['name']),
|
||||
self.escape(field['value']))
|
||||
s += '\n <input type="%s" name="%s" value="%s"/>' % (
|
||||
self.escape(field['type']),
|
||||
self.escape(field['name']),
|
||||
self.escape(field['value']),
|
||||
)
|
||||
s += '\n <input type="submit"'
|
||||
if self.submit_name:
|
||||
s += ' name="%s"' % self.escape(self.submit_name)
|
||||
|
|
|
@ -20,15 +20,7 @@ import warnings
|
|||
|
||||
from six.moves.urllib.parse import parse_qs, urlencode
|
||||
|
||||
from .common import (
|
||||
PaymentCommon,
|
||||
PaymentResponse,
|
||||
ResponseError,
|
||||
URL,
|
||||
PAID, ERROR, WAITING,
|
||||
force_text,
|
||||
_
|
||||
)
|
||||
from .common import PaymentCommon, PaymentResponse, ResponseError, URL, PAID, ERROR, WAITING, force_text, _
|
||||
|
||||
__all__ = ['Payment']
|
||||
|
||||
|
@ -38,25 +30,26 @@ LOGGER = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class Payment(PaymentCommon):
|
||||
'''
|
||||
Dummy implementation of the payment interface.
|
||||
"""
|
||||
Dummy implementation of the payment interface.
|
||||
|
||||
It is used with a dummy implementation of a bank payment service that
|
||||
you can find on:
|
||||
It is used with a dummy implementation of a bank payment service that
|
||||
you can find on:
|
||||
|
||||
http://dummy-payment.demo.entrouvert.com/
|
||||
http://dummy-payment.demo.entrouvert.com/
|
||||
|
||||
You must pass the following keys inside the options dictionnary:
|
||||
- dummy_service_url, the URL of the dummy payment service, it defaults
|
||||
to the one operated by Entr'ouvert.
|
||||
- automatic_return_url: where to POST to notify the service of a
|
||||
payment
|
||||
- origin: a human string to display to the user about the origin of
|
||||
the request.
|
||||
- siret: an identifier for the eCommerce site, fake.
|
||||
- normal_return_url: the return URL for the user (can be overriden on a
|
||||
per request basis).
|
||||
"""
|
||||
|
||||
You must pass the following keys inside the options dictionnary:
|
||||
- dummy_service_url, the URL of the dummy payment service, it defaults
|
||||
to the one operated by Entr'ouvert.
|
||||
- automatic_return_url: where to POST to notify the service of a
|
||||
payment
|
||||
- origin: a human string to display to the user about the origin of
|
||||
the request.
|
||||
- siret: an identifier for the eCommerce site, fake.
|
||||
- normal_return_url: the return URL for the user (can be overriden on a
|
||||
per request basis).
|
||||
'''
|
||||
description = {
|
||||
'caption': 'Dummy payment backend',
|
||||
'parameters': [
|
||||
|
@ -79,8 +72,7 @@ class Payment(PaymentCommon):
|
|||
},
|
||||
{
|
||||
'name': 'origin',
|
||||
'caption': _('name of the requesting service, '
|
||||
'to present in the user interface'),
|
||||
'caption': _('name of the requesting service, ' 'to present in the user interface'),
|
||||
'type': str,
|
||||
'default': 'origin',
|
||||
},
|
||||
|
@ -129,23 +121,51 @@ class Payment(PaymentCommon):
|
|||
],
|
||||
}
|
||||
|
||||
def request(self, amount, name=None, address=None, email=None, phone=None,
|
||||
orderid=None, info1=None, info2=None, info3=None,
|
||||
next_url=None, capture_day=None, subject=None, **kwargs):
|
||||
def request(
|
||||
self,
|
||||
amount,
|
||||
name=None,
|
||||
address=None,
|
||||
email=None,
|
||||
phone=None,
|
||||
orderid=None,
|
||||
info1=None,
|
||||
info2=None,
|
||||
info3=None,
|
||||
next_url=None,
|
||||
capture_day=None,
|
||||
subject=None,
|
||||
**kwargs,
|
||||
):
|
||||
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)
|
||||
__name__,
|
||||
amount,
|
||||
name,
|
||||
address,
|
||||
email,
|
||||
phone,
|
||||
info1,
|
||||
info2,
|
||||
info3,
|
||||
next_url,
|
||||
kwargs,
|
||||
)
|
||||
transaction_id = str(uuid.uuid4().hex)
|
||||
normal_return_url = self.normal_return_url
|
||||
if next_url and not normal_return_url:
|
||||
warnings.warn("passing next_url to request() is deprecated, "
|
||||
"set normal_return_url in options", DeprecationWarning)
|
||||
warnings.warn(
|
||||
"passing next_url to request() is deprecated, " "set normal_return_url in options",
|
||||
DeprecationWarning,
|
||||
)
|
||||
normal_return_url = next_url
|
||||
automatic_return_url = self.automatic_return_url
|
||||
if self.direct_notification_url and not automatic_return_url:
|
||||
warnings.warn("direct_notification_url option is deprecated, "
|
||||
"use automatic_return_url", DeprecationWarning)
|
||||
warnings.warn(
|
||||
"direct_notification_url option is deprecated, " "use automatic_return_url",
|
||||
DeprecationWarning,
|
||||
)
|
||||
automatic_return_url = self.direct_notification_url
|
||||
query = {
|
||||
'transaction_id': transaction_id,
|
||||
|
@ -153,11 +173,20 @@ class Payment(PaymentCommon):
|
|||
'email': email,
|
||||
'return_url': normal_return_url or '',
|
||||
'direct_notification_url': automatic_return_url or '',
|
||||
'origin': self.origin
|
||||
'origin': self.origin,
|
||||
}
|
||||
query.update(
|
||||
dict(name=name, address=address, email=email, phone=phone,
|
||||
orderid=orderid, info1=info1, info2=info2, info3=info3))
|
||||
dict(
|
||||
name=name,
|
||||
address=address,
|
||||
email=email,
|
||||
phone=phone,
|
||||
orderid=orderid,
|
||||
info1=info1,
|
||||
info2=info2,
|
||||
info3=info3,
|
||||
)
|
||||
)
|
||||
if capture_day is not None:
|
||||
query['capture_day'] = str(capture_day)
|
||||
if subject is not None:
|
||||
|
@ -193,7 +222,8 @@ class Payment(PaymentCommon):
|
|||
order_id=transaction_id,
|
||||
transaction_id=transaction_id,
|
||||
bank_status=form.get('reason'),
|
||||
test=True)
|
||||
test=True,
|
||||
)
|
||||
return response
|
||||
|
||||
def validate(self, amount, bank_data, **kwargs):
|
||||
|
|
|
@ -17,14 +17,25 @@
|
|||
import requests
|
||||
from six.moves.urllib.parse import parse_qs, urljoin
|
||||
|
||||
from .common import (CANCELLED, ERROR, PAID, URL, WAITING, PaymentCommon,
|
||||
PaymentException, PaymentResponse, ResponseError, _)
|
||||
from .common import (
|
||||
CANCELLED,
|
||||
ERROR,
|
||||
PAID,
|
||||
URL,
|
||||
WAITING,
|
||||
PaymentCommon,
|
||||
PaymentException,
|
||||
PaymentResponse,
|
||||
ResponseError,
|
||||
_,
|
||||
)
|
||||
|
||||
__all__ = ['Payment']
|
||||
|
||||
|
||||
class Payment(PaymentCommon):
|
||||
'''Implements EMS API, see https://dev.online.emspay.eu/.'''
|
||||
|
||||
service_url = 'https://api.online.emspay.eu/v1/'
|
||||
|
||||
description = {
|
||||
|
@ -67,7 +78,7 @@ class Payment(PaymentCommon):
|
|||
'email_address': email,
|
||||
'first_name': first_name,
|
||||
'last_name': last_name,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
resp = self.call_endpoint('POST', 'orders', data=body)
|
||||
|
@ -98,7 +109,7 @@ class Payment(PaymentCommon):
|
|||
order_id=order_id,
|
||||
transaction_id=order_id,
|
||||
bank_status=status,
|
||||
test=bool('is-test' in resp.get('flags', []))
|
||||
test=bool('is-test' in resp.get('flags', [])),
|
||||
)
|
||||
return response
|
||||
|
||||
|
@ -120,12 +131,12 @@ class Payment(PaymentCommon):
|
|||
result = response.json()
|
||||
except ValueError:
|
||||
self.logger.debug('received invalid json %r', response.text)
|
||||
raise PaymentException('%s on endpoint "%s" returned invalid JSON: %s' %
|
||||
(method, endpoint, response.text))
|
||||
raise PaymentException(
|
||||
'%s on endpoint "%s" returned invalid JSON: %s' % (method, endpoint, response.text)
|
||||
)
|
||||
self.logger.debug('received "%s" with status %s', result, response.status_code)
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
raise PaymentException(
|
||||
'%s error on endpoint "%s": %s "%s"' % (method, endpoint, e, result))
|
||||
raise PaymentException('%s error on endpoint "%s": %s "%s"' % (method, endpoint, e, result))
|
||||
return result
|
||||
|
|
|
@ -19,15 +19,25 @@ from .common import _
|
|||
import requests
|
||||
from six.moves.urllib.parse import parse_qs, urljoin
|
||||
|
||||
from .common import (CANCELLED, ERROR, PAID, URL, PaymentCommon,
|
||||
PaymentException, PaymentResponse, ResponseError, WAITING,
|
||||
ACCEPTED)
|
||||
from .common import (
|
||||
CANCELLED,
|
||||
ERROR,
|
||||
PAID,
|
||||
URL,
|
||||
PaymentCommon,
|
||||
PaymentException,
|
||||
PaymentResponse,
|
||||
ResponseError,
|
||||
WAITING,
|
||||
ACCEPTED,
|
||||
)
|
||||
|
||||
__all__ = ['Payment']
|
||||
|
||||
|
||||
class Payment(PaymentCommon):
|
||||
'''Implements Mollie API, see https://docs.mollie.com/reference/v2/.'''
|
||||
|
||||
service_url = 'https://api.mollie.com/v2/'
|
||||
|
||||
description = {
|
||||
|
@ -67,8 +77,9 @@ class Payment(PaymentCommon):
|
|||
def request(self, amount, **kwargs):
|
||||
amount = self.clean_amount(amount, cents=False)
|
||||
|
||||
metadata = {k: v for k, v in kwargs.items()
|
||||
if k in ('email', 'first_name', 'last_name') and v is not None}
|
||||
metadata = {
|
||||
k: v for k, v in kwargs.items() if k in ('email', 'first_name', 'last_name') and v is not None
|
||||
}
|
||||
body = {
|
||||
'amount': {
|
||||
'value': amount,
|
||||
|
@ -84,8 +95,7 @@ class Payment(PaymentCommon):
|
|||
|
||||
return resp['id'], URL, resp['_links']['checkout']['href']
|
||||
|
||||
def response(self, query_string, redirect=False, order_id_hint=None,
|
||||
order_status_hint=None, **kwargs):
|
||||
def response(self, query_string, redirect=False, order_id_hint=None, order_status_hint=None, **kwargs):
|
||||
if redirect:
|
||||
if order_status_hint in (PAID, CANCELLED, ERROR):
|
||||
return PaymentResponse(order_id=order_id_hint, result=order_status_hint)
|
||||
|
@ -118,7 +128,7 @@ class Payment(PaymentCommon):
|
|||
order_id=payment_id,
|
||||
transaction_id=payment_id,
|
||||
bank_status=status,
|
||||
test=resp['mode'] == 'test'
|
||||
test=resp['mode'] == 'test',
|
||||
)
|
||||
return response
|
||||
|
||||
|
@ -133,13 +143,14 @@ class Payment(PaymentCommon):
|
|||
result = response.json()
|
||||
except ValueError:
|
||||
self.logger.debug('received invalid json %r', response.text)
|
||||
raise PaymentException('%s on endpoint "%s" returned invalid JSON: %s' %
|
||||
(method, endpoint, response.text))
|
||||
raise PaymentException(
|
||||
'%s on endpoint "%s" returned invalid JSON: %s' % (method, endpoint, response.text)
|
||||
)
|
||||
self.logger.debug('received "%s" with status %s', result, response.status_code)
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
raise PaymentException(
|
||||
'%s error on endpoint "%s": %s "%s"' % (method, endpoint, e,
|
||||
result.get('detail', result)))
|
||||
'%s error on endpoint "%s": %s "%s"' % (method, endpoint, e, result.get('detail', result))
|
||||
)
|
||||
return result
|
||||
|
|
|
@ -20,9 +20,21 @@ import hashlib
|
|||
import uuid
|
||||
|
||||
from .common import (
|
||||
PaymentCommon, PaymentResponse, FORM, CANCELLED, PAID,
|
||||
ERROR, Form, DENIED, ACCEPTED, ORDERID_TRANSACTION_SEPARATOR,
|
||||
WAITING, ResponseError, force_byte, force_text, _
|
||||
PaymentCommon,
|
||||
PaymentResponse,
|
||||
FORM,
|
||||
CANCELLED,
|
||||
PAID,
|
||||
ERROR,
|
||||
Form,
|
||||
DENIED,
|
||||
ACCEPTED,
|
||||
ORDERID_TRANSACTION_SEPARATOR,
|
||||
WAITING,
|
||||
ResponseError,
|
||||
force_byte,
|
||||
force_text,
|
||||
_,
|
||||
)
|
||||
|
||||
|
||||
|
@ -489,7 +501,7 @@ class Payment(PaymentCommon):
|
|||
'default': 'EUR',
|
||||
'choices': ('EUR',),
|
||||
},
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
def __init__(self, options, logger=None):
|
||||
|
@ -522,9 +534,17 @@ class Payment(PaymentCommon):
|
|||
return ENVIRONMENT_PROD_URL
|
||||
raise NotImplementedError('unknown environment %s' % self.environment)
|
||||
|
||||
def request(self, amount, orderid=None, name=None, email=None,
|
||||
language=None, description=None, transaction_id=None,
|
||||
**kwargs):
|
||||
def request(
|
||||
self,
|
||||
amount,
|
||||
orderid=None,
|
||||
name=None,
|
||||
email=None,
|
||||
language=None,
|
||||
description=None,
|
||||
transaction_id=None,
|
||||
**kwargs,
|
||||
):
|
||||
# use complus for transmitting and receiving the transaction_id see
|
||||
# https://epayments-support.ingenico.com/fr/integration/all-sales-channels/integrate-with-e-commerce/guide#variable-feedback-parameters
|
||||
# orderid is now only used for unicity of payments check (it's
|
||||
|
@ -570,9 +590,8 @@ class Payment(PaymentCommon):
|
|||
form = Form(
|
||||
url=url,
|
||||
method='POST',
|
||||
fields=[{'type': 'hidden',
|
||||
'name': key,
|
||||
'value': params[key]} for key in params])
|
||||
fields=[{'type': 'hidden', 'name': key, 'value': params[key]} for key in params],
|
||||
)
|
||||
return complus, FORM, form
|
||||
|
||||
def response(self, query_string, **kwargs):
|
||||
|
@ -613,8 +632,9 @@ class Payment(PaymentCommon):
|
|||
# status 91: payment waiting/pending)
|
||||
result = WAITING
|
||||
else:
|
||||
self.logger.error('response STATUS=%s NCERROR=%s NCERRORPLUS=%s',
|
||||
status, error, params.get('NCERRORPLUS', ''))
|
||||
self.logger.error(
|
||||
'response STATUS=%s NCERROR=%s NCERRORPLUS=%s', status, error, params.get('NCERRORPLUS', '')
|
||||
)
|
||||
result = ERROR
|
||||
# extract reference from received order id
|
||||
|
||||
|
@ -623,4 +643,5 @@ class Payment(PaymentCommon):
|
|||
signed=signed,
|
||||
bank_data=params,
|
||||
order_id=complus or orderid,
|
||||
transaction_id=transaction_id)
|
||||
transaction_id=transaction_id,
|
||||
)
|
||||
|
|
|
@ -36,9 +36,20 @@ from six.moves.urllib import parse as urllib
|
|||
import base64
|
||||
import warnings
|
||||
|
||||
from .common import (PaymentCommon, PaymentResponse, FORM, PAID, CANCELLED,
|
||||
DENIED, ERROR, Form, ResponseError, force_text,
|
||||
force_byte, _)
|
||||
from .common import (
|
||||
PaymentCommon,
|
||||
PaymentResponse,
|
||||
FORM,
|
||||
PAID,
|
||||
CANCELLED,
|
||||
DENIED,
|
||||
ERROR,
|
||||
Form,
|
||||
ResponseError,
|
||||
force_text,
|
||||
force_byte,
|
||||
_,
|
||||
)
|
||||
from . import cb
|
||||
|
||||
__all__ = ['sign', 'Payment']
|
||||
|
@ -110,9 +121,7 @@ PAYBOX_ERROR_CODES = {
|
|||
'message': 'Opération sans authentification 3-DSecure, bloquée par le filtre.',
|
||||
'result': DENIED,
|
||||
},
|
||||
'99999': {
|
||||
'message': 'Opération en attente de validation par l’émetteur du moyen de paiement.'
|
||||
},
|
||||
'99999': {'message': 'Opération en attente de validation par l’émetteur du moyen de paiement.'},
|
||||
}
|
||||
|
||||
ALGOS = {
|
||||
|
@ -123,18 +132,15 @@ ALGOS = {
|
|||
}
|
||||
|
||||
URLS = {
|
||||
'test':
|
||||
'https://preprod-tpeweb.paybox.com/cgi/MYchoix_pagepaiement.cgi',
|
||||
'prod':
|
||||
'https://tpeweb.paybox.com/cgi/MYchoix_pagepaiement.cgi',
|
||||
'backup':
|
||||
'https://tpeweb1.paybox.com/cgi/MYchoix_pagepaiement.cgi',
|
||||
'test': 'https://preprod-tpeweb.paybox.com/cgi/MYchoix_pagepaiement.cgi',
|
||||
'prod': 'https://tpeweb.paybox.com/cgi/MYchoix_pagepaiement.cgi',
|
||||
'backup': 'https://tpeweb1.paybox.com/cgi/MYchoix_pagepaiement.cgi',
|
||||
}
|
||||
|
||||
PAYBOX_DIRECT_URLS = {
|
||||
'test': 'https://preprod-ppps.paybox.com/PPPS.php',
|
||||
'prod': 'https://ppps.paybox.com/PPPS.php',
|
||||
'backup': 'https://ppps1.paybox.com/PPPS.php'
|
||||
'backup': 'https://ppps1.paybox.com/PPPS.php',
|
||||
}
|
||||
|
||||
PAYBOX_DIRECT_CANCEL_OPERATION = '00055'
|
||||
|
@ -145,14 +151,13 @@ PAYBOX_DIRECT_VERSION_NUMBER = '00103'
|
|||
PAYBOX_DIRECT_SUCCESS_RESPONSE_CODE = '00000'
|
||||
|
||||
# payment modes
|
||||
PAYMENT_MODES = {'AUTHOR_CAPTURE': 'O',
|
||||
'IMMEDIATE': 'N'}
|
||||
PAYMENT_MODES = {'AUTHOR_CAPTURE': 'O', 'IMMEDIATE': 'N'}
|
||||
|
||||
|
||||
def sign(data, key):
|
||||
'''Take a list of tuple key, value and sign it by building a string to
|
||||
sign.
|
||||
'''
|
||||
"""Take a list of tuple key, value and sign it by building a string to
|
||||
sign.
|
||||
"""
|
||||
logger = logging.getLogger(__name__)
|
||||
algo = None
|
||||
logger.debug('signature key %r', key)
|
||||
|
@ -179,20 +184,21 @@ def verify(data, signature, key=PAYBOX_KEY):
|
|||
|
||||
|
||||
class Payment(PaymentCommon):
|
||||
'''Paybox backend for eopayment.
|
||||
"""Paybox backend for eopayment.
|
||||
|
||||
If you want to handle Instant Payment Notification, you must pass
|
||||
provide a automatic_return_url option specifying the URL of the
|
||||
callback endpoint.
|
||||
If you want to handle Instant Payment Notification, you must pass
|
||||
provide a automatic_return_url option specifying the URL of the
|
||||
callback endpoint.
|
||||
|
||||
Email is mandatory to emit payment requests with paybox.
|
||||
Email is mandatory to emit payment requests with paybox.
|
||||
|
||||
IP adresses to authorize:
|
||||
IN OUT
|
||||
test 195.101.99.73 195.101.99.76
|
||||
production 194.2.160.66 194.2.122.158
|
||||
backup 195.25.7.146 195.25.7.166
|
||||
"""
|
||||
|
||||
IP adresses to authorize:
|
||||
IN OUT
|
||||
test 195.101.99.73 195.101.99.76
|
||||
production 194.2.160.66 194.2.122.158
|
||||
backup 195.25.7.146 195.25.7.166
|
||||
'''
|
||||
callback = None
|
||||
|
||||
description = {
|
||||
|
@ -217,14 +223,13 @@ class Payment(PaymentCommon):
|
|||
('test', 'Test'),
|
||||
('backup', 'Backup'),
|
||||
('prod', 'Production'),
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
'name': 'site',
|
||||
'caption': 'Numéro de site',
|
||||
'required': True,
|
||||
'validation': lambda x: isinstance(x, str)
|
||||
and x.isdigit() and len(x) == 7,
|
||||
'validation': lambda x: isinstance(x, str) and x.isdigit() and len(x) == 7,
|
||||
},
|
||||
{
|
||||
'name': 'cle',
|
||||
|
@ -237,30 +242,27 @@ class Payment(PaymentCommon):
|
|||
'name': 'rang',
|
||||
'caption': 'Numéro de rang',
|
||||
'required': True,
|
||||
'validation': lambda x: isinstance(x, str)
|
||||
and x.isdigit() and len(x) == 3,
|
||||
'validation': lambda x: isinstance(x, str) and x.isdigit() and len(x) == 3,
|
||||
},
|
||||
{
|
||||
'name': 'identifiant',
|
||||
'caption': 'Identifiant',
|
||||
'required': True,
|
||||
'validation': lambda x: isinstance(x, str)
|
||||
and x.isdigit() and (0 < len(x) < 10),
|
||||
'validation': lambda x: isinstance(x, str) and x.isdigit() and (0 < len(x) < 10),
|
||||
},
|
||||
{
|
||||
'name': 'shared_secret',
|
||||
'caption': 'Secret partagé (clé HMAC)',
|
||||
'validation': lambda x: isinstance(x, str)
|
||||
and all(a.lower() in '0123456789abcdef' for a in x) and (len(x) % 2 == 0),
|
||||
and all(a.lower() in '0123456789abcdef' for a in x)
|
||||
and (len(x) % 2 == 0),
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'name': 'devise',
|
||||
'caption': 'Devise',
|
||||
'default': '978',
|
||||
'choices': (
|
||||
('978', 'Euro'),
|
||||
),
|
||||
'choices': (('978', 'Euro'),),
|
||||
},
|
||||
{
|
||||
'name': 'callback',
|
||||
|
@ -272,30 +274,29 @@ class Payment(PaymentCommon):
|
|||
'caption': 'Nombre de jours pour un paiement différé',
|
||||
'default': '',
|
||||
'required': False,
|
||||
'validation': lambda x: isinstance(x, str)
|
||||
and x.isdigit() and (1 <= len(x) <= 2)
|
||||
'validation': lambda x: isinstance(x, str) and x.isdigit() and (1 <= len(x) <= 2),
|
||||
},
|
||||
{
|
||||
'name': 'capture_mode',
|
||||
'caption': _('Capture Mode'),
|
||||
'default': 'IMMEDIATE',
|
||||
'required': False,
|
||||
'choices': list(PAYMENT_MODES)
|
||||
'choices': list(PAYMENT_MODES),
|
||||
},
|
||||
{
|
||||
'name': 'manual_validation',
|
||||
'caption': 'Validation manuelle',
|
||||
'type': bool,
|
||||
'default': False,
|
||||
'scope': 'transaction'
|
||||
'scope': 'transaction',
|
||||
},
|
||||
{
|
||||
'name': 'timezone',
|
||||
'caption': _('Default Timezone'),
|
||||
'default': 'Europe/Paris',
|
||||
'required': False,
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
def make_pbx_cmd(self, guid, orderid=None, transaction_id=None):
|
||||
|
@ -315,9 +316,9 @@ class Payment(PaymentCommon):
|
|||
d['PBX_TOTAL'] = self.clean_amount(amount)
|
||||
d['PBX_DEVISE'] = force_text(self.devise)
|
||||
guid = str(uuid.uuid4().hex)
|
||||
transaction_id = d['PBX_CMD'] = self.make_pbx_cmd(guid=guid,
|
||||
transaction_id=kwargs.get('transaction_id'),
|
||||
orderid=orderid)
|
||||
transaction_id = d['PBX_CMD'] = self.make_pbx_cmd(
|
||||
guid=guid, transaction_id=kwargs.get('transaction_id'), orderid=orderid
|
||||
)
|
||||
d['PBX_PORTEUR'] = force_text(email)
|
||||
d['PBX_RETOUR'] = (
|
||||
'montant:M;reference:R;code_autorisation:A;erreur:E;numero_appel:T;'
|
||||
|
@ -327,8 +328,8 @@ class Payment(PaymentCommon):
|
|||
)
|
||||
d['PBX_HASH'] = 'SHA512'
|
||||
d['PBX_TIME'] = kwargs.get('time') or (
|
||||
force_text(datetime.datetime.utcnow().isoformat('T')).split('.')[0]
|
||||
+ '+00:00')
|
||||
force_text(datetime.datetime.utcnow().isoformat('T')).split('.')[0] + '+00:00'
|
||||
)
|
||||
d['PBX_ARCHIVAGE'] = orderid or guid
|
||||
if self.normal_return_url:
|
||||
d['PBX_EFFECTUE'] = self.normal_return_url
|
||||
|
@ -337,8 +338,7 @@ class Payment(PaymentCommon):
|
|||
d['PBX_ATTENTE'] = self.normal_return_url
|
||||
automatic_return_url = self.automatic_return_url
|
||||
if not automatic_return_url and self.callback:
|
||||
warnings.warn("callback option is deprecated, "
|
||||
"use automatic_return_url", DeprecationWarning)
|
||||
warnings.warn("callback option is deprecated, " "use automatic_return_url", DeprecationWarning)
|
||||
automatic_return_url = self.callback
|
||||
capture_day = capture_day = kwargs.get('capture_day', self.capture_day)
|
||||
if capture_day:
|
||||
|
@ -350,19 +350,19 @@ class Payment(PaymentCommon):
|
|||
d['PBX_REPONDRE_A'] = force_text(automatic_return_url)
|
||||
d = d.items()
|
||||
|
||||
|
||||
shared_secret = codecs.decode(bytes(self.shared_secret, 'ascii'), 'hex')
|
||||
d = sign(d, shared_secret)
|
||||
url = URLS[self.platform]
|
||||
fields = []
|
||||
for k, v in d:
|
||||
fields.append({
|
||||
'type': 'hidden',
|
||||
'name': force_text(k),
|
||||
'value': force_text(v),
|
||||
})
|
||||
form = Form(url, 'POST', fields, submit_name=None,
|
||||
submit_value='Envoyer', encoding='utf-8')
|
||||
fields.append(
|
||||
{
|
||||
'type': 'hidden',
|
||||
'name': force_text(k),
|
||||
'value': force_text(v),
|
||||
}
|
||||
)
|
||||
form = Form(url, 'POST', fields, submit_name=None, submit_value='Envoyer', encoding='utf-8')
|
||||
return transaction_id, FORM, form
|
||||
|
||||
def response(self, query_string, callback=False, **kwargs):
|
||||
|
@ -378,9 +378,16 @@ class Payment(PaymentCommon):
|
|||
raise ResponseError('invalid signature')
|
||||
data = []
|
||||
if callback:
|
||||
for key in ('montant', 'reference', 'code_autorisation',
|
||||
'erreur', 'numero_appel', 'numero_transaction',
|
||||
'date_transaction', 'heure_transaction'):
|
||||
for key in (
|
||||
'montant',
|
||||
'reference',
|
||||
'code_autorisation',
|
||||
'erreur',
|
||||
'numero_appel',
|
||||
'numero_transaction',
|
||||
'date_transaction',
|
||||
'heure_transaction',
|
||||
):
|
||||
data.append('%s=%s' % (key, urllib.quote(d[key][0])))
|
||||
else:
|
||||
for key, value in urlparse.parse_qsl(query_string, True, True):
|
||||
|
@ -418,7 +425,8 @@ class Payment(PaymentCommon):
|
|||
bank_data=d,
|
||||
result=result,
|
||||
bank_status=message,
|
||||
transaction_date=transaction_date)
|
||||
transaction_date=transaction_date,
|
||||
)
|
||||
|
||||
def perform(self, amount, bank_data, operation):
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
|
@ -35,9 +35,21 @@ import zeep
|
|||
import zeep.exceptions
|
||||
|
||||
from .systempayv2 import isonow
|
||||
from .common import (PaymentCommon, PaymentResponse, URL, PAID, DENIED,
|
||||
CANCELLED, ERROR, ResponseError, PaymentException,
|
||||
WAITING, EXPIRED, force_text, _)
|
||||
from .common import (
|
||||
PaymentCommon,
|
||||
PaymentResponse,
|
||||
URL,
|
||||
PAID,
|
||||
DENIED,
|
||||
CANCELLED,
|
||||
ERROR,
|
||||
ResponseError,
|
||||
PaymentException,
|
||||
WAITING,
|
||||
EXPIRED,
|
||||
force_text,
|
||||
_,
|
||||
)
|
||||
|
||||
# The URL of the WSDL published in the documentation is still wrong, it
|
||||
# references XSD files which are not resolvable :/ we must use this other URL
|
||||
|
@ -54,7 +66,7 @@ REFDET_RE = re.compile(r'^[A-Za-z0-9]{1,30}$')
|
|||
def clear_namespace(element):
|
||||
def helper(element):
|
||||
if element.tag.startswith('{'):
|
||||
element.tag = element.tag[element.tag.index('}') + 1:]
|
||||
element.tag = element.tag[element.tag.index('}') + 1 :]
|
||||
for subelement in element:
|
||||
helper(subelement)
|
||||
|
||||
|
@ -111,7 +123,10 @@ class PayFiP:
|
|||
return self._client
|
||||
|
||||
def fault_to_exception(self, fault):
|
||||
if fault.message != 'fr.gouv.finances.cp.tpa.webservice.exceptions.FonctionnelleErreur' or fault.detail is None:
|
||||
if (
|
||||
fault.message != 'fr.gouv.finances.cp.tpa.webservice.exceptions.FonctionnelleErreur'
|
||||
or fault.detail is None
|
||||
):
|
||||
return
|
||||
detail = clear_namespace(fault.detail)
|
||||
code = detail.find('FonctionnelleErreur/code')
|
||||
|
@ -123,7 +138,8 @@ class PayFiP:
|
|||
code=code.text,
|
||||
message=(descriptif is not None and descriptif.text)
|
||||
or (libelle is not None and libelle.text)
|
||||
or '')
|
||||
or '',
|
||||
)
|
||||
|
||||
def _perform(self, request_qname, operation, **kwargs):
|
||||
RequestType = self.client.get_type(request_qname) # noqa: E501
|
||||
|
@ -142,9 +158,12 @@ class PayFiP:
|
|||
return self._perform(
|
||||
'{http://securite.service.tpa.cp.finances.gouv.fr/reponse}RecupererDetailClientRequest',
|
||||
'recupererDetailClient',
|
||||
numCli=numcli)
|
||||
numCli=numcli,
|
||||
)
|
||||
|
||||
def get_idop(self, numcli, saisie, exer, refdet, montant, mel, url_notification, url_redirect, objet=None):
|
||||
def get_idop(
|
||||
self, numcli, saisie, exer, refdet, montant, mel, url_notification, url_redirect, objet=None
|
||||
):
|
||||
return self._perform(
|
||||
'{http://securite.service.tpa.cp.finances.gouv.fr/requete}CreerPaiementSecuriseRequest',
|
||||
'creerPaiementSecurise',
|
||||
|
@ -156,20 +175,22 @@ class PayFiP:
|
|||
mel=mel,
|
||||
urlnotif=url_notification,
|
||||
urlredirect=url_redirect,
|
||||
objet=objet)
|
||||
objet=objet,
|
||||
)
|
||||
|
||||
def get_info_paiement(self, idop):
|
||||
return self._perform(
|
||||
'{http://securite.service.tpa.cp.finances.gouv.fr/reponse}RecupererDetailPaiementSecuriseRequest',
|
||||
'recupererDetailPaiementSecurise',
|
||||
idOp=idop)
|
||||
idOp=idop,
|
||||
)
|
||||
|
||||
|
||||
class Payment(PaymentCommon):
|
||||
'''Produce requests for and verify response from the TIPI online payment
|
||||
"""Produce requests for and verify response from the TIPI online payment
|
||||
processor from the French Finance Ministry.
|
||||
|
||||
'''
|
||||
"""
|
||||
|
||||
description = {
|
||||
'caption': 'TIPI, Titres Payables par Internet',
|
||||
|
@ -216,8 +237,9 @@ class Payment(PaymentCommon):
|
|||
def _generate_refdet(self):
|
||||
return '%s%010d' % (isonow(), random.randint(1, 1000000000))
|
||||
|
||||
def request(self, amount, email, refdet=None, exer=None, orderid=None,
|
||||
subject=None, transaction_id=None, **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
|
||||
|
@ -263,11 +285,17 @@ class Payment(PaymentCommon):
|
|||
if saisie not in ('T', 'X', 'W'):
|
||||
raise ValueError('SAISIE invalid format, %r, must be M, T, X or A' % saisie)
|
||||
|
||||
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)
|
||||
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,
|
||||
)
|
||||
|
||||
return str(idop), URL, PAYMENT_URL + '?idop=%s' % idop
|
||||
|
||||
|
@ -297,17 +325,10 @@ class Payment(PaymentCommon):
|
|||
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 == '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)
|
||||
return PaymentResponse(result=WAITING, signed=True, order_id=transaction_id)
|
||||
raise e
|
||||
return self.payfip_response_to_eopayment_response(idop, response)
|
||||
|
||||
|
@ -353,7 +374,8 @@ class Payment(PaymentCommon):
|
|||
bank_data={k: response[k] for k in response},
|
||||
order_id=idop,
|
||||
transaction_id=transaction_id,
|
||||
test=response.saisie == 'T')
|
||||
test=response.saisie == 'T',
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -366,6 +388,7 @@ if __name__ == '__main__':
|
|||
return func(*args, **kwargs)
|
||||
except PayFiPError as e:
|
||||
click.echo(click.style('PayFiP ERROR : %s "%s"' % (e.code, e.message), fg='red'))
|
||||
|
||||
return f
|
||||
|
||||
@click.group()
|
||||
|
@ -374,6 +397,7 @@ if __name__ == '__main__':
|
|||
@click.pass_context
|
||||
def main(ctx, wsdl_url, service_url):
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
# hide warning from zeep
|
||||
logging.getLogger('zeep.wsdl.bindings.soap').level = logging.ERROR
|
||||
|
@ -407,10 +431,17 @@ if __name__ == '__main__':
|
|||
@click.pass_obj
|
||||
@show_payfip_error
|
||||
def get_idop(payfip, numcli, saisie, exer, montant, refdet, mel, objet, url_notification, url_redirect):
|
||||
idop = payfip.get_idop(numcli=numcli, saisie=saisie, exer=exer,
|
||||
montant=montant, refdet=refdet, mel=mel,
|
||||
objet=objet, url_notification=url_notification,
|
||||
url_redirect=url_redirect)
|
||||
idop = payfip.get_idop(
|
||||
numcli=numcli,
|
||||
saisie=saisie,
|
||||
exer=exer,
|
||||
montant=montant,
|
||||
refdet=refdet,
|
||||
mel=mel,
|
||||
objet=objet,
|
||||
url_notification=url_notification,
|
||||
url_redirect=url_redirect,
|
||||
)
|
||||
print('idOp:', idop)
|
||||
print(PAYMENT_URL + '?idop=%s' % idop)
|
||||
|
||||
|
@ -422,6 +453,3 @@ if __name__ == '__main__':
|
|||
print(payfip.get_info_paiement(idop))
|
||||
|
||||
main()
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -23,8 +23,17 @@ import lxml.etree as ET
|
|||
import zeep
|
||||
import zeep.exceptions
|
||||
|
||||
from .common import (PaymentException, PaymentCommon, ResponseError, URL, PAID,
|
||||
DENIED, CANCELLED, ERROR, PaymentResponse)
|
||||
from .common import (
|
||||
PaymentException,
|
||||
PaymentCommon,
|
||||
ResponseError,
|
||||
URL,
|
||||
PAID,
|
||||
DENIED,
|
||||
CANCELLED,
|
||||
ERROR,
|
||||
PaymentResponse,
|
||||
)
|
||||
|
||||
_zeep_transport = None
|
||||
|
||||
|
@ -60,8 +69,17 @@ class Saga:
|
|||
raise SagaError('Invalid SAGA response "%s"' % content[:1024])
|
||||
return tree
|
||||
|
||||
def transaction(self, num_service, id_tiers, compte, lib_ecriture, montant,
|
||||
urlretour_asynchrone, email, urlretour_synchrone):
|
||||
def transaction(
|
||||
self,
|
||||
num_service,
|
||||
id_tiers,
|
||||
compte,
|
||||
lib_ecriture,
|
||||
montant,
|
||||
urlretour_asynchrone,
|
||||
email,
|
||||
urlretour_synchrone,
|
||||
):
|
||||
tree = self.soap_call(
|
||||
'Transaction',
|
||||
'url',
|
||||
|
@ -72,15 +90,13 @@ class Saga:
|
|||
montant=montant,
|
||||
urlretour_asynchrone=urlretour_asynchrone,
|
||||
email=email,
|
||||
urlretour_synchrone=urlretour_synchrone)
|
||||
urlretour_synchrone=urlretour_synchrone,
|
||||
)
|
||||
# tree == <url>...</url>
|
||||
return tree.text
|
||||
|
||||
def page_retour(self, operation, idop):
|
||||
tree = self.soap_call(
|
||||
operation,
|
||||
'ok',
|
||||
idop=idop)
|
||||
tree = self.soap_call(operation, 'ok', idop=idop)
|
||||
# tree == <ok id_tiers="A1"
|
||||
# etat="paye" email="albert,dupond@monsite.com" num_service="222222"
|
||||
# montant="100.00" compte="708"
|
||||
|
@ -125,13 +141,12 @@ class Payment(PaymentCommon):
|
|||
'caption': 'URL de notification',
|
||||
'required': False,
|
||||
},
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
@property
|
||||
def saga(self):
|
||||
return Saga(
|
||||
wsdl_url=urljoin(self.base_url, 'paiement_internet_ws_ministere?wsdl'))
|
||||
return Saga(wsdl_url=urljoin(self.base_url, 'paiement_internet_ws_ministere?wsdl'))
|
||||
|
||||
def request(self, amount, email, subject, orderid=None, **kwargs):
|
||||
num_service = self.num_service
|
||||
|
@ -150,7 +165,8 @@ class Payment(PaymentCommon):
|
|||
montant=montant,
|
||||
urlretour_asynchrone=urlretour_asynchrone,
|
||||
email=email,
|
||||
urlretour_synchrone=urlretour_synchrone)
|
||||
urlretour_synchrone=urlretour_synchrone,
|
||||
)
|
||||
|
||||
try:
|
||||
idop = parse_qs(url.split('?', 1)[-1])['idop'][0]
|
||||
|
@ -194,7 +210,8 @@ class Payment(PaymentCommon):
|
|||
bank_data=dict(response),
|
||||
order_id=idop,
|
||||
transaction_id=idop,
|
||||
test=False)
|
||||
test=False,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -207,6 +224,7 @@ if __name__ == '__main__':
|
|||
return func(*args, **kwargs)
|
||||
except SagaError as e:
|
||||
click.echo(click.style('SAGA ERROR : %s' % e, fg='red'))
|
||||
|
||||
return f
|
||||
|
||||
@click.group()
|
||||
|
@ -215,6 +233,7 @@ if __name__ == '__main__':
|
|||
@click.pass_context
|
||||
def main(ctx, wsdl_url, service_url):
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
# hide warning from zeep
|
||||
logging.getLogger('zeep.wsdl.bindings.soap').level = logging.ERROR
|
||||
|
@ -232,8 +251,17 @@ if __name__ == '__main__':
|
|||
@click.option('--urlretour-synchrone', type=str, required=True)
|
||||
@click.pass_obj
|
||||
@show_payfip_error
|
||||
def transaction(saga, num_service, id_tiers, compte, lib_ecriture, montant,
|
||||
urlretour_asynchrone, email, urlretour_synchrone):
|
||||
def transaction(
|
||||
saga,
|
||||
num_service,
|
||||
id_tiers,
|
||||
compte,
|
||||
lib_ecriture,
|
||||
montant,
|
||||
urlretour_asynchrone,
|
||||
email,
|
||||
urlretour_synchrone,
|
||||
):
|
||||
url = saga.transaction(
|
||||
num_service=num_service,
|
||||
id_tiers=id_tiers,
|
||||
|
@ -242,7 +270,8 @@ if __name__ == '__main__':
|
|||
montant=montant,
|
||||
urlretour_asynchrone=urlretour_asynchrone,
|
||||
email=email,
|
||||
urlretour_synchrone=urlretour_synchrone)
|
||||
urlretour_synchrone=urlretour_synchrone,
|
||||
)
|
||||
print('url:', url)
|
||||
|
||||
main()
|
||||
|
|
|
@ -28,14 +28,24 @@ import requests
|
|||
|
||||
import pytz
|
||||
|
||||
from .common import (PaymentCommon, FORM, Form, PaymentResponse, PAID, ERROR,
|
||||
CANCELED, ResponseError, force_text, _)
|
||||
from .common import (
|
||||
PaymentCommon,
|
||||
FORM,
|
||||
Form,
|
||||
PaymentResponse,
|
||||
PAID,
|
||||
ERROR,
|
||||
CANCELED,
|
||||
ResponseError,
|
||||
force_text,
|
||||
_,
|
||||
)
|
||||
|
||||
__all__ = ['Payment']
|
||||
|
||||
|
||||
class Payment(PaymentCommon):
|
||||
'''
|
||||
"""
|
||||
Payment backend module for the ATOS/SIPS system used by many French banks.
|
||||
|
||||
The necessary options are:
|
||||
|
@ -48,7 +58,8 @@ class Payment(PaymentCommon):
|
|||
|
||||
Worldline Benelux_Sips_Technical_integration_guide_Version_1.5.pdf
|
||||
|
||||
'''
|
||||
"""
|
||||
|
||||
has_free_transaction_id = True
|
||||
|
||||
URL = {
|
||||
|
@ -64,28 +75,27 @@ class Payment(PaymentCommon):
|
|||
RESPONSE_CODES = {
|
||||
'00': 'Authorisation accepted',
|
||||
'02': 'Authorisation request to be performed via telephone with the issuer, as the '
|
||||
'card authorisation threshold has been exceeded, if the forcing is authorised for '
|
||||
'the merchant',
|
||||
'card authorisation threshold has been exceeded, if the forcing is authorised for '
|
||||
'the merchant',
|
||||
'03': 'Invalid distance selling contract',
|
||||
'05': 'Authorisation refused',
|
||||
'12': 'Invalid transaction, verify the parameters transferred in the request.',
|
||||
'14': 'Invalid bank details or card security code',
|
||||
'17': 'Buyer cancellation',
|
||||
'24': 'Operation impossible. The operation the merchant wishes to perform is not '
|
||||
'compatible with the status of the transaction.',
|
||||
'compatible with the status of the transaction.',
|
||||
'25': 'Transaction not found in the Sips database',
|
||||
'30': 'Format error',
|
||||
'34': 'Suspicion of fraud',
|
||||
'40': 'Function not supported: the operation that the merchant would like to perform '
|
||||
'is not part of the list of operations for which the merchant is authorised',
|
||||
'is not part of the list of operations for which the merchant is authorised',
|
||||
'51': 'Amount too high',
|
||||
'54': 'Card is past expiry date',
|
||||
'60': 'Transaction pending',
|
||||
'63': 'Security rules not observed, transaction stopped',
|
||||
'75': 'Number of attempts at entering the card number exceeded',
|
||||
'90': 'Service temporarily unavailable',
|
||||
'94': 'Duplicated transaction: for a given day, the TransactionReference has already been '
|
||||
'used',
|
||||
'94': 'Duplicated transaction: for a given day, the TransactionReference has already been ' 'used',
|
||||
'97': 'Timeframe exceeded, transaction refused',
|
||||
'99': 'Temporary problem at the Sips Office Server level',
|
||||
}
|
||||
|
@ -149,23 +159,18 @@ class Payment(PaymentCommon):
|
|||
'caption': _('Capture Day'),
|
||||
'required': False,
|
||||
},
|
||||
{
|
||||
'name': 'payment_means',
|
||||
'caption': _('Payment Means'),
|
||||
'required': False
|
||||
},
|
||||
{'name': 'payment_means', 'caption': _('Payment Means'), 'required': False},
|
||||
{
|
||||
'name': 'timezone',
|
||||
'caption': _('SIPS server Timezone'),
|
||||
'default': 'Europe/Paris',
|
||||
'required': False,
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
def encode_data(self, data):
|
||||
return '|'.join('%s=%s' % (force_text(key), force_text(value))
|
||||
for key, value in data.items())
|
||||
return '|'.join('%s=%s' % (force_text(key), force_text(value)) for key, value in data.items())
|
||||
|
||||
def seal_data(self, data):
|
||||
s = self.encode_data(data)
|
||||
|
@ -192,9 +197,23 @@ class Payment(PaymentCommon):
|
|||
def get_url(self):
|
||||
return self.URL[self.platform]
|
||||
|
||||
def request(self, amount, name=None, first_name=None, last_name=None,
|
||||
address=None, email=None, phone=None, orderid=None,
|
||||
info1=None, info2=None, info3=None, next_url=None, transaction_id=None, **kwargs):
|
||||
def request(
|
||||
self,
|
||||
amount,
|
||||
name=None,
|
||||
first_name=None,
|
||||
last_name=None,
|
||||
address=None,
|
||||
email=None,
|
||||
phone=None,
|
||||
orderid=None,
|
||||
info1=None,
|
||||
info2=None,
|
||||
info3=None,
|
||||
next_url=None,
|
||||
transaction_id=None,
|
||||
**kwargs,
|
||||
):
|
||||
data = self.get_data()
|
||||
# documentation:
|
||||
# https://documentation.sips.worldline.com/fr/WLSIPS.801-MG-Presentation-generale-de-la-migration-vers-Sips-2.0.html#ariaid-title20
|
||||
|
@ -212,8 +231,10 @@ class Payment(PaymentCommon):
|
|||
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, "
|
||||
"set normal_return_url in options", DeprecationWarning)
|
||||
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:
|
||||
data['normalReturnUrl'] = normal_return_url
|
||||
|
@ -221,11 +242,7 @@ class Payment(PaymentCommon):
|
|||
url=self.get_url(),
|
||||
method='POST',
|
||||
fields=[
|
||||
{
|
||||
'type': 'hidden',
|
||||
'name': 'Data',
|
||||
'value': self.encode_data(data)
|
||||
},
|
||||
{'type': 'hidden', 'name': 'Data', 'value': self.encode_data(data)},
|
||||
{
|
||||
'type': 'hidden',
|
||||
'name': 'Seal',
|
||||
|
@ -236,7 +253,8 @@ class Payment(PaymentCommon):
|
|||
'name': 'InterfaceVersion',
|
||||
'value': self.INTERFACE_VERSION,
|
||||
},
|
||||
])
|
||||
],
|
||||
)
|
||||
self.logger.debug('emitting request %r', data)
|
||||
return transactionReference, FORM, form
|
||||
|
||||
|
@ -270,7 +288,9 @@ class Payment(PaymentCommon):
|
|||
transaction_date = None
|
||||
if 'transactionDateTime' in data:
|
||||
try:
|
||||
transaction_date = datetime.datetime.strptime(data['transactionDateTime'], '%Y-%m-%d %H:%M:%S')
|
||||
transaction_date = datetime.datetime.strptime(
|
||||
data['transactionDateTime'], '%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
else:
|
||||
|
@ -284,7 +304,8 @@ class Payment(PaymentCommon):
|
|||
transaction_id=data.get('authorisationId'),
|
||||
bank_status=self.RESPONSE_CODES.get(response_code, 'unknown code - ' + response_code),
|
||||
test=test,
|
||||
transaction_date=transaction_date)
|
||||
transaction_date=transaction_date,
|
||||
)
|
||||
|
||||
def get_seal_for_json_ws_data(self, data):
|
||||
data_to_send = []
|
||||
|
@ -293,7 +314,9 @@ class Payment(PaymentCommon):
|
|||
continue
|
||||
data_to_send.append(force_text(data[key]))
|
||||
data_to_send_str = ''.join(data_to_send).encode('utf-8')
|
||||
return hmac.new(force_text(self.secret_key).encode('utf-8'), data_to_send_str, hashlib.sha256).hexdigest()
|
||||
return hmac.new(
|
||||
force_text(self.secret_key).encode('utf-8'), data_to_send_str, hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
def perform_cash_management_operation(self, endpoint, data):
|
||||
data['merchantId'] = self.merchant_id
|
||||
|
@ -306,10 +329,8 @@ class Payment(PaymentCommon):
|
|||
response = requests.post(
|
||||
url,
|
||||
data=json.dumps(data),
|
||||
headers={
|
||||
'content-type': 'application/json',
|
||||
'accept': 'application/json'
|
||||
})
|
||||
headers={'content-type': 'application/json', 'accept': 'application/json'},
|
||||
)
|
||||
self.logger.debug('received %r', response.content)
|
||||
response.raise_for_status()
|
||||
json_response = response.json()
|
||||
|
@ -348,7 +369,8 @@ class Payment(PaymentCommon):
|
|||
headers={
|
||||
'content-type': 'application/json',
|
||||
'accept': 'application/json',
|
||||
})
|
||||
},
|
||||
)
|
||||
self.logger.debug('received %r', response.content)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
|
|
@ -25,8 +25,20 @@ import string
|
|||
from six.moves.urllib import parse as urlparse
|
||||
import warnings
|
||||
|
||||
from .common import (PaymentCommon, PaymentResponse, PAID, DENIED, CANCELLED,
|
||||
ERROR, FORM, Form, ResponseError, force_text, force_byte, _)
|
||||
from .common import (
|
||||
PaymentCommon,
|
||||
PaymentResponse,
|
||||
PAID,
|
||||
DENIED,
|
||||
CANCELLED,
|
||||
ERROR,
|
||||
FORM,
|
||||
Form,
|
||||
ResponseError,
|
||||
force_text,
|
||||
force_byte,
|
||||
_,
|
||||
)
|
||||
from .cb import translate_cb_error_code
|
||||
|
||||
__all__ = ['Payment']
|
||||
|
@ -55,8 +67,7 @@ VADS_EOPAYMENT_TRANS_ID = 'vads_ext_info_eopayment_trans_id'
|
|||
|
||||
|
||||
def isonow():
|
||||
return dt.datetime.utcnow().isoformat('T').replace('-', '') \
|
||||
.replace('T', '').replace(':', '')[:14]
|
||||
return dt.datetime.utcnow().isoformat('T').replace('-', '').replace('T', '').replace(':', '')[:14]
|
||||
|
||||
|
||||
def parse_utc(value):
|
||||
|
@ -68,9 +79,19 @@ def parse_utc(value):
|
|||
|
||||
|
||||
class Parameter:
|
||||
def __init__(self, name, ptype, code, max_length=None, length=None,
|
||||
needed=False, default=None, choices=None, description=None,
|
||||
help_text=None):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
ptype,
|
||||
code,
|
||||
max_length=None,
|
||||
length=None,
|
||||
needed=False,
|
||||
default=None,
|
||||
choices=None,
|
||||
description=None,
|
||||
help_text=None,
|
||||
):
|
||||
self.name = name
|
||||
self.ptype = ptype
|
||||
self.code = code
|
||||
|
@ -105,8 +126,9 @@ class Parameter:
|
|||
|
||||
PARAMETERS = [
|
||||
# amount as euro cents
|
||||
Parameter('vads_action_mode', None, 47, needed=True, default='INTERACTIVE',
|
||||
choices=('SILENT', 'INTERACTIVE')),
|
||||
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=''),
|
||||
# Same as 'vads_capture_delay' but matches other backend naming for
|
||||
|
@ -125,44 +147,38 @@ PARAMETERS = [
|
|||
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'),
|
||||
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="Complément d'information 1"),
|
||||
Parameter('vads_order_info2', 'an', 14, max_length=255,
|
||||
description="Complément d'information 2"),
|
||||
Parameter('vads_order_info3', 'an', 14, max_length=255,
|
||||
description="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='Liste des cartes de paiement acceptées',
|
||||
help_text='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'),
|
||||
Parameter('vads_order_info', 'an', 14, max_length=255, description="Complément d'information 1"),
|
||||
Parameter('vads_order_info2', 'an', 14, max_length=255, description="Complément d'information 2"),
|
||||
Parameter('vads_order_info3', 'an', 14, max_length=255, description="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='Liste des cartes de paiement acceptées',
|
||||
help_text='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
|
||||
Parameter('vads_payment_config', '', 7, default='SINGLE',
|
||||
choices=('SINGLE', 'MULTI'), needed=True),
|
||||
Parameter('vads_return_mode', None, 48, default='GET',
|
||||
choices=('', 'NONE', 'POST', 'GET')),
|
||||
Parameter('vads_payment_config', '', 7, default='SINGLE', choices=('SINGLE', 'MULTI'), needed=True),
|
||||
Parameter('vads_return_mode', None, 48, default='GET', choices=('', 'NONE', 'POST', 'GET')),
|
||||
Parameter('signature', 'an', None, length=40),
|
||||
Parameter('vads_site_id', 'n', 2, length=8, needed=True,
|
||||
description='Identifiant de la boutique'),
|
||||
Parameter('vads_site_id', 'n', 2, length=8, needed=True, description='Identifiant de la boutique'),
|
||||
Parameter('vads_theme_config', 'ans', 32, max_length=255),
|
||||
Parameter(VADS_TRANS_DATE, 'n', 4, length=14, needed=True,
|
||||
default=isonow),
|
||||
Parameter(VADS_TRANS_DATE, 'n', 4, length=14, needed=True, default=isonow),
|
||||
# https://paiement.systempay.fr/doc/fr-FR/form-payment/reference/vads-trans-id.html
|
||||
Parameter('vads_trans_id', 'an', 3, length=6, needed=True),
|
||||
Parameter('vads_validation_mode', 'n', 5, max_length=1, choices=('', '0', '1'),
|
||||
default=''),
|
||||
Parameter('vads_version', 'an', 1, default='V2', needed=True,
|
||||
choices=('V2',)),
|
||||
Parameter('vads_validation_mode', 'n', 5, max_length=1, choices=('', '0', '1'), default=''),
|
||||
Parameter('vads_version', 'an', 1, default='V2', needed=True, choices=('V2',)),
|
||||
Parameter('vads_url_success', 'ans', 24, max_length=1024),
|
||||
Parameter('vads_url_referral', 'ans', 26, max_length=127),
|
||||
Parameter('vads_url_refused', 'ans', 25, max_length=1024),
|
||||
|
@ -174,8 +190,7 @@ PARAMETERS = [
|
|||
Parameter(VADS_CUST_FIRST_NAME, 'ans', 104, max_length=63),
|
||||
Parameter(VADS_CUST_LAST_NAME, 'ans', 104, max_length=63),
|
||||
]
|
||||
PARAMETER_MAP = {parameter.name:
|
||||
parameter for parameter in PARAMETERS}
|
||||
PARAMETER_MAP = {parameter.name: parameter for parameter in PARAMETERS}
|
||||
|
||||
|
||||
def add_vads(kwargs):
|
||||
|
@ -194,33 +209,34 @@ def check_vads(kwargs, exclude=[]):
|
|||
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))
|
||||
raise ValueError(
|
||||
'parameter %s value %s is not of the type %s' % (name, kwargs[name], parameter.ptype)
|
||||
)
|
||||
|
||||
|
||||
class Payment(PaymentCommon):
|
||||
'''
|
||||
Produce request for and verify response from the SystemPay payment
|
||||
gateway.
|
||||
"""
|
||||
Produce request for and verify response from the SystemPay payment
|
||||
gateway.
|
||||
|
||||
>>> gw =Payment(dict(secret_test='xxx', secret_production='yyyy',
|
||||
site_id=123, ctx_mode='PRODUCTION'))
|
||||
>>> 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',
|
||||
'vads_capture_delay': '', 'vads_currency': '978', 'vads_amount': 100,
|
||||
'vads_return_mode': 'NONE'})
|
||||
>>> gw =Payment(dict(secret_test='xxx', secret_production='yyyy',
|
||||
site_id=123, ctx_mode='PRODUCTION'))
|
||||
>>> 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',
|
||||
'vads_capture_delay': '', 'vads_currency': '978', 'vads_amount': 100,
|
||||
'vads_return_mode': 'NONE'})
|
||||
|
||||
"""
|
||||
|
||||
'''
|
||||
has_free_transaction_id = True
|
||||
service_url = "https://paiement.systempay.fr/vads-payment/"
|
||||
signature_algo = 'sha1'
|
||||
|
@ -239,53 +255,71 @@ class Payment(PaymentCommon):
|
|||
'caption': _('Automatic return URL (ignored, must be set in Payzen/SystemPay backoffice)'),
|
||||
'required': False,
|
||||
},
|
||||
{'name': 'service_url',
|
||||
{
|
||||
'name': 'service_url',
|
||||
'default': service_url,
|
||||
'caption': 'URL du service de paiement',
|
||||
'help_text': 'ne pas modifier si vous ne savez pas',
|
||||
'validation': lambda x: x.startswith('http'),
|
||||
'required': True, },
|
||||
{'name': 'secret_test',
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'name': 'secret_test',
|
||||
'caption': 'Secret pour la configuration de TEST',
|
||||
'validation': lambda value: str.isalnum(value),
|
||||
'required': True, },
|
||||
{'name': 'secret_production',
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'name': 'secret_production',
|
||||
'caption': 'Secret pour la configuration de PRODUCTION',
|
||||
'validation': lambda value: str.isalnum(value), },
|
||||
{'name': 'signature_algo',
|
||||
'validation': lambda value: str.isalnum(value),
|
||||
},
|
||||
{
|
||||
'name': 'signature_algo',
|
||||
'caption': 'Algorithme de signature',
|
||||
'default': 'sha1',
|
||||
'choices': (
|
||||
('sha1', 'SHA-1'),
|
||||
('hmac_sha256', 'HMAC-SHA-256'),
|
||||
)},
|
||||
),
|
||||
},
|
||||
{
|
||||
'name': 'manual_validation',
|
||||
'caption': 'Validation manuelle',
|
||||
'type': bool,
|
||||
'default': False,
|
||||
'scope': 'transaction'
|
||||
}
|
||||
]
|
||||
'scope': 'transaction',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
for name in ('vads_ctx_mode', VADS_SITE_ID, 'vads_order_info',
|
||||
'vads_order_info2', 'vads_order_info3',
|
||||
'vads_payment_cards', 'vads_payment_config', 'capture_day'):
|
||||
for name in (
|
||||
'vads_ctx_mode',
|
||||
VADS_SITE_ID,
|
||||
'vads_order_info',
|
||||
'vads_order_info2',
|
||||
'vads_order_info3',
|
||||
'vads_payment_cards',
|
||||
'vads_payment_config',
|
||||
'capture_day',
|
||||
):
|
||||
parameter = PARAMETER_MAP[name]
|
||||
|
||||
def check_value(parameter):
|
||||
def validate(value):
|
||||
return parameter.check_value(value)
|
||||
|
||||
return validate
|
||||
|
||||
x = {'name': name,
|
||||
'caption': parameter.description or name,
|
||||
'validation': check_value(parameter),
|
||||
'default': parameter.default,
|
||||
'required': parameter.needed,
|
||||
'help_text': parameter.help_text,
|
||||
'max_length': parameter.max_length}
|
||||
x = {
|
||||
'name': name,
|
||||
'caption': parameter.description or name,
|
||||
'validation': check_value(parameter),
|
||||
'default': parameter.default,
|
||||
'required': parameter.needed,
|
||||
'help_text': parameter.help_text,
|
||||
'max_length': parameter.max_length,
|
||||
}
|
||||
description['parameters'].append(x)
|
||||
|
||||
def __init__(self, options, logger=None):
|
||||
|
@ -301,23 +335,45 @@ class Payment(PaymentCommon):
|
|||
|
||||
alphabet = string.ascii_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))
|
||||
)
|
||||
vads_trans_id = gen.choice(first_letter_alphabet) + ''.join(gen.choice(alphabet) for i in range(5))
|
||||
return vads_trans_id
|
||||
|
||||
def request(self, amount, name=None, first_name=None, last_name=None,
|
||||
address=None, email=None, phone=None, orderid=None, info1=None,
|
||||
info2=None, info3=None, next_url=None, manual_validation=None,
|
||||
transaction_id=None, **kwargs):
|
||||
'''
|
||||
Create the URL string to send a request to SystemPay
|
||||
'''
|
||||
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)
|
||||
def request(
|
||||
self,
|
||||
amount,
|
||||
name=None,
|
||||
first_name=None,
|
||||
last_name=None,
|
||||
address=None,
|
||||
email=None,
|
||||
phone=None,
|
||||
orderid=None,
|
||||
info1=None,
|
||||
info2=None,
|
||||
info3=None,
|
||||
next_url=None,
|
||||
manual_validation=None,
|
||||
transaction_id=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Create the URL string to send a request to SystemPay
|
||||
"""
|
||||
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,
|
||||
)
|
||||
# amount unit is cents
|
||||
amount = '%.0f' % (100 * amount)
|
||||
kwargs.update(add_vads({'amount': force_text(amount)}))
|
||||
|
@ -325,8 +381,10 @@ class Payment(PaymentCommon):
|
|||
raise ValueError('amount must be an integer >= 0')
|
||||
normal_return_url = self.normal_return_url
|
||||
if next_url:
|
||||
warnings.warn("passing next_url to request() is deprecated, "
|
||||
"set normal_return_url in options", DeprecationWarning)
|
||||
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:
|
||||
kwargs[VADS_URL_RETURN] = force_text(normal_return_url)
|
||||
|
@ -356,8 +414,7 @@ class Payment(PaymentCommon):
|
|||
ptype = 'an-'
|
||||
p = Parameter(name, ptype, 13, max_length=32)
|
||||
if not p.check_value(orderid):
|
||||
raise ValueError(
|
||||
'%s value %s is not of the type %s' % (name, orderid, ptype))
|
||||
raise ValueError('%s value %s is not of the type %s' % (name, orderid, ptype))
|
||||
kwargs[name] = orderid
|
||||
|
||||
vads_trans_id = self.make_vads_trans_id()
|
||||
|
@ -368,8 +425,7 @@ class Payment(PaymentCommon):
|
|||
for parameter in PARAMETERS:
|
||||
name = parameter.name
|
||||
# import default parameters from configuration
|
||||
if name not in fields \
|
||||
and name in self.options:
|
||||
if name not in fields and name in self.options:
|
||||
fields[name] = force_text(self.options[name])
|
||||
# import default parameters from module
|
||||
if name not in fields and parameter.default is not None:
|
||||
|
@ -399,7 +455,9 @@ class Payment(PaymentCommon):
|
|||
'name': force_text(field_name),
|
||||
'value': force_text(field_value),
|
||||
}
|
||||
for field_name, field_value in fields.items()])
|
||||
for field_name, field_value in fields.items()
|
||||
],
|
||||
)
|
||||
return transaction_id, FORM, form
|
||||
|
||||
RESULT_MAP = {
|
||||
|
@ -416,9 +474,11 @@ class Payment(PaymentCommon):
|
|||
'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.'},
|
||||
'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.'},
|
||||
|
@ -426,8 +486,10 @@ class Payment(PaymentCommon):
|
|||
'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.'},
|
||||
'99': {
|
||||
'message': 'Problème technique recontré par le serveur lors du traitement '
|
||||
'd\'un des contrôles locauxi.'
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
@ -471,8 +533,7 @@ class Payment(PaymentCommon):
|
|||
def response(self, query_string, **kwargs):
|
||||
fields = urlparse.parse_qs(query_string, True)
|
||||
if not set(fields) >= {SIGNATURE, VADS_CTX_MODE, VADS_AUTH_RESULT}:
|
||||
raise ResponseError('missing %s, %s or %s' % (SIGNATURE, VADS_CTX_MODE,
|
||||
VADS_AUTH_RESULT))
|
||||
raise ResponseError('missing %s, %s or %s' % (SIGNATURE, VADS_CTX_MODE, VADS_AUTH_RESULT))
|
||||
for key, value in fields.items():
|
||||
fields[key] = value[0]
|
||||
copy = fields.copy()
|
||||
|
@ -481,8 +542,7 @@ class Payment(PaymentCommon):
|
|||
self.logger.debug('checking systempay response on: %r', copy)
|
||||
signature_result = signature == fields[SIGNATURE]
|
||||
if not signature_result:
|
||||
self.logger.debug('signature check: %s <!> %s', signature,
|
||||
fields[SIGNATURE])
|
||||
self.logger.debug('signature check: %s <!> %s', signature, fields[SIGNATURE])
|
||||
|
||||
if not signature_result:
|
||||
message += ' signature invalide.'
|
||||
|
@ -510,7 +570,8 @@ class Payment(PaymentCommon):
|
|||
transaction_id=transaction_id,
|
||||
bank_status=message,
|
||||
transaction_date=transaction_date,
|
||||
test=test)
|
||||
test=test,
|
||||
)
|
||||
return response
|
||||
|
||||
def sha1_sign(self, secret, signed_data):
|
||||
|
@ -522,8 +583,7 @@ class Payment(PaymentCommon):
|
|||
|
||||
def signature(self, fields):
|
||||
self.logger.debug('got fields %s to sign' % fields)
|
||||
ordered_keys = sorted(
|
||||
key for key in fields.keys() if key.startswith('vads_'))
|
||||
ordered_keys = sorted(key for key in fields.keys() if key.startswith('vads_'))
|
||||
self.logger.debug('ordered keys %s' % ordered_keys)
|
||||
ordered_fields = [force_byte(fields[key]) for key in ordered_keys]
|
||||
secret = force_byte(getattr(self, 'secret_%s' % fields['vads_ctx_mode'].lower()))
|
||||
|
|
|
@ -21,8 +21,7 @@ import random
|
|||
|
||||
import pytz
|
||||
|
||||
from .common import (PaymentCommon, PaymentResponse, URL, PAID, DENIED,
|
||||
CANCELLED, ERROR, ResponseError, _)
|
||||
from .common import PaymentCommon, PaymentResponse, URL, PAID, DENIED, CANCELLED, ERROR, ResponseError, _
|
||||
from six.moves.urllib.parse import urlencode, parse_qs
|
||||
|
||||
import logging
|
||||
|
@ -35,10 +34,10 @@ LOGGER = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class Payment(PaymentCommon):
|
||||
'''Produce requests for and verify response from the TIPI online payment
|
||||
"""Produce requests for and verify response from the TIPI online payment
|
||||
processor from the French Finance Ministry.
|
||||
|
||||
'''
|
||||
"""
|
||||
|
||||
description = {
|
||||
'caption': 'TIPI, Titres Payables par Internet',
|
||||
|
@ -81,21 +80,34 @@ class Payment(PaymentCommon):
|
|||
maximal_amount = decimal.Decimal('100000.0')
|
||||
|
||||
def _generate_refdet(self):
|
||||
return '%s%010d' % (datetime.datetime.now(pytz.timezone('Europe/Paris')).strftime('%Y%m%d%H%M%S'),
|
||||
random.randint(1, 1000000000))
|
||||
return '%s%010d' % (
|
||||
datetime.datetime.now(pytz.timezone('Europe/Paris')).strftime('%Y%m%d%H%M%S'),
|
||||
random.randint(1, 1000000000),
|
||||
)
|
||||
|
||||
def request(self, amount, email, next_url=None, exer=None, orderid=None,
|
||||
refdet=None, objet=None, saisie=None, **kwargs):
|
||||
def request(
|
||||
self,
|
||||
amount,
|
||||
email,
|
||||
next_url=None,
|
||||
exer=None,
|
||||
orderid=None,
|
||||
refdet=None,
|
||||
objet=None,
|
||||
saisie=None,
|
||||
**kwargs,
|
||||
):
|
||||
montant = self.clean_amount(amount, max_amount=9999.99)
|
||||
|
||||
automatic_return_url = self.automatic_return_url
|
||||
if next_url and not automatic_return_url:
|
||||
warnings.warn("passing next_url to request() is deprecated, "
|
||||
"set automatic_return_url in options", DeprecationWarning)
|
||||
warnings.warn(
|
||||
"passing next_url to request() is deprecated, " "set automatic_return_url in options",
|
||||
DeprecationWarning,
|
||||
)
|
||||
automatic_return_url = next_url
|
||||
if automatic_return_url is not None:
|
||||
if (not isinstance(automatic_return_url, str)
|
||||
or not automatic_return_url.startswith('http')):
|
||||
if not isinstance(automatic_return_url, str) or not automatic_return_url.startswith('http'):
|
||||
raise ValueError('URLCL invalid URL format')
|
||||
try:
|
||||
if exer is not None:
|
||||
|
@ -189,4 +201,5 @@ class Payment(PaymentCommon):
|
|||
bank_data=fields,
|
||||
order_id=refdet,
|
||||
transaction_id=refdet,
|
||||
test=test)
|
||||
test=test,
|
||||
)
|
||||
|
|
36
setup.py
36
setup.py
|
@ -35,25 +35,23 @@ class TestCommand(distutils.core.Command):
|
|||
pass
|
||||
|
||||
def run(self):
|
||||
'''
|
||||
"""
|
||||
Finds all the tests modules in tests/, and runs them.
|
||||
'''
|
||||
"""
|
||||
testfiles = []
|
||||
for t in glob(pjoin(self._dir, 'tests', '*.py')):
|
||||
if not t.endswith('__init__.py'):
|
||||
testfiles.append('.'.join(
|
||||
['tests', splitext(basename(t))[0]])
|
||||
)
|
||||
testfiles.append('.'.join(['tests', splitext(basename(t))[0]]))
|
||||
|
||||
tests = TestLoader().loadTestsFromNames(testfiles)
|
||||
import eopayment
|
||||
|
||||
tests.addTests(doctest.DocTestSuite(eopayment))
|
||||
t = TextTestRunner(verbosity=4)
|
||||
t.run(tests)
|
||||
|
||||
|
||||
class eo_sdist(sdist):
|
||||
|
||||
def run(self):
|
||||
print("creating VERSION file")
|
||||
if os.path.exists('VERSION'):
|
||||
|
@ -69,16 +67,16 @@ class eo_sdist(sdist):
|
|||
|
||||
|
||||
def get_version():
|
||||
'''Use the VERSION, if absent generates a version with git describe, if not
|
||||
tag exists, take 0.0.0- and add the length of the commit log.
|
||||
'''
|
||||
"""Use the VERSION, if absent generates a version with git describe, if not
|
||||
tag exists, take 0.0.0- and add the length of the commit log.
|
||||
"""
|
||||
if os.path.exists('VERSION'):
|
||||
with open('VERSION') as v:
|
||||
return v.read()
|
||||
if os.path.exists('.git'):
|
||||
p = subprocess.Popen(['git', 'describe', '--dirty',
|
||||
'--match=v*'], stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
p = subprocess.Popen(
|
||||
['git', 'describe', '--dirty', '--match=v*'], stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
result = p.communicate()[0]
|
||||
if p.returncode == 0:
|
||||
result = result.decode('ascii').strip()[1:] # strip spaces/newlines and initial v
|
||||
|
@ -93,9 +91,7 @@ def get_version():
|
|||
version = result
|
||||
return version
|
||||
else:
|
||||
return '0.0.post%s' % len(
|
||||
subprocess.check_output(
|
||||
['git', 'rev-list', 'HEAD']).splitlines())
|
||||
return '0.0.post%s' % len(subprocess.check_output(['git', 'rev-list', 'HEAD']).splitlines())
|
||||
|
||||
return '0.0.0'
|
||||
|
||||
|
@ -131,13 +127,9 @@ setuptools.setup(
|
|||
name='eopayment',
|
||||
version=get_version(),
|
||||
license='GPLv3 or later',
|
||||
description='Common API to use all French online payment credit card '
|
||||
'processing services',
|
||||
description='Common API to use all French online payment credit card ' 'processing services',
|
||||
include_package_data=True,
|
||||
long_description=open(
|
||||
os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
'README.txt'), encoding='utf-8').read(),
|
||||
long_description=open(os.path.join(os.path.dirname(__file__), 'README.txt'), encoding='utf-8').read(),
|
||||
long_description_content_type='text/plain',
|
||||
url='http://dev.entrouvert.org/projects/eopayment/',
|
||||
author="Entr'ouvert",
|
||||
|
@ -168,5 +160,5 @@ setuptools.setup(
|
|||
'compile_translations': compile_translations,
|
||||
'install_lib': install_lib,
|
||||
'sdist': eo_sdist,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -71,8 +71,8 @@ def record_http_session(request):
|
|||
history = []
|
||||
|
||||
for request, response in adapter.history:
|
||||
request_content = (request.body or b'')
|
||||
response_content = (response.content or b'')
|
||||
request_content = request.body or b''
|
||||
response_content = response.content or b''
|
||||
|
||||
if is_xml_content_type(request):
|
||||
request_content = xmlindent(request_content)
|
||||
|
@ -103,6 +103,7 @@ def record_http_session(request):
|
|||
request_content = request_content.decode('utf-8')
|
||||
assert request_content == expected_request_content
|
||||
return response_content
|
||||
|
||||
with httmock.HTTMock(Mocker().mock):
|
||||
yield None
|
||||
|
||||
|
|
|
@ -37,22 +37,22 @@ def do_mock_backend(monkeypatch):
|
|||
'caption': 'Validation manuelle',
|
||||
'type': bool,
|
||||
'default': False,
|
||||
'scope': 'transaction'
|
||||
'scope': 'transaction',
|
||||
},
|
||||
{
|
||||
'name': 'global_param',
|
||||
'caption': 'Global Param',
|
||||
'type': bool,
|
||||
'default': False,
|
||||
'scope': 'global'
|
||||
'scope': 'global',
|
||||
},
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
def get_backend(*args, **kwargs):
|
||||
def backend(*args, **kwargs):
|
||||
return MockBackend
|
||||
|
||||
return backend
|
||||
|
||||
monkeypatch.setattr(eopayment, 'get_backend', get_backend)
|
||||
|
@ -62,14 +62,13 @@ def do_mock_backend(monkeypatch):
|
|||
def test_deferred_payment(monkeypatch):
|
||||
mock_backend, payment = do_mock_backend(monkeypatch)
|
||||
|
||||
capture_date = (datetime.now().date() + timedelta(days=3))
|
||||
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': '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.'):
|
||||
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
|
||||
|
@ -78,7 +77,7 @@ def test_deferred_payment(monkeypatch):
|
|||
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))
|
||||
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)
|
||||
|
@ -89,8 +88,7 @@ def test_paris_timezone(freezer, monkeypatch):
|
|||
_, 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'):
|
||||
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
|
||||
|
|
|
@ -25,7 +25,7 @@ import pytest
|
|||
def backend():
|
||||
options = {
|
||||
'automatic_notification_url': 'http://example.com/direct_notification_url',
|
||||
'origin': 'Mairie de Perpette-les-oies'
|
||||
'origin': 'Mairie de Perpette-les-oies',
|
||||
}
|
||||
return eopayment.Payment('dummy', options)
|
||||
|
||||
|
@ -33,7 +33,8 @@ def backend():
|
|||
def test_request(backend, freezer):
|
||||
freezer.move_to('2020-01-01 00:00:00+01:00')
|
||||
transaction_id, method, raw_url = backend.request(
|
||||
'10.10', capture_date=datetime.date(2020, 1, 7), subject='Repas pour 4 personnes')
|
||||
'10.10', capture_date=datetime.date(2020, 1, 7), subject='Repas pour 4 personnes'
|
||||
)
|
||||
assert transaction_id
|
||||
assert method == 1
|
||||
url = urlparse(raw_url)
|
||||
|
|
|
@ -33,9 +33,7 @@ QUERY_STRING = 'order_id=' + ORDER_ID
|
|||
|
||||
POST_ORDER_RESPONSE = {
|
||||
"amount": 995,
|
||||
"client": {
|
||||
"user_agent": "Testing API"
|
||||
},
|
||||
"client": {"user_agent": "Testing API"},
|
||||
"created": "2016-07-04T11:41:57.121017+00:00",
|
||||
"currency": "EUR",
|
||||
"description": "Example description",
|
||||
|
@ -44,14 +42,12 @@ POST_ORDER_RESPONSE = {
|
|||
"modified": "2016-07-04T11:41:57.183822+00:00",
|
||||
"order_url": "https://api.online.emspay.eu/pay/1c969951-f5f1-4290-ae41-6177961fb3cb/",
|
||||
"project_id": "1ef558ed-d77d-470d-b43b-c0f4a131bcef",
|
||||
"status": "new"
|
||||
"status": "new",
|
||||
}
|
||||
|
||||
GET_ORDER_RESPONSE = {
|
||||
"amount": 995,
|
||||
"client": {
|
||||
"user_agent": "Testing API"
|
||||
},
|
||||
"client": {"user_agent": "Testing API"},
|
||||
"created": "2016-07-04T11:41:55.635115+00:00",
|
||||
"currency": "EUR",
|
||||
"description": "Example order #1",
|
||||
|
@ -77,7 +73,7 @@ GET_ORDER_RESPONSE = {
|
|||
"id": "0c4bd0cd-f197-446b-b218-39cbeb028290",
|
||||
"noticed": "2016-07-04T11:41:55.987468+00:00",
|
||||
"occurred": "2016-07-04T11:41:55.831655+00:00",
|
||||
"source": "set_status"
|
||||
"source": "set_status",
|
||||
}
|
||||
],
|
||||
"expiration_period": "PT60M",
|
||||
|
@ -91,9 +87,9 @@ GET_ORDER_RESPONSE = {
|
|||
},
|
||||
"payment_url": "https://api.online.emspay.eu/redirect/6c81499c-14e4-4974-99e5-fe72ce019411/to/payment/",
|
||||
"project_id": "1ef558ed-d77d-470d-b43b-c0f4a131bcef",
|
||||
"status": "completed"
|
||||
"status": "completed",
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
@ -102,11 +98,13 @@ GET_ORDER_RESPONSE = {
|
|||
def add_order(url, request):
|
||||
return response(200, POST_ORDER_RESPONSE, request=request)
|
||||
|
||||
|
||||
@remember_called
|
||||
@urlmatch(scheme='https', netloc='api.online.emspay.eu', path='/v1/orders', method='GET')
|
||||
def successful_order(url, request):
|
||||
return response(200, GET_ORDER_RESPONSE, request=request)
|
||||
|
||||
|
||||
@remember_called
|
||||
@urlmatch(scheme='https', netloc='api.online.emspay.eu', path=r'/v1/orders', method='DELETE')
|
||||
def cancelled_order(url, request):
|
||||
|
@ -114,6 +112,7 @@ def cancelled_order(url, request):
|
|||
resp['status'] = 'cancelled'
|
||||
return response(200, resp, request=request)
|
||||
|
||||
|
||||
@remember_called
|
||||
@urlmatch(scheme='https', netloc='api.online.emspay.eu', path='/v1/orders')
|
||||
def error_order(url, request):
|
||||
|
@ -121,29 +120,35 @@ def error_order(url, request):
|
|||
resp['status'] = 'error'
|
||||
return response(200, resp, request=request)
|
||||
|
||||
|
||||
@remember_called
|
||||
@urlmatch(scheme='https', netloc='api.online.emspay.eu', path='/v1/orders', method='GET')
|
||||
def connection_error(url, request):
|
||||
raise requests.ConnectionError('test msg')
|
||||
|
||||
|
||||
@remember_called
|
||||
@urlmatch(scheme='https', netloc='api.online.emspay.eu', path='/v1/orders', method='GET')
|
||||
def http_error(url, request):
|
||||
error_payload = {'error': {'status': 400, 'type': 'Bad request', 'value': 'error'}}
|
||||
return response(400, error_payload, request=request)
|
||||
|
||||
|
||||
@remember_called
|
||||
@urlmatch(scheme='https', netloc='api.online.emspay.eu', path='/v1/orders', method='GET')
|
||||
def invalid_json(url, request):
|
||||
return response(200, '{', request=request)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def keyware():
|
||||
return Payment({
|
||||
'normal_return_url': RETURN_URL,
|
||||
'automatic_return_url': WEBHOOK_URL,
|
||||
'api_key': API_KEY,
|
||||
})
|
||||
return Payment(
|
||||
{
|
||||
'normal_return_url': RETURN_URL,
|
||||
'automatic_return_url': WEBHOOK_URL,
|
||||
'api_key': API_KEY,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@with_httmock(add_order)
|
||||
|
|
|
@ -36,10 +36,7 @@ POST_PAYMENTS_RESPONSE = {
|
|||
"id": PAYMENT_ID,
|
||||
"mode": "test",
|
||||
"createdAt": "2018-03-20T09:13:37+00:00",
|
||||
"amount": {
|
||||
"value": "3.50",
|
||||
"currency": "EUR"
|
||||
},
|
||||
"amount": {"value": "3.50", "currency": "EUR"},
|
||||
"description": "Payment #12345",
|
||||
"method": "null",
|
||||
"status": "open",
|
||||
|
@ -51,25 +48,16 @@ POST_PAYMENTS_RESPONSE = {
|
|||
"_links": {
|
||||
"checkout": {
|
||||
"href": "https://www.mollie.com/payscreen/select-method/7UhSN1zuXS",
|
||||
"type": "text/html"
|
||||
"type": "text/html",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
GET_PAYMENTS_RESPONSE = {
|
||||
"amount": {
|
||||
"currency": "EUR",
|
||||
"value": "3.50"
|
||||
},
|
||||
"amountRefunded": {
|
||||
"currency": "EUR",
|
||||
"value": "0.00"
|
||||
},
|
||||
"amountRemaining": {
|
||||
"currency": "EUR",
|
||||
"value": "3.50"
|
||||
},
|
||||
"amount": {"currency": "EUR", "value": "3.50"},
|
||||
"amountRefunded": {"currency": "EUR", "value": "0.00"},
|
||||
"amountRemaining": {"currency": "EUR", "value": "3.50"},
|
||||
"countryCode": "FR",
|
||||
"createdAt": "2020-05-06T13:04:26+00:00",
|
||||
"description": "Publik",
|
||||
|
@ -80,14 +68,10 @@ GET_PAYMENTS_RESPONSE = {
|
|||
"cardLabel": "Mastercard",
|
||||
"cardNumber": "6787",
|
||||
"cardSecurity": "normal",
|
||||
"feeRegion": "other"
|
||||
"feeRegion": "other",
|
||||
},
|
||||
"id": PAYMENT_ID,
|
||||
"metadata": {
|
||||
"email": "test@entrouvert.com",
|
||||
"first_name": "test",
|
||||
"last_name": "test"
|
||||
},
|
||||
"metadata": {"email": "test@entrouvert.com", "first_name": "test", "last_name": "test"},
|
||||
"isCancelable": True,
|
||||
"method": "creditcard",
|
||||
"mode": "test",
|
||||
|
@ -96,12 +80,9 @@ GET_PAYMENTS_RESPONSE = {
|
|||
"redirectUrl": "https://localhost/lingo/return-payment-backend/3/MTAw.1jWJis.6TbbjwSEurag6v4Z2VCheISBFjw/",
|
||||
"resource": "payment",
|
||||
"sequenceType": "oneoff",
|
||||
"settlementAmount": {
|
||||
"currency": "EUR",
|
||||
"value": "3.50"
|
||||
},
|
||||
"settlementAmount": {"currency": "EUR", "value": "3.50"},
|
||||
"status": "paid",
|
||||
"webhookUrl": "https://localhost/lingo/callback-payment-backend/3/"
|
||||
"webhookUrl": "https://localhost/lingo/callback-payment-backend/3/",
|
||||
}
|
||||
|
||||
|
||||
|
@ -174,11 +155,13 @@ def invalid_json(url, request):
|
|||
|
||||
@pytest.fixture
|
||||
def mollie():
|
||||
return Payment({
|
||||
'normal_return_url': RETURN_URL,
|
||||
'automatic_return_url': WEBHOOK_URL,
|
||||
'api_key': API_KEY,
|
||||
})
|
||||
return Payment(
|
||||
{
|
||||
'normal_return_url': RETURN_URL,
|
||||
'automatic_return_url': WEBHOOK_URL,
|
||||
'api_key': API_KEY,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@with_httmock(add_payment)
|
||||
|
@ -216,8 +199,9 @@ def test_mollie_response(mollie):
|
|||
|
||||
@with_httmock(successful_payment)
|
||||
def test_mollie_response_on_redirect(mollie):
|
||||
payment_response = mollie.response(query_string=None, redirect=True, order_id_hint=PAYMENT_ID,
|
||||
order_status_hint=0)
|
||||
payment_response = mollie.response(
|
||||
query_string=None, redirect=True, order_id_hint=PAYMENT_ID, order_status_hint=0
|
||||
)
|
||||
assert payment_response.result == eopayment.PAID
|
||||
|
||||
request = successful_payment.call['requests'][0]
|
||||
|
@ -225,8 +209,9 @@ def test_mollie_response_on_redirect(mollie):
|
|||
|
||||
|
||||
def test_mollie_response_on_redirect_final_status(mollie):
|
||||
payment_response = mollie.response(query_string=None, redirect=True, order_id_hint=PAYMENT_ID,
|
||||
order_status_hint=eopayment.PAID)
|
||||
payment_response = mollie.response(
|
||||
query_string=None, redirect=True, order_id_hint=PAYMENT_ID, order_status_hint=eopayment.PAID
|
||||
)
|
||||
assert payment_response.result == eopayment.PAID
|
||||
assert payment_response.order_id == PAYMENT_ID
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ def params(request):
|
|||
'pspid': PSPID,
|
||||
'sha_in': 'sécret',
|
||||
'sha_out': 'sécret',
|
||||
'automatic_return_url': 'http://example.com/autömatic_réturn_url'
|
||||
'automatic_return_url': 'http://example.com/autömatic_réturn_url',
|
||||
}
|
||||
encoding = request.param
|
||||
if encoding:
|
||||
|
@ -45,10 +45,7 @@ def test_request(params):
|
|||
ogone_backend = eopayment.Payment('ogone', params)
|
||||
amount = '42.42'
|
||||
order_id = 'my ordér'
|
||||
reference, kind, what = ogone_backend.request(
|
||||
amount=amount,
|
||||
orderid=order_id,
|
||||
email='foo@example.com')
|
||||
reference, kind, what = ogone_backend.request(amount=amount, orderid=order_id, email='foo@example.com')
|
||||
assert len(reference) == 32
|
||||
root = ET.fromstring(str(what))
|
||||
assert root.tag == 'form'
|
||||
|
@ -76,12 +73,18 @@ def test_request(params):
|
|||
def test_response(params):
|
||||
ogone_backend = eopayment.Payment('ogone', params)
|
||||
order_id = 'myorder'
|
||||
data = {'orderid': 'myorder', 'status': '9', 'payid': '3011229363',
|
||||
'cn': 'Usér', 'ncerror': '0',
|
||||
'trxdate': '10/24/16', 'acceptance': 'test123',
|
||||
'currency': 'eur', 'amount': '7.5'}
|
||||
data['shasign'] = ogone_backend.backend.sha_sign_out(
|
||||
data, encoding=params.get('encoding', 'iso-8859-1'))
|
||||
data = {
|
||||
'orderid': 'myorder',
|
||||
'status': '9',
|
||||
'payid': '3011229363',
|
||||
'cn': 'Usér',
|
||||
'ncerror': '0',
|
||||
'trxdate': '10/24/16',
|
||||
'acceptance': 'test123',
|
||||
'currency': 'eur',
|
||||
'amount': '7.5',
|
||||
}
|
||||
data['shasign'] = ogone_backend.backend.sha_sign_out(data, encoding=params.get('encoding', 'iso-8859-1'))
|
||||
# uniformize to utf-8 first
|
||||
for k in data:
|
||||
data[k] = eopayment.common.force_byte(data[k], encoding=params.get('encoding', 'iso-8859-1'))
|
||||
|
@ -96,7 +99,7 @@ def test_iso_8859_1_response():
|
|||
'pspid': PSPID,
|
||||
'sha_in': 'sécret',
|
||||
'sha_out': 'sécret',
|
||||
'automatic_return_url': 'http://example.com/autömatic_réturn_url'
|
||||
'automatic_return_url': 'http://example.com/autömatic_réturn_url',
|
||||
}
|
||||
ogone_backend = eopayment.Payment('ogone', params)
|
||||
order_id = 'lRXK4Rl1N2yIR3R5z7Kc'
|
||||
|
|
|
@ -45,25 +45,31 @@ BACKEND_PARAMS = {
|
|||
|
||||
|
||||
class PayboxTests(TestCase):
|
||||
|
||||
def test_sign(self):
|
||||
key = (b'0123456789ABCDEF0123456789ABCDEF0123456789'
|
||||
b'ABCDEF0123456789ABCDEF0123456789ABCDEF0123'
|
||||
b'456789ABCDEF0123456789ABCDEF0123456789ABCD'
|
||||
b'EF')
|
||||
key = (
|
||||
b'0123456789ABCDEF0123456789ABCDEF0123456789'
|
||||
b'ABCDEF0123456789ABCDEF0123456789ABCDEF0123'
|
||||
b'456789ABCDEF0123456789ABCDEF0123456789ABCD'
|
||||
b'EF'
|
||||
)
|
||||
key = codecs.decode(key, 'hex')
|
||||
d = dict(paybox.sign([
|
||||
['PBX_SITE', '12345678'],
|
||||
['PBX_RANG', '32'],
|
||||
['PBX_IDENTIFIANT', '12345678'],
|
||||
['PBX_TOTAL', '999'],
|
||||
['PBX_DEVISE', '978'],
|
||||
['PBX_CMD', 'appel à Paybox'],
|
||||
['PBX_PORTEUR', 'test@paybox.com'],
|
||||
['PBX_RETOUR', 'Mt:M;Ref:R;Auto:A;Erreur:E'],
|
||||
['PBX_HASH', 'SHA512'],
|
||||
['PBX_TIME', '2015-06-08T16:21:16+02:00'],
|
||||
], key))
|
||||
d = dict(
|
||||
paybox.sign(
|
||||
[
|
||||
['PBX_SITE', '12345678'],
|
||||
['PBX_RANG', '32'],
|
||||
['PBX_IDENTIFIANT', '12345678'],
|
||||
['PBX_TOTAL', '999'],
|
||||
['PBX_DEVISE', '978'],
|
||||
['PBX_CMD', 'appel à Paybox'],
|
||||
['PBX_PORTEUR', 'test@paybox.com'],
|
||||
['PBX_RETOUR', 'Mt:M;Ref:R;Auto:A;Erreur:E'],
|
||||
['PBX_HASH', 'SHA512'],
|
||||
['PBX_TIME', '2015-06-08T16:21:16+02:00'],
|
||||
],
|
||||
key,
|
||||
)
|
||||
)
|
||||
result = (
|
||||
'7E74D8E9A0DBB65AAE51C5C50C2668FD98FC99AED'
|
||||
'F18244BB1935F602B6C2E953B61FD84364F34FDB8'
|
||||
|
@ -81,8 +87,13 @@ class PayboxTests(TestCase):
|
|||
transaction = '1234'
|
||||
amount = '19.99'
|
||||
transaction_id, kind, what = backend.request(
|
||||
Decimal(amount), email=email, orderid=order_id,
|
||||
transaction_id=transaction, time=time, manual_validation=False)
|
||||
Decimal(amount),
|
||||
email=email,
|
||||
orderid=order_id,
|
||||
transaction_id=transaction,
|
||||
time=time,
|
||||
manual_validation=False,
|
||||
)
|
||||
self.assertEqual(kind, eopayment.FORM)
|
||||
self.assertEqual(transaction_id, '%s!%s' % (transaction, order_id))
|
||||
root = ET.fromstring(str(what))
|
||||
|
@ -111,7 +122,7 @@ class PayboxTests(TestCase):
|
|||
),
|
||||
'PBX_ARCHIVAGE': '20160216',
|
||||
'PBX_REPONDRE_A': 'http://example.com/callback',
|
||||
'PBX_AUTOSEULE': 'N'
|
||||
'PBX_AUTOSEULE': 'N',
|
||||
}
|
||||
|
||||
form_params = {}
|
||||
|
@ -137,12 +148,13 @@ class PayboxTests(TestCase):
|
|||
params['capture_day'] = capture_day
|
||||
backend = eopayment.Payment('paybox', params)
|
||||
transaction_id, kind, what = backend.request(
|
||||
Decimal(amount), email=email, orderid=order_id,
|
||||
transaction_id=transaction, time=time)
|
||||
Decimal(amount), email=email, orderid=order_id, transaction_id=transaction, time=time
|
||||
)
|
||||
root = ET.fromstring(str(what))
|
||||
|
||||
form_params = {
|
||||
node.attrib['name']: node.attrib['value'] for node in root if node.attrib['type'] == 'hidden'}
|
||||
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'], '07')
|
||||
|
||||
|
@ -150,13 +162,18 @@ class PayboxTests(TestCase):
|
|||
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')
|
||||
Decimal(amount),
|
||||
email=email,
|
||||
orderid=order_id,
|
||||
transaction_id=transaction,
|
||||
time=time,
|
||||
capture_day='2',
|
||||
)
|
||||
root = ET.fromstring(str(what))
|
||||
|
||||
form_params = {
|
||||
node.attrib['name']: node.attrib['value'] for node in root
|
||||
if node.attrib['type'] == 'hidden'}
|
||||
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')
|
||||
|
||||
|
@ -166,13 +183,18 @@ class PayboxTests(TestCase):
|
|||
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')
|
||||
Decimal(amount),
|
||||
email=email,
|
||||
orderid=order_id,
|
||||
transaction_id=transaction,
|
||||
time=time,
|
||||
capture_day='2',
|
||||
)
|
||||
root = ET.fromstring(str(what))
|
||||
|
||||
form_params = {
|
||||
node.attrib['name']: node.attrib['value'] for node in root
|
||||
if node.attrib['type'] == 'hidden'}
|
||||
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')
|
||||
|
||||
|
@ -187,12 +209,13 @@ class PayboxTests(TestCase):
|
|||
params['capture_mode'] = 'AUTHOR_CAPTURE'
|
||||
backend = eopayment.Payment('paybox', params)
|
||||
transaction_id, kind, what = backend.request(
|
||||
Decimal(amount), email=email, orderid=order_id,
|
||||
transaction_id=transaction, time=time)
|
||||
Decimal(amount), email=email, orderid=order_id, transaction_id=transaction, time=time
|
||||
)
|
||||
root = ET.fromstring(str(what))
|
||||
|
||||
form_params = {
|
||||
node.attrib['name']: node.attrib['value'] for node in root if node.attrib['type'] == 'hidden'}
|
||||
node.attrib['name']: node.attrib['value'] for node in root if node.attrib['type'] == 'hidden'
|
||||
}
|
||||
self.assertEqual(form_params['PBX_AUTOSEULE'], 'O')
|
||||
|
||||
def test_response(self):
|
||||
|
@ -206,7 +229,8 @@ class PayboxTests(TestCase):
|
|||
'code_autorisation': 'A',
|
||||
'erreur': '00000',
|
||||
'date_transaction': '20200101',
|
||||
'heure_transaction': '01:01:01'}
|
||||
'heure_transaction': '01:01:01',
|
||||
}
|
||||
response = backend.response(urllib.urlencode(data))
|
||||
self.assertEqual(response.order_id, reference)
|
||||
assert not response.signed
|
||||
|
@ -231,16 +255,18 @@ class PayboxTests(TestCase):
|
|||
'&SITE=1999888&RANG=32&AUTORISATION=XXXXXX&CODEREPONSE=00000'
|
||||
'&COMMENTAIRE=Demande traitée avec succès&REFABONNE=&PORTEUR='
|
||||
)
|
||||
backend_expected_response = {"CODEREPONSE": "00000",
|
||||
"RANG": "32",
|
||||
"AUTORISATION": "XXXXXX",
|
||||
"NUMTRANS": "0013989865",
|
||||
"PORTEUR": "",
|
||||
"COMMENTAIRE": "Demande traitée avec succès",
|
||||
"SITE": "1999888",
|
||||
"NUMAPPEL": "0030378572",
|
||||
"REFABONNE": "",
|
||||
"NUMQUESTION": "0013989862"}
|
||||
backend_expected_response = {
|
||||
"CODEREPONSE": "00000",
|
||||
"RANG": "32",
|
||||
"AUTORISATION": "XXXXXX",
|
||||
"NUMTRANS": "0013989865",
|
||||
"PORTEUR": "",
|
||||
"COMMENTAIRE": "Demande traitée avec succès",
|
||||
"SITE": "1999888",
|
||||
"NUMAPPEL": "0030378572",
|
||||
"REFABONNE": "",
|
||||
"NUMQUESTION": "0013989862",
|
||||
}
|
||||
|
||||
with mock.patch('eopayment.paybox.requests.post') as requests_post:
|
||||
response = mock.Mock(status_code=200, text=backend_raw_response)
|
||||
|
@ -263,7 +289,7 @@ class PayboxTests(TestCase):
|
|||
'REFERENCE': '830657461681',
|
||||
'RANG': backend.backend.rang,
|
||||
'SITE': backend.backend.site,
|
||||
'DEVISE': backend.backend.devise
|
||||
'DEVISE': backend.backend.devise,
|
||||
}
|
||||
self.assertEqual(params_sent, expected_params)
|
||||
self.assertEqual(backend_response, backend_expected_response)
|
||||
|
@ -285,7 +311,8 @@ class PayboxTests(TestCase):
|
|||
'Transaction non trouvée',
|
||||
getattr(backend, operation_name),
|
||||
Decimal('10'),
|
||||
bank_data)
|
||||
bank_data,
|
||||
)
|
||||
|
||||
def test_validate_payment(self):
|
||||
params = BACKEND_PARAMS.copy()
|
||||
|
@ -294,7 +321,7 @@ class PayboxTests(TestCase):
|
|||
bank_data = {
|
||||
'numero_transaction': ['13957441'],
|
||||
'numero_appel': ['30310733'],
|
||||
'reference': ['830657461681']
|
||||
'reference': ['830657461681'],
|
||||
}
|
||||
backend_raw_response = (
|
||||
'NUMTRANS=0013989865&NUMAPPEL=0030378572&NUMQUESTION=0013989862'
|
||||
|
@ -331,15 +358,12 @@ FBFKOZhgBJnkC+l6+XhT4aYWKaQ4ocmOMV92yjeXTE4='''
|
|||
backend = eopayment.Payment('paybox', params)
|
||||
|
||||
transaction_id, kind, what = backend.request(
|
||||
Decimal(amount),
|
||||
email=email,
|
||||
orderid=order_id,
|
||||
transaction_id=transaction,
|
||||
time=time)
|
||||
Decimal(amount), email=email, orderid=order_id, transaction_id=transaction, time=time
|
||||
)
|
||||
root = ET.fromstring(str(what))
|
||||
form_params = {
|
||||
node.attrib['name']: node.attrib['value'] for node in root
|
||||
if node.attrib['type'] == 'hidden'}
|
||||
node.attrib['name']: node.attrib['value'] for node in root if node.attrib['type'] == 'hidden'
|
||||
}
|
||||
self.assertIn('PBX_AUTOSEULE', form_params)
|
||||
self.assertEqual(form_params['PBX_AUTOSEULE'], 'N')
|
||||
|
||||
|
@ -349,11 +373,12 @@ FBFKOZhgBJnkC+l6+XhT4aYWKaQ4ocmOMV92yjeXTE4='''
|
|||
orderid=order_id,
|
||||
transaction_id=transaction,
|
||||
time=time,
|
||||
manual_validation=True)
|
||||
manual_validation=True,
|
||||
)
|
||||
root = ET.fromstring(str(what))
|
||||
form_params = {
|
||||
node.attrib['name']: node.attrib['value'] for node in root
|
||||
if node.attrib['type'] == 'hidden'}
|
||||
node.attrib['name']: node.attrib['value'] for node in root if node.attrib['type'] == 'hidden'
|
||||
}
|
||||
self.assertIn('PBX_AUTOSEULE', form_params)
|
||||
self.assertEqual(form_params['PBX_AUTOSEULE'], 'O')
|
||||
|
||||
|
@ -369,17 +394,21 @@ FBFKOZhgBJnkC+l6+XhT4aYWKaQ4ocmOMV92yjeXTE4='''
|
|||
'erreur': '00000',
|
||||
'date_transaction': '20200101',
|
||||
'heure_transaction': '01:01:01',
|
||||
'signature': 'a'}
|
||||
'signature': 'a',
|
||||
}
|
||||
with pytest.raises(eopayment.ResponseError, match='invalid signature'):
|
||||
backend.response(urllib.urlencode(data))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('name,value,result', [
|
||||
('shared_secret', '1f', True),
|
||||
('shared_secret', '1fxx', False),
|
||||
('shared_secret', '1fa', False),
|
||||
('shared_secret', '1fa2', True),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
'name,value,result',
|
||||
[
|
||||
('shared_secret', '1f', True),
|
||||
('shared_secret', '1fxx', False),
|
||||
('shared_secret', '1fa', False),
|
||||
('shared_secret', '1fa2', True),
|
||||
],
|
||||
)
|
||||
def test_param_validation(name, value, result):
|
||||
for param in paybox.Payment.description['parameters']:
|
||||
if param['name'] == name:
|
||||
|
|
|
@ -84,8 +84,7 @@ def history(history_name, request, zeep_history_plugin):
|
|||
yield None
|
||||
history_path = 'tests/data/payfip-%s.json' % history_name
|
||||
d = [
|
||||
(xmlindent(exchange['sent']['envelope']),
|
||||
xmlindent(exchange['received']['envelope']))
|
||||
(xmlindent(exchange['sent']['envelope']), xmlindent(exchange['received']['envelope']))
|
||||
for exchange in zeep_history_plugin._buffer
|
||||
]
|
||||
content = json.dumps(d)
|
||||
|
@ -104,17 +103,21 @@ def get_idop():
|
|||
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,
|
||||
})
|
||||
yield eopayment.Payment(
|
||||
'payfip_ws',
|
||||
{
|
||||
'numcli': '090909',
|
||||
'automatic_return_url': NOTIF_URL,
|
||||
'normal_return_url': REDIRECT_URL,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@httmock.urlmatch()
|
||||
def raise_on_request(url, request):
|
||||
# ensure we do not access network
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
raise RequestException('huhu')
|
||||
|
||||
|
||||
|
@ -126,8 +129,10 @@ def zeep_history_plugin():
|
|||
@pytest.fixture
|
||||
def payfip(zeep_history_plugin):
|
||||
with httmock.HTTMock(raise_on_request):
|
||||
payfip = PayFiP(wsdl_url='./eopayment/resource/PaiementSecuriseService.wsdl',
|
||||
zeep_client_kwargs={'plugins': [zeep_history_plugin]})
|
||||
payfip = PayFiP(
|
||||
wsdl_url='./eopayment/resource/PaiementSecuriseService.wsdl',
|
||||
zeep_client_kwargs={'plugins': [zeep_history_plugin]},
|
||||
)
|
||||
yield payfip
|
||||
|
||||
|
||||
|
@ -141,8 +146,10 @@ def set_history_name(name):
|
|||
def decorator(func):
|
||||
func.history_name = name
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
# pytestmark = pytest.mark.update_data
|
||||
|
||||
|
||||
|
@ -162,7 +169,8 @@ def test_get_idop_ok(history, payfip):
|
|||
objet='coucou',
|
||||
url_notification=NOTIF_URL,
|
||||
url_redirect=REDIRECT_URL,
|
||||
saisie='T')
|
||||
saisie='T',
|
||||
)
|
||||
assert result == 'cc0cb210-1cd4-11ea-8cca-0213ad91a103'
|
||||
|
||||
|
||||
|
@ -177,7 +185,8 @@ def test_get_idop_refdet_error(history, payfip):
|
|||
objet='coucou',
|
||||
url_notification='https://notif.payfip.example.com/',
|
||||
url_redirect='https://redirect.payfip.example.com/',
|
||||
saisie='T')
|
||||
saisie='T',
|
||||
)
|
||||
|
||||
|
||||
def test_get_idop_adresse_mel_incorrect(payfip, payfip_history):
|
||||
|
@ -191,7 +200,8 @@ def test_get_idop_adresse_mel_incorrect(payfip, payfip_history):
|
|||
objet='coucou',
|
||||
url_notification='https://notif.payfip.example.com/',
|
||||
url_redirect='https://redirect.payfip.example.com/',
|
||||
saisie='T')
|
||||
saisie='T',
|
||||
)
|
||||
|
||||
|
||||
def test_get_info_paiement_ok(history, payfip):
|
||||
|
@ -208,7 +218,7 @@ def test_get_info_paiement_ok(history, payfip):
|
|||
'objet': 'coucou',
|
||||
'refdet': 'EFEFAEFG',
|
||||
'resultrans': 'V',
|
||||
'saisie': 'T'
|
||||
'saisie': 'T',
|
||||
}
|
||||
|
||||
|
||||
|
@ -228,14 +238,18 @@ def test_P1_and_payment_status(history, backend):
|
|||
@set_history_name('test_get_info_paiement_P1')
|
||||
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)
|
||||
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(history, backend):
|
||||
now = datetime.datetime.now()
|
||||
response = backend.payment_status(transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=now)
|
||||
response = backend.payment_status(
|
||||
transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=now
|
||||
)
|
||||
assert response.result == eopayment.EXPIRED
|
||||
|
||||
|
||||
|
@ -243,7 +257,9 @@ def test_P1_and_payment_status_utc_naive_now(history, backend):
|
|||
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)
|
||||
response = backend.payment_status(
|
||||
transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=utc_now
|
||||
)
|
||||
assert response.result == eopayment.EXPIRED
|
||||
|
||||
|
||||
|
@ -251,7 +267,9 @@ def test_P1_and_payment_status_utc_aware_now_later(history, backend, freezer):
|
|||
def test_P1_and_payment_status_utc_naive_now_later(history, 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)
|
||||
response = backend.payment_status(
|
||||
transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=now
|
||||
)
|
||||
assert response.result == eopayment.EXPIRED
|
||||
|
||||
|
||||
|
@ -270,14 +288,18 @@ def test_P5_and_payment_status(history, payfip, backend, freezer):
|
|||
@set_history_name('test_get_info_paiement_P5')
|
||||
def test_P5_and_payment_status_utc_aware_now(history, 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)
|
||||
response = backend.payment_status(
|
||||
transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=utc_now
|
||||
)
|
||||
assert response.result == eopayment.WAITING
|
||||
|
||||
|
||||
@set_history_name('test_get_info_paiement_P5')
|
||||
def test_P5_and_payment_status_utc_naive_now(history, payfip, backend, freezer):
|
||||
now = datetime.datetime.now()
|
||||
response = backend.payment_status(transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=now)
|
||||
response = backend.payment_status(
|
||||
transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=now
|
||||
)
|
||||
assert response.result == eopayment.WAITING
|
||||
|
||||
|
||||
|
@ -285,7 +307,9 @@ def test_P5_and_payment_status_utc_naive_now(history, payfip, backend, freezer):
|
|||
def test_P5_and_payment_status_utc_aware_now_later(history, 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)
|
||||
response = backend.payment_status(
|
||||
transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=utc_now
|
||||
)
|
||||
assert response.result == eopayment.EXPIRED
|
||||
|
||||
|
||||
|
@ -293,7 +317,9 @@ def test_P5_and_payment_status_utc_aware_now_later(history, payfip, backend, fre
|
|||
def test_P5_and_payment_status_utc_naive_now_later(history, 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)
|
||||
response = backend.payment_status(
|
||||
transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=now
|
||||
)
|
||||
assert response.result == eopayment.EXPIRED
|
||||
|
||||
|
||||
|
@ -302,7 +328,8 @@ def test_payment_ok(history, payfip, backend):
|
|||
amount='10.00',
|
||||
email=MEL,
|
||||
# make test deterministic
|
||||
refdet='201912261758460053903194')
|
||||
refdet='201912261758460053903194',
|
||||
)
|
||||
|
||||
assert payment_id == 'cc0cb210-1cd4-11ea-8cca-0213ad91a103'
|
||||
assert kind == eopayment.URL
|
||||
|
@ -313,14 +340,17 @@ def test_payment_ok(history, payfip, backend):
|
|||
assert response.bank_status == 'paid CB'
|
||||
assert response.order_id == payment_id
|
||||
assert response.transaction_id == (
|
||||
'201912261758460053903194 cc0cb210-1cd4-11ea-8cca-0213ad91a103 112233445566-tip')
|
||||
'201912261758460053903194 cc0cb210-1cd4-11ea-8cca-0213ad91a103 112233445566-tip'
|
||||
)
|
||||
|
||||
|
||||
@set_history_name('test_payment_ok')
|
||||
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)
|
||||
response = backend.payment_status(
|
||||
transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=now
|
||||
)
|
||||
assert response.result == eopayment.PAID
|
||||
|
||||
|
||||
|
@ -329,7 +359,8 @@ def test_payment_denied(history, backend):
|
|||
amount='10.00',
|
||||
email=MEL,
|
||||
# make test deterministic
|
||||
refdet='201912261758460053903194')
|
||||
refdet='201912261758460053903194',
|
||||
)
|
||||
|
||||
assert payment_id == 'cc0cb210-1cd4-11ea-8cca-0213ad91a103'
|
||||
assert kind == eopayment.URL
|
||||
|
@ -346,7 +377,9 @@ def test_payment_denied(history, backend):
|
|||
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)
|
||||
response = backend.payment_status(
|
||||
transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=now
|
||||
)
|
||||
assert response.result == eopayment.DENIED
|
||||
|
||||
|
||||
|
@ -355,7 +388,8 @@ def test_payment_cancelled(history, backend):
|
|||
amount='10.00',
|
||||
email=MEL,
|
||||
# make test deterministic
|
||||
refdet='201912261758460053903194')
|
||||
refdet='201912261758460053903194',
|
||||
)
|
||||
|
||||
assert payment_id == 'cc0cb210-1cd4-11ea-8cca-0213ad91a103'
|
||||
assert kind == eopayment.URL
|
||||
|
@ -372,7 +406,9 @@ def test_payment_cancelled(history, backend):
|
|||
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)
|
||||
response = backend.payment_status(
|
||||
transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=now
|
||||
)
|
||||
assert response.result == eopayment.CANCELLED
|
||||
|
||||
|
||||
|
@ -390,7 +426,8 @@ def test_refdet_exer(get_idop, backend):
|
|||
email=MEL,
|
||||
# make test deterministic
|
||||
exer=EXER,
|
||||
refdet=REFDET)
|
||||
refdet=REFDET,
|
||||
)
|
||||
|
||||
assert payment_id == 'idop-1234'
|
||||
kwargs = get_idop.call_args[1]
|
||||
|
@ -416,7 +453,8 @@ def test_transaction_id_orderid_subject(get_idop, backend):
|
|||
exer=EXER,
|
||||
transaction_id='TR12345',
|
||||
orderid='F20190003',
|
||||
subject='Précompte famille #1234')
|
||||
subject='Précompte famille #1234',
|
||||
)
|
||||
|
||||
assert payment_id == 'idop-1234'
|
||||
kwargs = get_idop.call_args[1]
|
||||
|
@ -442,7 +480,8 @@ def test_invalid_transaction_id_valid_orderid(get_idop, backend):
|
|||
exer=EXER,
|
||||
transaction_id='TR-12345',
|
||||
orderid='F20190003',
|
||||
subject='Précompte famille #1234')
|
||||
subject='Précompte famille #1234',
|
||||
)
|
||||
|
||||
assert payment_id == 'idop-1234'
|
||||
kwargs = get_idop.call_args[1]
|
||||
|
@ -468,7 +507,8 @@ def test_invalid_transaction_id_invalid_orderid(get_idop, backend):
|
|||
exer=EXER,
|
||||
transaction_id='TR-12345',
|
||||
orderid='F/20190003',
|
||||
subject='Précompte famille #1234')
|
||||
subject='Précompte famille #1234',
|
||||
)
|
||||
|
||||
assert payment_id == 'idop-1234'
|
||||
kwargs = get_idop.call_args[1]
|
||||
|
@ -505,4 +545,5 @@ def test_request_error(payfip, backend):
|
|||
amount='10.00',
|
||||
email=MEL,
|
||||
# make test deterministic
|
||||
refdet='201912261758460053903194')
|
||||
refdet='201912261758460053903194',
|
||||
)
|
||||
|
|
|
@ -42,13 +42,20 @@ def saga(record_http_session):
|
|||
@pytest.fixture
|
||||
def backend_factory(saga, target_url):
|
||||
def factory(**kwargs):
|
||||
return eopayment.Payment('saga', dict({
|
||||
'base_url': target_url,
|
||||
'num_service': '868',
|
||||
'compte': '70688',
|
||||
'automatic_return_url': 'https://automatic.notif.url/automatic/',
|
||||
'normal_return_url': 'https://normal.notif.url/normal/',
|
||||
}, **kwargs))
|
||||
return eopayment.Payment(
|
||||
'saga',
|
||||
dict(
|
||||
{
|
||||
'base_url': target_url,
|
||||
'num_service': '868',
|
||||
'compte': '70688',
|
||||
'automatic_return_url': 'https://automatic.notif.url/automatic/',
|
||||
'normal_return_url': 'https://normal.notif.url/normal/',
|
||||
},
|
||||
**kwargs,
|
||||
),
|
||||
)
|
||||
|
||||
return factory
|
||||
|
||||
|
||||
|
@ -56,19 +63,19 @@ def test_error_parametrage(backend_factory):
|
|||
payment = backend_factory(num_service='1', compte='1')
|
||||
with pytest.raises(eopayment.PaymentException, match='Impossible de déterminer le paramétrage'):
|
||||
transaction_id, kind, url = payment.request(
|
||||
amount='10.00',
|
||||
email='john.doe@example.com',
|
||||
subject='Réservation concert XYZ numéro 1234')
|
||||
amount='10.00', email='john.doe@example.com', subject='Réservation concert XYZ numéro 1234'
|
||||
)
|
||||
|
||||
|
||||
def test_request(backend_factory):
|
||||
transaction_id, kind, url = backend_factory().request(
|
||||
amount='10.00',
|
||||
email='john.doe@example.com',
|
||||
subject='Réservation concert XYZ numéro 1234')
|
||||
amount='10.00', email='john.doe@example.com', subject='Réservation concert XYZ numéro 1234'
|
||||
)
|
||||
assert transaction_id == '347b2060-1a37-11eb-af92-0213ad91a103'
|
||||
assert kind == eopayment.URL
|
||||
assert url == 'https://www.tipi.budget.gouv.fr/tpa/paiementws.web?idop=347b2060-1a37-11eb-af92-0213ad91a103'
|
||||
assert (
|
||||
url == 'https://www.tipi.budget.gouv.fr/tpa/paiementws.web?idop=347b2060-1a37-11eb-af92-0213ad91a103'
|
||||
)
|
||||
|
||||
|
||||
def test_response(backend_factory):
|
||||
|
@ -81,7 +88,7 @@ def test_response(backend_factory):
|
|||
'montant': '10.00',
|
||||
'num_service': '868',
|
||||
'numcp': '70688',
|
||||
'numcpt_lib_ecriture': 'COUCOU'
|
||||
'numcpt_lib_ecriture': 'COUCOU',
|
||||
},
|
||||
'bank_status': 'paid',
|
||||
'order_id': '28b52f40-1ace-11eb-8ce3-0213ad91a104',
|
||||
|
@ -94,4 +101,3 @@ def test_response(backend_factory):
|
|||
}
|
||||
# Check bank_data is JSON serializable
|
||||
json.dumps(response.bank_data)
|
||||
|
||||
|
|
|
@ -21,8 +21,7 @@ import pytest
|
|||
|
||||
def test_build_request():
|
||||
backend = eopayment.Payment('sips2', {})
|
||||
transaction, f, form = backend.request(amount='12', last_name='Foo',
|
||||
first_name='Félix000000')
|
||||
transaction, f, form = backend.request(amount='12', last_name='Foo', first_name='Félix000000')
|
||||
data = [f for f in form.fields if f['name'] == 'Data']
|
||||
assert 'lix000000' not in data[0]['value']
|
||||
|
||||
|
|
|
@ -21,8 +21,7 @@ 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.systempayv2 import Payment, VADS_CUST_FIRST_NAME, VADS_CUST_LAST_NAME, PAID
|
||||
from eopayment import ResponseError
|
||||
|
||||
PARAMS = {
|
||||
|
@ -30,7 +29,7 @@ PARAMS = {
|
|||
'vads_site_id': '12345678',
|
||||
'vads_ctx_mode': 'TEST',
|
||||
'vads_trans_date': '20090501193530',
|
||||
'signature_algo': 'sha1'
|
||||
'signature_algo': 'sha1',
|
||||
}
|
||||
|
||||
|
||||
|
@ -48,12 +47,7 @@ def get_field(form, field_name):
|
|||
def test_systempayv2(caplog):
|
||||
caplog.set_level(0)
|
||||
p = Payment(PARAMS)
|
||||
data = {
|
||||
'amount': 15.24,
|
||||
'orderid': '654321',
|
||||
'first_name': 'Jean Michél',
|
||||
'last_name': 'Mihaï'
|
||||
}
|
||||
data = {'amount': 15.24, 'orderid': '654321', 'first_name': 'Jean Michél', 'last_name': 'Mihaï'}
|
||||
qs = (
|
||||
'vads_version=V2&vads_page_action=PAYMENT&vads_action_mode=INTERACTIV'
|
||||
'E&vads_payment_config=SINGLE&vads_site_id=12345678&vads_ctx_mode=TES'
|
||||
|
@ -71,16 +65,18 @@ def test_systempayv2(caplog):
|
|||
if field['name'] in (VADS_CUST_FIRST_NAME, VADS_CUST_LAST_NAME):
|
||||
assert field['value'] in ('Jean Michél', 'Mihaï')
|
||||
|
||||
response_qs = 'vads_amount=1042&vads_auth_mode=FULL&vads_auth_number=3feadf' \
|
||||
'&vads_auth_result=00&vads_capture_delay=0&vads_card_brand=CB' \
|
||||
'&vads_result=00' \
|
||||
'&vads_card_number=497010XXXXXX0000' \
|
||||
'&vads_payment_certificate=582ba2b725057618706d7a06e9e59acdbf69ff53' \
|
||||
'&vads_ctx_mode=TEST&vads_currency=978&vads_effective_amount=1042' \
|
||||
'&vads_site_id=70168983&vads_trans_date=20161013101355' \
|
||||
'&vads_trans_id=226787&vads_trans_uuid=4b5053b3b1fe4b02a07753e7a' \
|
||||
'&vads_effective_creation_date=20200330162530' \
|
||||
'&signature=c17fab393f94dc027dc029510c85d5fc46c4710f'
|
||||
response_qs = (
|
||||
'vads_amount=1042&vads_auth_mode=FULL&vads_auth_number=3feadf'
|
||||
'&vads_auth_result=00&vads_capture_delay=0&vads_card_brand=CB'
|
||||
'&vads_result=00'
|
||||
'&vads_card_number=497010XXXXXX0000'
|
||||
'&vads_payment_certificate=582ba2b725057618706d7a06e9e59acdbf69ff53'
|
||||
'&vads_ctx_mode=TEST&vads_currency=978&vads_effective_amount=1042'
|
||||
'&vads_site_id=70168983&vads_trans_date=20161013101355'
|
||||
'&vads_trans_id=226787&vads_trans_uuid=4b5053b3b1fe4b02a07753e7a'
|
||||
'&vads_effective_creation_date=20200330162530'
|
||||
'&signature=c17fab393f94dc027dc029510c85d5fc46c4710f'
|
||||
)
|
||||
response = p.response(response_qs)
|
||||
assert response.result == PAID
|
||||
assert response.signed
|
||||
|
@ -90,16 +86,18 @@ def test_systempayv2(caplog):
|
|||
PARAMS['signature_algo'] = 'hmac_sha256'
|
||||
p = Payment(PARAMS)
|
||||
assert p.signature(qs) == 'aHrJ7IzSGFa4pcYA8kh99+M/xBzoQ4Odnu3f4BUrpIA='
|
||||
response_qs = 'vads_amount=1042&vads_auth_mode=FULL&vads_auth_number=3feadf' \
|
||||
'&vads_result=00' \
|
||||
'&vads_auth_result=00&vads_capture_delay=0&vads_card_brand=CB' \
|
||||
'&vads_card_number=497010XXXXXX0000' \
|
||||
'&vads_payment_certificate=582ba2b725057618706d7a06e9e59acdbf69ff53' \
|
||||
'&vads_ctx_mode=TEST&vads_currency=978&vads_effective_amount=1042' \
|
||||
'&vads_site_id=70168983&vads_trans_date=20161013101355' \
|
||||
'&vads_trans_id=226787&vads_trans_uuid=4b5053b3b1fe4b02a07753e7a' \
|
||||
'&vads_effective_creation_date=20200330162530' \
|
||||
'&signature=Wbz3bP6R6wDvAwb2HnSiH9%2FiUUoRVCxK7mdLtCMz8Xw%3D'
|
||||
response_qs = (
|
||||
'vads_amount=1042&vads_auth_mode=FULL&vads_auth_number=3feadf'
|
||||
'&vads_result=00'
|
||||
'&vads_auth_result=00&vads_capture_delay=0&vads_card_brand=CB'
|
||||
'&vads_card_number=497010XXXXXX0000'
|
||||
'&vads_payment_certificate=582ba2b725057618706d7a06e9e59acdbf69ff53'
|
||||
'&vads_ctx_mode=TEST&vads_currency=978&vads_effective_amount=1042'
|
||||
'&vads_site_id=70168983&vads_trans_date=20161013101355'
|
||||
'&vads_trans_id=226787&vads_trans_uuid=4b5053b3b1fe4b02a07753e7a'
|
||||
'&vads_effective_creation_date=20200330162530'
|
||||
'&signature=Wbz3bP6R6wDvAwb2HnSiH9%2FiUUoRVCxK7mdLtCMz8Xw%3D'
|
||||
)
|
||||
response = p.response(response_qs)
|
||||
assert response.result == PAID
|
||||
assert response.signed
|
||||
|
@ -115,10 +113,7 @@ def test_systempayv2_deferred_payment():
|
|||
'vads_site_id': '12345678',
|
||||
'vads_ctx_mode': 'TEST',
|
||||
}
|
||||
default_data = {
|
||||
'amount': 15.24, 'orderid': '654321', 'first_name': 'John',
|
||||
'last_name': 'Doe'
|
||||
}
|
||||
default_data = {'amount': 15.24, 'orderid': '654321', 'first_name': 'John', 'last_name': 'Doe'}
|
||||
|
||||
# default vads_capture_delay used
|
||||
params = default_params.copy()
|
||||
|
@ -144,7 +139,7 @@ def test_systempayv2_deferred_payment():
|
|||
params['vads_capture_delay'] = 1
|
||||
p = eopayment.Payment('systempayv2', params)
|
||||
data = default_data.copy()
|
||||
data['capture_date'] = (datetime.now().date() + timedelta(days=4))
|
||||
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'
|
||||
|
||||
|
@ -155,10 +150,7 @@ def test_manual_validation():
|
|||
'vads_site_id': '12345678',
|
||||
'vads_ctx_mode': 'TEST',
|
||||
}
|
||||
data = {
|
||||
'amount': 15.24, 'orderid': '654321', 'first_name': 'John',
|
||||
'last_name': 'Doe'
|
||||
}
|
||||
data = {'amount': 15.24, 'orderid': '654321', 'first_name': 'John', 'last_name': 'Doe'}
|
||||
|
||||
backend = eopayment.Payment('systempayv2', params)
|
||||
transaction_id, f, form = backend.request(**data.copy())
|
||||
|
@ -172,8 +164,10 @@ def test_manual_validation():
|
|||
transaction_id, f, form = backend.request(**data.copy())
|
||||
assert get_field(form, 'vads_validation_mode')['value'] == ''
|
||||
|
||||
|
||||
FIXED_TRANSACTION_ID = '1234'
|
||||
|
||||
|
||||
def test_transaction_id_request(backend):
|
||||
transaction_id, kind, form = backend.request(10.0, transaction_id=FIXED_TRANSACTION_ID)
|
||||
assert transaction_id == FIXED_TRANSACTION_ID
|
||||
|
|
|
@ -34,7 +34,8 @@ def test_tipi():
|
|||
objet='tout a fait',
|
||||
email='info@entrouvert.com',
|
||||
urlcl='http://example.com/tipi/test',
|
||||
saisie='T')
|
||||
saisie='T',
|
||||
)
|
||||
assert eopayment.tipi.Payment.REFDET_RE.match(payment_id) is not None
|
||||
parsed_qs = parse_qs(urlparse(url).query)
|
||||
assert parsed_qs['objet'][0].startswith('tout a fait')
|
||||
|
@ -47,7 +48,8 @@ def test_tipi():
|
|||
|
||||
response = p.response(
|
||||
'objet=tout+a+fait&montant=12312&saisie=T&mel=info%40entrouvert.com'
|
||||
'&numcli=12345&exer=9999&refdet=999900000000999999&resultrans=P')
|
||||
'&numcli=12345&exer=9999&refdet=999900000000999999&resultrans=P'
|
||||
)
|
||||
assert response.signed # ...
|
||||
assert response.order_id == '999900000000999999'
|
||||
assert response.transaction_id == '999900000000999999'
|
||||
|
@ -60,10 +62,8 @@ def test_tipi():
|
|||
def test_tipi_no_orderid_no_refdet():
|
||||
p = eopayment.Payment('tipi', {'numcli': '12345'})
|
||||
payment_id, kind, url = p.request(
|
||||
amount=Decimal('123.12'),
|
||||
exer=9999,
|
||||
email='info@entrouvert.com',
|
||||
saisie='T')
|
||||
amount=Decimal('123.12'), exer=9999, email='info@entrouvert.com', saisie='T'
|
||||
)
|
||||
assert eopayment.tipi.Payment.REFDET_RE.match(payment_id) is not None
|
||||
parsed_qs = parse_qs(urlparse(url).query)
|
||||
assert 'objet' not in parsed_qs
|
||||
|
@ -72,15 +72,16 @@ def test_tipi_no_orderid_no_refdet():
|
|||
assert parsed_qs['mel'] == ['info@entrouvert.com']
|
||||
assert parsed_qs['numcli'] == ['12345']
|
||||
assert parsed_qs['exer'] == ['9999']
|
||||
assert parsed_qs['refdet'][0].startswith(datetime.datetime.now(pytz.timezone('Europe/Paris')).strftime('%Y%m%d'))
|
||||
assert parsed_qs['refdet'][0].startswith(
|
||||
datetime.datetime.now(pytz.timezone('Europe/Paris')).strftime('%Y%m%d')
|
||||
)
|
||||
|
||||
|
||||
def test_tipi_orderid_refdef_compatible():
|
||||
p = eopayment.Payment('tipi', {'numcli': '12345', 'saisie': 'A'})
|
||||
payment_id, kind, url = p.request(
|
||||
amount=Decimal('123.12'),
|
||||
email='info@entrouvert.com',
|
||||
orderid='F121212')
|
||||
amount=Decimal('123.12'), email='info@entrouvert.com', orderid='F121212'
|
||||
)
|
||||
assert eopayment.tipi.Payment.REFDET_RE.match(payment_id)
|
||||
expected_url = urlparse(eopayment.tipi.TIPI_URL)
|
||||
parsed_url = urlparse(url)
|
||||
|
@ -98,10 +99,8 @@ def test_tipi_orderid_refdef_compatible():
|
|||
def test_tipi_orderid_not_refdef_compatible():
|
||||
p = eopayment.Payment('tipi', {'numcli': '12345', 'saisie': 'A'})
|
||||
payment_id, kind, url = p.request(
|
||||
amount=Decimal('123.12'),
|
||||
email='info@entrouvert.com',
|
||||
objet='coucou',
|
||||
orderid='F12-12-12')
|
||||
amount=Decimal('123.12'), email='info@entrouvert.com', objet='coucou', orderid='F12-12-12'
|
||||
)
|
||||
assert eopayment.tipi.Payment.REFDET_RE.match(payment_id) is not None
|
||||
expected_url = urlparse(eopayment.tipi.TIPI_URL)
|
||||
parsed_url = urlparse(url)
|
||||
|
|
Loading…
Reference in New Issue