Index: /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/applicantsbrowser.py
===================================================================
--- /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/applicantsbrowser.py	(revision 15588)
+++ /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/applicantsbrowser.py	(revision 15589)
@@ -26,5 +26,7 @@
 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)
+from kofacustom.nigeria.etranzact.helpers import (
+    write_payments_log, process_response, query_history)
+from kofacustom.nigeria.applicants.browser import NigeriaOnlinePaymentDisplayFormPage
 from kofacustom.nigeria.payments.interfaces import INigeriaOnlinePayment
 from kofacustom.nigeria.applicants.interfaces import INigeriaApplicantOnlinePayment
@@ -42,6 +44,4 @@
     except KeyError:
         return False
-
-# Buttons
 
 class EtranzactActionButtonApplicant(ManageActionButton):
@@ -61,6 +61,4 @@
             return ''
         return self.view.url(self.view.context, self.target)
-
-# Forwarding pages
 
 class EtranzactPageApplicant(KofaPage):
@@ -96,5 +94,5 @@
         if time_delta.days > 7:
             return _("This payment ticket is too old. Please create a new ticket.")
-        self.responseurl = self.url(self.context, 'query_etranzact')
+        self.responseurl = self.url(self.context, 'receive_etranzact')
         # Already now it becomes a eTranzact payment
         self.context.r_company = u'etranzact'
@@ -116,2 +114,64 @@
             return
         return
+
+class EtranzactReceiveResponseApplicant(NigeriaOnlinePaymentDisplayFormPage):
+    """ View that receives the response from eTrantact payment gateway.
+    """
+    #grok.context(INigeriaApplicantOnlinePayment)
+    grok.name('receive_etranzact')
+    #grok.require('waeup.payApplicant')
+    #label = _('Response from eTranzact')
+
+    secret_key = SECRET_KEY
+    terminal_id = TERMINAL_ID
+
+    def update(self):
+        super(EtranzactReceiveResponseApplicant, self).update()
+        if not module_activated(self.context.__parent__.__parent__.year):
+            return
+        applicant = self.context.__parent__
+        form = self.request.form
+        verify = False
+        if self.context.p_state == 'paid':
+            verify = True
+        success, msg, log = process_response(self.context, form, self, verify)
+        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
+
+class EtranzactRequeryHistoryApplicant(KofaPage):
+    """ Request POST redirect from eTranzact
+    """
+
+    grok.context(INigeriaApplicantOnlinePayment)
+    grok.name('requery_history')
+    grok.require('waeup.payApplicant')
+    grok.template('requery_etranzact')
+    label = _('Requery eTranzact history')
+    submit_button = _('Requery 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/query.jsp'
+        return 'http://' + self.host + '/webconnect/v3/query.jsp'
+
+    def update(self):
+        if not module_activated(self.context.__parent__.__parent__.year):
+            return
+        self.responseurl = self.url(self.context, 'receive_etranzact')
+        self.transaction_id = self.context.p_id
+        return
Index: /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/browser_templates/requery_etranzact.pt
===================================================================
--- /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/browser_templates/requery_etranzact.pt	(revision 15589)
+++ /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/browser_templates/requery_etranzact.pt	(revision 15589)
@@ -0,0 +1,23 @@
+<form i18n:domain="kofacustom.nigeria"
+  tal:attributes="action view/action" method="POST">
+
+  <input type=hidden name="TERMINAL_ID" tal:attributes="value view/terminal_id">
+  <input type=hidden name="RESPONSE_URL" tal:attributes="value view/responseurl">
+  <input type=hidden name="TRANSACTION_ID" tal:attributes="value view/transaction_id">
+
+  <table class="form-table" i18n:domain="waeup.kofa">
+    <tbody>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          Transaction Id:</td>
+        <td>
+          <span tal:replace="view/context/p_id">TRANSID</span>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+  <br />
+  <input type="submit" class="btn btn-primary" tal:attributes="value view/submit_button"/>
+  <br /><br />
+  <img src="/static/img/etzLogo.png" /><img src="/static/img/webconnectLogo.png" />
+</form>
Index: /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/helpers.py
===================================================================
--- /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/helpers.py	(revision 15588)
+++ /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/helpers.py	(revision 15589)
@@ -37,38 +37,5 @@
         payment.thirdparty_amt))
 
