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

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

Add payment types for foundation programme fees.

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