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

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

Configure new PAYMENT_TYPEs.

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