source: main/waeup.uniben/trunk/src/waeup/uniben/interswitch/browser.py @ 9740

Last change on this file since 9740 was 9728, checked in by Henrik Bettermann, 12 years ago

We've been informed that provider amount is zero.

  • Property svn:keywords set to Id
File size: 20.3 KB
Line 
1## $Id: browser.py 9728 2012-11-27 11:17:29Z 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##
18from datetime import datetime
19import httplib
20import urllib
21from xml.dom.minidom import parseString
22import grok
23from zope.event import notify
24from zope.component import getUtility
25from waeup.kofa.browser.layout import KofaPage, UtilityView
26from waeup.kofa.accesscodes import create_accesscode
27from waeup.kofa.interfaces import RETURNING, IKofaUtils
28from waeup.kofa.utils.helpers import to_timezone
29from waeup.kofa.students.viewlets import ApprovePaymentActionButton as APABStudent
30from waeup.kofa.applicants.viewlets import ApprovePaymentActionButton as APABApplicant
31from waeup.uniben.students.interfaces import ICustomStudentOnlinePayment
32from waeup.uniben.applicants.interfaces import ICustomApplicantOnlinePayment
33from waeup.uniben.interfaces import MessageFactory as _
34
35PRODUCT_ID = '57'
36SITE_NAME = 'uniben-kofa.waeup.org'
37PROVIDER_ACCT = '1010764827'
38PROVIDER_BANK_ID = '117'
39PROVIDER_ITEM_NAME = 'BT Education'
40INSTITUTION_NAME = 'Uniben'
41CURRENCY = '566'
42#QUERY_URL = 'https://webpay.interswitchng.com/paydirect/services/TransactionQueryURL.aspx'
43#QUERY_URL = 'https://testwebpay.interswitchng.com/test_paydirect/services/TransactionQueryURL.aspx'
44POST_ACTION = 'https://webpay.interswitchng.com/paydirect/webpay/pay.aspx'
45#POST_ACTION = 'https://testwebpay.interswitchng.com/test_paydirect/webpay/pay.aspx'
46
47HOST = 'webpay.interswitchng.com'
48#HOST = 'testwebpay.interswitchng.com'
49URL = '/paydirect/services/TransactionQueryWs.asmx'
50#URL = '/test_paydirect/services/TransactionQueryWs.asmx'
51httplib.HTTPConnection.debuglevel = 0
52
53
54def SOAP_post(soap_action,xml):
55    """Handles making the SOAP request.
56
57    Further reading:
58    http://testwebpay.interswitchng.com/test_paydirect/services/TransactionQueryWs.asmx?op=getTransactionData
59    """
60    h = httplib.HTTPConnection(HOST)
61    headers={
62        'Host':HOST,
63        'Content-Type':'text/xml; charset=utf-8',
64        'Content-Length':len(xml),
65        'SOAPAction':'"%s"' % soap_action,
66    }
67    h.request('POST', URL, body=xml,headers=headers)
68    r = h.getresponse()
69    d = r.read()
70    if r.status!=200:
71        raise ValueError('Error connecting: %s, %s' % (r.status, r.reason))
72    return d
73
74def get_SOAP_response(product_id, transref):
75    xml="""\
76<?xml version="1.0" encoding="utf-8"?>
77<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/">
78  <soap:Body>
79    <getTransactionData xmlns="http://tempuri.org/">
80      <product_id>%s</product_id>
81      <trans_ref>%s</trans_ref>
82    </getTransactionData>
83  </soap:Body>
84</soap:Envelope>""" % (product_id, transref)
85    result_xml=SOAP_post("http://tempuri.org/getTransactionData",xml)
86    doc=parseString(result_xml)
87    response=doc.getElementsByTagName('getTransactionDataResult')[0].firstChild.data
88    return response
89
90def query_interswitch(payment):
91    sr = get_SOAP_response(PRODUCT_ID, payment.p_id)
92    wlist = sr.split(':')
93    if len(wlist) != 7:
94        msg = _('Invalid callback: ${a}', mapping = {'a': sr})
95        log = 'invalid callback for payment %s: %s' % (payment.p_id, sr)
96        return False, msg, log
97    payment.r_code = wlist[0]
98    payment.r_desc = wlist[1]
99    payment.r_amount_approved = float(wlist[2]) / 100
100    payment.r_card_num = wlist[3]
101    payment.r_pay_reference = wlist[5]
102    payment.r_company = u'interswitch'
103    if payment.r_code != '00':
104        msg = _('Unsuccessful callback: ${a}', mapping = {'a': sr})
105        log = 'unsuccessful callback for %s payment %s: %s' % (
106            payment.p_category, payment.p_id, sr)
107        payment.p_state = 'failed'
108        notify(grok.ObjectModifiedEvent(payment))
109        return False, msg, log
110    if payment.r_amount_approved != payment.amount_auth:
111        msg = _('Callback amount does not match.')
112        log = 'wrong callback for %s payment %s: %s' % (
113            payment.p_category, payment.p_id, sr)
114        payment.p_state = 'failed'
115        notify(grok.ObjectModifiedEvent(payment))
116        return False, msg, log
117    if wlist[4] != payment.p_id:
118        msg = _('Callback transaction id does not match.')
119        log = 'wrong callback for %s payment %s: %s' % (
120            payment.p_category, payment.p_id, sr)
121        payment.p_state = 'failed'
122        notify(grok.ObjectModifiedEvent(payment))
123        return False, msg, log
124    payment.p_state = 'paid'
125    payment.payment_date = datetime.utcnow()
126    msg = _('Successful callback received')
127    log = 'valid callback for %s payment %s: %s' % (
128        payment.p_category, payment.p_id, sr)
129    notify(grok.ObjectModifiedEvent(payment))
130    return True, msg, log
131
132class InterswitchActionButtonStudent(APABStudent):
133    grok.order(1)
134    grok.context(ICustomStudentOnlinePayment)
135    grok.require('waeup.payStudent')
136    icon = 'actionicon_pay.png'
137    text = _('CollegePAY')
138    target = 'goto_interswitch'
139
140    @property
141    def target_url(self):
142        if self.context.p_state != 'unpaid':
143            return ''
144        return self.view.url(self.view.context, self.target)
145
146class InterswitchActionButtonApplicant(APABApplicant):
147    grok.order(1)
148    grok.context(ICustomApplicantOnlinePayment)
149    grok.require('waeup.payApplicant')
150    icon = 'actionicon_pay.png'
151    text = _('CollegePAY')
152    target = 'goto_interswitch'
153
154    @property
155    def target_url(self):
156        if self.context.p_state != 'unpaid':
157            return ''
158        return self.view.url(self.view.context, self.target)
159
160# Deprecated
161#class InterswitchRequestCallbackActionButtonStudent(RCABStudent):
162#    grok.order(3)
163#    grok.context(ICustomStudentOnlinePayment)
164#    icon = 'actionicon_call.png'
165#    text = _('Request CollegePAY callback')
166
167#    def target_url(self):
168#        if self.context.p_state == 'paid':
169#            return ''
170#        site_redirect_url = self.view.url(self.view.context, 'isw_callback')
171#        args = {
172#            'transRef':self.context.p_id,
173#            'prodID':PRODUCT_ID,
174#            'redirectURL':site_redirect_url}
175#        return QUERY_URL + '?%s' % urllib.urlencode(args)
176
177# Alternative preferred solution
178class InterswitchRequestWebserviceActionButtonStudent(APABStudent):
179    grok.order(2)
180    grok.context(ICustomStudentOnlinePayment)
181    grok.require('waeup.payStudent')
182    icon = 'actionicon_call.png'
183    text = _('Requery CollegePAY')
184    target = 'request_webservice'
185
186class InterswitchRequestWebserviceActionButtonApplicant(APABApplicant):
187    grok.order(2)
188    grok.context(ICustomApplicantOnlinePayment)
189    grok.require('waeup.payApplicant')
190    icon = 'actionicon_call.png'
191    text = _('Requery CollegePAY')
192    target = 'request_webservice'
193
194
195class InterswitchPageStudent(KofaPage):
196    """ View which sends a POST request to the Interswitch
197    CollegePAY payment gateway.
198    """
199    grok.context(ICustomStudentOnlinePayment)
200    grok.name('goto_interswitch')
201    grok.template('student_goto_interswitch')
202    grok.require('waeup.payStudent')
203    label = _('Submit data to CollegePAY (Interswitch Payment Gateway)')
204    submit_button = _('Submit')
205    action = POST_ACTION
206    site_name = SITE_NAME
207    currency = CURRENCY
208    product_id = PRODUCT_ID
209
210    def update(self):
211        #if self.context.p_state != 'unpaid':
212        if self.context.p_state == 'paid':
213            self.flash(_("Payment ticket can't be re-send to CollegePAY."))
214            self.redirect(self.url(self.context, '@@index'))
215            return
216
217        student = self.student = self.context.student
218        certificate = getattr(student['studycourse'],'certificate',None)
219        self.amount_auth = 100 * self.context.amount_auth
220        xmldict = {}
221        if certificate is not None:
222            xmldict['department'] = certificate.__parent__.__parent__.code
223            xmldict['faculty'] = certificate.__parent__.__parent__.__parent__.code
224        else:
225            xmldict['department'] = None
226            xmldict['faculty'] = None
227        self.category = getUtility(IKofaUtils).PAYMENT_CATEGORIES[self.context.p_category]
228        tz = getUtility(IKofaUtils).tzinfo
229        self.local_date_time = to_timezone(
230            self.context.creation_date, tz).strftime("%Y-%m-%d %H:%M:%S %Z")
231        self.site_redirect_url = self.url(self.context, 'request_webservice')
232        # Provider data
233        xmldict['detail_ref'] = self.context.p_id
234        xmldict['provider_acct'] = PROVIDER_ACCT
235        xmldict['provider_bank_id'] = PROVIDER_BANK_ID
236        xmldict['provider_item_name'] = PROVIDER_ITEM_NAME
237        # Institution data
238        xmldict['institution_acct'] = '000000000000'
239        xmldict['institution_bank_id'] = '00'
240        xmldict['institution_amt'] = '0.0'
241        if self.context.p_category == 'schoolfee':
242            provider_amt = 1500
243            if student.current_mode.endswith('_ft'):
244                self.pay_item_id = '5700'
245                if student.current_mode in ('ug_ft','de_ft','ct_ft','ume_ft'):
246                    xmldict['institution_acct'] = '2017506430'
247                    xmldict['institution_bank_id'] = '8'
248                elif student.current_mode in ('dp_ft'):
249                    xmldict['institution_acct'] = '9201805071'
250                    xmldict['institution_bank_id'] = '17'
251                elif student.current_mode in ('pg_ft'):
252                    xmldict['institution_acct'] = '5330832799'
253                    xmldict['institution_bank_id'] = '51'
254            elif student.current_mode.endswith('_pt'):
255                self.pay_item_id = '5701'
256                if student.current_mode in ('ug_pt','de_pt','ct_pt'):
257                    xmldict['institution_acct'] = '0122009929'
258                    xmldict['institution_bank_id'] = '16'
259                elif student.current_mode in ('dp_pt'):
260                    xmldict['institution_acct'] = '9201805071'
261                    xmldict['institution_bank_id'] = '17'
262                elif student.current_mode in ('pg_pt'):
263                    xmldict['institution_acct'] = '0031716047'
264                    xmldict['institution_bank_id'] = '10'
265        elif self.context.p_category == 'clearance':
266            self.pay_item_id = '5702'
267            provider_amt = 1500
268            xmldict['institution_bank_id'] = '7'
269            xmldict['institution_acct'] = '1003475516'
270        elif self.context.p_category == 'gown':
271            self.pay_item_id = '5704'
272            provider_amt = 0
273            xmldict['institution_bank_id'] = '7'
274            xmldict['institution_acct'] = '1016232382'
275        elif self.context.p_category.startswith('hostel_maintenance'):
276            self.pay_item_id = '5705'
277            provider_amt = 0
278            xmldict['institution_bank_id'] = '129'
279            xmldict['institution_acct'] = '0014414547'
280
281        xmldict['provider_amt'] = 100 * provider_amt
282        xmldict['institution_item_name'] = self.category
283        xmldict['institution_name'] = INSTITUTION_NAME
284        xmldict['institution_amt'] = 100 * (
285            self.context.amount_auth - provider_amt - 150)
286        # Interswitch amount is not part of the xml data
287        if provider_amt == 0:
288            xmltext = """<payment_item_detail>
289<item_details detail_ref="%(detail_ref)s" college="%(institution_name)s" department="%(department)s" faculty="%(faculty)s">
290<item_detail item_id="1" item_name="%(institution_item_name)s" item_amt="%(institution_amt)d" bank_id="%(institution_bank_id)s" acct_num="%(institution_acct)s" />
291</item_details>
292</payment_item_detail>""" % xmldict
293        else:
294            xmltext = """<payment_item_detail>
295<item_details detail_ref="%(detail_ref)s" college="%(institution_name)s" department="%(department)s" faculty="%(faculty)s">
296<item_detail item_id="1" item_name="%(institution_item_name)s" item_amt="%(institution_amt)d" bank_id="%(institution_bank_id)s" acct_num="%(institution_acct)s" />
297<item_detail item_id="2" item_name="%(provider_item_name)s" item_amt="%(provider_amt)d" bank_id="%(provider_bank_id)s" acct_num="%(provider_acct)s" />
298</item_details>
299</payment_item_detail>""" % xmldict
300        self.xml_data = """<input type="hidden" name="xml_data" value='%s'  />""" % xmltext
301        return
302
303class InterswitchPageApplicant(KofaPage):
304    """ View which sends a POST request to the Interswitch
305    CollegePAY payment gateway.
306    """
307    grok.context(ICustomApplicantOnlinePayment)
308    grok.require('waeup.payApplicant')
309    grok.template('applicant_goto_interswitch')
310    grok.name('goto_interswitch')
311    label = _('Submit data to CollegePAY (Interswitch Payment Gateway)')
312    submit_button = _('Submit')
313    action = POST_ACTION
314    site_name = SITE_NAME
315    currency = CURRENCY
316    pay_item_id = '5703'
317    product_id = PRODUCT_ID
318
319    def update(self):
320        if self.context.p_state != 'unpaid':
321            self.flash(_("Payment ticket can't be re-send to CollegePAY."))
322            self.redirect(self.url(self.context, '@@index'))
323            return
324        if self.context.__parent__.__parent__.expired \
325            and self.context.__parent__.__parent__.strict_deadline:
326            self.flash(_("Payment ticket can't be send to CollegePAY. "
327                         "Application period has expired."))
328            self.redirect(self.url(self.context, '@@index'))
329            return
330        self.applicant = self.context.__parent__
331        self.amount_auth = 100 * self.context.amount_auth
332        xmldict = {}
333        self.category = getUtility(IKofaUtils).PAYMENT_CATEGORIES[self.context.p_category]
334        tz = getUtility(IKofaUtils).tzinfo
335        self.local_date_time = to_timezone(
336            self.context.creation_date, tz).strftime("%Y-%m-%d %H:%M:%S %Z")
337        self.site_redirect_url = self.url(self.context, 'request_webservice')
338        provider_amt = 400
339        if self.applicant.applicant_id.startswith('pg'):
340            xmldict['institution_acct'] = '0031716030'
341            xmldict['institution_bank_id'] = '10'
342        elif self.applicant.applicant_id.startswith('dp'):
343            xmldict['institution_acct'] = '9201805071'
344            xmldict['institution_bank_id'] = '17'
345        else:
346            xmldict['institution_acct'] = '6220032503'
347            xmldict['institution_bank_id'] = '51'
348        xmldict['detail_ref'] = self.context.p_id
349        xmldict['provider_amt'] = 100 * provider_amt
350        xmldict['provider_acct'] = PROVIDER_ACCT
351        xmldict['provider_bank_id'] = PROVIDER_BANK_ID
352        xmldict['provider_item_name'] = PROVIDER_ITEM_NAME
353        xmldict['institution_amt'] = 100 * (self.context.amount_auth - provider_amt - 150)
354        xmldict['institution_item_name'] = self.context.p_category
355        xmldict['institution_name'] = INSTITUTION_NAME
356        # Interswitch amount is not part of the xml data
357        xmltext = """<payment_item_detail>
358<item_details detail_ref="%(detail_ref)s" college="%(institution_name)s">
359<item_detail item_id="1" item_name="%(institution_item_name)s" item_amt="%(institution_amt)d" bank_id="%(institution_bank_id)s" acct_num="%(institution_acct)s" />
360<item_detail item_id="2" item_name="%(provider_item_name)s" item_amt="%(provider_amt)d" bank_id="%(provider_bank_id)s" acct_num="%(provider_acct)s" />
361</item_details>
362</payment_item_detail>""" % xmldict
363        self.xml_data = """<input type="hidden" name="xml_data" value='%s'  />""" % xmltext
364        return
365
366# Deprecated
367#class InterswitchPaymentCallbackPageStudent(UtilityView, grok.View):
368#    """ Callback view for the CollegePAY gateway
369#    """
370#    grok.context(ICustomStudentOnlinePayment)
371#    grok.name('isw_callback')
372#    grok.require('waeup.payStudent')
373
374    # This view is not yet working for offline querying transactions
375    # since the query string differs from the query string sent after
376    # posting transactions. This Interswitch bug must be removed first.
377    # Alternatively, we could use the webservice only and replace
378    # the RequestCallbackActionButton by a RequestWebserviceActionButton
379
380#    def update(self):
381#        if self.context.p_state == 'paid':
382#            self.flash(_('This ticket has already been paid.'))
383#            return
384#        student = self.context.student
385#        query = self.request.form
386#        write_log_message(self,'callback received: %s' % query)
387#        self.context.r_card_num = query.get('cardNum', None)
388#        self.context.r_code = query.get('resp', None)
389#        self.context.r_pay_reference  = query.get('payRef', None)
390#        self.context.r_amount_approved = float(query.get('apprAmt', '0.0')) / 100
391#        self.context.r_desc = query.get('desc', None)
392#        if self.context.r_code != '00':
393#            self.flash(_('Unsuccessful callback: ${a}',
394#                mapping = {'a': query.get('desc', _('Incomplete query string.'))}))
395#            write_log_message(self,'unsuccessful callback: %s' % self.context.p_id)
396#            self.context.p_state = 'failed'
397#            return
398#        if self.context.r_amount_approved != payment.amount_auth:
399#            self.flash(_('Wrong amount'))
400#            write_log_message(
401#                self,'successful but wrong amount: %s' % self.context.p_id)
402#            self.context.p_state = 'failed'
403#            return
404#        try:
405#            validation_list = get_SOAP_response(
406#                PRODUCT_ID, self.context.p_id).split(':')
407            # Validation does not make sense yet since the query string
408            # formats are conflicting. We are only printing the validation
409            # string, nothing else.
410#            print 'WARNING: Webservice validation is not yet implemented'
411#            print 'validation list: %s' % validation_list
412#        except:
413#            print 'Connection to webservice failed.'
414        # Add webservice validation here
415#        write_log_message(self,'valid callback: %s' % self.context.p_id)
416#        self.context.p_state = 'paid'
417#        self.context.payment_date = datetime.utcnow()
418#        actions_after_student_payment(student, self.context, self)
419#        return
420
421#    def render(self):
422#        self.redirect(self.url(self.context, '@@index'))
423#        return
424
425# Alternative solution, replaces InterswitchPaymentCallbackPage
426class InterswitchPaymentRequestWebservicePageStudent(UtilityView, grok.View):
427    """ Request webservice view for the CollegePAY gateway
428    """
429    grok.context(ICustomStudentOnlinePayment)
430    grok.name('request_webservice')
431    grok.require('waeup.payStudent')
432
433    def update(self):
434        ob_class = self.__implemented__.__name__
435        if self.context.p_state == 'paid':
436            self.flash(_('This ticket has already been paid.'))
437            return
438        student = self.context.student
439        success, msg, log = query_interswitch(self.context)
440        student.writeLogMessage(self, log)
441        if not success:
442            self.flash(msg)
443            return
444        success, msg, log = self.context.doAfterStudentPayment()
445        if log is not None:
446            student.writeLogMessage(self, log)
447        self.flash(msg)
448        return
449
450    def render(self):
451        self.redirect(self.url(self.context, '@@index'))
452        return
453
454class InterswitchPaymentRequestWebservicePageApplicant(UtilityView, grok.View):
455    """ Request webservice view for the CollegePAY gateway
456    """
457    grok.context(ICustomApplicantOnlinePayment)
458    grok.name('request_webservice')
459    grok.require('waeup.payApplicant')
460
461    def update(self):
462        if self.context.p_state == 'paid':
463            self.flash(_('This ticket has already been paid.'))
464            return
465        applicant = self.context.__parent__
466        success, msg, log = query_interswitch(self.context)
467        applicant.writeLogMessage(self, log)
468        if not success:
469            self.flash(msg)
470            return
471        success, msg, log = self.context.doAfterApplicantPayment()
472        if log is not None:
473            applicant.writeLogMessage(self, log)
474        self.flash(msg)
475        return
476
477    def render(self):
478        self.redirect(self.url(self.context, '@@index'))
479        return
Note: See TracBrowser for help on using the repository browser.