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

Last change on this file since 15477 was 15346, checked in by Henrik Bettermann, 6 years ago

Disable webservice.

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