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

Last change on this file since 10974 was 10954, checked in by Henrik Bettermann, 11 years ago

Return cleartext error messages.

  • Property svn:keywords set to Id
File size: 12.7 KB
Line 
1## $Id: browser.py 10954 2014-01-21 15:17:29Z 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
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
42class CustomPaymentDataWebservice(PaymentDataWebservice):
43    """A simple webservice to publish payment and payer details on request from
44    accepted IP addresses without authentication.
45
46    Etranzact is asking for the PAYEE_ID which is indeed misleading.
47    These are not the data of the payee but of the payer. And it's
48    not the id of the payer but of the payment.
49    """
50    grok.name('feerequest')
51
52    #ACCEPTED_IP = ('195.219.3.181', '195.219.3.184')
53    ACCEPTED_IP = None
54
55    def update(self, PAYEE_ID=None, PAYMENT_TYPE=None):
56        if PAYEE_ID == None:
57            self.output = 'Missing PAYEE_ID' # '-1'
58            return
59        real_ip = self.request.get('HTTP_X_FORWARDED_FOR', None)
60        # We can forego the logging once eTranzact payments run smoothly
61        # and the accepted IP addresses are used.
62        if real_ip:
63            self.context.logger.info('PaymentDataWebservice called: %s' % real_ip)
64        if real_ip  and self.ACCEPTED_IP:
65            if real_ip not in  self.ACCEPTED_IP:
66                self.output = 'Wrong IP address' # '-2'
67                return
68        if PAYMENT_TYPE not in ('SCHOOL-FEE', 'ACCEPTANCE-FEE', 'APPLICATION-FEE'):
69            self.output = 'Invalid PAYMENT_TYPE' # '-3'
70            return
71
72        # It seems eTranzact sends a POST request with an empty body but the URL
73        # contains a query string. So it's actually a GET request pretended
74        # to be a POST request. Although this does not comply with the
75        # RFC 2616 HTTP guidelines we may try to fetch the id from the QUERY_STRING
76        # value of the request.
77        #if PAYEE_ID is None:
78        #    try:
79        #        PAYEE_ID = self.request['QUERY_STRING'].split('=')[1]
80        #    except:
81        #        self.output = '-4'
82        #        return
83
84        cat = getUtility(ICatalog, name='payments_catalog')
85        results = list(cat.searchResults(p_id=(PAYEE_ID, PAYEE_ID)))
86        if len(results) != 1:
87            self.output = 'Invalid PAYEE_ID' # '-1'
88            return
89        if PAYMENT_TYPE == 'SCHOOL-FEE' \
90            and not results[0].p_category.startswith('schoolfee'):
91            self.output = 'Wrong PAYMENT_TYPE' # '-5'
92            return
93        if PAYMENT_TYPE == 'ACCEPTANCE-FEE' \
94            and not results[0].p_category == 'clearance':
95            self.output = 'Wrong PAYMENT_TYPE' # '-6'
96            return
97        if PAYMENT_TYPE == 'APPLICATION-FEE' \
98            and not results[0].p_category == 'application':
99            self.output = 'Wrong PAYMENT_TYPE' # '-7'
100            return
101        try:
102            owner = IPayer(results[0])
103            full_name = owner.display_fullname
104            matric_no = owner.id
105            faculty = owner.faculty
106            department = owner.department
107            study_type = owner.current_mode
108            email = owner.email
109            phone = owner.phone
110            level = owner.current_level
111        except (TypeError, AttributeError):
112            self.output = 'Unknown error, please contact Kofa administrator' # '-8'
113            return
114        amount = results[0].amount_auth
115        payment_type = results[0].category
116        programme_type = results[0].p_item
117
118        academic_session = academic_sessions_vocab.getTerm(
119            results[0].p_session).title
120        status = results[0].p_state
121        self.output = (
122            # Version 1
123            #'FULL_NAME=%s&' +
124            #'FACULTY=%s&' +
125            #'DEPARTMENT=%s&' +
126            #'RETURN_TYPE=%s&' +
127            #'PROGRAMME_TYPE=%s&' +
128            #'PAYMENT_TYPE=%s&' +
129            #'ACADEMIC_SESSION=%s&' +
130            #'MATRIC_NO=%s&' +
131            #'FEE_AMOUNT=%s&' +
132            #'TRANSACTION_STATUS=%s'
133
134            # Version 2
135            'PayeeName=%s~' +
136            'Faculty=%s~' +
137            'Department=%s~' +
138            'Level=%s~' +
139            'ProgrammeType=%s~' +
140            'StudyType=%s~' +
141            'Session=%s~' +
142            'PayeeID=%s~' +
143            'Amount=%s~' +
144            'FeeStatus=%s~' +
145            'Semester=N/A~' +
146            'PaymentType=%s~' +
147            'MatricNumber=%s~' +
148            'Email=%s~' +
149            'PhoneNumber=%s'
150
151            ) % (full_name, faculty,
152            department, level, programme_type, study_type,
153            academic_session, PAYEE_ID, amount, status, payment_type,
154            matric_no, email, phone)
155        return
156
157
158# Requerying eTranzact payments
159
160TERMINAL_ID = '0330000046'
161QUERY_URL =   'https://www.etranzact.net/Query/queryPayoutletTransaction.jsp'
162
163# Test environment
164#QUERY_URL =   'http://demo.etranzact.com:8080/WebConnect/queryPayoutletTransaction.jsp'
165#TERMINAL_ID = '5009892289'
166
167def query_etranzact(confirmation_number, payment):
168   
169    postdict = {}
170    postdict['TERMINAL_ID'] = TERMINAL_ID
171    #postdict['RESPONSE_URL'] = 'http://dummy'
172    postdict['CONFIRMATION_NO'] = confirmation_number
173    data = urllib.urlencode(postdict)
174    payment.conf_number = confirmation_number
175    try:
176        # eTranzact only accepts HTTP 1.1 requests. Therefore
177        # the urllib2 package is required here.
178        f = urllib2.urlopen(url=QUERY_URL, data=data)
179        success = f.read()
180        success = success.replace('\r\n','')
181        if 'CUSTOMER_ID' not in success:
182            msg = _('Invalid or unsuccessful callback: ${a}',
183                mapping = {'a': success})
184            log = 'invalid callback for payment %s: %s' % (payment.p_id, success)
185            payment.p_state = 'failed'
186            return False, msg, log
187        success = success.replace('%20',' ').split('&')
188        # We expect at least two parameters
189        if len(success) < 2:
190            msg = _('Invalid callback: ${a}', mapping = {'a': success})
191            log = 'invalid callback for payment %s: %s' % (payment.p_id, success)
192            payment.p_state = 'failed'
193            return False, msg, log
194        try:
195            success_dict = dict([tuple(i.split('=')) for i in success])
196        except ValueError:
197            msg = _('Invalid callback: ${a}', mapping = {'a': success})
198            log = 'invalid callback for payment %s: %s' % (payment.p_id, success)
199            payment.p_state = 'failed'
200            return False, msg, log
201    except IOError:
202        msg = _('eTranzact IOError')
203        log = 'eTranzact IOError'
204        return False, msg, log
205    payment.r_code = u'ET'
206    payment.r_company = u'etranzact'
207    payment.r_desc = u'%s' % success_dict.get('TRANS_DESCR')
208    payment.r_amount_approved = float(success_dict.get('TRANS_AMOUNT',0.0))
209    payment.r_card_num = None
210    payment.r_pay_reference = u'%s' % success_dict.get('RECEIPT_NO')
211    if payment.r_amount_approved != payment.amount_auth:
212        msg = _('Wrong amount')
213        log = 'wrong callback for payment %s: %s' % (payment.p_id, success)
214        payment.p_state = 'failed'
215        return False, msg, log
216    customer_id = success_dict.get('CUSTOMER_ID')
217    if payment.p_id != customer_id:
218        msg = _('Wrong payment id')
219        log = 'wrong callback for payment %s: %s' % (payment.p_id, success)
220        payment.p_state = 'failed'
221        return False, msg, log
222    log = 'valid callback for payment %s: %s' % (payment.p_id, success)
223    msg = _('Successful callback received')
224    payment.p_state = 'paid'
225    payment.payment_date = datetime.utcnow()
226    return True, msg, log
227
228class EtranzactEnterPinActionButtonApplicant(APABApplicant):
229    grok.context(ICustomApplicantOnlinePayment)
230    grok.require('waeup.payApplicant')
231    grok.order(3)
232    icon = 'actionicon_call.png'
233    text = _('Query eTranzact History')
234    target = 'enterpin'
235
236class EtranzactEnterPinActionButtonStudent(APABStudent):
237    grok.context(ICustomStudentOnlinePayment)
238    grok.require('waeup.payStudent')
239    grok.order(3)
240    icon = 'actionicon_call.png'
241    text = _('Query eTranzact History')
242    target = 'enterpin'
243
244class EtranzactEnterPinPageStudent(KofaPage):
245    """
246    """
247    grok.context(ICustomStudentOnlinePayment)
248    grok.name('enterpin')
249    grok.template('enterpin')
250    grok.require('waeup.payStudent')
251
252    buttonname = _('Submit to eTranzact')
253    label = _('Requery eTranzact History')
254    action = 'query_history'
255
256class EtranzactEnterPinPageApplicant(EtranzactEnterPinPageStudent):
257    """
258    """
259    grok.require('waeup.payApplicant')
260    grok.context(ICustomApplicantOnlinePayment)
261
262class EtranzactQueryHistoryPageStudent(UtilityView, grok.View):
263    """ Query history of eTranzact payments
264    """
265    grok.context(ICustomStudentOnlinePayment)
266    grok.name('query_history')
267    grok.require('waeup.payStudent')
268
269    def update(self, confirmation_number=None):
270        if self.context.p_state == 'paid':
271            self.flash(_('This ticket has already been paid.'))
272            return
273        student = self.context.student
274        success, msg, log = query_etranzact(confirmation_number,self.context)
275        student.writeLogMessage(self, log)
276        if not success:
277            self.flash(msg)
278            return
279        success, msg, log = self.context.doAfterStudentPayment()
280        if log is not None:
281            student.writeLogMessage(self, log)
282        self.flash(msg)
283        return
284
285    def render(self):
286        self.redirect(self.url(self.context, '@@index'))
287        return
288
289class EtranzactQueryHistoryPageApplicant(UtilityView, grok.View):
290    """ Query history of eTranzact payments
291    """
292    grok.context(ICustomApplicantOnlinePayment)
293    grok.name('query_history')
294    grok.require('waeup.payApplicant')
295
296    def update(self, confirmation_number=None):
297        ob_class = self.__implemented__.__name__
298        if self.context.p_state == 'paid':
299            self.flash(_('This ticket has already been paid.'))
300            return
301        applicant = self.context.__parent__
302        success, msg, log = query_etranzact(confirmation_number,self.context)
303        applicant.writeLogMessage(self, log)
304        if not success:
305            self.flash(msg)
306            return
307        success, msg, log = self.context.doAfterApplicantPayment()
308        if log is not None:
309            applicant.writeLogMessage(self, log)
310        self.flash(msg)
311        return
312
313    def render(self):
314        self.redirect(self.url(self.context, '@@index'))
315        return
316
317# Disable Interswitch viewlets. This could be avoided by defining the
318# action button viewlets of kofacustom.nigeria.interswitch.browser in the
319# context of INigeriaStudentOnlinePayment or INigeriaApplicantOnlinePayment
320# respectively. But then all interswitch.browser modules have to be extended.
321
322class InterswitchActionButtonStudent(InterswitchActionButtonStudent):
323
324    @property
325    def target_url(self):
326        return ''
327
328class InterswitchRequestWebserviceActionButtonStudent(
329    InterswitchRequestWebserviceActionButtonStudent):
330
331    @property
332    def target_url(self):
333        return ''
334
335class InterswitchActionButtonApplicant(InterswitchActionButtonApplicant):
336
337    @property
338    def target_url(self):
339        return ''
340
341class InterswitchRequestWebserviceActionButtonApplicant(
342    InterswitchRequestWebserviceActionButtonApplicant):
343
344    @property
345    def target_url(self):
346        return ''
Note: See TracBrowser for help on using the repository browser.