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

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

Ad Stuent ID card payments.

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