## $Id: browser.py 15346 2019-03-06 22:19:56Z henrik $ ## ## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with this program; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ## from datetime import datetime import httplib import urllib import urllib2 import re from xml.dom.minidom import parseString import grok from zope.component import getUtility from zope.catalog.interfaces import ICatalog from waeup.kofa.interfaces import IUniversity, CLEARED from waeup.kofa.payments.interfaces import IPayer from waeup.kofa.webservices import PaymentDataWebservice from waeup.kofa.browser.layout import KofaPage, UtilityView from waeup.kofa.students.viewlets import ApprovePaymentActionButton as APABStudent from waeup.kofa.applicants.viewlets import ApprovePaymentActionButton as APABApplicant from waeup.aaue.interfaces import academic_sessions_vocab from kofacustom.nigeria.interswitch.browser import ( InterswitchActionButtonStudent, InterswitchRequestWebserviceActionButtonStudent, InterswitchActionButtonApplicant, InterswitchRequestWebserviceActionButtonApplicant) from waeup.aaue.interfaces import MessageFactory as _ from waeup.aaue.students.interfaces import ICustomStudentOnlinePayment from waeup.aaue.applicants.interfaces import ICustomApplicantOnlinePayment ERROR_PART1 = ( 'PayeeName=N/A~' + 'Faculty=N/A~' + 'Department=N/A~' + 'Level=N/A~' + 'ProgrammeType=N/A~' + 'StudyType=N/A~' + 'Session=N/A~' + 'PayeeID=N/A~' + 'Amount=N/A~' + 'FeeStatus=') ERROR_PART2 = ( '~Semester=N/A~' + 'PaymentType=N/A~' + 'MatricNumber=N/A~' + 'Email=N/A~' + 'PhoneNumber=N/A') class CustomPaymentDataWebservice(PaymentDataWebservice): """A simple webservice to publish payment and payer details on request from accepted IP addresses without authentication. Etranzact is asking for the PAYEE_ID which is indeed misleading. These are not the data of the payee but of the payer. And it's not the id of the payer but of the payment. """ grok.name('feerequest') #ACCEPTED_IP = ('195.219.3.181', '195.219.3.184') ACCEPTED_IP = None def update(self, PAYEE_ID=None, PAYMENT_TYPE=None): # webservice disabled on 6/3/19 self.output = None return if PAYEE_ID == None: self.output = ERROR_PART1 + 'Missing PAYEE_ID' + ERROR_PART2 return real_ip = self.request.get('HTTP_X_FORWARDED_FOR', None) # We can forego the logging once eTranzact payments run smoothly # and the accepted IP addresses are used. if real_ip: self.context.logger.info('PaymentDataWebservice called: %s' % real_ip) if real_ip and self.ACCEPTED_IP: if real_ip not in self.ACCEPTED_IP: self.output = ERROR_PART1 + 'Wrong IP address' + ERROR_PART2 return category_mapping = { 'SCHOOL-FEE-NEW': ('schoolfee',), 'SCHOOL-FEE-RETURNING': ('schoolfee',), 'SCHOOL-FEE-PLUS-NEW': ('schoolfee_incl',), 'SCHOOL-FEE-PLUS-RETURNING': ('schoolfee_incl',), 'SCHOOL-FEE-PG-NEW': ('schoolfee',), 'SCHOOL-FEE-PG-RETURNING': ('schoolfee',), 'SCHOOL-FEE-FIRST-INSTALMENT-PLUS': ('schoolfee_1',), 'SCHOOL-FEE-SECOND-INSTALMENT': ('schoolfee_2',), 'SCHOOL-FEE-BALANCE': ('schoolfee','schoolfee_incl', 'schoolfee_1','schoolfee_2'), 'SCHOOL-FEE-PT-NEW': ('schoolfee',), 'SCHOOL-FEE-PT-RETURNING': ('schoolfee',), 'SCHOOL-FEE-PT-PLUS-NEW': ('schoolfee_incl',), 'SCHOOL-FEE-PT-PLUS-RETURNING': ('schoolfee_incl',), 'SCHOOL-FEE-PT-PG-NEW': ('schoolfee',), 'SCHOOL-FEE-PT-PG-RETURNING': ('schoolfee',), 'SCHOOL-FEE-PT-FIRST-INSTALMENT-PLUS': ('schoolfee_1',), 'SCHOOL-FEE-PT-SECOND-INSTALMENT': ('schoolfee_2',), 'SCHOOL-FEE-PT-BALANCE': ('schoolfee','schoolfee_incl', 'schoolfee_1','schoolfee_2'), 'SCHOOL-FEE-FP-NEW': ('schoolfee',), 'SCHOOL-FEE-IJMBE': ('schoolfee',), 'ACCEPTANCE-FEE': ('clearance',), 'ACCEPTANCE-FEE-PLUS': ('clearance_incl',), 'ACCEPTANCE-FEE-PG': ('clearance',), 'ACCEPTANCE-FEE-PT': ('clearance',), 'ACCEPTANCE-FEE-PT-PLUS': ('clearance_incl',), 'ACCEPTANCE-FEE-PT-PG': ('clearance',), 'ACCEPTANCE-FEE-FP': ('clearance',), 'ACCEPTANCE-FEE-IJMBE': ('clearance',), 'APPLICATION-FEE-UTME': ('application',), 'APPLICATION-FEE-PT': ('application',), 'APPLICATION-FEE-FP': ('application',), 'LATE-REGISTRATION': ('late_registration',), 'LATE-REGISTRATION-PT': ('late_registration',), 'AAU-STUDENT-WELFARE-ASSURANCE': ('welfare',), 'AAU-STUDENT-WELFARE-ASSURANCE-PT': ('welfare',), 'AAU-STUDENT-ID_CARD': ('id_card',), 'HOSTEL-ACCOMMODATION-FEE': ('hostel_maintenance',), 'HOSTEL-ACCOMMODATION-FEE-PT': ('hostel_maintenance',), 'AAU-LAPEL-FILE-FEE': ('lapel',), 'AAU-LAPEL-FILE-FEE-PT': ('lapel',), 'MATRICULATION-GOWN-FEE': ('matric_gown',), 'MATRICULATION-GOWN-FEE-PT': ('matric_gown',), 'CONCESSIONAL-FEE': ('concessional',), 'CONCESSIONAL-FEE-PT': ('concessional',), 'STUDENTS-UNION-DUES': ('union',), 'STUDENTS-UNION-DUES-PT': ('union',), 'RESTITUTION-FEE': ('restitution',), } if PAYMENT_TYPE not in category_mapping.keys(): self.output = ERROR_PART1 + 'Invalid PAYMENT_TYPE' + ERROR_PART2 return # It seems eTranzact sends a POST request with an empty body but the URL # contains a query string. So it's actually a GET request pretended # to be a POST request. Although this does not comply with the # RFC 2616 HTTP guidelines we may try to fetch the id from the QUERY_STRING # value of the request. #if PAYEE_ID is None: # try: # PAYEE_ID = self.request['QUERY_STRING'].split('=')[1] # except: # self.output = '-4' # return cat = getUtility(ICatalog, name='payments_catalog') results = list(cat.searchResults(p_id=(PAYEE_ID, PAYEE_ID))) if len(results) != 1: self.output = ERROR_PART1 + 'Invalid PAYEE_ID' + ERROR_PART2 return student = getattr(results[0], 'student', None) amount = results[0].amount_auth payment_type = results[0].category p_category = results[0].p_category programme_type = results[0].p_item if not programme_type: programme_type = 'N/A' academic_session = academic_sessions_vocab.getTerm( results[0].p_session).title status = results[0].p_state if status == 'paid': self.output = ERROR_PART1 + 'PAYEE_ID already used' + ERROR_PART2 return if p_category not in category_mapping[PAYMENT_TYPE]: self.output = ERROR_PART1 + 'Wrong PAYMENT_TYPE' + ERROR_PART2 return if student and PAYMENT_TYPE.endswith('-RETURNING') \ and student.state == CLEARED: self.output = ERROR_PART1 + 'Not a returning student' + ERROR_PART2 return if student and PAYMENT_TYPE.endswith('-NEW') \ and student.state != CLEARED: self.output = ERROR_PART1 + 'Not a new student' + ERROR_PART2 return if student and '-PG' in PAYMENT_TYPE and not student.is_postgrad: self.output = ERROR_PART1 + 'Not a postgrad student' + ERROR_PART2 return if student and '-PG' not in PAYMENT_TYPE and student.is_postgrad \ and results[0].p_item != 'Balance': self.output = ERROR_PART1 + 'Postgrad student' + ERROR_PART2 return if student and '-PT' in PAYMENT_TYPE \ and not student.current_mode.endswith('_pt'): self.output = ERROR_PART1 + 'Not a part-time student' + ERROR_PART2 return if student and '-PT' not in PAYMENT_TYPE \ and student.current_mode.endswith('_pt'): self.output = ERROR_PART1 + 'Part-time student' + ERROR_PART2 return if student and '-FP' in PAYMENT_TYPE and student.current_mode != 'found': self.output = ERROR_PART1 + 'Not a foundation programme student' + ERROR_PART2 return if student and '-FP' not in PAYMENT_TYPE and student.current_mode == 'found': self.output = ERROR_PART1 + 'Foundation programme student' + ERROR_PART2 return if student and '-IJMBE' in PAYMENT_TYPE and student.current_mode != 'ijmbe': self.output = ERROR_PART1 + 'Not IJMBE student' + ERROR_PART2 return if student and '-IJMBE' not in PAYMENT_TYPE and student.current_mode == 'ijmbe': self.output = ERROR_PART1 + 'IJMBE student student' + ERROR_PART2 return if '-BALANCE' in PAYMENT_TYPE and results[0].p_item != 'Balance': self.output = ERROR_PART1 + 'Not a balance payment' + ERROR_PART2 return if not '-BALANCE' in PAYMENT_TYPE and results[0].p_item == 'Balance': self.output = ERROR_PART1 + 'Balance payment' + ERROR_PART2 return try: owner = IPayer(results[0]) full_name = owner.display_fullname matric_no = owner.id faculty = owner.faculty department = owner.department study_type = owner.current_mode email = owner.email phone = owner.phone level = owner.current_level except (TypeError, AttributeError): self.output = ERROR_PART1 + 'Unknown error' + ERROR_PART2 return self.output = ( 'PayeeName=%s~' + 'Faculty=%s~' + 'Department=%s~' + 'Level=%s~' + 'ProgrammeType=%s~' + 'StudyType=%s~' + 'Session=%s~' + 'PayeeID=%s~' + 'Amount=%s~' + 'FeeStatus=%s~' + 'Semester=N/A~' + 'PaymentType=%s~' + 'MatricNumber=%s~' + 'Email=%s~' + 'PhoneNumber=%s' ) % (full_name, faculty, department, level, programme_type, study_type, academic_session, PAYEE_ID, amount, status, payment_type, matric_no, email, phone) return # Requerying eTranzact payments TERMINAL_ID = '0570000070' QUERY_URL = 'https://www.etranzact.net/WebConnectPlus/query.jsp' # Test environment #QUERY_URL = 'http://demo.etranzact.com:8080/WebConnect/queryPayoutletTransaction.jsp' #TERMINAL_ID = '5009892289' def query_etranzact(confirmation_number, payment): postdict = {} postdict['TERMINAL_ID'] = TERMINAL_ID #postdict['RESPONSE_URL'] = 'http://dummy' postdict['CONFIRMATION_NO'] = confirmation_number data = urllib.urlencode(postdict) payment.conf_number = confirmation_number try: # eTranzact only accepts HTTP 1.1 requests. Therefore # the urllib2 package is required here. f = urllib2.urlopen(url=QUERY_URL, data=data) success = f.read() success = success.replace('\r\n','') # eTranzact sends strange HTML tags which must be removed. success = re.sub("<.*?>", "", success) if 'CUSTOMER_ID' not in success: msg = _('Invalid or unsuccessful callback: ${a}', mapping = {'a': success}) log = 'invalid callback for payment %s: %s' % (payment.p_id, success) payment.p_state = 'failed' return False, msg, log success = success.replace('%20',' ').split('&') # We expect at least two parameters if len(success) < 2: msg = _('Invalid callback: ${a}', mapping = {'a': success}) log = 'invalid callback for payment %s: %s' % (payment.p_id, success) payment.p_state = 'failed' return False, msg, log try: success_dict = dict([tuple(i.split('=')) for i in success]) except ValueError: msg = _('Invalid callback: ${a}', mapping = {'a': success}) log = 'invalid callback for payment %s: %s' % (payment.p_id, success) payment.p_state = 'failed' return False, msg, log except IOError: msg = _('eTranzact IOError') log = 'eTranzact IOError' return False, msg, log payment.r_code = u'ET' payment.r_company = u'etranzact' payment.r_desc = u'%s' % success_dict.get('TRANS_DESCR') payment.r_amount_approved = float(success_dict.get('TRANS_AMOUNT',0.0)) payment.r_card_num = None payment.r_pay_reference = u'%s' % success_dict.get('RECEIPT_NO') if payment.r_amount_approved != payment.amount_auth: msg = _('Wrong amount') log = 'wrong callback for payment %s: %s' % (payment.p_id, success) payment.p_state = 'failed' return False, msg, log customer_id = success_dict.get('CUSTOMER_ID') if payment.p_id != customer_id: msg = _('Wrong payment id') log = 'wrong callback for payment %s: %s' % (payment.p_id, success) payment.p_state = 'failed' return False, msg, log log = 'valid callback for payment %s: %s' % (payment.p_id, success) msg = _('Successful callback received') payment.p_state = 'paid' payment.payment_date = datetime.utcnow() return True, msg, log class EtranzactEnterPinActionButtonApplicant(APABApplicant): grok.context(ICustomApplicantOnlinePayment) grok.require('waeup.payApplicant') grok.order(3) icon = 'actionicon_call.png' text = _('Query eTranzact History') target = 'enterpin' class EtranzactEnterPinActionButtonStudent(APABStudent): grok.context(ICustomStudentOnlinePayment) grok.require('waeup.payStudent') grok.order(3) icon = 'actionicon_call.png' text = _('Query eTranzact History') target = 'enterpin' class EtranzactEnterPinPageStudent(KofaPage): """ """ grok.context(ICustomStudentOnlinePayment) grok.name('enterpin') grok.template('enterpin') grok.require('waeup.payStudent') buttonname = _('Submit to eTranzact') label = _('Requery eTranzact History') action = 'query_history' placeholder = _('Confirmation Number (PIN)') def update(self): super(EtranzactEnterPinPageStudent, self).update() if not self.context.p_category.startswith('schoolfee'): return student = self.context.student if student.state != CLEARED: return if student.entry_session < 2013: return for ticket in student['payments'].values(): if ticket.p_state == 'paid' and \ ticket.p_category.startswith('clearance'): return self.flash(_('Please pay acceptance fee first.'), type="danger") self.redirect(self.url(self.context, '@@index')) return class EtranzactEnterPinPageApplicant(EtranzactEnterPinPageStudent): """ """ grok.require('waeup.payApplicant') grok.context(ICustomApplicantOnlinePayment) class EtranzactQueryHistoryPageStudent(UtilityView, grok.View): """ Query history of eTranzact payments """ grok.context(ICustomStudentOnlinePayment) grok.name('query_history') grok.require('waeup.payStudent') def update(self, confirmation_number=None): if self.context.p_state == 'paid': self.flash(_('This ticket has already been paid.')) return student = self.context.student success, msg, log = query_etranzact(confirmation_number,self.context) student.writeLogMessage(self, log) if not success: self.flash(msg) return flashtype, msg, log = self.context.doAfterStudentPayment() if log is not None: student.writeLogMessage(self, log) self.flash(msg, type=flashtype) return def render(self): self.redirect(self.url(self.context, '@@index')) return class EtranzactQueryHistoryPageApplicant(UtilityView, grok.View): """ Query history of eTranzact payments """ grok.context(ICustomApplicantOnlinePayment) grok.name('query_history') grok.require('waeup.payApplicant') def update(self, confirmation_number=None): ob_class = self.__implemented__.__name__ if self.context.p_state == 'paid': self.flash(_('This ticket has already been paid.')) return applicant = self.context.__parent__ success, msg, log = query_etranzact(confirmation_number,self.context) applicant.writeLogMessage(self, log) if not success: self.flash(msg) return flashtype, msg, log = self.context.doAfterApplicantPayment() if log is not None: applicant.writeLogMessage(self, log) self.flash(msg, type=flashtype) return def render(self): self.redirect(self.url(self.context, '@@index')) return # Disable Interswitch viewlets. This could be avoided by defining the # action button viewlets of kofacustom.nigeria.interswitch.browser in the # context of INigeriaStudentOnlinePayment or INigeriaApplicantOnlinePayment # respectively. But then all interswitch.browser modules have to be extended. #class InterswitchActionButtonStudent(InterswitchActionButtonStudent): # @property # def target_url(self): # return '' #class InterswitchRequestWebserviceActionButtonStudent( # InterswitchRequestWebserviceActionButtonStudent): # @property # def target_url(self): # return '' #class InterswitchActionButtonApplicant(InterswitchActionButtonApplicant): # @property # def target_url(self): # return '' #class InterswitchRequestWebserviceActionButtonApplicant( # InterswitchRequestWebserviceActionButtonApplicant): # @property # def target_url(self): # return ''