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

Last change on this file since 12117 was 11868, checked in by Henrik Bettermann, 10 years ago

Add Interswitch school fee components for testing.

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