source: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/helpers.py @ 17730

Last change on this file since 17730 was 17730, checked in by Henrik Bettermann, 7 months ago

Implement Etranzact Credo platform payments.

  • Property svn:keywords set to Id
File size: 16.3 KB
Line 
1## $Id: helpers.py 17730 2024-04-02 20:25:29Z henrik $
2##
3## Copyright (C) 2017 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""General helper functions for the etranzact module in custom packages.
19"""
20import grok
21import re
22from datetime import datetime
23from ssl import SSLError
24from datetime import datetime
25from urllib import urlencode
26from urllib2 import urlopen
27from urlparse import parse_qs
28import httplib
29import hashlib
30import json
31from zope.event import notify
32from waeup.kofa.payments.interfaces import IPayer
33from waeup.kofa.utils.helpers import extract_formvars
34from kofacustom.nigeria.interfaces import MessageFactory as _
35
36ERROR_PART1 = (
37        'PayeeName=N/A~'
38        + 'Faculty=N/A~'
39        + 'Department=N/A~'
40        + 'Level=N/A~'
41        + 'ProgrammeType=N/A~'
42        + 'StudyType=N/A~'
43        + 'Session=N/A~'
44        + 'PayeeID=N/A~'
45        + 'Amount=N/A~'
46        + 'FeeStatus=')
47ERROR_PART2 = (
48        '~Semester=N/A~'
49        + 'PaymentType=N/A~'
50        + 'MatricNumber=N/A~'
51        + 'Email=N/A~'
52        + 'PhoneNumber=N/A')
53
54def write_payments_log(id, payment):
55    payment.logger.info(
56        '%s,%s,%s,%s,%s,%s,%s,%s,,,' % (
57        id, payment.p_id, payment.p_category,
58        payment.amount_auth, payment.r_code,
59        payment.provider_amt, payment.gateway_amt,
60        payment.thirdparty_amt))
61
62def query_history(host, terminal_id, transaction_id, https):
63    headers={"Content-type": "application/x-www-form-urlencoded",
64             "Accept": "text/plain"}
65    url = "/webconnect/v3/query.jsp"
66    if https:
67        h = httplib.HTTPSConnection(host)
68    else:
69        h = httplib.HTTPConnection(host)
70    args = {'TERMINAL_ID': terminal_id,
71            'TRANSACTION_ID': transaction_id,
72            }
73    #args['RESPONSE_URL'] = responseurl
74    h.request('POST', url, urlencode(args), headers)
75    response = h.getresponse()
76    if response.status!=200:
77        return 'Connection error (%s, %s)' % (response.status, response.reason), None
78    raw = response.read()
79    return raw, extract_formvars(raw)
80
81 # A sample caller response sent to the RESPONSE_URL
82
83 # http://salsa:8080/app/applicants/cbt2015/449072/p5679522929425/receive_etranzact?
84 # AMOUNT=3333.0&
85 # DESCRIPTION=&
86 # CHECKSUM=8aab3904652f8ba69ebed42d3bae80a2&
87 # EMAIL=aa%40aa.de&
88 # SUCCESS=C&
89 # MESSAGE=Cancel&
90 # LOGO_URL=https%3A%2F%2Fiuokada.waeup.org%2Fstatic_custom%2Fiou_logo.png&
91 # RESPONSE_URL=http%3A%2F%2Fsalsa%3A8080%2Fapp%2Fapplicants%2Fcbt2015%2F449072%2Fp5679522929425%2Freceive_etranzact&
92 # CURRENCY_CODE=NGN&
93 # TERMINAL_ID=0000000001&
94 # TRANSACTION_ID=p5679522929425&
95 # MERCHANT_CODE=0339990001&
96 # RESPONSE_CODE=C&
97 # FINAL_CHECKSUM=E524590DBFAB719EEE428C778FFF1650&
98 # STATUS_REASON=Cancel&
99 # TRANS_NUM=01ESA20190913062149AHGUHQ&
100 # CARD_NO=null&
101 # CARD_TYPE=null
102
103 # A sample query response
104
105 # <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
106 #     "http://www.w3.org/TR/html4/loose.dtd">
107 # <script language='javascript'>
108 # var spanId = document.getElementById("message_container");
109 # spanId.textContent = 'Redirecting...';</script>
110 # <form method="GET" id="redirect_form" name="redirect_form"
111 # action="http://localhost:81/projects/Webconnect/response.php" target="_top">
112 # <input type="hidden" name = "LOGO_URL"
113 # value="http://localhost:81/projects/Webconnect/images/elogo.fw.png">
114 # <input type="hidden" name = "RESPONSE_URL"
115 # value="http://localhost:81/projects/Webconnect/response.php">
116 # <input type="hidden" name = "CURRENCY_CODE" value="NGN">
117 # <input type="hidden" name = "TERMINAL_ID" value="0000000001">
118 # <input type="hidden" name = "TRANSACTION_ID" value="etz1568638104web">
119 # <input type="hidden" name = "AMOUNT" value="100">
120 # <input type="hidden" name = "DESCRIPTION" value="Payment Description">
121 # <input type="hidden" name = "CHECKSUM"
122 # value="5be04064f4bb250f73650059a8e921cc">
123 # <input type="hidden" name = "MERCHANT_CODE" value="0339990001">
124 # <input type="hidden" name = "EMAIL" value="xyz@yahoo.com">
125 # <input type="hidden" name = "SUCCESS" value="0">
126 # <input type="hidden" name = "FINAL_CHECKSUM"
127 # value="FD67A4CCC39E2C8DEEEC78D6C64A61FE">
128 # <input type="hidden" name = "STATUS_REASON" value="Approved">
129 # <input type="hidden" name = "TRANS_NUM" value="01ESA20190916134824YA3YJ8">
130 # <input type="hidden" name = "CARD_NO" value="506066XXXXXXXXX6666">
131 # <input type="hidden" name = "CARD_TYPE" value="Verve">
132 # </form>
133 # <script language='javascript'>
134 # var fom = document.forms["redirect_form"];
135 # fom.submit();</script>
136
137 # A sample query response sent to the RESPONSE_URL after the browser has
138 # automatically executed the Javascript above (we don't use this response)
139
140 # http://salsa:8080/app/applicants/cbt2015/449072/p5686487280654/receive_etranzact?
141 # LOGO_URL=https%3A%2F%2Fiuokada.waeup.org%2Fstatic_custom%2Fiou_logo.png&
142 # RESPONSE_URL=http%3A%2F%2Fsalsa%3A8080%2Fapp%2Fapplicants%2Fcbt2015%2F449072%2Fp5686487280654%2Freceive_etranzact&
143 # CURRENCY_CODE=NGN&
144 # TERMINAL_ID=0000000001&
145 # TRANSACTION_ID=p5686487280654&
146 # AMOUNT=3333.0&
147 # DESCRIPTION=&
148 # CHECKSUM=3886118fcd91a376cc95c48c94dc499a&
149 # MERCHANT_CODE=0339990001&
150 # EMAIL=aa%40aa.de&
151 # SUCCESS=0&
152 # FINAL_CHECKSUM=EE105B703F84B1D67D0A4234622C03E8&
153 # STATUS_REASON=Approved&
154 # TRANS_NUM=01ESA20190916164636L2UTU7&
155 # CARD_NO=506066XXXXXXXXX6666&
156 # CARD_TYPE=Verve
157
158def process_response(payment, form, view, verify):
159    if not form or not form.get('SUCCESS', None):
160        msg = _('No (valid) response from Etranzact.')
161        log = 'invalid response for payment %s' % payment.p_id
162        payment.p_state = 'failed'
163        notify(grok.ObjectModifiedEvent(payment))
164        return False, msg, log
165    success = form.get('SUCCESS', None)
166    # Compute final checksum
167    transaction_id = payment.p_id
168    amount = "%.1f" % payment.amount_auth
169    responseurl = view.url(payment, 'receive_etranzact')
170    hashargs =  success + amount + view.terminal_id + transaction_id \
171        + responseurl + view.secret_key
172    final_checksum = hashlib.md5(hashargs).hexdigest().upper()
173    if form.get('FINAL_CHECKSUM', None) != final_checksum:
174        msg = _('Wrong checksum.')
175        log = 'wrong checksum for %s payment %s: %s' % (
176            payment.p_category, payment.p_id, str(form))
177        payment.p_state = 'failed'
178        notify(grok.ObjectModifiedEvent(payment))
179        return False, msg, log
180    payment.r_code = form.get('SUCCESS', None)
181    payment.r_desc = form.get('STATUS_REASON', None) # MESSAGE also available
182    payment.r_amount_approved = float(form.get('AMOUNT', None))
183    payment.r_pay_reference = form.get('TRANS_NUM', None)
184    payment.r_card_num = "%s %s" % (form.get('CARD_TYPE', None),
185                                    form.get('CARD_NO', None))
186    if payment.r_code != '0':
187        msg = _('Unsuccessful response: ${a}', mapping = {'a': payment.r_desc})
188        log = 'unsuccessful response for %s payment %s: %s' % (
189            payment.p_category, payment.p_id, payment.r_desc)
190        payment.p_state = 'failed'
191        notify(grok.ObjectModifiedEvent(payment))
192        return False, msg, log
193    if round(payment.r_amount_approved/10.0, 0) != round(
194        payment.amount_auth/10.0, 0):
195        msg = _('Response amount does not match.')
196        log = 'wrong response for %s payment %s: %s' % (
197            payment.p_category, payment.p_id, str(form))
198        payment.p_state = 'failed'
199        notify(grok.ObjectModifiedEvent(payment))
200        return False, msg, log
201    transaction_id = form.get('TRANSACTION_ID', None)
202    if transaction_id != payment.p_id:
203        msg = _('Response transaction id does not match.')
204        log = 'wrong response for %s payment %s: %s' % (
205            payment.p_category, payment.p_id, str(form))
206        payment.p_state = 'failed'
207        notify(grok.ObjectModifiedEvent(payment))
208        return False, msg, log
209    payment.p_state = 'paid'
210    if not verify:
211        payment.payment_date = datetime.utcnow()
212    msg = _('Successful response received')
213    log = 'valid response for %s payment %s: %s' % (
214        payment.p_category, payment.p_id, str(form))
215    notify(grok.ObjectModifiedEvent(payment))
216    return True, msg, log
217
218# Requerying Etranzact Payoutlet payments
219
220# Expected response:
221# RECEIPT_NO=500191030486&PAYMENT_CODE=500854291572447457669&MERCHANT_CODE=700602WDUB
222# &TRANS_AMOUNT=200000.0&TRANS_DATE=2019/10/30
223# 15:13:47&TRANS_DESCR=Test%20Test%20Test-CLEARANCE%20-001-p5723474039401
224# &CUSTOMER_ID=p5723474039401&BANK_CODE=500&BRANCH_CODE=001
225# &SERVICE_ID=p5723474039401&CUSTOMER_NAME=Test%20Test%20Test
226# &CUSTOMER_ADDRESS=ASS-ENG&TELLER_ID=etzbankteller&USERNAME=%20
227# &PASSWORD=%20&BANK_NAME=eTranzact%20Intl%20Plc
228# &BRANCH_NAME=ETRANZACT&CHANNEL_NAME=Bank&PAYMENT_METHOD_NAME=Cash
229# &PAYMENT_CURRENCY=566&TRANS_TYPE=101&TRANS_FEE=0.0
230# &TYPE_NAME=CLEARANCE&LEAD_BANK_CODE=700&LEAD_BANK_NAME=eTranzact%20Intl%20Plc
231# COL1=2018/2019&COL2=Acceptance
232# Fee&COL3=ASS&COL4=ENG&COL5=BARTENL&COL6=400&COL7=ug_ft&COL8=N/A&COL9=11/12345
233# &COL10=damms005@gmail.com&COL11=None&COL12=&COL13=
234
235def query_payoutlet(host, terminal_id, confirmation_number, payment, https):
236    headers={"Content-type": "application/x-www-form-urlencoded",
237             "Accept": "text/plain"}
238    url = "/WebConnectPlus/query.jsp"
239    if https:
240        h = httplib.HTTPSConnection(host)
241    else:
242        h = httplib.HTTPConnection(host)
243    args = {'TERMINAL_ID': terminal_id,
244            'CONFIRMATION_NO': confirmation_number,
245            }
246    h.request('POST', url, urlencode(args), headers)
247    response = h.getresponse()
248    if response.status!=200:
249        return False, 'Connection error (%s, %s)' % (response.status, response.reason), None
250    raw = response.read()
251    # Remove empty lines
252    raw = raw.replace('\r\n','')
253    success = parse_qs(raw)
254    if not success.get('CUSTOMER_ID'):
255        msg = _('Invalid or unsuccessful callback: ${a}',
256            mapping = {'a': raw})
257        log = 'invalid callback for payment %s: %s' % (payment.p_id, raw)
258        payment.p_state = 'failed'
259        return False, msg, log
260    # We expect at least two parameters
261    if len(success) < 2:
262        msg = _('Invalid callback: ${a}', mapping = {'a': raw})
263        log = 'invalid callback for payment %s: %s' % (payment.p_id, raw)
264        payment.p_state = 'failed'
265        return False, msg, log
266    customer_id = success.get('CUSTOMER_ID')[0]
267    if payment.p_id != customer_id:
268        msg = _('Wrong payment id')
269        log = 'wrong callback for payment %s: %s' % (payment.p_id, raw)
270        payment.p_state = 'failed'
271        return False, msg, log
272    payment.r_code = u'ET'
273    payment.r_desc = u'%s' % success.get('TRANS_DESCR')[0]
274    payment.r_amount_approved = float(success.get('TRANS_AMOUNT')[0])
275    payment.r_card_num = None
276    payment.r_pay_reference = u'%s' % success.get('RECEIPT_NO')[0]
277    if payment.r_amount_approved != payment.amount_auth:
278        msg = _('Wrong amount')
279        log = 'wrong callback for payment %s: %s' % (payment.p_id, raw)
280        payment.p_state = 'failed'
281        return False, msg, log
282    log = 'valid callback for payment %s: %s' % (payment.p_id, raw)
283    msg = _('Successful callback received')
284    payment.p_state = 'paid'
285    payment.payment_date = datetime.utcnow()
286    return True, msg, log
287
288
289# Etranzact Credo payments helper functions
290
291def get_JSON_response_initialize(payment, host, callbackUrl, public_api_key):
292    headers={
293        'Content-Type':'application/JSON',
294        'Authorization':public_api_key,
295    }
296    h = httplib.HTTPSConnection(host)
297
298    firstname = IPayer(payment).display_fullname.split()[0]
299    lastname = IPayer(payment).display_fullname.split()[-1]
300    email = IPayer(payment).email
301    phone = IPayer(payment).phone
302
303    args = {
304        'email': email,
305        'amount': 100 * payment.amount_auth,
306        'reference': payment.p_id,
307        'currency': 'NGN',
308        'callbackUrl': callbackUrl,
309        'customerFirstName': firstname,
310        'customerLastName': lastname,
311        'customerPhoneNumber': phone,
312        'bearer': '0',
313        }
314    try:
315        h.request('POST', '/transaction/initialize',
316                  body=json.dumps(args), headers=headers)
317    except SSLError:
318        return {'error': 'SSL handshake error'}
319    response = h.getresponse()
320    if response.status==400:
321        jsonout = response.read()
322        parsed_json = json.loads(jsonout)
323        return {'error': parsed_json['error']}
324    if response.status!=200:
325        return {'error': 'Connection error (%s, %s)' % (response.status, response.reason)}
326    jsonout = response.read()
327    parsed_json = json.loads(jsonout)
328
329    # A typical JSON result
330   
331    # {u'status': 200,
332    #  u'execTime': 5.109764,
333    #  u'message': u'Successfully processed',
334    #  u'data':
335    #     {u'credoReference': u'vsb200B5oM0521Mb00og',
336    #      u'reference': u'xyz',
337    #      u'authorizationUrl': u'https://pay.credodemo.com/vsb200B5oM0521Mb00og'
338    #      },
339    #  u'error': []
340    # }
341
342    return parsed_json
343
344
345def initiate_payment(payment, host, callbackUrl, public_api_key):
346    response = get_JSON_response_initialize(payment, host, callbackUrl, public_api_key)
347    if response['status'] == 200:
348        return True, response['data']['authorizationUrl']
349    return False, response['message']
350
351def get_JSON_response_verify(transref, host, secret_api_key):
352    headers={
353        'Content-Type':'text/xml; charset=utf-8',
354        'Authorization':secret_api_key,
355    }
356    h = httplib.HTTPSConnection(host)
357    url = '/transaction/%s/verify' % transref
358    try:
359        h.request("GET", url, headers=headers)
360    except SSLError:
361        return {'error': 'SSL handshake error'}
362    response = h.getresponse()
363    if response.status==404:
364        jsonout = response.read()
365        parsed_json = json.loads(jsonout)
366        return {'error': parsed_json['error']}
367    if response.status!=200:
368        return {'error': 'Connection error (%s, %s)' % (response.status, response.reason)}
369    jsonout = response.read()
370    parsed_json = json.loads(jsonout)
371    return parsed_json
372
373
374def query_credo_payment(payment, host, secret_api_key):
375
376    jr = get_JSON_response_verify(payment.p_id, host, secret_api_key)
377    error = jr.get('error')
378    if error:
379        msg = log = error
380        return False, msg, log
381
382    # A typical JSON result
383
384    #{u'status': 200,
385    # u'execTime': 35.20671,
386    # u'message': u'Pending bank credit notification',
387    # u'data': {
388    #   u'status': 12,
389    #   u'businessCode': u'700607003930001',
390    #   u'channelId': 0,
391    #   u'businessRef': u'xyz',
392    #   u'businessName': u'Igbinedion University Okada',
393    #   u'settlementAmount': 100000.0,
394    #   u'currencyCode': u'NGN',
395    #   u'transFeeAmount': 1600.0,
396    #   u'transRef': u'vsb200B5oM0521Mb00og',
397    #   u'transAmount': 100000.0,
398    #   u'debitedAmount': 101600.0,
399    #   u'customerId': u'aa@aa.ng',
400    #   u'statusMessage': u'Pending bank credit notification',
401    #   u'metadata': []
402    #  },
403    # u'error': []
404    #}
405
406    payment.r_code = unicode(jr['data']['status'])
407    payment.r_desc = jr['data']['statusMessage']
408    payment.r_amount_approved = jr['data']['debitedAmount']
409    payment.r_pay_reference = jr['data'].get('transRef', u'')
410    payment.r_payment_link = u'n/a'
411    payment.r_card_num = u'n/a'
412    if payment.r_code != '0':
413        msg = _('Unsuccessful callback: ${a}', mapping = {'a': payment.r_desc})
414        log = 'unsuccessful callback for %s payment %s: %s' % (
415            payment.p_category, payment.p_id, payment.r_desc)
416        payment.p_state = 'failed'
417        notify(grok.ObjectModifiedEvent(payment))
418        return False, msg, log
419    payment.p_state = 'paid'
420    payment.payment_date = datetime.utcnow()
421    msg = _('Successful callback received')
422    log = 'valid callback for %s payment %s: %s' % (
423        payment.p_category, payment.p_id, str(jr))
424    notify(grok.ObjectModifiedEvent(payment))
425    return True, msg, log
426
Note: See TracBrowser for help on using the repository browser.