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

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

Use serviceCode.

  • Property svn:keywords set to Id
File size: 16.5 KB
RevLine 
[15586]1## $Id: helpers.py 17783 2024-05-14 08:46:27Z henrik $
[15585]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
[17730]23from ssl import SSLError
24from datetime import datetime
[15585]25from urllib import urlencode
[15730]26from urllib2 import urlopen
27from urlparse import parse_qs
[15585]28import httplib
29import hashlib
30import json
31from zope.event import notify
[17730]32from waeup.kofa.payments.interfaces import IPayer
[15596]33from waeup.kofa.utils.helpers import extract_formvars
[15585]34from kofacustom.nigeria.interfaces import MessageFactory as _
35
[15734]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
[15585]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
[15596]62def query_history(host, terminal_id, transaction_id, https):
[15585]63    headers={"Content-type": "application/x-www-form-urlencoded",
64             "Accept": "text/plain"}
[15589]65    url = "/webconnect/v3/query.jsp"
[15585]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            }
[15589]73    #args['RESPONSE_URL'] = responseurl
[15585]74    h.request('POST', url, urlencode(args), headers)
75    response = h.getresponse()
76    if response.status!=200:
[15596]77        return 'Connection error (%s, %s)' % (response.status, response.reason), None
78    raw = response.read()
79    return raw, extract_formvars(raw)
[15589]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
[15596]103 # A sample query response
[15589]104
[15596]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
[15589]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):
[15598]159    if not form or not form.get('SUCCESS', None):
[16083]160        msg = _('No (valid) response from Etranzact.')
[16167]161        log = 'invalid response for payment %s' % payment.p_id
[15589]162        payment.p_state = 'failed'
163        notify(grok.ObjectModifiedEvent(payment))
164        return False, msg, log
[15598]165    success = form.get('SUCCESS', None)
[15589]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))
[15596]177        payment.p_state = 'failed'
178        notify(grok.ObjectModifiedEvent(payment))
[15589]179        return False, msg, log
[15598]180    payment.r_code = form.get('SUCCESS', None)
[15589]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))
[15730]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
[15788]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
[15730]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
[17730]287
288
289# Etranzact Credo payments helper functions
290
[17783]291def get_JSON_response_initialize(payment, host, callbackUrl,
292                                 public_api_key, serviceCode):
[17730]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',
[17783]314        'serviceCode': serviceCode,
[17730]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)
[17754]325        errormsg = ''
326        for value in parsed_json['error'].values():
327            errormsg += "%s. " %(value)
328        return {'error': errormsg}
[17730]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
[17783]350def initiate_payment(payment, host, callbackUrl, public_api_key, serviceCode):
351    response = get_JSON_response_initialize(
352        payment, host, callbackUrl, public_api_key, serviceCode)
[17754]353    if response.get('error', None):
354        return False, response['error']
[17730]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.