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

Last change on this file since 13629 was 13490, checked in by Henrik Bettermann, 9 years ago

Remove HTML tags from eTranzact responses.

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