Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/payoutletbrowser.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/payoutletbrowser.py	(revision 16483)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/payoutletbrowser.py	(revision 16484)
@@ -80,5 +80,5 @@
     grok.context(INigeriaStudentOnlinePayment)
     grok.require('waeup.payStudent')
-    grok.order(3)
+    grok.order(10)
     icon = 'actionicon_pay.png'
     text = _('Pay via Etranzact PayOutlet')
@@ -102,5 +102,5 @@
     grok.require('waeup.payStudent')
 
-    buttonname = _('Submit to Etranzact')
+    buttonname = _('Requery now')
     label = _('Requery Etranzact PayOutlet History')
     action = 'query_payoutlet_history'
@@ -131,5 +131,5 @@
     grok.template('enterpin')
 
-    buttonname = _('Submit to Etranzact')
+    buttonname = _('Requery now')
     label = _('Requery Etranzact PayOutlet History')
     action = 'query_payoutlet_history'
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/studentsbrowser.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/studentsbrowser.py	(revision 16483)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/studentsbrowser.py	(revision 16484)
@@ -52,5 +52,5 @@
 
 class EtranzactActionButtonStudent(ManageActionButton):
-    grok.order(1)
+    grok.order(8)
     grok.context(INigeriaOnlinePayment)
     grok.view(NOPDPStudent)
@@ -70,5 +70,5 @@
 
 class EtranzactRequeryActionButtonStudent(ManageActionButton):
-    grok.order(2)
+    grok.order(9)
     grok.context(INigeriaOnlinePayment)
     grok.view(NOPDPStudent)
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/tests.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/tests.py	(revision 16483)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/tests.py	(revision 16484)
@@ -261,5 +261,5 @@
         self.browser.getLink("Pay via Etranzact PayOutlet").click()
         self.browser.getControl(name="confirmation_number").value = '600854291572447457669'
-        self.browser.getControl("Submit to Etranzact").click()
+        self.browser.getControl("Requery now").click()
         # This response is strange
         self.assertTrue('-3:Wrong Setup' in self.browser.contents)
@@ -267,5 +267,5 @@
         # This confirmation number exists
         self.browser.getControl(name="confirmation_number").value = '500854291572447457669'
-        self.browser.getControl("Submit to Etranzact").click()
+        self.browser.getControl("Requery now").click()
         self.assertTrue('Wrong amount' in self.browser.contents)
         # Some attributes have been set
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interfaces.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interfaces.py	(revision 16483)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interfaces.py	(revision 16484)
@@ -126,5 +126,10 @@
 
     interswitch_enabled = schema.Bool(
-        title = _(u'Interswitch integration enabled'),
+        title = _(u'Interswitch Collegepay integration enabled'),
+        default = True,
+        )
+
+    interswitch_paydirect_enabled = schema.Bool(
+        title = _(u'Interswitch Paydirect integration enabled'),
         default = True,
         )
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/browser.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/browser.py	(revision 16483)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/browser.py	(revision 16484)
@@ -22,5 +22,5 @@
 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.layout import UtilityView, KofaPage, KofaFormPage, action
 from waeup.kofa.browser.viewlets import ManageActionButton
 from waeup.kofa.students.interfaces import IStudentsUtils
@@ -28,8 +28,9 @@
 from waeup.kofa.applicants.browser import OnlinePaymentDisplayFormPage as OPDPApplicant
 from kofacustom.nigeria.interswitch.helpers import (
-    query_interswitch, write_payments_log)
+    query_interswitch, write_payments_log, fetch_booking_details)
 from kofacustom.nigeria.payments.interfaces import INigeriaOnlinePayment
 from kofacustom.nigeria.students.interfaces import INigeriaStudentOnlinePayment
 from kofacustom.nigeria.applicants.interfaces import INigeriaApplicantOnlinePayment
+from kofacustom.nigeria.interswitch.tests import PAYDIRECT_URL, PAYDIRECT_HOST, MERCHANT_ID
 from kofacustom.nigeria.interfaces import MessageFactory as _
 
