Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/browser_templates/goto_credo.pt
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/browser_templates/goto_credo.pt	(revision 17730)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/browser_templates/goto_credo.pt	(revision 17730)
@@ -0,0 +1,61 @@
+<p i18n:translate="">
+  These data will be submitted to the Etranzact Credo payment gateway,
+  please check:
+</p>
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data">
+  <table class="form-table">
+    <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">TRANSID</span>
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          Firstname:</td>
+        <td>
+          <span tal:replace="view/customer/firstname">CUSTNAME</span>
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          Lastname:</td>
+        <td>
+          <span tal:replace="view/customer/lastname">CUSTNAME</span>
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          Email Address:</td>
+        <td>
+          <span tal:replace="view/customer/email">EMAIL</span>
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          Phone Number:</td>
+        <td>
+          <span tal:replace="view/customer/phone">PHONE</span>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+
+  <br />
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
+
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/credobrowser.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/credobrowser.py	(revision 17730)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/credobrowser.py	(revision 17730)
@@ -0,0 +1,300 @@
+## $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, KofaEditFormPage, action, NullValidator
+from waeup.kofa.browser.viewlets import ManageActionButton
+from kofacustom.nigeria.etranzact.helpers import (
+    write_payments_log,
+    initiate_payment,
+    query_credo_payment)
+from kofacustom.nigeria.students.browser import NigeriaOnlinePaymentDisplayFormPage as NOPDPStudent
+from kofacustom.nigeria.applicants.browser import NigeriaOnlinePaymentDisplayFormPage as NOPDPApplicant
+from kofacustom.nigeria.payments.interfaces import INigeriaOnlinePayment
+from kofacustom.nigeria.students.interfaces import INigeriaStudentOnlinePayment
+from kofacustom.nigeria.applicants.interfaces import INigeriaApplicantOnlinePayment
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+from kofacustom.nigeria.etranzact.tests import (
+    TERMINAL_ID, CREDO_HOST, HTTPS, SECRET_API_KEY, API_PUBLIC_KEY,
+    LOGO_URL, GATEWAY_AMT)
+
+grok.templatedir('browser_templates')
+
+def credo_module_activated(session, payment):
+    if payment.p_currency != 'NGN':
+        return False
+    if payment.r_company and payment.r_company != 'etranzact':
+        return False
+    try:
+        return getattr(grok.getSite()['configuration'][str(session)],
+            'etranzact_credo_enabled', False)
+    except KeyError:
+        session = datetime.now().year
+        try:
+            return getattr(grok.getSite()['configuration'][str(session)],
+                'etranzact_credo_enabled', False)
+        except KeyError:
+            return False
+
+class CredoActionButtonStudent(ManageActionButton):
+    grok.order(8)
+    grok.context(INigeriaOnlinePayment)
+    grok.view(NOPDPStudent)
+    grok.require('waeup.payStudent')
+    icon = 'actionicon_pay.png'
+    text = _('Pay via Etranzact Credo')
+    target = 'goto_credo'
+
+    @property
+    def target_url(self):
+        if not credo_module_activated(
+            self.context.student.current_session, self.context):
+            return ''
+        if self.context.p_state != 'unpaid':
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class CredoActionButtonApplicant(CredoActionButtonStudent):
+    grok.context(INigeriaOnlinePayment)
+    grok.require('waeup.payApplicant')
+    grok.view(NOPDPApplicant)
+
+    @property
+    def target_url(self):
+        if not credo_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 CredoVerifyActionButtonStudent(ManageActionButton):
+    grok.order(9)
+    grok.context(INigeriaOnlinePayment)
+    grok.view(NOPDPStudent)
+    grok.require('waeup.payStudent')
+    icon = 'actionicon_call.png'
+    text = _('Requery Etranzact Credo')
+    target = 'requery_credo'
+
+    @property
+    def target_url(self):
+        if not credo_module_activated(
+            self.context.student.current_session, self.context):
+            return ''
+        if self.context.p_state in ('paid', 'waived', 'scholarship', 'failed'):
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class CredoVerifyActionButtonApplicant(CredoVerifyActionButtonStudent):
+    grok.view(NOPDPApplicant)
+    grok.require('waeup.payApplicant')
+    icon = 'actionicon_call.png'
+    text = _('Requery Etranzact Credo')
+    target = 'requery_credo'
+
+    @property
+    def target_url(self):
+        if not credo_module_activated(
+            self.context.__parent__.__parent__.year, self.context):
+            return ''
+        if self.context.p_state in ('paid', 'waived', 'scholarship', 'failed'):
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class CredoPageStudent(KofaEditFormPage):
+    """ View which sends a POST request to the Credo payment gateway.
+    """
+    grok.context(INigeriaStudentOnlinePayment)
+    grok.name('goto_credo')
+    grok.template('goto_credo')
+    grok.require('waeup.payStudent')
+    label = _('Pay via Credo')
+    submit_button = _('Pay now')
+
+    host = CREDO_HOST
+    api_public_key = API_PUBLIC_KEY
+    gateway_amt = 0.0 # is being added by Etranzact
+
+    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.")
+        self.callbackUrl = self.url(self.context, 'requery_credo')
+        return
+
+    def update(self):
+        if not credo_module_activated(
+            self.context.student.current_session, self.context):
+            self.flash(_('Forbidden'), type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        # Already now it becomes an Etranzact 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'etranzact'
+        #self.amount = "%.1f" % (100.0 * self.context.amount_auth)
+        error = self.init_update()
+        self.customer = self.context.student
+        if error:
+            self.flash(error, type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        return
+
+    @action(_('Pay now'), validator=NullValidator, style='primary')
+    def pay(self, **data):
+        success, url_or_message = initiate_payment(
+            self.context, self.host, self.callbackUrl, self.api_public_key)
+        if success:
+            self.redirect(url_or_message, trusted=True)
+        else:
+            self.flash(url_or_message, type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+        return
+
+class CredoPageApplicant(CredoPageStudent):
+    """ View which sends a POST request to the Credo payment gateway.
+    """
+    grok.require('waeup.payApplicant')
+    grok.context(INigeriaApplicantOnlinePayment)
+
+    def update(self):
+        if not credo_module_activated(
+            self.context.__parent__.__parent__.year, self.context):
+            self.flash(_('Forbidden'), type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        # Already now it becomes an Etranzact 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'etranzact'
+        #self.amount = "%.1f" % (100.0 * self.context.amount_auth)
+        error = self.init_update()
+        self.customer = self.context.__parent__
+        if error:
+            self.flash(error, type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        return
+
+class CredoVerifyPaymentStatusPageStudent(UtilityView, grok.View):
+    """ Request webservice view for the Etranzact Credo gateway.
+    """
+    grok.context(INigeriaStudentOnlinePayment)
+    grok.name('requery_credo')
+    grok.require('waeup.payStudent')
+
+    host = CREDO_HOST
+    secret_api_key = SECRET_API_KEY
+
+    def update(self):
+        if not credo_module_activated(
+            self.context.student.current_session, self.context):
+            self.flash(_('Forbidden'), type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        if self.context.p_state in ('paid', 'waived', 'scholarship', 'failed'):
+            self.flash(_('This ticket has already been processed.'), type='danger')
+            return
+        student = self.context.student
+        #verify = False
+        success, msg, log = query_credo_payment(self.context, self.host, self.secret_api_key)
+        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
+
+
+class CredoVerifyPaymentStatusPageApplicant(CredoVerifyPaymentStatusPageStudent):
+    """ Request webservice view for the Etranzact Credo gateway.
+    """
+    grok.context(INigeriaApplicantOnlinePayment)
+    grok.name('requery_credo')
+    grok.require('waeup.payApplicant')
+
+    def update(self):
+        if not credo_module_activated(
+            self.context.__parent__.__parent__.year, self.context):
+            self.flash(_('Forbidden'), type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        if self.context.p_state in ('paid', 'waived', 'scholarship', 'failed'):
+            self.flash(_('This ticket has already been processed.'), type='danger')
+            return
+        applicant = self.context.__parent__
+        #verify = False
+        success, msg, log = query_credo_payment(self.context, self.host, self.secret_api_key)
+        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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/helpers.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/helpers.py	(revision 17642)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/helpers.py	(revision 17730)
@@ -21,4 +21,6 @@
 import re
 from datetime import datetime
+from ssl import SSLError
+from datetime import datetime
 from urllib import urlencode
 from urllib2 import urlopen
@@ -28,4 +30,5 @@
 import json
 from zope.event import notify
+from waeup.kofa.payments.interfaces import IPayer
 from waeup.kofa.utils.helpers import extract_formvars
 from kofacustom.nigeria.interfaces import MessageFactory as _
@@ -282,2 +285,142 @@
     payment.payment_date = datetime.utcnow()
     return True, msg, log
+
+
+# Etranzact Credo payments helper functions
+
+def get_JSON_response_initialize(payment, host, callbackUrl, public_api_key):
+    headers={
+        'Content-Type':'application/JSON',
+        'Authorization':public_api_key,
+    }
+    h = httplib.HTTPSConnection(host)
+
+    firstname = IPayer(payment).display_fullname.split()[0]
+    lastname = IPayer(payment).display_fullname.split()[-1]
+    email = IPayer(payment).email
+    phone = IPayer(payment).phone
+
+    args = {
+        'email': email,
+        'amount': 100 * payment.amount_auth,
+        'reference': payment.p_id,
+        'currency': 'NGN',
+        'callbackUrl': callbackUrl,
+        'customerFirstName': firstname,
+        'customerLastName': lastname,
+        'customerPhoneNumber': phone,
+        'bearer': '0',
+        }
+    try:
+        h.request('POST', '/transaction/initialize',
+                  body=json.dumps(args), headers=headers)
+    except SSLError:
+        return {'error': 'SSL handshake error'}
+    response = h.getresponse()
+    if response.status==400:
+        jsonout = response.read()
+        parsed_json = json.loads(jsonout)
+        return {'error': parsed_json['error']}
+    if response.status!=200:
+        return {'error': 'Connection error (%s, %s)' % (response.status, response.reason)}
+    jsonout = response.read()
+    parsed_json = json.loads(jsonout)
+
+    # A typical JSON result
+    
+    # {u'status': 200,
+    #  u'execTime': 5.109764,
+    #  u'message': u'Successfully processed',
+    #  u'data':
+    #     {u'credoReference': u'vsb200B5oM0521Mb00og',
+    #      u'reference': u'xyz',
+    #      u'authorizationUrl': u'https://pay.credodemo.com/vsb200B5oM0521Mb00og'
+    #      },
+    #  u'error': []
+    # }
+
+    return parsed_json
+
+
+def initiate_payment(payment, host, callbackUrl, public_api_key):
+    response = get_JSON_response_initialize(payment, host, callbackUrl, public_api_key)
+    if response['status'] == 200:
+        return True, response['data']['authorizationUrl']
+    return False, response['message']
+
+def get_JSON_response_verify(transref, host, secret_api_key):
+    headers={
+        'Content-Type':'text/xml; charset=utf-8',
+        'Authorization':secret_api_key,
+    }
+    h = httplib.HTTPSConnection(host)
+    url = '/transaction/%s/verify' % transref
+    try:
+        h.request("GET", url, headers=headers)
+    except SSLError:
+        return {'error': 'SSL handshake error'}
+    response = h.getresponse()
+    if response.status==404:
+        jsonout = response.read()
+        parsed_json = json.loads(jsonout)
+        return {'error': parsed_json['error']}
+    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 query_credo_payment(payment, host, secret_api_key):
+
+    jr = get_JSON_response_verify(payment.p_id, host, secret_api_key)
+    error = jr.get('error')
+    if error:
+        msg = log = error
+        return False, msg, log
+
+    # A typical JSON result
+
+    #{u'status': 200,
+    # u'execTime': 35.20671,
+    # u'message': u'Pending bank credit notification',
+    # u'data': {
+    #   u'status': 12,
+    #   u'businessCode': u'700607003930001',
+    #   u'channelId': 0,
+    #   u'businessRef': u'xyz',
+    #   u'businessName': u'Igbinedion University Okada',
+    #   u'settlementAmount': 100000.0,
+    #   u'currencyCode': u'NGN',
+    #   u'transFeeAmount': 1600.0,
+    #   u'transRef': u'vsb200B5oM0521Mb00og',
+    #   u'transAmount': 100000.0,
+    #   u'debitedAmount': 101600.0,
+    #   u'customerId': u'aa@aa.ng',
+    #   u'statusMessage': u'Pending bank credit notification',
+    #   u'metadata': []
+    #  },
+    # u'error': []
+    #}
+
+    payment.r_code = unicode(jr['data']['status'])
+    payment.r_desc = jr['data']['statusMessage']
+    payment.r_amount_approved = jr['data']['debitedAmount']
+    payment.r_pay_reference = jr['data'].get('transRef', u'')
+    payment.r_payment_link = u'n/a'
+    payment.r_card_num = u'n/a'
+    if payment.r_code != '0':
+        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
+    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, str(jr))
+    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 17642)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/etranzact/tests.py	(revision 17730)
@@ -33,5 +33,6 @@
 from kofacustom.nigeria.testing import FunctionalLayer
 from kofacustom.nigeria.etranzact.helpers import (
-    query_history, query_payoutlet, ERROR_PART1, ERROR_PART2)
+    query_history, query_payoutlet, ERROR_PART1, ERROR_PART2,
+    )
 
 #from kofacustom.nigeria.etranzact.helpers import (query_etranzact)
@@ -167,4 +168,6 @@
         return
 
+
+
 class EtranzactTestsApplicants(ApplicantsFullSetup):
     """Tests for the Etranzact payment gateway.
@@ -318,2 +321,152 @@
             % self.payment.p_id)
         self.assertEqual(self.browser.contents, ERROR_PART1 + 'PAYEE_ID already used' + ERROR_PART2)
+
+# Credo tests
+
+from kofacustom.nigeria.etranzact.helpers import (
+    query_credo_payment, get_JSON_response_initialize)
+
+SECRET_API_KEY = "0PRI0500LB11cBSSLcW0DcW1BcWgmf45"
+API_PUBLIC_KEY = "0PUB0500wuoOe9sdtSOLj5peqHKc8Q9W"
+EXTERNAL_TESTS_CREDO = True
+CREDO_HOST = "api.credodemo.com"
+
+def external_test_credo(func):
+    if not EXTERNAL_TESTS_CREDO:
+        myself = __file__
+        if myself.endswith('.pyc'):
+            myself = myself[:-1]
+        print "WARNING: external Credo tests are skipped!"
+        print "WARNING: edit %s to enable them." % myself
+        return
+    return func
+
+class CredoTestsStudents(StudentsFullSetup):
+
+    layer = FunctionalLayer
+
+    P_ID = 'p7120340727304' # works only if such a payment was made on the test platform
+
+    def setUp(self):
+        super(CredoTestsStudents, self).setUp()
+        self.app['configuration']['2004'].etranzact_credo_enabled = True
+        payment = createObject('waeup.StudentOnlinePayment')
+        payment.p_id = self.P_ID
+        payment.p_category = 'schoolfee'
+        payment.amount_auth = 100000.0
+        self.student['payments'][payment.p_id] = payment
+        self.payment2 = payment
+
+    @external_test_credo
+    def test_initialize(self):
+
+        public_api_key = API_PUBLIC_KEY
+        callbackUrl = 'aa.aa.aa'
+        response = get_JSON_response_initialize(self.payment2, CREDO_HOST, callbackUrl, public_api_key)
+
+        ## A typical response
+
+        #{u'status': 200,
+        # u'execTime': 5.109764,
+        # u'message': u'Successfully processed',
+        # u'data':
+        #    {u'credoReference': u'vsb200B5oM0521Mb00og',
+        #     u'reference': u'xyz',
+        #     u'authorizationUrl': u'https://pay.credodemo.com/vsb200B5oM0521Mb00og'
+        #     },
+        # u'error': []
+        #}
+
+        # Payment already exists
+        self.assertEqual(response, {'error': {u'reference': u'Reference must be unique'}})
+
+        #self.assertEqual(response['status'], 200)
+        #self.assertEqual(response['message'], 'Successfully processed')
+        #self.assertEqual(response['data']['reference'], self.P_ID)
+        #self.assertTrue('https://pay.credodemo.com/' in response['data']['authorizationUrl'])
+        return
+
+    @external_test_credo
+    def test_verify(self):
+        self.payment2.p_id = 'anything'
+        success, msg, log = query_credo_payment(self.payment2, CREDO_HOST, SECRET_API_KEY)
+        self.assertEqual(log, 'Transaction with ref# anything not found')
+        self.payment2.p_id = self.P_ID # manually paid
+        success, msg, log = query_credo_payment(self.payment2, CREDO_HOST, SECRET_API_KEY)
+        self.assertEqual(msg, 'Successful callback received')
+        self.assertEqual(self.payment2.r_code, '0')
+        self.assertEqual(self.payment2.r_desc, 'Successfully processed')
+        self.assertEqual(self.payment2.p_state, 'paid')
+        return
+
+    @external_test_credo
+    def test_student_credo_views(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.payments_path)
+        self.browser.getLink(self.P_ID).click()
+        self.browser.getLink("Requery Etranzact Credo").click()
+        self.assertTrue('<span>Successfully processed</span>' in self.browser.contents)
+        self.assertEqual(self.payment2.r_desc, 'Successfully processed')
+        return
+
+class CredoTestsApplicants(ApplicantsFullSetup):
+
+    layer = FunctionalLayer
+
+    P_ID = 'p7120808915674' # works only if such a payment was made on the test platform
+
+    def setUp(self):
+        super(CredoTestsApplicants, self).setUp()
+        # Add session configuration object
+        configuration = SessionConfiguration()
+        configuration.academic_session =  self.applicant.__parent__.year
+        configuration.etranzact_credo_enabled = True
+        self.app['configuration'].addSessionConfiguration(configuration)
+        payment = createObject('waeup.ApplicantOnlinePayment')
+        payment.p_id = self.P_ID
+        payment.p_category = 'application'
+        payment.amount_auth = 5000.0
+        self.applicant.email = 'xx@xx.xx'
+        self.applicant[payment.p_id] = payment
+        self.payment2 = payment
+
+    @external_test_credo
+    def test_initialize(self):
+
+        public_api_key = API_PUBLIC_KEY
+        callbackUrl = 'aa.aa.aa'
+        response = get_JSON_response_initialize(self.payment2, CREDO_HOST, callbackUrl, public_api_key)
+
+        # Payment already exists
+        self.assertEqual(response, {'error': {u'reference': u'Reference must be unique'}})
+
+        #self.assertEqual(response['status'], 200)
+        #self.assertEqual(response['message'], 'Successfully processed')
+        #self.assertEqual(response['data']['reference'], self.P_ID)
+        #self.assertTrue('https://pay.credodemo.com/' in response['data']['authorizationUrl'])
+        return
+
+    @external_test_credo
+    def test_verify(self):
+        self.payment2.p_id = 'anything'
+        success, msg, log = query_credo_payment(self.payment2, CREDO_HOST, SECRET_API_KEY)
+        self.assertEqual(log, 'Transaction with ref# anything not found')
+        self.payment2.p_id = self.P_ID # manually paid
+        success, msg, log = query_credo_payment(self.payment2, CREDO_HOST, SECRET_API_KEY)
+        self.assertEqual(msg, 'Successful callback received')
+        self.assertEqual(self.payment2.r_code, '0')
+        self.assertEqual(self.payment2.r_desc, 'Successfully processed')
+        self.assertEqual(self.payment2.p_state, 'paid')
+        return
+
+    @external_test_credo
+    def test_applicant_credo_views(self):
+        IWorkflowState(self.applicant).setState('started')
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.view_path)
+        self.browser.getLink(self.P_ID).click()
+        self.browser.getLink("Requery Etranzact Credo").click()
+        self.assertTrue('<span>Successfully processed</span>' in self.browser.contents)
+        self.assertEqual(self.payment2.r_desc, 'Successfully processed')
+        self.assertEqual(self.applicant.state, 'paid')
+        return
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interfaces.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interfaces.py	(revision 17642)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/interfaces.py	(revision 17730)
@@ -154,4 +154,9 @@
         )
 
+    etranzact_credo_enabled = schema.Bool(
+        title = _(u'Etranzact Credo integration enabled'),
+        default = False,
+        )
+
     etranzact_payoutlet_enabled = schema.Bool(
         title = _(u'Etranzact Payoutlet integration enabled'),
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/payments/interfaces.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/payments/interfaces.py	(revision 17642)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/payments/interfaces.py	(revision 17730)
@@ -42,5 +42,5 @@
 
     r_amount_approved = schema.Float(
-        title = _(u'Response Amount Approved'),
+        title = _(u'Response Amount Debited'),
         default = 0.0,
         required = False,
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/payments.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/payments.py	(revision 17642)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/payments.py	(revision 17730)
@@ -48,5 +48,8 @@
     @property
     def student(self):
-        return self.__parent__.__parent__
+        try:
+            return self.__parent__.__parent__
+        except AttributeError:
+            return None
 
 
