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

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

Ticket #495: " I am thinking if you change the fee type (PAYMENT_TYPE=APPLICATION-FEE) to "PAYMENT_TYPE=APPLICATION-FEE-UTME". The the new name, etranzact will not be able to call our service."

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