-def get_caller_response(host, https, terminal_id, transaction_id, responseurl,
-                        amount_auth, email, phone, display_fullname, hashvalue,
-                        logo_url):
-    headers={"Content-type": "application/x-www-form-urlencoded",
-             "Accept": "text/plain"}
-    url = "/webconnect/v3/caller.jsp"
-    if https:
-        h = httplib.HTTPSConnection(host)
-    else:
-        h = httplib.HTTPConnection(host)
-    args = {'TERMINAL_ID': terminal_id,
-            'TRANSACTION_ID': transaction_id,
-            'RESPONSE_URL': responseurl,
-            'AMOUNT': amount_auth,
-            'EMAIL': email,
-            'PHONENO': phone,
-            'FULL_NAME': display_fullname,
-            'CURRENCY_CODE': 'NGN',
-            'CHECKSUM': hashvalue,
-            'LOGO_URL': logo_url,
-            }
-    h.request('POST', url, urlencode(args), headers)
-    return
-    response = h.getresponse()
-    if response.status!=200:
-        return 'Connection error (%s, %s)' % (response.status, response.reason)
-    resp = response.read()
-    return resp
-    #resp = resp.replace('\r\n','')
-    ## eTranzact sends strange HTML tags which must be removed.
-    #resp = re.sub("<.*?>", "", resp)
-    #return resp
-
-def get_query_response(host, terminal_id, transaction_id, https):
+def query_history(host, terminal_id, transaction_id, https, responseurl):
     headers={"Content-type": "application/x-www-form-urlencoded",
              "Accept": "text/plain"}
@@ -80,5 +47,6 @@
     args = {'TERMINAL_ID': terminal_id,
             'TRANSACTION_ID': transaction_id,
-            'RESPONSE_URL': 'https://www.waeup.org'}
+            }
+    #args['RESPONSE_URL'] = responseurl
     h.request('POST', url, urlencode(args), headers)
     response = h.getresponse()
@@ -90,2 +58,103 @@
     resp = re.sub("<.*?>", "", resp)
     return resp
