Index: /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/applicantsbrowser.py
===================================================================
--- /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/applicantsbrowser.py	(revision 15599)
+++ /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/applicantsbrowser.py	(revision 15600)
@@ -25,8 +25,7 @@
 from waeup.kofa.browser.layout import UtilityView, KofaPage
 from waeup.kofa.browser.viewlets import ManageActionButton
-from waeup.kofa.applicants.browser import OnlinePaymentDisplayFormPage as OPDPApplicant
 from kofacustom.nigeria.etranzact.helpers import (
     write_payments_log, process_response, query_history)
-from kofacustom.nigeria.applicants.browser import NigeriaOnlinePaymentDisplayFormPage
+from kofacustom.nigeria.applicants.browser import NigeriaOnlinePaymentDisplayFormPage as NOPDPApplicant
 from kofacustom.nigeria.payments.interfaces import INigeriaOnlinePayment
 from kofacustom.nigeria.applicants.interfaces import INigeriaApplicantOnlinePayment
@@ -48,5 +47,5 @@
     grok.order(1)
     grok.context(INigeriaOnlinePayment)
-    grok.view(OPDPApplicant)
+    grok.view(NOPDPApplicant)
     grok.require('waeup.payApplicant')
     icon = 'actionicon_pay.png'
@@ -65,5 +64,5 @@
     grok.order(2)
     grok.context(INigeriaOnlinePayment)
-    grok.view(OPDPApplicant)
+    grok.view(NOPDPApplicant)
     grok.require('waeup.payApplicant')
     icon = 'actionicon_call.png'
@@ -137,5 +136,5 @@
         return
 
-class EtranzactReceiveResponseApplicant(NigeriaOnlinePaymentDisplayFormPage):
+class EtranzactReceiveResponseApplicant(NOPDPApplicant):
     """ View that receives the response from eTrantact payment gateway.
     """
