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

Last change on this file since 15359 was 15346, checked in by Henrik Bettermann, 6 years ago

Disable webservice.

  • Property svn:keywords set to Id
File size: 18.6 KB
Line 
1## $Id: browser.py 15346 2019-03-06 22:19:56Z 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
76        # webservice disabled on 6/3/19
77        self.output = None
78        return
79
80        if PAYEE_ID == None:
81            self.output = ERROR_PART1 + 'Missing PAYEE_ID' + ERROR_PART2
82            return
83        real_ip = self.request.get('HTTP_X_FORWARDED_FOR', None)
84        # We can forego the logging once eTranzact payments run smoothly
85        # and the accepted IP addresses are used.
86        if real_ip:
87            self.context.logger.info('PaymentDataWebservice called: %s' % real_ip)
88        if real_ip  and self.ACCEPTED_IP:
89            if real_ip not in  self.ACCEPTED_IP:
90                self.output = ERROR_PART1 + 'Wrong IP address' + ERROR_PART2
91                return
92        category_mapping = {
93            'SCHOOL-FEE-NEW': ('schoolfee',),
94            'SCHOOL-FEE-RETURNING': ('schoolfee',),
95            'SCHOOL-FEE-PLUS-NEW': ('schoolfee_incl',),
96            'SCHOOL-FEE-PLUS-RETURNING': ('schoolfee_incl',),
97            'SCHOOL-FEE-PG-NEW': ('schoolfee',),
98            'SCHOOL-FEE-PG-RETURNING': ('schoolfee',),
99            'SCHOOL-FEE-FIRST-INSTALMENT-PLUS': ('schoolfee_1',),
100            'SCHOOL-FEE-SECOND-INSTALMENT': ('schoolfee_2',),
101            'SCHOOL-FEE-BALANCE': ('schoolfee','schoolfee_incl',
102                                   'schoolfee_1','schoolfee_2'),
103            'SCHOOL-FEE-PT-NEW': ('schoolfee',),
104            'SCHOOL-FEE-PT-RETURNING': ('schoolfee',),
105            'SCHOOL-FEE-PT-PLUS-NEW': ('schoolfee_incl',),
106            'SCHOOL-FEE-PT-PLUS-RETURNING': ('schoolfee_incl',),
107            'SCHOOL-FEE-PT-PG-NEW': ('schoolfee',),
108            'SCHOOL-FEE-PT-PG-RETURNING': ('schoolfee',),
109            'SCHOOL-FEE-PT-FIRST-INSTALMENT-PLUS': ('schoolfee_1',),
110            'SCHOOL-FEE-PT-SECOND-INSTALMENT': ('schoolfee_2',),
111            'SCHOOL-FEE-PT-BALANCE': ('schoolfee','schoolfee_incl',
112                                      'schoolfee_1','schoolfee_2'),
113            'SCHOOL-FEE-FP-NEW': ('schoolfee',),
114            'SCHOOL-FEE-IJMBE': ('schoolfee',),
115
116            'ACCEPTANCE-FEE': ('clearance',),
117            'ACCEPTANCE-FEE-PLUS': ('clearance_incl',),
118            'ACCEPTANCE-FEE-PG': ('clearance',),
119            'ACCEPTANCE-FEE-PT': ('clearance',),
120            'ACCEPTANCE-FEE-PT-PLUS': ('clearance_incl',),
121            'ACCEPTANCE-FEE-PT-PG': ('clearance',),
122            'ACCEPTANCE-FEE-FP': ('clearance',),
123            'ACCEPTANCE-FEE-IJMBE': ('clearance',),
124
125            'APPLICATION-FEE-UTME': ('application',),
126            'APPLICATION-FEE-PT': ('application',),
127            'APPLICATION-FEE-FP': ('application',),
128
129            'LATE-REGISTRATION': ('late_registration',),
130            'LATE-REGISTRATION-PT': ('late_registration',),
131
132            'AAU-STUDENT-WELFARE-ASSURANCE': ('welfare',),
133            'AAU-STUDENT-WELFARE-ASSURANCE-PT': ('welfare',),
134
135            'AAU-STUDENT-ID_CARD': ('id_card',),
136
137            'HOSTEL-ACCOMMODATION-FEE': ('hostel_maintenance',),
138            'HOSTEL-ACCOMMODATION-FEE-PT': ('hostel_maintenance',),
139
140            'AAU-LAPEL-FILE-FEE': ('lapel',),
141            'AAU-LAPEL-FILE-FEE-PT': ('lapel',),
142
143            'MATRICULATION-GOWN-FEE': ('matric_gown',),
144            'MATRICULATION-GOWN-FEE-PT': ('matric_gown',),
145
146            'CONCESSIONAL-FEE': ('concessional',),
147            'CONCESSIONAL-FEE-PT': ('concessional',),
148
149            'STUDENTS-UNION-DUES': ('union',),
150            'STUDENTS-UNION-DUES-PT': ('union',),
151
152            'RESTITUTION-FEE': ('restitution',),
153            }
154
155        if PAYMENT_TYPE not in category_mapping.keys():
156            self.output = ERROR_PART1 + 'Invalid PAYMENT_TYPE' + ERROR_PART2
157            return
158
159        # It seems eTranzact sends a POST request with an empty body but the URL
160        # contains a query string. So it's actually a GET request pretended
161        # to be a POST request. Although this does not comply with the
162        # RFC 2616 HTTP guidelines we may try to fetch the id from the QUERY_STRING
163        # value of the request.
164        #if PAYEE_ID is None:
165        #    try:
166        #        PAYEE_ID = self.request['QUERY_STRING'].split('=')[1]
167        #    except:
168        #        self.output = '-4'
169        #        return
170
171        cat = getUtility(ICatalog, name='payments_catalog')
172        results = list(cat.searchResults(p_id=(PAYEE_ID, PAYEE_ID)))
173        if len(results) != 1:
174            self.output = ERROR_PART1 + 'Invalid PAYEE_ID' + ERROR_PART2
175            return
176        student = getattr(results[0], 'student', None)
177        amount = results[0].amount_auth
178        payment_type = results[0].category
179        p_category = results[0].p_category
180        programme_type = results[0].p_item
181        if not programme_type:
182            programme_type = 'N/A'
183        academic_session = academic_sessions_vocab.getTerm(
184            results[0].p_session).title
185        status = results[0].p_state
186
187        if status == 'paid':
188            self.output = ERROR_PART1 + 'PAYEE_ID already used' + ERROR_PART2
189            return
190        if p_category not in category_mapping[PAYMENT_TYPE]:
191            self.output = ERROR_PART1 + 'Wrong PAYMENT_TYPE' + ERROR_PART2
192            return
193        if student and PAYMENT_TYPE.endswith('-RETURNING') \
194            and student.state == CLEARED:
195            self.output = ERROR_PART1 + 'Not a returning student' + ERROR_PART2
196            return
197        if student and PAYMENT_TYPE.endswith('-NEW') \
198            and student.state != CLEARED:
199            self.output = ERROR_PART1 + 'Not a new student' + ERROR_PART2
200            return
201        if student and '-PG' in PAYMENT_TYPE and not student.is_postgrad:
202            self.output = ERROR_PART1 + 'Not a postgrad student' + ERROR_PART2
203            return
204        if student and '-PG' not in PAYMENT_TYPE and student.is_postgrad \
205            and results[0].p_item != 'Balance':
206            self.output = ERROR_PART1 + 'Postgrad student' + ERROR_PART2
207            return
208        if student and '-PT' in PAYMENT_TYPE \
209            and not student.current_mode.endswith('_pt'):
210            self.output = ERROR_PART1 + 'Not a part-time student' + ERROR_PART2
211            return
212        if student and '-PT' not in PAYMENT_TYPE \
213            and student.current_mode.endswith('_pt'):
214            self.output = ERROR_PART1 + 'Part-time student' + ERROR_PART2
215            return
216        if student and '-FP' in PAYMENT_TYPE and student.current_mode != 'found':
217            self.output = ERROR_PART1 + 'Not a foundation programme student' + ERROR_PART2
218            return
219        if student and '-FP' not in PAYMENT_TYPE and student.current_mode == 'found':
220            self.output = ERROR_PART1 + 'Foundation programme student' + ERROR_PART2
221            return
222        if student and '-IJMBE' in PAYMENT_TYPE and student.current_mode != 'ijmbe':
223            self.output = ERROR_PART1 + 'Not IJMBE student' + ERROR_PART2
224            return
225        if student and '-IJMBE' not in PAYMENT_TYPE and student.current_mode == 'ijmbe':
226            self.output = ERROR_PART1 + 'IJMBE student student' + ERROR_PART2
227            return
228        if '-BALANCE' in PAYMENT_TYPE and results[0].p_item != 'Balance':
229            self.output = ERROR_PART1 + 'Not a balance payment' + ERROR_PART2
230            return
231        if not '-BALANCE' in PAYMENT_TYPE and results[0].p_item == 'Balance':
232            self.output = ERROR_PART1 + 'Balance payment' + ERROR_PART2
233            return
234
235        try:
236            owner = IPayer(results[0])
237            full_name = owner.display_fullname
238            matric_no = owner.id
239            faculty = owner.faculty
240            department = owner.department
241            study_type = owner.current_mode
242            email = owner.email
243            phone = owner.phone
244            level = owner.current_level
245        except (TypeError, AttributeError):
246            self.output = ERROR_PART1 +  'Unknown error' + ERROR_PART2
247            return
248        self.output = (
249            'PayeeName=%s~' +
250            'Faculty=%s~' +
251            'Department=%s~' +
252            'Level=%s~' +
253            'ProgrammeType=%s~' +
254            'StudyType=%s~' +
255            'Session=%s~' +
256            'PayeeID=%s~' +
257            'Amount=%s~' +
258            'FeeStatus=%s~' +
259            'Semester=N/A~' +
260            'PaymentType=%s~' +
261            'MatricNumber=%s~' +
262            'Email=%s~' +
263            'PhoneNumber=%s'
264
265            ) % (full_name, faculty,
266            department, level, programme_type, study_type,
267            academic_session, PAYEE_ID, amount, status, payment_type,
268            matric_no, email, phone)
269        return
270
271
272# Requerying eTranzact payments
273
274TERMINAL_ID = '0570000070'
275QUERY_URL =   'https://www.etranzact.net/WebConnectPlus/query.jsp'
276
277# Test environment
278#QUERY_URL =   'http://demo.etranzact.com:8080/WebConnect/queryPayoutletTransaction.jsp'
279#TERMINAL_ID = '5009892289'
280
281def query_etranzact(confirmation_number, payment):
282   
283    postdict = {}
284    postdict['TERMINAL_ID'] = TERMINAL_ID
285    #postdict['RESPONSE_URL'] = 'http://dummy'
286    postdict['CONFIRMATION_NO'] = confirmation_number
287    data = urllib.urlencode(postdict)
288    payment.conf_number = confirmation_number
289    try:
290        # eTranzact only accepts HTTP 1.1 requests. Therefore
291        # the urllib2 package is required here.
292        f = urllib2.urlopen(url=QUERY_URL, data=data)
293        success = f.read()
294        success = success.replace('\r\n','')
295        # eTranzact sends strange HTML tags which must be removed.
296        success = re.sub("<.*?>", "", success)
297        if 'CUSTOMER_ID' not in success:
298            msg = _('Invalid or unsuccessful callback: ${a}',
299                mapping = {'a': success})
300            log = 'invalid callback for payment %s: %s' % (payment.p_id, success)
301            payment.p_state = 'failed'
302            return False, msg, log
303        success = success.replace('%20',' ').split('&')
304        # We expect at least two parameters
305        if len(success) < 2:
306            msg = _('Invalid callback: ${a}', mapping = {'a': success})
307            log = 'invalid callback for payment %s: %s' % (payment.p_id, success)
308            payment.p_state = 'failed'
309            return False, msg, log
310        try:
311            success_dict = dict([tuple(i.split('=')) for i in success])
312        except ValueError:
313            msg = _('Invalid callback: ${a}', mapping = {'a': success})
314            log = 'invalid callback for payment %s: %s' % (payment.p_id, success)
315            payment.p_state = 'failed'
316            return False, msg, log
317    except IOError:
318        msg = _('eTranzact IOError')
319        log = 'eTranzact IOError'
320        return False, msg, log
321    payment.r_code = u'ET'
322    payment.r_company = u'etranzact'
323    payment.r_desc = u'%s' % success_dict.get('TRANS_DESCR')
324    payment.r_amount_approved = float(success_dict.get('TRANS_AMOUNT',0.0))
325    payment.r_card_num = None
326    payment.r_pay_reference = u'%s' % success_dict.get('RECEIPT_NO')
327    if payment.r_amount_approved != payment.amount_auth:
328        msg = _('Wrong amount')
329        log = 'wrong callback for payment %s: %s' % (payment.p_id, success)
330        payment.p_state = 'failed'
331        return False, msg, log
332    customer_id = success_dict.get('CUSTOMER_ID')
333    if payment.p_id != customer_id:
334        msg = _('Wrong payment id')
335        log = 'wrong callback for payment %s: %s' % (payment.p_id, success)
336        payment.p_state = 'failed'
337        return False, msg, log
338    log = 'valid callback for payment %s: %s' % (payment.p_id, success)
339    msg = _('Successful callback received')
340    payment.p_state = 'paid'
341    payment.payment_date = datetime.utcnow()
342    return True, msg, log
343
344class EtranzactEnterPinActionButtonApplicant(APABApplicant):
345    grok.context(ICustomApplicantOnlinePayment)
346    grok.require('waeup.payApplicant')
347    grok.order(3)
348    icon = 'actionicon_call.png'
349    text = _('Query eTranzact History')
350    target = 'enterpin'
351
352class EtranzactEnterPinActionButtonStudent(APABStudent):
353    grok.context(ICustomStudentOnlinePayment)
354    grok.require('waeup.payStudent')
355    grok.order(3)
356    icon = 'actionicon_call.png'
357    text = _('Query eTranzact History')
358    target = 'enterpin'
359
360class EtranzactEnterPinPageStudent(KofaPage):
361    """
362    """
363    grok.context(ICustomStudentOnlinePayment)
364    grok.name('enterpin')
365    grok.template('enterpin')
366    grok.require('waeup.payStudent')
367
368    buttonname = _('Submit to eTranzact')
369    label = _('Requery eTranzact History')
370    action = 'query_history'
371    placeholder = _('Confirmation Number (PIN)')
372
373    def update(self):
374        super(EtranzactEnterPinPageStudent, self).update()
375        if not self.context.p_category.startswith('schoolfee'):
376            return
377        student = self.context.student
378        if student.state != CLEARED:
379            return
380        if student.entry_session < 2013:
381            return
382        for ticket in student['payments'].values():
383            if ticket.p_state == 'paid' and \
384                ticket.p_category.startswith('clearance'):
385                return
386        self.flash(_('Please pay acceptance fee first.'), type="danger")
387        self.redirect(self.url(self.context, '@@index'))
388        return
389
390class EtranzactEnterPinPageApplicant(EtranzactEnterPinPageStudent):
391    """
392    """
393    grok.require('waeup.payApplicant')
394    grok.context(ICustomApplicantOnlinePayment)
395
396class EtranzactQueryHistoryPageStudent(UtilityView, grok.View):
397    """ Query history of eTranzact payments
398    """
399    grok.context(ICustomStudentOnlinePayment)
400    grok.name('query_history')
401    grok.require('waeup.payStudent')
402
403    def update(self, confirmation_number=None):
404        if self.context.p_state == 'paid':
405            self.flash(_('This ticket has already been paid.'))
406            return
407        student = self.context.student
408        success, msg, log = query_etranzact(confirmation_number,self.context)
409        student.writeLogMessage(self, log)
410        if not success:
411            self.flash(msg)
412            return
413        flashtype, msg, log = self.context.doAfterStudentPayment()
414        if log is not None:
415            student.writeLogMessage(self, log)
416        self.flash(msg, type=flashtype)
417        return
418
419    def render(self):
420        self.redirect(self.url(self.context, '@@index'))
421        return
422
423class EtranzactQueryHistoryPageApplicant(UtilityView, grok.View):
424    """ Query history of eTranzact payments
425    """
426    grok.context(ICustomApplicantOnlinePayment)
427    grok.name('query_history')
428    grok.require('waeup.payApplicant')
429
430    def update(self, confirmation_number=None):
431        ob_class = self.__implemented__.__name__
432        if self.context.p_state == 'paid':
433            self.flash(_('This ticket has already been paid.'))
434            return
435        applicant = self.context.__parent__
436        success, msg, log = query_etranzact(confirmation_number,self.context)
437        applicant.writeLogMessage(self, log)
438        if not success:
439            self.flash(msg)
440            return
441        flashtype, msg, log = self.context.doAfterApplicantPayment()
442        if log is not None:
443            applicant.writeLogMessage(self, log)
444        self.flash(msg, type=flashtype)
445        return
446
447    def render(self):
448        self.redirect(self.url(self.context, '@@index'))
449        return
450
451# Disable Interswitch viewlets. This could be avoided by defining the
452# action button viewlets of kofacustom.nigeria.interswitch.browser in the
453# context of INigeriaStudentOnlinePayment or INigeriaApplicantOnlinePayment
454# respectively. But then all interswitch.browser modules have to be extended.
455
456#class InterswitchActionButtonStudent(InterswitchActionButtonStudent):
457
458#    @property
459#    def target_url(self):
460#        return ''
461
462#class InterswitchRequestWebserviceActionButtonStudent(
463#    InterswitchRequestWebserviceActionButtonStudent):
464
465#    @property
466#    def target_url(self):
467#        return ''
468
469#class InterswitchActionButtonApplicant(InterswitchActionButtonApplicant):
470
471#    @property
472#    def target_url(self):
473#        return ''
474
475#class InterswitchRequestWebserviceActionButtonApplicant(
476#    InterswitchRequestWebserviceActionButtonApplicant):
477
478#    @property
479#    def target_url(self):
480#        return ''
Note: See TracBrowser for help on using the repository browser.