+
+ # A sample caller response sent to the RESPONSE_URL
+
+ # http://salsa:8080/app/applicants/cbt2015/449072/p5679522929425/receive_etranzact?
+ # AMOUNT=3333.0&
+ # DESCRIPTION=&
+ # CHECKSUM=8aab3904652f8ba69ebed42d3bae80a2&
+ # EMAIL=aa%40aa.de&
+ # SUCCESS=C&
+ # MESSAGE=Cancel&
+ # LOGO_URL=https%3A%2F%2Fiuokada.waeup.org%2Fstatic_custom%2Fiou_logo.png&
+ # RESPONSE_URL=http%3A%2F%2Fsalsa%3A8080%2Fapp%2Fapplicants%2Fcbt2015%2F449072%2Fp5679522929425%2Freceive_etranzact&
+ # CURRENCY_CODE=NGN&
+ # TERMINAL_ID=0000000001&
+ # TRANSACTION_ID=p5679522929425&
+ # MERCHANT_CODE=0339990001&
+ # RESPONSE_CODE=C&
+ # FINAL_CHECKSUM=E524590DBFAB719EEE428C778FFF1650&
+ # STATUS_REASON=Cancel&
+ # TRANS_NUM=01ESA20190913062149AHGUHQ&
+ # CARD_NO=null&
+ # CARD_TYPE=null
+
+  # A sample query response sent to the RESPONSE_URL
+
+ # http://salsa:8080/app/applicants/cbt2015/449072/p5686487280654/receive_etranzact?
+ # LOGO_URL=https%3A%2F%2Fiuokada.waeup.org%2Fstatic_custom%2Fiou_logo.png&
+ # RESPONSE_URL=http%3A%2F%2Fsalsa%3A8080%2Fapp%2Fapplicants%2Fcbt2015%2F449072%2Fp5686487280654%2Freceive_etranzact&
+ # CURRENCY_CODE=NGN&
+ # TERMINAL_ID=0000000001&
+ # TRANSACTION_ID=p5686487280654&
+ # AMOUNT=3333.0&
+ # DESCRIPTION=&
+ # CHECKSUM=3886118fcd91a376cc95c48c94dc499a&
+ # MERCHANT_CODE=0339990001&
+ # EMAIL=aa%40aa.de&
+ # SUCCESS=0&
+ # FINAL_CHECKSUM=EE105B703F84B1D67D0A4234622C03E8&
+ # STATUS_REASON=Approved&
+ # TRANS_NUM=01ESA20190916164636L2UTU7&
+ # CARD_NO=506066XXXXXXXXX6666&
+ # CARD_TYPE=Verve
+
+def process_response(payment, form, view, verify):
+    success = form.get('RESPONSE_CODE', None)
+    if not success:
+        msg = _('No valid response from eTranzact.')
+        log = 'No valid response from eTranzact for payment %s' % payment.p_id
+        payment.p_state = 'failed'
+        notify(grok.ObjectModifiedEvent(payment))
+        return False, msg, log
+    # Compute final checksum
+    transaction_id = payment.p_id
+    amount = "%.1f" % payment.amount_auth
+    responseurl = view.url(payment, 'receive_etranzact')
+    hashargs = 	success + amount + view.terminal_id + transaction_id \
+        + responseurl + view.secret_key
+    final_checksum = hashlib.md5(hashargs).hexdigest().upper()
+    if form.get('FINAL_CHECKSUM', None) != final_checksum:
+        msg = _('Wrong checksum.')
+        log = 'wrong checksum for %s payment %s: %s' % (
+            payment.p_category, payment.p_id, str(form))
+        return False, msg, log
+    payment.r_code = form.get('RESPONSE_CODE', None)
+    payment.r_desc = form.get('STATUS_REASON', None) # MESSAGE also available
+    payment.r_amount_approved = float(form.get('AMOUNT', None))
+    payment.r_pay_reference = form.get('TRANS_NUM', None)
+    payment.r_card_num = "%s %s" % (form.get('CARD_TYPE', None),
+                                    form.get('CARD_NO', None))
+    payment.r_company = u'etranzact'
+    if payment.r_code != '0':
+        msg = _('Unsuccessful response: ${a}', mapping = {'a': payment.r_desc})
+        log = 'unsuccessful response 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/10.0, 0) != round(
+        payment.amount_auth/10.0, 0):
+        msg = _('Response amount does not match.')
+        log = 'wrong response for %s payment %s: %s' % (
+            payment.p_category, payment.p_id, str(form))
+        payment.p_state = 'failed'
+        notify(grok.ObjectModifiedEvent(payment))
+        return False, msg, log
+    transaction_id = form.get('TRANSACTION_ID', None)
+    if transaction_id != payment.p_id:
+        msg = _('Response transaction id does not match.')
+        log = 'wrong response for %s payment %s: %s' % (
+            payment.p_category, payment.p_id, str(form))
+        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 response received')
+    log = 'valid response for %s payment %s: %s' % (
+        payment.p_category, payment.p_id, str(form))
+    notify(grok.ObjectModifiedEvent(payment))
+    return True, msg, log
Index: /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/tests.py
===================================================================
--- /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/tests.py	(revision 15588)
+++ /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/tests.py	(revision 15589)
@@ -21,4 +21,6 @@
 import json
 import hashlib
+import httplib
+from urllib import urlencode
 from datetime import datetime, timedelta, date
 from zope.component import createObject, getUtility
@@ -31,5 +33,5 @@
 from kofacustom.nigeria.testing import FunctionalLayer
 from kofacustom.nigeria.etranzact.helpers import (
-    get_query_response, get_caller_response)
+    query_history,)
 
 #from kofacustom.nigeria.etranzact.helpers import (query_etranzact)
@@ -57,30 +59,60 @@
     return func
 
