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

Last change on this file since 13808 was 13799, checked in by Henrik Bettermann, 9 years ago

Catch pg balance payments.

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