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

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

Revert changes from last transition, see ticket 141.

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