627 lines
14 KiB
Python
627 lines
14 KiB
Python
# eopayment - online payment library
|
||
# Copyright (C) 2011-2020 Entr'ouvert
|
||
#
|
||
# This program is free software: you can redistribute it and/or modify it
|
||
# under the terms of the GNU Affero General Public License as published
|
||
# by the Free Software Foundation, either version 3 of the License, or
|
||
# (at your option) any later version.
|
||
#
|
||
# This program is distributed in the hope that it will be useful,
|
||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
# GNU Affero General Public License for more details.
|
||
#
|
||
# You should have received a copy of the GNU Affero General Public License
|
||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
||
from decimal import Decimal, ROUND_HALF_UP
|
||
from six.moves.urllib import parse as urlparse
|
||
import hashlib
|
||
import uuid
|
||
|
||
from .common import (
|
||
PaymentCommon, PaymentResponse, FORM, CANCELLED, PAID,
|
||
ERROR, Form, DENIED, ACCEPTED, ORDERID_TRANSACTION_SEPARATOR,
|
||
WAITING, ResponseError, force_byte, force_text, _
|
||
)
|
||
|
||
|
||
ENVIRONMENT_TEST = 'TEST'
|
||
ENVIRONMENT_TEST_URL = 'https://secure.ogone.com/ncol/test/orderstandard.asp'
|
||
ENVIRONMENT_PROD = 'PROD'
|
||
ENVIRONMENT_PROD_URL = 'https://secure.ogone.com/ncol/prod/orderstandard.asp'
|
||
ENVIRONMENT = [ENVIRONMENT_TEST, ENVIRONMENT_PROD]
|
||
|
||
# https://payment-services.ingenico.com/ogone/support/~/media/kdb/integration%20guides/sha-in_params.ashx
|
||
SHA_IN_PARAMS = """
|
||
ACCEPTANCE
|
||
ACCEPTURL
|
||
ADDMATCH
|
||
ADDRMATCH
|
||
AIACTIONNUMBER
|
||
AIAGIATA
|
||
AIAIRNAME
|
||
AIAIRTAX
|
||
AIBOOKIND*XX*
|
||
AICARRIER*XX*
|
||
AICHDET
|
||
AICLASS*XX*
|
||
AICONJTI
|
||
AIDEPTCODE
|
||
AIDESTCITY*XX*
|
||
AIDESTCITYL*XX*
|
||
AIEXTRAPASNAME*XX*
|
||
AIEYCD
|
||
AIFLDATE*XX*
|
||
AIFLNUM*XX*
|
||
AIGLNUM
|
||
AIINVOICE
|
||
AIIRST
|
||
AIORCITY*XX*
|
||
AIORCITYL*XX*
|
||
AIPASNAME
|
||
AIPROJNUM
|
||
AISTOPOV*XX*
|
||
AITIDATE
|
||
AITINUM
|
||
AITINUML*XX*
|
||
AITYPCH
|
||
AIVATAMNT
|
||
AIVATAPPL
|
||
ALIAS
|
||
ALIASOPERATION
|
||
ALIASPERSISTEDAFTERUSE
|
||
ALIASUSAGE
|
||
ALLOWCORRECTION
|
||
AMOUNT
|
||
AMOUNT*XX*
|
||
AMOUNTHTVA
|
||
AMOUNTTVA
|
||
ARP_TRN
|
||
BACKURL
|
||
BATCHID
|
||
BGCOLOR
|
||
BLVERNUM
|
||
BIC
|
||
BIN
|
||
BRAND
|
||
BRANDVISUAL
|
||
BUTTONBGCOLOR
|
||
BUTTONTXTCOLOR
|
||
CANCELURL
|
||
CARDNO
|
||
CATALOGURL
|
||
CAVV_3D
|
||
CAVVALGORITHM_3D
|
||
CERTID
|
||
CHECK_AAV
|
||
CIVILITY
|
||
CN
|
||
COM
|
||
COMPLUS
|
||
CONVCCY
|
||
COSTCENTER
|
||
COSTCODE
|
||
CREDITCODE
|
||
CREDITDEBIT
|
||
CUID
|
||
CURRENCY
|
||
CVC
|
||
CVCFLAG
|
||
DATA
|
||
DATATYPE
|
||
DATEIN
|
||
DATEOUT
|
||
DBXML
|
||
DCC_COMMPERC
|
||
DCC_CONVAMOUNT
|
||
DCC_CONVCCY
|
||
DCC_EXCHRATE
|
||
DCC_EXCHRATETS
|
||
DCC_INDICATOR
|
||
DCC_MARGINPERC
|
||
DCC_REF
|
||
DCC_SOURCE
|
||
DCC_VALID
|
||
DECLINEURL
|
||
DELIVERYDATE
|
||
DEVICE
|
||
DISCOUNTRATE
|
||
DISPLAYMODE
|
||
ECI
|
||
ECI_3D
|
||
ECOM_BILLTO_COMPANY
|
||
ECOM_BILLTO_POSTAL_CITY
|
||
ECOM_BILLTO_POSTAL_COUNTRYCODE
|
||
ECOM_BILLTO_POSTAL_COUNTY
|
||
ECOM_BILLTO_POSTAL_NAME_FIRST
|
||
ECOM_BILLTO_POSTAL_NAME_LAST
|
||
ECOM_BILLTO_POSTAL_NAME_PREFIX
|
||
ECOM_BILLTO_POSTAL_POSTALCODE
|
||
ECOM_BILLTO_POSTAL_STREET_LINE1
|
||
ECOM_BILLTO_POSTAL_STREET_LINE2
|
||
ECOM_BILLTO_POSTAL_STREET_LINE3
|
||
ECOM_BILLTO_POSTAL_STREET_NUMBER
|
||
ECOM_BILLTO_TELECOM_MOBILE_NUMBER
|
||
ECOM_BILLTO_TELECOM_PHONE_NUMBER
|
||
ECOM_CONSUMERID
|
||
ECOM_CONSUMER_GENDER
|
||
ECOM_CONSUMEROGID
|
||
ECOM_CONSUMERORDERID
|
||
ECOM_CONSUMERUSERALIAS
|
||
ECOM_CONSUMERUSERPWD
|
||
ECOM_CONSUMERUSERID
|
||
ECOM_ESTIMATEDDELIVERYDATE
|
||
ECOM_ESTIMATEDELIVERYDATE
|
||
ECOM_PAYMENT_CARD_EXPDATE_MONTH
|
||
ECOM_PAYMENT_CARD_EXPDATE_YEAR
|
||
ECOM_PAYMENT_CARD_NAME
|
||
ECOM_PAYMENT_CARD_VERIFICATION
|
||
ECOM_SHIPMETHOD
|
||
ECOM_SHIPMETHODDETAILS
|
||
ECOM_SHIPMETHODSPEED
|
||
ECOM_SHIPMETHODTYPE
|
||
ECOM_SHIPTO_COMPANY
|
||
ECOM_SHIPTO_DOB
|
||
ECOM_SHIPTO_ONLINE_EMAIL
|
||
ECOM_SHIPTO_POSTAL_CITY
|
||
ECOM_SHIPTO_POSTAL_COUNTRYCODE
|
||
ECOM_SHIPTO_POSTAL_COUNTY
|
||
ECOM_SHIPTO_POSTAL_NAME_FIRST
|
||
ECOM_SHIPTO_POSTAL_NAME_LAST
|
||
ECOM_SHIPTO_POSTAL_NAME_PREFIX
|
||
ECOM_SHIPTO_POSTAL_POSTALCODE
|
||
ECOM_SHIPTO_POSTAL_STATE
|
||
ECOM_SHIPTO_POSTAL_STREET_LINE1
|
||
ECOM_SHIPTO_POSTAL_STREET_LINE2
|
||
ECOM_SHIPTO_POSTAL_STREET_NUMBER
|
||
ECOM_SHIPTO_TELECOM_FAX_NUMBER
|
||
ECOM_SHIPTO_TELECOM_MOBILE_NUMBER
|
||
ECOM_SHIPTO_TELECOM_PHONE_NUMBER
|
||
ECOM_SHIPTO_TVA
|
||
ED
|
||
EMAIL
|
||
EXCEPTIONURL
|
||
EXCLPMLIST
|
||
EXECUTIONDATE*XX*
|
||
FACEXCL*XX*
|
||
FACTOTAL*XX*
|
||
FIRSTCALL
|
||
FLAG3D
|
||
FONTTYPE
|
||
FORCECODE1
|
||
FORCECODE2
|
||
FORCECODEHASH
|
||
FORCEPROCESS
|
||
FORCETP
|
||
FP_ACTIV
|
||
GENERIC_BL
|
||
GIROPAY_ACCOUNT_NUMBER
|
||
GIROPAY_BLZ
|
||
GIROPAY_OWNER_NAME
|
||
GLOBORDERID
|
||
GUID
|
||
HDFONTTYPE
|
||
HDTBLBGCOLOR
|
||
HDTBLTXTCOLOR
|
||
HEIGHTFRAME
|
||
HOMEURL
|
||
HTTP_ACCEPT
|
||
HTTP_USER_AGENT
|
||
INCLUDE_BIN
|
||
INCLUDE_COUNTRIES
|
||
INITIAL_REC_TRN
|
||
INVDATE
|
||
INVDISCOUNT
|
||
INVLEVEL
|
||
INVORDERID
|
||
ISSUERID
|
||
IST_MOBILE
|
||
ITEM_COUNT
|
||
ITEMATTRIBUTES*XX*
|
||
ITEMCATEGORY*XX*
|
||
ITEMCOMMENTS*XX*
|
||
ITEMDESC*XX*
|
||
ITEMDISCOUNT*XX*
|
||
ITEMFDMPRODUCTCATEG*XX*
|
||
ITEMID*XX*
|
||
ITEMNAME*XX*
|
||
ITEMPRICE*XX*
|
||
ITEMQUANT*XX*
|
||
ITEMQUANTORIG*XX*
|
||
ITEMUNITOFMEASURE*XX*
|
||
ITEMVAT*XX*
|
||
ITEMVATCODE*XX*
|
||
ITEMWEIGHT*XX*
|
||
LANGUAGE
|
||
LEVEL1AUTHCPC
|
||
LIDEXCL*XX*
|
||
LIMITCLIENTSCRIPTUSAGE
|
||
LINE_REF
|
||
LINE_REF1
|
||
LINE_REF2
|
||
LINE_REF3
|
||
LINE_REF4
|
||
LINE_REF5
|
||
LINE_REF6
|
||
LIST_BIN
|
||
LIST_COUNTRIES
|
||
LOGO
|
||
MANDATEID
|
||
MAXITEMQUANT*XX*
|
||
MERCHANTID
|
||
MODE
|
||
MTIME
|
||
MVER
|
||
NETAMOUNT
|
||
OPERATION
|
||
ORDERID
|
||
ORDERSHIPCOST
|
||
ORDERSHIPMETH
|
||
ORDERSHIPTAX
|
||
ORDERSHIPTAXCODE
|
||
ORIG
|
||
OR_INVORDERID
|
||
OR_ORDERID
|
||
OWNERADDRESS
|
||
OWNERADDRESS2
|
||
OWNERCTY
|
||
OWNERTELNO
|
||
OWNERTELNO2
|
||
OWNERTOWN
|
||
OWNERZIP
|
||
PAIDAMOUNT
|
||
PARAMPLUS
|
||
PARAMVAR
|
||
PAYID
|
||
PAYMETHOD
|
||
PM
|
||
PMLIST
|
||
PMLISTPMLISTTYPE
|
||
PMLISTTYPE
|
||
PMLISTTYPEPMLIST
|
||
PMTYPE
|
||
POPUP
|
||
POST
|
||
PSPID
|
||
PSWD
|
||
RECIPIENTACCOUNTNUMBER
|
||
RECIPIENTDOB
|
||
RECIPIENTLASTNAME
|
||
RECIPIENTZIP
|
||
REF
|
||
REFER
|
||
REFID
|
||
REFKIND
|
||
REF_CUSTOMERID
|
||
REF_CUSTOMERREF
|
||
REGISTRED
|
||
REMOTE_ADDR
|
||
REQGENFIELDS
|
||
RNPOFFERT
|
||
RTIMEOUT
|
||
RTIMEOUTREQUESTEDTIMEOUT
|
||
SCORINGCLIENT
|
||
SEQUENCETYPE
|
||
SETT_BATCH
|
||
SID
|
||
SIGNDATE
|
||
STATUS_3D
|
||
SUBSCRIPTION_ID
|
||
SUB_AM
|
||
SUB_AMOUNT
|
||
SUB_COM
|
||
SUB_COMMENT
|
||
SUB_CUR
|
||
SUB_ENDDATE
|
||
SUB_ORDERID
|
||
SUB_PERIOD_MOMENT
|
||
SUB_PERIOD_MOMENT_M
|
||
SUB_PERIOD_MOMENT_WW
|
||
SUB_PERIOD_NUMBER
|
||
SUB_PERIOD_NUMBER_D
|
||
SUB_PERIOD_NUMBER_M
|
||
SUB_PERIOD_NUMBER_WW
|
||
SUB_PERIOD_UNIT
|
||
SUB_STARTDATE
|
||
SUB_STATUS
|
||
TAAL
|
||
TAXINCLUDED*XX*
|
||
TBLBGCOLOR
|
||
TBLTXTCOLOR
|
||
TID
|
||
TITLE
|
||
TOTALAMOUNT
|
||
TP
|
||
TRACK2
|
||
TXTBADDR2
|
||
TXTCOLOR
|
||
TXTOKEN
|
||
TXTOKENTXTOKENPAYPAL
|
||
TXSHIPPING
|
||
TXSHIPPINGLOCATIONPROFILE
|
||
TXURL
|
||
TXVERIFIER
|
||
TYPE_COUNTRY
|
||
UCAF_AUTHENTICATION_DATA
|
||
UCAF_PAYMENT_CARD_CVC2
|
||
UCAF_PAYMENT_CARD_EXPDATE_MONTH
|
||
UCAF_PAYMENT_CARD_EXPDATE_YEAR
|
||
UCAF_PAYMENT_CARD_NUMBER
|
||
USERID
|
||
USERTYPE
|
||
VERSION
|
||
WBTU_MSISDN
|
||
WBTU_ORDERID
|
||
WEIGHTUNIT
|
||
WIN3DS
|
||
WITHROOT
|
||
XDL
|
||
""".split()
|
||
|
||
# https://payment-services.ingenico.com/ogone/support/~/media/kdb/integration%20guides/sha-out_params.ashx
|
||
SHA_OUT_PARAMS = """
|
||
AAVADDRESS
|
||
AAVCHECK
|
||
AAVMAIL
|
||
AAVNAME
|
||
AAVPHONE
|
||
AAVZIP
|
||
ACCEPTANCE
|
||
ALIAS
|
||
AMOUNT
|
||
BIC
|
||
BIN
|
||
BRAND
|
||
CARDNO
|
||
CCCTY
|
||
CN
|
||
COLLECTOR_BIC
|
||
COLLECTOR_IBAN
|
||
COMPLUS
|
||
CREATION_STATUS
|
||
CREDITDEBIT
|
||
CURRENCY
|
||
CVCCHECK
|
||
DCC_COMMPERCENTAGE
|
||
DCC_CONVAMOUNT
|
||
DCC_CONVCCY
|
||
DCC_EXCHRATE
|
||
DCC_EXCHRATESOURCE
|
||
DCC_EXCHRATETS
|
||
DCC_INDICATOR
|
||
DCC_MARGINPERCENTAGE
|
||
DCC_VALIDHOURS
|
||
DEVICEID
|
||
DIGESTCARDNO
|
||
ECI
|
||
ED
|
||
EMAIL
|
||
ENCCARDNO
|
||
FXAMOUNT
|
||
FXCURRENCY
|
||
IP
|
||
IPCTY
|
||
MANDATEID
|
||
MOBILEMODE
|
||
NBREMAILUSAGE
|
||
NBRIPUSAGE
|
||
NBRIPUSAGE_ALLTX
|
||
NBRUSAGE
|
||
NCERROR
|
||
ORDERID
|
||
PAYID
|
||
PAYIDSUB
|
||
PAYMENT_REFERENCE
|
||
PM
|
||
SCO_CATEGORY
|
||
SCORING
|
||
SEQUENCETYPE
|
||
SIGNDATE
|
||
STATUS
|
||
SUBBRAND
|
||
SUBSCRIPTION_ID
|
||
TRXDATE
|
||
VC
|
||
""".split()
|
||
|
||
|
||
class Payment(PaymentCommon):
|
||
# See http://payment-services.ingenico.com/fr/fr/ogone/support/guides/integration%20guides/e-commerce
|
||
description = {
|
||
'caption': _('Ogone / Ingenico Payment System e-Commerce'),
|
||
'parameters': [
|
||
{
|
||
'name': 'normal_return_url',
|
||
'caption': _('Normal return URL'),
|
||
'default': '',
|
||
'required': True,
|
||
},
|
||
{
|
||
'name': 'automatic_return_url',
|
||
'caption': _('Automatic return URL (ignored, must be set in Ogone backoffice)'),
|
||
'required': False,
|
||
},
|
||
{
|
||
'name': 'environment',
|
||
'default': ENVIRONMENT_TEST,
|
||
'caption': 'Environnement',
|
||
'choices': ENVIRONMENT,
|
||
},
|
||
{
|
||
'name': 'pspid',
|
||
'caption': "Nom d'affiliation dans le système",
|
||
'required': True,
|
||
},
|
||
{
|
||
'name': 'language',
|
||
'caption': _('Language'),
|
||
'default': 'fr_FR',
|
||
'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': 'Algorithme de hachage',
|
||
'default': 'sha1',
|
||
},
|
||
{
|
||
'name': 'sha_in',
|
||
'caption': 'Clé SHA-IN',
|
||
'required': True,
|
||
},
|
||
{
|
||
'name': 'sha_out',
|
||
'caption': 'Clé SHA-OUT',
|
||
'required': True,
|
||
},
|
||
{
|
||
'name': 'currency',
|
||
'caption': 'Monnaie',
|
||
'default': 'EUR',
|
||
'choices': ('EUR',),
|
||
},
|
||
]
|
||
}
|
||
|
||
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 = ['%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=encoding)
|
||
hashing = getattr(hashlib, algo)
|
||
return hashing(tosign).hexdigest().upper()
|
||
|
||
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, 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:
|
||
return ENVIRONMENT_TEST_URL
|
||
if self.environment == ENVIRONMENT_PROD:
|
||
return ENVIRONMENT_PROD_URL
|
||
raise NotImplementedError('unknown environment %s' % self.environment)
|
||
|
||
def request(self, amount, orderid=None, name=None, email=None,
|
||
language=None, description=None, transaction_id=None,
|
||
**kwargs):
|
||
# 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
|
||
|
||
language = language or self.language
|
||
# convertir en centimes
|
||
amount = Decimal(amount) * 100
|
||
# arrondi comptable francais
|
||
amount = amount.quantize(Decimal('1.'), rounding=ROUND_HALF_UP)
|
||
params = {
|
||
'AMOUNT': force_text(amount),
|
||
'ORDERID': orderid,
|
||
'PSPID': self.pspid,
|
||
'LANGUAGE': language,
|
||
'CURRENCY': self.currency,
|
||
'COMPLUS': complus,
|
||
}
|
||
if self.normal_return_url:
|
||
params['ACCEPTURL'] = self.normal_return_url
|
||
params['BACKURL'] = self.normal_return_url
|
||
params['CANCELURL'] = self.normal_return_url
|
||
params['DECLINEURL'] = self.normal_return_url
|
||
params['EXCEPTIONURL'] = self.normal_return_url
|
||
if name:
|
||
params['CN'] = name
|
||
if email:
|
||
params['EMAIL'] = email
|
||
if description:
|
||
params['COM'] = description
|
||
for key, value in kwargs.items():
|
||
params[key.upper()] = value
|
||
params['SHASIGN'] = self.sha_sign_in(params)
|
||
# uniformize all values to UTF-8 string
|
||
for key in params:
|
||
params[key] = force_text(params[key])
|
||
url = self.get_request_url()
|
||
form = Form(
|
||
url=url,
|
||
method='POST',
|
||
fields=[{'type': 'hidden',
|
||
'name': key,
|
||
'value': params[key]} for key in params])
|
||
return complus, FORM, form
|
||
|
||
def response(self, query_string, **kwargs):
|
||
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')
|
||
|
||
# py2: decode binary strings in query-string
|
||
for key in params:
|
||
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')
|
||
# 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':
|
||
result = DENIED
|
||
elif status == '5':
|
||
result = ACCEPTED
|
||
elif status in ('9', '95'):
|
||
result = PAID
|
||
elif len(status) == 2 and status[1] == '1':
|
||
# Statuses with two digits represent either ‘intermediary'
|
||
# situations or abnormal events. When the second digit is:
|
||
# 1, this means the payment processing is on hold. (e.g.
|
||
# status 91: payment waiting/pending)
|
||
result = WAITING
|
||
else:
|
||
self.logger.error('response STATUS=%s NCERROR=%s NCERRORPLUS=%s',
|
||
status, error, params.get('NCERRORPLUS', ''))
|
||
result = ERROR
|
||
# extract reference from received order id
|
||
|
||
return PaymentResponse(
|
||
result=result,
|
||
signed=signed,
|
||
bank_data=params,
|
||
order_id=complus or orderid,
|
||
transaction_id=transaction_id)
|