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

Last change on this file since 12774 was 12504, checked in by Henrik Bettermann, 10 years ago

Activate Late Registration Fee payments.

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