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

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

Improve and reorganize webservice.

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