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

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

Remove surcharge configuration completely.

Implement application fee and school fee payments via Interswitch (part 3)

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