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

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

Remove HTML tags from eTranzact responses.

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