@@ -58,5 +59,5 @@
     grok.require('waeup.payStudent')
     icon = 'actionicon_pay.png'
-    text = _('Pay via Interswitch')
+    text = _('Pay via Interswitch CollegePAY')
     target = 'goto_interswitch'
 
@@ -89,5 +90,5 @@
     grok.require('waeup.payStudent')
     icon = 'actionicon_call.png'
-    text = _('Requery Interswitch History')
+    text = _('Requery CollegePAY History')
     target = 'request_webservice'
 
@@ -315,5 +316,5 @@
     grok.template('student_goto_interswitch')
     grok.require('waeup.payStudent')
-    label = _('Submit data to CollegePAY (Interswitch Payment Gateway)')
+    label = _('Submit data to CollegePAY')
     submit_button = _('Submit')
 
@@ -392,5 +393,5 @@
     grok.template('applicant_goto_interswitch')
     grok.name('goto_interswitch')
-    label = _('Submit data to CollegePAY (Interswitch Payment Gateway)')
+    label = _('Submit data to CollegePAY')
     submit_button = _('Submit')
 
@@ -444,2 +445,3 @@
         self.amount_auth = int(100 * self.context.amount_auth)
         return
+
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/browser_templates/paydirect.pt
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/browser_templates/paydirect.pt	(revision 16484)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/browser_templates/paydirect.pt	(revision 16484)
@@ -0,0 +1,57 @@
+<p i18n:translate="">
+Go to your bank, show your PAYDirect Reference Number and make your payment.
+After having paid, you can fetch the data
+from Interswitch by pressing the button below. Interswitch will confirm that
+the payment has been successfully made.
+</p>
+
+<br />
+
+<table class="form-table" i18n:domain="waeup.kofa">
+  <tbody>
+    <tr>
+      <td i18n:translate="" class="fieldname">
+        Total Amount Authorized:</td>
+      <td>
+        <span tal:replace="view/context/amount_auth">AMOUNT</span>
+      </td>
+    </tr>
+    <tr>
+      <td i18n:translate="" class="fieldname">
+        PAYDirect Reference Number:</td>
+      <td>
+        <span tal:replace="view/ref_number">PAYREF</span>
+      </td>
+    </tr>
+    <tr>
+      <td i18n:translate="" class="fieldname">
+        Payment Category:</td>
+      <td>
+        <span tal:replace="view/context/p_category">PAYMENTCATEGORY</span>
+      </td>
+    </tr>
+    <tr>
+      <td i18n:translate="" class="fieldname">
+        Payment Item:</td>
+      <td>
+        <span tal:replace="view/context/display_item">PAYMENTITEM</span>
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+<br />
+
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data">
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
+
+<br /><br />
+
+<img src="/static/img/interswitch_logos.png" />
+
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/helpers.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/helpers.py	(revision 16483)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/helpers.py	(revision 16484)
@@ -30,7 +30,4 @@
 def SOAP_post(soap_action, xml, host, url, https):
     """Handles making the SOAP request.
-
-    Further reading:
-    http://testwebpay.interswitchng.com/test_paydirect/services/TransactionQueryWs.asmx?op=getTransactionData
     """
     if https:
@@ -48,4 +45,13 @@
     return response
 
+def write_payments_log(id, payment):
+    payment.logger.info(
+        '%s,%s,%s,%s,%s,%s,%s,%s,,,' % (
+        id, payment.p_id, payment.p_category,
+        payment.amount_auth, payment.r_code,
+        payment.provider_amt, payment.gateway_amt,
+        payment.thirdparty_amt))
+
+# CollegePAY helper functions
 
 def get_SOAP_response(product_id, transref, host, url, https):
@@ -228,9 +234,93 @@
     return True, msg, log
 