Index: /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/studentsbrowser.py
===================================================================
--- /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/studentsbrowser.py	(revision 15600)
+++ /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/studentsbrowser.py	(revision 15600)
@@ -0,0 +1,204 @@
+## $Id: studentsbrowser.py 15599 2019-09-20 13:24:59Z henrik $
+##
+## Copyright (C) 2017 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
+##
+import grok
+import hashlib
+from datetime import datetime, timedelta
+from zope.component import getUtility
+from zope.security import checkPermission
+from waeup.kofa.interfaces import IKofaUtils
+from waeup.kofa.utils.helpers import to_timezone
+from waeup.kofa.browser.layout import UtilityView, KofaPage
+from waeup.kofa.browser.viewlets import ManageActionButton
+from kofacustom.nigeria.etranzact.helpers import (
+    write_payments_log, process_response, query_history)
+from kofacustom.nigeria.students.browser import NigeriaOnlinePaymentDisplayFormPage as NOPDPStudent
+from kofacustom.nigeria.payments.interfaces import INigeriaOnlinePayment
+from kofacustom.nigeria.students.interfaces import INigeriaStudentOnlinePayment
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+from kofacustom.nigeria.etranzact.tests import (
+    TERMINAL_ID, HOST, HTTPS, SECRET_KEY, LOGO_URL)
+
+grok.templatedir('browser_templates')
+
+def module_activated(session):
+    try:
+        return getattr(grok.getSite()['configuration'][str(session)],
+            'etranzact_enabled', False)
+    except KeyError:
+        return False
+
+class EtranzactActionButtonStudent(ManageActionButton):
+    grok.order(1)
+    grok.context(INigeriaOnlinePayment)
+    grok.view(NOPDPStudent)
+    grok.require('waeup.payStudent')
+    icon = 'actionicon_pay.png'
+    text = _('Pay via eTranzact')
+    target = 'goto_etranzact'
+
+    @property
+    def target_url(self):
+        if not module_activated(self.context.student.current_session):
+            return ''
+        if self.context.p_state != 'unpaid':
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class EtranzactRequeryActionButtonStudent(ManageActionButton):
+    grok.order(2)
+    grok.context(INigeriaOnlinePayment)
+    grok.view(NOPDPStudent)
+    grok.require('waeup.payStudent')
+    icon = 'actionicon_call.png'
+    text = _('Requery eTranzact History')
+    target = 'requery_history'
+
+    @property
+    def target_url(self):
+        if not module_activated(self.context.student.current_session):
+            return ''
+        if self.context.p_state in ('paid', 'waived'):
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class EtranzactPageStudent(KofaPage):
+    """ View which sends a POST request to the eTranzact payment gateway.
+    """
+    grok.context(INigeriaStudentOnlinePayment)
+    grok.name('goto_etranzact')
+    grok.template('goto_etranzact')
+    grok.require('waeup.payStudent')
+    label = _('Pay via eTranzact')
+    submit_button = _('Pay now')
+
+    host = HOST
+    https = HTTPS
+    secret_key = SECRET_KEY
+    terminal_id = TERMINAL_ID
+    logo_url = LOGO_URL
+
+    @property
+    def action(self):
+        if self.https:
+            return 'https://' + self.host + '/webconnect/v3/caller.jsp'
+        return 'http://' + self.host + '/webconnect/v3/caller.jsp'
+
+    def init_update(self):
+        if self.context.p_state == 'paid':
+            return _("Payment ticket can't be re-sent to eTranzact.")
+        now = datetime.utcnow()
+        if self.context.creation_date.tzinfo is not None:
+            # That's bad. Please store timezone-naive datetimes only!
+            now = self.context.creation_date.tzinfo.localize(now)
+        time_delta = now - self.context.creation_date
+        if time_delta.days > 7:
+            return _("This payment ticket is too old. Please create a new ticket.")
+        # In contrast to the procedure in the Remita and Interswitch modules,
+        # we do not call requery_history but receive and evaluate
+        # the response form from eTranzact directly. This is possible
+        # because eTranzact provides the FINAL_CHECKSUM hash value
+        # which authenticates the response.
+        self.responseurl = self.url(self.context, 'receive_etranzact')
+        # Already now it becomes a eTranzact payment
+        self.context.r_company = u'etranzact'
+        hashargs = 	self.amount + self.terminal_id+self.transaction_id \
+            + self.responseurl + self.secret_key
+        self.hashvalue = hashlib.md5(hashargs).hexdigest()
+        self.customer = self.context.student
+        return
+
+    def update(self):
+        if not module_activated(self.context.student.current_session):
+            return
+        self.transaction_id = self.context.p_id
+        self.amount = "%.1f" % self.context.amount_auth
+        error = self.init_update()
+        if error:
+            self.flash(error, type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        return
+
+class EtranzactReceiveResponseStudent(NOPDPStudent):
+    """ View that receives the response from eTrantact payment gateway.
+    """
+    grok.name('receive_etranzact')
+
+    secret_key = SECRET_KEY
+    terminal_id = TERMINAL_ID
+
+    def update(self):
+        super(EtranzactReceiveResponseStudent, self).update()
+        if not module_activated(self.context.student.current_session):
+            return
+        student = self.context.student
+        form = self.request.form
+        verify = False
+        if self.context.p_state == 'paid':
+            verify = True
+        success, msg, log = process_response(self.context, form, self, verify)
+        student.writeLogMessage(self, log)
+        if not success:
+            self.flash(msg, type='danger')
+            return
+        write_payments_log(student.student_id, self.context)
+        flashtype, msg, log = self.context.doAfterStudentPayment()
+        if log is not None:
+            student.writeLogMessage(self, log)
+        self.flash(msg, type=flashtype)
+        return
+
+class EtranzactRequestPaymentStatusPageStudent(UtilityView, grok.View):
+    """ Request webservice view for the eTranzact gateway.
+    """
+    grok.context(INigeriaStudentOnlinePayment)
+    grok.name('requery_history')
+    grok.require('waeup.payStudent')
+
+    host = HOST
+    https = HTTPS
+    secret_key = SECRET_KEY
+    terminal_id = TERMINAL_ID
+    logo_url = LOGO_URL
+
+    def update(self):
+        if not module_activated(self.context.student.current_session):
+            return
+        if self.context.p_state in ('paid', 'waived'):
+            self.flash(_('This ticket has already been paid.'), type='danger')
+            return
+        student = self.context.student
+        verify = False
+        raw, form = query_history(self.host, self.terminal_id,
+                                  self.context.p_id, self.https)
+        success, msg, log = process_response(self.context, form, self, verify)
+        student.writeLogMessage(self, log)
+        if not success:
+            self.flash(msg, type='danger')
+            return
+        write_payments_log(student.student_id, self.context)
+        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))
+        return
Index: /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/tests.py
===================================================================
--- /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/tests.py	(revision 15599)
+++ /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/tests.py	(revision 15600)
@@ -175,5 +175,5 @@
 
     @external_test
-    def test_views(self):
+    def test_applicant_views(self):
         # Manager can access eTranzact form
         self.browser.getLink("Pay via eTranzact").click()
@@ -191,3 +191,46 @@
         return
 
-
+class EtranzactTestsStudents(StudentsFullSetup):
+    """Tests for the Etranzact payment gateway.
+    """
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(EtranzactTestsStudents, self).setUp()
+        self.app['configuration']['2004'].etranzact_enabled = True
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.payments_path)
+        IWorkflowState(self.student).setState('cleared')
+        self.student.nationality = u'NG'
+        self.browser.open(self.payments_path + '/addop')
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+        ctrl = self.browser.getControl(name='val_id')
+        self.value = ctrl.options[0]
+        self.browser.getLink(self.value).click()
+        self.assertMatches('...Amount Authorized...',
+                           self.browser.contents)
+        self.assertTrue('<span>40000.0</span>', self.browser.contents)
+        self.payment_url = self.browser.url
+        self.payment = self.student['payments'][self.value]
+
+
+    @external_test
+    def test_student_views(self):
+        # Manager can access eTranzact form
+        self.browser.getLink("Pay via eTranzact").click()
+        self.assertTrue("Pay now" in self.browser.contents)
+        # Means of testing end here.
+        # We requery an existing paiment now.
+        self.payment.p_id = TID
+        self.browser.open(self.payment_url)
+        self.browser.getLink("Requery eTranzact History").click()
+        self.assertTrue('Wrong checksum.' in self.browser.contents)
+        # ... probably because responseurl and amount stored in the
+        # system and the responseurl generated in process_response are
+        # different
+        # Means of testing end here again.
+        return
