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

Last change on this file since 14220 was 14087, checked in by Henrik Bettermann, 8 years ago

Add payment types for foundation programme fees.

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