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

Last change on this file was 17783, checked in by Henrik Bettermann, 6 months ago

Use serviceCode.

  • Property svn:keywords set to Id
File size: 16.5 KB
Line 
1## $Id: helpers.py 17783 2024-05-14 08:46:27Z 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,
292                                 public_api_key, serviceCode):
293    headers={
294        'Content-Type':'application/JSON',
295        'Authorization':public_api_key,
296    }
297    h = httplib.HTTPSConnection(host)
298
299    firstname = IPayer(payment).display_fullname.split()[0]
300    lastname = IPayer(payment).display_fullname.split()[-1]
301    email = IPayer(payment).email
302    phone = IPayer(payment).phone
303
304    args = {
305        'email': email,
306        'amount': 100 * payment.amount_auth,
307        'reference': payment.p_id,
308        'currency': 'NGN',
309        'callbackUrl': callbackUrl,
310        'customerFirstName': firstname,
311        'customerLastName': lastname,
312        'customerPhoneNumber': phone,
313        'bearer': '0',
314        'serviceCode': serviceCode,
315        }
316    try:
317        h.request('POST', '/transaction/initialize',
318                  body=json.dumps(args), headers=headers)
319    except SSLError:
320        return {'error': 'SSL handshake error'}
321    response = h.getresponse()
322    if response.status==400:
323        jsonout = response.read()
324        parsed_json = json.loads(jsonout)
325        errormsg = ''
326        for value in parsed_json['error'].values():
327            errormsg += "%s. " %(value)
328        return {'error': errormsg}
329    if response.status!=200:
330        return {'error': 'Connection error (%s, %s)' % (response.status, response.reason)}
331    jsonout = response.read()
332    parsed_json = json.loads(jsonout)
333
334    # A typical JSON result
335   
336    # {u'status': 200,
337    #  u'execTime': 5.109764,
338    #  u'message': u'Successfully processed',
339    #  u'data':
340    #     {u'credoReference': u'vsb200B5oM0521Mb00og',
341    #      u'reference': u'xyz',
342    #      u'authorizationUrl': u'https://pay.credodemo.com/vsb200B5oM0521Mb00og'
343    #      },
344    #  u'error': []
345    # }
346
347    return parsed_json
348
349
350def initiate_payment(payment, host, callbackUrl, public_api_key, serviceCode):
351    response = get_JSON_response_initialize(
352        payment, host, callbackUrl, public_api_key, serviceCode)
353    if response.get('error', None):
354        return False, response['error']
355    if response['status'] == 200:
356        return True, response['data']['authorizationUrl']
357    return False, response['message']
358
359def get_JSON_response_verify(transref, host, secret_api_key):
360    headers={
361        'Content-Type':'text/xml; charset=utf-8',
362        'Authorization':secret_api_key,
363    }
364    h = httplib.HTTPSConnection(host)
365    url = '/transaction/%s/verify' % transref
366    try:
367        h.request("GET", url, headers=headers)
368    except SSLError:
369        return {'error': 'SSL handshake error'}
370    response = h.getresponse()
371    if response.status==404:
372        jsonout = response.read()
373        parsed_json = json.loads(jsonout)
374        return {'error': parsed_json['error']}
375    if response.status!=200:
376        return {'error': 'Connection error (%s, %s)' % (response.status, response.reason)}
377    jsonout = response.read()
378    parsed_json = json.loads(jsonout)
379    return parsed_json
380
381
382def query_credo_payment(payment, host, secret_api_key):
383
384    jr = get_JSON_response_verify(payment.p_id, host, secret_api_key)
385    error = jr.get('error')
386    if error:
387        msg = log = error
388        return False, msg, log
389
390    # A typical JSON result
391
392    #{u'status': 200,
393    # u'execTime': 35.20671,
394    # u'message': u'Pending bank credit notification',
395    # u'data': {
396    #   u'status': 12,
397    #   u'businessCode': u'700607003930001',
398    #   u'channelId': 0,
399    #   u'businessRef': u'xyz',
400    #   u'businessName': u'Igbinedion University Okada',
401    #   u'settlementAmount': 100000.0,
402    #   u'currencyCode': u'NGN',
403    #   u'transFeeAmount': 1600.0,
404    #   u'transRef': u'vsb200B5oM0521Mb00og',
405    #   u'transAmount': 100000.0,
406    #   u'debitedAmount': 101600.0,
407    #   u'customerId': u'aa@aa.ng',
408    #   u'statusMessage': u'Pending bank credit notification',
409    #   u'metadata': []
410    #  },
411    # u'error': []
412    #}
413
414    payment.r_code = unicode(jr['data']['status'])
415    payment.r_desc = jr['data']['statusMessage']
416    payment.r_amount_approved = jr['data']['debitedAmount']
417    payment.r_pay_reference = jr['data'].get('transRef', u'')
418    payment.r_payment_link = u'n/a'
419    payment.r_card_num = u'n/a'
420    if payment.r_code != '0':
421        msg = _('Unsuccessful callback: ${a}', mapping = {'a': payment.r_desc})
422        log = 'unsuccessful callback for %s payment %s: %s' % (
423            payment.p_category, payment.p_id, payment.r_desc)
424        payment.p_state = 'failed'
425        notify(grok.ObjectModifiedEvent(payment))
426        return False, msg, log
427    payment.p_state = 'paid'
428    payment.payment_date = datetime.utcnow()
429    msg = _('Successful callback received')
430    log = 'valid callback for %s payment %s: %s' % (
431        payment.p_category, payment.p_id, str(jr))
432    notify(grok.ObjectModifiedEvent(payment))
433    return True, msg, log
434
Note: See TracBrowser for help on using the repository browser.