trivial: apply black (#58937)

This commit is contained in:
Serghei Mihai 2021-11-26 10:43:17 +01:00
parent f350cf3169
commit 3ee72e5336
26 changed files with 1096 additions and 768 deletions

View File

@ -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'):

View File

@ -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()

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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,
)

View File

@ -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__)

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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': 'Ladresse IP appartient à la liste grise du marchand.'},
'06': {'message': 'Le code bin appartient à la liste grise du marchand.'},
'07': {'message': 'Détection dune e-carte bleue.'},
@ -426,8 +486,10 @@ class Payment(PaymentCommon):
'09': {'message': 'Détection dune carte commerciale étrangère.'},
'14': {'message': 'Détection dune carte à autorisation systématique.'},
'30': {'message': 'Le pays de ladresse 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()))

View File

@ -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,
)

View File

@ -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,
}
},
)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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'

View File

@ -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:

View File

@ -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',
)

View File

@ -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)

View File

@ -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']

View File

@ -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

View File

@ -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)