source: main/waeup.aaue/trunk/src/waeup/aaue/etranzact/browser.py @ 12117

Last change on this file since 12117 was 11868, checked in by Henrik Bettermann, 10 years ago

Add Interswitch school fee components for testing.

  • Property svn:keywords set to Id
File size: 14.4 KB
RevLine 
[7929]1## $Id: browser.py 11868 2014-10-22 04:03:57Z 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
[10070]20import urllib
[10069]21import urllib2
[7929]22from xml.dom.minidom import parseString
23import grok
[8704]24from zope.component import getUtility
25from zope.catalog.interfaces import ICatalog
[11631]26from waeup.kofa.interfaces import IUniversity, CLEARED
[10032]27from waeup.kofa.payments.interfaces import IPayer
28from waeup.kofa.webservices import PaymentDataWebservice
[7929]29from waeup.kofa.browser.layout import KofaPage, UtilityView
[8430]30from waeup.kofa.students.viewlets import ApprovePaymentActionButton as APABStudent
31from waeup.kofa.applicants.viewlets import ApprovePaymentActionButton as APABApplicant
[8710]32from waeup.aaue.interfaces import academic_sessions_vocab
[9904]33from kofacustom.nigeria.interswitch.browser import (
34    InterswitchActionButtonStudent,
35    InterswitchRequestWebserviceActionButtonStudent,
36    InterswitchActionButtonApplicant,
37    InterswitchRequestWebserviceActionButtonApplicant)
[8444]38from waeup.aaue.interfaces import MessageFactory as _
39from waeup.aaue.students.interfaces import ICustomStudentOnlinePayment
40from waeup.aaue.applicants.interfaces import ICustomApplicantOnlinePayment
[7929]41
[10978]42ERROR_PART1 = (
43        'PayeeName=N/A~'
44        + 'Faculty=N/A~'
45        + 'Department=N/A~'
46        + 'Level=N/A~'
47        + 'ProgrammeType=N/A~'
48        + 'StudyType=N/A~'
49        + 'Session=N/A~'
50        + 'PayeeID=N/A~'
51        + 'Amount=N/A~'
52        + 'FeeStatus=')
53ERROR_PART2 = (
54        '~Semester=N/A~'
55        + 'PaymentType=N/A~'
56        + 'MatricNumber=N/A~'
57        + 'Email=N/A~'
58        + 'PhoneNumber=N/A')
59
[10032]60class CustomPaymentDataWebservice(PaymentDataWebservice):
61    """A simple webservice to publish payment and payer details on request from
62    accepted IP addresses without authentication.
[8746]63
[10032]64    Etranzact is asking for the PAYEE_ID which is indeed misleading.
65    These are not the data of the payee but of the payer. And it's
66    not the id of the payer but of the payment.
67    """
68    grok.name('feerequest')
[8769]69
[10032]70    #ACCEPTED_IP = ('195.219.3.181', '195.219.3.184')
71    ACCEPTED_IP = None
[8698]72
[10937]73    def update(self, PAYEE_ID=None, PAYMENT_TYPE=None):
[9508]74        if PAYEE_ID == None:
[10978]75            self.output = ERROR_PART1 + 'Missing PAYEE_ID' + ERROR_PART2
[9508]76            return
[8746]77        real_ip = self.request.get('HTTP_X_FORWARDED_FOR', None)
[8769]78        # We can forego the logging once eTranzact payments run smoothly
79        # and the accepted IP addresses are used.
80        if real_ip:
[10032]81            self.context.logger.info('PaymentDataWebservice called: %s' % real_ip)
82        if real_ip  and self.ACCEPTED_IP:
83            if real_ip not in  self.ACCEPTED_IP:
[10978]84                self.output = ERROR_PART1 + 'Wrong IP address' + ERROR_PART2
[8746]85                return
[11652]86        if PAYMENT_TYPE not in ('SCHOOL-FEE-RETURNING', 'SCHOOL-FEE-NEW',
87            'ACCEPTANCE-FEE', 'APPLICATION-FEE'):
[10978]88            self.output = ERROR_PART1 + 'Invalid PAYMENT_TYPE' + ERROR_PART2
[10937]89            return
[8754]90
91        # It seems eTranzact sends a POST request with an empty body but the URL
92        # contains a query string. So it's actually a GET request pretended
93        # to be a POST request. Although this does not comply with the
94        # RFC 2616 HTTP guidelines we may try to fetch the id from the QUERY_STRING
95        # value of the request.
96        #if PAYEE_ID is None:
97        #    try:
98        #        PAYEE_ID = self.request['QUERY_STRING'].split('=')[1]
99        #    except:
[10937]100        #        self.output = '-4'
[8754]101        #        return
102
[8704]103        cat = getUtility(ICatalog, name='payments_catalog')
104        results = list(cat.searchResults(p_id=(PAYEE_ID, PAYEE_ID)))
105        if len(results) != 1:
[10978]106            self.output = ERROR_PART1 + 'Invalid PAYEE_ID' + ERROR_PART2
[8746]107            return
[11554]108        amount = results[0].amount_auth
109        payment_type = results[0].category
110        programme_type = results[0].p_item
111        academic_session = academic_sessions_vocab.getTerm(
112            results[0].p_session).title
113        status = results[0].p_state
114        if status == 'paid':
115            self.output = ERROR_PART1 + 'PAYEE_ID already used' + ERROR_PART2
116            return
[11652]117        if PAYMENT_TYPE.startswith('SCHOOL-FEE'):
[11654]118            try:
119                student = results[0].student
120            except AttributeError:
121                self.output = ERROR_PART1 + 'Wrong PAYMENT_TYPE' + ERROR_PART2
122                return
[11652]123            if not results[0].p_category.startswith('schoolfee'):
124                self.output = ERROR_PART1 + 'Wrong PAYMENT_TYPE' + ERROR_PART2
125                return
126            if PAYMENT_TYPE == 'SCHOOL-FEE-RETURNING' and student.state == CLEARED:
127                self.output = ERROR_PART1 + 'Wrong PAYMENT_TYPE' + ERROR_PART2
128                return
129            if PAYMENT_TYPE == 'SCHOOL-FEE-NEW' and student.state != CLEARED:
130                self.output = ERROR_PART1 + 'Wrong PAYMENT_TYPE' + ERROR_PART2
131                return
[11651]132        if PAYMENT_TYPE == 'ACCEPTANCE-FEE' \
133            and not results[0].p_category == 'clearance':
134            self.output = ERROR_PART1 + 'Wrong PAYMENT_TYPE' + ERROR_PART2
135            return
136        if PAYMENT_TYPE == 'APPLICATION-FEE' \
137            and not results[0].p_category == 'application':
138            self.output = ERROR_PART1 + 'Wrong PAYMENT_TYPE' + ERROR_PART2
139            return
[8746]140        try:
[10032]141            owner = IPayer(results[0])
[8746]142            full_name = owner.display_fullname
143            matric_no = owner.id
144            faculty = owner.faculty
145            department = owner.department
[10907]146            study_type = owner.current_mode
147            email = owner.email
148            phone = owner.phone
149            level = owner.current_level
[8746]150        except (TypeError, AttributeError):
[10978]151            self.output = ERROR_PART1 +  'Unknown error' + ERROR_PART2
[8746]152            return
153        self.output = (
[10907]154            'PayeeName=%s~' +
155            'Faculty=%s~' +
156            'Department=%s~' +
157            'Level=%s~' +
158            'ProgrammeType=%s~' +
159            'StudyType=%s~' +
160            'Session=%s~' +
161            'PayeeID=%s~' +
162            'Amount=%s~' +
163            'FeeStatus=%s~' +
[10932]164            'Semester=N/A~' +
[10907]165            'PaymentType=%s~' +
166            'MatricNumber=%s~' +
167            'Email=%s~' +
168            'PhoneNumber=%s'
169
170            ) % (full_name, faculty,
171            department, level, programme_type, study_type,
172            academic_session, PAYEE_ID, amount, status, payment_type,
173            matric_no, email, phone)
[8698]174        return
175
176
177# Requerying eTranzact payments
178
[10983]179TERMINAL_ID = '0570000070'
180QUERY_URL =   'https://www.etranzact.net/WebConnectPlus/query.jsp'
[8259]181
[8680]182# Test environment
[8776]183#QUERY_URL =   'http://demo.etranzact.com:8080/WebConnect/queryPayoutletTransaction.jsp'
184#TERMINAL_ID = '5009892289'
[8680]185
[8430]186def query_etranzact(confirmation_number, payment):
187   
[8247]188    postdict = {}
189    postdict['TERMINAL_ID'] = TERMINAL_ID
190    #postdict['RESPONSE_URL'] = 'http://dummy'
191    postdict['CONFIRMATION_NO'] = confirmation_number
[10070]192    data = urllib.urlencode(postdict)
[8682]193    payment.conf_number = confirmation_number
[8247]194    try:
[10069]195        # eTranzact only accepts HTTP 1.1 requests. Therefore
196        # the urllib2 package is required here.
197        f = urllib2.urlopen(url=QUERY_URL, data=data)
[8247]198        success = f.read()
[8432]199        success = success.replace('\r\n','')
[9935]200        if 'CUSTOMER_ID' not in success:
[8430]201            msg = _('Invalid or unsuccessful callback: ${a}',
202                mapping = {'a': success})
203            log = 'invalid callback for payment %s: %s' % (payment.p_id, success)
[8247]204            payment.p_state = 'failed'
[8430]205            return False, msg, log
[8247]206        success = success.replace('%20',' ').split('&')
207        # We expect at least two parameters
208        if len(success) < 2:
[8430]209            msg = _('Invalid callback: ${a}', mapping = {'a': success})
210            log = 'invalid callback for payment %s: %s' % (payment.p_id, success)
[8247]211            payment.p_state = 'failed'
[8430]212            return False, msg, log
[8247]213        try:
214            success_dict = dict([tuple(i.split('=')) for i in success])
215        except ValueError:
[8430]216            msg = _('Invalid callback: ${a}', mapping = {'a': success})
217            log = 'invalid callback for payment %s: %s' % (payment.p_id, success)
[8247]218            payment.p_state = 'failed'
[8430]219            return False, msg, log
[8247]220    except IOError:
[8430]221        msg = _('eTranzact IOError')
222        log = 'eTranzact IOError'
223        return False, msg, log
[8247]224    payment.r_code = u'ET'
[9327]225    payment.r_company = u'etranzact'
[8247]226    payment.r_desc = u'%s' % success_dict.get('TRANS_DESCR')
227    payment.r_amount_approved = float(success_dict.get('TRANS_AMOUNT',0.0))
228    payment.r_card_num = None
229    payment.r_pay_reference = u'%s' % success_dict.get('RECEIPT_NO')
230    if payment.r_amount_approved != payment.amount_auth:
[8430]231        msg = _('Wrong amount')
232        log = 'wrong callback for payment %s: %s' % (payment.p_id, success)
[8247]233        payment.p_state = 'failed'
[8430]234        return False, msg, log
[9935]235    customer_id = success_dict.get('CUSTOMER_ID')
236    if payment.p_id != customer_id:
[8717]237        msg = _('Wrong payment id')
[8430]238        log = 'wrong callback for payment %s: %s' % (payment.p_id, success)
[8247]239        payment.p_state = 'failed'
[8430]240        return False, msg, log
241    log = 'valid callback for payment %s: %s' % (payment.p_id, success)
242    msg = _('Successful callback received')
[8247]243    payment.p_state = 'paid'
[8433]244    payment.payment_date = datetime.utcnow()
[8430]245    return True, msg, log
[8247]246
[8430]247class EtranzactEnterPinActionButtonApplicant(APABApplicant):
[8253]248    grok.context(ICustomApplicantOnlinePayment)
[8430]249    grok.require('waeup.payApplicant')
[8259]250    grok.order(3)
[7929]251    icon = 'actionicon_call.png'
252    text = _('Query eTranzact History')
[7976]253    target = 'enterpin'
[7929]254
[8430]255class EtranzactEnterPinActionButtonStudent(APABStudent):
[8253]256    grok.context(ICustomStudentOnlinePayment)
[8430]257    grok.require('waeup.payStudent')
[8259]258    grok.order(3)
[8247]259    icon = 'actionicon_call.png'
260    text = _('Query eTranzact History')
261    target = 'enterpin'
262
263class EtranzactEnterPinPageStudent(KofaPage):
[7976]264    """
265    """
[8253]266    grok.context(ICustomStudentOnlinePayment)
[7976]267    grok.name('enterpin')
268    grok.template('enterpin')
[7929]269    grok.require('waeup.payStudent')
270
[7976]271    buttonname = _('Submit to eTranzact')
272    label = _('Requery eTranzact History')
273    action = 'query_history'
[11291]274    placeholder = _('Confirmation Number (PIN)')
[7929]275
[11627]276    def update(self):
277        super(EtranzactEnterPinPageStudent, self).update()
278        if self.context.p_category != 'schoolfee':
279            return
280        student = self.context.student
[11631]281        if self.context.student.state != CLEARED:
282            return
283        if self.context.student.entry_session < 2013:
284            return
[11627]285        for ticket in student['payments'].values():
286            if ticket.p_state == 'paid' and \
287                ticket.p_category == 'clearance':
288                return
289        self.flash(_('Please pay acceptance fee first.'), type="danger")
290        self.redirect(self.url(self.context, '@@index'))
291        return
292
[8247]293class EtranzactEnterPinPageApplicant(EtranzactEnterPinPageStudent):
294    """
295    """
296    grok.require('waeup.payApplicant')
[8253]297    grok.context(ICustomApplicantOnlinePayment)
[8247]298
299class EtranzactQueryHistoryPageStudent(UtilityView, grok.View):
[7929]300    """ Query history of eTranzact payments
301    """
[8253]302    grok.context(ICustomStudentOnlinePayment)
[7929]303    grok.name('query_history')
304    grok.require('waeup.payStudent')
305
306    def update(self, confirmation_number=None):
307        if self.context.p_state == 'paid':
308            self.flash(_('This ticket has already been paid.'))
309            return
[8763]310        student = self.context.student
[8430]311        success, msg, log = query_etranzact(confirmation_number,self.context)
[8764]312        student.writeLogMessage(self, log)
[8430]313        if not success:
314            self.flash(msg)
315            return
[11582]316        flashtype, msg, log = self.context.doAfterStudentPayment()
[8430]317        if log is not None:
[8764]318            student.writeLogMessage(self, log)
[11582]319        self.flash(msg, type=flashtype)
[8247]320        return
[7929]321
[8247]322    def render(self):
323        self.redirect(self.url(self.context, '@@index'))
324        return
[7929]325
[8247]326class EtranzactQueryHistoryPageApplicant(UtilityView, grok.View):
327    """ Query history of eTranzact payments
328    """
[8253]329    grok.context(ICustomApplicantOnlinePayment)
[8247]330    grok.name('query_history')
331    grok.require('waeup.payApplicant')
332
333    def update(self, confirmation_number=None):
[8430]334        ob_class = self.__implemented__.__name__
[8247]335        if self.context.p_state == 'paid':
336            self.flash(_('This ticket has already been paid.'))
[7929]337            return
[8247]338        applicant = self.context.__parent__
[8430]339        success, msg, log = query_etranzact(confirmation_number,self.context)
[8769]340        applicant.writeLogMessage(self, log)
[8430]341        if not success:
342            self.flash(msg)
343            return
[11582]344        flashtype, msg, log = self.context.doAfterApplicantPayment()
[8430]345        if log is not None:
[8769]346            applicant.writeLogMessage(self, log)
[11582]347        self.flash(msg, type=flashtype)
[7929]348        return
349
350    def render(self):
351        self.redirect(self.url(self.context, '@@index'))
[8259]352        return
[9904]353
354# Disable Interswitch viewlets. This could be avoided by defining the
355# action button viewlets of kofacustom.nigeria.interswitch.browser in the
356# context of INigeriaStudentOnlinePayment or INigeriaApplicantOnlinePayment
357# respectively. But then all interswitch.browser modules have to be extended.
358
[11868]359#class InterswitchActionButtonStudent(InterswitchActionButtonStudent):
[9904]360
[11868]361#    @property
362#    def target_url(self):
363#        return ''
[9904]364
[11868]365#class InterswitchRequestWebserviceActionButtonStudent(
366#    InterswitchRequestWebserviceActionButtonStudent):
[9904]367
[11868]368#    @property
369#    def target_url(self):
370#        return ''
[9904]371
[11846]372#class InterswitchActionButtonApplicant(InterswitchActionButtonApplicant):
[9904]373
[11846]374#    @property
375#    def target_url(self):
376#        return ''
[9904]377
[11846]378#class InterswitchRequestWebserviceActionButtonApplicant(
379#    InterswitchRequestWebserviceActionButtonApplicant):
[9904]380
[11846]381#    @property
382#    def target_url(self):
383#        return ''
Note: See TracBrowser for help on using the repository browser.