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

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

All fees must must be distinguished whether the student is part-time or not.

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