summaryrefslogtreecommitdiffstats
path: root/eopayment/ogone.py
blob: 136462b8fb91ec9bf90545d07a494ad721da8bd4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
# -*- coding: utf-8 -*-
import hashlib
import string
import urlparse
from decimal import Decimal, ROUND_HALF_UP

from common import (PaymentCommon, PaymentResponse, FORM, CANCELLED, PAID,
        ERROR, Form, DENIED, ACCEPTED, ORDERID_TRANSACTION_SEPARATOR,
        WAITING, 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'
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': N_('Système de paiement Ogone / Ingenico Payment System e-Commerce'),
        'parameters': [
            {
                'name': 'normal_return_url',
                'caption': N_('Normal return URL'),
                'default': '',
                'required': True,
            },
            {
                'name': 'automatic_return_url',
                'caption': N_('Automatic return URL (ignored, must be set in Ogone backoffice)'),
                'required': False,
            },
            {'name': 'environment',
                'default': ENVIRONMENT_TEST,
                'caption': N_(u'Environnement'),
                'choices': ENVIRONMENT,
            },
            {'name': 'pspid',
                'caption': N_(u"Nom d'affiliation dans le système"),
                'required': True,
            },
            {'name': 'language',
                'caption': N_(u'Langage'),
                'default': 'fr_FR',
                'choices': (('fr_FR', N_('français')),),
            },
            {'name': 'hash_algorithm',
                'caption': N_(u'Algorithme de hachage'),
                'default': 'sha1',
            },
            {'name': 'sha_in',
                'caption': N_(u'Clé SHA-IN'),
                'required': True,
            },
            {'name': 'sha_out',
                'caption': N_(u'Clé SHA-OUT'),
                'required': True,
            },
            {'name': 'currency',
                'caption': N_(u'Monnaie'),
                'default': 'EUR',
                'choices': ('EUR',),
            },
        ]
    }

    def sha_sign(self, algo, key, params, keep):
        '''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]
        tosign = key.join(values)
        tosign += key
        tosign = force_byte(tosign)
        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_out(self, params):
        return self.sha_sign(self.hash_algorithm, self.sha_out, params, SHA_OUT_PARAMS)

    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, **kwargs):

        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
        # arrondi comptable francais
        amount = amount.quantize(Decimal('1.'), rounding=ROUND_HALF_UP)
        params = {
                'AMOUNT': unicode(amount),
                'ORDERID': reference,
                'PSPID': self.pspid,
                'LANGUAGE': language,
                'CURRENCY': self.currency,
        }
        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.iteritems():
            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 reference, FORM, form

    def response(self, query_string, **kwargs):
        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']):
            raise ResponseError()

        # uniformize iso-8859-1 encoded values
        for key in params:
            params[key] = force_text(params[key], 'iso-8859-1')
        reference = params['ORDERID']
        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
        if status == '1':
            result = CANCELLED
        elif status == '2':
            result = DENIED
        elif status == '5':
            result = ACCEPTED
        elif status == '9':
            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
        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)