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

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

Extend category_mapping. Distinguish part-time and full-time students.

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