+def post_caller(host, https, terminal_id, transaction_id, responseurl,
+                        amount_auth, email, phone, display_fullname, hashvalue,
+                        logo_url):
+    headers={"Content-type": "application/x-www-form-urlencoded",
+             "Accept": "text/plain"}
+    url = "/webconnect/v3/caller.jsp"
+    if https:
+        h = httplib.HTTPSConnection(host)
+    else:
+        h = httplib.HTTPConnection(host)
+    args = {'TERMINAL_ID': terminal_id,
+            'TRANSACTION_ID': transaction_id,
+            'RESPONSE_URL': responseurl,
+            'AMOUNT': amount_auth,
+            'EMAIL': email,
+            'PHONENO': phone,
+            'FULL_NAME': display_fullname,
+            'CURRENCY_CODE': 'NGN',
+            'CHECKSUM': hashvalue,
+            'LOGO_URL': logo_url,
+            }
+    h.request('POST', url, urlencode(args), headers)
+    return
+    response = h.getresponse()
+    if response.status!=200:
+        return 'Connection error (%s, %s)' % (response.status, response.reason)
+    resp = response.read()
+    return resp
+
+def create_transaction(transaction_id):
+    responseurl = 'http://xxxx'
+    amount = '4444.0'
+    email = 'aa@aa.ng'
+    phone = '12324'
+    display_fullname = 'Tester'
+    logo_url = 'http://xxxx'
+    hashargs = 	amount + TERMINAL_ID + transaction_id \
+        + responseurl + 'DEMO_KEY'
+    hashvalue = hashlib.md5(hashargs).hexdigest()
+    response = post_caller(HOST, HTTPS, TERMINAL_ID,
+                         transaction_id, responseurl,
+                         amount, email, phone,
+                         display_fullname, hashvalue,
+                         logo_url)
+    return response
+
+
 class HelperTests(unittest.TestCase):
 
     terminal_id = TERMINAL_ID
 
-    def _create_transaction(self, transaction_id):
-        responseurl = 'http://xxxx'
-        amount = '4444.0'
-        email = 'aa@aa.ng'
-        phone = '12324'
-        display_fullname = 'Tester'
-        logo_url = 'http://xxxx'
-        hashargs = 	amount + self.terminal_id + transaction_id \
-            + responseurl + 'DEMO_KEY'
-        hashvalue = hashlib.md5(hashargs).hexdigest()
-        response = get_caller_response(HOST, HTTPS, self.terminal_id,
-                                       transaction_id, responseurl,
-                                       amount, email, phone,
-                                       display_fullname, hashvalue,
-                                       logo_url)
-        return response
-
     @external_test
-    def test_query_etranzact(self):
+    def test_query_history(self):
         transaction_id = str(random.randint(100000000, 999999999))
-        response = get_query_response(HOST, self.terminal_id,
-                                transaction_id, HTTPS)
+        response = query_history(HOST, self.terminal_id,
+                                transaction_id, HTTPS, 'http://xxxxx')
         self.assertEqual(
             response,
@@ -88,13 +120,14 @@
             % transaction_id)
         # Okay, let's create a transaction
-        caller_response = self._create_transaction(transaction_id)
+        caller_response = create_transaction(transaction_id)
         self.assertEqual(caller_response, None)
         # It seems that the transaction has been created but we don't get a
         # useful response
-        query_response = get_query_response(HOST, self.terminal_id,
-                                transaction_id, HTTPS)
+        query_response = query_history(HOST, self.terminal_id,
+                                transaction_id, HTTPS, 'http://xxxxx')
         self.assertEqual(query_response, '')
         # The same, an 'empty' response obviously means that the transaction
-        # was found. The result was probably sent to the response_url.
+        # was found. The result was  sent to the response_url (see below).
+        # We need browser tests.
         return
 
@@ -126,9 +159,23 @@
 
     @external_test
-    def test_applicant_etranzact_form(self):
-        # Manager can access Etranzact form
+    def test_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.
+        # Means of testing end here. We have to manually
+        # create an eTranzact transaction.
+        logo_url = 'http://xxxx'
+        responseurl = self.payment_url + '/receive_etranzact'
+        amount = "%.1f" % self.payment.amount_auth
+        customer = self.applicant
+        hashargs = 	amount + TERMINAL_ID + self.payment.p_id \
+            + responseurl + 'DEMO_KEY'
+        hashvalue = hashlib.md5(hashargs).hexdigest()
+        post_caller(HOST, HTTPS, TERMINAL_ID,
+                   self.payment.p_id, responseurl,
+                   self.payment.amount_auth, customer.email,
+                   customer.phone,
+                   customer.display_fullname, hashvalue,
+                   logo_url)
         return
 
