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

Last change on this file since 17231 was 17231, checked in by Henrik Bettermann, 23 months ago

Use Mac validation.

  • Property svn:keywords set to Id
File size: 18.4 KB
RevLine 
[9746]1## $Id: helpers.py 17231 2022-12-18 09:24:18Z 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"""
[9747]20from datetime import datetime
[17014]21from ssl import SSLError
[9746]22import httplib
[13387]23import hashlib
24import json
25from urllib import urlencode
[9746]26import grok
27from xml.dom.minidom import parseString
28from zope.event import notify
[16487]29from waeup.kofa.payments.interfaces import IPayer
[9746]30from kofacustom.nigeria.interfaces import MessageFactory as _
31
[11914]32def SOAP_post(soap_action, xml, host, url, https):
[9746]33    """Handles making the SOAP request.
34    """
[11914]35    if https:
36        h = httplib.HTTPSConnection(host)
37    else:
38        h = httplib.HTTPConnection(host)
[9746]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
[16484]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))
[13387]56
[16484]57# CollegePAY helper functions
58
[11914]59def get_SOAP_response(product_id, transref, host, url, https):
[9746]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)
[11914]70    response=SOAP_post("http://tempuri.org/getTransactionData",xml, host, url, https)
[9746]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
[13387]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)
[17014]95    try:
96        h.request("GET", url, headers=headers)
97    except SSLError:
98        return {'error': 'SSL handshake error'}
[13387]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
[13584]106def query_interswitch_SOAP(payment, product_id, host, url, https, verify):
[11914]107    sr = get_SOAP_response(product_id, payment.p_id, host, url, https)
[9746]108    if sr.startswith('Connection error'):
109        msg = _('Connection error')
110        log = sr
111        return False, msg, log
112    wlist = sr.split(':')
[13709]113    if len(wlist) < 7:
[9746]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
[14245]130    if round(payment.r_amount_approved, 0) != round(payment.amount_auth, 0):
[9746]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'
[13584]145    if not verify:
146        payment.payment_date = datetime.utcnow()
[13581]147    msg = _('Successful callback received.')
[9746]148    log = 'valid callback for %s payment %s: %s' % (
149        payment.p_category, payment.p_id, sr)
150    notify(grok.ObjectModifiedEvent(payment))
[9774]151    return True, msg, log
152
[13584]153def query_interswitch(payment, product_id, host, url, https, mac, verify):
[13387]154    # If no mac mac key is given, fall back to deprecated SOAP method
155    # (Uniben, AAUA, FCEOkene).
156    if mac == None:
[13584]157        return query_interswitch_SOAP(
158            payment, product_id, host, url, https, verify)
[13387]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
[16114]167
168    # old:
169
[13387]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
[16114]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
[16167]195    if not 'ResponseCode' in jr.keys() \
196        or not 'ResponseDescription' in jr.keys() \
197        or not 'Amount' in jr.keys():
[13387]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
[13585]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
[13387]205    payment.r_code = jr['ResponseCode']
206    payment.r_desc = jr['ResponseDescription']
207    payment.r_amount_approved = jr['Amount'] / 100.0
[16167]208    payment.r_card_num = jr.get('CardNumber', u'')
[16114]209    payment.r_pay_reference = jr.get('PaymentReference', u'')
[15727]210    #payment.r_company = u'interswitch'
[13387]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
[14245]218    if round(payment.r_amount_approved, 0) != round(payment.amount_auth, 0):
[13387]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'
[13584]233    if not verify:
234        payment.payment_date = datetime.utcnow()
[13387]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
[16484]241# PAYDirect helper functions
242
[16587]243def create_paydirect_booking(merchant_id, payment, item_code, host, url, https):
[16487]244    p_id = payment.p_id
245    description = payment.p_category
[16515]246    amount = int(100*payment.amount_auth)  # Amount in Kobo
[16487]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,
[16587]282    email, item_code)
[16487]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
[16484]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:
[16488]333        payment.r_desc = "Channel Name: %s - Terminal Id: %s - Location: %s" % (
[16484]334            doc.getElementsByTagName('ChannelName')[0].firstChild.data,
335            doc.getElementsByTagName('TerminalId')[0].firstChild.data,
[16488]336            doc.getElementsByTagName('Location')[0].firstChild.data)
[16484]337    except AttributeError:
338        pass
339    try:
340        amount = doc.getElementsByTagName('Amount')[0].firstChild.data
[16515]341        payment.r_amount_approved = float(amount) / 100
[16484]342    except AttributeError:
343        pass
344    try:
345        payment.r_pay_reference = doc.getElementsByTagName(
[16488]346            'ReferenceNumber')[0].firstChild.data
[16484]347    except AttributeError:
348        pass
349    if p_status not in ('Pending', 'Completed'):
[16488]350        msg = _('Unknown status: %s' % p_status)
351        log = 'invalid callback for payment %s: %s' % (payment.p_id, p_status)
[16484]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
[16556]367    if payment.r_amount_approved != payment.amount_auth:
[16528]368        msg = _('Callback amount does not match net amount.')
[16484]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))
[17215]380    return True, msg, log
381
382# Web checkout helper functions
383
[17231]384def get_JSON_webcheckout_response(merchant_code, transref, host, url,
385                                  https, amount, mac=None):
386    amount = int(100 * amount)
387    hashargs = transref + merchant_code + str(amount) + mac
388    hashvalue = hashlib.sha512(hashargs).hexdigest()
[17215]389    headers={
390        'Content-Type':'text/xml; charset=utf-8',
[17231]391        'Hash':hashvalue,
[17215]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:
[17231]407        return {'error': 'Connection error (%s, %s)' % (
408            response.status, response.reason)}
[17215]409    jsonout = response.read()
410    parsed_json = json.loads(jsonout)
411    return parsed_json
412
[17231]413def confirm_transaction(payment, merchant_code, host, url, https, mac):
[17215]414    jr = get_JSON_webcheckout_response(merchant_code, payment.p_id, host, url,
[17231]415                           https, payment.amount_auth, mac)
[17215]416    error = jr.get('error')
417    if error:
418        msg = log = error
419        return False, msg, log
420
[17221]421    # A typical JSON response (test payment of Hector)
[17215]422
[17221]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'}
[17215]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))
[16484]473    return True, msg, log
Note: See TracBrowser for help on using the repository browser.