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

Last change on this file since 16515 was 16515, checked in by Henrik Bettermann, 3 years ago

Amounts are in Kobo. But it's still not clear if Amount also contains the the gateway fee.

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