Compare commits
79 Commits
wip/48619-
...
main
Author | SHA1 | Date |
---|---|---|
Benjamin Dauvergne | 59c38e2c90 | |
Thomas NOËL | 331f6e4a8a | |
Frédéric Péters | d69e64eb8b | |
Emmanuel Cazenave | 5749ee4266 | |
Valentin Deniaud | f0fbf9835f | |
Valentin Deniaud | 02781300dc | |
Valentin Deniaud | ac990b7920 | |
Frédéric Péters | afd9017a7c | |
Frédéric Péters | 174b902ee7 | |
Paul Marillonnet | 7c0d198ba7 | |
Agate | a193f26291 | |
Benjamin Dauvergne | d42c639a9d | |
Benjamin Dauvergne | 2649aa3d05 | |
Frédéric Péters | a8507ef299 | |
Frédéric Péters | e806db9e48 | |
Thomas NOËL | 1f781057c7 | |
Benjamin Dauvergne | 1841f1fa4a | |
Frédéric Péters | 97b6bac733 | |
Frédéric Péters | f5e05ee948 | |
Frédéric Péters | 95c59b7dea | |
Agate | 76252be8af | |
Benjamin Dauvergne | 7b73275f70 | |
Benjamin Dauvergne | fe27aeb4e5 | |
Benjamin Dauvergne | 02d1238761 | |
Serghei Mihai | 3abc468775 | |
Benjamin Dauvergne | 2f7c4374f3 | |
Frédéric Péters | 80c979113c | |
Frédéric Péters | 944e54873c | |
Benjamin Dauvergne | 7cb11a273e | |
Frédéric Péters | d43c66caa4 | |
Benjamin Dauvergne | 80c103a257 | |
Benjamin Dauvergne | ae14959c2a | |
Benjamin Dauvergne | 0608e27dfe | |
Emmanuel Cazenave | 6f9ebcd866 | |
Thomas NOËL | b0fe753a58 | |
Frédéric Péters | fd729ab30a | |
Frédéric Péters | ba6d08f8a8 | |
Frédéric Péters | 2365453f52 | |
Frédéric Péters | 4abdb75fed | |
Serghei Mihai | 2b74e7f34b | |
Serghei Mihai | 025d2320e3 | |
Serghei Mihai | 3afaa1148a | |
Serghei Mihai | 8bf4ab81c5 | |
Serghei Mihai | 766991930a | |
Serghei Mihai | 3ee72e5336 | |
Serghei Mihai | f350cf3169 | |
Serghei Mihai | caa40e7e77 | |
Serghei Mihai | 19ab05d59e | |
Serghei Mihai | 0f59994d80 | |
Benjamin Dauvergne | cc54798189 | |
Benjamin Dauvergne | 6284e0fbb3 | |
Benjamin Dauvergne | db91463687 | |
Benjamin Dauvergne | 8406ab8be8 | |
Frédéric Péters | b74ed9cde6 | |
Benjamin Dauvergne | e65214d4b1 | |
Benjamin Dauvergne | 842eeaacf0 | |
Benjamin Dauvergne | 3b5dbb6537 | |
Benjamin Dauvergne | a9516b5c64 | |
Benjamin Dauvergne | c9174c008f | |
Benjamin Dauvergne | 0c13ae109d | |
Benjamin Dauvergne | b456c19e6a | |
Benjamin Dauvergne | a546bbaf5b | |
Benjamin Dauvergne | 42644a5a71 | |
Benjamin Dauvergne | 5afe44fd60 | |
Frédéric Péters | 2acbdd4095 | |
Benjamin Dauvergne | 1f3c6826ee | |
Benjamin Dauvergne | dbb2301eb5 | |
Frédéric Péters | 715b7555f2 | |
Frédéric Péters | e826eed313 | |
Serghei Mihai | 74fc1fe43b | |
Benjamin Dauvergne | 7fcee11aa2 | |
Frédéric Péters | 62c5549eb6 | |
Frédéric Péters | 46f3145a09 | |
Frédéric Péters | 8dcae040e6 | |
Frédéric Péters | 52b90ec5c9 | |
Frédéric Péters | 064f205d93 | |
Serghei Mihai | d2bf85f4b6 | |
Frédéric Péters | c5245ed58f | |
Frédéric Péters | 6fe18d4d58 |
|
@ -1,2 +1,6 @@
|
|||
[run]
|
||||
dynamic_context = test_function
|
||||
omit = */.tox/*
|
||||
|
||||
[html]
|
||||
show_contexts = True
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
# trivial: apply pyupgrade (#58937)
|
||||
caa40e7e771b41bd1d164341a2e7f40689cbe4ba
|
||||
# trivial: apply black (#58937)
|
||||
3ee72e5336d03526c7ab297a5cf09057a6d5d1c2
|
||||
# trivial: apply isort (#58937)
|
||||
8bf4ab81c5483af946365645d15708796c832d7e
|
||||
# misc: apply double-quote-string-fixer (#79788)
|
||||
02781300dc176ec56da26763b69cb6b4ada965f1
|
|
@ -0,0 +1,22 @@
|
|||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: double-quote-string-fixer
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: ['--target-version', 'py37', '--skip-string-normalization', '--line-length', '110']
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
args: ['--profile', 'black', '--line-length', '110']
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.1.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: ['--keep-percent-format', '--py37-plus']
|
|
@ -1,4 +1,4 @@
|
|||
@Library('eo-jenkins-lib@master') import eo.Utils
|
||||
@Library('eo-jenkins-lib@main') import eo.Utils
|
||||
|
||||
pipeline {
|
||||
agent any
|
||||
|
@ -7,14 +7,32 @@ pipeline {
|
|||
steps {
|
||||
sh 'tox -rv'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
script {
|
||||
utils = new Utils()
|
||||
utils.publish_coverage('coverage.xml')
|
||||
utils.publish_coverage_native('index.html')
|
||||
}
|
||||
mergeJunitResults()
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Packaging') {
|
||||
steps {
|
||||
script {
|
||||
if (env.JOB_NAME == 'eopayment' && env.GIT_BRANCH == 'origin/master') {
|
||||
sh 'sudo -H -u eobuilder /usr/local/bin/eobuilder eopayment'
|
||||
env.SHORT_JOB_NAME=sh(
|
||||
returnStdout: true,
|
||||
// given JOB_NAME=gitea/project/PR-46, returns project
|
||||
// given JOB_NAME=project/main, returns project
|
||||
script: '''
|
||||
echo "${JOB_NAME}" | sed "s/gitea\\///" | awk -F/ '{print $1}'
|
||||
'''
|
||||
).trim()
|
||||
if (env.GIT_BRANCH == 'main' || env.GIT_BRANCH == 'origin/main') {
|
||||
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder -d bullseye,bookworm ${SHORT_JOB_NAME}"
|
||||
} else if (env.GIT_BRANCH.startsWith('hotfix/')) {
|
||||
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder --branch ${env.GIT_BRANCH} --hotfix eopayment"
|
||||
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder -d bullseye,bookworm --branch ${env.GIT_BRANCH} --hotfix ${SHORT_JOB_NAME}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,10 +43,7 @@ pipeline {
|
|||
script {
|
||||
utils = new Utils()
|
||||
utils.mail_notify(currentBuild, env, 'ci+jenkins-eopayment@entrouvert.org')
|
||||
utils.publish_coverage('coverage.xml')
|
||||
utils.publish_coverage_native('index.html')
|
||||
}
|
||||
mergeJunitResults()
|
||||
}
|
||||
success {
|
||||
cleanWs()
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
recursive-include debian *
|
||||
recursive-include tests *.py
|
||||
recursive-include eopayment/locale *.po *.mo
|
||||
include COPYING
|
||||
include VERSION
|
||||
include README.txt
|
||||
include eopayment/request
|
||||
include eopayment/response
|
||||
include eopayment/resource/PaiementSecuriseService.wsdl
|
||||
include eopayment/resource/PaiementSecuriseService1.xsd
|
||||
|
|
26
README.txt
26
README.txt
|
@ -26,8 +26,8 @@ from those services, reporting whether the transaction was successful and which
|
|||
one it was. The full content (which is specific to the service) is also
|
||||
reported for logging purpose.
|
||||
|
||||
The spplus and paybox module also depend upon the python Crypto library for DES
|
||||
decoding of the merchant key and RSA signature validation on the responses.
|
||||
The paybox module also depend upon the python Cryptodome library for RSA
|
||||
signature validation on the responses.
|
||||
|
||||
Some backends allow to specify the order and transaction ids in different
|
||||
fields, in order to allow to match them in payment system backoffice. They are:
|
||||
|
@ -179,3 +179,25 @@ You can put some configuration in ~/.config/eopayment.init ::
|
|||
order_id : 20201029093825_Vlco55
|
||||
test : True
|
||||
transaction_date : 2020-10-29 09:38:26+00:00
|
||||
|
||||
Code Style
|
||||
==========
|
||||
|
||||
black is used to format the code, using thoses parameters:
|
||||
|
||||
black --target-version py35 --skip-string-normalization --line-length 110
|
||||
|
||||
isort is used to format the imports, using those parameters:
|
||||
|
||||
isort --profile black --line-length 110
|
||||
|
||||
pyupgrade is used to automatically upgrade syntax, using those parameters:
|
||||
|
||||
pyupgrade --keep-percent-format --py37-plus
|
||||
|
||||
There is .pre-commit-config.yaml to use pre-commit to automatically run black,
|
||||
isort and pyupgrade before commits. (execute `pre-commit install` to install
|
||||
the git hook.)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
9
|
|
@ -2,40 +2,23 @@ Source: eopayment
|
|||
Section: python
|
||||
Priority: optional
|
||||
Maintainer: Frederic Peters <fpeters@debian.org>
|
||||
Build-Depends: debhelper (>= 9),
|
||||
python-all (>= 2.6),
|
||||
python-crypto,
|
||||
python-requests,
|
||||
python-setuptools (>= 0.6b3),
|
||||
python-six,
|
||||
python-tz,
|
||||
Build-Depends: debhelper-compat (= 12),
|
||||
python3-all,
|
||||
python3-crypto,
|
||||
python3-django,
|
||||
python3-requests,
|
||||
python3-setuptools,
|
||||
python3-six,
|
||||
python3-tz,
|
||||
dh-python,
|
||||
git
|
||||
Standards-Version: 3.9.1
|
||||
X-Python-Version: >= 2.7
|
||||
X-Python3-Version: >= 3.4
|
||||
Homepage: http://dev.entrouvert.org/projects/eopayment/
|
||||
|
||||
Package: python-eopayment
|
||||
Architecture: all
|
||||
Depends: ${python:Depends},
|
||||
python-zeep (>= 2.5),
|
||||
python-click
|
||||
Description: common API to access online payment services
|
||||
eopayment is a Python module to interface with French's bank credit
|
||||
card online payment services. Supported services are ATOS/SIP, SystemPay,
|
||||
and SPPLUS.
|
||||
|
||||
Package: python3-eopayment
|
||||
Architecture: all
|
||||
Depends: ${python3:Depends},
|
||||
python3-zeep (>= 2.5),
|
||||
python3-pycryptodome,
|
||||
python3-click
|
||||
Description: common API to access online payment services (Python 3)
|
||||
eopayment is a Python module to interface with French's bank credit
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
README.txt
|
|
@ -4,5 +4,5 @@ export PYBUILD_NAME=eopayment
|
|||
export PYBUILD_DISABLE=test
|
||||
|
||||
%:
|
||||
dh $@ --with python2,python3 --buildsystem=pybuild
|
||||
dh $@ --with python3 --buildsystem=pybuild
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# eopayment - online payment library
|
||||
# Copyright (C) 2011-2020 Entr'ouvert
|
||||
#
|
||||
|
@ -18,23 +17,53 @@
|
|||
import datetime
|
||||
import importlib
|
||||
import logging
|
||||
|
||||
import pytz
|
||||
|
||||
import six
|
||||
|
||||
from .common import ( # noqa: F401
|
||||
URL, HTML, FORM, RECEIVED, ACCEPTED, PAID, DENIED,
|
||||
CANCELED, CANCELLED, ERROR, WAITING, EXPIRED, force_text,
|
||||
ResponseError, PaymentException,
|
||||
ACCEPTED,
|
||||
CANCELED,
|
||||
CANCELLED,
|
||||
DENIED,
|
||||
ERROR,
|
||||
EXPIRED,
|
||||
FORM,
|
||||
HTML,
|
||||
PAID,
|
||||
RECEIVED,
|
||||
URL,
|
||||
WAITING,
|
||||
BackendNotFound,
|
||||
PaymentException,
|
||||
ResponseError,
|
||||
force_text,
|
||||
)
|
||||
|
||||
__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']
|
||||
|
||||
if six.PY3:
|
||||
__all__.extend(['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'
|
||||
|
@ -57,87 +86,87 @@ 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, 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 dict((backend, get_backend(backend)) for backend in __BACKENDS)
|
||||
"""
|
||||
return {backend: get_backend(backend) for backend in __BACKENDS}
|
||||
|
||||
|
||||
class Payment(object):
|
||||
'''
|
||||
Interface to credit card online payment servers of French banks. The
|
||||
only use case supported for now is a unique automatic payment.
|
||||
class 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.
|
||||
'''
|
||||
logger.debug(u'%r' % kwargs)
|
||||
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:
|
||||
capture_date = kwargs.pop('capture_date')
|
||||
|
@ -157,69 +186,68 @@ class Payment(object):
|
|||
# backend timezone should come from some backend configuration
|
||||
backend_tz = pytz.timezone('Europe/Paris')
|
||||
utc_tz = pytz.timezone('Etc/UTC')
|
||||
backend_trans_date = utc_tz.localize(
|
||||
datetime.datetime.utcnow()).astimezone(backend_tz)
|
||||
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.")
|
||||
raise ValueError('capture_date needs to be superior to the transaction date.')
|
||||
|
||||
kwargs['capture_day'] = force_text(capture_day)
|
||||
|
||||
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'):
|
||||
|
@ -230,11 +258,65 @@ class Payment(object):
|
|||
res.append(param)
|
||||
return res
|
||||
|
||||
def get_min_time_between_transactions(self):
|
||||
if hasattr(self.backend, 'min_time_between_transactions'):
|
||||
return self.backend.min_time_between_transactions
|
||||
return 0
|
||||
|
||||
@property
|
||||
def has_free_transaction_id(self):
|
||||
return self.backend.has_free_transaction_id
|
||||
|
||||
@property
|
||||
def has_empty_response(self):
|
||||
return self.backend.has_free_transaction_id
|
||||
|
||||
def payment_status(self, transaction_id, **kwargs):
|
||||
if not self.backend.payment_status:
|
||||
raise NotImplementedError('payment_status is not implemented on this backend')
|
||||
return self.backend.payment_status(transaction_id=transaction_id, **kwargs)
|
||||
|
||||
@property
|
||||
def has_payment_status(self):
|
||||
return hasattr(self.backend, 'payment_status')
|
||||
|
||||
def get_minimal_amount(self):
|
||||
return getattr(self.backend, 'minimal_amount', None)
|
||||
|
||||
def get_maximal_amount(self):
|
||||
return getattr(self.backend, 'maximal_amount', None)
|
||||
|
||||
@property
|
||||
def has_guess(self):
|
||||
return hasattr(self.backend.__class__, 'guess')
|
||||
|
||||
@classmethod
|
||||
def guess(self, *, method=None, query_string=None, body=None, headers=None, backends=(), **kwargs):
|
||||
'''Try to guess the kind of backend and the transaction_id given part of an HTTP response.
|
||||
|
||||
method CAN be GET or POST.
|
||||
query_string is the URL encoded query-string as a regular string.
|
||||
body is the bytes content of the response.
|
||||
headers can eventually give access to the response headers.
|
||||
backends is to limit the accepted kind of backends if the possible backends are known.
|
||||
'''
|
||||
|
||||
last_exception = None
|
||||
|
||||
for kind, backend in get_backends().items():
|
||||
if not hasattr(backend, 'guess'):
|
||||
continue
|
||||
if backends and kind not in backends:
|
||||
continue
|
||||
try:
|
||||
transaction_id = backend.guess(
|
||||
method=method, query_string=query_string, body=body, headers=headers
|
||||
)
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
continue
|
||||
if transaction_id:
|
||||
return kind, transaction_id
|
||||
if last_exception is not None:
|
||||
raise last_exception
|
||||
raise BackendNotFound
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import configparser
|
||||
import decimal
|
||||
|
@ -26,7 +25,7 @@ import tempfile
|
|||
|
||||
import click
|
||||
|
||||
from . import Payment, FORM, URL
|
||||
from . import FORM, URL, Payment
|
||||
|
||||
|
||||
def option(value):
|
||||
|
@ -68,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:
|
||||
|
@ -124,4 +123,5 @@ def response(backend, query_string, param):
|
|||
for line in formatted_value.splitlines(False):
|
||||
print(' ', line)
|
||||
|
||||
|
||||
main()
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# eopayment - online payment library
|
||||
# Copyright (C) 2011-2020 Entr'ouvert
|
||||
#
|
||||
|
@ -17,8 +16,7 @@
|
|||
|
||||
'''Responses codes emitted by EMV Card or 'Carte Bleu' in France'''
|
||||
|
||||
from . import PAID, CANCELLED, ERROR, DENIED
|
||||
|
||||
from . import CANCELLED, DENIED, ERROR, PAID
|
||||
|
||||
CB_RESPONSE_CODES = {
|
||||
'00': {'message': 'Transaction approuvée ou traitée avec succès', 'result': PAID},
|
||||
|
|
|
@ -14,22 +14,24 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os.path
|
||||
import os
|
||||
import random
|
||||
import html
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import random
|
||||
import sys
|
||||
from datetime import date
|
||||
from decimal import ROUND_DOWN, Decimal
|
||||
from gettext import gettext as _
|
||||
|
||||
import six
|
||||
try:
|
||||
if 'django' in sys.modules:
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if six.PY3:
|
||||
import html
|
||||
else:
|
||||
import cgi
|
||||
|
||||
__all__ = ['PaymentCommon', 'URL', 'HTML', 'RANDOM', 'RECEIVED', 'ACCEPTED',
|
||||
'PAID', 'ERROR', 'WAITING']
|
||||
__all__ = ['PaymentCommon', 'URL', 'HTML', 'RANDOM', 'RECEIVED', 'ACCEPTED', 'PAID', 'ERROR', 'WAITING']
|
||||
|
||||
|
||||
RANDOM = random.SystemRandom()
|
||||
|
@ -53,23 +55,18 @@ ORDERID_TRANSACTION_SEPARATOR = '!'
|
|||
|
||||
|
||||
def force_text(s, encoding='utf-8'):
|
||||
if issubclass(type(s), six.text_type):
|
||||
if issubclass(type(s), str):
|
||||
return s
|
||||
try:
|
||||
if not issubclass(type(s), six.string_types):
|
||||
if six.PY3:
|
||||
if isinstance(s, bytes):
|
||||
s = six.text_type(s, encoding)
|
||||
else:
|
||||
s = six.text_type(s)
|
||||
elif hasattr(s, '__unicode__'):
|
||||
s = six.text_type(s)
|
||||
if not issubclass(type(s), str):
|
||||
if isinstance(s, bytes):
|
||||
s = str(s, encoding)
|
||||
else:
|
||||
s = six.text_type(bytes(s), encoding)
|
||||
s = str(s)
|
||||
else:
|
||||
s = s.decode(encoding)
|
||||
except UnicodeDecodeError:
|
||||
return six.text_type(s, encoding, 'ignore')
|
||||
return str(s, encoding, 'ignore')
|
||||
return s
|
||||
|
||||
|
||||
|
@ -90,30 +87,43 @@ class ResponseError(PaymentException):
|
|||
pass
|
||||
|
||||
|
||||
class PaymentResponse(object):
|
||||
'''Holds a generic view on the result of payment transaction response.
|
||||
class BackendNotFound(PaymentException):
|
||||
pass
|
||||
|
||||
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):
|
||||
class PaymentResponse:
|
||||
"""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.
|
||||
"""
|
||||
|
||||
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
|
||||
|
@ -140,8 +150,9 @@ class PaymentResponse(object):
|
|||
return self.result == ERROR
|
||||
|
||||
|
||||
class PaymentCommon(object):
|
||||
class PaymentCommon:
|
||||
has_free_transaction_id = False
|
||||
has_empty_response = False
|
||||
|
||||
PATH = '/tmp'
|
||||
BANK_ID = '__bank_id'
|
||||
|
@ -160,11 +171,9 @@ class PaymentCommon(object):
|
|||
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:
|
||||
|
@ -176,21 +185,21 @@ class PaymentCommon(object):
|
|||
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
|
||||
amount = amount.to_integral_value(ROUND_DOWN)
|
||||
return str(amount)
|
||||
|
||||
payment_status = None
|
||||
|
||||
|
||||
class Form(object):
|
||||
def __init__(self, url, method, fields, encoding='utf-8',
|
||||
submit_name='Submit', submit_value='Submit'):
|
||||
class Form:
|
||||
def __init__(self, url, method, fields, encoding='utf-8', submit_name='Submit', submit_value='Submit'):
|
||||
self.url = url
|
||||
self.method = method
|
||||
self.fields = fields
|
||||
|
@ -208,17 +217,16 @@ class Form(object):
|
|||
return s
|
||||
|
||||
def escape(self, s):
|
||||
if six.PY3:
|
||||
return html.escape(force_text(s, self.encoding))
|
||||
else:
|
||||
return cgi.escape(force_text(s, self.encoding)).encode(self.encoding)
|
||||
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)
|
||||
|
|
|
@ -17,79 +17,67 @@
|
|||
import logging
|
||||
import uuid
|
||||
import warnings
|
||||
from urllib.parse import parse_qs, urlencode
|
||||
|
||||
from six.moves.urllib.parse import parse_qs, urlencode
|
||||
|
||||
from .common import (
|
||||
PaymentCommon,
|
||||
PaymentResponse,
|
||||
ResponseError,
|
||||
URL,
|
||||
PAID, ERROR, WAITING,
|
||||
force_text
|
||||
)
|
||||
from .common import ERROR, PAID, URL, WAITING, PaymentCommon, PaymentResponse, ResponseError, _, force_text
|
||||
|
||||
__all__ = ['Payment']
|
||||
|
||||
|
||||
def N_(message):
|
||||
return message
|
||||
|
||||
|
||||
SERVICE_URL = 'http://dummy-payment.demo.entrouvert.com/'
|
||||
SERVICE_URL = 'https://dummy-payment.entrouvert.com/'
|
||||
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/
|
||||
https://dummy-payment.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': [
|
||||
{
|
||||
'name': 'normal_return_url',
|
||||
'caption': N_('Normal return URL'),
|
||||
'caption': _('Normal return URL'),
|
||||
'default': '',
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'name': 'automatic_return_url',
|
||||
'caption': N_('Automatic return URL'),
|
||||
'caption': _('Automatic return URL'),
|
||||
'required': False,
|
||||
},
|
||||
{
|
||||
'name': 'dummy_service_url',
|
||||
'caption': 'URL of the dummy payment service',
|
||||
'caption': _('URL of the dummy payment service'),
|
||||
'default': SERVICE_URL,
|
||||
'type': str,
|
||||
},
|
||||
{
|
||||
'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',
|
||||
},
|
||||
{
|
||||
'name': 'consider_all_response_signed',
|
||||
'caption': (
|
||||
'caption': _(
|
||||
'All response will be considered as signed '
|
||||
'(to test payment locally for example, as you '
|
||||
'cannot received the signed callback)'
|
||||
|
@ -115,13 +103,13 @@ class Payment(PaymentCommon):
|
|||
},
|
||||
{
|
||||
'name': 'direct_notification_url',
|
||||
'caption': 'direct notification url (replaced by automatic_return_url)',
|
||||
'caption': _('direct notification url (replaced by automatic_return_url)'),
|
||||
'type': str,
|
||||
'deprecated': True,
|
||||
},
|
||||
{
|
||||
'name': 'next_url (replaced by normal_return_url)',
|
||||
'caption': 'Return URL for the user',
|
||||
'caption': _('Return URL for the user'),
|
||||
'type': str,
|
||||
'deprecated': True,
|
||||
},
|
||||
|
@ -132,23 +120,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,
|
||||
|
@ -156,11 +172,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:
|
||||
|
@ -196,7 +221,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):
|
||||
|
@ -204,3 +230,9 @@ class Payment(PaymentCommon):
|
|||
|
||||
def cancel(self, amount, bank_data, **kwargs):
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def guess(self, *, method=None, query_string=None, body=None, headers=None, backends=(), **kwargs):
|
||||
qs = parse_qs(force_text(query_string))
|
||||
if set(qs.keys()) >= {'transaction_id', 'signed'}:
|
||||
return qs['transaction_id'][0]
|
||||
|
|
|
@ -14,23 +14,33 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from gettext import gettext as _
|
||||
from urllib.parse import parse_qs, urljoin
|
||||
|
||||
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 = {
|
||||
'caption': 'Keyware payment backend',
|
||||
'caption': _('Keyware payment backend'),
|
||||
'parameters': [
|
||||
{
|
||||
'name': 'normal_return_url',
|
||||
|
@ -69,7 +79,7 @@ class Payment(PaymentCommon):
|
|||
'email_address': email,
|
||||
'first_name': first_name,
|
||||
'last_name': last_name,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
resp = self.call_endpoint('POST', 'orders', data=body)
|
||||
|
@ -100,7 +110,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
|
||||
|
||||
|
@ -122,12 +132,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
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
# French translation of eopayment
|
||||
# Copyright (C) 2011-2020 Entr'ouvert
|
||||
# This file is distributed under the same license as the eopayment package.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: eopayment 0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-20 14:06+0100\n"
|
||||
"PO-Revision-Date: 2021-05-06 17:12+0200\n"
|
||||
"Last-Translator: Frederic Peters <fpeters@entrouvert.com>\n"
|
||||
"Language: French\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: eopayment/dummy.py eopayment/keyware.py eopayment/mollie.py
|
||||
#: eopayment/ogone.py eopayment/paybox.py eopayment/sips2.py
|
||||
#: eopayment/systempayv2.py
|
||||
msgid "Normal return URL"
|
||||
msgstr "URL de retour normal"
|
||||
|
||||
#: eopayment/dummy.py eopayment/paybox.py eopayment/sips2.py eopayment/tipi.py
|
||||
msgid "Automatic return URL"
|
||||
msgstr "URL de retour automatique"
|
||||
|
||||
#: eopayment/dummy.py
|
||||
msgid "URL of the dummy payment service"
|
||||
msgstr "URL du service de paiement bouchon"
|
||||
|
||||
#: eopayment/dummy.py
|
||||
msgid "name of the requesting service, to present in the user interface"
|
||||
msgstr "nom du service appelant qui sera affiché dans l’interface utilisateur"
|
||||
|
||||
#: eopayment/dummy.py
|
||||
msgid ""
|
||||
"All response will be considered as signed (to test payment locally for "
|
||||
"example, as you cannot received the signed callback)"
|
||||
msgstr ""
|
||||
"Toutes les réponses seront considérées comme signées (utile pour tester le "
|
||||
"paiement en local où le retour signé ne peut pas être obtenu)"
|
||||
|
||||
#: eopayment/dummy.py
|
||||
msgid "direct notification url (replaced by automatic_return_url)"
|
||||
msgstr "URL de notification directe (remplacée par automatic_return_url)"
|
||||
|
||||
#: eopayment/dummy.py
|
||||
msgid "Return URL for the user"
|
||||
msgstr "URL de retour pour l’usager"
|
||||
|
||||
#: eopayment/keyware.py
|
||||
msgid "Keyware payment backend"
|
||||
msgstr "Service de paiement Keyware"
|
||||
|
||||
#: eopayment/keyware.py eopayment/mollie.py
|
||||
msgid "Asychronous return URL"
|
||||
msgstr "URL de retour asynchrone"
|
||||
|
||||
#: eopayment/keyware.py eopayment/mollie.py
|
||||
msgid "URL of the payment service"
|
||||
msgstr "URL du service de paiement"
|
||||
|
||||
#: eopayment/keyware.py eopayment/mollie.py
|
||||
msgid "API key"
|
||||
msgstr "Clé d’API"
|
||||
|
||||
#: eopayment/mollie.py
|
||||
msgid "General description that will be displayed for all payments"
|
||||
msgstr "Description générale qui sera affichée pour tous les paiements"
|
||||
|
||||
#: eopayment/ogone.py
|
||||
msgid "Ogone / Ingenico Payment System e-Commerce"
|
||||
msgstr "Système de paiement Ogone/Ingenico"
|
||||
|
||||
#: eopayment/ogone.py
|
||||
msgid "Automatic return URL (ignored, must be set in Ogone backoffice)"
|
||||
msgstr ""
|
||||
"URL de retour automatique (ignorée, doit être posée dans le backoffice Ogone)"
|
||||
|
||||
#: eopayment/ogone.py
|
||||
msgid "Language"
|
||||
msgstr "Langue"
|
||||
|
||||
#: eopayment/ogone.py
|
||||
msgid "Characters encoding"
|
||||
msgstr "Table d'encodage des caractères (Latin1 ou UTF-8)"
|
||||
|
||||
#: eopayment/paybox.py
|
||||
msgid "Paybox"
|
||||
msgstr "Paybox"
|
||||
|
||||
#: eopayment/paybox.py
|
||||
msgid "Callback URL"
|
||||
msgstr "URL de retour"
|
||||
|
||||
#: eopayment/paybox.py eopayment/sips2.py
|
||||
msgid "Capture Mode"
|
||||
msgstr "Mode de capture"
|
||||
|
||||
#: eopayment/paybox.py
|
||||
msgid "Default Timezone"
|
||||
msgstr "Fuseau horaire par défaut"
|
||||
|
||||
#: eopayment/payfip_ws.py eopayment/tipi.py
|
||||
msgid "Client number"
|
||||
msgstr "Numéro de clients"
|
||||
|
||||
#: eopayment/payfip_ws.py eopayment/tipi.py
|
||||
msgid "6 digits number provided by DGFIP"
|
||||
msgstr "6 chiffres fournis par la DGFIP"
|
||||
|
||||
#: eopayment/payfip_ws.py eopayment/tipi.py
|
||||
msgid "Payment type"
|
||||
msgstr "Type de paiement"
|
||||
|
||||
#: eopayment/payfip_ws.py eopayment/tipi.py
|
||||
msgid "test"
|
||||
msgstr "test"
|
||||
|
||||
#: eopayment/payfip_ws.py eopayment/tipi.py
|
||||
msgid "activation"
|
||||
msgstr "activation"
|
||||
|
||||
#: eopayment/payfip_ws.py eopayment/tipi.py
|
||||
msgid "production"
|
||||
msgstr "production"
|
||||
|
||||
#: eopayment/payfip_ws.py
|
||||
msgid "User return URL"
|
||||
msgstr "URL de retour usager"
|
||||
|
||||
#: eopayment/payfip_ws.py
|
||||
msgid "Asynchronous return URL"
|
||||
msgstr "URL de retour asynchrone"
|
||||
|
||||
#: eopayment/sips2.py
|
||||
msgid "Platform"
|
||||
msgstr "Plateforme"
|
||||
|
||||
#: eopayment/sips2.py
|
||||
msgid "Merchant ID"
|
||||
msgstr "Identifiant de marchand"
|
||||
|
||||
#: eopayment/sips2.py
|
||||
msgid "Secret Key"
|
||||
msgstr "Clé secrète"
|
||||
|
||||
#: eopayment/sips2.py
|
||||
msgid "Key Version"
|
||||
msgstr "Version de clé"
|
||||
|
||||
#: eopayment/sips2.py
|
||||
msgid "Currency code"
|
||||
msgstr "Code de la devise"
|
||||
|
||||
#: eopayment/sips2.py
|
||||
msgid "Capture Day"
|
||||
msgstr "Jour de capture"
|
||||
|
||||
#: eopayment/sips2.py
|
||||
msgid "Payment Means"
|
||||
msgstr "Méthodes de paiement"
|
||||
|
||||
#: eopayment/sips2.py
|
||||
msgid "SIPS server Timezone"
|
||||
msgstr "Fuseau horaire du serveur SIPS"
|
||||
|
||||
#: eopayment/systempayv2.py
|
||||
msgid ""
|
||||
"Automatic return URL (ignored, must be set in Payzen/SystemPay backoffice)"
|
||||
msgstr ""
|
||||
"URL de retour automatique (ignorée, doit être posée dans le backoffice "
|
||||
"Payzen/SystemPay)"
|
||||
|
||||
#: eopayment/tipi.py
|
||||
msgid "Normal return URL (unused by TIPI)"
|
||||
msgstr "URL de retour normal (pas utilisée par TIPI)"
|
|
@ -14,20 +14,32 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from gettext import gettext as _
|
||||
from urllib.parse import parse_qs, urljoin
|
||||
|
||||
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 (
|
||||
ACCEPTED,
|
||||
CANCELLED,
|
||||
ERROR,
|
||||
PAID,
|
||||
URL,
|
||||
WAITING,
|
||||
PaymentCommon,
|
||||
PaymentException,
|
||||
PaymentResponse,
|
||||
ResponseError,
|
||||
_,
|
||||
)
|
||||
|
||||
__all__ = ['Payment']
|
||||
|
||||
|
||||
class Payment(PaymentCommon):
|
||||
'''Implements Mollie API, see https://docs.mollie.com/reference/v2/.'''
|
||||
|
||||
has_empty_response = True
|
||||
|
||||
service_url = 'https://api.mollie.com/v2/'
|
||||
|
||||
description = {
|
||||
|
@ -66,9 +78,12 @@ class Payment(PaymentCommon):
|
|||
|
||||
def request(self, amount, **kwargs):
|
||||
amount = self.clean_amount(amount, cents=False)
|
||||
orderid = kwargs.pop('orderid', None)
|
||||
subject = kwargs.pop('subject', None)
|
||||
|
||||
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,
|
||||
|
@ -79,13 +94,17 @@ class Payment(PaymentCommon):
|
|||
'metadata': metadata,
|
||||
'description': self.description_text,
|
||||
}
|
||||
if orderid is not None:
|
||||
body['description'] = orderid
|
||||
metadata['orderid'] = orderid
|
||||
if subject is not None:
|
||||
body['description'] = subject
|
||||
|
||||
resp = self.call_endpoint('POST', 'payments', data=body)
|
||||
|
||||
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 +137,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 +152,28 @@ 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
|
||||
|
||||
@classmethod
|
||||
def guess(self, *, method=None, query_string=None, body=None, headers=None, backends=(), **kwargs):
|
||||
for content in [query_string, body]:
|
||||
if isinstance(content, bytes):
|
||||
try:
|
||||
content = content.decode()
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
if isinstance(content, str):
|
||||
fields = parse_qs(content)
|
||||
if set(fields) == {'id'}:
|
||||
return fields['id'][0]
|
||||
return None
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# eopayment - online payment library
|
||||
# Copyright (C) 2011-2020 Entr'ouvert
|
||||
#
|
||||
|
@ -16,21 +15,28 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import hashlib
|
||||
import string
|
||||
import six
|
||||
from six.moves.urllib import parse as urlparse
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
import uuid
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
from urllib import parse as urlparse
|
||||
|
||||
from .common import (
|
||||
PaymentCommon, PaymentResponse, FORM, CANCELLED, PAID,
|
||||
ERROR, Form, DENIED, ACCEPTED, ORDERID_TRANSACTION_SEPARATOR,
|
||||
WAITING, ResponseError, force_byte, force_text
|
||||
ACCEPTED,
|
||||
CANCELLED,
|
||||
DENIED,
|
||||
ERROR,
|
||||
FORM,
|
||||
ORDERID_TRANSACTION_SEPARATOR,
|
||||
PAID,
|
||||
WAITING,
|
||||
Form,
|
||||
PaymentCommon,
|
||||
PaymentResponse,
|
||||
ResponseError,
|
||||
_,
|
||||
force_byte,
|
||||
force_text,
|
||||
)
|
||||
|
||||
|
||||
def N_(message):
|
||||
return message
|
||||
|
||||
ENVIRONMENT_TEST = 'TEST'
|
||||
ENVIRONMENT_TEST_URL = 'https://secure.ogone.com/ncol/test/orderstandard.asp'
|
||||
ENVIRONMENT_PROD = 'PROD'
|
||||
|
@ -434,77 +440,91 @@ VC
|
|||
class Payment(PaymentCommon):
|
||||
# See http://payment-services.ingenico.com/fr/fr/ogone/support/guides/integration%20guides/e-commerce
|
||||
description = {
|
||||
'caption': N_('Système de paiement Ogone / Ingenico Payment System e-Commerce'),
|
||||
'caption': _('Ogone / Ingenico Payment System e-Commerce'),
|
||||
'parameters': [
|
||||
{
|
||||
'name': 'normal_return_url',
|
||||
'caption': N_('Normal return URL'),
|
||||
'caption': _('Normal return URL'),
|
||||
'default': '',
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'name': 'automatic_return_url',
|
||||
'caption': N_('Automatic return URL (ignored, must be set in Ogone backoffice)'),
|
||||
'caption': _('Automatic return URL (ignored, must be set in Ogone backoffice)'),
|
||||
'required': False,
|
||||
},
|
||||
{
|
||||
'name': 'environment',
|
||||
'default': ENVIRONMENT_TEST,
|
||||
'caption': N_(u'Environnement'),
|
||||
'caption': 'Environnement',
|
||||
'choices': ENVIRONMENT,
|
||||
},
|
||||
{
|
||||
'name': 'pspid',
|
||||
'caption': N_(u"Nom d'affiliation dans le système"),
|
||||
'caption': "Nom d'affiliation dans le système",
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'name': 'language',
|
||||
'caption': N_(u'Langage'),
|
||||
'caption': _('Language'),
|
||||
'default': 'fr_FR',
|
||||
'choices': (('fr_FR', N_('français')),),
|
||||
'choices': (('fr_FR', 'français'),),
|
||||
},
|
||||
{
|
||||
'name': 'encoding',
|
||||
'caption': _('Characters encoding'),
|
||||
'default': 'utf-8',
|
||||
'choices': [
|
||||
('iso-8859-1', 'Latin1 (ISO-8859-1)'),
|
||||
('utf-8', 'Unicode (UTF-8)'),
|
||||
],
|
||||
},
|
||||
{
|
||||
'name': 'hash_algorithm',
|
||||
'caption': N_(u'Algorithme de hachage'),
|
||||
'caption': 'Algorithme de hachage',
|
||||
'default': 'sha1',
|
||||
},
|
||||
{
|
||||
'name': 'sha_in',
|
||||
'caption': N_(u'Clé SHA-IN'),
|
||||
'caption': 'Clé SHA-IN',
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'name': 'sha_out',
|
||||
'caption': N_(u'Clé SHA-OUT'),
|
||||
'caption': 'Clé SHA-OUT',
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'name': 'currency',
|
||||
'caption': N_(u'Monnaie'),
|
||||
'caption': 'Monnaie',
|
||||
'default': 'EUR',
|
||||
'choices': ('EUR',),
|
||||
},
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
def sha_sign(self, algo, key, params, keep):
|
||||
def __init__(self, options, logger=None):
|
||||
# retro-compatibility with old default of latin1
|
||||
options.setdefault('encoding', 'iso-8859-1')
|
||||
super().__init__(options, logger=logger)
|
||||
|
||||
def sha_sign(self, algo, key, params, keep, encoding='iso-8859-1'):
|
||||
'''Ogone signature algorithm of query string'''
|
||||
values = params.items()
|
||||
values = [(a.upper(), b) for a, b in values]
|
||||
values = sorted(values)
|
||||
values = [u'%s=%s' % (a, b) for a, b in values if a in keep and b]
|
||||
values = ['%s=%s' % (a, b) for a, b in values if a in keep and b]
|
||||
tosign = key.join(values)
|
||||
tosign += key
|
||||
tosign = force_byte(tosign, encoding='iso-8859-1')
|
||||
tosign = force_byte(tosign, encoding=encoding)
|
||||
hashing = getattr(hashlib, algo)
|
||||
return hashing(tosign).hexdigest().upper()
|
||||
|
||||
def sha_sign_in(self, params):
|
||||
return self.sha_sign(self.hash_algorithm, self.sha_in, params, SHA_IN_PARAMS)
|
||||
def sha_sign_in(self, params, encoding='iso-8859-1'):
|
||||
return self.sha_sign(self.hash_algorithm, self.sha_in, params, SHA_IN_PARAMS, encoding=encoding)
|
||||
|
||||
def sha_sign_out(self, params):
|
||||
return self.sha_sign(self.hash_algorithm, self.sha_out, params, SHA_OUT_PARAMS)
|
||||
def sha_sign_out(self, params, encoding='iso-8859-1'):
|
||||
return self.sha_sign(self.hash_algorithm, self.sha_out, params, SHA_OUT_PARAMS, encoding=encoding)
|
||||
|
||||
def get_request_url(self):
|
||||
if self.environment == ENVIRONMENT_TEST:
|
||||
|
@ -513,20 +533,27 @@ 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, **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
|
||||
# garanteed that no payment for the same ORDERID can happen during a 45
|
||||
# days window, see
|
||||
# https://epayments-support.ingenico.com/fr/integration/all-sales-channels/integrate-with-e-commerce/guide#form-parameters)
|
||||
complus = transaction_id or uuid.uuid4().hex
|
||||
if not orderid:
|
||||
orderid = complus
|
||||
|
||||
reference = self.transaction_id(20, string.digits + string.ascii_letters)
|
||||
|
||||
# prepend order id in payment reference
|
||||
if orderid:
|
||||
if len(orderid) > 24:
|
||||
raise ValueError('orderid length exceeds 25 characters')
|
||||
reference = (
|
||||
orderid
|
||||
+ ORDERID_TRANSACTION_SEPARATOR
|
||||
+ self.transaction_id(29 - len(orderid),
|
||||
string.digits + string.ascii_letters))
|
||||
language = language or self.language
|
||||
# convertir en centimes
|
||||
amount = Decimal(amount) * 100
|
||||
|
@ -534,10 +561,11 @@ class Payment(PaymentCommon):
|
|||
amount = amount.quantize(Decimal('1.'), rounding=ROUND_HALF_UP)
|
||||
params = {
|
||||
'AMOUNT': force_text(amount),
|
||||
'ORDERID': reference,
|
||||
'ORDERID': orderid,
|
||||
'PSPID': self.pspid,
|
||||
'LANGUAGE': language,
|
||||
'CURRENCY': self.currency,
|
||||
'COMPLUS': complus,
|
||||
}
|
||||
if self.normal_return_url:
|
||||
params['ACCEPTURL'] = self.normal_return_url
|
||||
|
@ -561,32 +589,33 @@ class Payment(PaymentCommon):
|
|||
form = Form(
|
||||
url=url,
|
||||
method='POST',
|
||||
fields=[{'type': 'hidden',
|
||||
'name': key,
|
||||
'value': params[key]} for key in params])
|
||||
return reference, FORM, form
|
||||
fields=[{'type': 'hidden', 'name': key, 'value': params[key]} for key in params],
|
||||
)
|
||||
return complus, FORM, form
|
||||
|
||||
def response(self, query_string, **kwargs):
|
||||
if six.PY3:
|
||||
params = urlparse.parse_qs(query_string, True, encoding='iso-8859-1')
|
||||
else:
|
||||
params = urlparse.parse_qs(query_string, True)
|
||||
params = dict((key.upper(), params[key][0]) for key in params)
|
||||
if not set(params) >= set(['ORDERID', 'PAYID', 'STATUS', 'NCERROR']):
|
||||
params = urlparse.parse_qs(query_string, True, encoding=self.encoding)
|
||||
params = {key.upper(): params[key][0] for key in params}
|
||||
if not set(params) >= {'ORDERID', 'PAYID', 'STATUS', 'NCERROR'}:
|
||||
raise ResponseError('missing ORDERID, PAYID, STATUS or NCERROR')
|
||||
|
||||
# uniformize iso-8859-1 encoded values
|
||||
# py2: decode binary strings in query-string
|
||||
for key in params:
|
||||
params[key] = force_text(params[key], 'iso-8859-1')
|
||||
reference = params['ORDERID']
|
||||
params[key] = force_text(params[key], self.encoding)
|
||||
orderid = params['ORDERID']
|
||||
complus = params.get('COMPLUS')
|
||||
transaction_id = params['PAYID']
|
||||
status = params['STATUS']
|
||||
error = params['NCERROR']
|
||||
signed = False
|
||||
if self.sha_in:
|
||||
signature = params.get('SHASIGN')
|
||||
expected_signature = self.sha_sign_out(params)
|
||||
signed = signature == expected_signature
|
||||
# check signature against both encoding
|
||||
for encoding in ('iso-8859-1', 'utf-8'):
|
||||
expected_signature = self.sha_sign_out(params, encoding=encoding)
|
||||
signed = signature == expected_signature
|
||||
if signed:
|
||||
break
|
||||
if status == '1':
|
||||
result = CANCELLED
|
||||
elif status == '2':
|
||||
|
@ -602,15 +631,35 @@ 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
|
||||
if ORDERID_TRANSACTION_SEPARATOR in reference:
|
||||
reference, transaction_id = reference.split(ORDERID_TRANSACTION_SEPARATOR, 1)
|
||||
|
||||
return PaymentResponse(
|
||||
result=result,
|
||||
signed=signed,
|
||||
bank_data=params,
|
||||
order_id=reference,
|
||||
transaction_id=transaction_id)
|
||||
order_id=complus or orderid,
|
||||
transaction_id=transaction_id,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def guess(self, *, method=None, query_string=None, body=None, headers=None, backends=(), **kwargs):
|
||||
for content in [query_string, body]:
|
||||
if isinstance(content, bytes):
|
||||
try:
|
||||
content = content.decode()
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
if isinstance(content, str):
|
||||
fields = {key.upper(): values for key, values in urlparse.parse_qs(content).items()}
|
||||
if not set(fields) >= {'ORDERID', 'PAYID', 'STATUS', 'NCERROR'}:
|
||||
continue
|
||||
orderid = fields.get('ORDERID')
|
||||
complus = fields.get('COMPLUS')
|
||||
if complus:
|
||||
return complus[0]
|
||||
return orderid[0]
|
||||
return None
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8
|
||||
# eopayment - online payment library
|
||||
# Copyright (C) 2011-2020 Entr'ouvert
|
||||
#
|
||||
|
@ -15,34 +14,42 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import base64
|
||||
import codecs
|
||||
from collections import OrderedDict
|
||||
import datetime
|
||||
import logging
|
||||
import hashlib
|
||||
import hmac
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import requests
|
||||
import uuid
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
from urllib import parse as urllib
|
||||
from urllib import parse as urlparse
|
||||
from xml.sax.saxutils import escape as xml_escape
|
||||
|
||||
import pytz
|
||||
import requests
|
||||
from Cryptodome.Hash import SHA
|
||||
from Cryptodome.PublicKey import RSA
|
||||
from Cryptodome.Signature import PKCS1_v1_5
|
||||
|
||||
from Crypto.Signature import PKCS1_v1_5
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Hash import SHA
|
||||
|
||||
import six
|
||||
from six.moves.urllib import parse as urlparse
|
||||
from six.moves.urllib import parse as urllib
|
||||
|
||||
import base64
|
||||
from gettext import gettext as _
|
||||
import warnings
|
||||
|
||||
from .common import (PaymentCommon, PaymentResponse, FORM, PAID, CANCELLED,
|
||||
DENIED, ERROR, Form, ResponseError, force_text,
|
||||
force_byte)
|
||||
from . import cb
|
||||
from .common import (
|
||||
CANCELLED,
|
||||
DENIED,
|
||||
ERROR,
|
||||
FORM,
|
||||
PAID,
|
||||
Form,
|
||||
PaymentCommon,
|
||||
PaymentResponse,
|
||||
ResponseError,
|
||||
_,
|
||||
force_byte,
|
||||
force_text,
|
||||
)
|
||||
|
||||
__all__ = ['sign', 'Payment']
|
||||
|
||||
|
@ -113,9 +120,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 = {
|
||||
|
@ -126,18 +131,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'
|
||||
|
@ -148,14 +150,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)
|
||||
|
@ -182,20 +183,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 = {
|
||||
|
@ -214,55 +216,52 @@ class Payment(PaymentCommon):
|
|||
},
|
||||
{
|
||||
'name': 'platform',
|
||||
'caption': _('Plateforme cible'),
|
||||
'caption': 'Plateforme cible',
|
||||
'default': 'test',
|
||||
'choices': (
|
||||
('test', 'Test'),
|
||||
('backup', 'Backup'),
|
||||
('prod', 'Production'),
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
'name': 'site',
|
||||
'caption': _('Numéro de site'),
|
||||
'caption': 'Numéro de site',
|
||||
'required': True,
|
||||
'validation': lambda x: isinstance(x, six.string_types)
|
||||
and x.isdigit() and len(x) == 7,
|
||||
'validation': lambda x: isinstance(x, str) and x.isdigit() and len(x) == 7,
|
||||
},
|
||||
{
|
||||
'name': 'cle',
|
||||
'caption': _('Site key'),
|
||||
'caption': 'Clé',
|
||||
'help_text': 'Uniquement nécessaire pour l\'annulation / remboursement / encaissement (PayBox Direct)',
|
||||
'required': False,
|
||||
'validation': lambda x: isinstance(x, six.string_types),
|
||||
'validation': lambda x: isinstance(x, str),
|
||||
},
|
||||
{
|
||||
'name': 'rang',
|
||||
'caption': _('Numéro de rang'),
|
||||
'caption': 'Numéro de rang',
|
||||
'required': True,
|
||||
'validation': lambda x: isinstance(x, six.string_types)
|
||||
and x.isdigit() and len(x) == 3,
|
||||
'validation': lambda x: isinstance(x, str) and x.isdigit() and (len(x) in (2, 3)),
|
||||
},
|
||||
{
|
||||
'name': 'identifiant',
|
||||
'caption': _('Identifiant'),
|
||||
'caption': 'Identifiant',
|
||||
'required': True,
|
||||
'validation': lambda x: isinstance(x, six.string_types)
|
||||
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)'),
|
||||
'caption': 'Secret partagé (clé HMAC)',
|
||||
'validation': lambda x: isinstance(x, str)
|
||||
and all(a.lower() in '0123456789abcdef' for a in x),
|
||||
and all(a.lower() in '0123456789abcdef' for a in x)
|
||||
and (len(x) % 2 == 0),
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'name': 'devise',
|
||||
'caption': _('Devise'),
|
||||
'caption': 'Devise',
|
||||
'default': '978',
|
||||
'choices': (
|
||||
('978', 'Euro'),
|
||||
),
|
||||
'choices': (('978', 'Euro'),),
|
||||
},
|
||||
{
|
||||
'name': 'callback',
|
||||
|
@ -271,33 +270,32 @@ class Payment(PaymentCommon):
|
|||
},
|
||||
{
|
||||
'name': 'capture_day',
|
||||
'caption': _('Nombre de jours pour un paiement différé'),
|
||||
'caption': 'Nombre de jours pour un paiement différé',
|
||||
'default': '',
|
||||
'required': False,
|
||||
'validation': lambda x: isinstance(x, six.string_types)
|
||||
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):
|
||||
|
@ -309,17 +307,36 @@ class Payment(PaymentCommon):
|
|||
pbx_cmd += '!' + orderid
|
||||
return pbx_cmd
|
||||
|
||||
def request(self, amount, email, name=None, orderid=None, manual_validation=None, **kwargs):
|
||||
def make_pbx_archivage(self):
|
||||
return ''.join(random.choices('ABCDEFGHJKMNPQRSTUVWXYZ246789', k=12))
|
||||
|
||||
def request(
|
||||
self,
|
||||
amount,
|
||||
email,
|
||||
name=None,
|
||||
orderid=None,
|
||||
manual_validation=None,
|
||||
# 3DSv2 informations
|
||||
total_quantity=None,
|
||||
first_name=None,
|
||||
last_name=None,
|
||||
address1=None,
|
||||
zipcode=None,
|
||||
city=None,
|
||||
country_code=None,
|
||||
**kwargs,
|
||||
):
|
||||
d = OrderedDict()
|
||||
d['PBX_SITE'] = force_text(self.site)
|
||||
d['PBX_RANG'] = force_text(self.rang).strip()[-3:]
|
||||
d['PBX_IDENTIFIANT'] = force_text(self.identifiant)
|
||||
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)
|
||||
pbx_archivage = self.make_pbx_archivage()
|
||||
transaction_id = d['PBX_CMD'] = self.make_pbx_cmd(
|
||||
guid=pbx_archivage, 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;'
|
||||
|
@ -329,9 +346,9 @@ 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')
|
||||
d['PBX_ARCHIVAGE'] = orderid or guid
|
||||
force_text(datetime.datetime.utcnow().isoformat('T')).split('.')[0] + '+00:00'
|
||||
)
|
||||
d['PBX_ARCHIVAGE'] = pbx_archivage
|
||||
if self.normal_return_url:
|
||||
d['PBX_EFFECTUE'] = self.normal_return_url
|
||||
d['PBX_REFUSE'] = self.normal_return_url
|
||||
|
@ -339,8 +356,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,38 +366,69 @@ class Payment(PaymentCommon):
|
|||
d['PBX_AUTOSEULE'] = PAYMENT_MODES['AUTHOR_CAPTURE']
|
||||
if automatic_return_url:
|
||||
d['PBX_REPONDRE_A'] = force_text(automatic_return_url)
|
||||
# PBX_SHOPPINGCART and PBX_BILLING: 3DSv2 new informations
|
||||
if total_quantity:
|
||||
total_quantity = xml_escape('%s' % total_quantity)
|
||||
d['PBX_SHOPPINGCART'] = (
|
||||
'<?xml version="1.0" encoding="utf-8"?><shoppingcart><total>'
|
||||
'<totalQuantity>%s</totalQuantity></total></shoppingcart>' % total_quantity
|
||||
)
|
||||
if first_name or last_name or address1 or zipcode or city or country_code:
|
||||
pbx_billing = '<?xml version="1.0" encoding="utf-8"?><Billing><Address>'
|
||||
if first_name:
|
||||
pbx_billing += '<FirstName>%s</FirstName>' % xml_escape(first_name)
|
||||
if last_name:
|
||||
pbx_billing += '<LastName>%s</LastName>' % xml_escape(last_name)
|
||||
if address1:
|
||||
pbx_billing += '<Address1>%s</Address1>' % xml_escape(address1)
|
||||
if zipcode:
|
||||
pbx_billing += '<ZipCode>%s</ZipCode>' % xml_escape('%s' % zipcode)
|
||||
if city:
|
||||
pbx_billing += '<City>%s</City>' % xml_escape(city)
|
||||
if country_code:
|
||||
pbx_billing += '<CountryCode>%s</CountryCode>' % xml_escape('%s' % country_code)
|
||||
pbx_billing += '</Address></Billing>'
|
||||
d['PBX_BILLING'] = force_text(pbx_billing)
|
||||
d = d.items()
|
||||
|
||||
if six.PY3:
|
||||
shared_secret = codecs.decode(bytes(self.shared_secret, 'ascii'), 'hex')
|
||||
else:
|
||||
shared_secret = codecs.decode(bytes(self.shared_secret), 'hex')
|
||||
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': u'hidden',
|
||||
'name': force_text(k),
|
||||
'value': force_text(v),
|
||||
})
|
||||
form = Form(url, 'POST', fields, submit_name=None,
|
||||
submit_value=u'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):
|
||||
d = urlparse.parse_qs(query_string, True, False)
|
||||
if not set(d) >= set(['erreur', 'reference']):
|
||||
if not set(d) >= {'erreur', 'reference'}:
|
||||
raise ResponseError('missing erreur or reference')
|
||||
signed = False
|
||||
if 'signature' in d:
|
||||
sig = d['signature'][0]
|
||||
sig = base64.b64decode(sig)
|
||||
try:
|
||||
sig = base64.b64decode(sig)
|
||||
except (TypeError, ValueError):
|
||||
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):
|
||||
|
@ -419,7 +466,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__)
|
||||
|
@ -443,8 +491,6 @@ class Payment(PaymentCommon):
|
|||
logger.debug('received %r', response.text)
|
||||
data = dict(urlparse.parse_qsl(response.text, True, True))
|
||||
if data.get('CODEREPONSE') != PAYBOX_DIRECT_SUCCESS_RESPONSE_CODE:
|
||||
if six.PY2:
|
||||
raise ResponseError(data['COMMENTAIRE'].encode('utf-8'))
|
||||
raise ResponseError(data['COMMENTAIRE'])
|
||||
return data
|
||||
|
||||
|
@ -453,3 +499,17 @@ class Payment(PaymentCommon):
|
|||
|
||||
def cancel(self, amount, bank_data, **kwargs):
|
||||
return self.perform(amount, bank_data, PAYBOX_DIRECT_CANCEL_OPERATION)
|
||||
|
||||
@classmethod
|
||||
def guess(self, *, method=None, query_string=None, body=None, headers=None, backends=(), **kwargs):
|
||||
for content in [query_string, body]:
|
||||
if isinstance(content, bytes):
|
||||
try:
|
||||
content = content.decode()
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
if isinstance(content, str):
|
||||
fields = urlparse.parse_qs(content)
|
||||
if 'erreur' in fields and 'reference' in fields:
|
||||
return fields['reference'][0]
|
||||
return None
|
||||
|
|
|
@ -14,37 +14,48 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
import decimal
|
||||
import functools
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import unicodedata
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from gettext import gettext as _
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
import pytz
|
||||
|
||||
import six
|
||||
from six.moves.urllib.parse import parse_qs
|
||||
|
||||
import requests
|
||||
import zeep
|
||||
import zeep.exceptions
|
||||
|
||||
from .common import (
|
||||
CANCELLED,
|
||||
DENIED,
|
||||
ERROR,
|
||||
EXPIRED,
|
||||
PAID,
|
||||
URL,
|
||||
WAITING,
|
||||
PaymentCommon,
|
||||
PaymentException,
|
||||
PaymentResponse,
|
||||
ResponseError,
|
||||
_,
|
||||
force_text,
|
||||
)
|
||||
from .systempayv2 import isonow
|
||||
from .common import (PaymentCommon, PaymentResponse, URL, PAID, DENIED,
|
||||
CANCELLED, ERROR, ResponseError, PaymentException,
|
||||
WAITING, EXPIRED, force_text)
|
||||
|
||||
WSDL_URL = 'https://www.tipi.budget.gouv.fr/tpa/services/mas_securite/contrat_paiement_securise/PaiementSecuriseService?wsdl' # noqa: E501
|
||||
|
||||
SERVICE_URL = 'https://www.tipi.budget.gouv.fr/tpa/services/securite' # noqa: E501
|
||||
|
||||
PAYMENT_URL = 'https://www.tipi.budget.gouv.fr/tpa/paiementws.web'
|
||||
# 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
|
||||
# where the XSD files are resolvable. To not depend too much on those files, we
|
||||
# provide copy in eopayment and we patch them to fix the service binding URL
|
||||
# and the path of XSD files. The PayFiP development team is full of morons.
|
||||
WSDL_URL = 'https://www.payfip.gouv.fr/tpa/services/mas_securite/contrat_paiement_securise/PaiementSecuriseService?wsdl' # noqa: E501
|
||||
SERVICE_URL = 'https://www.payfip.gouv.fr/tpa/services/securite' # noqa: E501
|
||||
PAYMENT_URL = 'https://www.payfip.gouv.fr/tpa/paiementws.web'
|
||||
|
||||
REFDET_RE = re.compile(r'^[A-Za-z0-9]{1,30}$')
|
||||
|
||||
|
@ -52,7 +63,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)
|
||||
|
||||
|
@ -82,19 +93,37 @@ class PayFiPError(PaymentException):
|
|||
args = [code, message]
|
||||
if origin:
|
||||
args.append(origin)
|
||||
super(PayFiPError, self).__init__(*args)
|
||||
super().__init__(*args)
|
||||
|
||||
|
||||
class PayFiP(object):
|
||||
class PayFiP:
|
||||
'''Encapsulate SOAP web-services of PayFiP'''
|
||||
|
||||
def __init__(self, wsdl_url=None, service_url=None, zeep_client_kwargs=None):
|
||||
self.client = zeep.Client(wsdl_url or WSDL_URL, **(zeep_client_kwargs or {}))
|
||||
# distribued WSDL is wrong :/
|
||||
self.client.service._binding_options['address'] = service_url or SERVICE_URL
|
||||
def __init__(self, wsdl_url=None, service_url=None, zeep_client_kwargs=None, use_local_wsdl=True):
|
||||
# use cached WSDL
|
||||
if (not wsdl_url or (wsdl_url == WSDL_URL)) and use_local_wsdl:
|
||||
base_path = os.path.join(os.path.dirname(__file__), 'resource', 'PaiementSecuriseService.wsdl')
|
||||
wsdl_url = 'file://%s' % base_path
|
||||
self.wsdl_url = wsdl_url
|
||||
self.service_url = service_url
|
||||
self.zeep_client_kwargs = zeep_client_kwargs
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
if not hasattr(self, '_client'):
|
||||
try:
|
||||
self._client = zeep.Client(self.wsdl_url or WSDL_URL, **(self.zeep_client_kwargs or {}))
|
||||
# distribued WSDL is wrong :/
|
||||
self._client.service._binding_options['address'] = self.service_url or SERVICE_URL
|
||||
except Exception as e:
|
||||
raise PayFiPError('Cound not initialize the SOAP client', e)
|
||||
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')
|
||||
|
@ -106,7 +135,8 @@ class PayFiP(object):
|
|||
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
|
||||
|
@ -115,15 +145,22 @@ class PayFiP(object):
|
|||
except zeep.exceptions.Fault as fault:
|
||||
raise self.fault_to_exception(fault) or PayFiPError('unknown', fault.message, fault)
|
||||
except zeep.exceptions.Error as zeep_error:
|
||||
raise PayFiPError('erreur-soap', str(zeep_error), zeep_error)
|
||||
raise PayFiPError('SOAP error', str(zeep_error), zeep_error)
|
||||
except requests.RequestException as e:
|
||||
raise PayFiPError('HTTP error', e)
|
||||
except Exception as e:
|
||||
raise PayFiPError('Unexpected error', e)
|
||||
|
||||
def get_info_client(self, numcli):
|
||||
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',
|
||||
|
@ -135,52 +172,33 @@ class PayFiP(object):
|
|||
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',
|
||||
'parameters': [
|
||||
{
|
||||
'name': 'numcli',
|
||||
'caption': _(u'Client number'),
|
||||
'help_text': _(u'6 digits number provided by DGFIP'),
|
||||
'caption': _('Client number'),
|
||||
'help_text': _('6 digits number provided by DGFIP'),
|
||||
'validation': lambda s: str.isdigit(s) and len(s) == 6,
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'name': 'service_url',
|
||||
'default': SERVICE_URL,
|
||||
'caption': _(u'PayFIP WS service URL'),
|
||||
'help_text': _(u'do not modify if you do not know'),
|
||||
'validation': lambda x: x.startswith('http'),
|
||||
},
|
||||
{
|
||||
'name': 'wsdl_url',
|
||||
'default': WSDL_URL,
|
||||
'caption': _(u'PayFIP WS WSDL URL'),
|
||||
'help_text': _(u'do not modify if you do not know'),
|
||||
'validation': lambda x: x.startswith('http'),
|
||||
},
|
||||
{
|
||||
'name': 'payment_url',
|
||||
'default': PAYMENT_URL,
|
||||
'caption': _(u'PayFiP payment URL'),
|
||||
'help_text': _(u'do not modify if you do not know'),
|
||||
'validation': lambda x: x.startswith('http'),
|
||||
},
|
||||
{
|
||||
'name': 'saisie',
|
||||
'caption': _('Payment type'),
|
||||
|
@ -204,20 +222,21 @@ class Payment(PaymentCommon):
|
|||
],
|
||||
}
|
||||
|
||||
min_time_between_transactions = 60 * 20 # 20 minutes
|
||||
|
||||
minimal_amount = decimal.Decimal('1.0')
|
||||
maximal_amount = decimal.Decimal('100000.0')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Payment, self).__init__(*args, **kwargs)
|
||||
wsdl_url = self.wsdl_url
|
||||
# use cached WSDL
|
||||
if wsdl_url == WSDL_URL:
|
||||
base_path = os.path.join(os.path.dirname(__file__), 'resource', 'PaiementSecuriseService.wsdl')
|
||||
wsdl_url = 'file://%s' % base_path
|
||||
self.payfip = PayFiP(wsdl_url=wsdl_url, service_url=self.service_url)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.payfip = PayFiP()
|
||||
|
||||
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,13 +282,19 @@ 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, self.payment_url + '?idop=%s' % idop
|
||||
return str(idop), URL, PAYMENT_URL + '?idop=%s' % idop
|
||||
|
||||
def payment_status(self, transaction_id, transaction_date=None, **kwargs):
|
||||
# idop are valid for 15 minutes after their generation
|
||||
|
@ -297,19 +322,19 @@ 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)
|
||||
eopayment_response = self.payfip_response_to_eopayment_response(idop, response)
|
||||
# convert CANCELLED to WAITING during the first 20 minutes
|
||||
if eopayment_response.result == CANCELLED and delta < threshold:
|
||||
eopayment_response.result = WAITING
|
||||
eopayment_response.bank_status = (
|
||||
'%s - still waiting as idop is still active' % eopayment_response.bank_status
|
||||
)
|
||||
return eopayment_response
|
||||
|
||||
def response(self, query_string, **kwargs):
|
||||
fields = parse_qs(query_string, True)
|
||||
|
@ -353,7 +378,22 @@ 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',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def guess(self, *, method=None, query_string=None, body=None, headers=None, backends=(), **kwargs):
|
||||
for content in [query_string, body]:
|
||||
if isinstance(content, bytes):
|
||||
try:
|
||||
content = content.decode()
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
if isinstance(content, str):
|
||||
fields = parse_qs(content)
|
||||
if set(fields) == {'idOp'}:
|
||||
return fields['idOp'][0]
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -366,6 +406,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 +415,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
|
||||
|
@ -381,7 +423,7 @@ if __name__ == '__main__':
|
|||
ctx.obj = PayFiP(wsdl_url=wsdl_url, service_url=service_url)
|
||||
|
||||
def numcli(ctx, param, value):
|
||||
if not isinstance(value, six.string_types) or len(value) != 6 or not value.isdigit():
|
||||
if not isinstance(value, str) or len(value) != 6 or not value.isdigit():
|
||||
raise click.BadParameter('numcli must a 6 digits number')
|
||||
return value
|
||||
|
||||
|
@ -407,10 +449,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 +471,3 @@ if __name__ == '__main__':
|
|||
print(payfip.get_info_paiement(idop))
|
||||
|
||||
main()
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/bash
|
||||
echo -ne 0!!coin
|
|
@ -134,7 +134,7 @@
|
|||
</binding>
|
||||
<service name="PaiementSecuriseService">
|
||||
<port name="PaiementSecuriseServicePort" binding="tns:PaiementSecuriseServicePortBinding">
|
||||
<soap:address location="http://www.tipi.budget.gouv.fr:80/tpa/services/mas_securite/contrat_paiement_securise/PaiementSecuriseService"/>
|
||||
<soap:address location="https://www.payfip.gouv.fr/tpa/services/mas_securite/contrat_paiement_securise/PaiementSecuriseService"/>
|
||||
</port>
|
||||
</service>
|
||||
</definitions>
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
<xs:element name="saisie" type="xs:string" minOccurs="0"/>
|
||||
<xs:element name="urlnotif" type="xs:string" minOccurs="0"/>
|
||||
<xs:element name="urlredirect" type="xs:string" minOccurs="0"/>
|
||||
<xs:element name="typeAuthentification" type="xs:string" minOccurs="0"/>
|
||||
<xs:element name="typeUsager" type="xs:string" minOccurs="0"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# eopayment - online payment library
|
||||
# Copyright (C) 2011-2020 Entr'ouvert
|
||||
#
|
||||
|
@ -15,18 +14,25 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals, print_function
|
||||
|
||||
import functools
|
||||
|
||||
from six.moves.urllib.parse import urljoin, parse_qs
|
||||
from urllib.parse import parse_qs, urljoin
|
||||
|
||||
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 (
|
||||
CANCELLED,
|
||||
DENIED,
|
||||
ERROR,
|
||||
PAID,
|
||||
URL,
|
||||
PaymentCommon,
|
||||
PaymentException,
|
||||
PaymentResponse,
|
||||
ResponseError,
|
||||
)
|
||||
|
||||
_zeep_transport = None
|
||||
|
||||
|
@ -35,7 +41,7 @@ class SagaError(PaymentException):
|
|||
pass
|
||||
|
||||
|
||||
class Saga(object):
|
||||
class Saga:
|
||||
def __init__(self, wsdl_url, service_url=None, zeep_client_kwargs=None):
|
||||
self.wsdl_url = wsdl_url
|
||||
kwargs = (zeep_client_kwargs or {}).copy()
|
||||
|
@ -62,8 +68,17 @@ class Saga(object):
|
|||
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',
|
||||
|
@ -74,15 +89,13 @@ class Saga(object):
|
|||
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"
|
||||
|
@ -127,13 +140,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
|
||||
|
@ -152,7 +164,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]
|
||||
|
@ -196,7 +209,8 @@ class Payment(PaymentCommon):
|
|||
bank_data=dict(response),
|
||||
order_id=idop,
|
||||
transaction_id=idop,
|
||||
test=False)
|
||||
test=False,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -209,6 +223,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()
|
||||
|
@ -217,6 +232,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
|
||||
|
@ -234,8 +250,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,
|
||||
|
@ -244,7 +269,8 @@ if __name__ == '__main__':
|
|||
montant=montant,
|
||||
urlretour_asynchrone=urlretour_asynchrone,
|
||||
email=email,
|
||||
urlretour_synchrone=urlretour_synchrone)
|
||||
urlretour_synchrone=urlretour_synchrone,
|
||||
)
|
||||
print('url:', url)
|
||||
|
||||
main()
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# eopayment - online payment library
|
||||
# Copyright (C) 2011-2020 Entr'ouvert
|
||||
#
|
||||
|
@ -15,29 +14,36 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
from gettext import gettext as _
|
||||
import collections
|
||||
import datetime
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import uuid
|
||||
import warnings
|
||||
|
||||
from six.moves.urllib import parse as urlparse
|
||||
|
||||
import requests
|
||||
from urllib import parse as urlparse
|
||||
|
||||
import pytz
|
||||
import requests
|
||||
|
||||
from .common import (PaymentCommon, FORM, Form, PaymentResponse, PAID, ERROR,
|
||||
CANCELED, ResponseError, force_text)
|
||||
from .common import (
|
||||
CANCELED,
|
||||
ERROR,
|
||||
FORM,
|
||||
PAID,
|
||||
Form,
|
||||
PaymentCommon,
|
||||
PaymentResponse,
|
||||
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:
|
||||
|
@ -50,42 +56,44 @@ class Payment(PaymentCommon):
|
|||
|
||||
Worldline Benelux_Sips_Technical_integration_guide_Version_1.5.pdf
|
||||
|
||||
'''
|
||||
"""
|
||||
|
||||
has_free_transaction_id = True
|
||||
|
||||
URL = {
|
||||
'test': 'https://payment-webinit.simu.sips-atos.com/paymentInit',
|
||||
'prod': 'https://payment-webinit.sips-atos.com/paymentInit',
|
||||
'test': 'https://payment-webinit.simu.sips-services.com/paymentInit',
|
||||
'prod': 'https://payment-webinit.sips-services.com/paymentInit',
|
||||
}
|
||||
WS_URL = {
|
||||
'test': 'https://office-server.test.sips-atos.com',
|
||||
'prod': 'https://office-server.sips-atos.com',
|
||||
'test': 'https://office-server.test.sips-services.com',
|
||||
'prod': 'https://office-server.sips-services.com',
|
||||
}
|
||||
|
||||
INTERFACE_VERSION = 'HP_2.3'
|
||||
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 +157,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 u'|'.join(u'%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 +195,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 +229,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 +240,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,11 +251,13 @@ class Payment(PaymentCommon):
|
|||
'name': 'InterfaceVersion',
|
||||
'value': self.INTERFACE_VERSION,
|
||||
},
|
||||
])
|
||||
],
|
||||
)
|
||||
self.logger.debug('emitting request %r', data)
|
||||
return transactionReference, FORM, form
|
||||
|
||||
def decode_data(self, data):
|
||||
@classmethod
|
||||
def decode_data(cls, data):
|
||||
data = data.split('|')
|
||||
data = [map(force_text, p.split('=', 1)) for p in data]
|
||||
return collections.OrderedDict(data)
|
||||
|
@ -255,7 +272,7 @@ class Payment(PaymentCommon):
|
|||
|
||||
def response(self, query_string, **kwargs):
|
||||
form = urlparse.parse_qs(query_string)
|
||||
if not set(form) >= set(['Data', 'Seal', 'InterfaceVersion']):
|
||||
if not set(form) >= {'Data', 'Seal', 'InterfaceVersion'}:
|
||||
raise ResponseError('missing Data, Seal or InterfaceVersion')
|
||||
self.logger.debug('received query string %r', form)
|
||||
data = self.decode_data(form['Data'][0])
|
||||
|
@ -270,7 +287,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:
|
||||
|
@ -282,9 +301,10 @@ class Payment(PaymentCommon):
|
|||
bank_data=data,
|
||||
order_id=transaction_id,
|
||||
transaction_id=data.get('authorisationId'),
|
||||
bank_status=self.RESPONSE_CODES.get(response_code, u'unknown code - ' + response_code),
|
||||
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 = []
|
||||
|
@ -292,8 +312,10 @@ class Payment(PaymentCommon):
|
|||
if key in ('keyVersion', 'sealAlgorithm', 'seal'):
|
||||
continue
|
||||
data_to_send.append(force_text(data[key]))
|
||||
data_to_send_str = u''.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()
|
||||
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()
|
||||
|
||||
def perform_cash_management_operation(self, endpoint, data):
|
||||
data['merchantId'] = self.merchant_id
|
||||
|
@ -306,10 +328,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 +368,25 @@ 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()
|
||||
|
||||
@classmethod
|
||||
def guess(self, *, method=None, query_string=None, body=None, headers=None, backends=(), **kwargs):
|
||||
for content in [query_string, body]:
|
||||
if isinstance(content, bytes):
|
||||
try:
|
||||
content = content.decode()
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
if isinstance(content, str):
|
||||
fields = urlparse.parse_qs(content)
|
||||
if not set(fields) >= {'Data', 'Seal', 'InterfaceVersion'}:
|
||||
continue
|
||||
data = self.decode_data(fields['Data'][0])
|
||||
if 'transactionReference' in data:
|
||||
return data['transactionReference']
|
||||
return None
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# eopayment - online payment library
|
||||
# Copyright (C) 2011-2020 Entr'ouvert
|
||||
#
|
||||
|
@ -16,21 +15,32 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import base64
|
||||
import pytz
|
||||
import datetime as dt
|
||||
import hashlib
|
||||
import hmac
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import six
|
||||
from six.moves.urllib import parse as urlparse
|
||||
import warnings
|
||||
from gettext import gettext as _
|
||||
from urllib import parse as urlparse
|
||||
|
||||
import pytz
|
||||
|
||||
from .common import (PaymentCommon, PaymentResponse, PAID, DENIED, CANCELLED,
|
||||
ERROR, FORM, Form, ResponseError, force_text, force_byte)
|
||||
from .cb import translate_cb_error_code
|
||||
from .common import (
|
||||
CANCELLED,
|
||||
DENIED,
|
||||
ERROR,
|
||||
FORM,
|
||||
PAID,
|
||||
Form,
|
||||
PaymentCommon,
|
||||
PaymentResponse,
|
||||
ResponseError,
|
||||
_,
|
||||
force_byte,
|
||||
force_text,
|
||||
)
|
||||
|
||||
__all__ = ['Payment']
|
||||
|
||||
|
@ -58,8 +68,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):
|
||||
|
@ -71,9 +80,20 @@ 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,
|
||||
scope='global',
|
||||
):
|
||||
self.name = name
|
||||
self.ptype = ptype
|
||||
self.code = code
|
||||
|
@ -84,6 +104,7 @@ class Parameter:
|
|||
self.choices = choices
|
||||
self.description = description
|
||||
self.help_text = help_text
|
||||
self.scope = scope
|
||||
|
||||
def check_value(self, value):
|
||||
if self.length and len(value) != self.length:
|
||||
|
@ -108,8 +129,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
|
||||
|
@ -128,44 +150,60 @@ 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=_(u"Complément d'information 1")),
|
||||
Parameter('vads_order_info2', 'an', 14, max_length=255,
|
||||
description=_(u"Complément d'information 2")),
|
||||
Parameter('vads_order_info3', 'an', 14, max_length=255,
|
||||
description=_(u"Complément d'information 3")),
|
||||
Parameter('vads_page_action', None, 46, needed=True, default='PAYMENT',
|
||||
choices=('PAYMENT',)),
|
||||
Parameter('vads_payment_cards', 'an;', 8, max_length=127, default='',
|
||||
description=_(u'Liste des cartes de paiement acceptées'),
|
||||
help_text=_(u'vide ou des valeurs sépareés par un point-virgule '
|
||||
'parmi AMEX, AURORE-MULTI, BUYSTER, CB, COFINOGA, '
|
||||
'E-CARTEBLEUE, MASTERCARD, JCB, MAESTRO, ONEY, '
|
||||
'ONEY_SANDBOX, PAYPAL, PAYPAL_SB, PAYSAFECARD, '
|
||||
'VISA')),
|
||||
Parameter(
|
||||
'vads_order_info',
|
||||
'an',
|
||||
14,
|
||||
max_length=255,
|
||||
description="Complément d'information 1",
|
||||
scope='transaction',
|
||||
),
|
||||
Parameter(
|
||||
'vads_order_info2',
|
||||
'an',
|
||||
14,
|
||||
max_length=255,
|
||||
description="Complément d'information 2",
|
||||
scope='transaction',
|
||||
),
|
||||
Parameter(
|
||||
'vads_order_info3',
|
||||
'an',
|
||||
14,
|
||||
max_length=255,
|
||||
description="Complément d'information 3",
|
||||
scope='transaction',
|
||||
),
|
||||
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',
|
||||
scope='transaction',
|
||||
),
|
||||
# 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=_(u'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),
|
||||
|
@ -177,8 +215,7 @@ PARAMETERS = [
|
|||
Parameter(VADS_CUST_FIRST_NAME, 'ans', 104, max_length=63),
|
||||
Parameter(VADS_CUST_LAST_NAME, 'ans', 104, max_length=63),
|
||||
]
|
||||
PARAMETER_MAP = dict(((parameter.name,
|
||||
parameter) for parameter in PARAMETERS))
|
||||
PARAMETER_MAP = {parameter.name: parameter for parameter in PARAMETERS}
|
||||
|
||||
|
||||
def add_vads(kwargs):
|
||||
|
@ -197,39 +234,40 @@ 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/"
|
||||
service_url = 'https://paiement.systempay.fr/vads-payment/'
|
||||
signature_algo = 'sha1'
|
||||
|
||||
description = {
|
||||
'caption': 'SystemPay, système de paiment du groupe BPCE',
|
||||
'caption': 'SystemPay, système de paiement du groupe BPCE',
|
||||
'parameters': [
|
||||
{
|
||||
'name': 'normal_return_url',
|
||||
|
@ -242,57 +280,76 @@ 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': _(u'URL du service de paiment'),
|
||||
'help_text': _(u'ne pas modifier si vous ne savez pas'),
|
||||
'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',
|
||||
'caption': _(u'Secret pour la configuration de TEST'),
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'name': 'secret_test',
|
||||
'caption': 'Secret pour la configuration de TEST',
|
||||
'validation': lambda value: str.isalnum(value),
|
||||
'required': True, },
|
||||
{'name': 'secret_production',
|
||||
'caption': _(u'Secret pour la configuration de PRODUCTION'),
|
||||
'validation': lambda value: str.isalnum(value), },
|
||||
{'name': 'signature_algo',
|
||||
'caption': _(u'Algorithme de signature'),
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'name': 'secret_production',
|
||||
'caption': 'Secret pour la configuration de PRODUCTION',
|
||||
'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,
|
||||
'scope': parameter.scope,
|
||||
}
|
||||
description['parameters'].append(x)
|
||||
|
||||
def __init__(self, options, logger=None):
|
||||
super(Payment, self).__init__(options, logger=logger)
|
||||
super().__init__(options, logger=logger)
|
||||
options = add_vads(options)
|
||||
self.options = options
|
||||
|
||||
|
@ -301,28 +358,48 @@ class Payment(PaymentCommon):
|
|||
# trans_id starting with 9 are reserved for the systempay backoffice
|
||||
# https://paiement.systempay.fr/doc/fr-FR/form-payment/reference/vads-trans-id.html
|
||||
gen = random.SystemRandom()
|
||||
if six.PY3:
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
else:
|
||||
alphabet = string.letters + string.digits
|
||||
|
||||
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)}))
|
||||
|
@ -330,8 +407,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)
|
||||
|
@ -361,8 +440,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()
|
||||
|
@ -373,8 +451,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:
|
||||
|
@ -400,11 +477,13 @@ class Payment(PaymentCommon):
|
|||
method='POST',
|
||||
fields=[
|
||||
{
|
||||
'type': u'hidden',
|
||||
'type': 'hidden',
|
||||
'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 = {
|
||||
|
@ -421,9 +500,11 @@ class Payment(PaymentCommon):
|
|||
'00': {'message': 'Tous les contrôles se sont déroulés avec succés.'},
|
||||
'02': {'message': 'La carte a dépassé l\'encours autorisé.'},
|
||||
'03': {'message': 'La carte appartient à la liste grise du commerçant.'},
|
||||
'04': {'messaǵe': 'Le pays d\'émission de la carte appartient à la liste grise du '
|
||||
'commerçant ou le pays d\'émission de la carte n\'appartient pas à la '
|
||||
'liste blanche du commerçant.'},
|
||||
'04': {
|
||||
'messaǵe': 'Le pays d\'émission de la carte appartient à la liste grise du '
|
||||
'commerçant ou le pays d\'émission de la carte n\'appartient pas à la '
|
||||
'liste blanche du commerçant.'
|
||||
},
|
||||
'05': {'message': 'L’adresse IP appartient à la liste grise du marchand.'},
|
||||
'06': {'message': 'Le code bin appartient à la liste grise du marchand.'},
|
||||
'07': {'message': 'Détection d’une e-carte bleue.'},
|
||||
|
@ -431,8 +512,10 @@ class Payment(PaymentCommon):
|
|||
'09': {'message': 'Détection d’une carte commerciale étrangère.'},
|
||||
'14': {'message': 'Détection d’une carte à autorisation systématique.'},
|
||||
'30': {'message': 'Le pays de l’adresse IP appartient à la liste grise.'},
|
||||
'99': {'message': 'Problème technique recontré par le serveur lors du traitement '
|
||||
'd\'un des contrôles locauxi.'},
|
||||
'99': {
|
||||
'message': 'Problème technique recontré par le serveur lors du traitement '
|
||||
'd\'un des contrôles locauxi.'
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
@ -475,9 +558,8 @@ class Payment(PaymentCommon):
|
|||
|
||||
def response(self, query_string, **kwargs):
|
||||
fields = urlparse.parse_qs(query_string, True)
|
||||
if not set(fields) >= set([SIGNATURE, VADS_CTX_MODE, VADS_AUTH_RESULT]):
|
||||
raise ResponseError('missing %s, %s or %s' % (SIGNATURE, VADS_CTX_MODE,
|
||||
VADS_AUTH_RESULT))
|
||||
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))
|
||||
for key, value in fields.items():
|
||||
fields[key] = value[0]
|
||||
copy = fields.copy()
|
||||
|
@ -486,17 +568,21 @@ 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.'
|
||||
|
||||
test = fields[VADS_CTX_MODE] == 'TEST'
|
||||
if VADS_EOPAYMENT_TRANS_ID in fields:
|
||||
transaction_id = fields[VADS_EOPAYMENT_TRANS_ID]
|
||||
vads_eopayment_trans_id = fields.get(VADS_EOPAYMENT_TRANS_ID)
|
||||
vads_trans_date = fields.get(VADS_TRANS_DATE)
|
||||
vads_trans_id = fields.get(VADS_TRANS_ID)
|
||||
if vads_eopayment_trans_id:
|
||||
transaction_id = vads_eopayment_trans_id
|
||||
elif vads_trans_date and vads_trans_id:
|
||||
transaction_id = vads_trans_date + '_' + vads_trans_id
|
||||
else:
|
||||
transaction_id = '%s_%s' % (copy[VADS_TRANS_DATE], copy[VADS_TRANS_ID])
|
||||
raise ResponseError('backend error', message)
|
||||
# the VADS_AUTH_NUMBER is the number to match payment in bank logs
|
||||
copy[self.BANK_ID] = copy.get(VADS_AUTH_NUMBER, '')
|
||||
transaction_date = None
|
||||
|
@ -510,7 +596,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,15 +609,35 @@ 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()))
|
||||
signed_data = b'+'.join(ordered_fields)
|
||||
signed_data = b'%s+%s' % (signed_data, secret)
|
||||
self.logger.debug(u'generating signature on «%s»', signed_data)
|
||||
self.logger.debug('generating signature on «%s»', signed_data)
|
||||
sign_method = getattr(self, '%s_sign' % self.signature_algo)
|
||||
sign = sign_method(secret, signed_data)
|
||||
self.logger.debug(u'signature «%s»', sign)
|
||||
self.logger.debug('signature «%s»', sign)
|
||||
return force_text(sign)
|
||||
|
||||
@classmethod
|
||||
def guess(self, *, method=None, query_string=None, body=None, headers=None, backends=(), **kwargs):
|
||||
for content in [query_string, body]:
|
||||
if isinstance(content, bytes):
|
||||
try:
|
||||
content = content.decode()
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
if isinstance(content, str):
|
||||
fields = urlparse.parse_qs(content)
|
||||
if not set(fields) >= {SIGNATURE, VADS_CTX_MODE, VADS_AUTH_RESULT}:
|
||||
continue
|
||||
vads_eopayment_trans_id = fields.get(VADS_EOPAYMENT_TRANS_ID)
|
||||
vads_trans_date = fields.get(VADS_TRANS_DATE)
|
||||
vads_trans_id = fields.get(VADS_TRANS_ID)
|
||||
if vads_eopayment_trans_id:
|
||||
return vads_eopayment_trans_id[0]
|
||||
elif vads_trans_date and vads_trans_id:
|
||||
return vads_trans_date[0] + '_' + vads_trans_id[0]
|
||||
return None
|
||||
|
|
|
@ -15,49 +15,39 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
import re
|
||||
import decimal
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import warnings
|
||||
from urllib.parse import parse_qs, urlencode
|
||||
|
||||
import pytz
|
||||
|
||||
from .common import (PaymentCommon, PaymentResponse, URL, PAID, DENIED,
|
||||
CANCELLED, ERROR, ResponseError)
|
||||
from six.moves.urllib.parse import urlencode, parse_qs
|
||||
|
||||
from gettext import gettext as _
|
||||
import logging
|
||||
import warnings
|
||||
from .common import CANCELLED, DENIED, ERROR, PAID, URL, PaymentCommon, PaymentResponse, ResponseError, _
|
||||
|
||||
__all__ = ['Payment']
|
||||
|
||||
TIPI_URL = 'https://www.tipi.budget.gouv.fr/tpa/paiement.web'
|
||||
TIPI_URL = 'https://www.payfip.gouv.fr/tpa/paiement.web'
|
||||
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',
|
||||
'parameters': [
|
||||
{
|
||||
'name': 'numcli',
|
||||
'caption': _(u'Client number'),
|
||||
'help_text': _(u'6 digits number provided by DGFIP'),
|
||||
'caption': _('Client number'),
|
||||
'help_text': _('6 digits number provided by DGFIP'),
|
||||
'validation': lambda s: str.isdigit(s) and (0 < int(s) < 1000000),
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'name': 'service_url',
|
||||
'default': TIPI_URL,
|
||||
'caption': _(u'TIPI service URL'),
|
||||
'help_text': _(u'do not modify if you do not know'),
|
||||
'validation': lambda x: x.startswith('http'),
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'name': 'normal_return_url',
|
||||
'caption': _('Normal return URL (unused by TIPI)'),
|
||||
|
@ -75,9 +65,8 @@ class Payment(PaymentCommon):
|
|||
'default': 'T',
|
||||
'choices': [
|
||||
('T', _('test')),
|
||||
('X', _('production')),
|
||||
('A', _('with user account')),
|
||||
('M', _('manual entry')),
|
||||
('X', _('activation')),
|
||||
('A', _('production')),
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -85,22 +74,38 @@ class Payment(PaymentCommon):
|
|||
|
||||
REFDET_RE = re.compile('^[a-zA-Z0-9]{6,30}$')
|
||||
|
||||
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))
|
||||
minimal_amount = decimal.Decimal('1.0')
|
||||
maximal_amount = decimal.Decimal('100000.0')
|
||||
|
||||
def request(self, amount, email, next_url=None, exer=None, orderid=None,
|
||||
refdet=None, objet=None, saisie=None, **kwargs):
|
||||
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),
|
||||
)
|
||||
|
||||
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:
|
||||
|
@ -158,12 +163,12 @@ class Payment(PaymentCommon):
|
|||
params['objet'] = objet
|
||||
if automatic_return_url:
|
||||
params['urlcl'] = automatic_return_url
|
||||
url = '%s?%s' % (self.service_url, urlencode(params))
|
||||
url = '%s?%s' % (TIPI_URL, urlencode(params))
|
||||
return transaction_id, URL, url
|
||||
|
||||
def response(self, query_string, **kwargs):
|
||||
fields = parse_qs(query_string, True)
|
||||
if not set(fields) >= set(['refdet', 'resultrans']):
|
||||
if not set(fields) >= {'refdet', 'resultrans'}:
|
||||
raise ResponseError('missing refdet or resultrans')
|
||||
for key, value in fields.items():
|
||||
fields[key] = value[0]
|
||||
|
@ -194,4 +199,19 @@ class Payment(PaymentCommon):
|
|||
bank_data=fields,
|
||||
order_id=refdet,
|
||||
transaction_id=refdet,
|
||||
test=test)
|
||||
test=test,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def guess(self, *, method=None, query_string=None, body=None, headers=None, backends=(), **kwargs):
|
||||
for content in [query_string, body]:
|
||||
if isinstance(content, bytes):
|
||||
try:
|
||||
content = content.decode()
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
if isinstance(content, str):
|
||||
fields = parse_qs(content)
|
||||
if 'refdet' in fields and 'resultrans' in fields:
|
||||
return fields['refdet'][0]
|
||||
return None
|
||||
|
|
117
setup.py
117
setup.py
|
@ -4,17 +4,24 @@
|
|||
Setup script for eopayment
|
||||
'''
|
||||
|
||||
import io
|
||||
import subprocess
|
||||
import distutils
|
||||
import distutils.core
|
||||
import setuptools
|
||||
from setuptools.command.sdist import sdist
|
||||
from glob import glob
|
||||
from os.path import splitext, basename, join as pjoin
|
||||
import os
|
||||
from unittest import TextTestRunner, TestLoader
|
||||
import doctest
|
||||
import io
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from distutils.cmd import Command
|
||||
from distutils.command.build import build as _build
|
||||
from glob import glob
|
||||
from os.path import basename
|
||||
from os.path import join as pjoin
|
||||
from os.path import splitext
|
||||
from unittest import TestLoader, TextTestRunner
|
||||
|
||||
import setuptools
|
||||
from setuptools.command.install_lib import install_lib as _install_lib
|
||||
from setuptools.command.sdist import sdist
|
||||
|
||||
|
||||
class TestCommand(distutils.core.Command):
|
||||
|
@ -27,27 +34,25 @@ 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")
|
||||
print('creating VERSION file')
|
||||
if os.path.exists('VERSION'):
|
||||
os.remove('VERSION')
|
||||
version = get_version()
|
||||
|
@ -55,22 +60,22 @@ class eo_sdist(sdist):
|
|||
version_file.write(version)
|
||||
version_file.close()
|
||||
sdist.run(self)
|
||||
print("removing VERSION file")
|
||||
print('removing VERSION file')
|
||||
if os.path.exists('VERSION'):
|
||||
os.remove('VERSION')
|
||||
|
||||
|
||||
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', 'r') as v:
|
||||
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
|
||||
|
@ -82,32 +87,63 @@ def get_version():
|
|||
commit_count = 0
|
||||
version = '%s.post%s+%s' % (real_number, commit_count, commit_hash)
|
||||
else:
|
||||
version = result
|
||||
version = result.replace('.dirty', '+dirty')
|
||||
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'
|
||||
|
||||
|
||||
class compile_translations(Command):
|
||||
description = 'compile message catalogs to MO files via django compilemessages'
|
||||
user_options = []
|
||||
|
||||
def initialize_options(self):
|
||||
pass
|
||||
|
||||
def finalize_options(self):
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
curdir = os.getcwd()
|
||||
try:
|
||||
from django.core.management import call_command
|
||||
|
||||
for path, dirs, files in os.walk('eopayment'):
|
||||
if 'locale' not in dirs:
|
||||
continue
|
||||
os.chdir(os.path.realpath(path))
|
||||
call_command('compilemessages')
|
||||
except ImportError:
|
||||
sys.stderr.write('!!! Please install Django >= 3.2 to build translations\n')
|
||||
finally:
|
||||
os.chdir(curdir)
|
||||
|
||||
|
||||
class build(_build):
|
||||
sub_commands = [('compile_translations', None)] + _build.sub_commands
|
||||
|
||||
|
||||
class install_lib(_install_lib):
|
||||
def run(self):
|
||||
self.run_command('compile_translations')
|
||||
_install_lib.run(self)
|
||||
|
||||
|
||||
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=io.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",
|
||||
author_email="info@entrouvert.com",
|
||||
maintainer="Benjamin Dauvergne",
|
||||
maintainer_email="bdauvergne@entrouvert.com",
|
||||
author_email='info@entrouvert.com',
|
||||
maintainer='Benjamin Dauvergne',
|
||||
maintainer_email='bdauvergne@entrouvert.com',
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Environment :: Web Environment',
|
||||
|
@ -115,19 +151,20 @@ setuptools.setup(
|
|||
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
|
||||
'Operating System :: POSIX',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 3',
|
||||
],
|
||||
packages=['eopayment'],
|
||||
install_requires=[
|
||||
'pycrypto >= 2.5',
|
||||
'pycryptodomex',
|
||||
'pytz',
|
||||
'requests',
|
||||
'six',
|
||||
'click',
|
||||
'zeep >= 2.5',
|
||||
],
|
||||
cmdclass={
|
||||
'build': build,
|
||||
'compile_translations': compile_translations,
|
||||
'install_lib': install_lib,
|
||||
'sdist': eo_sdist,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -16,27 +16,25 @@
|
|||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
import httmock
|
||||
import lxml.etree as ET
|
||||
|
||||
from requests.adapters import HTTPAdapter
|
||||
import pytest
|
||||
from requests import Session
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--save-http-session", action="store_true", help="save HTTP session")
|
||||
parser.addoption("--target-url", help="target URL")
|
||||
parser.addoption('--save-http-session', action='store_true', help='save HTTP session')
|
||||
parser.addoption('--target-url', help='target URL')
|
||||
|
||||
|
||||
class LoggingAdapter(HTTPAdapter):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.history = []
|
||||
super(LoggingAdapter, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def send(self, request, *args, **kwargs):
|
||||
response = super(LoggingAdapter, self).send(request, *args, **kwargs)
|
||||
response = super().send(request, *args, **kwargs)
|
||||
self.history.append((request, response))
|
||||
return response
|
||||
|
||||
|
@ -71,8 +69,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 +101,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
|
||||
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from datetime import date, datetime, timedelta
|
||||
from unittest import mock
|
||||
|
||||
import mock
|
||||
import pytest
|
||||
|
||||
import eopayment
|
||||
|
@ -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': u'3'})
|
||||
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
|
||||
|
@ -112,6 +110,19 @@ def test_get_parameters(monkeypatch):
|
|||
|
||||
def test_payment_status(monkeypatch):
|
||||
_, payment = do_mock_backend(monkeypatch)
|
||||
assert not payment.has_payment_status
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
payment.payment_status('whatever')
|
||||
|
||||
def test_get_min_time_between_transaction(monkeypatch):
|
||||
_, payment = do_mock_backend(monkeypatch)
|
||||
assert payment.get_min_time_between_transactions() == 0
|
||||
|
||||
|
||||
def test_get_minimal_amount(monkeypatch):
|
||||
_, payment = do_mock_backend(monkeypatch)
|
||||
assert payment.get_minimal_amount() is None
|
||||
|
||||
|
||||
def test_get_maximal_amount(monkeypatch):
|
||||
_, payment = do_mock_backend(monkeypatch)
|
||||
assert payment.get_maximal_amount() is None
|
||||
|
|
|
@ -14,18 +14,19 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from six.moves.urllib.parse import urlparse, parse_qs
|
||||
import datetime
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import pytest
|
||||
|
||||
import eopayment
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
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,12 +34,13 @@ 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)
|
||||
assert url.scheme == 'http'
|
||||
assert url.netloc == 'dummy-payment.demo.entrouvert.com'
|
||||
assert url.scheme == 'https'
|
||||
assert url.netloc == 'dummy-payment.entrouvert.com'
|
||||
assert url.path == '/'
|
||||
assert url.fragment == ''
|
||||
qs = {k: v[0] for k, v in parse_qs(url.query).items()}
|
||||
|
|
|
@ -18,85 +18,78 @@ import json
|
|||
|
||||
import pytest
|
||||
import requests
|
||||
import six
|
||||
from httmock import response, urlmatch, HTTMock, with_httmock, all_requests, remember_called
|
||||
from httmock import HTTMock, all_requests, remember_called, response, urlmatch, with_httmock
|
||||
|
||||
import eopayment
|
||||
from eopayment.keyware import Payment
|
||||
|
||||
pytestmark = pytest.mark.skipif(six.PY2, reason='this payment module only supports python3')
|
||||
|
||||
WEBHOOK_URL = 'https://callback.example.com'
|
||||
RETURN_URL = 'https://return.example.com'
|
||||
API_KEY = 'test'
|
||||
|
||||
ORDER_ID = "1c969951-f5f1-4290-ae41-6177961fb3cb"
|
||||
ORDER_ID = '1c969951-f5f1-4290-ae41-6177961fb3cb'
|
||||
QUERY_STRING = 'order_id=' + ORDER_ID
|
||||
|
||||
|
||||
POST_ORDER_RESPONSE = {
|
||||
"amount": 995,
|
||||
"client": {
|
||||
"user_agent": "Testing API"
|
||||
},
|
||||
"created": "2016-07-04T11:41:57.121017+00:00",
|
||||
"currency": "EUR",
|
||||
"description": "Example description",
|
||||
"id": ORDER_ID,
|
||||
"merchant_id": "7131b462-1b7d-489f-aba9-de2f0eadc9dc",
|
||||
"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"
|
||||
'amount': 995,
|
||||
'client': {'user_agent': 'Testing API'},
|
||||
'created': '2016-07-04T11:41:57.121017+00:00',
|
||||
'currency': 'EUR',
|
||||
'description': 'Example description',
|
||||
'id': ORDER_ID,
|
||||
'merchant_id': '7131b462-1b7d-489f-aba9-de2f0eadc9dc',
|
||||
'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',
|
||||
}
|
||||
|
||||
GET_ORDER_RESPONSE = {
|
||||
"amount": 995,
|
||||
"client": {
|
||||
"user_agent": "Testing API"
|
||||
},
|
||||
"created": "2016-07-04T11:41:55.635115+00:00",
|
||||
"currency": "EUR",
|
||||
"description": "Example order #1",
|
||||
"id": ORDER_ID,
|
||||
"last_transaction_added": "2016-07-04T11:41:55.831655+00:00",
|
||||
"merchant_id": "7131b462-1b7d-489f-aba9-de2f0eadc9dc",
|
||||
"merchant_order_id": "EXAMPLE001",
|
||||
"modified": "2016-07-04T11:41:56.215543+00:00",
|
||||
"project_id": "1ef558ed-d77d-470d-b43b-c0f4a131bcef",
|
||||
"return_url": "http://www.example.com/",
|
||||
"status": "completed",
|
||||
"transactions": [
|
||||
'amount': 995,
|
||||
'client': {'user_agent': 'Testing API'},
|
||||
'created': '2016-07-04T11:41:55.635115+00:00',
|
||||
'currency': 'EUR',
|
||||
'description': 'Example order #1',
|
||||
'id': ORDER_ID,
|
||||
'last_transaction_added': '2016-07-04T11:41:55.831655+00:00',
|
||||
'merchant_id': '7131b462-1b7d-489f-aba9-de2f0eadc9dc',
|
||||
'merchant_order_id': 'EXAMPLE001',
|
||||
'modified': '2016-07-04T11:41:56.215543+00:00',
|
||||
'project_id': '1ef558ed-d77d-470d-b43b-c0f4a131bcef',
|
||||
'return_url': 'http://www.example.com/',
|
||||
'status': 'completed',
|
||||
'transactions': [
|
||||
{
|
||||
"amount": 995,
|
||||
"balance": "internal",
|
||||
"created": "2016-07-04T11:41:55.831655+00:00",
|
||||
"credit_debit": "credit",
|
||||
"currency": "EUR",
|
||||
"description": "Example order #1",
|
||||
"events": [
|
||||
'amount': 995,
|
||||
'balance': 'internal',
|
||||
'created': '2016-07-04T11:41:55.831655+00:00',
|
||||
'credit_debit': 'credit',
|
||||
'currency': 'EUR',
|
||||
'description': 'Example order #1',
|
||||
'events': [
|
||||
{
|
||||
"event": "new",
|
||||
"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"
|
||||
'event': 'new',
|
||||
'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',
|
||||
}
|
||||
],
|
||||
"expiration_period": "PT60M",
|
||||
"id": "6c81499c-14e4-4974-99e5-fe72ce019411",
|
||||
"merchant_id": "7131b462-1b7d-489f-aba9-de2f0eadc9dc",
|
||||
"modified": "2016-07-04T11:41:56.065147+00:00",
|
||||
"order_id": ORDER_ID,
|
||||
"payment_method": "ideal",
|
||||
"payment_method_details": {
|
||||
"issuer_id": "INGBNL2A",
|
||||
'expiration_period': 'PT60M',
|
||||
'id': '6c81499c-14e4-4974-99e5-fe72ce019411',
|
||||
'merchant_id': '7131b462-1b7d-489f-aba9-de2f0eadc9dc',
|
||||
'modified': '2016-07-04T11:41:56.065147+00:00',
|
||||
'order_id': ORDER_ID,
|
||||
'payment_method': 'ideal',
|
||||
'payment_method_details': {
|
||||
'issuer_id': 'INGBNL2A',
|
||||
},
|
||||
"payment_url": "https://api.online.emspay.eu/redirect/6c81499c-14e4-4974-99e5-fe72ce019411/to/payment/",
|
||||
"project_id": "1ef558ed-d77d-470d-b43b-c0f4a131bcef",
|
||||
"status": "completed"
|
||||
'payment_url': 'https://api.online.emspay.eu/redirect/6c81499c-14e4-4974-99e5-fe72ce019411/to/payment/',
|
||||
'project_id': '1ef558ed-d77d-470d-b43b-c0f4a131bcef',
|
||||
'status': 'completed',
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
@ -105,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):
|
||||
|
@ -117,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):
|
||||
|
@ -124,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)
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
# eopayment - online payment library
|
||||
# Copyright (C) 2011-2022 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import pytest
|
||||
|
||||
import eopayment
|
||||
|
||||
|
||||
def test_get_backends():
|
||||
assert len(eopayment.get_backends()) > 1
|
||||
|
||||
|
||||
GUESS_TEST_VECTORS = [
|
||||
{
|
||||
'name': 'tipi',
|
||||
'kwargs': {
|
||||
'query_string': 'objet=tout+a+fait&montant=12312&saisie=T&mel=info%40entrouvert.com'
|
||||
'&numcli=12345&exer=9999&refdet=999900000000999999&resultrans=P',
|
||||
},
|
||||
'result': ['tipi', '999900000000999999'],
|
||||
},
|
||||
{
|
||||
'name': 'payfip_ws',
|
||||
'kwargs': {
|
||||
'query_string': 'idOp=1234',
|
||||
},
|
||||
'result': ['payfip_ws', '1234'],
|
||||
},
|
||||
{
|
||||
'name': 'systempayv2-old-transaction-id',
|
||||
'kwargs': {
|
||||
'query_string': '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',
|
||||
},
|
||||
'result': ['systempayv2', '20161013101355_226787'],
|
||||
},
|
||||
{
|
||||
'name': 'systempayv2-eo-trans-id',
|
||||
'kwargs': {
|
||||
'query_string': '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'
|
||||
'&vads_ext_info_eopayment_trans_id=123456',
|
||||
},
|
||||
'result': ['systempayv2', '123456'],
|
||||
},
|
||||
{
|
||||
'name': 'paybox',
|
||||
'kwargs': {
|
||||
'query_string': 'montant=4242&reference=abcdef&code_autorisation=A'
|
||||
'&erreur=00000&date_transaction=20200101&heure_transaction=01%3A01%3A01',
|
||||
},
|
||||
'result': ['paybox', 'abcdef'],
|
||||
},
|
||||
{
|
||||
'name': 'ogone-no-complus',
|
||||
'kwargs': {
|
||||
'query_string': 'orderid=myorder&status=9&payid=3011229363&cn=Us%C3%A9r&ncerror=0'
|
||||
'&trxdate=10%2F24%2F16&acceptance=test123¤cy=eur&amount=7.5',
|
||||
},
|
||||
'result': ['ogone', 'myorder'],
|
||||
},
|
||||
{
|
||||
'name': 'ogone-with-complus',
|
||||
'kwargs': {
|
||||
'query_string': 'complus=neworder&orderid=myorder&status=9&payid=3011229363&cn=Us%C3%A9r'
|
||||
'&ncerror=0&trxdate=10%2F24%2F16&acceptance=test123¤cy=eur&amount=7.5',
|
||||
},
|
||||
'result': ['ogone', 'neworder'],
|
||||
},
|
||||
{
|
||||
'name': 'mollie',
|
||||
'kwargs': {
|
||||
'body': b'id=tr_7UhSN1zuXS',
|
||||
},
|
||||
'result': ['mollie', 'tr_7UhSN1zuXS'],
|
||||
},
|
||||
{
|
||||
'name': 'sips2',
|
||||
'kwargs': {
|
||||
'body': (
|
||||
b'Data=captureDay%3D0%7CcaptureMode%3DAUTHOR_CAPTURE%7CcurrencyCode%3D978%7CmerchantId%3D002001000000001%7CorderChannel%3D'
|
||||
b'INTERNET%7CresponseCode%3D00%7CtransactionDateTime%3D2016-02-01T17%3A44%3A20%2B01%3A00%7C'
|
||||
b'transactionReference%3D668930%7CkeyVersion%3D1%7CacquirerResponseCode%3D00%7Camou'
|
||||
b'nt%3D1200%7CauthorisationId%3D12345%7CcardCSCResultCode%3D4E%7CpanExpiryDate%3D201605%7Cpay'
|
||||
b'mentMeanBrand%3DMASTERCARD%7CpaymentMeanType%3DCARD%7CcustomerIpAddress%3D82.244.203.243%7CmaskedPan'
|
||||
b'%3D5100%23%23%23%23%23%23%23%23%23%23%23%2300%7CorderId%3Dd4903de7027f4d56ac01634fd7ab9526%7CholderAuthentRelegation'
|
||||
b'%3DN%7CholderAuthentStatus%3D3D_ERROR%7CtransactionOrigin%3DINTERNET%7CpaymentPattern%3D'
|
||||
b'ONE_SHOT&Seal=6ca3247765a19b45d25ad54ef4076483e7d55583166bd5ac9c64357aac097602&InterfaceVersion=HP_2.0&Encode='
|
||||
),
|
||||
},
|
||||
'result': ['sips2', '668930'],
|
||||
},
|
||||
{
|
||||
'name': 'dummy',
|
||||
'kwargs': {
|
||||
'query_string': b'transaction_id=123&ok=1&signed=1',
|
||||
},
|
||||
'result': ['dummy', '123'],
|
||||
},
|
||||
{
|
||||
'name': 'notfound',
|
||||
'kwargs': {},
|
||||
'exception': eopayment.BackendNotFound,
|
||||
},
|
||||
{
|
||||
'name': 'notfound-2',
|
||||
'kwargs': {'query_string': None, 'body': [12323], 'headers': {b'1': '2'}},
|
||||
'exception': eopayment.BackendNotFound,
|
||||
},
|
||||
{
|
||||
'name': 'backends-limitation',
|
||||
'kwargs': {
|
||||
'body': b'id=tr_7UhSN1zuXS',
|
||||
'backends': ['payfips_ws'],
|
||||
},
|
||||
'exception': eopayment.BackendNotFound,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('test_vector', GUESS_TEST_VECTORS, ids=lambda tv: tv['name'])
|
||||
def test_guess(test_vector):
|
||||
kwargs, result, exception = test_vector['kwargs'], test_vector.get('result'), test_vector.get('exception')
|
||||
if exception is not None:
|
||||
with pytest.raises(exception):
|
||||
eopayment.Payment.guess(**kwargs)
|
||||
else:
|
||||
assert list(eopayment.Payment.guess(**kwargs)) == result
|
|
@ -16,95 +16,73 @@
|
|||
|
||||
import json
|
||||
|
||||
import requests
|
||||
import six
|
||||
|
||||
import eopayment
|
||||
import pytest
|
||||
from eopayment.mollie import Payment
|
||||
import requests
|
||||
from httmock import remember_called, response, urlmatch, with_httmock
|
||||
|
||||
pytestmark = pytest.mark.skipif(six.PY2, reason='this payment module only supports python3')
|
||||
import eopayment
|
||||
from eopayment.mollie import Payment
|
||||
|
||||
WEBHOOK_URL = 'https://callback.example.com'
|
||||
RETURN_URL = 'https://return.example.com'
|
||||
API_KEY = 'test'
|
||||
|
||||
PAYMENT_ID = "tr_7UhSN1zuXS"
|
||||
PAYMENT_ID = 'tr_7UhSN1zuXS'
|
||||
QUERY_STRING = 'id=' + PAYMENT_ID
|
||||
|
||||
|
||||
POST_PAYMENTS_RESPONSE = {
|
||||
"resource": "payment",
|
||||
"id": PAYMENT_ID,
|
||||
"mode": "test",
|
||||
"createdAt": "2018-03-20T09:13:37+00:00",
|
||||
"amount": {
|
||||
"value": "3.50",
|
||||
"currency": "EUR"
|
||||
},
|
||||
"description": "Payment #12345",
|
||||
"method": "null",
|
||||
"status": "open",
|
||||
"isCancelable": True,
|
||||
"expiresAt": "2018-03-20T09:28:37+00:00",
|
||||
"sequenceType": "oneoff",
|
||||
"redirectUrl": "https://webshop.example.org/payment/12345/",
|
||||
"webhookUrl": "https://webshop.example.org/payments/webhook/",
|
||||
"_links": {
|
||||
"checkout": {
|
||||
"href": "https://www.mollie.com/payscreen/select-method/7UhSN1zuXS",
|
||||
"type": "text/html"
|
||||
'resource': 'payment',
|
||||
'id': PAYMENT_ID,
|
||||
'mode': 'test',
|
||||
'createdAt': '2018-03-20T09:13:37+00:00',
|
||||
'amount': {'value': '3.50', 'currency': 'EUR'},
|
||||
'description': 'Payment #12345',
|
||||
'method': 'null',
|
||||
'status': 'open',
|
||||
'isCancelable': True,
|
||||
'expiresAt': '2018-03-20T09:28:37+00:00',
|
||||
'sequenceType': 'oneoff',
|
||||
'redirectUrl': 'https://webshop.example.org/payment/12345/',
|
||||
'webhookUrl': 'https://webshop.example.org/payments/webhook/',
|
||||
'_links': {
|
||||
'checkout': {
|
||||
'href': 'https://www.mollie.com/payscreen/select-method/7UhSN1zuXS',
|
||||
'type': 'text/html',
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
GET_PAYMENTS_RESPONSE = {
|
||||
"amount": {
|
||||
"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',
|
||||
'details': {
|
||||
'cardAudience': 'consumer',
|
||||
'cardCountryCode': 'NL',
|
||||
'cardHolder': 'T. TEST',
|
||||
'cardLabel': 'Mastercard',
|
||||
'cardNumber': '6787',
|
||||
'cardSecurity': 'normal',
|
||||
'feeRegion': 'other',
|
||||
},
|
||||
"amountRefunded": {
|
||||
"currency": "EUR",
|
||||
"value": "0.00"
|
||||
},
|
||||
"amountRemaining": {
|
||||
"currency": "EUR",
|
||||
"value": "3.50"
|
||||
},
|
||||
"countryCode": "FR",
|
||||
"createdAt": "2020-05-06T13:04:26+00:00",
|
||||
"description": "Publik",
|
||||
"details": {
|
||||
"cardAudience": "consumer",
|
||||
"cardCountryCode": "NL",
|
||||
"cardHolder": "T. TEST",
|
||||
"cardLabel": "Mastercard",
|
||||
"cardNumber": "6787",
|
||||
"cardSecurity": "normal",
|
||||
"feeRegion": "other"
|
||||
},
|
||||
"id": PAYMENT_ID,
|
||||
"metadata": {
|
||||
"email": "test@entrouvert.com",
|
||||
"first_name": "test",
|
||||
"last_name": "test"
|
||||
},
|
||||
"isCancelable": True,
|
||||
"method": "creditcard",
|
||||
"mode": "test",
|
||||
"paidAt": "2020-05-06T14:01:04+00:00",
|
||||
"profileId": "pfl_WNPCPTGepu",
|
||||
"redirectUrl": "https://localhost/lingo/return-payment-backend/3/MTAw.1jWJis.6TbbjwSEurag6v4Z2VCheISBFjw/",
|
||||
"resource": "payment",
|
||||
"sequenceType": "oneoff",
|
||||
"settlementAmount": {
|
||||
"currency": "EUR",
|
||||
"value": "3.50"
|
||||
},
|
||||
"status": "paid",
|
||||
"webhookUrl": "https://localhost/lingo/callback-payment-backend/3/"
|
||||
'id': PAYMENT_ID,
|
||||
'metadata': {'email': 'test@entrouvert.com', 'first_name': 'test', 'last_name': 'test'},
|
||||
'isCancelable': True,
|
||||
'method': 'creditcard',
|
||||
'mode': 'test',
|
||||
'paidAt': '2020-05-06T14:01:04+00:00',
|
||||
'profileId': 'pfl_WNPCPTGepu',
|
||||
'redirectUrl': 'https://localhost/lingo/return-payment-backend/3/MTAw.1jWJis.6TbbjwSEurag6v4Z2VCheISBFjw/',
|
||||
'resource': 'payment',
|
||||
'sequenceType': 'oneoff',
|
||||
'settlementAmount': {'currency': 'EUR', 'value': '3.50'},
|
||||
'status': 'paid',
|
||||
'webhookUrl': 'https://localhost/lingo/callback-payment-backend/3/',
|
||||
}
|
||||
|
||||
|
||||
|
@ -177,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)
|
||||
|
@ -201,6 +181,44 @@ def test_mollie_request(mollie):
|
|||
assert body['redirectUrl'] == RETURN_URL
|
||||
|
||||
|
||||
@with_httmock(add_payment)
|
||||
def test_mollie_request_orderid(mollie):
|
||||
email = 'test@test.com'
|
||||
payment_id, kind, url = mollie.request(2.5, email=email, orderid='1234')
|
||||
|
||||
assert payment_id == PAYMENT_ID
|
||||
assert kind == eopayment.URL
|
||||
assert 'mollie.com/payscreen/' in url
|
||||
|
||||
body = json.loads(add_payment.call['requests'][0].body.decode())
|
||||
assert body['amount']['value'] == '2.5'
|
||||
assert body['amount']['currency'] == 'EUR'
|
||||
assert body['metadata']['email'] == email
|
||||
assert body['metadata']['orderid'] == '1234'
|
||||
assert body['webhookUrl'] == WEBHOOK_URL
|
||||
assert body['redirectUrl'] == RETURN_URL
|
||||
assert body['description'] == '1234'
|
||||
|
||||
|
||||
@with_httmock(add_payment)
|
||||
def test_mollie_request_orderid_subject(mollie):
|
||||
email = 'test@test.com'
|
||||
payment_id, kind, url = mollie.request(2.5, email=email, orderid='1234', subject='Ticket cantine #1234')
|
||||
|
||||
assert payment_id == PAYMENT_ID
|
||||
assert kind == eopayment.URL
|
||||
assert 'mollie.com/payscreen/' in url
|
||||
|
||||
body = json.loads(add_payment.call['requests'][0].body.decode())
|
||||
assert body['amount']['value'] == '2.5'
|
||||
assert body['amount']['currency'] == 'EUR'
|
||||
assert body['metadata']['email'] == email
|
||||
assert body['metadata']['orderid'] == '1234'
|
||||
assert body['webhookUrl'] == WEBHOOK_URL
|
||||
assert body['redirectUrl'] == RETURN_URL
|
||||
assert body['description'] == 'Ticket cantine #1234'
|
||||
|
||||
|
||||
@with_httmock(successful_payment)
|
||||
def test_mollie_response(mollie):
|
||||
payment_response = mollie.response(QUERY_STRING)
|
||||
|
@ -219,8 +237,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]
|
||||
|
@ -228,8 +247,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
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# eopayment - online payment library
|
||||
# Copyright (C) 2011-2020 Entr'ouvert
|
||||
#
|
||||
|
@ -15,117 +14,144 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from unittest import TestCase
|
||||
from urllib import parse as urllib
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
import six
|
||||
from six.moves.urllib import parse as urllib
|
||||
import pytest
|
||||
|
||||
import eopayment
|
||||
import eopayment.ogone as ogone
|
||||
from eopayment import ResponseError
|
||||
|
||||
PSPID = u'2352566ö'
|
||||
|
||||
BACKEND_PARAMS = {
|
||||
'environment': ogone.ENVIRONMENT_TEST,
|
||||
'pspid': PSPID,
|
||||
'sha_in': u'sécret',
|
||||
'sha_out': u'sécret',
|
||||
'automatic_return_url': u'http://example.com/autömatic_réturn_url'
|
||||
}
|
||||
PSPID = '2352566ö'
|
||||
|
||||
|
||||
class OgoneTests(TestCase):
|
||||
if six.PY2:
|
||||
def assertRaisesRegex(self, *args, **kwargs):
|
||||
return self.assertRaisesRegexp(*args, **kwargs)
|
||||
@pytest.fixture(params=[None, 'iso-8859-1', 'utf-8'])
|
||||
def params(request):
|
||||
params = {
|
||||
'environment': ogone.ENVIRONMENT_TEST,
|
||||
'pspid': PSPID,
|
||||
'sha_in': 'sécret',
|
||||
'sha_out': 'sécret',
|
||||
'automatic_return_url': 'http://example.com/autömatic_réturn_url',
|
||||
}
|
||||
encoding = request.param
|
||||
if encoding:
|
||||
params['encoding'] = encoding
|
||||
return params
|
||||
|
||||
def test_request(self):
|
||||
ogone_backend = eopayment.Payment('ogone', BACKEND_PARAMS)
|
||||
amount = '42.42'
|
||||
order_id = u'my ordér'
|
||||
reference, kind, what = ogone_backend.request(
|
||||
amount=amount,
|
||||
orderid=order_id,
|
||||
email='foo@example.com')
|
||||
self.assertEqual(len(reference), 30)
|
||||
assert reference.startswith(order_id)
|
||||
root = ET.fromstring(str(what))
|
||||
self.assertEqual(root.tag, 'form')
|
||||
self.assertEqual(root.attrib['method'], 'POST')
|
||||
self.assertEqual(root.attrib['action'], ogone.ENVIRONMENT_TEST_URL)
|
||||
values = {
|
||||
'CURRENCY': u'EUR',
|
||||
'ORDERID': reference,
|
||||
'PSPID': PSPID,
|
||||
'EMAIL': 'foo@example.com',
|
||||
'AMOUNT': amount.replace('.', ''),
|
||||
'LANGUAGE': 'fr_FR',
|
||||
}
|
||||
values.update({'SHASIGN': ogone_backend.backend.sha_sign_in(values)})
|
||||
for node in root:
|
||||
self.assertIn(node.attrib['type'], ('hidden', 'submit'))
|
||||
self.assertEqual(set(node.attrib.keys()), set(['type', 'name', 'value']))
|
||||
name = node.attrib['name']
|
||||
if node.attrib['type'] == 'hidden':
|
||||
self.assertIn(name, values)
|
||||
self.assertEqual(node.attrib['value'], values[name])
|
||||
|
||||
def test_unicode_response(self):
|
||||
ogone_backend = eopayment.Payment('ogone', BACKEND_PARAMS)
|
||||
order_id = 'myorder'
|
||||
data = {'orderid': u'myorder', 'status': u'9', 'payid': u'3011229363',
|
||||
'cn': u'Usér', 'ncerror': u'0',
|
||||
'trxdate': u'10/24/16', 'acceptance': u'test123',
|
||||
'currency': u'eur', 'amount': u'7.5',
|
||||
'shasign': u'CA4B3C2767B5EFAB33B9122A5D4CF6F27747303D'}
|
||||
# uniformize to utf-8 first
|
||||
for k in data:
|
||||
data[k] = eopayment.common.force_byte(data[k])
|
||||
response = ogone_backend.response(urllib.urlencode(data))
|
||||
assert response.signed
|
||||
self.assertEqual(response.order_id, order_id)
|
||||
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')
|
||||
assert len(reference) == 32
|
||||
root = ET.fromstring(str(what))
|
||||
assert root.tag == 'form'
|
||||
assert root.attrib['method'] == 'POST'
|
||||
assert root.attrib['action'] == ogone.ENVIRONMENT_TEST_URL
|
||||
values = {
|
||||
'CURRENCY': 'EUR',
|
||||
'ORDERID': order_id,
|
||||
'PSPID': PSPID,
|
||||
'EMAIL': 'foo@example.com',
|
||||
'AMOUNT': amount.replace('.', ''),
|
||||
'LANGUAGE': 'fr_FR',
|
||||
'COMPLUS': reference,
|
||||
}
|
||||
values.update({'SHASIGN': ogone_backend.backend.sha_sign_in(values)})
|
||||
for node in root:
|
||||
assert node.attrib['type'] in ('hidden', 'submit')
|
||||
assert set(node.attrib.keys()), {'type', 'name' == 'value'}
|
||||
name = node.attrib['name']
|
||||
if node.attrib['type'] == 'hidden':
|
||||
assert name in values
|
||||
assert node.attrib['value'] == values[name]
|
||||
|
||||
def test_iso_8859_1_response(self):
|
||||
ogone_backend = eopayment.Payment('ogone', BACKEND_PARAMS)
|
||||
order_id = 'lRXK4Rl1N2yIR3R5z7Kc'
|
||||
backend_response = (
|
||||
'orderID=lRXK4Rl1N2yIR3R5z7Kc¤cy=EUR&amount=7%2E5'
|
||||
'&PM=CreditCard&ACCEPTANCE=test123&STATUS=9'
|
||||
'&CARDNO=XXXXXXXXXXXX9999&ED=0118'
|
||||
'&CN=Miha%EF+Serghe%EF&TRXDATE=10%2F24%2F16'
|
||||
'&PAYID=3011228911&NCERROR=0&BRAND=MasterCard'
|
||||
'&IP=80%2E12%2E92%2E47&SHASIGN=C429BE892FACFBFCE5E2CC809B102D866DD3D48C'
|
||||
)
|
||||
response = ogone_backend.response(backend_response)
|
||||
assert response.signed
|
||||
self.assertEqual(response.order_id, order_id)
|
||||
|
||||
def test_bad_response(self):
|
||||
ogone_backend = eopayment.Payment('ogone', BACKEND_PARAMS)
|
||||
data = {'payid': '32100123', 'status': 9, 'ncerror': 0}
|
||||
with self.assertRaisesRegex(ResponseError, 'missing ORDERID, PAYID, STATUS or NCERROR'):
|
||||
ogone_backend.response(urllib.urlencode(data))
|
||||
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'))
|
||||
# uniformize to utf-8 first
|
||||
for k in data:
|
||||
data[k] = eopayment.common.force_byte(data[k], encoding=params.get('encoding', 'iso-8859-1'))
|
||||
response = ogone_backend.response(urllib.urlencode(data))
|
||||
assert response.signed
|
||||
assert response.order_id == order_id
|
||||
|
||||
def test_bank_transfer_response(self):
|
||||
ogone_backend = eopayment.Payment('ogone', BACKEND_PARAMS)
|
||||
data = {
|
||||
'orderid': u'myorder',
|
||||
'status': u'41',
|
||||
'payid': u'3011229363',
|
||||
'cn': u'User',
|
||||
'ncerror': u'0',
|
||||
'trxdate': u'10/24/16',
|
||||
'brand': 'Bank transfer',
|
||||
'pm': 'bank transfer',
|
||||
'currency': u'eur',
|
||||
'amount': u'7.5',
|
||||
'shasign': u'944CBD1E010BA4945415AE4B16CC40FD533F6CE2',
|
||||
}
|
||||
# uniformize to utf-8 first
|
||||
for k in data:
|
||||
data[k] = eopayment.common.force_byte(data[k])
|
||||
response = ogone_backend.response(urllib.urlencode(data))
|
||||
assert response.signed
|
||||
assert response.result == eopayment.WAITING
|
||||
|
||||
def test_iso_8859_1_response():
|
||||
params = {
|
||||
'environment': ogone.ENVIRONMENT_TEST,
|
||||
'pspid': PSPID,
|
||||
'sha_in': 'sécret',
|
||||
'sha_out': 'sécret',
|
||||
'automatic_return_url': 'http://example.com/autömatic_réturn_url',
|
||||
}
|
||||
ogone_backend = eopayment.Payment('ogone', params)
|
||||
order_id = 'lRXK4Rl1N2yIR3R5z7Kc'
|
||||
backend_response = (
|
||||
'orderID=lRXK4Rl1N2yIR3R5z7Kc¤cy=EUR&amount=7%2E5'
|
||||
'&PM=CreditCard&ACCEPTANCE=test123&STATUS=9'
|
||||
'&CARDNO=XXXXXXXXXXXX9999&ED=0118'
|
||||
'&CN=Miha%EF+Serghe%EF&TRXDATE=10%2F24%2F16'
|
||||
'&PAYID=3011228911&NCERROR=0&BRAND=MasterCard'
|
||||
'&IP=80%2E12%2E92%2E47&SHASIGN=C429BE892FACFBFCE5E2CC809B102D866DD3D48C'
|
||||
)
|
||||
response = ogone_backend.response(backend_response)
|
||||
assert response.signed
|
||||
assert response.order_id == order_id
|
||||
|
||||
|
||||
def test_bad_response(params):
|
||||
ogone_backend = eopayment.Payment('ogone', params)
|
||||
data = {'payid': '32100123', 'status': 9, 'ncerror': 0}
|
||||
with pytest.raises(ResponseError, match='missing ORDERID, PAYID, STATUS or NCERROR'):
|
||||
ogone_backend.response(urllib.urlencode(data))
|
||||
|
||||
|
||||
def test_bank_transfer_response(params):
|
||||
ogone_backend = eopayment.Payment('ogone', params)
|
||||
data = {
|
||||
'orderid': 'myorder',
|
||||
'status': '41',
|
||||
'payid': '3011229363',
|
||||
'cn': 'User',
|
||||
'ncerror': '0',
|
||||
'trxdate': '10/24/16',
|
||||
'brand': 'Bank transfer',
|
||||
'pm': 'bank transfer',
|
||||
'currency': 'eur',
|
||||
'amount': '7.5',
|
||||
'shasign': '944CBD1E010BA4945415AE4B16CC40FD533F6CE2',
|
||||
}
|
||||
# uniformize to expected encoding
|
||||
for k in data:
|
||||
data[k] = eopayment.common.force_byte(data[k], encoding=params.get('encoding', 'iso-8859-1'))
|
||||
response = ogone_backend.response(urllib.urlencode(data))
|
||||
assert response.signed
|
||||
assert response.result == eopayment.WAITING
|
||||
|
||||
# check utf-8 based signature is also ok
|
||||
data['shasign'] = b'0E35F687ACBEAA6CA769E0ADDBD0863EB6C1678A'
|
||||
response = ogone_backend.response(urllib.urlencode(data))
|
||||
assert response.signed
|
||||
assert response.result == eopayment.WAITING
|
||||
|
||||
# check invalid signature is not marked ok
|
||||
data['shasign'] = b'0000000000000000000000000000000000000000'
|
||||
response = ogone_backend.response(urllib.urlencode(data))
|
||||
assert not response.signed
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# eopayment - online payment library
|
||||
# Copyright (C) 2011-2020 Entr'ouvert
|
||||
#
|
||||
|
@ -16,57 +15,59 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import codecs
|
||||
from unittest import TestCase
|
||||
from decimal import Decimal
|
||||
import base64
|
||||
import mock
|
||||
import six
|
||||
from six.moves.urllib import parse as urllib
|
||||
import codecs
|
||||
from decimal import Decimal
|
||||
from unittest import TestCase, mock
|
||||
from urllib import parse as urllib
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
import eopayment.paybox as paybox
|
||||
import eopayment
|
||||
import pytest
|
||||
|
||||
import eopayment
|
||||
import eopayment.paybox as paybox
|
||||
|
||||
BACKEND_PARAMS = {
|
||||
'platform': u'test',
|
||||
'site': u'12345678',
|
||||
'rang': u'001',
|
||||
'identifiant': u'12345678',
|
||||
'platform': 'test',
|
||||
'site': '12345678',
|
||||
'rang': '001',
|
||||
'identifiant': '12345678',
|
||||
'shared_secret': (
|
||||
u'0123456789ABCDEF0123456789ABCDEF01234'
|
||||
u'56789ABCDEF0123456789ABCDEF0123456789'
|
||||
u'ABCDEF0123456789ABCDEF0123456789ABCDE'
|
||||
u'F0123456789ABCDEF'
|
||||
'0123456789ABCDEF0123456789ABCDEF01234'
|
||||
'56789ABCDEF0123456789ABCDEF0123456789'
|
||||
'ABCDEF0123456789ABCDEF0123456789ABCDE'
|
||||
'F0123456789ABCDEF'
|
||||
),
|
||||
'automatic_return_url': u'http://example.com/callback',
|
||||
'automatic_return_url': 'http://example.com/callback',
|
||||
}
|
||||
|
||||
|
||||
class PayboxTests(TestCase):
|
||||
if six.PY2:
|
||||
def assertRaisesRegex(self, *args, **kwargs):
|
||||
return self.assertRaisesRegexp(*args, **kwargs)
|
||||
|
||||
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', u'12345678'],
|
||||
['PBX_RANG', u'32'],
|
||||
['PBX_IDENTIFIANT', u'12345678'],
|
||||
['PBX_TOTAL', u'999'],
|
||||
['PBX_DEVISE', u'978'],
|
||||
['PBX_CMD', u'appel à Paybox'],
|
||||
['PBX_PORTEUR', u'test@paybox.com'],
|
||||
['PBX_RETOUR', u'Mt:M;Ref:R;Auto:A;Erreur:E'],
|
||||
['PBX_HASH', u'SHA512'],
|
||||
['PBX_TIME', u'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'
|
||||
|
@ -76,16 +77,32 @@ class PayboxTests(TestCase):
|
|||
self.assertIn('PBX_HMAC', d)
|
||||
self.assertEqual(d['PBX_HMAC'], result)
|
||||
|
||||
def test_request(self):
|
||||
@mock.patch('eopayment.paybox.Payment.make_pbx_archivage', wraps=True)
|
||||
def test_request(self, make_pbx_archivage):
|
||||
backend = eopayment.Payment('paybox', BACKEND_PARAMS)
|
||||
# fix PBX_ARCHIVAGE for test purpose
|
||||
make_pbx_archivage.return_value = '4YQEFSFZSNWA'
|
||||
|
||||
time = '2015-07-15T18:26:32+02:00'
|
||||
email = 'bdauvergne@entrouvert.com'
|
||||
order_id = '20160216'
|
||||
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,
|
||||
total_quantity='1',
|
||||
first_name='Kyan <oo>',
|
||||
last_name='Khojandi',
|
||||
address1='169 rue du Château',
|
||||
zipcode='75014',
|
||||
city='Paris',
|
||||
country_code=250,
|
||||
)
|
||||
self.assertEqual(kind, eopayment.FORM)
|
||||
self.assertEqual(transaction_id, '%s!%s' % (transaction, order_id))
|
||||
root = ET.fromstring(str(what))
|
||||
|
@ -108,22 +125,37 @@ class PayboxTests(TestCase):
|
|||
'PBX_DEVISE': '978',
|
||||
'PBX_HASH': 'SHA512',
|
||||
'PBX_HMAC': (
|
||||
'A9F561A6EA79390F1741A6B72872470BC1A1688E4581'
|
||||
'F097EC80B99D2038413AB350F2F5429FFA4F8D426D99'
|
||||
'B72E038164642F6F9BA10D46837EE486EEB944A2'
|
||||
'EC9B753691D804F15B3369BEF9CA49F20585BE32E84'
|
||||
'9A9758815903CE5A89822C251C7EBC712145FCA6321'
|
||||
'C6A6F90EE45EBEC618FFC8B7A69CC23E1BFC6CACC7'
|
||||
),
|
||||
'PBX_ARCHIVAGE': '20160216',
|
||||
'PBX_ARCHIVAGE': '4YQEFSFZSNWA',
|
||||
'PBX_REPONDRE_A': 'http://example.com/callback',
|
||||
'PBX_AUTOSEULE': 'N'
|
||||
'PBX_AUTOSEULE': 'N',
|
||||
'PBX_BILLING': (
|
||||
'<?xml version="1.0" encoding="utf-8"?>'
|
||||
'<Billing><Address>'
|
||||
'<FirstName>Kyan <oo></FirstName>'
|
||||
'<LastName>Khojandi</LastName>'
|
||||
'<Address1>169 rue du Château</Address1>'
|
||||
'<ZipCode>75014</ZipCode>'
|
||||
'<City>Paris</City>'
|
||||
'<CountryCode>250</CountryCode>'
|
||||
'</Address></Billing>'
|
||||
),
|
||||
'PBX_SHOPPINGCART': (
|
||||
'<?xml version="1.0" encoding="utf-8"?>'
|
||||
'<shoppingcart><total><totalQuantity>1</totalQuantity></total></shoppingcart>'
|
||||
),
|
||||
}
|
||||
|
||||
form_params = {}
|
||||
for node in root:
|
||||
self.assertIn(node.attrib['type'], ('hidden', 'submit'))
|
||||
if node.attrib['type'] == 'submit':
|
||||
self.assertEqual(set(node.attrib.keys()), set(['type', 'value']))
|
||||
self.assertEqual(set(node.attrib.keys()), {'type', 'value'})
|
||||
if node.attrib['type'] == 'hidden':
|
||||
self.assertEqual(set(node.attrib.keys()), set(['type', 'name', 'value']))
|
||||
self.assertEqual(set(node.attrib.keys()), {'type', 'name', 'value'})
|
||||
name = node.attrib['name']
|
||||
form_params[name] = node.attrib['value']
|
||||
assert form_params == expected_form_values
|
||||
|
@ -140,12 +172,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 = dict(
|
||||
((node.attrib['name'], node.attrib['value']) for node in root if node.attrib['type'] == 'hidden'))
|
||||
form_params = {
|
||||
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')
|
||||
|
||||
|
@ -153,13 +186,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 = dict(((
|
||||
node.attrib['name'], node.attrib['value']) for node in root
|
||||
if node.attrib['type'] == 'hidden'))
|
||||
form_params = {
|
||||
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')
|
||||
|
||||
|
@ -169,13 +207,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 = dict(((
|
||||
node.attrib['name'], node.attrib['value']) for node in root
|
||||
if node.attrib['type'] == 'hidden'))
|
||||
form_params = {
|
||||
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')
|
||||
|
||||
|
@ -190,12 +233,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 = dict(
|
||||
((node.attrib['name'], node.attrib['value']) for node in root if node.attrib['type'] == 'hidden'))
|
||||
form_params = {
|
||||
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):
|
||||
|
@ -209,7 +253,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
|
||||
|
@ -230,20 +275,22 @@ class PayboxTests(TestCase):
|
|||
'reference': ['830657461681'],
|
||||
}
|
||||
backend_raw_response = (
|
||||
u'NUMTRANS=0013989865&NUMAPPEL=0030378572&NUMQUESTION=0013989862'
|
||||
u'&SITE=1999888&RANG=32&AUTORISATION=XXXXXX&CODEREPONSE=00000'
|
||||
u'&COMMENTAIRE=Demande traitée avec succès&REFABONNE=&PORTEUR='
|
||||
'NUMTRANS=0013989865&NUMAPPEL=0030378572&NUMQUESTION=0013989862'
|
||||
'&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": u"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)
|
||||
|
@ -266,7 +313,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)
|
||||
|
@ -280,7 +327,7 @@ class PayboxTests(TestCase):
|
|||
self.assertEqual(requests_post.call_args[0][0], 'https://ppps.paybox.com/PPPS.php')
|
||||
|
||||
with mock.patch('eopayment.paybox.requests.post') as requests_post:
|
||||
error_response = u"""CODEREPONSE=00015&COMMENTAIRE=PAYBOX : Transaction non trouvée"""
|
||||
error_response = """CODEREPONSE=00015&COMMENTAIRE=PAYBOX : Transaction non trouvée"""
|
||||
response = mock.Mock(status_code=200, text=error_response)
|
||||
requests_post.return_value = response
|
||||
self.assertRaisesRegex(
|
||||
|
@ -288,7 +335,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()
|
||||
|
@ -297,12 +345,12 @@ class PayboxTests(TestCase):
|
|||
bank_data = {
|
||||
'numero_transaction': ['13957441'],
|
||||
'numero_appel': ['30310733'],
|
||||
'reference': ['830657461681']
|
||||
'reference': ['830657461681'],
|
||||
}
|
||||
backend_raw_response = (
|
||||
u'NUMTRANS=0013989865&NUMAPPEL=0030378572&NUMQUESTION=0013989862'
|
||||
u'&SITE=1999888&RANG=32&AUTORISATION=XXXXXX&CODEREPONSE=00000'
|
||||
u'&COMMENTAIRE=Demande traitée avec succès&REFABONNE=&PORTEUR='
|
||||
'NUMTRANS=0013989865&NUMAPPEL=0030378572&NUMQUESTION=0013989862'
|
||||
'&SITE=1999888&RANG=32&AUTORISATION=XXXXXX&CODEREPONSE=00000'
|
||||
'&COMMENTAIRE=Demande traitée avec succès&REFABONNE=&PORTEUR='
|
||||
)
|
||||
|
||||
with mock.patch('eopayment.paybox.requests.post') as requests_post:
|
||||
|
@ -334,15 +382,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 = dict((
|
||||
(node.attrib['name'], node.attrib['value']) for node in root
|
||||
if node.attrib['type'] == 'hidden'))
|
||||
form_params = {
|
||||
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')
|
||||
|
||||
|
@ -352,10 +397,46 @@ FBFKOZhgBJnkC+l6+XhT4aYWKaQ4ocmOMV92yjeXTE4='''
|
|||
orderid=order_id,
|
||||
transaction_id=transaction,
|
||||
time=time,
|
||||
manual_validation=True)
|
||||
manual_validation=True,
|
||||
)
|
||||
root = ET.fromstring(str(what))
|
||||
form_params = dict((
|
||||
(node.attrib['name'], node.attrib['value']) for node in root
|
||||
if node.attrib['type'] == 'hidden'))
|
||||
form_params = {
|
||||
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')
|
||||
|
||||
def test_invalid_signature(self):
|
||||
backend = eopayment.Payment('paybox', BACKEND_PARAMS)
|
||||
order_id = '20160216'
|
||||
transaction = '1234'
|
||||
reference = '%s!%s' % (transaction, order_id)
|
||||
data = {
|
||||
'montant': '4242',
|
||||
'reference': reference,
|
||||
'code_autorisation': 'A',
|
||||
'erreur': '00000',
|
||||
'date_transaction': '20200101',
|
||||
'heure_transaction': '01:01:01',
|
||||
'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),
|
||||
],
|
||||
)
|
||||
def test_param_validation(name, value, result):
|
||||
for param in paybox.Payment.description['parameters']:
|
||||
if param['name'] == name:
|
||||
assert param['validation'](value) is result
|
||||
break
|
||||
else:
|
||||
assert False, 'param %s not found' % name
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
# eopayment - online payment library
|
||||
# Copyright (C) 2011-2020 Entr'ouvert
|
||||
|
@ -16,24 +15,20 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import lxml.etree as ET
|
||||
import mock
|
||||
|
||||
import pytz
|
||||
from unittest import mock
|
||||
|
||||
import httmock
|
||||
import lxml.etree as ET
|
||||
import pytest
|
||||
|
||||
import pytz
|
||||
from zeep.plugins import HistoryPlugin
|
||||
|
||||
import eopayment
|
||||
from eopayment.payfip_ws import PayFiP, PayFiPError, normalize_objet
|
||||
|
||||
|
||||
NUMCLI = '090909'
|
||||
NOTIF_URL = 'https://notif.payfip.example.com/'
|
||||
REDIRECT_URL = 'https://redirect.payfip.example.com/'
|
||||
|
@ -56,7 +51,7 @@ def freezer(freezer):
|
|||
return freezer
|
||||
|
||||
|
||||
class PayFiPHTTMock(object):
|
||||
class PayFiPHTTMock:
|
||||
def __init__(self, history_name):
|
||||
history_path = 'tests/data/payfip-%s.json' % history_name
|
||||
with open(history_path) as fd:
|
||||
|
@ -77,13 +72,21 @@ def history_name(request):
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def history(history_name, request):
|
||||
def history(history_name, request, zeep_history_plugin):
|
||||
if 'update_data' not in request.keywords:
|
||||
history_mock = PayFiPHTTMock(history_name)
|
||||
with httmock.HTTMock(history_mock.mock):
|
||||
yield history_mock
|
||||
else:
|
||||
yield None
|
||||
history_path = 'tests/data/payfip-%s.json' % history_name
|
||||
d = [
|
||||
(xmlindent(exchange['sent']['envelope']), xmlindent(exchange['received']['envelope']))
|
||||
for exchange in zeep_history_plugin._buffer
|
||||
]
|
||||
content = json.dumps(d)
|
||||
with open(history_path, 'wb') as fd:
|
||||
fd.write(content)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -97,39 +100,42 @@ 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')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def payfip(history, history_name, request):
|
||||
history = HistoryPlugin()
|
||||
def zeep_history_plugin():
|
||||
return HistoryPlugin()
|
||||
|
||||
@httmock.urlmatch()
|
||||
def raise_on_request(url, request):
|
||||
# ensure we do not access network
|
||||
from requests.exceptions import RequestException
|
||||
raise RequestException('huhu')
|
||||
|
||||
@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': [history]})
|
||||
payfip = PayFiP(
|
||||
wsdl_url='./eopayment/resource/PaiementSecuriseService.wsdl',
|
||||
zeep_client_kwargs={'plugins': [zeep_history_plugin]},
|
||||
)
|
||||
yield payfip
|
||||
|
||||
# add @pytest.mark.update_data to test to update fixtures data
|
||||
if 'update_data' in request.keywords:
|
||||
history_path = 'tests/data/payfip-%s.json' % history_name
|
||||
d = [
|
||||
(xmlindent(exchange['sent']['envelope']),
|
||||
xmlindent(exchange['received']['envelope']))
|
||||
for exchange in history._buffer
|
||||
]
|
||||
content = json.dumps(d)
|
||||
with open(history_path, 'wb') as fd:
|
||||
fd.write(content)
|
||||
|
||||
@pytest.fixture
|
||||
def payfip_history(history, payfip, zeep_history_plugin, request):
|
||||
yield
|
||||
|
||||
|
||||
def set_history_name(name):
|
||||
|
@ -137,18 +143,20 @@ def set_history_name(name):
|
|||
def decorator(func):
|
||||
func.history_name = name
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
# pytestmark = pytest.mark.update_data
|
||||
|
||||
|
||||
def test_get_client_info(payfip):
|
||||
def test_get_client_info(history, payfip):
|
||||
result = payfip.get_info_client(NUMCLI)
|
||||
assert result.numcli == NUMCLI
|
||||
assert result.libelleN2 == 'POUETPOUET'
|
||||
|
||||
|
||||
def test_get_idop_ok(payfip):
|
||||
def test_get_idop_ok(history, payfip):
|
||||
result = payfip.get_idop(
|
||||
numcli=NUMCLI,
|
||||
exer='2019',
|
||||
|
@ -158,11 +166,12 @@ def test_get_idop_ok(payfip):
|
|||
objet='coucou',
|
||||
url_notification=NOTIF_URL,
|
||||
url_redirect=REDIRECT_URL,
|
||||
saisie='T')
|
||||
saisie='T',
|
||||
)
|
||||
assert result == 'cc0cb210-1cd4-11ea-8cca-0213ad91a103'
|
||||
|
||||
|
||||
def test_get_idop_refdet_error(payfip):
|
||||
def test_get_idop_refdet_error(history, payfip):
|
||||
with pytest.raises(PayFiPError, match='.*R3.*Le format.*REFDET.*conforme'):
|
||||
payfip.get_idop(
|
||||
numcli=NUMCLI,
|
||||
|
@ -173,10 +182,11 @@ def test_get_idop_refdet_error(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):
|
||||
def test_get_idop_adresse_mel_incorrect(payfip, payfip_history):
|
||||
with pytest.raises(PayFiPError, match='.*A2.*Adresse.*incorrecte'):
|
||||
payfip.get_idop(
|
||||
numcli=NUMCLI,
|
||||
|
@ -187,10 +197,11 @@ def test_get_idop_adresse_mel_incorrect(payfip):
|
|||
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(payfip):
|
||||
def test_get_info_paiement_ok(history, payfip):
|
||||
result = payfip.get_info_paiement('cc0cb210-1cd4-11ea-8cca-0213ad91a103')
|
||||
assert {k: result[k] for k in result} == {
|
||||
'dattrans': '12122019',
|
||||
|
@ -204,11 +215,11 @@ def test_get_info_paiement_ok(payfip):
|
|||
'objet': 'coucou',
|
||||
'refdet': 'EFEFAEFG',
|
||||
'resultrans': 'V',
|
||||
'saisie': 'T'
|
||||
'saisie': 'T',
|
||||
}
|
||||
|
||||
|
||||
def test_get_info_paiement_P1(payfip, freezer):
|
||||
def test_get_info_paiement_P1(history, payfip, freezer):
|
||||
# idop par pas encore reçu par la plate-forme ou déjà nettoyé (la nuit)
|
||||
with pytest.raises(PayFiPError, match='.*P1.*IdOp incorrect.*'):
|
||||
payfip.get_info_paiement('cc0cb210-1cd4-11ea-8cca-0213ad91a103')
|
||||
|
@ -216,6 +227,7 @@ def test_get_info_paiement_P1(payfip, freezer):
|
|||
|
||||
@set_history_name('test_get_info_paiement_P1')
|
||||
def test_P1_and_payment_status(history, backend):
|
||||
assert backend.has_payment_status
|
||||
response = backend.payment_status(transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103')
|
||||
assert response.result == eopayment.EXPIRED
|
||||
|
||||
|
@ -223,14 +235,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
|
||||
|
||||
|
||||
|
@ -238,84 +254,100 @@ 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
|
||||
|
||||
|
||||
@set_history_name('test_get_info_paiement_P1')
|
||||
def test_P1_and_payment_status_utc_naive_now_later(payfip, 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
|
||||
|
||||
|
||||
def test_get_info_paiement_P5(payfip):
|
||||
def test_get_info_paiement_P5(history, payfip):
|
||||
# idop reçu par la plate-forme mais transaction en cours
|
||||
with pytest.raises(PayFiPError, match='.*P5.*sultat de la transaction non connu.*'):
|
||||
payfip.get_info_paiement('cc0cb210-1cd4-11ea-8cca-0213ad91a103')
|
||||
|
||||
|
||||
@set_history_name('test_get_info_paiement_P5')
|
||||
def test_P5_and_payment_status(payfip, backend, freezer):
|
||||
def test_P5_and_payment_status(history, payfip, backend, freezer):
|
||||
response = backend.payment_status(transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103')
|
||||
assert response.result == eopayment.WAITING
|
||||
|
||||
|
||||
@set_history_name('test_get_info_paiement_P5')
|
||||
def test_P5_and_payment_status_utc_aware_now(payfip, backend, freezer):
|
||||
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(payfip, backend, freezer):
|
||||
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
|
||||
|
||||
|
||||
@set_history_name('test_get_info_paiement_P5')
|
||||
def test_P5_and_payment_status_utc_aware_now_later(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
|
||||
|
||||
|
||||
@set_history_name('test_get_info_paiement_P5')
|
||||
def test_P5_and_payment_status_utc_naive_now_later(payfip, backend, freezer):
|
||||
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
|
||||
|
||||
|
||||
def test_payment_ok(payfip, backend):
|
||||
def test_payment_ok(history, payfip, backend):
|
||||
payment_id, kind, url = backend.request(
|
||||
amount='10.00',
|
||||
email=MEL,
|
||||
# make test deterministic
|
||||
refdet='201912261758460053903194')
|
||||
refdet='201912261758460053903194',
|
||||
)
|
||||
|
||||
assert payment_id == 'cc0cb210-1cd4-11ea-8cca-0213ad91a103'
|
||||
assert kind == eopayment.URL
|
||||
assert url == 'https://www.tipi.budget.gouv.fr/tpa/paiementws.web?idop=cc0cb210-1cd4-11ea-8cca-0213ad91a103'
|
||||
assert url == 'https://www.payfip.gouv.fr/tpa/paiementws.web?idop=cc0cb210-1cd4-11ea-8cca-0213ad91a103'
|
||||
|
||||
response = backend.response('idop=%s' % payment_id)
|
||||
assert response.result == eopayment.PAID
|
||||
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
|
||||
|
||||
|
||||
|
@ -324,11 +356,12 @@ 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
|
||||
assert url == 'https://www.tipi.budget.gouv.fr/tpa/paiementws.web?idop=cc0cb210-1cd4-11ea-8cca-0213ad91a103'
|
||||
assert url == 'https://www.payfip.gouv.fr/tpa/paiementws.web?idop=cc0cb210-1cd4-11ea-8cca-0213ad91a103'
|
||||
|
||||
response = backend.response('idop=%s' % payment_id)
|
||||
assert response.result == eopayment.DENIED
|
||||
|
@ -341,7 +374,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
|
||||
|
||||
|
||||
|
@ -350,15 +385,16 @@ 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
|
||||
assert url == 'https://www.tipi.budget.gouv.fr/tpa/paiementws.web?idop=cc0cb210-1cd4-11ea-8cca-0213ad91a103'
|
||||
assert url == 'https://www.payfip.gouv.fr/tpa/paiementws.web?idop=cc0cb210-1cd4-11ea-8cca-0213ad91a103'
|
||||
|
||||
response = backend.response('idop=%s' % payment_id)
|
||||
assert response.result == eopayment.CANCELLED
|
||||
assert response.bank_status == 'cancelled CB'
|
||||
assert response.result == eopayment.WAITING
|
||||
assert response.bank_status == 'cancelled CB - still waiting as idop is still active'
|
||||
assert response.order_id == payment_id
|
||||
assert response.transaction_id == '201912261758460053903194 cc0cb210-1cd4-11ea-8cca-0213ad91a103'
|
||||
|
||||
|
@ -367,8 +403,20 @@ 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.WAITING
|
||||
assert response.bank_status == 'cancelled CB - still waiting as idop is still active'
|
||||
|
||||
freezer.move_to(datetime.timedelta(minutes=20, seconds=1))
|
||||
|
||||
history.counter = 1 # only the response
|
||||
response = backend.payment_status(
|
||||
transaction_id='cc0cb210-1cd4-11ea-8cca-0213ad91a103', transaction_date=now
|
||||
)
|
||||
assert response.result == eopayment.CANCELLED
|
||||
assert response.bank_status == 'cancelled CB'
|
||||
|
||||
|
||||
def test_normalize_objet():
|
||||
|
@ -385,7 +433,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]
|
||||
|
@ -411,7 +460,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]
|
||||
|
@ -437,7 +487,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]
|
||||
|
@ -463,7 +514,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]
|
||||
|
@ -479,3 +531,26 @@ def test_invalid_transaction_id_invalid_orderid(get_idop, backend):
|
|||
'url_notification': NOTIF_URL,
|
||||
'url_redirect': REDIRECT_URL,
|
||||
}
|
||||
|
||||
|
||||
def test_get_min_time_between_transactions(backend):
|
||||
assert backend.get_min_time_between_transactions() == 20 * 60
|
||||
|
||||
|
||||
def test_get_minimal_amount(backend):
|
||||
assert backend.get_minimal_amount() is not None
|
||||
|
||||
|
||||
def test_get_maximal_amount(backend):
|
||||
assert backend.get_maximal_amount() is not None
|
||||
|
||||
|
||||
def test_request_error(payfip, backend):
|
||||
with pytest.raises(PayFiPError):
|
||||
with httmock.HTTMock(raise_on_request):
|
||||
payment_id, kind, url = backend.request(
|
||||
amount='10.00',
|
||||
email=MEL,
|
||||
# make test deterministic
|
||||
refdet='201912261758460053903194',
|
||||
)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
# eopayment - online payment library
|
||||
# Copyright (C) 2011-2020 Entr'ouvert
|
||||
|
@ -16,12 +15,10 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
import zeep.transports
|
||||
|
||||
import eopayment
|
||||
|
@ -44,13 +41,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
|
||||
|
||||
|
||||
|
@ -58,19 +62,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):
|
||||
|
@ -83,7 +87,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',
|
||||
|
@ -96,4 +100,3 @@ def test_response(backend_factory):
|
|||
}
|
||||
# Check bank_data is JSON serializable
|
||||
json.dumps(response.bank_data)
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# eopayment - online payment library
|
||||
# Copyright (C) 2011-2020 Entr'ouvert
|
||||
#
|
||||
|
@ -16,32 +15,32 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import eopayment
|
||||
import pytest
|
||||
|
||||
import eopayment
|
||||
|
||||
|
||||
def test_build_request():
|
||||
backend = eopayment.Payment('sips2', {})
|
||||
transaction, f, form = backend.request(amount=u'12', last_name=u'Foo',
|
||||
first_name=u'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 u'lix000000' not in data[0]['value']
|
||||
assert 'lix000000' not in data[0]['value']
|
||||
|
||||
transaction, f, form = backend.request(amount=u'12')
|
||||
transaction, f, form = backend.request(amount='12')
|
||||
data = [f for f in form.fields if f['name'] == 'Data']
|
||||
assert 'statementReference=%s' % transaction in data[0]['value']
|
||||
|
||||
transaction, f, form = backend.request(amount=u'12', info1='foobar')
|
||||
transaction, f, form = backend.request(amount='12', info1='foobar')
|
||||
data = [f for f in form.fields if f['name'] == 'Data']
|
||||
assert 'statementReference=foobar' in data[0]['value']
|
||||
|
||||
transaction, f, form = backend.request(amount=u'12', info1='foobar', capture_day=u'1')
|
||||
transaction, f, form = backend.request(amount='12', info1='foobar', capture_day='1')
|
||||
data = [f for f in form.fields if f['name'] == 'Data']
|
||||
assert 'captureDay=1' in data[0]['value']
|
||||
|
||||
|
||||
def test_options():
|
||||
payment = eopayment.Payment('sips2', {'capture_mode': u'VALIDATION'})
|
||||
payment = eopayment.Payment('sips2', {'capture_mode': 'VALIDATION'})
|
||||
assert payment.backend.get_data()['captureMode'] == 'VALIDATION'
|
||||
|
||||
payment = eopayment.Payment('sips2', {})
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# eopayment - online payment library
|
||||
# Copyright (C) 2011-2020 Entr'ouvert
|
||||
#
|
||||
|
@ -17,21 +16,20 @@
|
|||
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from urllib import parse as urlparse
|
||||
|
||||
import pytest
|
||||
from six.moves.urllib import parse as urlparse
|
||||
|
||||
import eopayment
|
||||
from eopayment.systempayv2 import Payment, VADS_CUST_FIRST_NAME, \
|
||||
VADS_CUST_LAST_NAME, PAID
|
||||
from eopayment import ResponseError
|
||||
from eopayment.systempayv2 import PAID, VADS_CUST_FIRST_NAME, VADS_CUST_LAST_NAME, Payment
|
||||
|
||||
PARAMS = {
|
||||
'secret_test': u'1122334455667788',
|
||||
'vads_site_id': u'12345678',
|
||||
'vads_ctx_mode': u'TEST',
|
||||
'vads_trans_date': u'20090501193530',
|
||||
'signature_algo': 'sha1'
|
||||
'secret_test': '1122334455667788',
|
||||
'vads_site_id': '12345678',
|
||||
'vads_ctx_mode': 'TEST',
|
||||
'vads_trans_date': '20090501193530',
|
||||
'signature_algo': 'sha1',
|
||||
}
|
||||
|
||||
|
||||
|
@ -49,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': u'Jean Michél',
|
||||
'last_name': u'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'
|
||||
|
@ -70,18 +63,20 @@ def test_systempayv2(caplog):
|
|||
# check that user first and last names are unicode
|
||||
for field in form.fields:
|
||||
if field['name'] in (VADS_CUST_FIRST_NAME, VADS_CUST_LAST_NAME):
|
||||
assert field['value'] in (u'Jean Michél', u'Mihaï')
|
||||
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
|
||||
|
@ -91,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
|
||||
|
@ -112,14 +109,11 @@ def test_systempayv2(caplog):
|
|||
|
||||
def test_systempayv2_deferred_payment():
|
||||
default_params = {
|
||||
'secret_test': u'1122334455667788',
|
||||
'vads_site_id': u'12345678',
|
||||
'vads_ctx_mode': u'TEST',
|
||||
}
|
||||
default_data = {
|
||||
'amount': 15.24, 'orderid': '654321', 'first_name': u'John',
|
||||
'last_name': u'Doe'
|
||||
'secret_test': '1122334455667788',
|
||||
'vads_site_id': '12345678',
|
||||
'vads_ctx_mode': 'TEST',
|
||||
}
|
||||
default_data = {'amount': 15.24, 'orderid': '654321', 'first_name': 'John', 'last_name': 'Doe'}
|
||||
|
||||
# default vads_capture_delay used
|
||||
params = default_params.copy()
|
||||
|
@ -145,21 +139,18 @@ 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'
|
||||
|
||||
|
||||
def test_manual_validation():
|
||||
params = {
|
||||
'secret_test': u'1122334455667788',
|
||||
'vads_site_id': u'12345678',
|
||||
'vads_ctx_mode': u'TEST',
|
||||
}
|
||||
data = {
|
||||
'amount': 15.24, 'orderid': '654321', 'first_name': u'John',
|
||||
'last_name': u'Doe'
|
||||
'secret_test': '1122334455667788',
|
||||
'vads_site_id': '12345678',
|
||||
'vads_ctx_mode': 'TEST',
|
||||
}
|
||||
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())
|
||||
|
@ -173,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
|
||||
|
|
|
@ -16,13 +16,13 @@
|
|||
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
from six.moves.urllib.parse import urlparse, parse_qs
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import pytest
|
||||
import pytz
|
||||
|
||||
import eopayment
|
||||
import eopayment.tipi
|
||||
import pytest
|
||||
|
||||
|
||||
def test_tipi():
|
||||
|
@ -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)
|
||||
|
@ -115,3 +114,16 @@ def test_tipi_orderid_not_refdef_compatible():
|
|||
assert parsed_qs['refdet'][0].startswith(datetime.datetime.now().strftime('%Y%m%d'))
|
||||
assert 'coucou' in parsed_qs['objet'][0]
|
||||
assert 'F12-12-12' in parsed_qs['objet'][0]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def payment():
|
||||
return eopayment.Payment('tipi', {'numcli': '12345'})
|
||||
|
||||
|
||||
def test_get_minimal_amount(payment):
|
||||
assert payment.get_minimal_amount() is not None
|
||||
|
||||
|
||||
def test_get_maximal_amount(payment):
|
||||
assert payment.get_maximal_amount() is not None
|
||||
|
|
7
tox.ini
7
tox.ini
|
@ -4,7 +4,7 @@
|
|||
# and then run "tox" from this directory.
|
||||
|
||||
[tox]
|
||||
envlist = py2,py3
|
||||
envlist = py3,codestyle
|
||||
toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/eopayment/{env:BRANCH_NAME:}
|
||||
|
||||
[testenv]
|
||||
|
@ -12,8 +12,8 @@ toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/eopayment/{env:BRANCH_NAME:}
|
|||
setenv =
|
||||
SETUPTOOLS_USE_DISTUTILS=stdlib
|
||||
commands =
|
||||
py2: py.test {posargs: --junitxml=junit-{envname}.xml --cov-report xml --cov-report html --cov=eopayment/ tests}
|
||||
py3: py.test {posargs: --junitxml=junit-{envname}.xml --cov-report xml --cov-report html --cov=eopayment/ tests}
|
||||
py3: py.test {posargs: --junitxml=junit-{envname}.xml --cov-report xml --cov-report html --cov-config .coveragerc --cov=eopayment/ tests}
|
||||
codestyle: pre-commit run --all-files --show-diff-on-failure
|
||||
usedevelop = True
|
||||
deps = coverage
|
||||
pytest
|
||||
|
@ -22,6 +22,7 @@ deps = coverage
|
|||
mock<4
|
||||
httmock
|
||||
lxml
|
||||
pre-commit
|
||||
|
||||
[pytest]
|
||||
filterwarnings =
|
||||
|
|
Loading…
Reference in New Issue