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

Last change on this file since 17778 was 17754, checked in by Henrik Bettermann, 6 months ago

Show correct error message.

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