-def write_payments_log(id, payment):
-    payment.logger.info(
-        '%s,%s,%s,%s,%s,%s,%s,%s,,,' % (
-        id, payment.p_id, payment.p_category,
-        payment.amount_auth, payment.r_code,
-        payment.provider_amt, payment.gateway_amt,
-        payment.thirdparty_amt))
+# PAYDirect helper functions
+
+def get_SOAP_response_paydirect(merchant_id, p_id, host, url, https):
+    xml="""\
+<?xml version="1.0" encoding="utf-8"?>
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:book="http://interswitchng.com/bookonhold">
+   <soap:Header/>
+   <soap:Body>
+      <book:FetchBookingDetails>
+         <book:ReservationDetailsRequest>
+            <book:MerchantId>%s</book:MerchantId>
+            <book:Bookings>
+               <book:Booking>
+                  <book:ReferenceNumber>%s%s</book:ReferenceNumber>
+               </book:Booking>
+            </book:Bookings>
+         </book:ReservationDetailsRequest>
+      </book:FetchBookingDetails>
+   </soap:Body>
+</soap:Envelope>""" % (merchant_id, merchant_id, p_id[1:])
+
+    response=SOAP_post(
+        "http://interswitchng.com/bookonhold/FetchBookingDetails",
+        xml, host, url, https)
+    if response.status!=200:
+        error = 'Connection error (%s, %s)' % (response.status, response.reason)
+        return error
+    result_xml = response.read()
+    return result_xml
+
+def fetch_booking_details(payment, merchant_id, host, url, https):
+    result_xml = get_SOAP_response_paydirect(
+        merchant_id, payment.p_id, host, url, https)
+    if result_xml.startswith('Connection error'):
+        return False, result_xml, result_xml
+    doc=parseString(result_xml)
+    if not doc.getElementsByTagName('PaymentStatus'):
+        msg = _('Your payment %s was not found.' % payment.p_id)
+        log = 'payment %s cannot be found' % payment.p_id
+        return False, msg, log
+    p_status = doc.getElementsByTagName('PaymentStatus')[0].firstChild.data
+    payment.r_code = p_status
+    try:
+        payment.r_desc = "%s - %s - %s" % (
+            doc.getElementsByTagName('ChannelName')[0].firstChild.data,
+            doc.getElementsByTagName('TerminalId')[0].firstChild.data,
+            doc.getElementsByTagName('Location'))[0].firstChild.data
+    except AttributeError:
+        pass
+    try:
+        amount = doc.getElementsByTagName('Amount')[0].firstChild.data
+        payment.r_amount_approved = int(amount)
+    except AttributeError:
+        pass
+    try:
+        payment.r_pay_reference = doc.getElementsByTagName(
+            'ReferenceNumber').firstChild.data
+    except AttributeError:
+        pass
+    if p_status not in ('Pending', 'Completed'):
+        msg = _('Unknown status: %s' % sr[0])
+        log = 'invalid callback for payment %s: %s' % (payment.p_id, status)
+        payment.p_state = 'failed'
+        notify(grok.ObjectModifiedEvent(payment))
+        return False, msg, log
+    if p_status == 'Completed' and not payment.r_amount_approved:
+        msg = _('Amount unconfirmed')
+        log = 'unsuccessful callback for payment %s: amount unconfirmed' % payment.p_id
+        payment.p_state = 'failed'
+        notify(grok.ObjectModifiedEvent(payment))
+        return False, msg, log
+    if p_status == 'Pending':
+        msg = _('Payment pending')
+        log = 'unsuccessful callback for payment %s: pending' % payment.p_id
+        payment.p_state = 'failed'
+        notify(grok.ObjectModifiedEvent(payment))
+        return False, msg, log
+    if payment.r_amount_approved != payment.amount_auth:
+        msg = _('Callback amount does not match.')
+        log = 'unsuccessful callback for %s payment %s: callback amount %s does not match' % (
+            payment.p_category, payment.p_id, amount)
+        payment.p_state = 'failed'
+        notify(grok.ObjectModifiedEvent(payment))
+        return False, msg, log
+    payment.p_state = 'paid'
+    payment.payment_date = datetime.utcnow()
+    msg = _('Successful callback received')
+    log = 'valid callback for %s payment %s: %s' % (
+        payment.p_category, payment.p_id, p_status)
+    notify(grok.ObjectModifiedEvent(payment))
+    return True, msg, log
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/paydirectbrowser.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/paydirectbrowser.py	(revision 16484)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/paydirectbrowser.py	(revision 16484)
@@ -0,0 +1,257 @@
+## $Id$
+##
+## Copyright (C) 2021 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
+from datetime import datetime, timedelta
+from zope.i18n import translate
+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, KofaFormPage, action
+from waeup.kofa.browser.viewlets import ManageActionButton
+from waeup.kofa.students.interfaces import IStudentsUtils
+from waeup.kofa.students.browser import OnlinePaymentDisplayFormPage as OPDPStudent
+from waeup.kofa.students.browser import StudentBasePDFFormPage
+from waeup.kofa.applicants.browser import OnlinePaymentDisplayFormPage as OPDPApplicant
+from kofacustom.nigeria.interswitch.helpers import (
+    query_interswitch, write_payments_log, fetch_booking_details)
+from kofacustom.nigeria.payments.interfaces import INigeriaOnlinePayment
+from kofacustom.nigeria.students.interfaces import INigeriaStudentOnlinePayment
+from kofacustom.nigeria.applicants.interfaces import INigeriaApplicantOnlinePayment
+from kofacustom.nigeria.interswitch.tests import PAYDIRECT_URL, PAYDIRECT_HOST, MERCHANT_ID
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+GATEWAY_AMT = 300.0
+
+grok.templatedir('browser_templates')
+
+def paydirect_module_activated(session, payment):
+    if payment.r_company and payment.r_company != 'interswitch':
+        return False
+    try:
+        return getattr(grok.getSite()['configuration'][str(session)],
+            'interswitch_paydirect_enabled', True)
+    except KeyError:
+        session = datetime.now().year
+        try:
+            return getattr(grok.getSite()['configuration'][str(session)],
+                'interswitch_paydirect_enabled', True)
+        except KeyError:
+            return False
+
+class PAYDirectActionButtonStudent(ManageActionButton):
+    grok.order(2)
+    grok.context(INigeriaOnlinePayment)
+    grok.view(OPDPStudent)
+    grok.require('waeup.payStudent')
+    icon = 'actionicon_pay.png'
+    text = _('Pay via Interswitch PAYDirect')
+    target = 'paydirect'
+
+    @property
+    def target_url(self):
+        if not paydirect_module_activated(
+            self.context.student.current_session, self.context):
+            return ''
+        if self.context.p_state == 'paid':
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class PAYDirectActionButtonApplicant(PAYDirectActionButtonStudent):
+    grok.view(OPDPApplicant)
+    grok.require('waeup.payApplicant')
+
+    @property
+    def target_url(self):
+        if not paydirect_module_activated(
+            self.context.__parent__.__parent__.year, self.context):
+            return ''
+        if self.context.p_state != 'unpaid':
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class PAYDirectPageStudent(KofaFormPage):
+    """Inform student how to proceed
+    """
+    grok.context(INigeriaStudentOnlinePayment)
+    grok.template('paydirect')
+    grok.name('paydirect')
+    grok.require('waeup.payStudent')
+    label = _('Requery Interswitch PAYDirect History')
+
+    gateway_amt = GATEWAY_AMT
+    merchant_id = MERCHANT_ID
+    gateway_url = PAYDIRECT_URL
+    gateway_host = PAYDIRECT_HOST
+    https = True
+
+    def init_update(self):
+        if self.context.p_state == 'paid':
+            return _("Payment ticket can't be used again.")
+        if self.context.r_company and self.context.r_company != 'interswitch':
+            return _("Payment ticket has been used for another payment gateway.")
+        tz = getUtility(IKofaUtils).tzinfo
+        time_delta = datetime.utcnow() - self.context.creation_date
+        if time_delta.days > 7:
+            return _("This payment ticket is too old. Please create a new ticket.")
+        self.ref_number = self.merchant_id + self.context.p_id[1:]
+        return
+
+    def update(self):
+        if not paydirect_module_activated(
+            self.context.student.current_session, self.context):
+            self.flash(_('Forbidden'), type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        error = self.init_update()
+        if error:
+            self.flash(error, type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        # Already now it becomes an Interswitch payment. We set the net amount
+        # and add the gateway amount.
+        if not self.context.r_company:
+            self.context.net_amt = self.context.amount_auth
+            self.context.amount_auth += self.gateway_amt
+            self.context.gateway_amt = self.gateway_amt
+            self.context.r_company = u'interswitch'
+        return
+
+    @action('Requery now', style='primary')
+    def fetch(self):
+        student = self.context.student
+        success, msg, log = fetch_booking_details(
+            self.context, self.merchant_id, self.gateway_host,
+            self.gateway_url, self.https)
+        student.writeLogMessage(self, log)
+        if not success:
+            self.flash(msg, type='warning')
+            self.redirect(self.url(self.context, '@@index'))
+            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 PAYDirectPageApplicant(PAYDirectPageStudent):
+    """ Inform applicant how to proceed.
+    """
+    grok.context(INigeriaApplicantOnlinePayment)
+    grok.require('waeup.payApplicant')
+
+    def update(self):
+        if not paydirect_module_activated(
+            self.context.__parent__.__parent__.year, self.context):
+            self.flash(_('Forbidden'), type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        error = self.init_update()
+        if error:
+            self.flash(error, type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+        if self.context.__parent__.__parent__.expired \
+            and self.context.__parent__.__parent__.strict_deadline:
+            return _("Payment ticket can't be used. "
+                     "Application period has expired.")
+        # Already now it becomes an Interswitch payment. We set the net amount
+        # and add the gateway amount.
+        if not self.context.r_company:
+            self.context.net_amt = self.context.amount_auth
+            self.context.amount_auth += self.gateway_amt
+            self.context.gateway_amt = self.gateway_amt
+            self.context.r_company = u'interswitch'
+        return
+
+    @action('Requery now', style='primary')
+    def fetch(self):
+        applicant = self.context.__parent__
+        success, msg, log = fetch_booking_details(
+            self.context, self.merchant_id, self.gateway_host,
+            self.gateway_url, self.https)
+        applicant.writeLogMessage(self, log)
+        if not success:
+            self.flash(msg, type='warning')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        write_payments_log(applicant.applicant_id, self.context)
+        flashtype, msg, log = self.context.doAfterApplicantPayment()
+        if log is not None:
+            applicant.writeLogMessage(self, log)
+        self.flash(msg, type=flashtype)
+        return
+
+class PaymentRefNumberSlipActionButton(ManageActionButton):
+    grok.order(1)  # This button should always be the last one.
+    grok.context(INigeriaOnlinePayment)
+    grok.view(PAYDirectPageStudent)
+    grok.require('waeup.viewStudent')
+    icon = 'actionicon_pdf.png'
+    text = _('Download reference number slip')
+    target = 'refnumberslip.pdf'
+
+    @property
+    def target_url(self):
+        if self.context.p_state == 'paid':
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class RefNumberSlipStudent(UtilityView, grok.View):
+    """Deliver a PDF slip of the context.
+    """
+    grok.context(INigeriaOnlinePayment)
+    grok.name('refnumberslip.pdf')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(INigeriaOnlinePayment).select(
+        'amount_auth', 'p_category')
+    prefix = 'form'
+    omit_fields = (
+        'password', 'suspended', 'phone', 'date_of_birth',
+        'adm_code', 'sex', 'suspended_comment', 'current_level',
+        'flash_notice', 'parents_email', 'current_mode',
+        'entry_session', 'reg_number')
+    merchant_id = MERCHANT_ID
+
+    @property
+    def refnumber(self):
+        return self.merchant_id + self.context.p_id[1:]
+
+    @property
+    def label(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        return translate(_('Interswitch PAYDirect Payment'),
+            'waeup.kofa', target_language=portal_language) \
+            + ' %s' % self.refnumber
+
+    @property
+    def note(self):
+        return """<br /><br />
+Go to your bank and make your PAYDirect payment with the reference number <strong>%s</strong>.
+""" % self.refnumber
+    def render(self):
+        if self.context.p_state == 'paid':
+            self.flash('Payment has already been made.')
+            self.redirect(self.url(self.context))
+            return
+        studentview = StudentBasePDFFormPage(self.context.student,
+            self.request, self.omit_fields)
+        students_utils = getUtility(IStudentsUtils)
+        return students_utils.renderPDF(self, 'refnumberslip.pdf',
+            self.context.student, studentview, note=self.note,
+            omit_fields=self.omit_fields)
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/tests.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/tests.py	(revision 16483)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/tests.py	(revision 16484)
@@ -20,4 +20,5 @@
 from zope.component import createObject, getUtility
 from zope.catalog.interfaces import ICatalog
+from xml.dom.minidom import parseString
 from hurry.workflow.interfaces import IWorkflowState
 from waeup.kofa.students.tests.test_browser import StudentsFullSetup
@@ -25,5 +26,7 @@
 from waeup.kofa.configuration import SessionConfiguration
 from waeup.kofa.students.payments import StudentOnlinePayment
-from kofacustom.nigeria.interswitch.helpers import query_interswitch
+from waeup.kofa.browser.tests.test_pdf import samples_dir
+from kofacustom.nigeria.interswitch.helpers import (
+    query_interswitch, get_SOAP_response_paydirect, SOAP_post)
 from kofacustom.nigeria.testing import FunctionalLayer
 
@@ -31,8 +34,23 @@
 #   If you enable this, please make sure the external services
 #   do exist really and are not bothered by being spammed by a test programme.
-EXTERNAL_TESTS = False
-
-def external_test(func):
-    if not EXTERNAL_TESTS:
+EXTERNAL_TESTS_1 = True
+EXTERNAL_TESTS_2 = True
+
+PAYDIRECT_HOST = 'sandbox.interswitchng.com'
+PAYDIRECT_URL = '/bookonhold/bookonhold.asmx'
+MERCHANT_ID = '6033'
+
+def external_test_1(func):
+    if not EXTERNAL_TESTS_1:
+        myself = __file__
+        if myself.endswith('.pyc'):
+            myself = myself[:-1]
+        print "WARNING: external tests are skipped!"
+        print "WARNING: edit %s to enable them." % myself
+        return
+    return func
+
+def external_test_2(func):
+    if not EXTERNAL_TESTS_2:
         myself = __file__
         if myself.endswith('.pyc'):
@@ -72,7 +90,5 @@
     def test_interswitch_form(self):
         # Manager can access InterswitchForm
-        self.browser.getLink("Pay via Interswitch", index=0).click()
-        # The total amount to be processed by Interswitch
-        # has been reduced by the Interswitch fee of 150 Nairas
+        self.browser.getLink("Pay via Interswitch CollegePAY", index=0).click()
         self.assertMatches('...<input type="hidden" name="pay_item_id" />...',
                            self.browser.contents)
@@ -101,5 +117,5 @@
         self.assertEqual(self.student['payments'][value].gateway_amt, 0.0)
         self.browser.getLink(value).click()
-        self.browser.getLink("Pay via Interswitch", index=0).click()
+        self.browser.getLink("Pay via Interswitch CollegePAY", index=0).click()
         # Split amounts have been set.
         self.assertEqual(self.student['payments'][value].provider_amt, 0.0)
@@ -113,5 +129,5 @@
     def test_interswitch_form_ticket_expired(self):
         # Manager can access InterswitchForm
-        self.browser.getLink("Pay via Interswitch", index=0).click()
+        self.browser.getLink("Pay via Interswitch CollegePAY", index=0).click()
         self.assertMatches('...<input type="hidden" name="pay_item_id" />...',
                            self.browser.contents)
@@ -125,5 +141,5 @@
         self.payment.creation_date -= delta
         self.browser.open(self.payment_url)
-        self.browser.getLink("Pay via Interswitch", index=0).click()
+        self.browser.getLink("Pay via Interswitch CollegePAY", index=0).click()
         self.assertMatches(
             '...This payment ticket is too old. Please create a new ticket...',
@@ -132,5 +148,5 @@
         self.payment.creation_date += delta
         self.browser.open(self.payment_url)
-        self.browser.getLink("Pay via Interswitch", index=0).click()
+        self.browser.getLink("Pay via Interswitch CollegePAY").click()
         self.assertMatches('...Total Amount Authorized:...',
                            self.browser.contents)
@@ -140,5 +156,5 @@
         # We should not have TZ data in timestamps processed, but it looks
         # like we get some with imports :-/
-        self.browser.getLink("Pay via Interswitch", index=0).click()
+        self.browser.getLink("Pay via Interswitch CollegePAY", index=0).click()
         delta = timedelta(days=8)
         self.payment.creation_date -= delta
@@ -147,5 +163,5 @@
             self.payment.creation_date)
         self.browser.open(self.payment_url)
-        self.browser.getLink("Pay via Interswitch", index=0).click()
+        self.browser.getLink("Pay via Interswitch CollegePAY", index=0).click()
         self.assertMatches(
             '...This payment ticket is too old. Please create a new ticket...',
@@ -154,9 +170,9 @@
         self.payment.creation_date += delta
         self.browser.open(self.payment_url)
-        self.browser.getLink("Pay via Interswitch", index=0).click()
-        self.assertMatches('...Total Amount Authorized:...',
-                           self.browser.contents)
-
-    @external_test
+        self.browser.getLink("Pay via Interswitch CollegePAY").click()
+        self.assertMatches('...Total Amount Authorized:...',
+                           self.browser.contents)
+
+    @external_test_1
     def test_query_interswitch_SOAP(self):
         host = 'webpay.interswitchng.com'
@@ -176,5 +192,5 @@
             'ZIB|WEB|ABAL|3-11-2015|021336:000457580882' in log)
 
-    @external_test
+    @external_test_1
     def test_query_interswitch_JSON(self):
         host = 'webpay.interswitchng.com'
@@ -209,4 +225,112 @@
             "u'LeadBankName': None}" in log)
 
+# PAYDirect tests
+
+    def create_booking(self, p_id):
+        xml="""\
+<?xml version="1.0" encoding="utf-8"?>
+<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
+  <soap:Body>
+    <CreateBooking xmlns="http://interswitchng.com/bookonhold">
+      <ReservationRequest>
+        <MerchantId>%s</MerchantId>
+        <Bookings>
+          <Booking>
+            <ReferenceNumber>%s%s</ReferenceNumber>
+            <ResponseCode>01</ResponseCode>
+            <ResponseDescription>02</ResponseDescription>
+            <Description>My Description</Description>
+            <Amount>1000</Amount>
+            <DateBooked>2021-05-05</DateBooked>
+            <DateExpired>2025-05-05</DateExpired>
+            <FirstName>Dummy</FirstName>
+            <LastName>Student</LastName>
+            <Status>string</Status>
+            <PaymentStatus>string</PaymentStatus>
+            <PaymentReference>string</PaymentReference>
+            <ChannelName>04</ChannelName>
+            <TerminalId>05</TerminalId>
+            <Location>string</Location>
+            <PaymentDate>2020-05-05</PaymentDate>
+            <PaymentMethod>string</PaymentMethod>
+            <Email>aa@aa.aa</Email>
+            <ItemCode>01</ItemCode>
+          </Booking>
+        </Bookings>
+      </ReservationRequest>
+    </CreateBooking>
+  </soap:Body>
+</soap:Envelope>""" % (MERCHANT_ID, MERCHANT_ID, p_id[1:])
+
+        SOAP_post("http://interswitchng.com/bookonhold/CreateBooking",xml,
+                  PAYDIRECT_HOST, PAYDIRECT_URL, True)
+
+    @external_test_2
+    def test_get_SOAP_response_paydirect(self):
+        p_id = 'p6021570467807'
+        self.create_booking(p_id)
+        result_xml = get_SOAP_response_paydirect(
+            MERCHANT_ID, p_id, PAYDIRECT_HOST, PAYDIRECT_URL, True)
+        doc=parseString(result_xml)
+        status=doc.getElementsByTagName('PaymentStatus')[0].firstChild.data
+        amount=doc.getElementsByTagName('Amount')[0].firstChild
+        self.assertEqual(status, 'Pending')
+        self.assertEqual(amount, None)
+        p_id = 'p5812734587097'
+        result_xml = get_SOAP_response_paydirect(MERCHANT_ID, p_id, PAYDIRECT_HOST, PAYDIRECT_URL, True)
+        doc=parseString(result_xml)
+        self.assertEqual(doc.getElementsByTagName('PaymentStatus'), [])
+
+    @external_test_2
+    def test_paydirect(self):
+        # Manager can access InterswitchForm
+        self.browser.getLink("Pay via Interswitch PAYDirect").click()
+        self.assertMatches('...Total Amount Authorized:...',
+                           self.browser.contents)
+        self.assertEqual(self.student.current_mode, 'ug_ft')
+        # Create school fee ticket for returning students. Payment is made
+        # for next session.
+        current_payment_key = self.student['payments'].keys()[0]
+        self.certificate.study_mode = u'ug_pt'
+        IWorkflowState(self.student).setState('returning')
+        configuration = createObject('waeup.SessionConfiguration')
+        configuration.academic_session = 2005
+        self.app['configuration'].addSessionConfiguration(configuration)
+        self.browser.open(self.payments_path + '/addop')
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl("Create ticket").click()
+        self.browser.open(self.payments_path)
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[1]
+        self.assertEqual(self.student['payments'][value].provider_amt, 0.0)
+        self.assertEqual(self.student['payments'][value].gateway_amt, 0.0)
+        self.browser.getLink(value).click()
+        self.browser.getLink("Pay via Interswitch PAYDirect").click()
+        # Split amounts have been set.
+        self.assertEqual(self.student['payments'][value].provider_amt, 0.0)
+        self.assertEqual(self.student['payments'][value].gateway_amt, 300.0)
+        # Reference number has not yet been saved but can be seen in browser
+        self.assertEqual(self.student['payments'][value].r_pay_reference, None)
+        ref_number = '%s%s' % (MERCHANT_ID,value[1:])
+        self.assertTrue(ref_number in self.browser.contents)
+        self.browser.getControl("Requery").click()
+        self.assertTrue('Your payment %s was not found.' % value in self.browser.contents)
+        self.create_booking(value)
+        self.browser.getLink("Pay via Interswitch PAYDirect").click()
+        self.browser.getControl("Requery").click()
+        self.assertTrue('pending' in self.browser.contents)
+        # Reference number has still not been saved because the element was empty
+        self.assertEqual(self.student['payments'][value].r_pay_reference, None)
+        # Here we have to stop testing because we cannot create completed bookings
+
+        # Students can download reference number slip
+        self.browser.getLink("Pay via Interswitch PAYDirect").click()
+        self.browser.getLink("Download reference number slip").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        path = os.path.join(samples_dir(), 'refnumberslip.pdf')
+        open(path, 'wb').write(self.browser.contents)
+        print "Sample PDF refnumberslip.pdf written to %s" % path
+
 class InterswitchTestsApplicants(ApplicantsFullSetup):
     """Tests for the Interswitch payment gateway.
@@ -241,5 +365,5 @@
             self.browser.contents)
         # Manager can access InterswitchForm
-        self.browser.getLink("Pay via Interswitch", index=0).click()
+        self.browser.getLink("Pay via Interswitch CollegePAY", index=0).click()
         self.assertMatches('...Total Amount Authorized:...',
                            self.browser.contents)
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/remita/studentsbrowser.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/remita/studentsbrowser.py	(revision 16483)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/remita/studentsbrowser.py	(revision 16484)
@@ -55,5 +55,5 @@
 
 class RemitaActionButtonStudent(ManageActionButton):
-    grok.order(1)
+    grok.order(5)
     grok.context(INigeriaOnlinePayment)
     grok.view(OPDPStudent)
@@ -73,5 +73,5 @@
 
 class RemitaRequestPaymentStatusActionButtonStudent(ManageActionButton):
-    grok.order(2)
+    grok.order(6)
     grok.context(INigeriaOnlinePayment)
     grok.view(OPDPStudent)
