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

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

Update payments catalog after interswitch payment.

  • Property svn:keywords set to Id
File size: 20.0 KB
Line 
1## $Id: browser.py 9713 2012-11-23 07:58:22Z 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        if 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
276        xmldict['provider_amt'] = 100 * provider_amt
277        xmldict['institution_item_name'] = self.category
278        xmldict['institution_name'] = INSTITUTION_NAME
279        xmldict['institution_amt'] = 100 * (
280            self.context.amount_auth - provider_amt - 150)
281        # Interswitch amount is not part of the xml data
282        if provider_amt == 0:
283            xmltext = """<payment_item_detail>
284<item_details detail_ref="%(detail_ref)s" college="%(institution_name)s" department="%(department)s" faculty="%(faculty)s">
285<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" />
286</item_details>
287</payment_item_detail>""" % xmldict
288        else:
289            xmltext = """<payment_item_detail>
290<item_details detail_ref="%(detail_ref)s" college="%(institution_name)s" department="%(department)s" faculty="%(faculty)s">
291<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" />
292<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" />
293</item_details>
294</payment_item_detail>""" % xmldict
295        self.xml_data = """<input type="hidden" name="xml_data" value='%s'  />""" % xmltext
296        return
297
298class InterswitchPageApplicant(KofaPage):
299    """ View which sends a POST request to the Interswitch
300    CollegePAY payment gateway.
301    """
302    grok.context(ICustomApplicantOnlinePayment)
303    grok.require('waeup.payApplicant')
304    grok.template('applicant_goto_interswitch')
305    grok.name('goto_interswitch')
306    label = _('Submit data to CollegePAY (Interswitch Payment Gateway)')
307    submit_button = _('Submit')
308    action = POST_ACTION
309    site_name = SITE_NAME
310    currency = CURRENCY
311    pay_item_id = '5703'
312    product_id = PRODUCT_ID
313
314    def update(self):
315        if self.context.p_state != 'unpaid':
316            self.flash(_("Payment ticket can't be re-send to CollegePAY."))
317            self.redirect(self.url(self.context, '@@index'))
318            return
319        if self.context.__parent__.__parent__.expired \
320            and self.context.__parent__.__parent__.strict_deadline:
321            self.flash(_("Payment ticket can't be send to CollegePAY. "
322                         "Application period has expired."))
323            self.redirect(self.url(self.context, '@@index'))
324            return
325        self.applicant = self.context.__parent__
326        self.amount_auth = 100 * self.context.amount_auth
327        xmldict = {}
328        self.category = getUtility(IKofaUtils).PAYMENT_CATEGORIES[self.context.p_category]
329        tz = getUtility(IKofaUtils).tzinfo
330        self.local_date_time = to_timezone(
331            self.context.creation_date, tz).strftime("%Y-%m-%d %H:%M:%S %Z")
332        self.site_redirect_url = self.url(self.context, 'request_webservice')
333        provider_amt = 400
334        if self.applicant.applicant_id.startswith('pg'):
335            xmldict['institution_acct'] = '0031716030'
336            xmldict['institution_bank_id'] = '10'
337        elif self.applicant.applicant_id.startswith('dp'):
338            xmldict['institution_acct'] = '9201805071'
339            xmldict['institution_bank_id'] = '17'
340        else:
341            xmldict['institution_acct'] = '6220032503'
342            xmldict['institution_bank_id'] = '51'
343        xmldict['detail_ref'] = self.context.p_id
344        xmldict['provider_amt'] = 100 * provider_amt
345        xmldict['provider_acct'] = PROVIDER_ACCT
346        xmldict['provider_bank_id'] = PROVIDER_BANK_ID
347        xmldict['provider_item_name'] = PROVIDER_ITEM_NAME
348        xmldict['institution_amt'] = 100 * (self.context.amount_auth - provider_amt - 150)
349        xmldict['institution_item_name'] = self.context.p_category
350        xmldict['institution_name'] = INSTITUTION_NAME
351        # Interswitch amount is not part of the xml data
352        xmltext = """<payment_item_detail>
353<item_details detail_ref="%(detail_ref)s" college="%(institution_name)s">
354<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" />
355<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" />
356</item_details>
357</payment_item_detail>""" % xmldict
358        self.xml_data = """<input type="hidden" name="xml_data" value='%s'  />""" % xmltext
359        return
360
361# Deprecated
362#class InterswitchPaymentCallbackPageStudent(UtilityView, grok.View):
363#    """ Callback view for the CollegePAY gateway
364#    """
365#    grok.context(ICustomStudentOnlinePayment)
366#    grok.name('isw_callback')
367#    grok.require('waeup.payStudent')
368
369    # This view is not yet working for offline querying transactions
370    # since the query string differs from the query string sent after
371    # posting transactions. This Interswitch bug must be removed first.
372    # Alternatively, we could use the webservice only and replace
373    # the RequestCallbackActionButton by a RequestWebserviceActionButton
374
375#    def update(self):
376#        if self.context.p_state == 'paid':
377#            self.flash(_('This ticket has already been paid.'))
378#            return
379#        student = self.context.student
380#        query = self.request.form
381#        write_log_message(self,'callback received: %s' % query)
382#        self.context.r_card_num = query.get('cardNum', None)
383#        self.context.r_code = query.get('resp', None)
384#        self.context.r_pay_reference  = query.get('payRef', None)
385#        self.context.r_amount_approved = float(query.get('apprAmt', '0.0')) / 100
386#        self.context.r_desc = query.get('desc', None)
387#        if self.context.r_code != '00':
388#            self.flash(_('Unsuccessful callback: ${a}',
389#                mapping = {'a': query.get('desc', _('Incomplete query string.'))}))
390#            write_log_message(self,'unsuccessful callback: %s' % self.context.p_id)
391#            self.context.p_state = 'failed'
392#            return
393#        if self.context.r_amount_approved != payment.amount_auth:
394#            self.flash(_('Wrong amount'))
395#            write_log_message(
396#                self,'successful but wrong amount: %s' % self.context.p_id)
397#            self.context.p_state = 'failed'
398#            return
399#        try:
400#            validation_list = get_SOAP_response(
401#                PRODUCT_ID, self.context.p_id).split(':')
402            # Validation does not make sense yet since the query string
403            # formats are conflicting. We are only printing the validation
404            # string, nothing else.
405#            print 'WARNING: Webservice validation is not yet implemented'
406#            print 'validation list: %s' % validation_list
407#        except:
408#            print 'Connection to webservice failed.'
409        # Add webservice validation here
410#        write_log_message(self,'valid callback: %s' % self.context.p_id)
411#        self.context.p_state = 'paid'
412#        self.context.payment_date = datetime.utcnow()
413#        actions_after_student_payment(student, self.context, self)
414#        return
415
416#    def render(self):
417#        self.redirect(self.url(self.context, '@@index'))
418#        return
419
420# Alternative solution, replaces InterswitchPaymentCallbackPage
421class InterswitchPaymentRequestWebservicePageStudent(UtilityView, grok.View):
422    """ Request webservice view for the CollegePAY gateway
423    """
424    grok.context(ICustomStudentOnlinePayment)
425    grok.name('request_webservice')
426    grok.require('waeup.payStudent')
427
428    def update(self):
429        ob_class = self.__implemented__.__name__
430        if self.context.p_state == 'paid':
431            self.flash(_('This ticket has already been paid.'))
432            return
433        student = self.context.student
434        success, msg, log = query_interswitch(self.context)
435        student.writeLogMessage(self, log)
436        if not success:
437            self.flash(msg)
438            return
439        success, msg, log = self.context.doAfterStudentPayment()
440        if log is not None:
441            student.writeLogMessage(self, log)
442        self.flash(msg)
443        return
444
445    def render(self):
446        self.redirect(self.url(self.context, '@@index'))
447        return
448
449class InterswitchPaymentRequestWebservicePageApplicant(UtilityView, grok.View):
450    """ Request webservice view for the CollegePAY gateway
451    """
452    grok.context(ICustomApplicantOnlinePayment)
453    grok.name('request_webservice')
454    grok.require('waeup.payApplicant')
455
456    def update(self):
457        if self.context.p_state == 'paid':
458            self.flash(_('This ticket has already been paid.'))
459            return
460        applicant = self.context.__parent__
461        success, msg, log = query_interswitch(self.context)
462        applicant.writeLogMessage(self, log)
463        if not success:
464            self.flash(msg)
465            return
466        success, msg, log = self.context.doAfterApplicantPayment()
467        if log is not None:
468            applicant.writeLogMessage(self, log)
469        self.flash(msg)
470        return
471
472    def render(self):
473        self.redirect(self.url(self.context, '@@index'))
474        return
Note: See TracBrowser for help on using the repository browser.