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

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

Configure Interswitch split payment and eTrabzact payment type.

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