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

Last change on this file since 13780 was 13731, checked in by Henrik Bettermann, 9 years ago

Email from eTranzact: Please note that all the output string delimited by "~"​ must have a value when passed or value of NA if no record can be spooled. for example this record below highlighted does not have any value.

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