source: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/helpers.py @ 17259

Last change on this file since 17259 was 17233, checked in by Henrik Bettermann, 2 years ago

Catch TypeError?.

  • Property svn:keywords set to Id
File size: 18.4 KB
Line 
1## $Id: helpers.py 17233 2022-12-21 10:21:27Z henrik $
2##
3## Copyright (C) 2012 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 interswitch module in custom packages.
19"""
20from datetime import datetime
21from ssl import SSLError
22import httplib
23import hashlib
24import json
25from urllib import urlencode
26import grok
27from xml.dom.minidom import parseString
28from zope.event import notify
29from waeup.kofa.payments.interfaces import IPayer
30from kofacustom.nigeria.interfaces import MessageFactory as _
31
32def SOAP_post(soap_action, xml, host, url, https):
33    """Handles making the SOAP request.
34    """
35    if https:
36        h = httplib.HTTPSConnection(host)
37    else:
38        h = httplib.HTTPConnection(host)
39    headers={
40        'Host':host,
41        'Content-Type':'text/xml; charset=utf-8',
42        'Content-Length':len(xml),
43        'SOAPAction':'"%s"' % soap_action,
44    }
45    h.request('POST', url, body=xml,headers=headers)
46    response = h.getresponse()
47    return response
48
49def write_payments_log(id, payment):
50    payment.logger.info(
51        '%s,%s,%s,%s,%s,%s,%s,%s,,,' % (
52        id, payment.p_id, payment.p_category,
53        payment.amount_auth, payment.r_code,
54        payment.provider_amt, payment.gateway_amt,
55        payment.thirdparty_amt))
56
57# CollegePAY helper functions
58
59def get_SOAP_response(product_id, transref, host, url, https):
60    xml="""\
61<?xml version="1.0" encoding="utf-8"?>
62<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
63  <soap:Body>
64    <getTransactionData xmlns="http://tempuri.org/">
65      <product_id>%s</product_id>
66      <trans_ref>%s</trans_ref>
67    </getTransactionData>
68  </soap:Body>
69</soap:Envelope>""" % (product_id, transref)
70    response=SOAP_post("http://tempuri.org/getTransactionData",xml, host, url, https)
71    if response.status!=200:
72        return 'Connection error (%s, %s)' % (response.status, response.reason)
73    result_xml = response.read()
74    doc=parseString(result_xml)
75    response=doc.getElementsByTagName('getTransactionDataResult')[0].firstChild.data
76    return response
77
78
79def get_JSON_response(product_id, transref, host, url, https, mac, amount):
80    hashargs = product_id + transref + mac
81    hashvalue = hashlib.sha512(hashargs).hexdigest()
82    headers={
83        'Content-Type':'text/xml; charset=utf-8',
84        'Hash':hashvalue,
85    }
86    if https:
87        h = httplib.HTTPSConnection(host)
88    else:
89        h = httplib.HTTPConnection(host)
90    amount = int(100 * amount)
91    args = {'productid': product_id,
92            'transactionreference': transref,
93            'amount': amount}
94    url = '%s?' % url + urlencode(args)
95    try:
96        h.request("GET", url, headers=headers)
97    except SSLError:
98        return {'error': 'SSL handshake error'}
99    response = h.getresponse()
100    if response.status!=200:
101        return {'error': 'Connection error (%s, %s)' % (response.status, response.reason)}
102    jsonout = response.read()
103    parsed_json = json.loads(jsonout)
104    return parsed_json
105
106def query_interswitch_SOAP(payment, product_id, host, url, https, verify):
107    sr = get_SOAP_response(product_id, payment.p_id, host, url, https)
108    if sr.startswith('Connection error'):
109        msg = _('Connection error')
110        log = sr
111        return False, msg, log
112    wlist = sr.split(':')
113    if len(wlist) < 7:
114        msg = _('Invalid callback: ${a}', mapping = {'a': sr})
115        log = 'invalid callback for payment %s: %s' % (payment.p_id, sr)
116        return False, msg, log
117    payment.r_code = wlist[0]
118    payment.r_desc = wlist[1]
119    payment.r_amount_approved = float(wlist[2]) / 100
120    payment.r_card_num = wlist[3]
121    payment.r_pay_reference = wlist[5]
122    payment.r_company = u'interswitch'
123    if payment.r_code != '00':
124        msg = _('Unsuccessful callback: ${a}', mapping = {'a': sr})
125        log = 'unsuccessful callback for %s payment %s: %s' % (
126            payment.p_category, payment.p_id, sr)
127        payment.p_state = 'failed'
128        notify(grok.ObjectModifiedEvent(payment))
129        return False, msg, log
130    if round(payment.r_amount_approved, 0) != round(payment.amount_auth, 0):
131        msg = _('Callback amount does not match.')
132        log = 'wrong callback for %s payment %s: %s' % (
133            payment.p_category, payment.p_id, sr)
134        payment.p_state = 'failed'
135        notify(grok.ObjectModifiedEvent(payment))
136        return False, msg, log
137    if wlist[4] != payment.p_id:
138        msg = _('Callback transaction id does not match.')
139        log = 'wrong callback for %s payment %s: %s' % (
140            payment.p_category, payment.p_id, sr)
141        payment.p_state = 'failed'
142        notify(grok.ObjectModifiedEvent(payment))
143        return False, msg, log
144    payment.p_state = 'paid'
145    if not verify:
146        payment.payment_date = datetime.utcnow()
147    msg = _('Successful callback received.')
148    log = 'valid callback for %s payment %s: %s' % (
149        payment.p_category, payment.p_id, sr)
150    notify(grok.ObjectModifiedEvent(payment))
151    return True, msg, log
152
153def query_interswitch(payment, product_id, host, url, https, mac, verify):
154    # If no mac mac key is given, fall back to deprecated SOAP method
155    # (Uniben, AAUA, FCEOkene).
156    if mac == None:
157        return query_interswitch_SOAP(
158            payment, product_id, host, url, https, verify)
159    jr = get_JSON_response(product_id, payment.p_id, host, url,
160                           https, mac, payment.amount_auth)
161    error = jr.get('error')
162    if error:
163        msg = log = error
164        return False, msg, log
165
166    # A typical JSON response
167
168    # old:
169
170    #  {u'SplitAccounts': [],
171    #   u'MerchantReference':u'p4210665523377',
172    #   u'PaymentReference':u'GTB|WEB|KPOLY|12-01-2015|013138',
173    #   u'TransactionDate':u'2015-01-12T13:43:39.27',
174    #   u'RetrievalReferenceNumber':u'000170548791',
175    #   u'ResponseDescription': u'Approved Successful',
176    #   u'Amount': 2940000,
177    #   u'CardNumber': u'2507',
178    #   u'ResponseCode': u'00',
179    #   u'LeadBankCbnCode': None,
180    #   u'LeadBankName': None}
181
182    # new:
183
184    # 'PaymentReference' is maybe missing
185
186    #  {u'SplitAccounts': [],
187    #  u'MerchantReference':u'p5918633006916',
188    #  u'TransactionDate':u'2020-06-11T09:17:37',
189    #  u'ResponseDescription':u'Customer Cancellation',
190    #  u'Amount': 89525000,
191    #  u'CardNumber': u'',
192    #  u'ResponseCode': u'Z6',
193    #  u'BankCode': u''}
194
195    if not 'ResponseCode' in jr.keys() \
196        or not 'ResponseDescription' in jr.keys() \
197        or not 'Amount' in jr.keys():
198        msg = _('Invalid callback: ${a}', mapping = {'a': str(jr)})
199        log = 'invalid callback for payment %s: %s' % (payment.p_id, str(jr))
200        return False, msg, log
201    if verify and jr['ResponseCode'] == '20050':
202        msg = _('Integration method has changed.')
203        log = 'invalid callback for payment %s: %s' % (payment.p_id, str(jr))
204        return False, msg, log
205    payment.r_code = jr['ResponseCode']
206    payment.r_desc = jr['ResponseDescription']
207    payment.r_amount_approved = jr['Amount'] / 100.0
208    payment.r_card_num = jr.get('CardNumber', u'')
209    payment.r_pay_reference = jr.get('PaymentReference', u'')
210    #payment.r_company = u'interswitch'
211    if payment.r_code != '00':
212        msg = _('Unsuccessful callback: ${a}', mapping = {'a': payment.r_desc})
213        log = 'unsuccessful callback for %s payment %s: %s' % (
214            payment.p_category, payment.p_id, payment.r_desc)
215        payment.p_state = 'failed'
216        notify(grok.ObjectModifiedEvent(payment))
217        return False, msg, log
218    if round(payment.r_amount_approved, 0) != round(payment.amount_auth, 0):
219        msg = _('Callback amount does not match.')
220        log = 'wrong callback for %s payment %s: %s' % (
221            payment.p_category, payment.p_id, str(jr))
222        payment.p_state = 'failed'
223        notify(grok.ObjectModifiedEvent(payment))
224        return False, msg, log
225    if jr['MerchantReference'] != payment.p_id:
226        msg = _('Callback transaction id does not match.')
227        log = 'wrong callback for %s payment %s: %s' % (
228            payment.p_category, payment.p_id, str(jr))
229        payment.p_state = 'failed'
230        notify(grok.ObjectModifiedEvent(payment))
231        return False, msg, log
232    payment.p_state = 'paid'
233    if not verify:
234        payment.payment_date = datetime.utcnow()
235    msg = _('Successful callback received')
236    log = 'valid callback for %s payment %s: %s' % (
237        payment.p_category, payment.p_id, str(jr))
238    notify(grok.ObjectModifiedEvent(payment))
239    return True, msg, log
240
241# PAYDirect helper functions
242
243def create_paydirect_booking(merchant_id, payment, item_code, host, url, https):
244    p_id = payment.p_id
245    description = payment.p_category
246    amount = int(100*payment.amount_auth)  # Amount in Kobo
247    date_booked = payment.creation_date.strftime("%Y-%m-%d")
248    date_expired = "2099-12-31"
249    firstname = IPayer(payment).display_fullname.split()[0]
250    lastname = IPayer(payment).display_fullname.split()[-1]
251    id = IPayer(payment).id
252    email = IPayer(payment).email
253
254    xml="""\
255<?xml version="1.0" encoding="utf-8"?>
256<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
257  <soap:Body>
258    <CreateBooking xmlns="http://interswitchng.com/bookonhold">
259      <ReservationRequest>
260        <MerchantId>%s</MerchantId>
261        <Bookings>
262          <Booking>
263            <ReferenceNumber>%s%s</ReferenceNumber>
264            <Description>%s</Description>
265            <Amount>%s</Amount>
266            <DateBooked>%s</DateBooked>
267            <DateExpired>%s</DateExpired>
268            <FirstName>%s</FirstName>
269            <LastName>%s</LastName>
270            <Email>%s</Email>
271            <ItemCode>%s</ItemCode>
272          </Booking>
273        </Bookings>
274      </ReservationRequest>
275    </CreateBooking>
276  </soap:Body>
277</soap:Envelope>""" % (
278    merchant_id, merchant_id, p_id[1:],
279    description, amount,
280    date_booked, date_expired,
281    firstname, lastname,
282    email, item_code)
283    response=SOAP_post(
284        "http://interswitchng.com/bookonhold/CreateBooking",
285        xml, host, url, https)
286    if response.status!=200:
287        error = 'Connection error (%s, %s)' % (response.status, response.reason)
288        return error
289    result_xml = response.read()
290    return result_xml
291
292def get_SOAP_response_paydirect(merchant_id, p_id, host, url, https):
293    xml="""\
294<?xml version="1.0" encoding="utf-8"?>
295<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:book="http://interswitchng.com/bookonhold">
296   <soap:Header/>
297   <soap:Body>
298      <book:FetchBookingDetails>
299         <book:ReservationDetailsRequest>
300            <book:MerchantId>%s</book:MerchantId>
301            <book:Bookings>
302               <book:Booking>
303                  <book:ReferenceNumber>%s%s</book:ReferenceNumber>
304               </book:Booking>
305            </book:Bookings>
306         </book:ReservationDetailsRequest>
307      </book:FetchBookingDetails>
308   </soap:Body>
309</soap:Envelope>""" % (merchant_id, merchant_id, p_id[1:])
310
311    response=SOAP_post(
312        "http://interswitchng.com/bookonhold/FetchBookingDetails",
313        xml, host, url, https)
314    if response.status!=200:
315        error = 'Connection error (%s, %s)' % (response.status, response.reason)
316        return error
317    result_xml = response.read()
318    return result_xml
319
320def fetch_booking_details(payment, merchant_id, host, url, https):
321    result_xml = get_SOAP_response_paydirect(
322        merchant_id, payment.p_id, host, url, https)
323    if result_xml.startswith('Connection error'):
324        return False, result_xml, result_xml
325    doc=parseString(result_xml)
326    if not doc.getElementsByTagName('PaymentStatus'):
327        msg = _('Your payment %s was not found.' % payment.p_id)
328        log = 'payment %s cannot be found' % payment.p_id
329        return False, msg, log
330    p_status = doc.getElementsByTagName('PaymentStatus')[0].firstChild.data
331    payment.r_code = p_status
332    try:
333        payment.r_desc = "Channel Name: %s - Terminal Id: %s - Location: %s" % (
334            doc.getElementsByTagName('ChannelName')[0].firstChild.data,
335            doc.getElementsByTagName('TerminalId')[0].firstChild.data,
336            doc.getElementsByTagName('Location')[0].firstChild.data)
337    except AttributeError:
338        pass
339    try:
340        amount = doc.getElementsByTagName('Amount')[0].firstChild.data
341        payment.r_amount_approved = float(amount) / 100
342    except AttributeError:
343        pass
344    try:
345        payment.r_pay_reference = doc.getElementsByTagName(
346            'ReferenceNumber')[0].firstChild.data
347    except AttributeError:
348        pass
349    if p_status not in ('Pending', 'Completed'):
350        msg = _('Unknown status: %s' % p_status)
351        log = 'invalid callback for payment %s: %s' % (payment.p_id, p_status)
352        payment.p_state = 'failed'
353        notify(grok.ObjectModifiedEvent(payment))
354        return False, msg, log
355    if p_status == 'Completed' and not payment.r_amount_approved:
356        msg = _('Amount unconfirmed')
357        log = 'unsuccessful callback for payment %s: amount unconfirmed' % payment.p_id
358        payment.p_state = 'failed'
359        notify(grok.ObjectModifiedEvent(payment))
360        return False, msg, log
361    if p_status == 'Pending':
362        msg = _('Payment pending')
363        log = 'unsuccessful callback for payment %s: pending' % payment.p_id
364        payment.p_state = 'failed'
365        notify(grok.ObjectModifiedEvent(payment))
366        return False, msg, log
367    if payment.r_amount_approved != payment.amount_auth:
368        msg = _('Callback amount does not match net amount.')
369        log = 'unsuccessful callback for %s payment %s: callback amount %s does not match' % (
370            payment.p_category, payment.p_id, amount)
371        payment.p_state = 'failed'
372        notify(grok.ObjectModifiedEvent(payment))
373        return False, msg, log
374    payment.p_state = 'paid'
375    payment.payment_date = datetime.utcnow()
376    msg = _('Successful callback received')
377    log = 'valid callback for %s payment %s: %s' % (
378        payment.p_category, payment.p_id, p_status)
379    notify(grok.ObjectModifiedEvent(payment))
380    return True, msg, log
381
382# Web checkout helper functions
383
384def get_JSON_webcheckout_response(merchant_code, transref, host, url,
385                                  https, amount, mac=''):
386    amount = int(100 * amount)
387    hashargs = transref + merchant_code + str(amount) + mac
388    hashvalue = hashlib.sha512(hashargs).hexdigest()
389    headers={
390        'Content-Type':'text/xml; charset=utf-8',
391        'Hash':hashvalue,
392    }
393    if https:
394        h = httplib.HTTPSConnection(host)
395    else:
396        h = httplib.HTTPConnection(host)
397    args = {'merchantcode': merchant_code,
398            'transactionreference': transref,
399            'amount': amount}
400    url = '%s?' % url + urlencode(args)
401    try:
402        h.request("GET", url, headers=headers)
403    except SSLError:
404        return {'error': 'SSL handshake error'}
405    response = h.getresponse()
406    if response.status!=200:
407        return {'error': 'Connection error (%s, %s)' % (
408            response.status, response.reason)}
409    jsonout = response.read()
410    parsed_json = json.loads(jsonout)
411    return parsed_json
412
413def confirm_transaction(payment, merchant_code, host, url, https, mac):
414    jr = get_JSON_webcheckout_response(merchant_code, payment.p_id, host, url,
415                           https, payment.amount_auth, mac)
416    error = jr.get('error')
417    if error:
418        msg = log = error
419        return False, msg, log
420
421    # A typical JSON response (test payment of Hector)
422
423    #{u'SplitAccounts': [],
424    #u'RemittanceAmount': 0,
425    #u'MerchantReference': u'p6709347986663',
426    #u'PaymentReference': u'FBN|WEB|MX76823|13-12-2022|935097929|608001',
427    #u'TransactionDate': u'2022-12-13T01:34:21',
428    #u'RetrievalReferenceNumber': u'814212374638',
429    #u'ResponseDescription': u'Approved by Financial Institution',
430    #u'Amount': 10000,
431    #u'CardNumber': u'',
432    #u'ResponseCode': u'00',
433    #u'BankCode': u'011'}
434
435    if not 'ResponseCode' in jr.keys() \
436        or not 'ResponseDescription' in jr.keys() \
437        or not 'Amount' in jr.keys():
438        msg = _('Invalid callback: ${a}', mapping = {'a': str(jr)})
439        log = 'invalid callback for payment %s: %s' % (payment.p_id, str(jr))
440        return False, msg, log
441    payment.r_code = jr['ResponseCode']
442    payment.r_desc = jr['ResponseDescription']
443    payment.r_amount_approved = jr['Amount'] / 100.0
444    payment.r_card_num = jr.get('CardNumber', u'')
445    payment.r_pay_reference = jr.get('PaymentReference', u'')
446    #payment.r_company = u'interswitch'
447    if payment.r_code != '00':
448        msg = _('Unsuccessful callback: ${a}', mapping = {'a': payment.r_desc})
449        log = 'unsuccessful callback for %s payment %s: %s' % (
450            payment.p_category, payment.p_id, payment.r_desc)
451        payment.p_state = 'failed'
452        notify(grok.ObjectModifiedEvent(payment))
453        return False, msg, log
454    if round(payment.r_amount_approved, 0) != round(payment.amount_auth, 0):
455        msg = _('Callback amount does not match.')
456        log = 'wrong callback for %s payment %s: %s' % (
457            payment.p_category, payment.p_id, str(jr))
458        payment.p_state = 'failed'
459        notify(grok.ObjectModifiedEvent(payment))
460        return False, msg, log
461    if jr['MerchantReference'] != payment.p_id:
462        msg = _('Callback transaction id does not match.')
463        log = 'wrong callback for %s payment %s: %s' % (
464            payment.p_category, payment.p_id, str(jr))
465        payment.p_state = 'failed'
466        notify(grok.ObjectModifiedEvent(payment))
467        return False, msg, log
468    payment.p_state = 'paid'
469    msg = _('Successful callback received')
470    log = 'valid callback for %s payment %s: %s' % (
471        payment.p_category, payment.p_id, str(jr))
472    notify(grok.ObjectModifiedEvent(payment))
473    return True, msg, log
Note: See TracBrowser for help on using the repository browser.