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

Last change on this file since 8485 was 8433, checked in by Henrik Bettermann, 13 years ago

Store utc not local time which lead to the addition of one hour of one hour.

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