Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interfaces.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interfaces.py	(revision 17214)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interfaces.py	(revision 17215)
@@ -141,4 +141,9 @@
         )
 
+    interswitch_webcheckout_enabled = schema.Bool(
+        title = _(u'Interswitch WebCheckout integration enabled'),
+        default = False,
+        )
+
     etranzact_webconnect_enabled = schema.Bool(
         title = _(u'Etranzact Webconnect integration enabled'),
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/browser_templates/applicant_goto_webcheckout.pt
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/browser_templates/applicant_goto_webcheckout.pt	(revision 17215)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/browser_templates/applicant_goto_webcheckout.pt	(revision 17215)
@@ -0,0 +1,74 @@
+<form i18n:domain="kofacustom.nigeria"
+  tal:attributes="action view/action" method="POST">
+
+  <p i18n:translate="">
+    These data will be submitted to the Interswitch Web Checkout payment gateway,
+    please check:
+  </p>
+
+  <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">
+          Transaction Reference:</td>
+        <td>
+          <span tal:replace="view/context/p_id">TXNREF</span>
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          Customer Id:</td>
+        <td>
+          <span tal:replace="view/applicant/applicant_id">CUSTID</span>
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          Customer Name:</td>
+        <td>
+          <span tal:replace="view/applicant/display_fullname">CUSTNAME</span>
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          Customer Email:</td>
+        <td>
+          <span tal:replace="view/applicant/email">EMAIL</span>
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          Payment Category:</td>
+        <td>
+          <span tal:replace="view/category">PAYMENTCATEGORY</span>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+
+  <input type="hidden" name="merchant_code" tal:attributes="value view/merchant_code" />
+  <input type="hidden" name="currency" tal:attributes="value view/currency" />
+  <input type="hidden" name="pay_item_id" tal:attributes="value view/pay_item_id" />
+  <input type="hidden" name="site_redirect_url" tal:attributes="value view/site_redirect_url" />
+  <input type="hidden" name="amount" tal:attributes="value view/amount_auth" />
+  <input type="hidden" name="txn_ref" tal:attributes="value view/context/p_id" />
+  <input type="hidden" name="pay_item_name" tal:attributes="value view/category" />
+  <input type="hidden" name="cust_id" tal:attributes="value view/applicant/applicant_id" />
+  <input type="hidden" name="cust_name" tal:attributes="value view/applicant/display_fullname" />
+  <input type="hidden" name="cust_email" tal:attributes="value view/applicant/email" />
+
+  <br />
+  <input type="submit" class="btn btn-primary" tal:attributes="value view/submit_button"/>
+
+  <br /><br />
+
+  <img src="/static/img/interswitch_logos.png" />
+
+</form>
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/browser_templates/student_goto_webcheckout.pt
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/browser_templates/student_goto_webcheckout.pt	(revision 17215)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/browser_templates/student_goto_webcheckout.pt	(revision 17215)
@@ -0,0 +1,75 @@
+<form i18n:domain="kofacustom.nigeria"
+  tal:attributes="action view/action" method="POST">
+
+  <p i18n:translate="">
+    These data will be submitted to the WebCheckout payment gateway.
+    Gateway and portal provider fees might have been added to the total amount,
+    please check:
+  </p>
+
+  <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">
+          Transaction Reference:</td>
+        <td>
+          <span tal:replace="view/context/p_id">TXNREF</span>
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          Customer Id:</td>
+        <td>
+          <span tal:replace="view/student/student_id">CUSTID</span>
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          Customer Name:</td>
+        <td>
+          <span tal:replace="view/student/display_fullname">CUSTNAME</span>
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          Customer Email:</td>
+        <td>
+          <span tal:replace="view/student/email">EMAIL</span>
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          Payment Category:</td>
+        <td>
+          <span tal:replace="view/category">PAYMENTCATEGORY</span>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+
+  <input type="hidden" name="merchant_code" tal:attributes="value view/product_id" />
+  <input type="hidden" name="currency" tal:attributes="value view/currency" />
+  <input type="hidden" name="pay_item_id" tal:attributes="value view/pay_item_id" />
+  <input type="hidden" name="site_redirect_url" tal:attributes="value view/site_redirect_url" />
+  <input type="hidden" name="amount" tal:attributes="value view/amount_auth" />
+  <input type="hidden" name="txn_ref" tal:attributes="value view/context/p_id" />
+  <input type="hidden" name="pay_item_name" tal:attributes="value view/category" />
+  <input type="hidden" name="cust_id" tal:attributes="value view/student/student_id" />
+  <input type="hidden" name="cust_name" tal:attributes="value view/student/display_fullname" />
+  <input type="hidden" name="cust_email" tal:attributes="value view/student/email" />
+
+  <br />
+  <input type="submit" class="btn btn-primary" tal:attributes="value view/submit_button"/>
+
+  <br /><br />
+
+  <img src="/static/img/interswitch_logos.png" />
+
+</form>
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/helpers.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/helpers.py	(revision 17214)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/helpers.py	(revision 17215)
@@ -379,2 +379,89 @@
     notify(grok.ObjectModifiedEvent(payment))
     return True, msg, log
+
+# Web checkout helper functions
+
+def get_JSON_webcheckout_response(merchant_code, transref, host, url, https, amount):
+    headers={
+        'Content-Type':'text/xml; charset=utf-8',
+    }
+    if https:
+        h = httplib.HTTPSConnection(host)
+    else:
+        h = httplib.HTTPConnection(host)
+    amount = int(100 * amount)
+    args = {'merchantcode': merchant_code,
+            'transactionreference': transref,
+            'amount': amount}
+    url = '%s?' % url + urlencode(args)
+    try:
+        h.request("GET", url, headers=headers)
+    except SSLError:
+        return {'error': 'SSL handshake error'}
+    response = h.getresponse()
+    if response.status!=200:
+        return {'error': 'Connection error (%s, %s)' % (response.status, response.reason)}
+    jsonout = response.read()
+    parsed_json = json.loads(jsonout)
+    return parsed_json
+
+def confirm_transaction(payment, merchant_code, host, url, https=True):
+    jr = get_JSON_webcheckout_response(merchant_code, payment.p_id, host, url,
+                           https, payment.amount_auth)
+    error = jr.get('error')
+    if error:
+        msg = log = error
+        return False, msg, log
+
+    # A typical JSON response ?????????????????
+
+    #  {u'SplitAccounts': [],
+    #  u'MerchantReference':u'p5918633006916',
+    #  u'TransactionDate':u'2020-06-11T09:17:37',
+    #  u'ResponseDescription':u'Customer Cancellation',
+    #  u'Amount': 89525000,
+    #  u'CardNumber': u'',
+    #  u'ResponseCode': u'Z6',
+    #  u'BankCode': u''}
+
+    if not 'ResponseCode' in jr.keys() \
+        or not 'ResponseDescription' in jr.keys() \
+        or not 'Amount' in jr.keys():
+        msg = _('Invalid callback: ${a}', mapping = {'a': str(jr)})
+        log = 'invalid callback for payment %s: %s' % (payment.p_id, str(jr))
+        return False, msg, log
+    payment.r_code = jr['ResponseCode']
+    payment.r_desc = jr['ResponseDescription']
+    payment.r_amount_approved = jr['Amount'] / 100.0
+    payment.r_card_num = jr.get('CardNumber', u'')
+    payment.r_pay_reference = jr.get('PaymentReference', u'')
+    #payment.r_company = u'interswitch'
+    if payment.r_code != '00':
+        msg = _('Unsuccessful callback: ${a}', mapping = {'a': payment.r_desc})
+        log = 'unsuccessful callback for %s payment %s: %s' % (
+            payment.p_category, payment.p_id, payment.r_desc)
+        payment.p_state = 'failed'
+        notify(grok.ObjectModifiedEvent(payment))
+        return False, msg, log
+    if round(payment.r_amount_approved, 0) != round(payment.amount_auth, 0):
+        msg = _('Callback amount does not match.')
+        log = 'wrong callback for %s payment %s: %s' % (
+            payment.p_category, payment.p_id, str(jr))
+        payment.p_state = 'failed'
+        notify(grok.ObjectModifiedEvent(payment))
+        return False, msg, log
+    if jr['MerchantReference'] != payment.p_id:
+        msg = _('Callback transaction id does not match.')
+        log = 'wrong callback for %s payment %s: %s' % (
+            payment.p_category, payment.p_id, str(jr))
+        payment.p_state = 'failed'
+        notify(grok.ObjectModifiedEvent(payment))
+        return False, msg, log
+    payment.p_state = 'paid'
+    if not verify:
+        payment.payment_date = datetime.utcnow()
+    msg = _('Successful callback received')
+    log = 'valid callback for %s payment %s: %s' % (
+        payment.p_category, payment.p_id, str(jr))
+    notify(grok.ObjectModifiedEvent(payment))
+    return True, msg, log
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/webcheckoutbrowser.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/webcheckoutbrowser.py	(revision 17215)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interswitch/webcheckoutbrowser.py	(revision 17215)
@@ -0,0 +1,337 @@
+## $Id: webcheckoutbrowser.py 16587 2021-08-31 06:28:35Z henrik $
+##
+## 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 xml.dom.minidom import parseString
+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.payments.interfaces import IPayer
+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 waeup.kofa.applicants.browser import ApplicantBaseDisplayFormPage
+from kofacustom.nigeria.interswitch.helpers import (
+    write_payments_log, confirm_transaction)
+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 (
+    MERCHANT_ID)
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+GATEWAY_AMT = 0.0
+
+grok.templatedir('browser_templates')
+
+def webcheckout_module_activated(session, payment):
+    if payment.r_company and payment.r_company != 'interswitch':
+        return False
+    try:
+        return getattr(grok.getSite()['configuration'][str(session)],
+            'interswitch_webcheckout_enabled', False)
+    except KeyError:
+        session = datetime.now().year
+        try:
+            return getattr(grok.getSite()['configuration'][str(session)],
+                'interswitch_webcheckout_enabled', False)
+        except KeyError:
+            return False
+
+# Buttons
+
+class WebcheckoutActionButtonStudent(ManageActionButton):
+    grok.context(INigeriaOnlinePayment)
+    grok.view(OPDPStudent)
+    grok.require('waeup.payStudent')
+    grok.order(10)
+    icon = 'actionicon_pay.png'
+    text = _('Pay via Interswitch WebCheckout')
+    target = 'webcheckout'
+
+    @property
+    def target_url(self):
+        if not webcheckout_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 WebcheckoutActionButtonApplicant(WebcheckoutActionButtonStudent):
+    grok.view(OPDPApplicant)
+    grok.require('waeup.payApplicant')
+
+    @property
+    def target_url(self):
+        if not webcheckout_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 WebCheckoutConfirmTransactionActionButtonStudent(ManageActionButton):
+    grok.order(11)
+    grok.context(INigeriaOnlinePayment)
+    grok.view(OPDPStudent)
+    grok.require('waeup.payStudent')
+    icon = 'actionicon_call.png'
+    text = _('Confirm WebCheckout Transaction')
+    target = 'confirm_transaction'
+
+    @property
+    def target_url(self):
+        if not webcheckout_module_activated(
+            self.context.student.current_session, self.context):
+            return ''
+        if self.context.amount_auth == 0:
+            return ''
+        if self.context.p_state in ('paid', 'waived', 'scholarship'):
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class WebCheckoutConfirmTransactionActionButtonApplicant(
+    WebCheckoutConfirmTransactionActionButtonStudent):
+    grok.view(OPDPApplicant)
+    grok.require('waeup.payApplicant')
+
+    @property
+    def target_url(self):
+        if not webcheckout_module_activated(
+            self.context.__parent__.__parent__.year, self.context):
+            return ''
+        if self.context.amount_auth == 0:
+            return ''
+        if self.context.p_state in ('paid', 'waived', 'scholarship'):
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+# Forwarding pages
+
+class WebCheckoutPageStudent(KofaPage):
+    """ View which sends a POST request to the Interswitch
+    WebCheckout payment gateway.
+    """
+    grok.context(INigeriaOnlinePayment)
+    grok.name('webcheckout')
+    grok.template('student_goto_webcheckout')
+    grok.require('waeup.payStudent')
+    label = _('Submit data to Interswitch')
+    submit_button = _('Submit')
+
+    action = None
+    currency = None
+    pay_item_id = None
+    merchant_code = None
+    gateway_amt = GATEWAY_AMT
+
+    def init_update(self):
+        if self.context.p_state == 'paid':
+            return _("Payment ticket can't be re-sent to WebCheckout.")
+        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.")
+        if self.context.r_company and self.context.r_company != 'interswitch':
+            return _("Payment ticket has been used for another payment gateway.")
+        student = self.context.student
+        kofa_utils = getUtility(IKofaUtils)
+        student_utils = getUtility(IStudentsUtils)
+        if student_utils.samePaymentMade(student, self.context.p_category,
+            self.context.p_item, self.context.p_session):
+            return _("This type of payment has already been made.")
+        xmldict = {}
+        self.category = self.context.category
+        tz = kofa_utils.tzinfo
+        self.local_date_time = to_timezone(
+            self.context.creation_date, tz).strftime("%Y-%m-%d %H:%M:%S %Z")
+        self.site_redirect_url = self.url(self.context, 'confirm_transaction')
+        self.student = student
+        return
+
+    def update(self):
+        if not webcheckout_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'))
+        # 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'
+        self.amount_auth = int(100 * self.context.amount_auth)
+        return
+
+class WebCheckoutPageApplicant(KofaPage):
+    """ View which sends a POST request to the Interswitch
+    WebCheckout payment gateway.
+    """
+    grok.context(INigeriaApplicantOnlinePayment)
+    grok.require('waeup.payApplicant')
+    grok.template('applicant_goto_webcheckout')
+    grok.name('webcheckout')
+    label = _('Submit data to Interswitch')
+    submit_button = _('Submit')
+
+    action = 'https://newwebpay.interswitchng.com/collections/w/pay'
+    currency = '566'
+    pay_item_id = 'Default_Payable_MX76823'
+    merchant_code = 'MX76823'
+    gateway_amt = GATEWAY_AMT
+
+    def init_update(self):
+        if self.context.p_state != 'unpaid':
+            return _("Payment ticket can't be re-sent to WebCheckout.")
+        if self.context.__parent__.__parent__.expired \
+            and self.context.__parent__.__parent__.strict_deadline:
+            return _("Payment ticket can't be sent to WebCheckout. "
+                     "Application period has expired.")
+        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.applicant = self.context.__parent__
+        self.category = self.context.category
+        tz = getUtility(IKofaUtils).tzinfo
+        self.local_date_time = to_timezone(
+            self.context.creation_date, tz).strftime("%Y-%m-%d %H:%M:%S %Z")
+        self.site_redirect_url = self.url(self.context, 'confirm_transaction')
+        return
+
+    def update(self):
+        if not webcheckout_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'))
+        # 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'
+        self.amount_auth = int(100 * self.context.amount_auth)
+        return
+
+# Webservice request views
+
+class WebCheckoutConfirmTransactionPageApplicant(UtilityView, grok.View):
+    """ Request webservice view for the WebCheckout gateway
+    """
+    grok.context(INigeriaApplicantOnlinePayment)
+    grok.name('confirm_transaction')
+    grok.require('waeup.payApplicant')
+
+    merchant_code = 'MX76823'
+    gateway_host = 'webpay.interswitchng.com'
+    gateway_url = '/collections/api/v1/gettransaction.json'
+    https = True
+
+    def update(self):
+        if not webcheckout_module_activated(
+            self.context.__parent__.__parent__.year, self.context):
+            return
+        if self.context.p_state == 'paid':
+            self.flash(_('This ticket has already been paid.'), type='danger')
+            return
+        applicant = self.context.__parent__
+        success, msg, log = confirm_transaction(
+            self.context,
+            self.merchant_code,
+            self.gateway_host,
+            self.gateway_url,
+            self.https)
+        applicant.writeLogMessage(self, log)
+        if not success:
+            self.flash(msg, type='danger')
+            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
+
+    def render(self):
+        self.redirect(self.url(self.context.__parent__, 'edit'))
+        return
+
+class WebCheckoutConfirmTransactionRequestPageStudent(UtilityView, grok.View):
+    """ Request webservice view for the WebCheckout gateway
+    """
+    grok.context(INigeriaStudentOnlinePayment)
+    grok.name('confirm_transaction')
+    grok.require('waeup.payStudent')
+
+    merchant_code = 'MX76823'
+    gateway_host = 'webpay.interswitchng.com'
+    gateway_url = '/collections/api/v1/gettransaction.json'
+    https = True
+
+    def update(self):
+        if not webcheckout_module_activated(
+            self.context.student.current_session, self.context):
+            return
+        if self.context.p_state in ('paid', 'waived', 'scholarship'):
+            self.flash(_('This ticket has already been paid.'), type='danger')
+            return
+        student = self.context.student
+        success, msg, log = confirm_transaction(
+            self.context,
+            self.merchant_code,
+            self.gateway_host,
+            self.gateway_url,
+            self.https)
+        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, '@@index'))
+        return
+
