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

Last change on this file since 14569 was 14520, checked in by Henrik Bettermann, 8 years ago

Add SCHOOL-FEE-IJMBE and ACCEPTANCE-FEE-IJMBE.

  • Property svn:keywords set to Id
File size: 18.5 KB
RevLine 
[7929]1## $Id: browser.py 14520 2017-02-08 10:16:49Z 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
[13784]87        category_mapping = {
88            'SCHOOL-FEE-NEW': ('schoolfee',),
89            'SCHOOL-FEE-RETURNING': ('schoolfee',),
90            'SCHOOL-FEE-PLUS-NEW': ('schoolfee_incl',),
91            'SCHOOL-FEE-PLUS-RETURNING': ('schoolfee_incl',),
92            'SCHOOL-FEE-PG-NEW': ('schoolfee',),
93            'SCHOOL-FEE-PG-RETURNING': ('schoolfee',),
94            'SCHOOL-FEE-FIRST-INSTALMENT-PLUS': ('schoolfee_1',),
95            'SCHOOL-FEE-SECOND-INSTALMENT': ('schoolfee_2',),
96            'SCHOOL-FEE-BALANCE': ('schoolfee','schoolfee_incl',
97                                   'schoolfee_1','schoolfee_2'),
[14083]98            'SCHOOL-FEE-PT-NEW': ('schoolfee',),
99            'SCHOOL-FEE-PT-RETURNING': ('schoolfee',),
100            'SCHOOL-FEE-PT-PLUS-NEW': ('schoolfee_incl',),
101            'SCHOOL-FEE-PT-PLUS-RETURNING': ('schoolfee_incl',),
102            'SCHOOL-FEE-PT-PG-NEW': ('schoolfee',),
103            'SCHOOL-FEE-PT-PG-RETURNING': ('schoolfee',),
104            'SCHOOL-FEE-PT-FIRST-INSTALMENT-PLUS': ('schoolfee_1',),
105            'SCHOOL-FEE-PT-SECOND-INSTALMENT': ('schoolfee_2',),
106            'SCHOOL-FEE-PT-BALANCE': ('schoolfee','schoolfee_incl',
107                                      'schoolfee_1','schoolfee_2'),
[14087]108            'SCHOOL-FEE-FP-NEW': ('schoolfee',),
[14520]109            'SCHOOL-FEE-IJMBE': ('schoolfee',),
[14083]110
[13784]111            'ACCEPTANCE-FEE': ('clearance',),
112            'ACCEPTANCE-FEE-PLUS': ('clearance_incl',),
113            'ACCEPTANCE-FEE-PG': ('clearance',),
[14085]114            'ACCEPTANCE-FEE-PT': ('clearance',),
115            'ACCEPTANCE-FEE-PT-PLUS': ('clearance_incl',),
116            'ACCEPTANCE-FEE-PT-PG': ('clearance',),
[14087]117            'ACCEPTANCE-FEE-FP': ('clearance',),
[14520]118            'ACCEPTANCE-FEE-IJMBE': ('clearance',),
[14085]119
[13784]120            'APPLICATION-FEE': ('application',),
[14085]121            'APPLICATION-FEE-PT': ('application',),
[14087]122            'APPLICATION-FEE-FP': ('application',),
[14085]123
[13784]124            'LATE-REGISTRATION': ('late_registration',),
[14085]125            'LATE-REGISTRATION-PT': ('late_registration',),
126
[13784]127            'AAU-STUDENT-WELFARE-ASSURANCE': ('welfare',),
[14085]128            'AAU-STUDENT-WELFARE-ASSURANCE-PT': ('welfare',),
129
[14234]130            'AAU-STUDENT-ID_CARD': ('id_card',),
131
[13784]132            'HOSTEL-ACCOMMODATION-FEE': ('hostel_maintenance',),
[14085]133            'HOSTEL-ACCOMMODATION-FEE-PT': ('hostel_maintenance',),
134
[13784]135            'AAU-LAPEL-FILE-FEE': ('lapel',),
[14085]136            'AAU-LAPEL-FILE-FEE-PT': ('lapel',),
137
[13784]138            'MATRICULATION-GOWN-FEE': ('matric_gown',),
[14085]139            'MATRICULATION-GOWN-FEE-PT': ('matric_gown',),
140
[13784]141            'CONCESSIONAL-FEE': ('concessional',),
[14085]142            'CONCESSIONAL-FEE-PT': ('concessional',),
143
[13784]144            'STUDENTS-UNION-DUES': ('union',),
[14085]145            'STUDENTS-UNION-DUES-PT': ('union',),
[14376]146
147            'RESTITUTION-FEE': ('restitution',),
[13784]148            }
149
150        if PAYMENT_TYPE not in category_mapping.keys():
[10978]151            self.output = ERROR_PART1 + 'Invalid PAYMENT_TYPE' + ERROR_PART2
[10937]152            return
[8754]153
154        # It seems eTranzact sends a POST request with an empty body but the URL
155        # contains a query string. So it's actually a GET request pretended
156        # to be a POST request. Although this does not comply with the
157        # RFC 2616 HTTP guidelines we may try to fetch the id from the QUERY_STRING
158        # value of the request.
159        #if PAYEE_ID is None:
160        #    try:
161        #        PAYEE_ID = self.request['QUERY_STRING'].split('=')[1]
162        #    except:
[10937]163        #        self.output = '-4'
[8754]164        #        return
165
[8704]166        cat = getUtility(ICatalog, name='payments_catalog')
167        results = list(cat.searchResults(p_id=(PAYEE_ID, PAYEE_ID)))
168        if len(results) != 1:
[10978]169            self.output = ERROR_PART1 + 'Invalid PAYEE_ID' + ERROR_PART2
[8746]170            return
[13783]171        student = getattr(results[0], 'student', None)
[11554]172        amount = results[0].amount_auth
173        payment_type = results[0].category
[13784]174        p_category = results[0].p_category
[11554]175        programme_type = results[0].p_item
[13731]176        if not programme_type:
177            programme_type = 'N/A'
[11554]178        academic_session = academic_sessions_vocab.getTerm(
179            results[0].p_session).title
180        status = results[0].p_state
[13784]181
[11554]182        if status == 'paid':
183            self.output = ERROR_PART1 + 'PAYEE_ID already used' + ERROR_PART2
184            return
[13784]185        if p_category not in category_mapping[PAYMENT_TYPE]:
[11651]186            self.output = ERROR_PART1 + 'Wrong PAYMENT_TYPE' + ERROR_PART2
187            return
[13783]188        if student and PAYMENT_TYPE.endswith('-RETURNING') \
189            and student.state == CLEARED:
[13784]190            self.output = ERROR_PART1 + 'Not a returning student' + ERROR_PART2
[13783]191            return
192        if student and PAYMENT_TYPE.endswith('-NEW') \
193            and student.state != CLEARED:
[13784]194            self.output = ERROR_PART1 + 'Not a new student' + ERROR_PART2
[13783]195            return
196        if student and '-PG' in PAYMENT_TYPE and not student.is_postgrad:
[13784]197            self.output = ERROR_PART1 + 'Not a postgrad student' + ERROR_PART2
[13783]198            return
[13799]199        if student and '-PG' not in PAYMENT_TYPE and student.is_postgrad \
200            and results[0].p_item != 'Balance':
[13784]201            self.output = ERROR_PART1 + 'Postgrad student' + ERROR_PART2
202            return
[14083]203        if student and '-PT' in PAYMENT_TYPE \
204            and not student.current_mode.endswith('_pt'):
205            self.output = ERROR_PART1 + 'Not a part-time student' + ERROR_PART2
206            return
207        if student and '-PT' not in PAYMENT_TYPE \
208            and student.current_mode.endswith('_pt'):
209            self.output = ERROR_PART1 + 'Part-time student' + ERROR_PART2
210            return
[14087]211        if student and '-FP' in PAYMENT_TYPE and student.current_mode != 'found':
212            self.output = ERROR_PART1 + 'Not a foundation programme student' + ERROR_PART2
213            return
214        if student and '-FP' not in PAYMENT_TYPE and student.current_mode == 'found':
215            self.output = ERROR_PART1 + 'Foundation programme student' + ERROR_PART2
216            return
[14520]217        if student and '-IJMBE' in PAYMENT_TYPE and student.current_mode != 'ijmbe':
218            self.output = ERROR_PART1 + 'Not IJMBE student' + ERROR_PART2
219            return
220        if student and '-IJMBE' not in PAYMENT_TYPE and student.current_mode == 'ijmbe':
221            self.output = ERROR_PART1 + 'IJMBE student student' + ERROR_PART2
222            return
[13783]223        if '-BALANCE' in PAYMENT_TYPE and results[0].p_item != 'Balance':
[13784]224            self.output = ERROR_PART1 + 'Not a balance payment' + ERROR_PART2
[13783]225            return
[13784]226        if not '-BALANCE' in PAYMENT_TYPE and results[0].p_item == 'Balance':
227            self.output = ERROR_PART1 + 'Balance payment' + ERROR_PART2
[13783]228            return
[13784]229
[8746]230        try:
[10032]231            owner = IPayer(results[0])
[8746]232            full_name = owner.display_fullname
233            matric_no = owner.id
234            faculty = owner.faculty
235            department = owner.department
[10907]236            study_type = owner.current_mode
237            email = owner.email
238            phone = owner.phone
239            level = owner.current_level
[8746]240        except (TypeError, AttributeError):
[10978]241            self.output = ERROR_PART1 +  'Unknown error' + ERROR_PART2
[8746]242            return
243        self.output = (
[10907]244            'PayeeName=%s~' +
245            'Faculty=%s~' +
246            'Department=%s~' +
247            'Level=%s~' +
248            'ProgrammeType=%s~' +
249            'StudyType=%s~' +
250            'Session=%s~' +
251            'PayeeID=%s~' +
252            'Amount=%s~' +
253            'FeeStatus=%s~' +
[10932]254            'Semester=N/A~' +
[10907]255            'PaymentType=%s~' +
256            'MatricNumber=%s~' +
257            'Email=%s~' +
258            'PhoneNumber=%s'
259
260            ) % (full_name, faculty,
261            department, level, programme_type, study_type,
262            academic_session, PAYEE_ID, amount, status, payment_type,
263            matric_no, email, phone)
[8698]264        return
265
266
267# Requerying eTranzact payments
268
[10983]269TERMINAL_ID = '0570000070'
270QUERY_URL =   'https://www.etranzact.net/WebConnectPlus/query.jsp'
[8259]271
[8680]272# Test environment
[8776]273#QUERY_URL =   'http://demo.etranzact.com:8080/WebConnect/queryPayoutletTransaction.jsp'
274#TERMINAL_ID = '5009892289'
[8680]275
[8430]276def query_etranzact(confirmation_number, payment):
277   
[8247]278    postdict = {}
279    postdict['TERMINAL_ID'] = TERMINAL_ID
280    #postdict['RESPONSE_URL'] = 'http://dummy'
281    postdict['CONFIRMATION_NO'] = confirmation_number
[10070]282    data = urllib.urlencode(postdict)
[8682]283    payment.conf_number = confirmation_number
[8247]284    try:
[10069]285        # eTranzact only accepts HTTP 1.1 requests. Therefore
286        # the urllib2 package is required here.
287        f = urllib2.urlopen(url=QUERY_URL, data=data)
[8247]288        success = f.read()
[8432]289        success = success.replace('\r\n','')
[13490]290        # eTranzact sends strange HTML tags which must be removed.
291        success = re.sub("<.*?>", "", success)
[9935]292        if 'CUSTOMER_ID' not in success:
[8430]293            msg = _('Invalid or unsuccessful callback: ${a}',
294                mapping = {'a': success})
295            log = 'invalid callback for payment %s: %s' % (payment.p_id, success)
[8247]296            payment.p_state = 'failed'
[8430]297            return False, msg, log
[8247]298        success = success.replace('%20',' ').split('&')
299        # We expect at least two parameters
300        if len(success) < 2:
[8430]301            msg = _('Invalid callback: ${a}', mapping = {'a': success})
302            log = 'invalid callback for payment %s: %s' % (payment.p_id, success)
[8247]303            payment.p_state = 'failed'
[8430]304            return False, msg, log
[8247]305        try:
306            success_dict = dict([tuple(i.split('=')) for i in success])
307        except ValueError:
[8430]308            msg = _('Invalid callback: ${a}', mapping = {'a': success})
309            log = 'invalid callback for payment %s: %s' % (payment.p_id, success)
[8247]310            payment.p_state = 'failed'
[8430]311            return False, msg, log
[8247]312    except IOError:
[8430]313        msg = _('eTranzact IOError')
314        log = 'eTranzact IOError'
315        return False, msg, log
[8247]316    payment.r_code = u'ET'
[9327]317    payment.r_company = u'etranzact'
[8247]318    payment.r_desc = u'%s' % success_dict.get('TRANS_DESCR')
319    payment.r_amount_approved = float(success_dict.get('TRANS_AMOUNT',0.0))
320    payment.r_card_num = None
321    payment.r_pay_reference = u'%s' % success_dict.get('RECEIPT_NO')
322    if payment.r_amount_approved != payment.amount_auth:
[8430]323        msg = _('Wrong amount')
324        log = 'wrong callback for payment %s: %s' % (payment.p_id, success)
[8247]325        payment.p_state = 'failed'
[8430]326        return False, msg, log
[9935]327    customer_id = success_dict.get('CUSTOMER_ID')
328    if payment.p_id != customer_id:
[8717]329        msg = _('Wrong payment id')
[8430]330        log = 'wrong callback for payment %s: %s' % (payment.p_id, success)
[8247]331        payment.p_state = 'failed'
[8430]332        return False, msg, log
333    log = 'valid callback for payment %s: %s' % (payment.p_id, success)
334    msg = _('Successful callback received')
[8247]335    payment.p_state = 'paid'
[8433]336    payment.payment_date = datetime.utcnow()
[8430]337    return True, msg, log
[8247]338
[8430]339class EtranzactEnterPinActionButtonApplicant(APABApplicant):
[8253]340    grok.context(ICustomApplicantOnlinePayment)
[8430]341    grok.require('waeup.payApplicant')
[8259]342    grok.order(3)
[7929]343    icon = 'actionicon_call.png'
344    text = _('Query eTranzact History')
[7976]345    target = 'enterpin'
[7929]346
[8430]347class EtranzactEnterPinActionButtonStudent(APABStudent):
[8253]348    grok.context(ICustomStudentOnlinePayment)
[8430]349    grok.require('waeup.payStudent')
[8259]350    grok.order(3)
[8247]351    icon = 'actionicon_call.png'
352    text = _('Query eTranzact History')
353    target = 'enterpin'
354
355class EtranzactEnterPinPageStudent(KofaPage):
[7976]356    """
357    """
[8253]358    grok.context(ICustomStudentOnlinePayment)
[7976]359    grok.name('enterpin')
360    grok.template('enterpin')
[7929]361    grok.require('waeup.payStudent')
362
[7976]363    buttonname = _('Submit to eTranzact')
364    label = _('Requery eTranzact History')
365    action = 'query_history'
[11291]366    placeholder = _('Confirmation Number (PIN)')
[7929]367
[11627]368    def update(self):
369        super(EtranzactEnterPinPageStudent, self).update()
[13420]370        if not self.context.p_category.startswith('schoolfee'):
[11627]371            return
372        student = self.context.student
[12975]373        if student.state != CLEARED:
[11631]374            return
[12975]375        if student.entry_session < 2013:
[11631]376            return
[11627]377        for ticket in student['payments'].values():
378            if ticket.p_state == 'paid' and \
[13420]379                ticket.p_category.startswith('clearance'):
[11627]380                return
381        self.flash(_('Please pay acceptance fee first.'), type="danger")
382        self.redirect(self.url(self.context, '@@index'))
383        return
384
[8247]385class EtranzactEnterPinPageApplicant(EtranzactEnterPinPageStudent):
386    """
387    """
388    grok.require('waeup.payApplicant')
[8253]389    grok.context(ICustomApplicantOnlinePayment)
[8247]390
391class EtranzactQueryHistoryPageStudent(UtilityView, grok.View):
[7929]392    """ Query history of eTranzact payments
393    """
[8253]394    grok.context(ICustomStudentOnlinePayment)
[7929]395    grok.name('query_history')
396    grok.require('waeup.payStudent')
397
398    def update(self, confirmation_number=None):
399        if self.context.p_state == 'paid':
400            self.flash(_('This ticket has already been paid.'))
401            return
[8763]402        student = self.context.student
[8430]403        success, msg, log = query_etranzact(confirmation_number,self.context)
[8764]404        student.writeLogMessage(self, log)
[8430]405        if not success:
406            self.flash(msg)
407            return
[11582]408        flashtype, msg, log = self.context.doAfterStudentPayment()
[8430]409        if log is not None:
[8764]410            student.writeLogMessage(self, log)
[11582]411        self.flash(msg, type=flashtype)
[8247]412        return
[7929]413
[8247]414    def render(self):
415        self.redirect(self.url(self.context, '@@index'))
416        return
[7929]417
[8247]418class EtranzactQueryHistoryPageApplicant(UtilityView, grok.View):
419    """ Query history of eTranzact payments
420    """
[8253]421    grok.context(ICustomApplicantOnlinePayment)
[8247]422    grok.name('query_history')
423    grok.require('waeup.payApplicant')
424
425    def update(self, confirmation_number=None):
[8430]426        ob_class = self.__implemented__.__name__
[8247]427        if self.context.p_state == 'paid':
428            self.flash(_('This ticket has already been paid.'))
[7929]429            return
[8247]430        applicant = self.context.__parent__
[8430]431        success, msg, log = query_etranzact(confirmation_number,self.context)
[8769]432        applicant.writeLogMessage(self, log)
[8430]433        if not success:
434            self.flash(msg)
435            return
[11582]436        flashtype, msg, log = self.context.doAfterApplicantPayment()
[8430]437        if log is not None:
[8769]438            applicant.writeLogMessage(self, log)
[11582]439        self.flash(msg, type=flashtype)
[7929]440        return
441
442    def render(self):
443        self.redirect(self.url(self.context, '@@index'))
[8259]444        return
[9904]445
446# Disable Interswitch viewlets. This could be avoided by defining the
447# action button viewlets of kofacustom.nigeria.interswitch.browser in the
448# context of INigeriaStudentOnlinePayment or INigeriaApplicantOnlinePayment
449# respectively. But then all interswitch.browser modules have to be extended.
450
[11868]451#class InterswitchActionButtonStudent(InterswitchActionButtonStudent):
[9904]452
[11868]453#    @property
454#    def target_url(self):
455#        return ''
[9904]456
[11868]457#class InterswitchRequestWebserviceActionButtonStudent(
458#    InterswitchRequestWebserviceActionButtonStudent):
[9904]459
[11868]460#    @property
461#    def target_url(self):
462#        return ''
[9904]463
[11846]464#class InterswitchActionButtonApplicant(InterswitchActionButtonApplicant):
[9904]465
[11846]466#    @property
467#    def target_url(self):
468#        return ''
[9904]469
[11846]470#class InterswitchRequestWebserviceActionButtonApplicant(
471#    InterswitchRequestWebserviceActionButtonApplicant):
[9904]472
[11846]473#    @property
474#    def target_url(self):
475#        return ''
Note: See TracBrowser for help on using the repository browser.