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

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

Email from eTranzact: Please note that all the output string delimited by "~"​ must have a value when passed or value of NA if no record can be spooled. for example this record below highlighted does not have any value.

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