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

Last change on this file since 11645 was 11631, checked in by Henrik Bettermann, 11 years ago

This requirement applies to students in state 'cleared' and entry_session
greater than 2013 only.

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