Index: main/kofacustom.nigeria/branches/uli-paypal/CHANGES.txt
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/CHANGES.txt	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/CHANGES.txt	(revision 17238)
@@ -0,0 +1,86 @@
+Changes
+*******
+
+1.3.dev0 (unreleased)
+=====================
+
+- Add `provisionally_cleared` field an allow students to edit
+  their clearance data if set.
+
+- Add fields, permissions, browser views and buttons to enable
+  financial clearance by bursory officers.
+
+- Add buttons and pages to verify CollegePay payments.
+
+- Implement Interswitch JSON transaction query. This webservice is
+  automatically used if a mac key is set in the request webservice
+  view.
+
+1.2 (2015-09-14)
+================
+
+- Do not allow to requery waived payments.
+
+- Replace `jamb_subjects` field by the list-of-choices field
+  `jamb_subjects_list` which allows to select items from the `jambsubjects`
+  vocabulary. This field is available only for CBT application.
+
+- Implement CBT application (Uniben).
+  Extend ug application interface. Redefine omit tuples.
+  Remove `jamb_age` field.
+
+- Implement JUPEB pre-studies application (Uniben).
+  Extend pg application interface.
+
+- Payment tickets must not be submitted to Interswitch if they
+  are older than 7 days.
+
+- Ensure that lga and nationality are not contradictory.
+
+- Be prepared for https webservice requests.
+
+- Add more exam subjects.
+
+- All fee amounts must be passed to Interswitch as integers
+  (mutltiples of Kobos) not floats.
+
+- Add `jamb_age` field.
+
+- Check if same payment has been made before calling `goto_interswitch`.
+
+- Add interswitch browser tests and add/modify components which are needed
+  for these tests.
+
+- Add base classes InterswitchPageApplicant and InterswitchPageStudent which
+  can be used in custom packages.
+
+- Add more exam grades (pass, distinction, credit-1 and credit-2).
+
+- Add NigeriaStudentStudyLevelExporter.
+
+- Add higher qualification 'WAEC Technical Examination' as first and second
+  sitting exam type.
+
+- Add LGA `Akwa Ibom / Nsit-Atai`.
+
+- Do not show current current level in student base data section on
+  slips (default).
+
+1.1 (2014-03-14)
+================
+
+- Add display_fullname column to CourseTicketExporter (requested by Uniben).
+
+
+1.0 (2014-02-22)
+================
+
+- Diazofied complete package.
+
+- We now work with waeup.kofa >= 1.0.
+
+
+0.1 (2014-02-21)
+================
+
+- Initial implementation.
Index: main/kofacustom.nigeria/branches/uli-paypal/README.txt
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/README.txt	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/README.txt	(revision 17238)
@@ -0,0 +1,4 @@
+kofacustom.nigeria
+******************
+
+Customizations of the `waeup.kofa` package for the nigerian market.
Index: main/kofacustom.nigeria/branches/uli-paypal/bootstrap.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/bootstrap.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/bootstrap.py	(revision 17238)
@@ -0,0 +1,210 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+"""
+
+import os
+import shutil
+import sys
+import tempfile
+
+from optparse import OptionParser
+
+__version__ = '2015-07-01'
+# See zc.buildout's changelog if this version is up to date.
+
+tmpeggs = tempfile.mkdtemp(prefix='bootstrap-')
+
+usage = '''\
+[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
+
+Bootstraps a buildout-based project.
+
+Simply run this script in a directory containing a buildout.cfg, using the
+Python that you want bin/buildout to use.
+
+Note that by using --find-links to point to local resources, you can keep
+this script from going over the network.
+'''
+
+parser = OptionParser(usage=usage)
+parser.add_option("--version",
+                  action="store_true", default=False,
+                  help=("Return bootstrap.py version."))
+parser.add_option("-t", "--accept-buildout-test-releases",
+                  dest='accept_buildout_test_releases',
+                  action="store_true", default=False,
+                  help=("Normally, if you do not specify a --version, the "
+                        "bootstrap script and buildout gets the newest "
+                        "*final* versions of zc.buildout and its recipes and "
+                        "extensions for you.  If you use this flag, "
+                        "bootstrap and buildout will get the newest releases "
+                        "even if they are alphas or betas."))
+parser.add_option("-c", "--config-file",
+                  help=("Specify the path to the buildout configuration "
+                        "file to be used."))
+parser.add_option("-f", "--find-links",
+                  help=("Specify a URL to search for buildout releases"))
+parser.add_option("--allow-site-packages",
+                  action="store_true", default=False,
+                  help=("Let bootstrap.py use existing site packages"))
+parser.add_option("--buildout-version",
+                  help="Use a specific zc.buildout version")
+parser.add_option("--setuptools-version",
+                  help="Use a specific setuptools version")
+parser.add_option("--setuptools-to-dir",
+                  help=("Allow for re-use of existing directory of "
+                        "setuptools versions"))
+
+options, args = parser.parse_args()
+if options.version:
+    print("bootstrap.py version %s" % __version__)
+    sys.exit(0)
+
+
+######################################################################
+# load/install setuptools
+
+try:
+    from urllib.request import urlopen
+except ImportError:
+    from urllib2 import urlopen
+
+ez = {}
+if os.path.exists('ez_setup.py'):
+    exec(open('ez_setup.py').read(), ez)
+else:
+    exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez)
+
+if not options.allow_site_packages:
+    # ez_setup imports site, which adds site packages
+    # this will remove them from the path to ensure that incompatible versions
+    # of setuptools are not in the path
+    import site
+    # inside a virtualenv, there is no 'getsitepackages'.
+    # We can't remove these reliably
+    if hasattr(site, 'getsitepackages'):
+        for sitepackage_path in site.getsitepackages():
+            # Strip all site-packages directories from sys.path that
+            # are not sys.prefix; this is because on Windows
+            # sys.prefix is a site-package directory.
+            if sitepackage_path != sys.prefix:
+                sys.path[:] = [x for x in sys.path
+                               if sitepackage_path not in x]
+
+setup_args = dict(to_dir=tmpeggs, download_delay=0)
+
+if options.setuptools_version is not None:
+    setup_args['version'] = options.setuptools_version
+if options.setuptools_to_dir is not None:
+    setup_args['to_dir'] = options.setuptools_to_dir
+
+ez['use_setuptools'](**setup_args)
+import setuptools
+import pkg_resources
+
+# This does not (always?) update the default working set.  We will
+# do it.
+for path in sys.path:
+    if path not in pkg_resources.working_set.entries:
+        pkg_resources.working_set.add_entry(path)
+
+######################################################################
+# Install buildout
+
+ws = pkg_resources.working_set
+
+setuptools_path = ws.find(
+    pkg_resources.Requirement.parse('setuptools')).location
+
+# Fix sys.path here as easy_install.pth added before PYTHONPATH
+cmd = [sys.executable, '-c',
+       'import sys; sys.path[0:0] = [%r]; ' % setuptools_path +
+       'from setuptools.command.easy_install import main; main()',
+       '-mZqNxd', tmpeggs]
+
+find_links = os.environ.get(
+    'bootstrap-testing-find-links',
+    options.find_links or
+    ('http://downloads.buildout.org/'
+     if options.accept_buildout_test_releases else None)
+    )
+if find_links:
+    cmd.extend(['-f', find_links])
+
+requirement = 'zc.buildout'
+version = options.buildout_version
+if version is None and not options.accept_buildout_test_releases:
+    # Figure out the most recent final version of zc.buildout.
+    import setuptools.package_index
+    _final_parts = '*final-', '*final'
+
+    def _final_version(parsed_version):
+        try:
+            return not parsed_version.is_prerelease
+        except AttributeError:
+            # Older setuptools
+            for part in parsed_version:
+                if (part[:1] == '*') and (part not in _final_parts):
+                    return False
+            return True
+
+    index = setuptools.package_index.PackageIndex(
+        search_path=[setuptools_path])
+    if find_links:
+        index.add_find_links((find_links,))
+    req = pkg_resources.Requirement.parse(requirement)
+    if index.obtain(req) is not None:
+        best = []
+        bestv = None
+        for dist in index[req.project_name]:
+            distv = dist.parsed_version
+            if _final_version(distv):
+                if bestv is None or distv > bestv:
+                    best = [dist]
+                    bestv = distv
+                elif distv == bestv:
+                    best.append(dist)
+        if best:
+            best.sort()
+            version = best[-1].version
+if version:
+    requirement = '=='.join((requirement, version))
+cmd.append(requirement)
+
+import subprocess
+if subprocess.call(cmd) != 0:
+    raise Exception(
+        "Failed to execute command:\n%s" % repr(cmd)[1:-1])
+
+######################################################################
+# Import and run buildout
+
+ws.add_entry(tmpeggs)
+ws.require(requirement)
+import zc.buildout.buildout
+
+if not [a for a in args if '=' not in a]:
+    args.append('bootstrap')
+
+# if -c was provided, we push it back into args for buildout' main function
+if options.config_file is not None:
+    args[0:0] = ['-c', options.config_file]
+
+zc.buildout.buildout.main(args)
+shutil.rmtree(tmpeggs)
Index: main/kofacustom.nigeria/branches/uli-paypal/buildout-init.cfg
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/buildout-init.cfg	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/buildout-init.cfg	(revision 17238)
@@ -0,0 +1,11 @@
+[buildout]
+# See http://pypi.python.org/pypi/mr.developer for details
+extensions = mr.developer
+auto-checkout = waeup.kofa
+sources-dir = sources
+develop = .
+parts =
+
+[sources]
+waeup.kofa = svn https://svn.waeup.org/repos/main/waeup.kofa/trunk
+
Index: main/kofacustom.nigeria/branches/uli-paypal/buildout.cfg
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/buildout.cfg	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/buildout.cfg	(revision 17238)
@@ -0,0 +1,218 @@
+[buildout]
+# See http://pypi.python.org/pypi/mr.developer for details
+extensions = mr.developer
+# Install waeup.kofa as develop egg
+auto-checkout = waeup.kofa
+sources-dir = sources
+develop = .
+parts =
+    eggbasket
+    app
+    i18n
+    test
+    mkdirs
+    zpasswd
+    zope_conf
+    site_zcml
+    zdaemon_conf
+    raw_deploy_ini
+    raw_debug_ini
+    themed_deploy_ini
+    themed_debug_ini
+    profile_ini
+# waeupdocs
+    coverage-detect
+    coverage-report
+# kofactl creation must be _after_ app!
+    kofactl
+    lxml
+    diazo
+# For backward compatibility, telling buildout not to throw away
+# the data and log subdirectories from the parts directory.
+    data
+    log
+# newest = false
+
+extends = versions.cfg
+show-picked-versions = true
+update-versions-file = versions.cfg
+
+index = https://pypi.python.org/simple/
+
+# eggs will be installed in the default buildout location
+# (see .buildout/default.cfg in your home directory)
+# unless you specify an eggs-directory option here.
+
+[sources]
+waeup.kofa = svn https://svn.waeup.org/repos/main/waeup.kofa/trunk
+[kofa_params]
+## Basic parameters for deployment.
+# default is 127.0.0.1
+host = 0.0.0.0
+# default is 8080
+base_port = 8080
+zeo1_port = 8080
+zeo2_port = 8081
+zeo3_port = 8082
+server_port = 8100
+# default 7. Number of pre-opened ZODB threads/client
+pool_size = 7
+# default 20MB. Cache size of each client (ZEO only)
+client_cache = 20MB
+# default 5000. Number of objects cached.
+object_cache = 5000
+# number of preopened paster threads, default 10
+threadpool_workers = 10
+devel_pkg = kofacustom.nigeria
+
+[app]
+# This creates all scripts in bin/. The kofactl created here is 'faulty'.
+recipe = zc.recipe.egg
+eggs = kofacustom.nigeria [beaker, diazo]
+       z3c.evalexception>=2.0
+       Paste
+       PasteScript
+       PasteDeploy
+       repoze.profile
+       ZODB3
+interpreter = python-console
+
+[kofactl]
+# See http://pypi.python.org/pypi/zc.recipe.egg for details...
+# Here we create the kofactl script again, this time with a
+# fixed path to zdaemon.conf as argument.
+recipe = zc.recipe.egg
+eggs = kofacustom.nigeria [beaker, diazo]
+       waeup.kofa
+arguments = "${buildout:parts-directory}/etc/zdaemon.conf"
+scripts = kofactl=kofactl
+
+[mkdirs]
+recipe = z3c.recipe.mkdir
+paths =
+    ${zope_conf:filestorage}
+    ${zope_conf:logfiles}
+    ${zope_conf:blobstorage}
+    ${buildout:directory}/var/mailqueue/cur
+    ${buildout:directory}/var/mailqueue/tmp
+    ${buildout:directory}/var/mailqueue/new
+    ${buildout:directory}/var/datacenter/media
+    ${buildout:directory}/parts/test/datacenter
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = kofacustom.nigeria [beaker, test]
+defaults = ['--tests-pattern', '^f?tests$', '-v']
+
+# this section named so that the i18n scripts are called bin/i18n...
+[i18n]
+recipe = z3c.recipe.i18n:i18n
+packages = kofacustom.nigeria
+eggs = kofacustom.nigeria
+domain = kofacustom.nigeria
+output = src/kofacustom/nigeria/locales
+zcml =
+
+# This section is named so that the zpasswd utility is
+# called `zpasswd`
+[zpasswd]
+recipe = z3c.recipe.dev:script
+eggs = kofacustom.nigeria
+module = zope.app.server.zpasswd
+method = main
+
+[zope_conf]
+recipe = collective.recipe.template
+input = etc/zope.conf.in
+output = ${buildout:parts-directory}/etc/zope.conf
+filestorage = ${buildout:directory}/var/filestorage
+blobstorage = ${buildout:directory}/var/blobstorage
+logfiles = ${buildout:directory}/var/log
+extra =
+# extra = <grokwarnings>True</grokwarnings>
+# 'extra' is copied verbatim. Use it for product config sections and so.
+
+# The [data] and [log] parts are still in here to instruct buildout to not
+# unintentionally throw away the parts/data and parts/log subdirectories
+# that contain the Data.fs and the log files. These files should be
+# copied to the new locations. See the upgrade notes for more information.
+[data]
+recipe = zc.recipe.filestorage
+
+[log]
+recipe = zc.recipe.filestorage
+
+[mailqueue]
+recipe = zc.recipe.filestorage
+
+[site_zcml]
+recipe = collective.recipe.template
+input = etc/site.zcml.in
+output = ${buildout:parts-directory}/etc/site.zcml
+
+[zdaemon_conf]
+recipe = collective.recipe.template
+input = etc/zdaemon.conf.in
+output = ${buildout:parts-directory}/etc/zdaemon.conf
+
+[raw_debug_ini]
+recipe = collective.recipe.template
+input = etc/raw/debug.ini.in
+output = ${buildout:parts-directory}/etc/raw-debug.ini
+
+[raw_deploy_ini]
+recipe = collective.recipe.template
+input = etc/raw/deploy.ini.in
+output = ${buildout:parts-directory}/etc/raw-deploy.ini
+
+[themed_debug_ini]
+recipe = collective.recipe.template
+input = etc/themed/debug.ini.in
+output = ${buildout:parts-directory}/etc/themed-debug.ini
+
+[themed_deploy_ini]
+recipe = collective.recipe.template
+input = etc/themed/deploy.ini.in
+output = ${buildout:parts-directory}/etc/themed-deploy.ini
+
+[profile_ini]
+recipe = collective.recipe.template
+input = etc/profile.ini.in
+output = ${buildout:parts-directory}/etc/profile.ini
+
+[eggbasket]
+recipe = z3c.recipe.eggbasket
+eggs = grok
+url = http://grok.zope.org/releaseinfo/grok-eggs-1.1.tgz
+
+#[waeupdocs]
+#recipe = collective.recipe.sphinxbuilder
+#eggs = kofacustom.nigeria[docs]
+#source = ${buildout:directory}/docs/source
+#build = ${buildout:directory}/docs/build
+
+# Collect test coverage data.
+[coverage-detect]
+recipe = zc.recipe.testrunner
+eggs = kofacustom.nigeria[beaker]
+defaults = ['--tests-pattern', '^f?tests$', '-v', '--coverage', 'coverage']
+
+# Create a coverage report.
+# Make sure to run bin/coverage-detect to collect the data for the report
+# first!
+[coverage-report]
+recipe = zc.recipe.egg
+eggs = z3c.coverage
+scripts = coveragereport
+arguments = ('${buildout:parts-directory}/coverage-detect/coverage', '${buildout:directory}/coverage-report')
+
+[lxml]
+recipe = z3c.recipe.staticlxml
+egg = lxml
+
+[diazo]
+recipe = zc.recipe.egg
+eggs =
+    diazo [wsgi]
+    PasteScript
+    kofacustom.nigeria [beaker,diazo]
Index: main/kofacustom.nigeria/branches/uli-paypal/etc/README.txt
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/etc/README.txt	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/etc/README.txt	(revision 17238)
@@ -0,0 +1,26 @@
+In this directory you can find templates which are used by
+``zc.buildout`` to create the configuration files in the parts/etc/ subdir
+of your project.
+
+If you modify files in this directory, you have to run::
+
+  $ bin/buildout
+
+afterwards to rebuild the configuration files in parts/etc/.
+
+In the templates you can use placesholders recognized by zc.buildout
+to name local paths, etc. A zc.buildout placeholder looks like this::
+
+  ${buildout:directory}
+
+which gives you the path of the project directory and will be
+substituted with the real path when you run buildout the next
+time. The set of available placeholders depends on your
+buildout.cfg.
+
+You can also modify files in parts/etc directly, but those changes
+will be overwritten after running bin/buildout the next time.
+
+To run your project you can do::
+
+  $ bin/paster serve parts/etc/deploy.ini
Index: main/kofacustom.nigeria/branches/uli-paypal/etc/profile.ini.in
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/etc/profile.ini.in	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/etc/profile.ini.in	(revision 17238)
@@ -0,0 +1,93 @@
+# profile.ini
+#
+# Profiling configuration for use with paster/WSGI
+#
+# Call http://localhost:8080/__profile__ to see the profiler in action
+
+[loggers]
+keys = root, wsgi, async, async_trace
+
+[handlers]
+keys = console, accesslog, asynclog, async_tracelog
+
+[formatters]
+keys = generic, accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+handlers = accesslog
+qualname = wsgi
+propagate = 0
+
+[logger_async]
+level = INFO
+handlers = asynclog
+qualname = zc.async
+propagate = 0
+
+[logger_async_trace]
+level = INFO
+handlers = async_tracelog
+qualname = zc.async.trace
+propagate = 0
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[handler_accesslog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'access.log'),
+        'a')
+level = INFO
+formatter = accesslog
+
+[handler_asynclog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_async_tracelog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async_trace.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[filter:translogger]
+use = egg:Paste#translogger
+setup_console_handler = False
+logger_name = wsgi
+
+[filter:profiler]
+use = egg:repoze.profile#profile
+
+[app:app]
+use = egg:${kofa_params:devel_pkg}
+
+[pipeline:main]
+pipeline = translogger profiler app
+
+[server:main]
+use = egg:Paste#http
+host = ${kofa_params:host}
+port = ${kofa_params:base_port}
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope.conf
+env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
Index: main/kofacustom.nigeria/branches/uli-paypal/etc/raw/debug.ini.in
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/etc/raw/debug.ini.in	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/etc/raw/debug.ini.in	(revision 17238)
@@ -0,0 +1,94 @@
+# debug.ini
+#
+# Debugging configuration for use with paster/WSGI
+#
+
+[loggers]
+keys = root, wsgi
+
+[handlers]
+keys = console, accesslog, asynclog, async_tracelog
+
+[formatters]
+keys = generic, accesslog
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[handler_asynclog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_async_tracelog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async_trace.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[handler_accesslog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'access.log'),
+        'a')
+level = INFO
+formatter = accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+handlers = accesslog
+qualname = wsgi
+propagate = 0
+
+[logger_async]
+level = INFO
+handlers = asynclog
+qualname = zc.async
+propagate = 0
+
+[logger_async_trace]
+level = INFO
+handlers = async_tracelog
+qualname = zc.async.trace
+propagate = 0
+
+[filter:translogger]
+use = egg:Paste#translogger
+setup_console_handler = False
+logger_name = wsgi
+
+[filter-app:main]
+# Change the last part from 'ajax' to 'pdb' for a post-mortem debugger
+# on the console:
+use = egg:z3c.evalexception#ajax
+next = zope
+
+[app:zope]
+use = egg:${kofa_params:devel_pkg}#debug
+filter-with = translogger
+exempt-exceptions = zope.security.interfaces.IUnauthorized
+
+[server:main]
+use = egg:Paste#http
+host = ${kofa_params:host}
+port = ${kofa_params:base_port}
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope.conf
+env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
Index: main/kofacustom.nigeria/branches/uli-paypal/etc/raw/deploy.ini.in
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/etc/raw/deploy.ini.in	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/etc/raw/deploy.ini.in	(revision 17238)
@@ -0,0 +1,94 @@
+# deploy.ini
+#
+# Deployment configuration for use with paster/WSGI
+#
+
+[loggers]
+keys = root, wsgi, async, async_trace
+
+[handlers]
+keys = null, console, accesslog, asynclog, async_tracelog
+
+[formatters]
+keys = generic, accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+#handlers = accesslog
+handlers = null
+qualname = wsgi
+propagate = 0
+
+[logger_async]
+level = INFO
+handlers = asynclog
+qualname = zc.async
+propagate = 0
+
+[logger_async_trace]
+level = INFO
+handlers = async_tracelog
+qualname = zc.async.trace
+propagate = 0
+
+[handler_null]
+# you can use 'NullHandler' (w/o the path) if under Python 2.7+
+class = waeup.kofa.utils.helpers.NullHandler
+args = ()
+level = NOTSET
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[handler_accesslog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'access.log'),
+        'a')
+level = INFO
+formatter = accesslog
+
+[handler_asynclog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_async_tracelog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async_trace.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[filter:translogger]
+use = egg:Paste#translogger
+setup_console_handler = False
+logger_name = wsgi
+
+[app:main]
+use = egg:${kofa_params:devel_pkg}
+filter-with = translogger
+
+[server:main]
+use = egg:Paste#http
+host = ${kofa_params:host}
+port = ${kofa_params:base_port}
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope.conf
+env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
Index: main/kofacustom.nigeria/branches/uli-paypal/etc/site.zcml.in
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/etc/site.zcml.in	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/etc/site.zcml.in	(revision 17238)
@@ -0,0 +1,81 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           xmlns:kofa="http://namespaces.waeup.org/kofa"
+           i18n_domain="${kofa_params:devel_pkg}">
+
+  <include package="${kofa_params:devel_pkg}" />
+  <include package="${kofa_params:devel_pkg}" file="mail.zcml" />
+  <!-- install job container
+
+       install a job container for asynchronous jobs. Pick one of the
+       two possibilites: 'single' for installation in the one big ZODB
+       or 'multidb' for setup with a dedicated ZODB for job handling.
+  -->
+  <!-- include package="waeup.kofa" file="async_single.zcml" / -->
+  <include package="waeup.kofa" file="async_multidb.zcml" />
+
+  <!-- install dispatcher and other needed async components
+
+       install other needed components for asynchronous jobs. Pick one
+       of the two possibilities: 'basic_dispatcher' for single ZODB
+       setup, 'multidb_dispatcher' for setup with a dedicated ZODB for
+       job handling. Make sure the setting matches the setting picked
+       above. -->
+  <!-- include package="zc.async" file="basic_dispatcher_policy.zcml" / -->
+  <include package="zc.async" file="multidb_dispatcher_policy.zcml" />
+
+  <!-- Where should the datacenter reside by default? -->
+  <kofa:datacenter
+      path="${buildout:directory}/var/datacenter" />
+
+    <configure i18n_domain="${kofa_params:devel_pkg}">
+
+
+      <unauthenticatedPrincipal id="zope.anybody"
+                                title="Unauthenticated User" />
+      <unauthenticatedGroup id="zope.Anybody"
+                            title="Unauthenticated Users" />
+      <authenticatedGroup id="zope.Authenticated"
+                        title="Authenticated Users" />
+      <everybodyGroup id="zope.Everybody"
+                      title="All Users" />
+      <principal id="zope.manager"
+                 title="Manager"
+                 login="grok"
+                 password_manager="SHA1"
+                 password="321b9cd3739bd029629243732eb682db005a7541f7509622"
+                 />
+
+      <!-- XMLRPC user for demo and testing
+           Plaintext password is xmlrpcuser1
+           Don't use it in real deployment
+      <principal id="zope.xmlrpcuser1"
+                 title="XMLRPC User 1"
+                 login="xmlrpcuser1"
+                 password="{SSHA}ZaL4-UiwmSu6AbLYY-yCS_8vtKpH5Hc6"
+                 password_manager="SSHA"
+                 />
+      <grant role="waeup.xmlrpcusers1"
+             principal="zope.xmlrpcuser1" />
+      -->
+
+      <!-- Replace the following directive if you do not want
+           public access -->
+      <grant permission="zope.app.dublincore.view"
+             principal="zope.Anybody" />
+      <grant permission="zope.View"
+             principal="zope.Authenticated" />
+      <grant permission="zope.app.dublincore.view"
+             principal="zope.Authenticated" />
+      <grant permission="waeup.Public"
+             principal="zope.Everybody" />
+      <grant permission="waeup.Anonymous"
+             principal="zope.Anybody" />
+      <grant permission="waeup.Authenticated"
+             principal="zope.Authenticated" />
+
+      <role id="zope.Manager" title="Site Manager" />
+      <grantAll role="zope.Manager" />
+      <grant role="zope.Manager"
+             principal="zope.manager" />
+   </configure>
+</configure>
Index: main/kofacustom.nigeria/branches/uli-paypal/etc/themed/debug.ini.in
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/etc/themed/debug.ini.in	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/etc/themed/debug.ini.in	(revision 17238)
@@ -0,0 +1,118 @@
+# debug.ini
+#
+# Debugging configuration for use with paster/WSGI and diazo
+#
+
+[loggers]
+keys = root, wsgi
+
+[handlers]
+keys = console, accesslog, asynclog, async_tracelog
+
+[formatters]
+keys = generic, accesslog
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[handler_asynclog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_async_tracelog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async_trace.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[handler_accesslog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'access.log'),
+        'a')
+level = INFO
+formatter = accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+handlers = accesslog
+qualname = wsgi
+propagate = 0
+
+[logger_async]
+level = INFO
+handlers = asynclog
+qualname = zc.async
+propagate = 0
+
+[logger_async_trace]
+level = INFO
+handlers = async_tracelog
+qualname = zc.async.trace
+propagate = 0
+
+[filter:translogger]
+use = egg:Paste#translogger
+setup_console_handler = False
+logger_name = wsgi
+
+[filter:debugger]
+# Change the last part from 'ajax' to 'pdb' for a post-mortem debugger
+# on the console:
+use = egg:z3c.evalexception#ajax
+
+[app:zope]
+use = egg:${kofa_params:devel_pkg}#debug
+exempt-exceptions = zope.security.interfaces.IUnauthorized
+
+[app:static]
+use = egg:Paste#static
+document_root = %(LAYOUT_DIR)s/static
+
+[composite:main]
+use = egg:Paste#urlmap
+/static = static
+/ = default
+
+[filter:theme]
+use = egg:diazo
+rules = file:///%(LAYOUT_DIR)s/rules.xml
+prefix = /
+doctype = <!DOCTYPE html>
+# set to false, if you don't want rules.xml being rebuild on each request
+debug = true
+
+[pipeline:default]
+pipeline = theme
+           translogger
+           debugger
+           zope
+
+[server:main]
+use = egg:Paste#http
+host = ${kofa_params:host}
+port = ${kofa_params:base_port}
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope.conf
+env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
+# the directory where all real diazo config is done (no trailing slash)
+LAYOUT_DIR = ${buildout:directory}/sources/waeup.kofa/layout
+STATIC_DIR = %(LAYOUT_DIR)s/static
Index: main/kofacustom.nigeria/branches/uli-paypal/etc/themed/deploy.ini.in
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/etc/themed/deploy.ini.in	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/etc/themed/deploy.ini.in	(revision 17238)
@@ -0,0 +1,118 @@
+# deploy.ini
+#
+# Deployment configuration for use with paster/WSGI
+#
+
+[loggers]
+keys = root, wsgi, async, async_trace
+
+[handlers]
+keys = null, console, accesslog, asynclog, async_tracelog
+
+[formatters]
+keys = generic, accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+#handlers = accesslog
+handlers = null
+qualname = wsgi
+propagate = 0
+
+[logger_async]
+level = INFO
+handlers = asynclog
+qualname = zc.async
+propagate = 0
+
+[logger_async_trace]
+level = INFO
+handlers = async_tracelog
+qualname = zc.async.trace
+propagate = 0
+
+[handler_null]
+# you can use 'NullHandler' (w/o the path) if under Python 2.7+
+class = waeup.kofa.utils.helpers.NullHandler
+args = ()
+level = NOTSET
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[handler_accesslog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'access.log'),
+        'a')
+level = INFO
+formatter = accesslog
+
+[handler_asynclog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_async_tracelog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async_trace.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[filter:translogger]
+use = egg:Paste#translogger
+setup_console_handler = False
+logger_name = wsgi
+
+[app:content]
+use = egg:${kofa_params:devel_pkg}
+filter-with = translogger
+
+[app:static]
+use = egg:Paste#static
+document_root = %(LAYOUT_DIR)s/static/
+
+[composite:main]
+use = egg:Paste#urlmap
+/static = static
+/ = default
+
+[server:main]
+use = egg:Paste#http
+host = ${kofa_params:host}
+port = ${kofa_params:base_port}
+
+[filter:theme]
+use = egg:diazo
+rules = file:///%(LAYOUT_DIR)s/rules.xml
+prefix = /
+doctype = <!DOCTYPE html>
+# set to true, if you want rules.xml being rebuild on each request
+debug = false
+
+[pipeline:default]
+pipeline = theme
+           content
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope.conf
+env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
+# the directory where all real diazo config is done (no trailing slash)
+LAYOUT_DIR = ${buildout:directory}/sources/waeup.kofa/layout
+STATIC_DIR = %(LAYOUT_DIR)s/static
Index: main/kofacustom.nigeria/branches/uli-paypal/etc/zdaemon.conf.in
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/etc/zdaemon.conf.in	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/etc/zdaemon.conf.in	(revision 17238)
@@ -0,0 +1,8 @@
+<runner>
+  program ${buildout:directory}/bin/paster serve ${buildout:directory}/parts/etc/themed-deploy.ini
+  daemon on
+  transcript ${zope_conf:logfiles}/zdaemon.log
+  socket-name ${zope_conf:logfiles}/zdaemonsock
+  # Enable this to run the child process as a different user
+  # user zope
+</runner>
Index: main/kofacustom.nigeria/branches/uli-paypal/etc/zope.conf.in
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/etc/zope.conf.in	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/etc/zope.conf.in	(revision 17238)
@@ -0,0 +1,62 @@
+# Identify the component configuration used to define the site:
+site-definition ${site_zcml:output}
+
+<zodb>
+  # Standard blob storage
+  <blobstorage>
+    blob-dir ${zope_conf:blobstorage}
+    <filestorage>
+      path ${zope_conf:filestorage}/Data.fs
+    </filestorage>
+  </blobstorage>
+
+# Uncomment this if you want a blob-less standard file storage instead:
+#  <filestorage>
+#       path ${zope_conf:filestorage}
+#  </filestorage>
+
+# Uncomment this if you want to connect to a ZEO server instead:
+#  <zeoclient>
+#    server localhost:8100
+#    storage 1
+#    # ZEO client cache, in bytes
+#    cache-size 20MB
+#    # Uncomment to have a persistent disk cache
+#    #client zeo1
+#  </zeoclient>
+
+</zodb>
+
+<zodb async>
+  <filestorage>
+    # create true
+    path ${zope_conf:filestorage}/Data.async.fs
+  </filestorage>
+</zodb>
+
+<eventlog>
+  # This sets up logging to a file.
+  # The "path" setting can be a relative or absolute
+  # filesystem path.
+
+  <logfile>
+    path ${zope_conf:logfiles}/z3.log
+    formatter zope.exceptions.log.Formatter
+  </logfile>
+
+  # This sets up logging to to standard output.
+  # The "path" setting can be the tokens STDOUT or STDERR
+  
+#  <logfile>
+#    path STDOUT
+#    formatter zope.exceptions.log.Formatter
+#  </logfile>
+</eventlog>
+
+# Comment this line to disable developer mode.  This should be done in
+# production
+# devmode on
+
+# Extra configuration lines can be added to zope_conf's extra option. Put for 
+# instance productconf sections in here.
+${zope_conf:extra}
Index: main/kofacustom.nigeria/branches/uli-paypal/setup.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/setup.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/setup.py	(revision 17238)
@@ -0,0 +1,100 @@
+import os
+from setuptools import setup, find_packages
+
+version = '1.3.dev0'
+
+install_requires =[
+    'setuptools',
+    'grok',
+    'waeup.kofa >= 1.3.2',
+    ],
+
+# Having beaker installed additionally is a feature very recommended
+# for production use. The default buildout includes beaker for tests,
+# start scripts, and other parts that can benefit from it. The windows
+# buildout does not include it due to compiling problems with the
+# beaker package.
+beaker_require = [
+    'dolmen.beaker',
+    ]
+
+tests_require = [
+    'z3c.testsetup',
+    'zope.app.testing',
+    'zope.testbrowser',
+    'zope.testing',
+    'unittest2',
+    ]
+
+docs_require = [
+    'Sphinx',
+    'collective.recipe.sphinxbuilder',
+    'docutils',
+    'roman',
+    'repoze.sphinx.autointerface',
+    ]
+
+diazo_require = [
+    'diazo',
+    'webob',
+    ]
+
+def read(*rnames):
+    return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
+
+long_description = (
+    read('README.txt')
+    + '\n\n'
+    + read('CHANGES.txt')
+    + '\n\n'
+    + 'Download\n'
+    + '********\n'
+    )
+
+setup(name = 'kofacustom.nigeria',
+      version = version,
+      description = "Nigerian customizations of the waeup.kofa package",
+      long_description = long_description,
+
+      keywords = "portal waeup kofa student university registration nigeria",
+      # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+      classifiers = [
+        'Development Status :: 3 - Alpha',
+        'Environment :: Web Environment',
+        'Intended Audience :: Education',
+        'License :: OSI Approved :: GNU General Public License (GPL)',
+        'Programming Language :: Python',
+        'Operating System :: POSIX',
+        'Operating System :: POSIX :: Linux',
+        'Framework :: Zope3',
+        'Topic :: Education',
+        'Topic :: Internet :: WWW/HTTP',
+        ],
+      author = "The WAeUP team.",
+      author_email = "",
+      url = "http://www.waeup.org/",
+      license = "GPL",
+      package_dir = {'': 'src'},
+      packages= find_packages('src'),
+      namespace_packages = ['kofacustom',],
+      include_package_data = True,
+      zip_safe = False,
+      install_requires = install_requires,
+      tests_require = tests_require,
+      extras_require = dict(
+        test = tests_require,
+        docs = docs_require,
+        beaker = beaker_require,
+        diazo = diazo_require,
+        ),
+      entry_points="""
+      # Add entry points here
+      [console_scripts]
+      kofa-debug = grokcore.startup:interactive_debug_prompt
+      kofactl = grokcore.startup:zdaemon_controller
+      [paste.app_factory]
+      main = waeup.kofa.startup:env_app_factory
+      debug = waeup.kofa.startup:env_debug_app_factory
+
+      """,
+      )
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/__init__.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/__init__.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/__init__.py	(revision 17238)
@@ -0,0 +1,7 @@
+# namespace package boilerplate
+try:
+    import pkg_resources
+    pkg_resources.declare_namespace(__name__)
+except ImportError, e:
+    from pkgutil import extend_path
+    __path__ = extend_path(__path__, __name__)
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/__init__.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/__init__.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/__init__.py	(revision 17238)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/applicant.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/applicant.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/applicant.py	(revision 17238)
@@ -0,0 +1,82 @@
+## $Id$
+##
+## Copyright (C) 2011 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 zope.interface import implementedBy
+from waeup.kofa.applicants.applicant import Applicant, ApplicantFactory
+from waeup.kofa.utils.helpers import attrs_to_fields
+from kofacustom.nigeria.applicants.interfaces import(
+    INigeriaApplicant,
+    INigeriaUGApplicantEdit, INigeriaPGApplicantEdit,
+    IPUTMEApplicantEdit, IBankAccount)
+
+class NigeriaApplicant(Applicant):
+
+    grok.implements(INigeriaApplicant, INigeriaUGApplicantEdit,
+        INigeriaPGApplicantEdit, IPUTMEApplicantEdit, IBankAccount)
+    grok.provides(INigeriaApplicant)
+
+    applicant_student_mapping = Applicant.applicant_student_mapping + [
+        ('lga', 'lga'),
+        ('nationality', 'nationality'),
+        ('fst_sit_fname', 'fst_sit_fname'),
+        ('fst_sit_no', 'fst_sit_no'),
+        ('fst_sit_date', 'fst_sit_date'),
+        ('fst_sit_type', 'fst_sit_type'),
+        ('fst_sit_results', 'fst_sit_results'),
+        ('scd_sit_fname', 'scd_sit_fname'),
+        ('scd_sit_no', 'scd_sit_no'),
+        ('scd_sit_date', 'scd_sit_date'),
+        ('scd_sit_type', 'scd_sit_type'),
+        ('scd_sit_results', 'scd_sit_results'),
+        ('hq_type', 'hq_type'),
+        ('hq_fname', 'hq_fname'),
+        ('hq_matric_no', 'hq_matric_no'),
+        ('hq_degree', 'hq_degree'),
+        ('hq_school', 'hq_school'),
+        ('hq_session', 'hq_session'),
+        ('hq_disc', 'hq_disc'),
+        ]
+
+    def _setStudyCourseAttributes(self, studycourse):
+        if self.applicant_id.startswith('pude'):
+            studycourse.entry_mode = u'de_ft'
+            studycourse.current_level = 200
+        else:
+            studycourse.entry_mode = self.course_admitted.study_mode
+            studycourse.current_level = self.course_admitted.start_level
+        studycourse.certificate = self.course_admitted
+        studycourse.entry_session = self.__parent__.year
+        studycourse.current_session = self.__parent__.year
+        return
+
+# Set all attributes of NigeriaApplicant required in INigeriaApplicant as field
+# properties. Doing this, we do not have to set initial attributes
+# ourselves and as a bonus we get free validation when an attribute is
+# set.
+NigeriaApplicant = attrs_to_fields(NigeriaApplicant)
+
+class NigeriaApplicantFactory(ApplicantFactory):
+    """A factory for customized applicants.
+    """
+
+    def __call__(self, *args, **kw):
+        return NigeriaApplicant()
+
+    def getInterfaces(self):
+        return implementedBy(NigeriaApplicant)
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/batching.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/batching.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/batching.py	(revision 17238)
@@ -0,0 +1,38 @@
+## $Id$
+##
+## Copyright (C) 2011 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
+##
+"""Batch processing for applicants.
+"""
+from waeup.kofa.utils.batching import BatchProcessor
+from waeup.kofa.applicants.batching import ApplicantProcessor
+from kofacustom.nigeria.applicants.interfaces import (
+    INigeriaApplicant, INigeriaApplicantUpdateByRegNo)
+
+class NigeriaApplicantProcessor(ApplicantProcessor):
+    """A batch processor for INigeriaApplicant objects.
+
+    In create mode container_code is required. If application_number is given
+    an applicant with this number is created in the designated container.
+    If application_number is not given a random application_number is assigned.
+    applicant_id is being determined by the system and can't be imported.
+
+    In update or remove mode container_code and application_number columns
+    must not exist. The applicant object is solely searched by its applicant_id
+    or reg_number.
+    """
+    iface = INigeriaApplicant
+    iface_byregnumber = INigeriaApplicantUpdateByRegNo
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/browser.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/browser.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/browser.py	(revision 17238)
@@ -0,0 +1,209 @@
+## $Id$
+##
+## Copyright (C) 2011 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
+##
+"""UI components for basic applicants and related components.
+"""
+import grok
+from zope.component import getUtility
+from zope.i18n import translate
+from waeup.kofa.widgets.datewidget import FriendlyDatetimeDisplayWidget
+from zope.formlib.textwidgets import BytesDisplayWidget
+from waeup.kofa.students.interfaces import IStudentsUtils
+from waeup.kofa.applicants.browser import (ApplicantDisplayFormPage,
+    ApplicantManageFormPage, ApplicantEditFormPage,
+    ApplicantRegistrationPage,
+    OnlinePaymentDisplayFormPage,
+    OnlinePaymentBreadcrumb, ExportPDFPaymentSlipPage,
+    )
+from waeup.kofa.applicants.viewlets import (
+    PDFActionButton, PaymentReceiptActionButton)
+from waeup.kofa.applicants.pdf import PDFApplicationSlip
+from kofacustom.nigeria.applicants.interfaces import (
+    INigeriaPGApplicant, INigeriaUGApplicant,
+    INigeriaPGApplicantEdit, INigeriaUGApplicantEdit,
+    INigeriaApplicantOnlinePayment,
+    UG_OMIT_DISPLAY_FIELDS,
+    UG_OMIT_PDF_FIELDS,
+    UG_OMIT_MANAGE_FIELDS,
+    UG_OMIT_EDIT_FIELDS,
+    PG_OMIT_DISPLAY_FIELDS,
+    PG_OMIT_PDF_FIELDS,
+    PG_OMIT_MANAGE_FIELDS,
+    PG_OMIT_EDIT_FIELDS,
+    )
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+class NigeriaOnlinePaymentBreadcrumb(OnlinePaymentBreadcrumb):
+    """A breadcrumb for payments.
+    """
+    grok.context(INigeriaApplicantOnlinePayment)
+
+class PDFActionButton(PDFActionButton):
+
+    @property
+    def text(self):
+        if getattr(self.context, 'result_uploaded', False):
+            return _('Download result slip')
+        return _('Download application slip')
+
+class PaymentReceiptActionButton(PaymentReceiptActionButton):
+
+    @property
+    def target_url(self):
+        if not self.context.p_state in ('paid', 'waived', 'scholarship') \
+            and not self.context.r_company:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class NigeriaApplicantDisplayFormPage(ApplicantDisplayFormPage):
+    """A display view for applicant data.
+    """
+
+    def _not_paid(self):
+        return self.context.state in ('initialized', 'started',)
+
+    @property
+    def form_fields(self):
+        if self.target is not None and self.target.startswith('pg'):
+            form_fields = grok.AutoFields(INigeriaPGApplicant)
+            for field in PG_OMIT_DISPLAY_FIELDS:
+                form_fields = form_fields.omit(field)
+        else:
+            form_fields = grok.AutoFields(INigeriaUGApplicant)
+            for field in UG_OMIT_DISPLAY_FIELDS:
+                form_fields = form_fields.omit(field)
+        #form_fields['perm_address'].custom_widget = BytesDisplayWidget
+        form_fields['notice'].custom_widget = BytesDisplayWidget
+        if not getattr(self.context, 'student_id'):
+            form_fields = form_fields.omit('student_id')
+        if not getattr(self.context, 'screening_score'):
+            form_fields = form_fields.omit('screening_score')
+        if not getattr(self.context, 'screening_venue') or self._not_paid():
+            form_fields = form_fields.omit('screening_venue')
+        if not getattr(self.context, 'screening_date') or self._not_paid():
+            form_fields = form_fields.omit('screening_date')
+        return form_fields
+
+class NigeriaPDFApplicationSlip(PDFApplicationSlip):
+
+    def _addLoginInformation(self):
+        """ We do no longer render login information on
+        pdf application slips.
+        """
+        return
+
+    def _reduced_slip(self):
+        return getattr(self.context, 'result_uploaded', False)
+
+    @property
+    def form_fields(self):
+        if self.target is not None and self.target.startswith('pg'):
+            form_fields = grok.AutoFields(INigeriaPGApplicant)
+            for field in PG_OMIT_PDF_FIELDS:
+                form_fields = form_fields.omit(field)
+        else:
+            form_fields = grok.AutoFields(INigeriaUGApplicant)
+            for field in UG_OMIT_PDF_FIELDS:
+                form_fields = form_fields.omit(field)
+        if not getattr(self.context, 'student_id'):
+            form_fields = form_fields.omit('student_id')
+        if not getattr(self.context, 'screening_score'):
+            form_fields = form_fields.omit('screening_score')
+        if not getattr(self.context, 'screening_venue'):
+            form_fields = form_fields.omit('screening_venue')
+        if not getattr(self.context, 'screening_date'):
+            form_fields = form_fields.omit('screening_date')
+        return form_fields
+
+class NigeriaApplicantManageFormPage(ApplicantManageFormPage):
+    """A full edit view for applicant data.
+    """
+    
+    @property
+    def form_fields(self):
+        if self.target is not None and self.target.startswith('pg'):
+            form_fields = grok.AutoFields(INigeriaPGApplicant)
+            for field in PG_OMIT_MANAGE_FIELDS:
+                form_fields = form_fields.omit(field)
+        else:
+            form_fields = grok.AutoFields(INigeriaUGApplicant)
+            for field in UG_OMIT_MANAGE_FIELDS:
+                form_fields = form_fields.omit(field)
+        form_fields['student_id'].for_display = True
+        form_fields['applicant_id'].for_display = True
+        return form_fields
+
+class NigeriaApplicantEditFormPage(ApplicantEditFormPage):
+    """An applicant-centered edit view for applicant data.
+    """
+
+    @property
+    def form_fields(self):
+        if self.target is not None and self.target.startswith('pg'):
+            form_fields = grok.AutoFields(INigeriaPGApplicantEdit)
+            for field in PG_OMIT_EDIT_FIELDS:
+                form_fields = form_fields.omit(field)
+        else:
+            form_fields = grok.AutoFields(INigeriaUGApplicantEdit)
+            for field in UG_OMIT_EDIT_FIELDS:
+                form_fields = form_fields.omit(field)
+        form_fields['applicant_id'].for_display = True
+        form_fields['reg_number'].for_display = True
+        return form_fields
+
+class NigeriaOnlinePaymentDisplayFormPage(OnlinePaymentDisplayFormPage):
+    """ Page to view an online payment ticket
+    """
+    grok.context(INigeriaApplicantOnlinePayment)
+    form_fields = grok.AutoFields(INigeriaApplicantOnlinePayment).omit('ac',
+        'provider_amt', 'gateway_amt', 'thirdparty_amt', 'p_item')
+    form_fields[
+        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    form_fields[
+        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+
+class NigeriaExportPDFPaymentSlipPage(ExportPDFPaymentSlipPage):
+    """Deliver a PDF slip of the context.
+    """
+    grok.context(INigeriaApplicantOnlinePayment)
+    form_fields = grok.AutoFields(INigeriaApplicantOnlinePayment).omit(
+        'ac', 'provider_amt', 'gateway_amt', 'thirdparty_amt', 'p_item',
+        'p_split_data')
+    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+
+    @property
+    def payment_slip_download_warning(self):
+        return ''
+
+    def update(self):
+        if not self.context.p_state in ('paid', 'waived', 'scholarship') \
+            and not self.context.r_company:
+            self.redirect(self.url(self.context))
+            return
+        return
+
+class NigeriaApplicantRegistrationPage(ApplicantRegistrationPage):
+    """Captcha'd registration page for applicants.
+    """
+
+    #def _redirect(self, email, password, applicant_id):
+    #    # Forward email and credentials to landing page.
+    #    self.redirect(self.url(self.context, 'registration_complete',
+    #        data = dict(email=email, password=password,
+    #        applicant_id=applicant_id)))
+    #    return
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/export.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/export.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/export.py	(revision 17238)
@@ -0,0 +1,48 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""Exporters for applicant-related stuff.
+"""
+import grok
+from waeup.kofa.applicants.interfaces import IApplicantBaseData
+from waeup.kofa.applicants.export import ApplicantExporter
+from waeup.kofa.utils.helpers import iface_names
+from kofacustom.nigeria.applicants.interfaces import (
+    INigeriaUGApplicant, INigeriaPGApplicant)
+
+class NigeriaApplicantExporter(ApplicantExporter):
+    """Exporter for Nigeria Applicants.
+    """
+
+    # In contrast to ApplicantExporter we export all fields
+    # and attributes of the interfaces.
+    fields = tuple(sorted(set(
+        iface_names(INigeriaUGApplicant) +
+        iface_names(INigeriaPGApplicant) +
+        iface_names(IApplicantBaseData)
+        ))) + (
+        'password', 'state', 'history', 'container_code', 'application_number',
+        'display_fullname', 'application_date')
+
+    def mangle_value(self, value, name, context=None):
+        if name == 'jamb_subjects' and value:
+            value = value.replace('\n', '++')
+        if '_result' in name and value:
+            value = [eval(entry.to_string()) for entry in value]
+        return super(
+            NigeriaApplicantExporter, self).mangle_value(
+            value, name, context=context)
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/interfaces.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/interfaces.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/interfaces.py	(revision 17238)
@@ -0,0 +1,826 @@
+## $Id$
+##
+## Copyright (C) 2011 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
+##
+"""Customized interfaces of the university application package.
+"""
+
+from zope import schema
+from zope.component import getUtility
+from zope.schema import getFields
+from waeup.kofa.applicants.interfaces import (
+    contextual_reg_num_source,
+    IApplicantBaseData,
+    AppCatCertificateSource, CertificateSource)
+from waeup.kofa.schoolgrades import ResultEntryField
+from waeup.kofa.interfaces import (
+    SimpleKofaVocabulary,
+    academic_sessions_vocab,
+    validate_email,
+    SubjectSource,
+    IKofaUtils,
+    IKofaObject)
+from waeup.kofa.schema import FormattedDate, TextLineChoice
+from waeup.kofa.students.vocabularies import nats_vocab, GenderSource
+from kofacustom.nigeria.interfaces import (
+    LGASource, DisabilitiesSource,
+    high_qual, high_grade, exam_types, validate_jamb_reg_number)
+from kofacustom.nigeria.interfaces import MessageFactory as _
+from kofacustom.nigeria.payments.interfaces import INigeriaOnlinePayment
+
+programme_types_vocab = SimpleKofaVocabulary(
+    (_('Post UTME'), 'putme'),
+    (_('Post DE'), 'pude'),
+    (_('Admission Screening Exercise'), 'ase'),
+    (_('not applicable'), 'na'),
+    )
+
+jambsubjects = SimpleKofaVocabulary(
+    (_('Use of English'),'english_language'),
+    (_('Agricultural Science'),'agricultural_science'),
+    (_('Arabic'),'arabic'),
+    (_('Biology'),'biology'),
+    (_('Book Keeping'),'book_keeping'),
+    (_('Chemistry'),'chemistry'),
+    (_('Christian Religious Studies'),'christian_religious_studies'),
+    (_('Commerce'),'commerce'),
+    (_('Economics'),'economics'),
+    (_('Financial Accounting'),'financial_accounting'),
+    (_('Fine Art'),'fine_art'),
+    (_('Food and Nutrition'),'food_and_nutrition'),
+    (_('French'),'french'),
+    (_('Geography'),'geography'),
+    (_('German'),'german'),
+    (_('Government'),'government'),
+    (_('Hausa'),'hausa'),
+    (_('Home Economics'),'home_economics'),
+    (_('History'),'history'),
+    (_('Igbo'),'igbo'),
+    (_('Literature in English'),'literature_in_english'),
+    (_('Literature in Nigerian Languages'),'literature_in_nigerian_languages'),
+    (_('Mathematics'),'mathematics'),
+    (_('Music'),'music'),
+    (_('Physics'),'physics'),
+    (_('Yoruba'),'yoruba'),
+    )
+
+# Define a validation method for jamb subjects
+class NotExactNumberOfItems(schema.ValidationError):
+    __doc__ = u"Not exactly 4 items selected."
+
+def four_items(value):
+    if len(value) and len(value) != 4:
+        raise NotExactNumberOfItems(value)
+    return True
+
+class JAMBSubjectSource(SubjectSource):
+    """A source for school subjects used in exam documentation.
+    """
+
+    def getTitle(self, value):
+        subjects_dict = getUtility(IKofaUtils).EXAM_SUBJECTS_DICT
+        return "%s" % subjects_dict[value]
+
+# Fields to be omitted in all display forms. course_admitted is
+# rendered separately.
+
+OMIT_DISPLAY_FIELDS = ('locked', 'course_admitted',
+    'result_uploaded', 'suspended', 'special_application',
+    'bank_account_number',
+    'bank_account_name',
+    'bank_name')
+
+# UG students are all undergraduate students.
+UG_OMIT_DISPLAY_FIELDS = OMIT_DISPLAY_FIELDS + (
+    'jamb_subjects_list', 'programme_type')
+UG_OMIT_PDF_FIELDS = UG_OMIT_DISPLAY_FIELDS + ('phone',)
+UG_OMIT_MANAGE_FIELDS = (
+    'special_application',
+    'jamb_subjects_list',
+    'programme_type')
+UG_OMIT_EDIT_FIELDS = UG_OMIT_MANAGE_FIELDS + OMIT_DISPLAY_FIELDS + (
+    'student_id',
+    'notice',
+    'screening_score',
+    'screening_venue',
+    'screening_date',
+    'jamb_age',
+    'jamb_subjects',
+    'jamb_score',
+    'jamb_reg_number',
+    'aggregate')
+
+# CBT is a subgroup of UG with the same interface.
+CBT_OMIT_FIELDS = (
+    'hq_type', 'hq_matric_no',
+    'hq_degree', 'hq_school',
+    'hq_session', 'hq_disc',
+    'aggregate', 'jamb_subjects',
+    'jamb_subjects_list',
+    'jamb_score',
+    'jamb_reg_number',
+    'programme_type',
+    'fst_sit_fname',
+    'fst_sit_no',
+    'fst_sit_date',
+    'fst_sit_type',
+    'fst_sit_results',
+    'scd_sit_fname',
+    'scd_sit_no',
+    'scd_sit_date',
+    'scd_sit_type',
+    'scd_sit_results',
+    'course2',
+    'screening_venue',
+    'screening_date',
+    'screening_score',
+    'bank_account_number',
+    'bank_account_name',
+    'bank_name',
+    'disabilities',
+    )
+CBT_OMIT_DISPLAY_FIELDS = OMIT_DISPLAY_FIELDS + CBT_OMIT_FIELDS
+CBT_OMIT_MANAGE_FIELDS = CBT_OMIT_FIELDS + ('special_application',)
+CBT_OMIT_EDIT_FIELDS = OMIT_DISPLAY_FIELDS + CBT_OMIT_FIELDS + (
+    'special_application',
+    'student_id',
+    'notice',
+    #'jamb_age',
+    #'jamb_subjects',
+    #'jamb_score',
+    #'jamb_reg_number',
+    )
+CBT_OMIT_PDF_FIELDS = CBT_OMIT_DISPLAY_FIELDS + ('phone',)
+
+# AFFIL is a subgroup of UG with the same interface.
+AFFIL_OMIT_FIELDS = (
+    'hq_type', 'hq_matric_no',
+    'hq_degree', 'hq_school',
+    'hq_session', 'hq_disc',
+    'jamb_subjects')
+AFFIL_OMIT_DISPLAY_FIELDS = OMIT_DISPLAY_FIELDS + AFFIL_OMIT_FIELDS
+AFFIL_OMIT_MANAGE_FIELDS = AFFIL_OMIT_FIELDS + ('special_application',)
+AFFIL_OMIT_EDIT_FIELDS = OMIT_DISPLAY_FIELDS + AFFIL_OMIT_FIELDS + (
+    'special_application',
+    'student_id',
+    'notice',
+    'screening_score',
+    'screening_venue',
+    'screening_date',
+    'aggregate',
+    )
+AFFIL_OMIT_PDF_FIELDS = AFFIL_OMIT_DISPLAY_FIELDS + ('phone',)
+
+# PUTME is a subgroup of UG with the same interface.
+PUTME_OMIT_FIELDS = (
+    'hq_type', 'hq_matric_no',
+    'hq_degree', 'hq_school',
+    'hq_session', 'hq_disc',
+    'jamb_subjects_list', 'programme_type')
+
+PUTME_OMIT_DISPLAY_FIELDS = OMIT_DISPLAY_FIELDS + PUTME_OMIT_FIELDS
+
+# temporary solution to display bank account fields only
+# for PUTME (ase) application
+#PUTME_OMIT_DISPLAY_FIELDS = (
+#    'locked', 'course_admitted', 'result_uploaded',
+#    'suspended', 'special_application') + PUTME_OMIT_FIELDS
+
+PUTME_OMIT_MANAGE_FIELDS = UG_OMIT_MANAGE_FIELDS + PUTME_OMIT_FIELDS
+
+PUTME_OMIT_EDIT_FIELDS = UG_OMIT_EDIT_FIELDS + PUTME_OMIT_FIELDS + (
+    'firstname', 'middlename', 'lastname', 'sex',
+    'course1', 'lga')
+
+# temporary solution to display bank account fields only
+# for PUTME (ase) application
+#PUTME_OMIT_EDIT_FIELDS = UG_OMIT_MANAGE_FIELDS  + (
+#    'locked', 'course_admitted', 'result_uploaded',
+#    'suspended', 'special_application',
+#    'student_id',
+#    'notice',
+#    'screening_score',
+#    'screening_venue',
+#    'screening_date',
+#    'jamb_age',
+#    'jamb_subjects',
+#    'jamb_score',
+#    'jamb_reg_number',
+#    'aggregate',
+#    'firstname', 'middlename', 'lastname', 'sex',
+#    'course1', 'lga') + PUTME_OMIT_FIELDS
+
+PUTME_OMIT_PDF_FIELDS = PUTME_OMIT_DISPLAY_FIELDS + ('phone',)
+PUTME_OMIT_RESULT_SLIP_FIELDS = PUTME_OMIT_DISPLAY_FIELDS + (
+    'phone',
+    'date_of_birth', 'sex',
+    'nationality', 'lga', #'perm_address',
+    'course2', 'screening_venue',
+    'screening_date')
+
+# PUDE is a subgroup of UG with the same interface.
+PUDE_OMIT_FIELDS = (
+    'jamb_subjects',
+    'jamb_score',
+    'jamb_age',
+    'aggregate',
+    'jamb_subjects_list',
+    'programme_type')
+PUDE_OMIT_DISPLAY_FIELDS = OMIT_DISPLAY_FIELDS + PUDE_OMIT_FIELDS
+PUDE_OMIT_MANAGE_FIELDS = UG_OMIT_MANAGE_FIELDS + PUDE_OMIT_FIELDS
+PUDE_OMIT_EDIT_FIELDS = set(UG_OMIT_EDIT_FIELDS + PUDE_OMIT_FIELDS + (
+    'firstname', 'middlename', 'lastname', 'sex',
+    'course1', 'lga'))
+PUDE_OMIT_PDF_FIELDS = PUDE_OMIT_DISPLAY_FIELDS + ('phone',)
+PUDE_OMIT_RESULT_SLIP_FIELDS = PUDE_OMIT_DISPLAY_FIELDS + (
+    'phone',
+    'date_of_birth', 'sex',
+    'nationality', 'lga', #'perm_address',
+    'course2', 'screening_venue',
+    'screening_date')
+
+# PG has its own interface
+PG_OMIT_DISPLAY_FIELDS = OMIT_DISPLAY_FIELDS
+PG_OMIT_PDF_FIELDS = PG_OMIT_DISPLAY_FIELDS + ('phone',)
+PG_OMIT_MANAGE_FIELDS = ('special_application',)
+PG_OMIT_EDIT_FIELDS = PG_OMIT_MANAGE_FIELDS + PG_OMIT_DISPLAY_FIELDS + (
+    'student_id', 'notice',
+    'screening_score', 'screening_venue',
+    'screening_date',)
+
+class IBankAccount(IKofaObject):
+
+    bank_name = schema.TextLine(
+        title = _(u'Bank Name'),
+        required = False,
+        readonly = False,
+        )
+
+    bank_account_name = schema.TextLine(
+        title = _(u'Bank Account Name'),
+        required = False,
+        readonly = False,
+        )
+
+    bank_account_number = schema.TextLine(
+        title = _(u'Bank Account Number'),
+        required = False,
+        readonly = False,
+        )
+
+class INigeriaUGApplicant(IApplicantBaseData, IBankAccount):
+    """An undergraduate applicant.
+
+    This interface defines the least common multiple of all fields
+    in ug application forms. In customized forms, fields can be excluded by
+    adding them to the UG_OMIT* tuples.
+    """
+
+    disabilities = schema.Choice(
+        title = _(u'Disability'),
+        source = DisabilitiesSource(),
+        required = False,
+        )
+    nationality = schema.Choice(
+        source = nats_vocab,
+        title = _(u'Nationality'),
+        required = False,
+        )
+    lga = schema.Choice(
+        source = LGASource(),
+        title = _(u'State/LGA (Nigerians only)'),
+        required = False,
+        )
+    #perm_address = schema.Text(
+    #    title = _(u'Permanent Address'),
+    #    required = False,
+    #    )
+    course1 = schema.Choice(
+        title = _(u'1st Choice Course of Study'),
+        source = AppCatCertificateSource(),
+        required = True,
+        )
+    course2 = schema.Choice(
+        title = _(u'2nd Choice Course of Study'),
+        source = AppCatCertificateSource(),
+        required = False,
+        )
+
+    programme_type = schema.Choice(
+        title = _(u'Programme Type'),
+        vocabulary = programme_types_vocab,
+        required = False,
+        )
+
+    hq_type = schema.Choice(
+        title = _(u'Qualification Obtained'),
+        required = False,
+        readonly = False,
+        vocabulary = high_qual,
+        )
+    hq_matric_no = schema.TextLine(
+        title = _(u'Former Matric Number'),
+        required = False,
+        readonly = False,
+        )
+    hq_degree = schema.Choice(
+        title = _(u'Class of Degree'),
+        required = False,
+        readonly = False,
+        vocabulary = high_grade,
+        )
+    hq_school = schema.TextLine(
+        title = _(u'Institution Attended'),
+        required = False,
+        readonly = False,
+        )
+    hq_session = schema.TextLine(
+        title = _(u'Years Attended'),
+        required = False,
+        readonly = False,
+        )
+    hq_disc = schema.TextLine(
+        title = _(u'Discipline'),
+        required = False,
+        readonly = False,
+        )
+    jamb_subjects = schema.Text(
+        title = _(u'Subjects and Scores'),
+        required = False,
+        )
+    jamb_subjects_list = schema.List(
+        title = _(u'JAMB Subjects'),
+        required = False,
+        defaultFactory=list,
+        value_type = schema.Choice(
+            vocabulary = jambsubjects
+            #source = JAMBSubjectSource(),
+            ),
+        )
+    jamb_score = schema.Float(
+        title = _(u'Total JAMB Score'),
+        required = False,
+        )
+    #jamb_age = schema.Int(
+    #    title = _(u'Age (provided by JAMB)'),
+    #    required = False,
+    #    )
+    jamb_reg_number = schema.TextLine(
+        title = _(u'JAMB Registration Number'),
+        required = False,
+        constraint=validate_jamb_reg_number,
+        )
+    notice = schema.Text(
+        title = _(u'Notice'),
+        required = False,
+        )
+    screening_venue = schema.TextLine(
+        title = _(u'Screening Venue'),
+        required = False,
+        )
+    screening_date = schema.TextLine(
+        title = _(u'Screening Date'),
+        required = False,
+        )
+    screening_score = schema.Float(
+        title = _(u'Screening Score (%)'),
+        required = False,
+        )
+    aggregate = schema.Float(
+        title = _(u'Aggregate Score (%)'),
+        description = _(u'(average of relative JAMB and PUTME scores)'),
+        required = False,
+        )
+    result_uploaded = schema.Bool(
+        title = _(u'Result uploaded'),
+        default = False,
+        required = False,
+        )
+    student_id = schema.TextLine(
+        title = _(u'Student Id'),
+        required = False,
+        readonly = False,
+        )
+    course_admitted = schema.Choice(
+        title = _(u'Admitted Course of Study'),
+        source = CertificateSource(),
+        required = False,
+        )
+    locked = schema.Bool(
+        title = _(u'Form locked'),
+        default = False,
+        required = False,
+        )
+
+INigeriaUGApplicant[
+    'locked'].order =  IApplicantBaseData['suspended'].order
+INigeriaUGApplicant[
+    'result_uploaded'].order =  INigeriaUGApplicant['suspended'].order
+
+class INigeriaPGApplicant(IApplicantBaseData):
+    """A postgraduate applicant.
+
+    This interface defines the least common multiple of all fields
+    in pg application forms. In customized forms, fields can be excluded by
+    adding them to the PG_OMIT* tuples.
+    """
+
+    nationality = schema.Choice(
+        source = nats_vocab,
+        title = _(u'Nationality'),
+        required = True,
+        )
+    lga = schema.Choice(
+        source = LGASource(),
+        title = _(u'State/LGA (Nigerians only)'),
+        required = False,
+        )
+    #perm_address = schema.Text(
+    #    title = _(u'Permanent Address'),
+    #    required = False,
+    #    )
+    course1 = schema.Choice(
+        title = _(u'1st Choice Course of Study'),
+        source = AppCatCertificateSource(),
+        required = True,
+        )
+    course2 = schema.Choice(
+        title = _(u'2nd Choice Course of Study'),
+        source = AppCatCertificateSource(),
+        required = False,
+        )
+    hq_type = schema.Choice(
+        title = _(u'Qualification Obtained'),
+        required = False,
+        readonly = False,
+        vocabulary = high_qual,
+        )
+    hq_fname = schema.TextLine(
+        title = _(u'Full Name'),
+        required = False,
+        readonly = False,
+        )
+    hq_matric_no = schema.TextLine(
+        title = _(u'Former Matric Number'),
+        required = False,
+        readonly = False,
+        )
+    hq_degree = schema.Choice(
+        title = _(u'Class of Degree'),
+        required = False,
+        readonly = False,
+        vocabulary = high_grade,
+        )
+    hq_school = schema.TextLine(
+        title = _(u'Institution Attended'),
+        required = False,
+        readonly = False,
+        )
+    hq_session = schema.TextLine(
+        title = _(u'Years Attended'),
+        required = False,
+        readonly = False,
+        )
+    hq_disc = schema.TextLine(
+        title = _(u'Discipline'),
+        required = False,
+        readonly = False,
+        )
+    fst_sit_fname = schema.TextLine(
+        title = _(u'Full Name'),
+        required = False,
+        readonly = False,
+        )
+    fst_sit_no = schema.TextLine(
+        title = _(u'Exam Number'),
+        required = False,
+        readonly = False,
+        )
+    fst_sit_date = FormattedDate(
+        title = _(u'Exam Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+    fst_sit_type = schema.Choice(
+        title = _(u'Exam Type'),
+        required = False,
+        readonly = False,
+        vocabulary = exam_types,
+        )
+    fst_sit_results = schema.List(
+        title = _(u'Exam Results'),
+        value_type = ResultEntryField(),
+        required = False,
+        readonly = False,
+        defaultFactory=list,
+        )
+    scd_sit_fname = schema.TextLine(
+        title = _(u'Full Name'),
+        required = False,
+        readonly = False,
+        )
+    scd_sit_no = schema.TextLine(
+        title = _(u'Exam Number'),
+        required = False,
+        readonly = False,
+        )
+    scd_sit_date = FormattedDate(
+        title = _(u'Exam Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+    scd_sit_type = schema.Choice(
+        title = _(u'Exam Type'),
+        required = False,
+        readonly = False,
+        vocabulary = exam_types,
+        )
+    scd_sit_results = schema.List(
+        title = _(u'Exam Results'),
+        value_type = ResultEntryField(),
+        required = False,
+        readonly = False,
+        defaultFactory=list,
+        )
+    # Replaced by first and second sitting
+    #pp_school = schema.Choice(
+    #    title = _(u'Qualification Obtained'),
+    #    required = False,
+    #    readonly = False,
+    #    vocabulary = exam_types,
+    #    )
+    presently_inst = schema.TextLine(
+        title = _(u'If yes, name of institution'),
+        required = False,
+        readonly = False,
+        )
+    nysc_year = schema.Int(
+        title = _(u'Nysc Year'),
+        required = False,
+        readonly = False,
+        )
+    nysc_lga = schema.Choice(
+        source = LGASource(),
+        title = _(u'Nysc Location'),
+        description = _(u'Leave blank for exception letters.'),
+        required = False,
+        )
+    employer = schema.TextLine(
+        title = _(u'Employer'),
+        required = False,
+        readonly = False,
+        )
+    emp_position = schema.TextLine(
+        title = _(u'Employer Position'),
+        required = False,
+        readonly = False,
+        )
+    emp_start = FormattedDate(
+        title = _(u'Start Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+    emp_end = FormattedDate(
+        title = _(u'End Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+    emp_reason = schema.TextLine(
+        title = _(u'Reason for Leaving'),
+        required = False,
+        readonly = False,
+        )
+    employer2 = schema.TextLine(
+        title = _(u'2nd Employer'),
+        required = False,
+        readonly = False,
+        )
+    emp2_position = schema.TextLine(
+        title = _(u'2nd Employer Position'),
+        required = False,
+        readonly = False,
+        )
+    emp2_start = FormattedDate(
+        title = _(u'Start Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+    emp2_end = FormattedDate(
+        title = _(u'End Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+    emp2_reason = schema.TextLine(
+        title = _(u'Reason for Leaving'),
+        required = False,
+        readonly = False,
+        )
+    notice = schema.Text(
+        title = _(u'Notice'),
+        required = False,
+        readonly = False,
+        )
+    screening_venue = schema.TextLine(
+        title = _(u'Screening Venue'),
+        required = False,
+        )
+    screening_date = schema.TextLine(
+        title = _(u'Screening Date'),
+        required = False,
+        )
+    screening_score = schema.Float(
+        title = _(u'Screening Score (%)'),
+        required = False,
+        )
+    student_id = schema.TextLine(
+        title = _(u'Student Id'),
+        required = False,
+        readonly = False,
+        )
+    course_admitted = schema.Choice(
+        title = _(u'Admitted Course of Study'),
+        source = CertificateSource(),
+        required = False,
+        readonly = False,
+        )
+    locked = schema.Bool(
+        title = _(u'Form locked'),
+        default = False,
+        required = False,
+        )
+
+INigeriaPGApplicant[
+    'locked'].order =  IApplicantBaseData['suspended'].order
+
+# PRE is used by Uniben
+#  med: "it is as named... PRE - DEGREE... much like a 1 year diploma programme"
+PRE_OMIT_FIELDS = tuple([
+    i for i in getFields(INigeriaPGApplicant).keys()
+        if i[:3] in ('hq_', 'emp', 'nys')]) + ('referees',)
+PRE_OMIT_DISPLAY_FIELDS = OMIT_DISPLAY_FIELDS + PRE_OMIT_FIELDS
+PRE_OMIT_MANAGE_FIELDS = PG_OMIT_MANAGE_FIELDS + PRE_OMIT_FIELDS
+PRE_OMIT_EDIT_FIELDS = set(PG_OMIT_EDIT_FIELDS + PRE_OMIT_FIELDS)
+PRE_OMIT_PDF_FIELDS = PRE_OMIT_DISPLAY_FIELDS
+PRE_OMIT_RESULT_SLIP_FIELDS = PRE_OMIT_DISPLAY_FIELDS
+
+class INigeriaApplicant(INigeriaUGApplicant, INigeriaPGApplicant):
+    """An interface for both types of applicants.
+
+    Attention: The INigeriaPGApplicant field seetings will be overwritten
+    by INigeriaPGApplicant field settings. If a field is defined
+    in both interfaces zope.schema validates only against the
+    constraints in INigeriaUGApplicant. This does not affect the forms
+    since they are build on either INigeriaUGApplicant or INigeriaPGApplicant.
+    """
+
+    def writeLogMessage(view, comment):
+        """Adds an INFO message to the log file
+        """
+
+    def createStudent():
+        """Create a student object from applicant data
+        and copy applicant object.
+        """
+
+class INigeriaUGApplicantEdit(INigeriaUGApplicant):
+    """An undergraduate applicant interface for edit forms.
+
+    Here we can repeat the fields from base data and set the
+    `required` and `readonly` attributes to True to further restrict
+    the data access. Or we can allow only certain certificates to be
+    selected by choosing the appropriate source.
+
+    We cannot omit fields here. This has to be done in the
+    respective form page.
+    """
+
+    email = schema.ASCIILine(
+        title = _(u'Email Address'),
+        required = True,
+        constraint=validate_email,
+        )
+    date_of_birth = FormattedDate(
+        title = _(u'Date of Birth'),
+        required = True,
+        show_year = True,
+        )
+    jamb_subjects_list = schema.List(
+        title = _(u'JAMB Subjects'),
+        description = _(u'Select four subjects.'),
+        required = True,
+        constraint = four_items,
+        value_type = schema.Choice(
+            vocabulary = jambsubjects
+            #source = JAMBSubjectSource(),
+            ),
+        )
+
+INigeriaUGApplicantEdit[
+    'date_of_birth'].order = INigeriaUGApplicant['date_of_birth'].order
+INigeriaUGApplicantEdit[
+    'email'].order = INigeriaUGApplicant['email'].order
+INigeriaUGApplicantEdit[
+    'jamb_subjects_list'].order = INigeriaUGApplicant['jamb_subjects_list'].order
+
+class INigeriaPGApplicantEdit(INigeriaPGApplicant):
+    """A postgraduate applicant interface for editing.
+
+    Here we can repeat the fields from base data and set the
+    `required` and `readonly` attributes to True to further restrict
+    the data access. Or we can allow only certain certificates to be
+    selected by choosing the appropriate source.
+
+    We cannot omit fields here. This has to be done in the
+    respective form page.
+    """
+
+    email = schema.ASCIILine(
+        title = _(u'Email Address'),
+        required = True,
+        constraint=validate_email,
+        )
+    date_of_birth = FormattedDate(
+        title = _(u'Date of Birth'),
+        required = True,
+        show_year = True,
+        )
+
+INigeriaPGApplicantEdit[
+    'date_of_birth'].order =  INigeriaPGApplicant['date_of_birth'].order
+INigeriaPGApplicantEdit[
+    'email'].order =  INigeriaPGApplicant['email'].order
+
+class INigeriaApplicantOnlinePayment(INigeriaOnlinePayment):
+    """An applicant payment via payment gateways.
+    """
+
+    p_split_data = schema.Text(
+        title = _(u'Split Payment Data'),
+        required = False,
+        readonly = False,
+        )
+
+
+class IPUTMEApplicantEdit(INigeriaUGApplicant):
+    """An undergraduate applicant interface for editing.
+
+    Here we can repeat the fields from base data and set the
+    `required` and `readonly` attributes to True to further restrict
+    the data access. Or we can allow only certain certificates to be
+    selected by choosing the appropriate source.
+
+    We cannot omit fields here. This has to be done in the
+    respective form page.
+    """
+    email = schema.ASCIILine(
+        title = _(u'Email Address'),
+        required = True,
+        constraint=validate_email,
+        )
+    date_of_birth = FormattedDate(
+        title = _(u'Date of Birth'),
+        required = True,
+        show_year = True,
+        )
+    nationality = schema.Choice(
+        source = nats_vocab,
+        title = _(u'Nationality'),
+        required = True,
+        )
+
+IPUTMEApplicantEdit[
+    'date_of_birth'].order =  INigeriaUGApplicant['date_of_birth'].order
+IPUTMEApplicantEdit[
+    'email'].order =  INigeriaUGApplicant['email'].order
+IPUTMEApplicantEdit[
+    'nationality'].order =  INigeriaUGApplicant['nationality'].order
+
+class INigeriaApplicantUpdateByRegNo(INigeriaApplicant):
+    """Representation of an applicant.
+
+    Skip regular reg_number validation if reg_number is used for finding
+    the applicant object.
+    """
+    reg_number = schema.TextLine(
+        title = u'Registration Number',
+        required = False,
+        )
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/payment.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/payment.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/payment.py	(revision 17238)
@@ -0,0 +1,59 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""
+Application fee payment.
+"""
+import grok
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.kofa.applicants.payment import ApplicantOnlinePayment
+from waeup.kofa.utils.helpers import attrs_to_fields
+from kofacustom.nigeria.applicants.interfaces import INigeriaApplicantOnlinePayment
+
+class NigeriaApplicantOnlinePayment(ApplicantOnlinePayment):
+    """This is a custom online payment for applicants.
+
+    Since this class inherits from ApplicantOnlinePayment, it automatically
+    implements the interfaces of the parent class which means
+    all viewlets and views meant for StudentOnlinePayment
+    can also be accessed in the customized system.
+    """
+    grok.implements(INigeriaApplicantOnlinePayment)
+    grok.provides(INigeriaApplicantOnlinePayment)
+
+    def __init__(self):
+        super(NigeriaApplicantOnlinePayment, self).__init__()
+        return
+
+NigeriaApplicantOnlinePayment = attrs_to_fields(
+    NigeriaApplicantOnlinePayment, omit=['display_item',])
+
+# Applicant online payments must be importable. So we might need a factory.
+class NigeriaApplicantOnlinePaymentFactory(grok.GlobalUtility):
+    """A factory for applicant online payments.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.ApplicantOnlinePayment')
+    title = u"Create a new online payment.",
+    description = u"This factory instantiates new online payment instances."
+
+    def __call__(self, *args, **kw):
+        return NigeriaApplicantOnlinePayment()
+
+    def getInterfaces(self):
+        return implementedBy(NigeriaApplicantOnlinePayment)
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/tests/__init__.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/tests/__init__.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/tests/__init__.py	(revision 17238)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/tests/test_applicant.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/tests/test_applicant.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/tests/test_applicant.py	(revision 17238)
@@ -0,0 +1,79 @@
+## $Id$
+##
+## Copyright (C) 2011 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
+##
+"""Tests for applicants and related.
+"""
+import grok
+import shutil
+import tempfile
+from StringIO import StringIO
+from zope.component import adapts, queryUtility
+from zope.component.interfaces import IFactory
+from zope.interface import verify, implements
+from zope.location.interfaces import ILocation
+from waeup.kofa.image.interfaces import IKofaImageFile
+from waeup.kofa.imagestorage import DefaultStorage
+from waeup.kofa.interfaces import IFileStoreHandler, IFileStoreNameChooser
+from waeup.kofa.testing import FunctionalTestCase
+from kofacustom.nigeria.applicants.applicant import (
+    NigeriaApplicant, NigeriaApplicantFactory,
+    )
+from kofacustom.nigeria.applicants.interfaces import INigeriaApplicant
+from kofacustom.nigeria.testing import FunctionalLayer
+
+class NigeriaApplicantTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(NigeriaApplicantTest, self).setUp()
+        self.applicant = NigeriaApplicant()
+        return
+
+    def tearDown(self):
+        super(NigeriaApplicantTest, self).tearDown()
+        return
+
+    def test_interfaces(self):
+        verify.verifyClass(INigeriaApplicant, NigeriaApplicant)
+        verify.verifyObject(INigeriaApplicant, self.applicant)
+        return
+
+class NigeriaApplicantFactoryTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(NigeriaApplicantFactoryTest, self).setUp()
+        self.factory = NigeriaApplicantFactory()
+        return
+
+    def tearDown(self):
+        super(NigeriaApplicantFactoryTest, self).tearDown()
+        return
+
+    def test_interfaces(self):
+        verify.verifyClass(IFactory, NigeriaApplicantFactory)
+        verify.verifyObject(IFactory, self.factory)
+
+    def test_factory(self):
+        obj = self.factory()
+        assert isinstance(obj, NigeriaApplicant)
+
+    def test_getInterfaces(self):
+        implemented_by = self.factory.getInterfaces()
+        assert implemented_by.isOrExtends(INigeriaApplicant)
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/tests/test_applicantcopier.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/tests/test_applicantcopier.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/tests/test_applicantcopier.py	(revision 17238)
@@ -0,0 +1,83 @@
+## $Id$
+##
+## Copyright (C) 2011 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
+##
+"""
+Tests for the creation of student containers with data from admitted applicants.
+"""
+import os
+import grok
+from datetime import datetime
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from zope.event import notify
+from zope.component import getUtility
+from zope.i18n import translate
+from waeup.kofa.interfaces import IExtFileStore, IFileStoreNameChooser
+from waeup.kofa.applicants.tests.test_browser import ApplicantsFullSetup
+from kofacustom.nigeria.testing import FunctionalLayer
+
+session = datetime.now().year - 2
+
+class ApplicantCopierFunctionalTests(ApplicantsFullSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantCopierFunctionalTests, self).setUp()
+        return
+
+    def test_copier_ume(self):
+        IWorkflowState(self.applicant).setState('admitted')
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_path)
+        self.fill_correct_values()
+        self.browser.getControl("Save").click()
+        self.browser.getControl(name="form.nationality").value = ['NG']
+        self.browser.getControl(name="form.course_admitted").value = ['CERT1']
+        self.browser.getControl("Save").click()
+        (success, msg) = self.applicant.createStudent()
+        student_id = translate(msg, 'waeup.kofa').split()[1]
+        student = self.app['students'][student_id]
+        self.assertEqual(student.state, 'admitted')
+        # Has the ume student studycourse the correct attributes?
+        self.assertEqual(student['studycourse'].certificate.code, 'CERT1')
+        self.assertEqual(student['studycourse'].entry_session, session)
+        self.assertEqual(student['studycourse'].entry_mode, 'ug_ft')
+        self.assertEqual(student['studycourse'].current_session, session)
+        self.assertEqual(student['studycourse'].current_level, 100)
+
+    def test_copier_de(self):
+        IWorkflowState(self.applicant).setState('admitted')
+        # The applicant_id decides how studycourse attributes
+        # are being set.
+        self.applicant.applicant_id = u'pude2010_anything'
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_path)
+        self.fill_correct_values()
+        self.browser.getControl("Save").click()
+        self.browser.getControl(name="form.nationality").value = ['NG']
+        self.browser.getControl(name="form.course_admitted").value = ['CERT1']
+        self.browser.getControl("Save").click()
+        (success, msg) = self.applicant.createStudent()
+        student_id = translate(msg, 'waeup.kofa').split()[1]
+        student = self.app['students'][student_id]
+        self.assertEqual(student.state, 'admitted')
+        # Has the ume student studycourse the correct attributes?
+        self.assertEqual(student['studycourse'].certificate.code, 'CERT1')
+        self.assertEqual(student['studycourse'].entry_session, session)
+        self.assertEqual(student['studycourse'].entry_mode, 'de_ft')
+        self.assertEqual(student['studycourse'].current_session, session)
+        self.assertEqual(student['studycourse'].current_level, 200)
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/tests/test_browser.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/tests/test_browser.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/tests/test_browser.py	(revision 17238)
@@ -0,0 +1,496 @@
+## $Id$
+##
+## Copyright (C) 2011 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 os
+import shutil
+import tempfile
+import datetime
+import grok
+import pytz
+from zope.event import notify
+from zope.intid.interfaces import IIntIds
+from zope.interface.verify import verifyClass, verifyObject
+from zope.schema.interfaces import ConstraintNotSatisfied
+from zope.component.hooks import setSite, clearSite
+from zope.component import createObject, getUtility
+from zope.catalog.interfaces import ICatalog
+from zope.testbrowser.testing import Browser
+from hurry.workflow.interfaces import IWorkflowState
+from waeup.kofa.app import University
+from waeup.kofa.university.faculty import Faculty
+from waeup.kofa.university.department import Department
+from waeup.kofa.testing import FunctionalTestCase
+from waeup.kofa.configuration import SessionConfiguration
+from waeup.kofa.applicants.container import ApplicantsContainer
+from waeup.kofa.applicants.tests.test_batching import ApplicantImportExportSetup
+from waeup.kofa.interfaces import IBatchProcessor, IUserAccount
+from kofacustom.nigeria.testing import FunctionalLayer
+from kofacustom.nigeria.applicants.export import NigeriaApplicantExporter
+from kofacustom.nigeria.applicants.batching import NigeriaApplicantProcessor
+
+session_1 = datetime.datetime.now().year - 2
+app_container_name = u'app%s' % session_1
+pgft_container_name = u'pgft%s' % session_1
+cbt_container_name = u'cbt%s' % session_1
+
+class ApplicantUITest(FunctionalTestCase):
+    """Perform some browser tests.
+    """
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantUITest, self).setUp()
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        # we add the site immediately after creation to the
+        # ZODB. Catalogs and other local utilities are not setup
+        # before that step.
+        self.app = self.getRootFolder()['app']
+        # Set site here. Some of the following setup code might need
+        # to access grok.getSite() and should get our new app then
+        setSite(app)
+
+        # Add three different applicants containers
+        self.pgcontainer = ApplicantsContainer()
+        self.pgcontainer.code = pgft_container_name
+        self.pgcontainer.prefix = u'pgft'
+        self.pgcontainer.application_category = u'pg_ft'
+        self.pgcontainer.year = session_1
+        self.pgcontainer.application_fee = 300.0
+        self.pgcontainer.title = u'This is the %s container' % pgft_container_name
+        self.app['applicants'][pgft_container_name] = self.pgcontainer
+
+        self.ugcontainer = ApplicantsContainer()
+        self.ugcontainer.code = app_container_name
+        self.ugcontainer.prefix = u'app'
+        self.ugcontainer.application_category = u'basic'
+        self.ugcontainer.year = session_1
+        self.ugcontainer.application_fee = 200.0
+        self.ugcontainer.title = u'This is the %s container' % app_container_name
+        self.app['applicants'][app_container_name] = self.ugcontainer
+        self.ugcontainer.mode = 'update'
+        delta = datetime.timedelta(days=10)
+        self.ugcontainer.startdate = datetime.datetime.now(pytz.utc) - delta
+        self.ugcontainer.enddate = datetime.datetime.now(pytz.utc) + delta
+
+        self.cbtcontainer = ApplicantsContainer()
+        self.cbtcontainer.code = cbt_container_name
+        self.cbtcontainer.prefix = u'cbt'
+        self.cbtcontainer.application_category = u'basic'
+        self.cbtcontainer.year = session_1
+        self.cbtcontainer.application_fee = 300.0
+        self.cbtcontainer.title = u'This is the %s container' % cbt_container_name
+        self.app['applicants'][cbt_container_name] = self.cbtcontainer
+        self.cbtcontainer.mode = 'update'
+        delta = datetime.timedelta(days=10)
+        self.cbtcontainer.startdate = datetime.datetime.now(pytz.utc) - delta
+        self.cbtcontainer.enddate = datetime.datetime.now(pytz.utc) + delta
+
+        # Populate university
+        self.certificate = createObject('waeup.Certificate')
+        self.certificate.code = 'CERT1'
+        self.certificate.application_category = 'basic'
+        self.certificate.start_level = 100
+        self.certificate.end_level = 500
+        self.certificate.study_mode = u'ug_ft'
+        self.app['faculties']['fac1'] = Faculty()
+        self.app['faculties']['fac1']['dep1'] = Department()
+        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
+            self.certificate)
+        self.certificate2 = createObject('waeup.Certificate')
+        self.certificate2.code = 'CERT2'
+        self.certificate2.application_category = 'pg_ft'
+        self.certificate2.start_level = 100
+        self.certificate2.end_level = 500
+        self.certificate.study_mode = u'pg_ft'
+        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
+            self.certificate2)
+
+        # Add (customized) applicants
+        pgapplicant = createObject(u'waeup.Applicant')
+        pgapplicant.firstname = u'Anna'
+        pgapplicant.lastname = u'Post'
+        self.app['applicants'][pgft_container_name].addApplicant(pgapplicant)
+        self.pgapplication_number = pgapplicant.application_number
+        self.pgapplicant = self.app['applicants'][pgft_container_name][
+            self.pgapplication_number]
+
+        ugapplicant = createObject(u'waeup.Applicant')
+        ugapplicant.firstname = u'Klaus'
+        ugapplicant.lastname = u'Under'
+        self.app['applicants'][app_container_name].addApplicant(ugapplicant)
+        self.ugapplication_number = ugapplicant.application_number
+        self.ugapplicant = self.app['applicants'][app_container_name][
+            self.ugapplication_number]
+
+        cbtapplicant = createObject(u'waeup.Applicant')
+        cbtapplicant.firstname = u'Anna'
+        cbtapplicant.lastname = u'Cbt'
+        self.app['applicants'][cbt_container_name].addApplicant(cbtapplicant)
+        self.cbtapplication_number = cbtapplicant.application_number
+        self.cbtapplicant = self.app['applicants'][cbt_container_name][
+            self.cbtapplication_number]
+        IUserAccount(self.cbtapplicant).setPassword('apwd')
+
+        self.login_path = 'http://localhost/app/login'
+        self.logout_path = 'http://localhost/app/logout'
+        self.pgapplicant_path = ('http://localhost/app/applicants/%s/%s'
+            % (pgft_container_name, self.pgapplication_number))
+        self.ugapplicant_path = ('http://localhost/app/applicants/%s/%s'
+            % (app_container_name, self.ugapplication_number))
+        self.cbtapplicant_path = ('http://localhost/app/applicants/%s/%s'
+            % (cbt_container_name, self.cbtapplication_number))
+
+        self.browser = Browser()
+        self.browser.handleErrors = False
+
+        configuration = SessionConfiguration()
+        configuration.academic_session = session_1
+        configuration.application_fee = 200.0
+        self.app['configuration'].addSessionConfiguration(configuration)
+
+    def tearDown(self):
+        super(ApplicantUITest, self).tearDown()
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def fill_correct_values(self):
+        self.browser.getControl(name="form.reg_number").value = '1234'
+        self.browser.getControl(name="form.firstname").value = 'John'
+        self.browser.getControl(name="form.lastname").value = 'Tester'
+        self.browser.getControl(name="form.date_of_birth").value = '09/09/1988'
+        self.browser.getControl(name="form.lga").value = ['foreigner']
+        self.browser.getControl(name="form.nationality").value = ['NG']
+        self.browser.getControl(name="form.sex").value = ['m']
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+
+    def test_manage_and_view_applicant(self):
+        # Managers can manage pg applicants.
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        # The IPGApplicant interface is really used in all pages.
+        self.browser.open(self.pgapplicant_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertTrue('Employer' in self.browser.contents)
+        self.browser.open(self.pgapplicant_path + '/manage')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertTrue('Employer' in self.browser.contents)
+        self.browser.open(self.pgapplicant_path + '/edit')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertTrue('Employer' in self.browser.contents)
+        self.browser.open(self.pgapplicant_path + '/application_slip.pdf')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        # If we view the applicant in the ug container,
+        # the employer field doesn't appear.
+        self.browser.open(self.ugapplicant_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertFalse('Employer' in self.browser.contents)
+        self.browser.open(self.ugapplicant_path + '/manage')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        # We can save the applicant.
+        self.fill_correct_values()
+        self.browser.getControl(name="form.course1").value = ['CERT1']
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Form has been saved...', self.browser.contents)
+        self.assertFalse('Employer' in self.browser.contents)
+        self.browser.open(self.ugapplicant_path + '/edit')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertFalse('Employer' in self.browser.contents)
+        self.browser.open(self.ugapplicant_path + '/application_slip.pdf')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        return
+
+    def test_set_wrong_course1(self):
+        self.ugapplicant.course1 = self.certificate
+        self.assertRaises(
+            ConstraintNotSatisfied,
+            setattr, self.ugapplicant, 'course1', self.certificate2)
+        self.pgapplicant.course1 = self.certificate2
+        self.assertRaises(
+            ConstraintNotSatisfied,
+            setattr, self.pgapplicant, 'course1', self.certificate)
+        return
+
+    def test_application_payment(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+
+        # UG (UTME) applicant
+        self.browser.open(self.ugapplicant_path)
+        self.browser.open(self.ugapplicant_path + '/manage')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.fill_correct_values()
+        self.browser.getControl(name="form.course1").value = ['CERT1']
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Form has been saved...', self.browser.contents)
+        self.browser.getControl("Add online payment ticket").click()
+        self.assertMatches('...Payment ticket created...',
+                           self.browser.contents)
+        self.assertMatches('...Amount Authorized...',
+                           self.browser.contents)
+        payment_id = self.ugapplicant.keys()[0]
+        payment = self.ugapplicant[payment_id]
+        self.assertEqual(payment.p_item,'This is the %s container' % app_container_name)
+        self.assertEqual(payment.p_session,session_1)
+        self.assertEqual(payment.p_category,'application')
+        self.assertEqual(payment.amount_auth, 200.0)
+
+        # PG applicants pay more
+        self.browser.open(self.pgapplicant_path)
+        self.browser.open(self.pgapplicant_path + '/manage')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.fill_correct_values()
+        self.browser.getControl(name="form.course1").value = ['CERT2']
+        self.browser.getControl(name="form.reg_number").value = '2345'
+        self.browser.getControl(name="form.firstname").value = 'Anna'
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Form has been saved...', self.browser.contents)
+        self.browser.getControl("Add online payment ticket").click()
+        self.assertMatches('...Payment ticket created...',
+                           self.browser.contents)
+        self.assertMatches('...Amount Authorized...',
+                           self.browser.contents)
+        payment_id = self.pgapplicant.keys()[0]
+        payment = self.pgapplicant[payment_id]
+        self.assertEqual(payment.p_item,'This is the %s container' % pgft_container_name)
+        self.assertEqual(payment.p_session,session_1)
+        self.assertEqual(payment.p_category,'application')
+        self.assertEqual(payment.amount_auth, 300.0)
+        return
+
+    def test_hide_screening_data(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.ugapplicant_path + '/manage')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.fill_correct_values()
+        self.browser.getControl(name="form.screening_venue").value = 'Mensa'
+        self.browser.getControl(name="form.screening_date").value = 'Easter'
+        self.browser.getControl(name="form.course1").value = ['CERT1']
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Form has been saved...', self.browser.contents)
+        self.browser.getControl("Save").click()
+        # Venue and date are not shown
+        self.browser.open(self.ugapplicant_path)
+        self.assertFalse('Screening' in self.browser.contents)
+        IWorkflowState(self.ugapplicant).setState('paid')
+        # Venue and date are shown if applicant has paid
+        self.browser.open(self.ugapplicant_path)
+        self.assertTrue('Screening' in self.browser.contents)
+        return
+
+    def test_register_applicant_update(self):
+        # An applicant can register himself.
+        self.ugapplicant.reg_number = u'1234'
+        notify(grok.ObjectModifiedEvent(self.ugapplicant))
+        self.browser.open('http://localhost/app/applicants/%s/' % app_container_name)
+        self.browser.getLink("Register for application").click()
+        # Fill the edit form with suitable values
+        self.browser.getControl(name="form.lastname").value = 'Under'
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+        self.browser.getControl(name="form.reg_number").value = '1234'
+        self.browser.getControl("Send login credentials").click()
+        self.assertMatches('...Your registration was successful...',
+            self.browser.contents)
+        self.assertFalse('...<td>Password:</td>...' in self.browser.contents)
+        # The new applicant can be found in the catalog via the email address
+        cat = getUtility(ICatalog, name='applicants_catalog')
+        results = list(
+            cat.searchResults(email=('xx@yy.zz', 'xx@yy.zz')))
+        applicant = results[0]
+        self.assertEqual(applicant.lastname,'Under')
+        # The applicant can be found in the catalog via the reg_number
+        results = list(
+            cat.searchResults(
+            reg_number=(applicant.reg_number, applicant.reg_number)))
+        self.assertEqual(applicant,results[0])
+        return
+
+    def test_create_ugstudent(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        manage_path = 'http://localhost/app/applicants/%s/%s/%s' % (
+            app_container_name, self.ugapplicant.application_number, 'manage')
+        self.browser.open(manage_path)
+        self.fill_correct_values()
+        self.browser.getControl("Save").click()
+        IWorkflowState(self.ugapplicant).setState('admitted')
+        self.browser.getControl(name="form.course1").value = ['CERT1']
+        self.browser.getControl(name="form.course_admitted").value = ['CERT1']
+        self.browser.getControl("Save").click()
+        self.browser.getLink("Create student").click()
+        student_id =  self.app['students'].keys()[0]
+        self.assertTrue(('Student %s created' % student_id)
+            in self.browser.contents)
+        student = self.app['students'][student_id]
+        self.assertEqual(student.email, 'xx@yy.zz')
+        self.assertEqual(student.firstname, 'John')
+        self.assertEqual(student.lastname, 'Tester')
+        # Also additional attributes have been copied.
+        self.assertEqual(student.lga, 'foreigner')
+        self.assertEqual(student.nationality, 'NG')
+        return
+
+    def test_applicant_access(self):
+        # Applicants can edit their record
+        self.browser.open(self.login_path)
+        self.browser.getControl(
+            name="form.login").value = self.cbtapplicant.applicant_id
+        self.browser.getControl(name="form.password").value = 'apwd'
+        self.browser.getControl("Login").click()
+        self.assertTrue(
+            'You logged in.' in self.browser.contents)
+        self.browser.getLink("Edit application record").click()
+        self.browser.getControl(name="form.date_of_birth").value = '09/09/1988'
+        self.browser.getControl(name="form.lga").value = ['foreigner']
+        self.browser.getControl(name="form.nationality").value = ['NG']
+        self.browser.getControl(name="form.sex").value = ['m']
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+        self.browser.getControl(name="form.course1").value = ['CERT1']
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Form has been saved...', self.browser.contents)
+
+class ApplicantExporterTest(ApplicantImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantExporterTest, self).setUp()
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        self.cat = getUtility(ICatalog, name='applicants_catalog')
+        self.intids = getUtility(IIntIds)
+        return
+
+    def setup_applicant(self, applicant):
+        # set predictable values for `applicant`
+        applicant.reg_number = u'123456'
+        applicant.applicant_id = u'dp2011_654321'
+        applicant.firstname = u'Anna'
+        applicant.lastname = u'Tester'
+        applicant.middlename = u'M.'
+        applicant.nationality = u'NG'
+        applicant.date_of_birth = datetime.date(1981, 2, 4)
+        applicant.sex = 'f'
+        applicant.email = 'anna@sample.com'
+        applicant.phone = u'+234-123-12345'
+        applicant.course1 = self.certificate
+        applicant.course2 = self.certificate
+        applicant.course_admitted = self.certificate
+        applicant.notice = u'Some notice\nin lines.'
+        applicant.jamb_subjects = u'Line 1\nLine 2'
+        applicant.jamb_subjects_list = ['english_language', 'fine_art']
+        applicant.screening_score = 98
+        applicant.screening_venue = u'Exam Room'
+        applicant.screening_date = u'Saturday, 16th June 2012 2:00:00 PM'
+        applicant.password = 'any password'
+        return applicant
+
+    def test_export_reimport_all(self):
+        # we can export all applicants in a portal
+        # set values we can expect in export file
+        self.applicant = self.setup_applicant(self.applicant)
+        exporter = NigeriaApplicantExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(result,
+            'aggregate,applicant_id,bank_account_name,bank_account_number,'
+            'bank_name,course1,course2,course_admitted,date_of_birth,'
+            'disabilities,email,'
+            'emp2_end,emp2_position,emp2_reason,emp2_start,emp_end,'
+            'emp_position,emp_reason,emp_start,employer,employer2,'
+            'firstname,fst_sit_date,fst_sit_fname,fst_sit_no,'
+            'fst_sit_results,fst_sit_type,hq_degree,hq_disc,'
+            'hq_fname,hq_matric_no,hq_school,hq_session,hq_type,'
+            'jamb_reg_number,jamb_score,jamb_subjects,jamb_subjects_list,'
+            'lastname,lga,locked,middlename,nationality,notice,nysc_lga,'
+            'nysc_year,phone,presently_inst,programme_type,reg_number,'
+            'result_uploaded,scd_sit_date,scd_sit_fname,scd_sit_no,'
+            'scd_sit_results,scd_sit_type,screening_date,screening_score,'
+            'screening_venue,sex,special_application,student_id,'
+            'suspended,password,state,history,container_code,'
+            'application_number,display_fullname,application_date\r\n'
+
+            ',dp2011_654321,,,,CERT1,CERT1,CERT1,1981-02-04#,,'
+            'anna@sample.com,,,,,,,,,,,Anna,,,,,,,,,,,,,,,Line 1++Line 2,'
+            '"[\'english_language\', \'fine_art\']",'
+            'Tester,,0,M.,NG,"Some notice\nin lines.",,,+234-123-12345#,,,'
+            '123456,,,,,,,"Saturday, 16th June 2012 2:00:00 PM",98,'
+            'Exam Room,f,,,0,any password,initialized,'
+            '[u\'2016-08-19 07:30:05 WAT - Application initialized by system\'],'
+            'dp2011,654321,Anna M. Tester,\r\n')
+        # We can import the same file if we ignore some columns.
+        # Since the applicants_catalog hasn't been notified, the same
+        # record with same reg_number can be imported twice.
+        processor = NigeriaApplicantProcessor()
+        result = processor.doImport(
+            self.outfile,
+            [
+            'aggregate','ignore_applicant_id','bank_account_name','bank_account_number',
+            'bank_name','course1','course2','course_admitted','date_of_birth',
+            'disabilities','email',
+            'emp2_end','emp2_position','emp2_reason','emp2_start','emp_end',
+            'emp_position','emp_reason','emp_start','employer','employer2',
+            'firstname','fst_sit_date','fst_sit_fname','fst_sit_no',
+            'fst_sit_results','fst_sit_type','hq_degree','hq_disc',
+            'hq_fname','hq_matric_no','hq_school','hq_session','hq_type',
+            'jamb_reg_number','jamb_score','jamb_subjects','jamb_subjects_list',
+            'lastname','lga','locked','middlename','nationality','notice','nysc_lga',
+            'nysc_year','phone','presently_inst','programme_type','reg_number',
+            'result_uploaded','scd_sit_date','scd_sit_fname','scd_sit_no',
+            'scd_sit_results','scd_sit_type','screening_date','screening_score',
+            'screening_venue','sex','special_application','student_id',
+            'suspended','password','state','history','container_code',
+            'application_number','display_fullname','application_date'
+            ],
+            mode='create')
+        num_succ, num_fail, finished_path, failed_path = result
+        #content = open(failed_path).read()
+        self.assertEqual(num_succ,1)
+        self.assertEqual(num_fail,0)
+        # Now we ignore also the container_code and import the same file 
+        # in update mode which means that INigeriaApplicantUpdateByRegNo
+        # is used for field conversion. applicant_id must be ignored
+        # too since the previous import has notified the applicants_catalog
+        # so that the portal 'knows' that reg_number is in use.
+        processor = NigeriaApplicantProcessor()
+        result = processor.doImport(
+            self.outfile,
+            [
+            'aggregate','ignore_applicant_id','bank_account_name','bank_account_number',
+            'bank_name','course1','course2','course_admitted','date_of_birth',
+            'disabilities', 'email',
+            'emp2_end','emp2_position','emp2_reason','emp2_start','emp_end',
+            'emp_position','emp_reason','emp_start','employer','employer2',
+            'firstname','fst_sit_date','fst_sit_fname','fst_sit_no',
+            'fst_sit_results','fst_sit_type','hq_degree','hq_disc',
+            'hq_fname','hq_matric_no','hq_school','hq_session','hq_type',
+            'jamb_reg_number','jamb_score','jamb_subjects','jamb_subjects_list',
+            'lastname','lga','locked','middlename','nationality','notice','nysc_lga',
+            'nysc_year','phone','presently_inst','programme_type','reg_number',
+            'result_uploaded','scd_sit_date','scd_sit_fname','scd_sit_no',
+            'scd_sit_results','scd_sit_type','screening_date','screening_score',
+            'screening_venue','sex','special_application','student_id',
+            'suspended','password','state','ignore_history','ignore_container_code',
+            'ignore_application_number','display_fullname','application_date'
+            ],
+            mode='update')
+        num_succ, num_fail, finished_path, failed_path = result
+        self.assertEqual(num_succ,1)
+        self.assertEqual(num_fail,0)
+        return
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/utils.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/utils.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/applicants/utils.py	(revision 17238)
@@ -0,0 +1,66 @@
+## $Id$
+##
+## Copyright (C) 2011 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
+##
+"""General helper functions and utilities for the application section.
+"""
+
+import grok
+from waeup.kofa.applicants.utils import ApplicantsUtils
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+
+class NigeriaApplicantsUtils(ApplicantsUtils):
+    """A collection of parameters and methods subject to customization.
+    """
+    #: A tuple of tuple of file names to be uploaded by applicants and copied
+    #: over to the students section.
+    ADDITIONAL_FILES = ()
+
+    APP_TYPES_DICT = {
+        'pude': ['Post-UDE Screening', 'PUDE'],
+        'prence': ['Pre-NCE Programme', 'PRE'],
+        'sandwich': ['Part-Time Degree in Education', 'SAND'],
+        'pt': ['Part-Time Degree Programmes', 'PTP'],
+        'putme': ['Post-UTME Screening Exercise', 'PUTME'],
+        'app': ['General Studies', 'APP'],
+        'cest': ['Common Entry Screening Test', 'CEST'],
+        'ct': ['Certificate Programmes', 'CTP'],
+        'dp': ['Diploma Programmes', 'DPP'],
+        'pce': ['PCE Screening', 'PCE'],
+        'pgft': ['Postgraduate Full-Time Programmes', 'PG'],
+        'pgpt': ['Postgraduate Part-Time Programmes', 'PG'],
+        'pre': ['Pre-Degree Studies', 'PRE'],
+        'cbt': ['CBT Practice Test', 'CBT'],
+        'ase': ['Admission Screening Exercise', 'ASE'], # successor of putme
+        }
+
+    SEPARATORS_DICT = {
+        'form.applicant_id': _(u'Base Data'),
+        'form.course1': _(u'Programmes/Courses Desired'),
+        'form.hq_type': _(u'Higher Education Record'),
+        'form.presently': _(u'Course or Programme Presently Attending'),
+        'form.nysc_year': _(u'NYSC Information'),
+        'form.employer': _(u'Employment History'),
+        'form.jamb_subjects': _(u'JAMB Data'),
+        'form.jamb_subjects_list': _(u'JAMB Data'),
+        'form.notice': _(u'Application Process Information'),
+        'form.pp_school': _(u'Post Primary School Qualification'),
+        'form.presently_inst': _(u'Presently attending a course or programme'),
+        'form.fst_sit_fname': _(u'First Sitting Record'),
+        'form.scd_sit_fname': _(u'Second Sitting Record'),
+        'form.referees': _(u'Referees'),
+        }
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/browser/__init__.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/browser/__init__.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/browser/__init__.py	(revision 17238)
@@ -0,0 +1,15 @@
+import os
+
+#from waeup.kofa.browser.layout import (
+#    KofaPage, KofaForm, KofaLayout, KofaDisplayFormPage, KofaEditFormPage,
+#    KofaAddFormPage, NullValidator)
+#from waeup.kofa.browser.pages import ContactAdminForm
+
+IMAGE_PATH = os.path.join(
+    os.path.dirname(__file__),
+    'static'
+    )
+
+#: Filesystem paths to default images
+DEFAULT_IMAGE_PATH = os.path.join(IMAGE_PATH, 'placeholder.jpg')
+DEFAULT_PASSPORT_IMAGE_PATH = os.path.join(IMAGE_PATH, 'placeholder.jpg')
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/browser/pages.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/browser/pages.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/browser/pages.py	(revision 17238)
@@ -0,0 +1,90 @@
+## $Id$
+##
+## Copyright (C) 2015 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
+##
+""" Viewing components for Kofa objects.
+"""
+
+import grok
+from zope.component import getUtility
+from waeup.kofa.interfaces import IKofaUtils
+from waeup.kofa.browser.pages import (
+    SessionConfigurationAddFormPage,
+    SessionConfigurationManageFormPage,
+    ConfigurationContainerManageFormPage,
+    SourcesOverview)
+from waeup.kofa.browser.layout import KofaPage
+from kofacustom.nigeria.interfaces import (
+    high_qual, high_grade, exam_types, LGASource,
+    ICustomSessionConfiguration,
+    ICustomSessionConfigurationAdd)
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+class NigeriaSourcesOverview(SourcesOverview):
+
+    def _set_high_qual(self):
+        vocab_terms = high_qual.by_value.values()
+        self.high_qual = sorted([(x.token, x.title,) for x in vocab_terms])
+        return
+
+    def _set_high_grade(self):
+        vocab_terms = high_grade.by_value.values()
+        self.high_grade = sorted([(x.token, x.title,) for x in vocab_terms])
+        return
+
+    def _set_exam_types(self):
+        vocab_terms = exam_types.by_value.values()
+        self.exam_types = sorted([(x.token, x.title,) for x in vocab_terms])
+        return
+
+    def _set_lgas(self):
+        lgasource = LGASource().factory
+        self.lgas = []
+        for key in lgasource.getValues():
+            title = lgasource.getTitle(key)
+            self.lgas.append((key, title))
+        return
+
+    def _set_grading_sys(self):
+        grading_sys = getUtility(IKofaUtils).GRADING_SYSTEM_DICT
+        self.grading_sys = sorted(grading_sys.items())
+        return
+
+    def update(self):
+        super(NigeriaSourcesOverview, self).update()
+        self._set_high_qual()
+        self._set_high_grade()
+        self._set_exam_types()
+        self._set_lgas()
+        self._set_grading_sys()
+        return
+
+class NigeriaSourcesOverviewPage(KofaPage, NigeriaSourcesOverview):
+    grok.name('sources')
+    grok.require('waeup.Public')
+    label = _(u'Sources & Vocabularies')
+    pnav = 0
+
+class CustomSessionConfigurationAddFormPage(SessionConfigurationAddFormPage):
+    """Add a session configuration object to configuration container.
+    """
+    form_fields = grok.AutoFields(ICustomSessionConfigurationAdd)
+
+class CustomSessionConfigurationManageFormPage(
+    SessionConfigurationManageFormPage):
+    """Manage session configuration object.
+    """
+    form_fields = grok.AutoFields(ICustomSessionConfiguration)
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/browser/tests.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/browser/tests.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/browser/tests.py	(revision 17238)
@@ -0,0 +1,30 @@
+## $Id$
+##
+## Copyright (C) 2011 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
+##
+
+from waeup.kofa.browser.tests.test_browser import UniversitySetup
+from kofacustom.nigeria.testing import FunctionalLayer
+
+class SupplementaryBrowserTests(UniversitySetup):
+    # These are additional tests to browser.txt
+
+    layer = FunctionalLayer
+
+    def test_sources_overview(self):
+        self.browser.open('http://localhost/app/sources')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertTrue('id="headingLGAS"' in self.browser.contents)
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/browser/viewlets.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/browser/viewlets.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/browser/viewlets.py	(revision 17238)
@@ -0,0 +1,71 @@
+## $Id$
+##
+## Copyright (C) 2015 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 waeup.kofa.interfaces import IKofaUtils, IKofaObject
+from waeup.kofa.browser.viewlets import (
+    InstTypesSource, SourcesLeft, SourcesRight)
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+grok.context(IKofaObject) # Make IKofaObject the default context
+
+class LGASSource(InstTypesSource):
+    """Available lgas for the sources overview page.
+    """
+    grok.order(7)
+    name = 'LGAS'
+    source_name = 'lgas'
+    title = _('LGAs')
+
+class HigherQualSource(InstTypesSource):
+    """Available higher qualifications for the sources overview page.
+    """
+    grok.order(7)
+    grok.viewletmanager(SourcesRight)
+    column = 'right'
+    name = 'HighQual'
+    source_name = 'high_qual'
+    title = _('Higher Qualification')
+
+class HigherGradeSource(InstTypesSource):
+    """Available higher qualification grades for the sources overview page.
+    """
+    grok.order(8)
+    grok.viewletmanager(SourcesRight)
+    column = 'right'
+    name = 'HighGrade'
+    source_name = 'high_grade'
+    title = _('Higher Qualification Grades')
+
+class ExamTypesSource(InstTypesSource):
+    """Available  higher qualification exam types for the sources overview page.
+    """
+    grok.order(9)
+    grok.viewletmanager(SourcesRight)
+    column = 'right'
+    name = 'ExamTypes'
+    source_name = 'exam_types'
+    title = _('Higher Qualification Examination Types')
+
+class GradingSysSource(InstTypesSource):
+    """Available grading systems for the sources overview page.
+    """
+    grok.order(10)
+    name = 'GradingSystems'
+    source_name = 'grading_sys'
+    title = _('Nigerian Course Grading Systems')
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/configuration.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/configuration.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/configuration.py	(revision 17238)
@@ -0,0 +1,46 @@
+## $Id$
+##
+## Copyright (C) 2011 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
+##
+"""
+Containers for session configuration objects.
+"""
+import grok
+from zope.interface import implementedBy
+from waeup.kofa.utils.helpers import attrs_to_fields
+from waeup.kofa.configuration import (
+    SessionConfigurationFactory, SessionConfiguration)
+from kofacustom.nigeria.interfaces import (
+    ICustomSessionConfiguration, ICustomSessionConfigurationAdd)
+
+class CustomSessionConfiguration(SessionConfiguration):
+    """
+    Customized session configuration model
+    """
+
+    grok.implements(ICustomSessionConfiguration, ICustomSessionConfigurationAdd)
+
+CustomSessionConfiguration = attrs_to_fields(CustomSessionConfiguration)
+
+class CustomSessionConfigurationFactory(SessionConfigurationFactory):
+    """A customized factory for session configuration objects.
+    """
+
+    def __call__(self, *args, **kw):
+        return CustomSessionConfiguration(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(CustomSessionConfiguration)
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/configure.zcml
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/configure.zcml	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/configure.zcml	(revision 17238)
@@ -0,0 +1,20 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           xmlns:grok="http://namespaces.zope.org/grok">
+
+  <!-- Loading waeup.kofa also configures a lot of other needed
+       packages correctly.
+  -->
+  <include package="waeup.kofa" />
+  <include package="kofacustom.nigeria" file="locales.zcml" />
+  <grok:grok package="." />
+
+  <!-- Register edit widget for PhoneNumbers -->
+  <adapter
+      for="waeup.kofa.schema.interfaces.IPhoneNumber
+           zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.formlib.interfaces.ISimpleInputWidget"
+      factory="kofacustom.nigeria.widgets.phonewidget.NigeriaPhoneWidget"
+      permission="zope.Public"
+      />
+
+</configure>
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/__init__.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/__init__.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/__init__.py	(revision 17238)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/applicantsbrowser.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/applicantsbrowser.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/applicantsbrowser.py	(revision 17238)
@@ -0,0 +1,216 @@
+## $Id$
+##
+## Copyright (C) 2017 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import grok
+import hashlib
+from datetime import datetime, timedelta
+from zope.component import getUtility
+from zope.security import checkPermission
+from waeup.kofa.interfaces import IKofaUtils
+from waeup.kofa.utils.helpers import to_timezone
+from waeup.kofa.browser.layout import UtilityView, KofaPage
+from waeup.kofa.browser.viewlets import ManageActionButton
+from kofacustom.nigeria.etranzact.helpers import (
+    write_payments_log, process_response, query_history)
+from kofacustom.nigeria.applicants.browser import NigeriaOnlinePaymentDisplayFormPage as NOPDPApplicant
+from kofacustom.nigeria.payments.interfaces import INigeriaOnlinePayment
+from kofacustom.nigeria.applicants.interfaces import INigeriaApplicantOnlinePayment
+from kofacustom.nigeria.interfaces import MessageFactory as _
+from kofacustom.nigeria.etranzact.studentsbrowser import webconnect_module_activated
+from kofacustom.nigeria.etranzact.tests import (
+    TERMINAL_ID, HOST, HTTPS, SECRET_KEY, LOGO_URL, GATEWAY_AMT)
+
+grok.templatedir('browser_templates')
+
+class EtranzactActionButtonApplicant(ManageActionButton):
+    grok.order(1)
+    grok.context(INigeriaOnlinePayment)
+    grok.view(NOPDPApplicant)
+    grok.require('waeup.payApplicant')
+    icon = 'actionicon_pay.png'
+    text = _('Pay via Etranzact WebConnect')
+    target = 'goto_etranzact'
+
+    @property
+    def target_url(self):
+        if not webconnect_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 EtranzactRequeryActionButtonApplicant(ManageActionButton):
+    grok.order(2)
+    grok.context(INigeriaOnlinePayment)
+    grok.view(NOPDPApplicant)
+    grok.require('waeup.payApplicant')
+    icon = 'actionicon_call.png'
+    text = _('Requery Etranzact WebConnect History')
+    target = 'requery_history'
+
+    @property
+    def target_url(self):
+        if not webconnect_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 EtranzactPageApplicant(KofaPage):
+    """ View which sends a POST request to the Etranzact payment gateway.
+    """
+    grok.context(INigeriaApplicantOnlinePayment)
+    grok.name('goto_etranzact')
+    grok.template('goto_etranzact')
+    grok.require('waeup.payApplicant')
+    label = _('Pay via Etranzact')
+    submit_button = _('Pay now')
+
+    host = HOST
+    https = HTTPS
+    secret_key = SECRET_KEY
+    terminal_id = TERMINAL_ID
+    logo_url = LOGO_URL
+    gateway_amt = GATEWAY_AMT
+
+    @property
+    def action(self):
+        if self.https:
+            return 'https://' + self.host + '/webconnect/v3/caller.jsp'
+        return 'http://' + self.host + '/webconnect/v3/caller.jsp'
+
+    def init_update(self):
+        if self.context.p_state == 'paid':
+            return _("Payment ticket can't be re-sent to Etranzact.")
+        now = datetime.utcnow()
+        if self.context.creation_date.tzinfo is not None:
+            # That's bad. Please store timezone-naive datetimes only!
+            now = self.context.creation_date.tzinfo.localize(now)
+        time_delta = now - self.context.creation_date
+        if time_delta.days > 7:
+            return _("This payment ticket is too old. Please create a new ticket.")
+        # In contrast to the procedure in the Remita and Interswitch modules,
+        # we do not call requery_history but receive and evaluate
+        # the response form from Etranzact directly. This is possible
+        # because Etranzact provides the FINAL_CHECKSUM hash value
+        # which authenticates the response.
+        self.responseurl = self.url(self.context, 'receive_etranzact')
+        self.transaction_id = self.context.p_id
+        self.description = "%s - (%s)" % (
+            self.context.p_category, self.context.p_item)
+        hashargs = 	self.amount + self.terminal_id + self.transaction_id \
+            + self.responseurl + self.secret_key
+        self.hashvalue = hashlib.md5(hashargs).hexdigest()
+        self.customer = self.context.__parent__
+        return
+
+    def update(self):
+        if not webconnect_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" % self.context.amount_auth
+        error = self.init_update()
+        if error:
+            self.flash(error, type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        return
+
+class EtranzactReceiveResponseApplicant(NOPDPApplicant):
+    """ View that receives the response from eTrantact payment gateway.
+    """
+    grok.name('receive_etranzact')
+
+    secret_key = SECRET_KEY
+    terminal_id = TERMINAL_ID
+
+    def update(self):
+        super(EtranzactReceiveResponseApplicant, self).update()
+        if not webconnect_module_activated(
+            self.context.__parent__.__parent__.year, self.context):
+            self.flash(_('Forbidden'), type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+            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 EtranzactRequestPaymentStatusPageApplicant(UtilityView, grok.View):
+    """ Request webservice view for the Etranzact gateway.
+    """
+    grok.context(INigeriaApplicantOnlinePayment)
+    grok.name('requery_history')
+    grok.require('waeup.payApplicant')
+
+    host = HOST
+    https = HTTPS
+    secret_key = SECRET_KEY
+    terminal_id = TERMINAL_ID
+    logo_url = LOGO_URL
+
+    def update(self):
+        if not webconnect_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
+        raw, form = query_history(self.host, self.terminal_id,
+                                  self.context.p_id, self.https)
+        success, msg, log = process_response(self.context, form, self, verify)
+        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))
+        return
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/browser_templates/enterpin.pt
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/browser_templates/enterpin.pt	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/browser_templates/enterpin.pt	(revision 17238)
@@ -0,0 +1,20 @@
+<br />
+
+<p i18n:domain="kofacustom.nigeria" i18n:translate="">
+    Go to the bank, show your payment slip and make your payment.
+    You'll receive a confirmation mumber.
+    Enter the confirmation number here.
+    Take note of the authorized payment amount which is
+    <span tal:replace="context/amount_auth">1000.0</span> Naira.
+</p>
+<br />
+<form method="POST" class="form-inline" tal:attributes="action view/action">
+  <div class="form-group half">
+    <input name="confirmation_number" type="text" class="form-control" maxlength=""
+           value="" tal:attributes="placeholder view/placeholder"/>
+  </div>
+  <div class="form-group">
+    <input type="submit" name="SUBMIT" class="btn btn-primary"
+           tal:attributes="value view/buttonname" />
+  </div>
+</form>
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/browser_templates/goto_etranzact.pt
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/browser_templates/goto_etranzact.pt	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/browser_templates/goto_etranzact.pt	(revision 17238)
@@ -0,0 +1,59 @@
+<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">
+  <input type=hidden name="AMOUNT" tal:attributes="value view/context/amount_auth">
+  <input type=hidden name="DESCRIPTION" tal:attributes="value view/description">
+  <input type=hidden name="EMAIL" tal:attributes="value view/customer/email">
+  <input type=hidden name="PHONENO" tal:attributes="value view/customer/phone">
+  <input type=hidden name="FULL_NAME" tal:attributes="value view/customer/display_fullname">
+  <input type=hidden name="CURRENCY_CODE" value="NGN">
+  <input type=hidden name="CHECKSUM" tal:attributes="value view/hashvalue">
+  <input type=hidden name="LOGO_URL" tal:attributes="value view/logo_url" height="60" width="60">
+
+  <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 Id:</td>
+        <td>
+          <span tal:replace="view/context/p_id">TRANSID</span>
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          Customer Name:</td>
+        <td>
+          <span tal:replace="view/customer/display_fullname">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 />
+  <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/branches/uli-paypal/src/kofacustom/nigeria/etranzact/helpers.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/helpers.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/helpers.py	(revision 17238)
@@ -0,0 +1,283 @@
+## $Id$
+##
+## 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
+##
+"""General helper functions for the etranzact module in custom packages.
+"""
+import grok
+import re
+from datetime import datetime
+from urllib import urlencode
+from urllib2 import urlopen
+from urlparse import parse_qs
+import httplib
+import hashlib
+import json
+from zope.event import notify
+from waeup.kofa.utils.helpers import extract_formvars
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+ERROR_PART1 = (
+        'PayeeName=N/A~'
+        + 'Faculty=N/A~'
+        + 'Department=N/A~'
+        + 'Level=N/A~'
+        + 'ProgrammeType=N/A~'
+        + 'StudyType=N/A~'
+        + 'Session=N/A~'
+        + 'PayeeID=N/A~'
+        + 'Amount=N/A~'
+        + 'FeeStatus=')
+ERROR_PART2 = (
+        '~Semester=N/A~'
+        + 'PaymentType=N/A~'
+        + 'MatricNumber=N/A~'
+        + 'Email=N/A~'
+        + 'PhoneNumber=N/A')
+
+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))
+
+def query_history(host, terminal_id, transaction_id, https):
+    headers={"Content-type": "application/x-www-form-urlencoded",
+             "Accept": "text/plain"}
+    url = "/webconnect/v3/query.jsp"
+    if https:
+        h = httplib.HTTPSConnection(host)
+    else:
+        h = httplib.HTTPConnection(host)
+    args = {'TERMINAL_ID': terminal_id,
+            'TRANSACTION_ID': transaction_id,
+            }
+    #args['RESPONSE_URL'] = responseurl
+    h.request('POST', url, urlencode(args), headers)
+    response = h.getresponse()
+    if response.status!=200:
+        return 'Connection error (%s, %s)' % (response.status, response.reason), None
+    raw = response.read()
+    return raw, extract_formvars(raw)
+
+ # 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
+
+ # <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ #     "http://www.w3.org/TR/html4/loose.dtd">
+ # <script language='javascript'>
+ # var spanId = document.getElementById("message_container");
+ # spanId.textContent = 'Redirecting...';</script>
+ # <form method="GET" id="redirect_form" name="redirect_form"
+ # action="http://localhost:81/projects/Webconnect/response.php" target="_top">
+ # <input type="hidden" name = "LOGO_URL"
+ # value="http://localhost:81/projects/Webconnect/images/elogo.fw.png">
+ # <input type="hidden" name = "RESPONSE_URL"
+ # value="http://localhost:81/projects/Webconnect/response.php">
+ # <input type="hidden" name = "CURRENCY_CODE" value="NGN">
+ # <input type="hidden" name = "TERMINAL_ID" value="0000000001">
+ # <input type="hidden" name = "TRANSACTION_ID" value="etz1568638104web">
+ # <input type="hidden" name = "AMOUNT" value="100">
+ # <input type="hidden" name = "DESCRIPTION" value="Payment Description">
+ # <input type="hidden" name = "CHECKSUM"
+ # value="5be04064f4bb250f73650059a8e921cc">
+ # <input type="hidden" name = "MERCHANT_CODE" value="0339990001">
+ # <input type="hidden" name = "EMAIL" value="xyz@yahoo.com">
+ # <input type="hidden" name = "SUCCESS" value="0">
+ # <input type="hidden" name = "FINAL_CHECKSUM"
+ # value="FD67A4CCC39E2C8DEEEC78D6C64A61FE">
+ # <input type="hidden" name = "STATUS_REASON" value="Approved">
+ # <input type="hidden" name = "TRANS_NUM" value="01ESA20190916134824YA3YJ8">
+ # <input type="hidden" name = "CARD_NO" value="506066XXXXXXXXX6666">
+ # <input type="hidden" name = "CARD_TYPE" value="Verve">
+ # </form>
+ # <script language='javascript'>
+ # var fom = document.forms["redirect_form"];
+ # fom.submit();</script>
+
+ # A sample query response sent to the RESPONSE_URL after the browser has
+ # automatically executed the Javascript above (we don't use this response)
+
+ # 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):
+    if not form or not form.get('SUCCESS', None):
+        msg = _('No (valid) response from Etranzact.')
+        log = 'invalid response for payment %s' % payment.p_id
+        payment.p_state = 'failed'
+        notify(grok.ObjectModifiedEvent(payment))
+        return False, msg, log
+    success = form.get('SUCCESS', None)
+    # 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))
+        payment.p_state = 'failed'
+        notify(grok.ObjectModifiedEvent(payment))
+        return False, msg, log
+    payment.r_code = form.get('SUCCESS', 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))
+    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
+
+# Requerying Etranzact Payoutlet payments
+
+# Expected response:
+# RECEIPT_NO=500191030486&PAYMENT_CODE=500854291572447457669&MERCHANT_CODE=700602WDUB
+# &TRANS_AMOUNT=200000.0&TRANS_DATE=2019/10/30
+# 15:13:47&TRANS_DESCR=Test%20Test%20Test-CLEARANCE%20-001-p5723474039401
+# &CUSTOMER_ID=p5723474039401&BANK_CODE=500&BRANCH_CODE=001
+# &SERVICE_ID=p5723474039401&CUSTOMER_NAME=Test%20Test%20Test
+# &CUSTOMER_ADDRESS=ASS-ENG&TELLER_ID=etzbankteller&USERNAME=%20
+# &PASSWORD=%20&BANK_NAME=eTranzact%20Intl%20Plc
+# &BRANCH_NAME=ETRANZACT&CHANNEL_NAME=Bank&PAYMENT_METHOD_NAME=Cash
+# &PAYMENT_CURRENCY=566&TRANS_TYPE=101&TRANS_FEE=0.0
+# &TYPE_NAME=CLEARANCE&LEAD_BANK_CODE=700&LEAD_BANK_NAME=eTranzact%20Intl%20Plc
+# COL1=2018/2019&COL2=Acceptance
+# Fee&COL3=ASS&COL4=ENG&COL5=BARTENL&COL6=400&COL7=ug_ft&COL8=N/A&COL9=11/12345
+# &COL10=damms005@gmail.com&COL11=None&COL12=&COL13=
+
+def query_payoutlet(host, terminal_id, confirmation_number, payment, https):
+    headers={"Content-type": "application/x-www-form-urlencoded",
+             "Accept": "text/plain"}
+    url = "/WebConnectPlus/query.jsp"
+    if https:
+        h = httplib.HTTPSConnection(host)
+    else:
+        h = httplib.HTTPConnection(host)
+    args = {'TERMINAL_ID': terminal_id,
+            'CONFIRMATION_NO': confirmation_number,
+            }
+    h.request('POST', url, urlencode(args), headers)
+    response = h.getresponse()
+    if response.status!=200:
+        return False, 'Connection error (%s, %s)' % (response.status, response.reason), None
+    raw = response.read()
+    # Remove empty lines
+    raw = raw.replace('\r\n','')
+    success = parse_qs(raw)
+    if not success.get('CUSTOMER_ID'):
+        msg = _('Invalid or unsuccessful callback: ${a}',
+            mapping = {'a': raw})
+        log = 'invalid callback for payment %s: %s' % (payment.p_id, raw)
+        payment.p_state = 'failed'
+        return False, msg, log
+    # We expect at least two parameters
+    if len(success) < 2:
+        msg = _('Invalid callback: ${a}', mapping = {'a': raw})
+        log = 'invalid callback for payment %s: %s' % (payment.p_id, raw)
+        payment.p_state = 'failed'
+        return False, msg, log
+    customer_id = success.get('CUSTOMER_ID')[0]
+    if payment.p_id != customer_id:
+        msg = _('Wrong payment id')
+        log = 'wrong callback for payment %s: %s' % (payment.p_id, raw)
+        payment.p_state = 'failed'
+        return False, msg, log
+    payment.r_code = u'ET'
+    payment.r_desc = u'%s' % success.get('TRANS_DESCR')[0]
+    payment.r_amount_approved = float(success.get('TRANS_AMOUNT')[0])
+    payment.r_card_num = None
+    payment.r_pay_reference = u'%s' % success.get('RECEIPT_NO')[0]
+    if payment.r_amount_approved != payment.amount_auth:
+        msg = _('Wrong amount')
+        log = 'wrong callback for payment %s: %s' % (payment.p_id, raw)
+        payment.p_state = 'failed'
+        return False, msg, log
+    log = 'valid callback for payment %s: %s' % (payment.p_id, raw)
+    msg = _('Successful callback received')
+    payment.p_state = 'paid'
+    payment.payment_date = datetime.utcnow()
+    return True, msg, log
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/payoutletbrowser.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/payoutletbrowser.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/payoutletbrowser.py	(revision 17238)
@@ -0,0 +1,246 @@
+## $Id: browser.py 15346 2019-03-06 22:19:56Z henrik $
+##
+## Copyright (C) 2012 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
+##
+from datetime import datetime
+import httplib
+import urllib
+import urllib2
+import re
+from xml.dom.minidom import parseString
+import grok
+from zope.component import getUtility
+from zope.catalog.interfaces import ICatalog
+from waeup.kofa.interfaces import IUniversity, CLEARED
+from waeup.kofa.payments.interfaces import IPayer
+from waeup.kofa.webservices import PaymentDataWebservice
+from waeup.kofa.browser.layout import KofaPage, UtilityView
+from waeup.kofa.students.viewlets import ApprovePaymentActionButton as APABStudent
+from waeup.kofa.applicants.viewlets import ApprovePaymentActionButton as APABApplicant
+from waeup.kofa.students.viewlets import PaymentReceiptActionButton as PRABStudent
+from waeup.kofa.applicants.viewlets import PaymentReceiptActionButton as PRABApplicant
+from kofacustom.nigeria.interswitch.browser import (
+    InterswitchActionButtonStudent,
+    InterswitchRequestWebserviceActionButtonStudent,
+    InterswitchActionButtonApplicant,
+    InterswitchRequestWebserviceActionButtonApplicant)
+from kofacustom.nigeria.interfaces import MessageFactory as _
+from kofacustom.nigeria.students.interfaces import INigeriaStudentOnlinePayment
+from kofacustom.nigeria.applicants.interfaces import INigeriaApplicantOnlinePayment
+from kofacustom.nigeria.etranzact.tests import HOST, TERMINAL_ID, HTTPS, GATEWAY_AMT
+from kofacustom.nigeria.etranzact.helpers import query_payoutlet
+
+grok.templatedir('browser_templates')
+
+def payoutlet_module_activated(session, payment):
+    if payment.r_company and payment.r_company != 'etranzact':
+        return False
+    try:
+        return getattr(grok.getSite()['configuration'][str(session)],
+            'etranzact_payoutlet_enabled', False)
+    except KeyError:
+        session = datetime.now().year
+        try:
+            return getattr(grok.getSite()['configuration'][str(session)],
+                'etranzact_payoutlet_enabled', False)
+        except KeyError:
+            return False
+
+class EtranzactEnterPinActionButtonApplicant(APABApplicant):
+    grok.context(INigeriaApplicantOnlinePayment)
+    grok.require('waeup.payApplicant')
+    grok.order(3)
+    icon = 'actionicon_pay.png'
+    text = _('Pay via Etranzact PayOutlet')
+    target = 'enterpin'
+
+    @property
+    def target_url(self):
+        if not payoutlet_module_activated(
+            self.context.__parent__.__parent__.year, self.context):
+            return ''
+        if self.context.p_state in ('paid', 'waived', 'scholarship'):
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class EtranzactEnterPinActionButtonStudent(APABStudent):
+    grok.context(INigeriaStudentOnlinePayment)
+    grok.require('waeup.payStudent')
+    grok.order(10)
+    icon = 'actionicon_pay.png'
+    text = _('Pay via Etranzact PayOutlet')
+    target = 'enterpin'
+
+    @property
+    def target_url(self):
+        if not payoutlet_module_activated(
+            self.context.student.current_session, self.context):
+            return ''
+        if self.context.p_state in ('paid', 'waived', 'scholarship'):
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class EtranzactEnterPinPageStudent(KofaPage):
+    """Enter confirmation PIN and submit to `EtranzactQueryHistoryPageStudent`
+    """
+    grok.context(INigeriaStudentOnlinePayment)
+    grok.name('enterpin')
+    grok.template('enterpin')
+    grok.require('waeup.payStudent')
+
+    buttonname = _('Requery now')
+    label = _('Requery Etranzact PayOutlet History')
+    action = 'query_payoutlet_history'
+    placeholder = _('Confirmation Number (PIN)')
+    gateway_amt = GATEWAY_AMT
+
+    def update(self):
+        if not payoutlet_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'
+        return
+
+class EtranzactEnterPinPageApplicant(KofaPage):
+    """Enter confirmation PIN and submit to `EtranzactQueryHistoryPageApplicant`
+    """
+    grok.require('waeup.payApplicant')
+    grok.context(INigeriaApplicantOnlinePayment)
+    grok.name('enterpin')
+    grok.template('enterpin')
+
+    buttonname = _('Requery now')
+    label = _('Requery Etranzact PayOutlet History')
+    action = 'query_payoutlet_history'
+    placeholder = _('Confirmation Number (PIN)')
+    gateway_amt = GATEWAY_AMT
+
+    def update(self):
+        if not payoutlet_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'
+        return
+
+class PayoutletPaymentReceiptActionButtonApplicant(PRABApplicant):
+
+    grok.view(EtranzactEnterPinPageApplicant)
+
+    @property
+    def target_url(self):
+        if not self.context.r_company:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class PayoutletPaymentReceiptActionButtonStudent(PRABStudent):
+
+    grok.view(EtranzactEnterPinPageStudent)
+
+    @property
+    def target_url(self):
+        if not self.context.r_company:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class EtranzactQueryHistoryPageStudent(UtilityView, grok.View):
+    """ Query history of Etranzact payments
+    """
+    grok.context(INigeriaStudentOnlinePayment)
+    grok.name('query_payoutlet_history')
+    grok.require('waeup.payStudent')
+    terminal_id = TERMINAL_ID
+    host = HOST
+    https = HTTPS
+
+    def update(self, confirmation_number=None):
+        if not payoutlet_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 == 'paid':
+            self.flash(_('This ticket has already been paid.'))
+            return
+        student = self.context.student
+        success, msg, log = query_payoutlet(
+            self.host, self.terminal_id, confirmation_number,
+            self.context, self.https)
+        student.writeLogMessage(self, log)
+        if not success:
+            self.flash(msg, type="danger")
+            return
+        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
+
+class EtranzactQueryHistoryPageApplicant(UtilityView, grok.View):
+    """ Query history of Etranzact payments
+    """
+    grok.context(INigeriaApplicantOnlinePayment)
+    grok.name('query_payoutlet_history')
+    grok.require('waeup.payApplicant')
+    terminal_id = TERMINAL_ID
+    host = HOST
+    https = HTTPS
+
+    def update(self, confirmation_number=None):
+        if not payoutlet_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 == 'paid':
+            self.flash(_('This ticket has already been paid.'))
+            return
+        applicant = self.context.__parent__
+        success, msg, log = query_payoutlet(
+            self.host, self.terminal_id, confirmation_number,
+            self.context, self.https)
+        applicant.writeLogMessage(self, log)
+        if not success:
+            self.flash(msg)
+            return
+        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, '@@index'))
+        return
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/payoutletwebservice.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/payoutletwebservice.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/payoutletwebservice.py	(revision 17238)
@@ -0,0 +1,118 @@
+## $Id: browser.py 15346 2019-03-06 22:19:56Z henrik $
+##
+## Copyright (C) 2012 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 zope.component import getUtility
+from zope.catalog.interfaces import ICatalog
+from waeup.kofa.interfaces import academic_sessions_vocab, IKofaUtils
+from waeup.kofa.payments.interfaces import IPayer
+from waeup.kofa.webservices import PaymentDataWebservice
+from kofacustom.nigeria.etranzact.helpers import ERROR_PART1, ERROR_PART2
+
+class NigeriaPaymentDataWebservice(PaymentDataWebservice):
+    """A simple webservice to publish payment and payer details on request from
+    accepted IP addresses without authentication.
+
+    Etranzact is asking for the PAYEE_ID which is indeed misleading.
+    These are not the data of the payee but of the payer. And it's
+    not the id of the payer but of the payment.
+    """
+    grok.name('feerequest')
+
+    #ACCEPTED_IP = ('195.219.3.181', '195.219.3.184')
+    ACCEPTED_IP = None
+
+    CATEGORY_MAPPING = {
+        'SCHOOLFEE': ('schoolfee',),
+         }
+
+    def update(self, PAYEE_ID=None, PAYMENT_TYPE=None):
+
+        if PAYEE_ID == None:
+            self.output = ERROR_PART1 + 'Missing PAYEE_ID' + ERROR_PART2
+            return
+        real_ip = self.request.get('HTTP_X_FORWARDED_FOR', None)
+        # We can forego the logging once eTranzact payments run smoothly
+        # and the accepted IP addresses are used.
+        if real_ip:
+            self.context.logger.info('PaymentDataWebservice called: %s' % real_ip)
+        if real_ip  and self.ACCEPTED_IP:
+            if real_ip not in  self.ACCEPTED_IP:
+                self.output = ERROR_PART1 + 'Wrong IP address' + ERROR_PART2
+                return
+        payment_cats_dict = getUtility(IKofaUtils).PAYMENT_CATEGORIES
+        if not PAYMENT_TYPE or (PAYMENT_TYPE not in self.CATEGORY_MAPPING.keys() \
+            and PAYMENT_TYPE.lower() not in payment_cats_dict.keys()):
+            self.output = ERROR_PART1 + 'Invalid PAYMENT_TYPE' + ERROR_PART2
+            return
+        cat = getUtility(ICatalog, name='payments_catalog')
+        results = list(cat.searchResults(p_id=(PAYEE_ID, PAYEE_ID)))
+        if len(results) != 1:
+            self.output = ERROR_PART1 + 'Invalid PAYEE_ID' + ERROR_PART2
+            return
+        amount = results[0].amount_auth
+        payment_type = results[0].category
+        p_category = results[0].p_category
+        programme_type = results[0].p_item
+        if not programme_type:
+            programme_type = 'N/A'
+        academic_session = academic_sessions_vocab.getTerm(
+            results[0].p_session).title
+        status = results[0].p_state
+        if status == 'paid':
+            self.output = ERROR_PART1 + 'PAYEE_ID already used' + ERROR_PART2
+            return
+        if p_category not in self.CATEGORY_MAPPING.get(PAYMENT_TYPE, []) \
+            and p_category != PAYMENT_TYPE.lower():
+            self.output = ERROR_PART1 + 'Wrong PAYMENT_TYPE' + ERROR_PART2
+            return
+        try:
+            owner = IPayer(results[0])
+            full_name = owner.display_fullname
+            matric_no = owner.matric_number
+            faculty = owner.faculty
+            department = owner.department
+            study_type = owner.current_mode
+            email = owner.email
+            phone = owner.phone
+            level = owner.current_level
+        except (TypeError, AttributeError):
+            self.output = ERROR_PART1 +  'Unknown error' + ERROR_PART2
+            return
+        self.output = (
+            'PayeeName=%s~' +
+            'Faculty=%s~' +
+            'Department=%s~' +
+            'Level=%s~' +
+            'ProgrammeType=%s~' +
+            'StudyType=%s~' +
+            'Session=%s~' +
+            'PayeeID=%s~' +
+            'Amount=%s~' +
+            'FeeStatus=%s~' +
+            'Semester=N/A~' +
+            'PaymentType=%s~' +
+            'MatricNumber=%s~' +
+            'Email=%s~' +
+            'PhoneNumber=%s'
+
+            ) % (full_name, faculty,
+            department, level, programme_type, study_type,
+            academic_session, PAYEE_ID, amount, status, payment_type,
+            matric_no, email, phone)
+        return
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/studentsbrowser.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/studentsbrowser.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/studentsbrowser.py	(revision 17238)
@@ -0,0 +1,230 @@
+## $Id: studentsbrowser.py 15599 2019-09-20 13:24:59Z henrik $
+##
+## Copyright (C) 2017 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import grok
+import hashlib
+from datetime import datetime, timedelta
+from zope.component import getUtility
+from zope.security import checkPermission
+from waeup.kofa.interfaces import IKofaUtils
+from waeup.kofa.utils.helpers import to_timezone
+from waeup.kofa.browser.layout import UtilityView, KofaPage
+from waeup.kofa.browser.viewlets import ManageActionButton
+from kofacustom.nigeria.etranzact.helpers import (
+    write_payments_log, process_response, query_history)
+from kofacustom.nigeria.students.browser import NigeriaOnlinePaymentDisplayFormPage as NOPDPStudent
+from kofacustom.nigeria.payments.interfaces import INigeriaOnlinePayment
+from kofacustom.nigeria.students.interfaces import INigeriaStudentOnlinePayment
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+from kofacustom.nigeria.etranzact.tests import (
+    TERMINAL_ID, HOST, HTTPS, SECRET_KEY, LOGO_URL, GATEWAY_AMT)
+
+grok.templatedir('browser_templates')
+
+def webconnect_module_activated(session, payment):
+    if payment.r_company and payment.r_company != 'etranzact':
+        return False
+    try:
+        return getattr(grok.getSite()['configuration'][str(session)],
+            'etranzact_webconnect_enabled', False)
+    except KeyError:
+        session = datetime.now().year
+        try:
+            return getattr(grok.getSite()['configuration'][str(session)],
+                'etranzact_webconnect_enabled', False)
+        except KeyError:
+            return False
+
+class EtranzactActionButtonStudent(ManageActionButton):
+    grok.order(8)
+    grok.context(INigeriaOnlinePayment)
+    grok.view(NOPDPStudent)
+    grok.require('waeup.payStudent')
+    icon = 'actionicon_pay.png'
+    text = _('Pay via Etranzact WebConnect')
+    target = 'goto_etranzact'
+
+    @property
+    def target_url(self):
+        if not webconnect_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 EtranzactRequeryActionButtonStudent(ManageActionButton):
+    grok.order(9)
+    grok.context(INigeriaOnlinePayment)
+    grok.view(NOPDPStudent)
+    grok.require('waeup.payStudent')
+    icon = 'actionicon_call.png'
+    text = _('Requery Etranzact WebConnect History')
+    target = 'requery_history'
+
+    @property
+    def target_url(self):
+        if not webconnect_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 EtranzactPageStudent(KofaPage):
+    """ View which sends a POST request to the Etranzact payment gateway.
+    """
+    grok.context(INigeriaStudentOnlinePayment)
+    grok.name('goto_etranzact')
+    grok.template('goto_etranzact')
+    grok.require('waeup.payStudent')
+    label = _('Pay via Etranzact')
+    submit_button = _('Pay now')
+
+    host = HOST
+    https = HTTPS
+    secret_key = SECRET_KEY
+    terminal_id = TERMINAL_ID
+    logo_url = LOGO_URL
+    gateway_amt = GATEWAY_AMT
+
+    @property
+    def action(self):
+        if self.https:
+            return 'https://' + self.host + '/webconnect/v3/caller.jsp'
+        return 'http://' + self.host + '/webconnect/v3/caller.jsp'
+
+    def init_update(self):
+        if self.context.p_state == 'paid':
+            return _("Payment ticket can't be re-sent to Etranzact.")
+        now = datetime.utcnow()
+        if self.context.creation_date.tzinfo is not None:
+            # That's bad. Please store timezone-naive datetimes only!
+            now = self.context.creation_date.tzinfo.localize(now)
+        time_delta = now - self.context.creation_date
+        if time_delta.days > 7:
+            return _("This payment ticket is too old. Please create a new ticket.")
+        # In contrast to the procedure in the Remita and Interswitch modules,
+        # we do not call requery_history but receive and evaluate
+        # the response form from Etranzact directly. This is possible
+        # because Etranzact provides the FINAL_CHECKSUM hash value
+        # which authenticates the response.
+        self.responseurl = self.url(self.context, 'receive_etranzact')
+        self.transaction_id = self.context.p_id
+        self.description = "%s (%s)" % (
+            self.context.p_category, self.context.p_item)
+        hashargs = 	self.amount + self.terminal_id + self.transaction_id \
+            + self.responseurl + self.secret_key
+        self.hashvalue = hashlib.md5(hashargs).hexdigest()
+        self.customer = self.context.student
+        return
+
+    def update(self):
+        if not webconnect_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" % self.context.amount_auth
+        error = self.init_update()
+        if error:
+            self.flash(error, type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        return
+
+class EtranzactReceiveResponseStudent(NOPDPStudent):
+    """ View that receives the response from eTrantact payment gateway.
+    """
+    grok.name('receive_etranzact')
+
+    secret_key = SECRET_KEY
+    terminal_id = TERMINAL_ID
+
+    def update(self):
+        super(EtranzactReceiveResponseStudent, self).update()
+        if not webconnect_module_activated(
+            self.context.student.current_session, self.context):
+            self.flash(_('Forbidden'), type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        student = self.context.student
+        form = self.request.form
+        verify = False
+        if self.context.p_state == 'paid':
+            verify = True
+        success, msg, log = process_response(self.context, form, self, verify)
+        student.writeLogMessage(self, log)
+        if not success:
+            self.flash(msg, type='danger')
+            return
+        write_payments_log(student.student_id, self.context)
+        flashtype, msg, log = self.context.doAfterStudentPayment()
+        if log is not None:
+            student.writeLogMessage(self, log)
+        self.flash(msg, type=flashtype)
+        return
+
+class EtranzactRequestPaymentStatusPageStudent(UtilityView, grok.View):
+    """ Request webservice view for the Etranzact gateway.
+    """
+    grok.context(INigeriaStudentOnlinePayment)
+    grok.name('requery_history')
+    grok.require('waeup.payStudent')
+
+    host = HOST
+    https = HTTPS
+    secret_key = SECRET_KEY
+    terminal_id = TERMINAL_ID
+    logo_url = LOGO_URL
+
+    def update(self):
+        if not webconnect_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
+        raw, form = query_history(self.host, self.terminal_id,
+                                  self.context.p_id, self.https)
+        success, msg, log = process_response(self.context, form, self, verify)
+        student.writeLogMessage(self, log)
+        if not success:
+            self.flash(msg, type='danger')
+            return
+        write_payments_log(student.student_id, self.context)
+        flashtype, msg, log = self.context.doAfterStudentPayment()
+        if log is not None:
+            student.writeLogMessage(self, log)
+        self.flash(msg, type=flashtype)
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context))
+        return
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/tests.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/tests.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/etranzact/tests.py	(revision 17238)
@@ -0,0 +1,319 @@
+## $Id$
+##
+## 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 os
+import unittest
+import random
+import json
+import hashlib
+import httplib
+from urllib import urlencode
+from datetime import datetime, timedelta, date
+from zope.component import createObject, getUtility
+from zope.catalog.interfaces import ICatalog
+from hurry.workflow.interfaces import IWorkflowState
+from waeup.kofa.students.tests.test_browser import StudentsFullSetup
+from waeup.kofa.applicants.tests.test_browser import ApplicantsFullSetup
+from waeup.kofa.configuration import SessionConfiguration
+from kofacustom.nigeria.students.payments import NigeriaStudentOnlinePayment
+from kofacustom.nigeria.testing import FunctionalLayer
+from kofacustom.nigeria.etranzact.helpers import (
+    query_history, query_payoutlet, ERROR_PART1, ERROR_PART2)
+
+#from kofacustom.nigeria.etranzact.helpers import (query_etranzact)
+
+# Also run tests that send requests to external servers?
+#   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
+
+TERMINAL_ID = '5003021194'
+HOST = 'demo.etranzactng.com'
+HTTPS = True
+SECRET_KEY = 'DEMO_KEY'
+LOGO_URL = 'https://iuokada.waeup.org/static_custom/iou_logo.png'
+GATEWAY_AMT = 500.0
+
+# Valid transaction id in Etranzact system
+TID = 'p5689785145198'
+
+def external_test(func):
+    if not EXTERNAL_TESTS:
+        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 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
+
+    @external_test
+    def test_query_history(self):
+        transaction_id = str(random.randint(100000000, 999999999))
+        raw, formvars = query_history(HOST, self.terminal_id,
+                                transaction_id, HTTPS)
+        self.assertTrue(
+            'Transaction with the given transaction_id (%s) not found'
+            % transaction_id in raw)
+        # Okay, let's create a transaction
+        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
+        raw, formvars = query_history(HOST, self.terminal_id,
+                                transaction_id, HTTPS)
+        self.assertTrue(
+            '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"\r\n    '
+            '"http://www.w3.org/TR/html4/loose.dtd">' in raw)
+        # The same, an 'empty' response obviously means that the transaction
+        # was found but no result was  sent to the response_url because the
+        # payment was cancelled.
+
+        # Peter: No response would be returned because payment status information
+        # was not saved. Whenever you get a null response, means payment was
+        # not received only payments with error code 0 is successful.
+        # So in this case transaction fails.
+
+        # We manually created and tried to pay a transaction 
+        # which seems to be valid till next restart of the demo portal.
+        raw, formvars = query_history(HOST, self.terminal_id, TID, HTTPS)
+        # Now Etranzact is redirecting but the response is still useless
+        self.assertTrue('Redirecting...' in raw)
+        self.assertEqual(formvars['TRANSACTION_ID'], TID)
+        return
+
+    @external_test
+    def test_query_payoutlet(self):
+        # We've got some test numbers from Etranzact for testing
+        payment = NigeriaStudentOnlinePayment()
+        payment.p_id = 'p5723474039401'
+        success, msg, log = query_payoutlet(
+            HOST, '5003021194', '500854291572447457669', payment, True)
+        self.assertTrue(msg, 'Wrong amount')
+        payment.amount_auth = 200000.0
+        success, msg, log = query_payoutlet(
+            HOST, '5003021194', '500854291572447457669', payment, True)
+        self.assertTrue(success)
+        payment.p_id = 'xyz'
+        success, msg, log = query_payoutlet(
+            HOST, '5003021194', '500854291572447457669', payment, True)
+        self.assertTrue(msg, 'Wrong payment id')
+        return
+
+class EtranzactTestsApplicants(ApplicantsFullSetup):
+    """Tests for the Etranzact payment gateway.
+    """
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(EtranzactTestsApplicants, self).setUp()
+        configuration = SessionConfiguration()
+        configuration.academic_session = datetime.now().year - 2
+        configuration.etranzact_webconnect_enabled = True
+        configuration.etranzact_payoutlet_enabled = True
+        self.app['configuration'].addSessionConfiguration(configuration)
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_path)
+        #IWorkflowState(self.student).setState('started')
+        super(EtranzactTestsApplicants, self).fill_correct_values()
+        self.applicantscontainer.application_fee = 3333.0
+        self.browser.getControl(name="form.nationality").value = ['NG']
+        self.browser.getControl(name="transition").value = ['start']
+        self.browser.getControl("Save").click()
+        self.browser.getControl("Add online").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+        self.payment = self.applicant.values()[0]
+        self.payment_url = self.browser.url
+
+    @external_test
+    def test_applicant_views(self):
+        # Manager can access Etranzact form
+        self.browser.getLink("Pay via Etranzact WebConnect").click()
+        self.assertTrue("Pay now" in self.browser.contents)
+        # Means of testing end here.
+        # We requery an existing paiment now.
+        self.payment.p_id = TID
+        self.browser.open(self.payment_url)
+        self.browser.getLink("Requery Etranzact WebConnect History").click()
+        self.assertTrue('Wrong checksum.' in self.browser.contents)
+        # ... probably because responseurl of the transaction stored in the
+        # system and the responseurl generated in process_response are
+        # different
+        # Means of testing end here again.
+        return
+
+class EtranzactTestsStudents(StudentsFullSetup):
+    """Tests for the Etranzact payment gateway.
+    """
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(EtranzactTestsStudents, self).setUp()
+        self.app['configuration']['2004'].etranzact_webconnect_enabled = True
+        self.app['configuration']['2004'].etranzact_payoutlet_enabled = True
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.payments_path)
+        IWorkflowState(self.student).setState('cleared')
+        self.student.nationality = u'NG'
+        self.browser.open(self.payments_path + '/addop')
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+        self.browser.open(self.payments_path)
+        ctrl = self.browser.getControl(name='val_id')
+        self.value = ctrl.options[0]
+        self.browser.getLink(self.value).click()
+        self.assertMatches('...Amount Authorized...',
+                           self.browser.contents)
+        self.assertTrue('<span>40000.0</span>', self.browser.contents)
+        self.payment_url = self.browser.url
+        self.payment = self.student['payments'][self.value]
+
+    @external_test
+    def test_student_views(self):
+        # Manager can access Etranzact form
+        self.browser.getLink("Pay via Etranzact WebConnect").click()
+        self.assertTrue("Pay now" in self.browser.contents)
+        # Means of testing end here.
+        # We requery an existing paiment now.
+        self.payment.p_id = TID
+        self.browser.open(self.payment_url)
+        self.browser.getLink("Requery Etranzact WebConnect History").click()
+        self.assertTrue('Wrong checksum.' in self.browser.contents)
+        # ... probably because responseurl and amount stored in the
+        # system and the responseurl generated in process_response are
+        # different
+        # Means of testing end here again.
+        return
+
+    @external_test
+    def test_student_payoutlet_views(self):
+        self.browser.getLink("Pay via Etranzact PayOutlet").click()
+        self.browser.getControl(name="confirmation_number").value = '600854291572447457669'
+        self.browser.getControl("Requery now").click()
+        # This response is strange
+        self.assertTrue('-3:Wrong Setup' in self.browser.contents)
+        self.browser.getLink("Pay via Etranzact PayOutlet").click()
+        # This confirmation number exists
+        self.browser.getControl(name="confirmation_number").value = '500854291572447457669'
+        self.browser.getControl("Requery now").click()
+        self.assertTrue('Wrong amount' in self.browser.contents)
+        # Some attributes have been set
+        self.assertEqual(self.payment.r_desc, 'Test Test Test-CLEARANCE -001-p5723474039401')
+        return
+
+    def test_webservice(self):
+        self.browser.open(
+            'http://localhost/app/feerequest?PAYEE_ID=%s&PAYMENT_TYPE=SCHOOLFEE'
+            % self.payment.p_id)
+        self.assertEqual(self.browser.contents,
+            'PayeeName=Anna Tester~'
+            'Faculty=fac1~Department=dep1~'
+            'Level=100~ProgrammeType=CERT1~'
+            'StudyType=ug_ft~Session=2004/2005~'
+            'PayeeID=%s~'
+            'Amount=40000.0~FeeStatus=unpaid~'
+            'Semester=N/A~PaymentType=School Fee~'
+            'MatricNumber=234~Email=aa@aa.ng~'
+            'PhoneNumber=1234' % self.payment.p_id)
+        self.browser.open('http://localhost/app/feerequest')
+        self.assertEqual(self.browser.contents, ERROR_PART1 + 'Missing PAYEE_ID' + ERROR_PART2)
+
+        self.browser.open('http://localhost/app/feerequest?NONSENSE=nonsense')
+        self.assertEqual(self.browser.contents, ERROR_PART1 + 'Missing PAYEE_ID' + ERROR_PART2)
+
+        self.browser.open(
+            'http://localhost/app/feerequest?PAYEE_ID=nonsense&PAYMENT_TYPE=SCHOOLFEE')
+        self.assertEqual(self.browser.contents, ERROR_PART1 + 'Invalid PAYEE_ID' + ERROR_PART2)
+
+        self.browser.open(
+            'http://localhost/app/feerequest?PAYEE_ID=%s&PAYMENT_TYPE=NONSENSE'
+            % self.payment.p_id)
+        self.assertEqual(self.browser.contents, ERROR_PART1 + 'Invalid PAYMENT_TYPE' + ERROR_PART2)
+
+        self.browser.open(
+            'http://localhost/app/feerequest?PAYEE_ID=%s' % self.payment.p_id)
+        self.assertEqual(self.browser.contents, ERROR_PART1 + 'Invalid PAYMENT_TYPE' + ERROR_PART2)
+
+        self.browser.open(
+            'http://localhost/app/feerequest?PAYEE_ID=%s&PAYMENT_TYPE=CLEARANCE'
+            % self.payment.p_id)
+        self.assertEqual(self.browser.contents, ERROR_PART1 + 'Wrong PAYMENT_TYPE' + ERROR_PART2)
+
+        # Change payment state
+        self.student['payments'][self.payment.p_id].p_state = 'paid'
+
+        self.browser.open(
+            'http://localhost/app/feerequest?PAYEE_ID=%s&PAYMENT_TYPE=SCHOOLFEE'
+            % self.payment.p_id)
+        self.assertEqual(self.browser.contents, ERROR_PART1 + 'PAYEE_ID already used' + ERROR_PART2)
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/ftesting.zcml
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/ftesting.zcml	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/ftesting.zcml	(revision 17238)
@@ -0,0 +1,91 @@
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   xmlns:kofa="http://namespaces.waeup.org/kofa"
+   i18n_domain="kofacustom.nigeria"
+   package="kofacustom.nigeria"
+   >
+
+  <include package="grok" />
+  <includeOverrides package="kofacustom.nigeria" />
+  <include package="kofacustom.nigeria" file="mail.zcml" />
+
+  <!-- Where should the datacenter reside by default?
+
+       The path is relative to the `package` defined in top
+       `configure` directive.
+  -->
+  <kofa:datacenter
+      path="../../../parts/test/datacenter" />
+
+
+  <!-- Typical functional testing security setup -->
+  <securityPolicy
+      component="zope.securitypolicy.zopepolicy.ZopeSecurityPolicy"
+      />
+
+
+  <unauthenticatedPrincipal
+      id="zope.anybody"
+      title="Unauthenticated User"
+      />
+  <unauthenticatedGroup
+      id="zope.Anybody"
+      title="Unauthenticated Users"
+      />
+  <authenticatedGroup
+      id="zope.Authenticated"
+      title="Authenticated Users"
+      />
+  <everybodyGroup
+      id="zope.Everybody"
+      title="All Users"
+      />
+
+
+  <!-- grant
+      permission="zope.View"
+      principal="zope.Anybody"
+      / -->
+
+  <grant
+      permission="zope.app.dublincore.view"
+      principal="zope.Anybody" />
+  <grant
+      permission="zope.View"
+      principal="zope.Authenticated"
+      />
+  <grant
+      permission="zope.app.dublincore.view"
+      principal="zope.Authenticated"
+      />
+  <grant
+      permission="waeup.Public"
+      principal="zope.Everybody"
+      />
+  <grant
+      permission="waeup.Anonymous"
+      principal="zope.Anybody"
+      />
+
+  <principal
+      id="zope.mgr"
+      title="Manager"
+      login="mgr"
+      password="mgrpw"
+      />
+
+  <role
+      id="zope.Manager"
+      title="Site Manager"
+      />
+
+  <grantAll
+      role="zope.Manager"
+      />
+
+  <grant
+      role="zope.Manager"
+      principal="zope.mgr"
+      />
+
+</configure>
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interfaces.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interfaces.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interfaces.py	(revision 17238)
@@ -0,0 +1,172 @@
+## $Id$
+##
+## Copyright (C) 2011 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 re
+import zope.i18nmessageid
+from zope import schema
+from zc.sourcefactory.basic import BasicSourceFactory
+from waeup.kofa.interfaces import (SimpleKofaVocabulary,
+    ISessionConfiguration, academic_sessions_vocab,
+    ContextualDictSourceFactoryBase)
+from kofacustom.nigeria.utils.lgas import LGAS
+
+_ = MessageFactory = zope.i18nmessageid.MessageFactory('kofacustom.nigeria')
+
+high_qual = SimpleKofaVocabulary(
+    ('National Certificate of Education','nce'),
+    ('Pre Degree Programme','pre'),
+    ('Diploma Programme','dip'),
+    ('National Diploma','ond'),
+    ('Bachelors Degree','bd'),
+    ('Masters Degree','md'),
+    ('Professional Qualification','pq'),
+    ('Other Certification','oc'),
+    ('Higher National Diploma','hnd'),
+    ('Post Graduate Diploma','pgd'),
+    ('NCE AL OTH','nce_al_oth'),
+    ('National Defence Academy','nda'),
+    ('RMC','rmc'),
+    ('Ph.D.(Doctor of Philosophy)','phd'),
+    ('M.Phil (Master of Philosophy)','mphil'),
+    ('JUPEB','jupeb'),
+    ('IJMBE','ijmbe'),
+    (_('Awaiting Results'),'a_rslt'),
+    )
+
+high_grade = SimpleKofaVocabulary(
+    ('Upper Credit','upper_credit'),
+    ('Distinction','distinction'),
+    ('Credit','credit'),
+    ('Merit','merit'),
+    ('Lower Credit','lower_credit'),
+    ('First Class','first_class'),
+    ('Second Class Upper','second_class_upper'),
+    ('Second Class Lower','second_class_lower'),
+    ('Third Class','third_class'),
+    ('Pass','pass'),
+    ('A Levels','al'),
+    ('Unclassified Certificate', 'uc'),
+    (_('Awaiting Results'),'a_rslt'),
+    )
+
+exam_types = SimpleKofaVocabulary(
+    ('SSCE','ssce'),
+    ('WAEC','waec'),
+    ('GCE O\' LEVEL','gce_o_level'),
+    ('TC II','tc_ii'),
+    ('RSA','rsa'),
+    ('NABTEB','nabteb'),
+    ('NECO','neco'),
+    ('ACE','ace'),
+    ('GCE A\' LEVEL','gce_a_level'),
+    ('IGCSE','igcse'),
+    ('WAEC Technical Examination','wte'),
+    )
+
+# Define a validation method for JAMB reg numbers 
+class NotJAMBRegNumber(schema.ValidationError):
+    __doc__ = u"Invalid JAMB registration number"
+
+#: Regular expression to check jamb_reg_number formats.
+check_jamb_reg_number = re.compile(r"^\d{12}[A-Z]{2}$").match
+
+def validate_jamb_reg_number(value):
+    if not check_jamb_reg_number(value):
+        raise NotJAMBRegNumber(value)
+    return True
+
+#lgas_vocab = SimpleKofaVocabulary(
+#    *sorted([(x[1],x[0]) for x in LGAS]))
+
+class LGASource(BasicSourceFactory):
+    """A source for school subjects used in exam documentation.
+    """
+    lga_dict = dict(LGAS)
+
+    def getValues(self):
+        return sorted(self.lga_dict.keys())
+
+    def getToken(self, value):
+        return str(value)
+
+    def getTitle(self, value):
+        return self.lga_dict.get(value,
+            _('Invalid key: ${a}', mapping = {'a':value}))
+
+class DisabilitiesSource(ContextualDictSourceFactoryBase):
+    """A source for filtering groups of students
+    """
+    #: name of dict to deliver from kofa utils.
+    DICT_NAME = 'DISABILITIES_DICT'
+
+class GradingSystemSource(ContextualDictSourceFactoryBase):
+    """A application category source delivers all special handling categories
+    provided for accommodation booking.
+    """
+    #: name of dict to deliver from kofa utils.
+    DICT_NAME = 'GRADING_SYSTEM_DICT'
+
+class ICustomSessionConfiguration(ISessionConfiguration):
+    """A session configuration object.
+    """
+
+    remita_enabled = schema.Bool(
+        title = _(u'Remita integration enabled'),
+        default = False,
+        )
+
+    interswitch_enabled = schema.Bool(
+        title = _(u'Interswitch Collegepay integration enabled'),
+        default = True,
+        )
+
+    interswitch_paydirect_enabled = schema.Bool(
+        title = _(u'Interswitch Paydirect integration enabled'),
+        default = True,
+        )
+
+    interswitch_webcheckout_enabled = schema.Bool(
+        title = _(u'Interswitch WebCheckout integration enabled'),
+        default = False,
+        )
+
+    etranzact_webconnect_enabled = schema.Bool(
+        title = _(u'Etranzact Webconnect integration enabled'),
+        default = False,
+        )
+
+    etranzact_payoutlet_enabled = schema.Bool(
+        title = _(u'Etranzact Payoutlet integration enabled'),
+        default = False,
+        )
+
+class ICustomSessionConfigurationAdd(ICustomSessionConfiguration):
+    """A session configuration object in add mode.
+    """
+
+    academic_session = schema.Choice(
+        title = _(u'Academic Session'),
+        source = academic_sessions_vocab,
+        default = None,
+        required = True,
+        readonly = False,
+        )
+
+ICustomSessionConfigurationAdd[
+    'academic_session'].order =  ICustomSessionConfiguration[
+    'academic_session'].order
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/__init__.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/__init__.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/__init__.py	(revision 17238)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/browser.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/browser.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/browser.py	(revision 17238)
@@ -0,0 +1,455 @@
+## $Id$
+##
+## Copyright (C) 2012 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.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.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
+
+# Buttons
+
+def module_activated(session, payment):
+    if payment.r_company and payment.r_company != 'interswitch':
+        return False
+    try:
+        return getattr(grok.getSite()['configuration'][str(session)],
+            'interswitch_enabled', True)
+    except KeyError:
+        session = datetime.now().year
+        try:
+            return getattr(grok.getSite()['configuration'][str(session)],
+                'interswitch_enabled', True)
+        except KeyError:
+            return False
+
+class InterswitchActionButtonStudent(ManageActionButton):
+    grok.order(1)
+    grok.context(INigeriaOnlinePayment)
+    grok.view(OPDPStudent)
+    grok.require('waeup.payStudent')
+    icon = 'actionicon_pay.png'
+    text = _('Pay via Interswitch CollegePAY')
+    target = 'goto_interswitch'
+
+    @property
+    def target_url(self):
+        if not module_activated(
+            self.context.student.current_session, self.context):
+            return ''
+        if self.context.p_state != 'unpaid':
+            return ''
+        if self.context.amount_auth == 0:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class InterswitchActionButtonApplicant(InterswitchActionButtonStudent):
+    grok.view(OPDPApplicant)
+    grok.require('waeup.payApplicant')
+
+    @property
+    def target_url(self):
+        if not module_activated(
+            self.context.__parent__.__parent__.year, self.context):
+            return ''
+        if self.context.p_state != 'unpaid':
+            return ''
+        if self.context.amount_auth == 0:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class InterswitchRequestWebserviceActionButtonStudent(ManageActionButton):
+    grok.order(2)
+    grok.context(INigeriaOnlinePayment)
+    grok.view(OPDPStudent)
+    grok.require('waeup.payStudent')
+    icon = 'actionicon_call.png'
+    text = _('Requery CollegePAY History')
+    target = 'request_webservice'
+
+    @property
+    def target_url(self):
+        if not 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 InterswitchRequestWebserviceActionButtonApplicant(
+    InterswitchRequestWebserviceActionButtonStudent):
+    grok.view(OPDPApplicant)
+    grok.require('waeup.payApplicant')
+
+    @property
+    def target_url(self):
+        if not 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)
+
+class InterswitchVerifyWebserviceActionButtonStudent(ManageActionButton):
+    grok.order(3)
+    grok.context(INigeriaOnlinePayment)
+    grok.view(OPDPStudent)
+    grok.require('waeup.manageStudent')
+    icon = 'actionicon_call.png'
+    text = _('Verify Payment')
+    target = 'verify_payment'
+
+    @property
+    def target_url(self):
+        if not module_activated(
+            self.context.student.current_session, self.context):
+            return ''
+        if self.context.p_state != 'paid' \
+            or self.context.r_company != u'interswitch':
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class InterswitchVerifyWebserviceActionButtonApplicant(
+    InterswitchVerifyWebserviceActionButtonStudent):
+    grok.view(OPDPApplicant)
+    grok.require('waeup.manageApplication')
+
+    @property
+    def target_url(self):
+        if not module_activated(
+            self.context.__parent__.__parent__.year, self.context):
+            return ''
+        if self.context.p_state != 'paid' \
+            or self.context.r_company != u'interswitch':
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+# Webservice request views
+
+class InterswitchPaymentRequestWebservicePageStudent(UtilityView, grok.View):
+    """ Request webservice view for the CollegePAY gateway
+    """
+    grok.context(INigeriaStudentOnlinePayment)
+    grok.name('request_webservice')
+    grok.require('waeup.payStudent')
+
+    product_id = None
+    gateway_host = None
+    gateway_url = None
+    https = True
+    mac = None
+
+    def update(self):
+        if not 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 = query_interswitch(
+            self.context, self.product_id,
+            self.gateway_host, self.gateway_url,
+            self.https, self.mac, False)
+        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
+
+class InterswitchPaymentRequestWebservicePageApplicant(UtilityView, grok.View):
+    """ Request webservice view for the CollegePAY gateway
+    """
+    grok.context(INigeriaApplicantOnlinePayment)
+    grok.name('request_webservice')
+    grok.require('waeup.payApplicant')
+
+    product_id = None
+    gateway_host = None
+    gateway_url = None
+    https = True
+    mac = None
+
+    def update(self):
+        if not 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 = query_interswitch(
+            self.context, self.product_id,
+            self.gateway_host, self.gateway_url,
+            self.https, self.mac, False)
+        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 InterswitchPaymentVerifyWebservicePageStudent(UtilityView, grok.View):
+    """ Verify payment view for the CollegePAY gateway
+    """
+    grok.context(INigeriaStudentOnlinePayment)
+    grok.name('verify_payment')
+    grok.require('waeup.manageStudent')
+
+    product_id = None
+    gateway_host = None
+    gateway_url = None
+    https = True
+    mac = None
+
+    def update(self):
+        if not module_activated(
+            self.context.student.current_session, self.context):
+            return
+        if self.context.p_state  != 'paid' \
+            or self.context.r_company != u'interswitch':
+            self.flash(_('This ticket has not been paid.'), type='danger')
+            return
+        student = self.context.student
+        success, msg, log = query_interswitch(
+            self.context, self.product_id,
+            self.gateway_host, self.gateway_url,
+            self.https, self.mac, True)
+        student.writeLogMessage(self, log)
+        if not success:
+            self.flash(msg, type='danger')
+            return
+        self.flash(msg)
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, '@@index'))
+        return
+
+class InterswitchPaymentVerifyWebservicePageApplicant(UtilityView, grok.View):
+    """ Verify payment view for the CollegePAY gateway
+    """
+    grok.context(INigeriaApplicantOnlinePayment)
+    grok.name('verify_payment')
+    grok.require('waeup.manageApplication')
+
+    product_id = None
+    gateway_host = None
+    gateway_url = None
+    https = True
+    mac = None
+
+    def update(self):
+        if not module_activated(
+            self.context.__parent__.__parent__.year, self.context):
+            return
+        if self.context.p_state != 'paid' \
+            or self.context.r_company != u'interswitch':
+            self.flash(_('This ticket has not been paid.'), type='danger')
+            return
+        applicant = self.context.__parent__
+        success, msg, log = query_interswitch(
+            self.context, self.product_id,
+            self.gateway_host, self.gateway_url,
+            self.https, self.mac, True)
+        applicant.writeLogMessage(self, log)
+        if not success:
+            self.flash(msg, type='danger')
+            return
+        self.flash(msg)
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, '@@index'))
+        return
+
+# Forwarding pages
+
+class InterswitchPageStudent(KofaPage):
+    """ View which sends a POST request to the Interswitch
+    CollegePAY payment gateway.
+    """
+    grok.context(INigeriaOnlinePayment)
+    grok.name('goto_interswitch')
+    grok.template('student_goto_interswitch')
+    grok.require('waeup.payStudent')
+    label = _('Submit data to CollegePAY')
+    submit_button = _('Submit')
+
+    action = None
+    site_name = None
+    currency = None
+    pay_item_id = None
+    product_id = None
+    xml_data = None
+    hashvalue = None
+    gateway_amt = GATEWAY_AMT
+
+    def init_update(self):
+        if self.context.p_state == 'paid':
+            return _("Payment ticket can't be re-sent to CollegePAY.")
+        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
+        certificate = getattr(student['studycourse'],'certificate',None)
+        if certificate is None:
+            return _("Study course data are incomplete.")
+        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 = {}
+        if certificate is not None:
+            xmldict['department'] = certificate.__parent__.__parent__.code
+            xmldict['faculty'] = certificate.__parent__.__parent__.__parent__.code
+        else:
+            xmldict['department'] = None
+            xmldict['faculty'] = None
+        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, 'request_webservice')
+        self.student = student
+        self.xmldict = xmldict
+        return
+
+    def update(self):
+        if not 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 InterswitchPageApplicant(KofaPage):
+    """ View which sends a POST request to the Interswitch
+    CollegePAY payment gateway.
+    """
+    grok.context(INigeriaApplicantOnlinePayment)
+    grok.require('waeup.payApplicant')
+    grok.template('applicant_goto_interswitch')
+    grok.name('goto_interswitch')
+    label = _('Submit data to CollegePAY')
+    submit_button = _('Submit')
+
+    action = None
+    site_name = None
+    currency = None
+    pay_item_id = None
+    product_id = None
+    xml_data = None
+    hashvalue = None
+    gateway_amt = GATEWAY_AMT
+
+    def init_update(self):
+        if self.context.p_state != 'unpaid':
+            return _("Payment ticket can't be re-sent to CollegePAY.")
+        if self.context.__parent__.__parent__.expired \
+            and self.context.__parent__.__parent__.strict_deadline:
+            return _("Payment ticket can't be sent to CollegePAY. "
+                     "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, 'request_webservice')
+        return
+
+    def update(self):
+        if not 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
+
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/browser_templates/applicant_goto_interswitch.pt
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/browser_templates/applicant_goto_interswitch.pt	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/browser_templates/applicant_goto_interswitch.pt	(revision 17238)
@@ -0,0 +1,80 @@
+<form i18n:domain="kofacustom.nigeria"
+  tal:attributes="action view/action" method="POST">
+
+  <p i18n:translate="">
+    These data will be submitted to the CollegePAY 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">
+          Payment Item:</td>
+        <td>
+          <span tal:replace="view/context/display_item">PAYMENTITEM</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="product_id" 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="hash" tal:condition="python: getattr(view, 'hashvalue', False)"
+                                   tal:attributes="value view/hashvalue" />
+  <input type="hidden" name="site_redirect_url" tal:attributes="value view/site_redirect_url" />
+  <input type="hidden" name="site_name" tal:attributes="value view/site_name" />
+  <input type="hidden" name="local_date_time" tal:attributes="value view/local_date_time" />
+  <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/context/display_item" />
+  <input type="hidden" name="cust_id_desc" 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 tal:condition="view/xml_data" type="hidden" name="payment_params" value="college_split" />
+  <span tal:replace="structure view/xml_data"></span>
+  <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/branches/uli-paypal/src/kofacustom/nigeria/interswitch/browser_templates/applicant_goto_webcheckout.pt
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/browser_templates/applicant_goto_webcheckout.pt	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/browser_templates/applicant_goto_webcheckout.pt	(revision 17238)
@@ -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 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" />
+  <input type="hidden" name="split_accounts" tal:condition="view/split_accounts" tal:attributes="value view/split_accounts" />
+
+  <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/branches/uli-paypal/src/kofacustom/nigeria/interswitch/browser_templates/paydirect.pt
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/browser_templates/paydirect.pt	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/browser_templates/paydirect.pt	(revision 17238)
@@ -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/branches/uli-paypal/src/kofacustom/nigeria/interswitch/browser_templates/student_goto_interswitch.pt
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/browser_templates/student_goto_interswitch.pt	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/browser_templates/student_goto_interswitch.pt	(revision 17238)
@@ -0,0 +1,81 @@
+<form i18n:domain="kofacustom.nigeria"
+  tal:attributes="action view/action" method="POST">
+
+  <p i18n:translate="">
+    These data will be submitted to the CollegePAY 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">
+          Payment Item:</td>
+        <td>
+          <span tal:replace="view/context/display_item">PAYMENTITEM</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="product_id" 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="hash" tal:condition="python: getattr(view, 'hashvalue', False)"
+                                   tal:attributes="value view/hashvalue" />
+  <input type="hidden" name="site_redirect_url" tal:attributes="value view/site_redirect_url" />
+  <input type="hidden" name="site_name" tal:attributes="value view/site_name" />
+  <input type="hidden" name="local_date_time" tal:attributes="value view/local_date_time" />
+  <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/context/display_item" />
+  <input type="hidden" name="cust_id_desc" 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 tal:condition="view/xml_data" type="hidden" name="payment_params" value="college_split" />
+  <span tal:replace="structure view/xml_data"></span>
+  <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/branches/uli-paypal/src/kofacustom/nigeria/interswitch/browser_templates/student_goto_webcheckout.pt
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/browser_templates/student_goto_webcheckout.pt	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/browser_templates/student_goto_webcheckout.pt	(revision 17238)
@@ -0,0 +1,76 @@
+<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" />
+  <input type="hidden" name="split_accounts" tal:condition="view/split_accounts" tal:attributes="value view/split_accounts" />
+
+  <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/branches/uli-paypal/src/kofacustom/nigeria/interswitch/helpers.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/helpers.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/helpers.py	(revision 17238)
@@ -0,0 +1,473 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""General helper functions for the interswitch module in custom packages.
+"""
+from datetime import datetime
+from ssl import SSLError
+import httplib
+import hashlib
+import json
+from urllib import urlencode
+import grok
+from xml.dom.minidom import parseString
+from zope.event import notify
+from waeup.kofa.payments.interfaces import IPayer
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+def SOAP_post(soap_action, xml, host, url, https):
+    """Handles making the SOAP request.
+    """
+    if https:
+        h = httplib.HTTPSConnection(host)
+    else:
+        h = httplib.HTTPConnection(host)
+    headers={
+        'Host':host,
+        'Content-Type':'text/xml; charset=utf-8',
+        'Content-Length':len(xml),
+        'SOAPAction':'"%s"' % soap_action,
+    }
+    h.request('POST', url, body=xml,headers=headers)
+    response = h.getresponse()
+    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):
+    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>
+    <getTransactionData xmlns="http://tempuri.org/">
+      <product_id>%s</product_id>
+      <trans_ref>%s</trans_ref>
+    </getTransactionData>
+  </soap:Body>
+</soap:Envelope>""" % (product_id, transref)
+    response=SOAP_post("http://tempuri.org/getTransactionData",xml, host, url, https)
+    if response.status!=200:
+        return 'Connection error (%s, %s)' % (response.status, response.reason)
+    result_xml = response.read()
+    doc=parseString(result_xml)
+    response=doc.getElementsByTagName('getTransactionDataResult')[0].firstChild.data
+    return response
+
+
+def get_JSON_response(product_id, transref, host, url, https, mac, amount):
+    hashargs = product_id + transref + mac
+    hashvalue = hashlib.sha512(hashargs).hexdigest()
+    headers={
+        'Content-Type':'text/xml; charset=utf-8',
+        'Hash':hashvalue,
+    }
+    if https:
+        h = httplib.HTTPSConnection(host)
+    else:
+        h = httplib.HTTPConnection(host)
+    amount = int(100 * amount)
+    args = {'productid': product_id,
+            '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 query_interswitch_SOAP(payment, product_id, host, url, https, verify):
+    sr = get_SOAP_response(product_id, payment.p_id, host, url, https)
+    if sr.startswith('Connection error'):
+        msg = _('Connection error')
+        log = sr
+        return False, msg, log
+    wlist = sr.split(':')
+    if len(wlist) < 7:
+        msg = _('Invalid callback: ${a}', mapping = {'a': sr})
+        log = 'invalid callback for payment %s: %s' % (payment.p_id, sr)
+        return False, msg, log
+    payment.r_code = wlist[0]
+    payment.r_desc = wlist[1]
+    payment.r_amount_approved = float(wlist[2]) / 100
+    payment.r_card_num = wlist[3]
+    payment.r_pay_reference = wlist[5]
+    payment.r_company = u'interswitch'
+    if payment.r_code != '00':
+        msg = _('Unsuccessful callback: ${a}', mapping = {'a': sr})
+        log = 'unsuccessful callback for %s payment %s: %s' % (
+            payment.p_category, payment.p_id, sr)
+        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, sr)
+        payment.p_state = 'failed'
+        notify(grok.ObjectModifiedEvent(payment))
+        return False, msg, log
+    if wlist[4] != payment.p_id:
+        msg = _('Callback transaction id does not match.')
+        log = 'wrong callback for %s payment %s: %s' % (
+            payment.p_category, payment.p_id, sr)
+        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, sr)
+    notify(grok.ObjectModifiedEvent(payment))
+    return True, msg, log
+
+def query_interswitch(payment, product_id, host, url, https, mac, verify):
+    # If no mac mac key is given, fall back to deprecated SOAP method
+    # (Uniben, AAUA, FCEOkene).
+    if mac == None:
+        return query_interswitch_SOAP(
+            payment, product_id, host, url, https, verify)
+    jr = get_JSON_response(product_id, payment.p_id, host, url,
+                           https, mac, payment.amount_auth)
+    error = jr.get('error')
+    if error:
+        msg = log = error
+        return False, msg, log
+
+    # A typical JSON response
+
+    # old:
+
+    #  {u'SplitAccounts': [],
+    #   u'MerchantReference':u'p4210665523377',
+    #   u'PaymentReference':u'GTB|WEB|KPOLY|12-01-2015|013138',
+    #   u'TransactionDate':u'2015-01-12T13:43:39.27',
+    #   u'RetrievalReferenceNumber':u'000170548791',
+    #   u'ResponseDescription': u'Approved Successful',
+    #   u'Amount': 2940000,
+    #   u'CardNumber': u'2507',
+    #   u'ResponseCode': u'00',
+    #   u'LeadBankCbnCode': None,
+    #   u'LeadBankName': None}
+
+    # new:
+
+    # 'PaymentReference' is maybe missing
+
+    #  {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
+    if verify and jr['ResponseCode'] == '20050':
+        msg = _('Integration method has changed.')
+        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
+
+# PAYDirect helper functions
+
+def create_paydirect_booking(merchant_id, payment, item_code, host, url, https):
+    p_id = payment.p_id
+    description = payment.p_category
+    amount = int(100*payment.amount_auth)  # Amount in Kobo
+    date_booked = payment.creation_date.strftime("%Y-%m-%d")
+    date_expired = "2099-12-31"
+    firstname = IPayer(payment).display_fullname.split()[0]
+    lastname = IPayer(payment).display_fullname.split()[-1]
+    id = IPayer(payment).id
+    email = IPayer(payment).email
+
+    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>
+            <Description>%s</Description>
+            <Amount>%s</Amount>
+            <DateBooked>%s</DateBooked>
+            <DateExpired>%s</DateExpired>
+            <FirstName>%s</FirstName>
+            <LastName>%s</LastName>
+            <Email>%s</Email>
+            <ItemCode>%s</ItemCode>
+          </Booking>
+        </Bookings>
+      </ReservationRequest>
+    </CreateBooking>
+  </soap:Body>
+</soap:Envelope>""" % (
+    merchant_id, merchant_id, p_id[1:],
+    description, amount,
+    date_booked, date_expired,
+    firstname, lastname,
+    email, item_code)
+    response=SOAP_post(
+        "http://interswitchng.com/bookonhold/CreateBooking",
+        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 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 = "Channel Name: %s - Terminal Id: %s - Location: %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 = float(amount) / 100
+    except AttributeError:
+        pass
+    try:
+        payment.r_pay_reference = doc.getElementsByTagName(
+            'ReferenceNumber')[0].firstChild.data
+    except AttributeError:
+        pass
+    if p_status not in ('Pending', 'Completed'):
+        msg = _('Unknown status: %s' % p_status)
+        log = 'invalid callback for payment %s: %s' % (payment.p_id, p_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 net amount.')
+        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
+
+# Web checkout helper functions
+
+def get_JSON_webcheckout_response(merchant_code, transref, host, url,
+                                  https, amount, mac=''):
+    amount = int(100 * amount)
+    hashargs = transref + merchant_code + str(amount) + mac
+    hashvalue = hashlib.sha512(hashargs).hexdigest()
+    headers={
+        'Content-Type':'text/xml; charset=utf-8',
+        'Hash':hashvalue,
+    }
+    if https:
+        h = httplib.HTTPSConnection(host)
+    else:
+        h = httplib.HTTPConnection(host)
+    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, mac):
+    jr = get_JSON_webcheckout_response(merchant_code, payment.p_id, host, url,
+                           https, payment.amount_auth, mac)
+    error = jr.get('error')
+    if error:
+        msg = log = error
+        return False, msg, log
+
+    # A typical JSON response (test payment of Hector)
+
+    #{u'SplitAccounts': [],
+    #u'RemittanceAmount': 0,
+    #u'MerchantReference': u'p6709347986663',
+    #u'PaymentReference': u'FBN|WEB|MX76823|13-12-2022|935097929|608001',
+    #u'TransactionDate': u'2022-12-13T01:34:21',
+    #u'RetrievalReferenceNumber': u'814212374638',
+    #u'ResponseDescription': u'Approved by Financial Institution',
+    #u'Amount': 10000,
+    #u'CardNumber': u'',
+    #u'ResponseCode': u'00',
+    #u'BankCode': u'011'}
+
+    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'
+    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/branches/uli-paypal/src/kofacustom/nigeria/interswitch/paydirectbrowser.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/paydirectbrowser.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/paydirectbrowser.py	(revision 17238)
@@ -0,0 +1,286 @@
+## $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 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 (
+    query_interswitch, write_payments_log,
+    fetch_booking_details, create_paydirect_booking)
+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', False)
+    except KeyError:
+        session = datetime.now().year
+        try:
+            return getattr(grok.getSite()['configuration'][str(session)],
+                'interswitch_paydirect_enabled', False)
+        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
+
+    @property
+    def item_code(self):
+        return '01' # for testing
+
+    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.")
+        self.ref_number = self.merchant_id + self.context.p_id[1:]
+        # Create a PAYDirect Booking
+        result_xml = create_paydirect_booking(
+            self.merchant_id, self.context, self.item_code, self.gateway_host,
+            self.gateway_url, True)
+        if result_xml.startswith('Connection error'):
+            IPayer(self.context).payer.writeLogMessage(self, result_xml)
+            return result_xml
+        doc=parseString(result_xml)
+        if not doc.getElementsByTagName('ResponseCode'):
+            IPayer(self.context).payer.writeLogMessage(self, 'invalid callback')
+            return _('Invalid callback from Interswitch')
+        rc = doc.getElementsByTagName('ResponseCode')[0].firstChild.data
+        IPayer(self.context).payer.writeLogMessage(self, 'response code: %s' % rc)
+        if rc not in ('100', '108'):
+            return 'Error response code from Interswitch: %s' % rc
+        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
+        # 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'
+        error = self.init_update()
+        if error:
+            self.flash(error, type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        return
+
+    @action('Requery now', style='primary')
+    def fetch(self):
+        success, msg, log = fetch_booking_details(
+            self.context, self.merchant_id, self.gateway_host,
+            self.gateway_url, self.https)
+        IPayer(self.context).payer.writeLogMessage(self, log)
+        if not success:
+            self.flash(msg, type='warning')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        write_payments_log(IPayer(self.context).id, self.context)
+        flashtype, msg, log = IPayer(self.context).doAfterPayment()
+        if log is not None:
+            IPayer(self.context).payer.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
+        # 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'
+        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.")
+        return
+
+class StudentPaymentRefNumberSlipActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(INigeriaStudentOnlinePayment)
+    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 ApplicantPaymentRefNumberSlipActionButton(StudentPaymentRefNumberSlipActionButton):
+    grok.context(INigeriaApplicantOnlinePayment)
+    grok.view(PAYDirectPageApplicant)
+    grok.require('waeup.viewApplication')
+
+class StudentRefNumberSlip(UtilityView, grok.View):
+    """Deliver a PDF slip of the context.
+    """
+    grok.context(INigeriaStudentOnlinePayment)
+    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)
+
+class ApplicantRefNumberSlip(StudentRefNumberSlip):
+    """Deliver a PDF slip of the context.
+    """
+    grok.context(INigeriaApplicantOnlinePayment)
+    grok.require('waeup.viewApplication')
+
+    @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
+        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
+            self.request)
+        students_utils = getUtility(IStudentsUtils)
+        return students_utils.renderPDF(self, 'refnumberslip.pdf',
+            self.context.__parent__, applicantview, note=self.note,
+            omit_fields=self.omit_fields)
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/tests.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/tests.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/tests.py	(revision 17238)
@@ -0,0 +1,460 @@
+## $Id$
+##
+## Copyright (C) 2011 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 os
+import random
+from datetime import datetime, timedelta, date
+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
+from waeup.kofa.applicants.tests.test_browser import ApplicantsFullSetup
+from waeup.kofa.configuration import SessionConfiguration
+from waeup.kofa.students.payments import StudentOnlinePayment
+from waeup.kofa.browser.tests.test_pdf import samples_dir
+from kofacustom.nigeria.interswitch.helpers import (
+    query_interswitch, get_SOAP_response_paydirect, SOAP_post,
+    create_paydirect_booking,
+    confirm_transaction
+    )
+from kofacustom.nigeria.testing import FunctionalLayer
+
+# Also run tests that send requests to external servers?
+#   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_1 = False
+EXTERNAL_TESTS_2 = False
+EXTERNAL_TESTS_3 = 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'):
+            myself = myself[:-1]
+        print "WARNING: external tests are skipped!"
+        print "WARNING: edit %s to enable them." % myself
+        return
+    return func
+
+def external_test_3(func):
+    if not EXTERNAL_TESTS_3:
+        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
+
+class InterswitchTestsStudents(StudentsFullSetup):
+    """Tests for the Interswitch payment gateway.
+    """
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(InterswitchTestsStudents, self).setUp()
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.payments_path)
+        IWorkflowState(self.student).setState('cleared')
+        self.student.nationality = u'NG'
+        self.browser.open(self.payments_path + '/addop')
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+        self.browser.open(self.payments_path)
+        ctrl = self.browser.getControl(name='val_id')
+        self.value = ctrl.options[0]
+        self.browser.getLink(self.value).click()
+        self.assertMatches('...Amount Authorized...',
+                           self.browser.contents)
+        self.assertTrue('<span>40000.0</span>', self.browser.contents)
+        self.payment_url = self.browser.url
+        self.payment = self.student['payments'][self.value]
+
+    def test_interswitch_form(self):
+        # Manager can access InterswitchForm
+        self.browser.getLink("Pay via Interswitch CollegePAY", index=0).click()
+        self.assertMatches('...<input type="hidden" name="pay_item_id" />...',
+                           self.browser.contents)
+        self.assertMatches('...Total Amount Authorized:...',
+                           self.browser.contents)
+        self.assertEqual(self.student.current_mode, 'ug_ft')
+        self.assertMatches(
+            '...<input type="hidden" name="amount" value="4000000" />...',
+            self.browser.contents)
+
+        # 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 CollegePAY", index=0).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)
+        self.assertMatches('...<input type="hidden" name="pay_item_id" />...',
+                           self.browser.contents)
+        self.assertTrue(
+            '<input type="hidden" name="amount" value="2030000" />' in
+            self.browser.contents)
+
+    def test_interswitch_form_ticket_expired(self):
+        # Manager can access InterswitchForm
+        self.browser.getLink("Pay via Interswitch CollegePAY", index=0).click()
+        self.assertMatches('...<input type="hidden" name="pay_item_id" />...',
+                           self.browser.contents)
+        self.assertMatches('...Total Amount Authorized:...',
+                           self.browser.contents)
+        self.assertEqual(self.student.current_mode, 'ug_ft')
+        self.assertTrue(
+            '<input type="hidden" name="amount" value="4030000" />' in
+            self.browser.contents)
+        delta = timedelta(days=8)
+        self.payment.creation_date -= delta
+        self.browser.open(self.payment_url)
+        self.browser.getLink("Pay via Interswitch CollegePAY", index=0).click()
+        self.assertMatches(
+            '...This payment ticket is too old. Please create a new ticket...',
+            self.browser.contents)
+        delta = timedelta(days=2)
+        self.payment.creation_date += delta
+        self.browser.open(self.payment_url)
+        self.browser.getLink("Pay via Interswitch CollegePAY").click()
+        self.assertMatches('...Total Amount Authorized:...',
+                           self.browser.contents)
+
+    def test_interswitch_form_ticket_expired_tz(self):
+        # The form copes with timezones when calculating expirements.
+        # We should not have TZ data in timestamps processed, but it looks
+        # like we get some with imports :-/
+        #self.browser.getLink("Pay via Interswitch CollegePAY", index=0).click()
+        delta = timedelta(days=9) # In European summertime, 8 days is not enough
+        self.payment.creation_date -= delta
+        from pytz import timezone
+        self.payment.creation_date = timezone("Europe/Berlin").localize(
+            self.payment.creation_date)
+        self.browser.open(self.payment_url)
+        self.browser.getLink("Pay via Interswitch CollegePAY", index=0).click()
+        self.assertMatches(
+            '...This payment ticket is too old. Please create a new ticket...',
+            self.browser.contents)
+        delta = timedelta(days=2)
+        self.payment.creation_date += delta
+        self.browser.open(self.payment_url)
+        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'
+        url = '/paydirect/services/TransactionQueryWs.asmx'
+        https = True
+        mac = None
+        product_id = '5845' # AAUE regular
+        payment = StudentOnlinePayment()
+        payment.p_id ='p4465649308559'
+        payment.amount_auth = 60250.0
+        success, msg, log = query_interswitch(
+            payment, product_id, host, url, https, mac, False)
+        self.assertEqual('Successful callback received.', msg)
+        self.assertTrue(success)
+        self.assertTrue(
+            '00:Approved Successful:6025000:3154:p4465649308559:'
+            'ZIB|WEB|ABAL|3-11-2015|021336:000457580882' in log)
+
+    @external_test_1
+    def test_query_interswitch_JSON(self):
+        host = 'webpay.interswitchng.com'
+        url = '/paydirect/api/v1/gettransaction.json'
+        mac = '9718FA00B0F5070B388A9896ADCED9B2FB02D30F71E12E68BDADC63F6852A3496FF97D8A0F9DA9F753B911A49BB09BB87B55FD02046BD325C74C46C0123CF023'
+        https = True
+        product_id = '5845' # AAUE regular
+        payment = StudentOnlinePayment()
+        payment.p_id ='p4465649308559'
+        payment.amount_auth = 1.0
+        success, msg, log = query_interswitch(
+            payment, product_id, host, url, https, mac, False)
+        self.assertFalse(success)
+        self.assertTrue('Unsuccessful callback:' in msg)
+        self.assertTrue('Amount Inconsistency' in log)
+        payment.amount_auth = 60250.0
+        success, msg, log = query_interswitch(
+            payment, product_id, host, url, https, mac, False)
+        self.assertEqual('Successful callback received', msg)
+        self.assertTrue(success)
+        self.assertTrue(
+            "{u'SplitAccounts': [], "
+            "u'MerchantReference': u'p4465649308559', "
+            "u'PaymentReference': u'ZIB|WEB|ABAL|3-11-2015|021336', "
+            "u'TransactionDate': u'2015-11-03T16:40:54.487', "
+            "u'RetrievalReferenceNumber': u'000457580882', "
+            "u'ResponseDescription': u'Approved Successful', "
+            "u'Amount': 6025000, "
+            "u'CardNumber': u'3154', "
+            "u'ResponseCode': u'00', "
+            "u'LeadBankCbnCode': None, "
+            "u'LeadBankName': None}" in log)
+
+# PAYDirect tests
+
+    @external_test_2
+    def test_SOAP_paydirect(self):
+        payment = createObject('waeup.StudentOnlinePayment')
+        payment.p_category = u'schoolfee'
+        payment.p_session = self.student.current_session
+        payment.p_item = u'My Certificate'
+        payment.p_id = 'p' + str(random.randint(10000000000, 90000000000))
+        payment.amount_auth = 1000.0
+        item_code = '01'
+        self.student['payments'][payment.p_id] = payment
+        result_xml = create_paydirect_booking(
+            MERCHANT_ID, payment, item_code, PAYDIRECT_HOST, PAYDIRECT_URL, True)
+        result_xml = get_SOAP_response_paydirect(
+            MERCHANT_ID, payment.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_pending(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)
+        ref_number = '%s%s' % (MERCHANT_ID,value[1:])
+        self.assertTrue(ref_number in self.browser.contents)
+        self.assertEqual(self.student['payments'][value].r_pay_reference, None)
+        self.browser.getControl("Requery").click()
+        self.assertTrue('pending' in self.browser.contents)
+        # Reference number has been saved
+        self.assertEqual(self.student['payments'][value].r_pay_reference, ref_number)
+        # 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
+
+    @external_test_2
+    def test_paydirect_completed(self):
+        payment = createObject('waeup.StudentOnlinePayment')
+        payment.p_category = u'schoolfee'
+        payment.p_session = self.student.current_session
+        payment.p_item = u'My Certificate'
+        # p6214352306153 has been manually set completed by Interswitch
+        payment.p_id = u'p6214352306153'
+        payment.amount_auth = 10.0 # 300 will be added
+        self.student['payments'][payment.p_id] = payment
+        result_xml = get_SOAP_response_paydirect(
+            MERCHANT_ID, payment.p_id, PAYDIRECT_HOST, PAYDIRECT_URL, True)
+        doc=parseString(result_xml)
+        status=doc.getElementsByTagName('PaymentStatus')[0].firstChild.data
+        refnumber=doc.getElementsByTagName('ReferenceNumber')[0].firstChild.data
+        amount=float(doc.getElementsByTagName('Amount')[0].firstChild.data)
+        self.assertEqual(status, 'Completed')
+        self.assertEqual(amount, 1000)
+        self.assertEqual(refnumber, '60336214352306153')
+        # Now we login and check what will happen with the student after payment
+        self.browser.open(self.payments_path)
+        self.browser.getLink(payment.p_id).click()
+        self.browser.getLink("Pay via Interswitch PAYDirect").click()
+        # 300 has been added
+        self.assertEqual(payment.amount_auth, 310)
+        self.assertEqual(payment.net_amt, 10)
+
+        # Unfortunately the payment 60336214352306153 was done without surcharge.
+        # Therefore we have to deduct 300 again in this test.
+        payment.amount_auth = 10.0
+
+        # Reference number has been saved 
+        self.browser.getControl("Requery").click()
+        self.assertEqual(
+            self.student['payments'][payment.p_id].r_pay_reference, '60336214352306153')
+        # Payment is made
+        self.assertEqual(
+            self.student['payments'][payment.p_id].p_state, 'paid')
+        # Rubbish has been stored too
+        self.assertTrue(
+            self.student['payments'][payment.p_id].r_desc.startswith('Channel Name: Bank Branc'))
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - kofacustom.nigeria.interswitch.paydirectbrowser.PAYDirectPageStudent '
+            '- K1000000 - valid callback for schoolfee payment %s: Completed' % payment.p_id in logcontent)
+        self.assertTrue(
+            'zope.mgr - K1000000 - First school fee payment made' in logcontent)
+        self.assertTrue(
+            'zope.mgr - kofacustom.nigeria.interswitch.paydirectbrowser.PAYDirectPageStudent '
+            '- K1000000 - successful schoolfee payment: %s' % payment.p_id in logcontent)
+
+class InterswitchTestsApplicants(ApplicantsFullSetup):
+    """Tests for the Interswitch payment gateway.
+    """
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(InterswitchTestsApplicants, self).setUp()
+        configuration = SessionConfiguration()
+        configuration.academic_session = datetime.now().year - 2
+        self.app['configuration'].addSessionConfiguration(configuration)
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_path)
+        #IWorkflowState(self.student).setState('started')
+        super(InterswitchTestsApplicants, self).fill_correct_values()
+        self.applicantscontainer.application_fee = 1000.0
+        self.browser.getControl(name="form.nationality").value = ['NG']
+        self.browser.getControl(name="transition").value = ['start']
+        self.browser.getControl("Save").click()
+        self.browser.getControl("Add online").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+        self.payment = self.applicant.values()[0]
+        self.payment_url = self.browser.url
+
+    def test_interswitch_form(self):
+        self.assertMatches('...Amount Authorized...',
+                           self.browser.contents)
+        self.assertMatches(
+            '...<span>1000.0</span>...',
+            self.browser.contents)
+        # Manager can access InterswitchForm
+        self.browser.getLink("Pay via Interswitch CollegePAY", index=0).click()
+        self.assertMatches('...Total Amount Authorized:...',
+                           self.browser.contents)
+        self.assertMatches(
+            '...<input type="hidden" name="amount" value="100000" />...',
+            self.browser.contents)
+        delta = timedelta(days=8)
+        self.payment.creation_date -= delta
+        self.browser.open(self.payment_url)
+        self.browser.getLink("Pay via Interswitch", index=0).click()
+        self.assertMatches(
+            '...This payment ticket is too old. Please create a new ticket...',
+            self.browser.contents)
+        delta = timedelta(days=2)
+        self.payment.creation_date += delta
+        self.browser.getLink("Pay via Interswitch", index=0).click()
+        self.assertMatches('...Total Amount Authorized:...',
+                           self.browser.contents)
+
+# WebCheckout tests
+
+
+    # https://webpay.interswitchng.com/collections/api/v1/gettransaction.json?merchantcode=MX76823&transactionreference=p6709347986663&amount=100
+
+    mac = "uS6U18pC6GFKCpJoZH6J6jlOmR81FSrHkjBRpMaaydNQtywuG0hdB02J56MCqLV8rmzeAkhiaQR2nNcX3EPJePl5ppidImeKSzdunhddQh61UGpZPiS2CxMdAem8ueA1"
+
+    @external_test_3
+    def test_confirm_transaction(self):
+        host = 'webpay.interswitchng.com'
+        url = '/collections/api/v1/gettransaction.json'
+        https = True
+        merchant_code = 'MX76823'
+        payment = StudentOnlinePayment()
+        payment.p_id ='p4465649308559'
+        payment.amount_auth = 100000.0
+        success, msg, log = confirm_transaction(
+            payment, merchant_code, host, url, https, self.mac)
+        self.assertFalse(success)
+        self.assertTrue('Unsuccessful callback:' in msg)
+        self.assertTrue('Transaction not Found' in log)
+
+        payment.p_id ='p6709347986663'
+        payment.amount_auth = 1.0
+        success, msg, log = confirm_transaction(
+            payment, merchant_code, host, url, https, self.mac)
+        self.assertTrue('Amount Inconsistency' in log)
+        payment.amount_auth = 100.0
+        success, msg, log = confirm_transaction(
+            payment, merchant_code, host, url, https, self.mac)
+        self.assertEqual('Successful callback received', msg)
+        self.assertTrue(success)
+        self.assertTrue(
+            "{u'SplitAccounts': [], "
+            "u'RemittanceAmount': 0, "
+            "u'MerchantReference': u'p6709347986663', "
+            "u'PaymentReference': u'FBN|WEB|MX76823|13-12-2022|935097929|608001', "
+            "u'TransactionDate': u'2022-12-13T01:34:21', "
+            "u'RetrievalReferenceNumber': u'814212374638', "
+            "u'ResponseDescription': u'Approved by Financial Institution', "
+            "u'Amount': 10000, "
+            "u'CardNumber': u'', "
+            "u'ResponseCode': u'00', "
+            "u'BankCode': u'011'}" in log)
+
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/webcheckoutbrowser.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/webcheckoutbrowser.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/interswitch/webcheckoutbrowser.py	(revision 17238)
@@ -0,0 +1,344 @@
+## $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 = None
+    currency = None
+    pay_item_id = None
+    merchant_code = None
+    gateway_amt = GATEWAY_AMT
+    split_accounts = None
+
+    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 WebCheckoutConfirmTransactionApplicant(UtilityView, grok.View):
+    """ Request webservice view for the WebCheckout gateway
+    """
+    grok.context(INigeriaApplicantOnlinePayment)
+    grok.name('confirm_transaction')
+    grok.require('waeup.payApplicant')
+
+    merchant_code = None
+    gateway_host = None
+    gateway_url = None
+    https = True
+    split_accounts = None
+    mac = ''
+
+
+    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,
+            self.mac)
+        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 WebCheckoutConfirmTransactionStudent(UtilityView, grok.View):
+    """ Request webservice view for the WebCheckout gateway
+    """
+    grok.context(INigeriaStudentOnlinePayment)
+    grok.name('confirm_transaction')
+    grok.require('waeup.payStudent')
+
+    merchant_code = None
+    gateway_host = None
+    gateway_url = None
+    https = True
+    mac = ''
+
+    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,
+            self.mac)
+        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
+
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/locales.zcml
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/locales.zcml	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/locales.zcml	(revision 17238)
@@ -0,0 +1,8 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           xmlns:i18n="http://namespaces.zope.org/i18n">
+
+  <include package="zope.i18n" file="meta.zcml" />
+  <i18n:registerTranslations directory="locales" />
+  <include package="waeup.kofa" file="locales.zcml" />
+
+</configure>
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/locales/kofacustom.nigeria.pot
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/locales/kofacustom.nigeria.pot	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/locales/kofacustom.nigeria.pot	(revision 17238)
@@ -0,0 +1,1134 @@
+##############################################################################
+#
+# Copyright (c) 2003-2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+msgid ""
+msgstr ""
+"Project-Id-Version: Development/Unknown\n"
+"POT-Creation-Date: Mon Sep  4 19:01:53 2017\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Zope 3 Developers <zope3-dev@zope.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: zope/app/locales/extract.py\n"
+
+#: kofacustom/nigeria/applicants/browser.py:60
+msgid "Download result slip"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/browser.py:61
+msgid "Download application slip"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:247
+msgid "Bank Name"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:253
+msgid "Bank Account Name"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:259
+msgid "Bank Account Number"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:274
+#: kofacustom/nigeria/applicants/interfaces.py:417
+#: kofacustom/nigeria/applicants/interfaces.py:772
+#: kofacustom/nigeria/students/interfaces.py:108
+msgid "Nationality"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:279
+#: kofacustom/nigeria/applicants/interfaces.py:422
+msgid "State/LGA (Nigerians only)"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:287
+#: kofacustom/nigeria/applicants/interfaces.py:430
+msgid "1st Choice Course of Study"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:292
+#: kofacustom/nigeria/applicants/interfaces.py:435
+msgid "2nd Choice Course of Study"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:298
+msgid "Programme Type"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:304
+#: kofacustom/nigeria/applicants/interfaces.py:440
+#: kofacustom/nigeria/students/interfaces.py:218
+#: kofacustom/nigeria/students/interfaces.py:275
+msgid "Qualification Obtained"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:310
+#: kofacustom/nigeria/applicants/interfaces.py:451
+#: kofacustom/nigeria/students/interfaces.py:231
+#: kofacustom/nigeria/students/interfaces.py:282
+msgid "Former Matric Number"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:315
+#: kofacustom/nigeria/applicants/interfaces.py:456
+#: kofacustom/nigeria/students/interfaces.py:237
+#: kofacustom/nigeria/students/interfaces.py:288
+msgid "Class of Degree"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:321
+#: kofacustom/nigeria/applicants/interfaces.py:462
+#: kofacustom/nigeria/students/interfaces.py:244
+#: kofacustom/nigeria/students/interfaces.py:295
+msgid "Institution Attended"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:326
+#: kofacustom/nigeria/applicants/interfaces.py:467
+#: kofacustom/nigeria/students/interfaces.py:250
+#: kofacustom/nigeria/students/interfaces.py:301
+msgid "Years Attended"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:331
+#: kofacustom/nigeria/applicants/interfaces.py:472
+#: kofacustom/nigeria/students/interfaces.py:256
+#: kofacustom/nigeria/students/interfaces.py:307
+msgid "Discipline"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:336
+msgid "Subjects and Scores"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:340
+#: kofacustom/nigeria/applicants/interfaces.py:700
+msgid "JAMB Subjects"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:349
+msgid "Total JAMB Score"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:357
+msgid "JAMB Registration Number"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:361
+#: kofacustom/nigeria/applicants/interfaces.py:611
+msgid "Notice"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:365
+#: kofacustom/nigeria/applicants/interfaces.py:616
+msgid "Screening Venue"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:369
+#: kofacustom/nigeria/applicants/interfaces.py:620
+msgid "Screening Date"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:373
+#: kofacustom/nigeria/applicants/interfaces.py:624
+msgid "Screening Score (%)"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:377
+msgid "Aggregate Score (%)"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:378
+msgid "(average of relative JAMB and PUTME scores)"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:382
+msgid "Result uploaded"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:387
+#: kofacustom/nigeria/applicants/interfaces.py:628
+msgid "Student Id"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:392
+#: kofacustom/nigeria/applicants/interfaces.py:633
+msgid "Admitted Course of Study"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:397
+#: kofacustom/nigeria/applicants/interfaces.py:639
+msgid "Form locked"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:44
+msgid "Post UTME"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:446
+#: kofacustom/nigeria/applicants/interfaces.py:477
+#: kofacustom/nigeria/applicants/interfaces.py:506
+#: kofacustom/nigeria/students/interfaces.py:126
+#: kofacustom/nigeria/students/interfaces.py:159
+#: kofacustom/nigeria/students/interfaces.py:192
+#: kofacustom/nigeria/students/interfaces.py:225
+msgid "Full Name"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:45
+msgid "Post DE"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:46
+msgid "Admission Screening Exercise"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:47
+msgid "not applicable"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:482
+#: kofacustom/nigeria/applicants/interfaces.py:511
+#: kofacustom/nigeria/students/interfaces.py:131
+#: kofacustom/nigeria/students/interfaces.py:164
+#: kofacustom/nigeria/students/interfaces.py:197
+msgid "Exam Number"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:487
+#: kofacustom/nigeria/applicants/interfaces.py:516
+#: kofacustom/nigeria/students/interfaces.py:137
+#: kofacustom/nigeria/students/interfaces.py:170
+#: kofacustom/nigeria/students/interfaces.py:203
+msgid "Exam Date"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:493
+#: kofacustom/nigeria/applicants/interfaces.py:522
+#: kofacustom/nigeria/students/interfaces.py:144
+#: kofacustom/nigeria/students/interfaces.py:177
+msgid "Exam Type"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:499
+#: kofacustom/nigeria/applicants/interfaces.py:528
+#: kofacustom/nigeria/students/interfaces.py:151
+#: kofacustom/nigeria/students/interfaces.py:184
+#: kofacustom/nigeria/students/interfaces.py:210
+msgid "Exam Results"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:51
+msgid "Use of English"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:52
+msgid "Agricultural Science"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:53
+msgid "Arabic"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:54
+msgid "Biology"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:542
+msgid "If yes, name of institution"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:547
+#: kofacustom/nigeria/students/interfaces.py:313
+msgid "Nysc Year"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:55
+msgid "Book Keeping"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:553
+#: kofacustom/nigeria/students/interfaces.py:319
+msgid "Nysc Location"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:557
+#: kofacustom/nigeria/students/interfaces.py:330
+msgid "Employer"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:56
+msgid "Chemistry"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:562
+#: kofacustom/nigeria/students/interfaces.py:336
+msgid "Employer Position"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:567
+#: kofacustom/nigeria/applicants/interfaces.py:594
+#: kofacustom/nigeria/students/interfaces.py:342
+#: kofacustom/nigeria/students/interfaces.py:374
+msgid "Start Date"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:57
+msgid "Christian Religious Studies"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:573
+#: kofacustom/nigeria/applicants/interfaces.py:600
+#: kofacustom/nigeria/students/interfaces.py:349
+#: kofacustom/nigeria/students/interfaces.py:380
+msgid "End Date"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:579
+#: kofacustom/nigeria/applicants/interfaces.py:606
+#: kofacustom/nigeria/students/interfaces.py:356
+#: kofacustom/nigeria/students/interfaces.py:387
+msgid "Reason for Leaving"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:58
+msgid "Commerce"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:584
+#: kofacustom/nigeria/students/interfaces.py:362
+msgid "2nd Employer"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:589
+#: kofacustom/nigeria/students/interfaces.py:368
+msgid "2nd Employer Position"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:59
+msgid "Economics"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:60
+msgid "Financial Accounting"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:61
+msgid "Fine Art"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:62
+msgid "Food and Nutrition"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:63
+msgid "French"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:64
+msgid "Geography"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:65
+msgid "German"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:66
+msgid "Government"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:67
+msgid "Hausa"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:68
+msgid "Home Economics"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:69
+msgid "History"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:690
+#: kofacustom/nigeria/applicants/interfaces.py:730
+#: kofacustom/nigeria/applicants/interfaces.py:761
+msgid "Email Address"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:695
+#: kofacustom/nigeria/applicants/interfaces.py:735
+#: kofacustom/nigeria/applicants/interfaces.py:766
+#: kofacustom/nigeria/students/interfaces.py:101
+msgid "Date of Birth"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:70
+msgid "Igbo"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:701
+msgid "Select four subjects."
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:71
+msgid "Literature in English"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:72
+msgid "Literature in Nigerian Languages"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:73
+msgid "Mathematics"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:74
+msgid "Music"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:75
+msgid "Physics"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/interfaces.py:76
+msgid "Yoruba"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/utils.py:49
+msgid "Base Data"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/utils.py:50
+msgid "Programmes/Courses Desired"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/utils.py:51
+#: kofacustom/nigeria/students/utils.py:31
+msgid "Higher Education Record"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/utils.py:52
+msgid "Course or Programme Presently Attending"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/utils.py:53
+#: kofacustom/nigeria/students/utils.py:33
+msgid "NYSC Information"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/utils.py:54
+#: kofacustom/nigeria/students/utils.py:34
+msgid "Employment History"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/utils.py:55
+#: kofacustom/nigeria/applicants/utils.py:56
+msgid "JAMB Data"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/utils.py:57
+msgid "Application Process Information"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/utils.py:58
+msgid "Post Primary School Qualification"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/utils.py:59
+msgid "Presently attending a course or programme"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/utils.py:60
+#: kofacustom/nigeria/students/utils.py:28
+msgid "First Sitting Record"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/utils.py:61
+#: kofacustom/nigeria/students/utils.py:29
+msgid "Second Sitting Record"
+msgstr ""
+
+#: kofacustom/nigeria/applicants/utils.py:62
+msgid "Referees"
+msgstr ""
+
+#: kofacustom/nigeria/browser/pages.py:70
+msgid "Sources & Vocabularies"
+msgstr ""
+
+#: kofacustom/nigeria/browser/viewlets.py:33
+msgid "LGAs"
+msgstr ""
+
+#: kofacustom/nigeria/browser/viewlets.py:43
+msgid "Higher Qualification"
+msgstr ""
+
+#: kofacustom/nigeria/browser/viewlets.py:53
+msgid "Higher Qualification Grades"
+msgstr ""
+
+#: kofacustom/nigeria/browser/viewlets.py:63
+msgid "Higher Qualification Examination Types"
+msgstr ""
+
+#: kofacustom/nigeria/interfaces.py:100
+msgid "Remita integration enabled"
+msgstr ""
+
+#: kofacustom/nigeria/interfaces.py:105
+msgid "Interswitch integration enabled"
+msgstr ""
+
+#: kofacustom/nigeria/interfaces.py:114
+msgid "Academic Session"
+msgstr ""
+
+#: kofacustom/nigeria/interfaces.py:44
+#: kofacustom/nigeria/interfaces.py:60
+msgid "Awaiting Results"
+msgstr ""
+
+#: kofacustom/nigeria/interfaces.py:93
+#. Default: ""
+msgid "Invalid key: ${a}"
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/browser.py:110
+msgid "Verify Payment"
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/browser.py:155
+#: kofacustom/nigeria/interswitch/browser.py:194
+#: kofacustom/nigeria/remita/applicantsbrowser.py:112
+#: kofacustom/nigeria/remita/studentsbrowser.py:120
+msgid "This ticket has already been paid."
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/browser.py:234
+#: kofacustom/nigeria/interswitch/browser.py:270
+#: kofacustom/nigeria/remita/applicantsbrowser.py:165
+#: kofacustom/nigeria/remita/studentsbrowser.py:173
+msgid "This ticket has not been paid."
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/browser.py:298
+#: kofacustom/nigeria/interswitch/browser.py:362
+msgid "Submit data to CollegePAY (Interswitch Payment Gateway)"
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/browser.py:299
+#: kofacustom/nigeria/interswitch/browser.py:363
+msgid "Submit"
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/browser.py:311
+#: kofacustom/nigeria/interswitch/browser.py:375
+msgid "Payment ticket can't be re-sent to CollegePAY."
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/browser.py:318
+#: kofacustom/nigeria/interswitch/browser.py:383
+#: kofacustom/nigeria/remita/applicantsbrowser.py:243
+#: kofacustom/nigeria/remita/studentsbrowser.py:251
+msgid "This payment ticket is too old. Please create a new ticket."
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/browser.py:322
+#: kofacustom/nigeria/remita/studentsbrowser.py:254
+msgid "Study course data are incomplete."
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/browser.py:327
+#: kofacustom/nigeria/remita/studentsbrowser.py:259
+msgid "This type of payment has already been made."
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/browser.py:378
+msgid "Payment ticket can't be send to CollegePAY. Application period has expired."
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/browser.py:51
+msgid "CollegePAY"
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/browser.py:80
+msgid "Requery CollegePAY"
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/browser_templates/applicant_goto_interswitch.pt:4
+#: kofacustom/nigeria/interswitch/browser_templates/student_goto_interswitch.pt:4
+msgid "These data will be submitted to the CollegePAY payment gateway, please check:"
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/helpers.py:103
+#: kofacustom/nigeria/interswitch/helpers.py:169
+#. Default: ""
+msgid "Invalid callback: ${a}"
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/helpers.py:113
+#: kofacustom/nigeria/interswitch/helpers.py:183
+#. Default: ""
+msgid "Unsuccessful callback: ${a}"
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/helpers.py:120
+#: kofacustom/nigeria/interswitch/helpers.py:190
+msgid "Callback amount does not match."
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/helpers.py:127
+#: kofacustom/nigeria/interswitch/helpers.py:197
+msgid "Callback transaction id does not match."
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/helpers.py:136
+msgid "Successful callback received."
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/helpers.py:173
+msgid "Integration method has changed."
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/helpers.py:206
+msgid "Successful callback received"
+msgstr ""
+
+#: kofacustom/nigeria/interswitch/helpers.py:98
+msgid "Connection error"
+msgstr ""
+
+#: kofacustom/nigeria/payments/interfaces.py:103
+msgid "Third Party Amount"
+msgstr ""
+
+#: kofacustom/nigeria/payments/interfaces.py:30
+msgid "Activation Code"
+msgstr ""
+
+#: kofacustom/nigeria/payments/interfaces.py:37
+msgid "Response Amount Approved"
+msgstr ""
+
+#: kofacustom/nigeria/payments/interfaces.py:44
+msgid "Response Code"
+msgstr ""
+
+#: kofacustom/nigeria/payments/interfaces.py:51
+msgid "Response Description"
+msgstr ""
+
+#: kofacustom/nigeria/payments/interfaces.py:60
+msgid "Response Payment Reference"
+msgstr ""
+
+#: kofacustom/nigeria/payments/interfaces.py:67
+msgid "Response Card Number"
+msgstr ""
+
+#: kofacustom/nigeria/payments/interfaces.py:74
+msgid "Payment Gateway"
+msgstr ""
+
+#: kofacustom/nigeria/payments/interfaces.py:79
+msgid "Interswitch"
+msgstr ""
+
+#: kofacustom/nigeria/payments/interfaces.py:80
+msgid "eTranzact"
+msgstr ""
+
+#: kofacustom/nigeria/payments/interfaces.py:81
+msgid "Remita"
+msgstr ""
+
+#: kofacustom/nigeria/payments/interfaces.py:82
+msgid "Scratch Card"
+msgstr ""
+
+#: kofacustom/nigeria/payments/interfaces.py:83
+msgid "Manifest"
+msgstr ""
+
+#: kofacustom/nigeria/payments/interfaces.py:84
+msgid "Sponsor"
+msgstr ""
+
+#: kofacustom/nigeria/payments/interfaces.py:89
+msgid "BT Amount"
+msgstr ""
+
+#: kofacustom/nigeria/payments/interfaces.py:96
+msgid "Gateway Amount"
+msgstr ""
+
+#: kofacustom/nigeria/remita/applicantsbrowser.py:117
+#: kofacustom/nigeria/remita/applicantsbrowser.py:170
+#: kofacustom/nigeria/remita/studentsbrowser.py:125
+#: kofacustom/nigeria/remita/studentsbrowser.py:178
+msgid "Remita Retrieval Reference not found."
+msgstr ""
+
+#: kofacustom/nigeria/remita/applicantsbrowser.py:208
+#: kofacustom/nigeria/remita/studentsbrowser.py:216
+msgid "Pay now"
+msgstr ""
+
+#: kofacustom/nigeria/remita/applicantsbrowser.py:236
+#: kofacustom/nigeria/remita/studentsbrowser.py:244
+msgid "Payment ticket can't be re-sent to Remita."
+msgstr ""
+
+#: kofacustom/nigeria/remita/applicantsbrowser.py:48
+#: kofacustom/nigeria/remita/applicantsbrowser.py:207
+#: kofacustom/nigeria/remita/studentsbrowser.py:55
+#: kofacustom/nigeria/remita/studentsbrowser.py:215
+msgid "Pay via Remita"
+msgstr ""
+
+#: kofacustom/nigeria/remita/applicantsbrowser.py:65
+#: kofacustom/nigeria/remita/studentsbrowser.py:72
+msgid "Requery Remita Payment Status"
+msgstr ""
+
+#: kofacustom/nigeria/remita/applicantsbrowser.py:82
+#: kofacustom/nigeria/remita/studentsbrowser.py:90
+msgid "Verify Remita Payment Status"
+msgstr ""
+
+#: kofacustom/nigeria/remita/browser_templates/goto_remita.pt:4
+msgid "Your payment details were sent to Remita and a retrieval reference number (RRR) has been generated. Please click the button below to make your RRR payment."
+msgstr ""
+
+#: kofacustom/nigeria/remita/helpers.py:118
+#. Default: ""
+msgid "Unsuccessful response: ${a}"
+msgstr ""
+
+#: kofacustom/nigeria/remita/helpers.py:126
+msgid "Response amount does not match."
+msgstr ""
+
+#: kofacustom/nigeria/remita/helpers.py:134
+msgid "Response order id does not match."
+msgstr ""
+
+#: kofacustom/nigeria/remita/helpers.py:143
+msgid "Successful response received"
+msgstr ""
+
+#: kofacustom/nigeria/students/browser.py:276
+msgid "This student has already been financially cleared."
+msgstr ""
+
+#: kofacustom/nigeria/students/browser.py:292
+msgid "Student has been financially cleared."
+msgstr ""
+
+#: kofacustom/nigeria/students/browser.py:308
+msgid "This student has not yet been financially cleared."
+msgstr ""
+
+#: kofacustom/nigeria/students/browser.py:317
+msgid "Financial clearance withdrawn."
+msgstr ""
+
+#: kofacustom/nigeria/students/browser.py:349
+msgid "Fee Payment History for"
+msgstr ""
+
+#: kofacustom/nigeria/students/browser.py:356
+msgid "Date, Checking Officer Signature"
+msgstr ""
+
+#: kofacustom/nigeria/students/browser.py:357
+msgid "Date, Approving Officer Signature"
+msgstr ""
+
+#: kofacustom/nigeria/students/browser.py:378
+msgid "Successful Payments"
+msgstr ""
+
+#: kofacustom/nigeria/students/browser.py:384
+msgid "Payment Id"
+msgstr ""
+
+#: kofacustom/nigeria/students/browser.py:386
+msgid "Payment Date"
+msgstr ""
+
+#: kofacustom/nigeria/students/browser.py:387
+msgid "Payment Category"
+msgstr ""
+
+#: kofacustom/nigeria/students/browser.py:388
+msgid "Payment Item"
+msgstr ""
+
+#: kofacustom/nigeria/students/browser.py:389
+msgid "Amount (Naira)"
+msgstr ""
+
+#: kofacustom/nigeria/students/browser.py:390
+msgid "Payment Session"
+msgstr ""
+
+#: kofacustom/nigeria/students/export.py:110
+msgid "Clearance Invitation Data"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:114
+msgid "State / LGA"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:119
+msgid "Deferment of Admission"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:264
+#: kofacustom/nigeria/students/interfaces.py:266
+msgid "Nationalty and LGA are contradictory."
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:325
+msgid "Nysc LGA"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:393
+msgid "If yes, matric number"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:406
+#: kofacustom/nigeria/students/interfaces.py:476
+msgid "Next of Kin Name"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:412
+#: kofacustom/nigeria/students/interfaces.py:482
+msgid "Next of Kin Relationship"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:418
+#: kofacustom/nigeria/students/interfaces.py:488
+msgid "Next of Kin Address"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:424
+#: kofacustom/nigeria/students/interfaces.py:494
+msgid "Next of Kin Phone"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:435
+msgid "Unmarried"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:436
+msgid "Married"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:44
+#: kofacustom/nigeria/students/interfaces.py:555
+msgid "Registration Number"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:444
+msgid "Muslim"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:445
+msgid "Christian"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:446
+msgid "Others"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:447
+msgid "Prefer not to say"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:471
+msgid "Permanent Address"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:51
+msgid "Staff Member"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:531
+msgid "Current Session Payment"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:537
+msgid "Payment Level"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:564
+msgid "Matriculation Number"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:58
+msgid "Financially Cleared by"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:64
+msgid "Financial Clearance Date"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:70
+msgid "Provisionally Cleared"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:85
+msgid "Officer's Comment"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:90
+msgid "Physical Clearance Date"
+msgstr ""
+
+#: kofacustom/nigeria/students/interfaces.py:95
+msgid "CLR Activation Code"
+msgstr ""
+
+#: kofacustom/nigeria/students/utils.py:30
+msgid "Advanced Level Record"
+msgstr ""
+
+#: kofacustom/nigeria/students/utils.py:32
+msgid "Second Higher Education Record"
+msgstr ""
+
+#: kofacustom/nigeria/students/utils.py:35
+msgid "Former Student"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:116
+#: kofacustom/nigeria/students/viewlets.py:127
+msgid "LGA Identification"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:117
+#: kofacustom/nigeria/students/viewlets.py:128
+msgid "LGA Identification Scan"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:144
+#: kofacustom/nigeria/students/viewlets.py:145
+#: kofacustom/nigeria/students/viewlets.py:155
+msgid "First Sitting Result"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:156
+msgid "First Sitting Result Scan"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:172
+#: kofacustom/nigeria/students/viewlets.py:173
+#: kofacustom/nigeria/students/viewlets.py:183
+msgid "Second Sitting Result"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:184
+msgid "Second Sitting Result Scan"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:200
+#: kofacustom/nigeria/students/viewlets.py:201
+#: kofacustom/nigeria/students/viewlets.py:211
+msgid "Higher Qualification Result"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:212
+msgid "Higher Qualification Result Scan"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:228
+#: kofacustom/nigeria/students/viewlets.py:229
+#: kofacustom/nigeria/students/viewlets.py:239
+msgid "Second Higher Qualification Result"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:240
+msgid "Second Higher Qualification Result Scan"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:260
+#: kofacustom/nigeria/students/viewlets.py:261
+#: kofacustom/nigeria/students/viewlets.py:271
+msgid "Advanced Level Result"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:272
+msgid "Advanced Level Result Scan"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:288
+#: kofacustom/nigeria/students/viewlets.py:289
+#: kofacustom/nigeria/students/viewlets.py:299
+msgid "Certificate"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:300
+msgid "Certificate Scan"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:316
+#: kofacustom/nigeria/students/viewlets.py:317
+#: kofacustom/nigeria/students/viewlets.py:327
+msgid "Second Certificate"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:328
+msgid "Second Certificate Scan"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:348
+#: kofacustom/nigeria/students/viewlets.py:349
+#: kofacustom/nigeria/students/viewlets.py:359
+msgid "Third Certificate"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:360
+msgid "Third Certificate Scan"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:380
+#: kofacustom/nigeria/students/viewlets.py:381
+#: kofacustom/nigeria/students/viewlets.py:391
+msgid "Evidence of Name"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:392
+msgid "Evidence of Name Scan"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:408
+#: kofacustom/nigeria/students/viewlets.py:409
+#: kofacustom/nigeria/students/viewlets.py:419
+msgid "Result Statement"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:420
+msgid "Result Statement Scan"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:436
+#: kofacustom/nigeria/students/viewlets.py:437
+#: kofacustom/nigeria/students/viewlets.py:447
+msgid "Guarantor/Referee Letter"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:448
+msgid "Guarantor/Referee Letter Scan"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:464
+#: kofacustom/nigeria/students/viewlets.py:465
+#: kofacustom/nigeria/students/viewlets.py:475
+msgid "Second Referee Letter"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:47
+msgid "Clear student financially"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:476
+msgid "Second Referee Letter Scan"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:496
+#: kofacustom/nigeria/students/viewlets.py:497
+#: kofacustom/nigeria/students/viewlets.py:507
+msgid "Third Referee Letter"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:508
+msgid "Third Referee Letter Scan"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:528
+#: kofacustom/nigeria/students/viewlets.py:529
+#: kofacustom/nigeria/students/viewlets.py:539
+msgid "Affidavit of Good Conduct"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:540
+msgid "Affidavit of Good Conduct Scan"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:556
+#: kofacustom/nigeria/students/viewlets.py:557
+#: kofacustom/nigeria/students/viewlets.py:567
+msgid "Letter of Admission"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:568
+msgid "Letter of Admission Scan"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:62
+msgid "Withdraw financial clearance"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:77
+msgid "Download fee payment history"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:87
+#: kofacustom/nigeria/students/viewlets.py:98
+msgid "Acceptance Letter"
+msgstr ""
+
+#: kofacustom/nigeria/students/viewlets.py:88
+#: kofacustom/nigeria/students/viewlets.py:99
+msgid "Acceptance Letter Scan"
+msgstr ""
+
+#: kofacustom/nigeria/utils/utils.py:27
+msgid "Nigeria"
+msgstr ""
+
+#: kofacustom/nigeria/utils/utils.py:28
+msgid "Germany"
+msgstr ""
+
+#: kofacustom/nigeria/utils/utils.py:29
+msgid "United States"
+msgstr ""
+
+#: kofacustom/nigeria/utils/utils.py:30
+msgid "Niger"
+msgstr ""
+
+#: kofacustom/nigeria/utils/utils.py:31
+msgid "Benin"
+msgstr ""
+
+#: kofacustom/nigeria/utils/utils.py:32
+msgid "Cameroon"
+msgstr ""
+
+#: kofacustom/nigeria/utils/utils.py:33
+msgid "United Kingdom"
+msgstr ""
+
+#: kofacustom/nigeria/utils/utils.py:34
+msgid "France"
+msgstr ""
+
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/mail.zcml
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/mail.zcml	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/mail.zcml	(revision 17238)
@@ -0,0 +1,82 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           xmlns:mail="http://namespaces.zope.org/mail">
+  <!--
+
+   SMTP Settings
+   *************
+
+   In here we define the mailers and mail deliveries we want to be
+   used by the portal.
+
+   They are set here (and not dynamically in the portal) to increase
+   security. Even portal users with all permissions cannot change mail
+   server credentials and related. You need filesystem access to
+   change/override the settings in here.
+
+   In the portal you can pick one of the deliveries for use with all
+   SMTP (aka mail) related functionality.
+
+   Each delivery (direct or queued) is bound to a certain mailer,
+   defined by a 'mail:smtpMailer' directive.
+
+   The smtpMailer directives define real mail hosts (local or remote)
+   where mail from the portal should be processed.
+
+   The 'mail:directDelivery' and 'mail:queuedDelivery' directives send
+   local mail to the mailer set in their 'mailer' attribute. While
+   directDelivery sends mail immediately when a request is finished
+   (see zope.sendmail for more info), queuedDelivery handles mail in a
+   background thread and sends mail independently from requests.
+
+   By default in KOFA we have a fake mailer activated, that does not
+   send real mail to the outside world.
+  -->
+
+  <!-- A mail server on localhost -->
+  <!-- Mail server definitions require the following attributes:
+
+        - name: (string) name to be used by deliveries
+
+       Mail server definitions allow the following additional attribs:
+
+        - hostname   (string) hostname or IP(?) of mail server to use
+                              ('localhost' by default)
+        - port       (int number) port to connect to SMTP mailer
+                              (25 by default)
+        - username   (string) Username to log in to mail server
+        - password   (string) Password to log in to mail server
+
+  -->
+  <mail:smtpMailer
+      name="smtp.localhost"
+      hostname="localhost"
+      port="25"
+      />
+
+  <!-- The mail deliveries -->
+  <!-- Mail deliveries require the following attributes:
+
+        - name:       (string) this string will appear in portal select
+                               boxes
+        - permission: (string) zope default perms only
+        - mailer:     (string) name of some mailer registered above or
+                               'smtp'
+        - queuePath:  (string) must exist and contain cur/, new/, and
+                               tmp/ dirs. Relative paths are relative
+                               to the directory this files lives in.
+
+   -->
+  <mail:queuedDelivery
+      name="Queued SMTP on localhost"
+      permission="zope.Public"
+      mailer="smtp.localhost"
+      queuePath="../../../var/mailqueue"
+      />
+
+  <mail:directDelivery
+      name="Direct SMTP on localhost"
+      permission="zope.Public"
+      mailer="smtp.localhost"
+      />
+
+</configure>
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/payments/__init__.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/payments/__init__.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/payments/__init__.py	(revision 17238)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/payments/interfaces.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/payments/interfaces.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/payments/interfaces.py	(revision 17238)
@@ -0,0 +1,118 @@
+## $Id$
+##
+## Copyright (C) 2011 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
+##
+from zope.interface import Attribute
+from zope import schema
+from waeup.kofa.interfaces import SimpleKofaVocabulary
+from waeup.kofa.payments.interfaces import IPayment
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+class INigeriaOnlinePayment(IPayment):
+    """A payment via payment gateways.
+
+    """
+
+    ac = schema.TextLine(
+        title = _(u'Activation Code'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
+
+    r_amount_approved = schema.Float(
+        title = _(u'Response Amount Approved'),
+        default = 0.0,
+        required = False,
+        readonly = False,
+        )
+
+    r_code = schema.TextLine(
+        title = _(u'Response Code'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
+
+    r_desc = schema.TextLine(
+        title = _(u'Response Description'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
+
+    # Only defined in kofacustom.nigeria package
+
+    r_pay_reference = schema.TextLine(
+        title = _(u'Response Payment Reference'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
+
+    r_card_num = schema.TextLine(
+        title = _(u'Response Card Number'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
+
+    r_company = schema.Choice(
+        title = _(u'Payment Gateway'),
+        default = None,
+        required = False,
+        readonly = False,
+        vocabulary = SimpleKofaVocabulary(
+            (_('Interswitch'), 'interswitch'),
+            (_('Etranzact'), 'etranzact'),
+            (_('Remita'), 'remita'),
+            (_('Scratch Card'), 'sc'),
+            (_('Manifest'), 'manifest'),
+            (_('Sponsor'), 'sponsored'),
+            (_('unknown'), 'unknown'),
+            )
+        )
+
+    provider_amt = schema.Float(
+        title = _(u'Portal Charge'),
+        default = 0.0,
+        required = False,
+        readonly = False,
+        )
+
+    gateway_amt = schema.Float(
+        title = _(u'Gateway Amount'),
+        default = 0.0,
+        required = False,
+        readonly = False,
+        )
+
+    thirdparty_amt = schema.Float(
+        title = _(u'Third Party Amount'),
+        default = 0.0,
+        required = False,
+        readonly = False,
+        )
+
+    net_amt = schema.Float(
+        title = _(u'Net Amount'),
+        default = 0.0,
+        required = False,
+        readonly = False,
+        )
+
+INigeriaOnlinePayment['net_amt'].order = INigeriaOnlinePayment[
+    'amount_auth'].order
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/__init__.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/__init__.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/__init__.py	(revision 17238)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/applicantsbrowser.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/applicantsbrowser.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/applicantsbrowser.py	(revision 17238)
@@ -0,0 +1,302 @@
+## $Id: browser.py 14759 2017-08-03 09:09:54Z henrik $
+##
+## Copyright (C) 2017 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import grok
+import hashlib
+from datetime import datetime, timedelta
+from zope.component import getUtility
+from zope.security import checkPermission
+from waeup.kofa.interfaces import IKofaUtils
+from waeup.kofa.utils.helpers import to_timezone
+from waeup.kofa.browser.layout import UtilityView, KofaPage
+from waeup.kofa.browser.viewlets import ManageActionButton
+from waeup.kofa.applicants.browser import OnlinePaymentDisplayFormPage as OPDPApplicant
+from kofacustom.nigeria.remita.helpers import (
+    get_JSON_POST_response, query_remita, write_payments_log)
+from kofacustom.nigeria.payments.interfaces import INigeriaOnlinePayment
+from kofacustom.nigeria.applicants.interfaces import INigeriaApplicantOnlinePayment
+from kofacustom.nigeria.interfaces import MessageFactory as _
+from kofacustom.nigeria.remita.studentsbrowser import module_activated
+
+from kofacustom.nigeria.remita.tests import (
+    MERCHANTID, HOST, HTTPS, API_KEY, SERVICETYPEID, GATEWAY_AMT)
+
+grok.templatedir('browser_templates')
+
+# Buttons
+
+class RemitaActionButtonApplicant(ManageActionButton):
+    grok.order(1)
+    grok.context(INigeriaOnlinePayment)
+    grok.view(OPDPApplicant)
+    grok.require('waeup.payApplicant')
+    icon = 'actionicon_pay.png'
+    text = _('Pay via Remita')
+    target = 'goto_remita'
+
+    @property
+    def target_url(self):
+        if not 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 RemitaRequestPaymentStatusActionButtonApplicant(ManageActionButton):
+    grok.order(2)
+    grok.context(INigeriaOnlinePayment)
+    grok.view(OPDPApplicant)
+    grok.require('waeup.payApplicant')
+    icon = 'actionicon_call.png'
+    text = _('Requery Remita Payment Status')
+    target = 'request_payment_status'
+
+    @property
+    def target_url(self):
+        if not module_activated(
+            self.context.__parent__.__parent__.year, self.context):
+            return ''
+        if self.context.p_state in ('paid', 'waived', 'scholarship'):
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class RemitaVerifyPaymentStatusActionButtonApplicant(ManageActionButton):
+    grok.order(3)
+    grok.context(INigeriaOnlinePayment)
+    grok.view(OPDPApplicant)
+    grok.require('waeup.manageApplication')
+    icon = 'actionicon_call.png'
+    text = _('Verify Remita Payment Status')
+    target = 'verify_payment_status'
+
+    @property
+    def target_url(self):
+        if not module_activated(
+            self.context.__parent__.__parent__.year, self.context):
+            return ''
+        if self.context.p_state != 'paid' \
+            or self.context.r_company != u'remita':
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+# Webservice request views
+
+class RemitaRequestPaymentStatusPageApplicant(UtilityView, grok.View):
+    """ Request webservice view for the Remita gateway.
+    """
+    grok.context(INigeriaApplicantOnlinePayment)
+    grok.name('request_payment_status')
+    grok.require('waeup.payApplicant')
+
+    merchantId = MERCHANTID
+    host = HOST
+    https = HTTPS
+    api_key = API_KEY
+
+    def update(self):
+        if not 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'):
+            self.flash(_('This ticket has already been paid.'), type='danger')
+            return
+        applicant = self.context.__parent__
+        RRR = self.context.r_pay_reference
+        if not RRR:
+            self.flash(_('Remita Retrieval Reference not found.'), type='danger')
+            return
+        # Remita sends a POST request which may contain more information
+        # if a payment was not successful.
+        resp = self.request.form
+        if resp and resp.get('statuscode') not in (None, '025', '00', '01'):
+            self.flash('Transaction status message from Remita: %s'
+                % resp.get('status'), type='warning')
+        success, msg, log = query_remita(
+            self.context,
+            self.merchantId,
+            self.api_key,
+            RRR,
+            self.host,
+            self.https,
+            False)
+        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 RemitaVerifyPaymentStatusPageApplicant(UtilityView, grok.View):
+    """ Request webservice view for the Remita gateway.
+    """
+    grok.context(INigeriaApplicantOnlinePayment)
+    grok.name('verify_payment_status')
+    grok.require('waeup.manageApplication')
+
+    merchantId = MERCHANTID
+    host = HOST
+    https = HTTPS
+    api_key = API_KEY
+
+    def update(self):
+        if not 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  != 'paid' \
+            or self.context.r_company != u'remita':
+            self.flash(_('This ticket has not been paid.'), type='danger')
+            return
+        applicant = self.context.__parent__
+        RRR = self.context.r_pay_reference
+        if not RRR:
+            self.flash(_('Remita Retrieval Reference not found.'), type='danger')
+            return
+        # Remita sends a POST request which may contain more information
+        # if a payment was not successful.
+        resp = self.request.form
+        if resp and resp.get('statuscode') not in (None, '025', '00', '01'):
+            self.flash('Transaction status message from Remita: %s'
+                % resp.get('status'), type='warning')
+        success, msg, log = query_remita(
+            self.context,
+            self.merchantId,
+            self.api_key,
+            RRR,
+            self.host,
+            self.https,
+            True)
+        applicant.writeLogMessage(self, log)
+        if not success:
+            self.flash(msg, type='danger')
+            return
+        self.flash(msg)
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context))
+        return
+
+
+# Forwarding pages
+
+class RemitaPageApplicant(KofaPage):
+    """ View which sends a POST request to the Remita payment gateway.
+    """
+    grok.context(INigeriaApplicantOnlinePayment)
+    grok.name('goto_remita')
+    grok.template('goto_remita')
+    grok.require('waeup.payApplicant')
+    label = _('Pay via Remita')
+    submit_button = _('Pay now')
+
+    merchantId = MERCHANTID
+    host = HOST
+    https = HTTPS
+    api_key = API_KEY
+    serviceTypeId = SERVICETYPEID
+
+    #orderId = '3456346346'
+    init_url = '/remita/ecomm/split/init.reg'
+    amount='1000'
+    lineitems = (
+                  {"lineItemsId":"itemid1","beneficiaryName":"Klaus Mueller",
+                  "beneficiaryAccount":"6020067886","bankCode":"011",
+                  "beneficiaryAmount":"500","deductFeeFrom":"1"},
+                  {"lineItemsId":"itemid2","beneficiaryName":"Werner Rumm",
+                  "beneficiaryAccount":"0360883515","bankCode":"050",
+                  "beneficiaryAmount":"500","deductFeeFrom":"0"}
+                )
+
+    @property
+    def action(self):
+        if self.https:
+            return 'https://' + self.host + '/remita/ecomm/finalize.reg'
+        return 'http://' + self.host + '/remita/ecomm/finalize.reg'
+
+    def init_update(self):
+        if self.context.r_pay_reference or self.context.p_state == 'paid':
+            return _("Payment ticket can't be re-sent to Remita.")
+        if self.context.r_company and self.context.r_company != 'remita':
+            return _("Payment ticket has been used for another payment gateway.")
+        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.responseurl = self.url(self.context, 'request_payment_status')
+        resp = get_JSON_POST_response(
+            merchantId=self.merchantId,
+            serviceTypeId=self.serviceTypeId,
+            api_key=self.api_key,
+            orderId=self.orderId,
+            amount=self.amount,
+            responseurl=self.responseurl,
+            host=self.host,
+            url=self.init_url,
+            https=self.https,
+            fullname=self.context.__parent__.display_fullname,
+            email=self.context.__parent__.email,
+            lineitems=self.lineitems)
+        if resp.get('error'):
+            return resp.get('error')
+        if resp.get('statuscode') not in ('021', '025', '055'):
+            return 'RRR generation message from Remita: ' + resp.get('status')
+        self.rrr = self.context.r_pay_reference = resp['RRR'].rstrip()
+        hashargs = 	self.merchantId + self.rrr + self.api_key
+        self.hashvalue = hashlib.sha512(hashargs).hexdigest()
+        self.customer = self.context.__parent__
+        self.customer.writeLogMessage(self,
+            'RRR retrieved: %s, ServiceTypeId: %s'
+            % (self.rrr, self.serviceTypeId))
+        return
+
+    def update(self):
+        if not module_activated(
+            self.context.__parent__.__parent__.year, self.context):
+            self.flash(_('Forbidden'), type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        self.orderId = self.context.p_id
+        error = self.init_update()
+        if error:
+            self.flash(error, type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        # Already now it becomes a Remita 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'remita'
+        self.amount_auth = int(100 * self.context.amount_auth)
+        return
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/browser_templates/goto_remita.pt
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/browser_templates/goto_remita.pt	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/browser_templates/goto_remita.pt	(revision 17238)
@@ -0,0 +1,77 @@
+<form i18n:domain="kofacustom.nigeria"
+  tal:attributes="action view/action" method="POST">
+
+  <p i18n:translate="">
+    Your payment details were sent to Remita and a retrieval reference
+    number (RRR) has been generated. Please click the button below to make
+    your RRR payment.
+  </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">
+          Order Id:</td>
+        <td>
+          <span tal:replace="view/context/p_id">ORDERID</span>
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          Remita Retrieval Reference:</td>
+        <td>
+          <span tal:replace="view/rrr">RRR</span>
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          Remita Service Type Id:</td>
+        <td>
+          <span tal:replace="view/serviceTypeId">SERVCICETYPEID</span>
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          Customer Name:</td>
+        <td>
+          <span tal:replace="view/customer/display_fullname">CUSTNAME</span>
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          Email Address:</td>
+        <td>
+          <span tal:replace="view/customer/email">EMAIL</span>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+
+  <input name="merchantId" tal:attributes="value view/merchantId" type="hidden">
+  <input name="hash" tal:attributes="value view/hashvalue" type="hidden">
+  <input name="rrr" tal:attributes="value view/rrr" type="hidden">
+  <input name="responseurl" tal:attributes="value view/responseurl" type="hidden">
+
+  <br />
+
+  <p i18n:translate="">
+    Please note that Remita is adding a service charge which will not be shown
+    on the payment slip after payment.
+  </p>
+
+  <br />
+
+  <input type="submit" class="btn btn-primary" tal:attributes="value view/submit_button"/>
+
+  <br /><br />
+
+  <img src="/static/img/remita-payment-logo-horizonal.png" />
+
+</form>
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/helpers.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/helpers.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/helpers.py	(revision 17238)
@@ -0,0 +1,171 @@
+## $Id$
+##
+## 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
+##
+"""General helper functions for the remita module in custom packages.
+"""
+import grok
+from datetime import datetime
+import httplib
+import hashlib
+import json
+
+from zope.event import notify
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+def get_JSON_POST_response(
+        merchantId, serviceTypeId, api_key, orderId,
+        amount, responseurl, host, url, https, fullname, email, lineitems):
+    hashargs = 	merchantId + serviceTypeId + orderId + str(amount) + responseurl + api_key
+    hashvalue = hashlib.sha512(hashargs).hexdigest()
+    headers={
+        'Content-Type':'application/json; charset=utf-8',
+    }
+    if https:
+        h = httplib.HTTPSConnection(host)
+    else:
+        h = httplib.HTTPConnection(host)
+    data = {
+            "merchantId":merchantId,
+            "serviceTypeId":serviceTypeId,
+            "totalAmount":amount,
+            "hash":hashvalue,
+            "payerName":fullname,
+            "payerEmail":email,
+            "orderId":orderId,
+            "responseurl":responseurl,
+            "lineItems":lineitems
+            }
+    try:
+        h.request("POST", url, body=json.dumps(data), headers=headers)
+        resp = h.getresponse()
+    except:
+        return {'error': 'Socket Error: Connection to Remita gateway refused.'}
+    if resp.status!=200:
+        return {'error': 'Connection Error (%s, %s)' % (resp.status, resp.reason)}
+    jsonout = resp.read()
+    try:
+        parsed_json = json.loads(jsonout[6:-1])
+    except ValueError:
+        return {'error': 'No JSON response'}
+    return parsed_json
+
+def get_payment_status_via_rrr(merchantId, api_key, RRR, host, https):
+    RRR = RRR.rstrip()
+    hashargs = 	RRR + api_key + merchantId
+    hashvalue = hashlib.sha512(hashargs).hexdigest()
+    headers={
+        'Content-Type':'application/json; charset=utf-8',
+    }
+    
+    # url = '/remita/ecomm/%s/%s/%s/status.reg' % (merchantId, RRR, hashvalue)
+
+    # On 28/10/22 Balogun Olalekan wrote:
+    # Kindly update the transaction validation endpoint being used to the below.
+    # https://login.remita.net/remita/exapp/api/v1/send/api/echannelsvc/{{merchantId}}/{{rrr}}/{{apiHash}}/status.reg.
+
+    url = '/remita/exapp/api/v1/send/api/echannelsvc/%s/%s/%s/status.reg' % (merchantId, RRR, hashvalue)
+
+    if https:
+        h = httplib.HTTPSConnection(host)
+    else:
+        h = httplib.HTTPConnection(host)
+    try:
+        h.request("GET", url, headers=headers)
+        resp = h.getresponse()
+    except:
+        return {'error': 'Socket Error: Connection to Remita gateway refused.'}
+    if resp.status!=200:
+        return {'error': 'Connection error (%s, %s)' % (resp.status, resp.reason)}
+    jsonout = resp.read()
+    try:
+        parsed_json = json.loads(jsonout)
+    except ValueError:
+        return {'error': 'No JSON response'}
+    return parsed_json
+
+def query_remita(payment, merchantId, api_key, RRR, host, https, verify):
+
+    jr = get_payment_status_via_rrr(merchantId, api_key, RRR, host, https)
+    error = jr.get('error')
+    if error:
+        msg = log = error
+        return False, msg, log
+
+    # A typical JSON response
+    # {
+    # u'orderId': u'3456346346',
+    # u'status': u'021',
+    # u'amount': 1000.0,
+    # u'transactiontime': u'2017-07-31 11:17:24 AM',
+    # u'message': u'Transaction Pending',
+    # u'lineitems': [{u'status': u'021', u'lineItemsId': u'itemid1'},
+    #                {u'status': u'021', u'lineItemsId': u'itemid2'}
+    #               ],
+    # u'RRR': u'280007640804'}
+
+    payment.r_code = jr['status']
+    payment.r_desc = jr['message']
+    payment.r_amount_approved = jr['amount']
+    try:
+        payment.r_pay_reference = jr['RRR']
+    except KeyError:
+        msg = _('Error message from Remita: ${a}', mapping = {'a': payment.r_desc})
+        log = 'unsuccessful response 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.r_company = u'remita'
+    if payment.r_code not in ('00', '01'):
+        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(jr))
+        payment.p_state = 'failed'
+        notify(grok.ObjectModifiedEvent(payment))
+        return False, msg, log
+    orderId = jr.get('orderId', None)
+    if orderId and orderId != payment.p_id:
+        msg = _('Response order id does not match.')
+        log = 'wrong response 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 response received')
+    log = 'valid response for %s payment %s: %s' % (
+        payment.p_category, payment.p_id, str(jr))
+    notify(grok.ObjectModifiedEvent(payment))
+    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))
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/studentsbrowser.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/studentsbrowser.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/studentsbrowser.py	(revision 17238)
@@ -0,0 +1,326 @@
+## $Id$
+##
+## Copyright (C) 2017 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import grok
+import hashlib
+from datetime import datetime, timedelta
+from zope.component import getUtility
+from zope.security import checkPermission
+from waeup.kofa.interfaces import IKofaUtils
+from waeup.kofa.utils.helpers import to_timezone
+from waeup.kofa.browser.layout import UtilityView, KofaPage
+from waeup.kofa.browser.viewlets import ManageActionButton
+from waeup.kofa.students.interfaces import IStudentsUtils
+from waeup.kofa.students.browser import OnlinePaymentDisplayFormPage as OPDPStudent
+from kofacustom.nigeria.remita.helpers import (
+    get_JSON_POST_response, query_remita, write_payments_log)
+from kofacustom.nigeria.payments.interfaces import INigeriaOnlinePayment
+from kofacustom.nigeria.students.interfaces import INigeriaStudentOnlinePayment
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+from kofacustom.nigeria.remita.tests import (
+    MERCHANTID, HOST, HTTPS, API_KEY, SERVICETYPEID, GATEWAY_AMT)
+
+grok.templatedir('browser_templates')
+
+def module_activated(session, payment):
+    if payment.r_company and payment.r_company != 'remita':
+        return False
+    try:
+        return getattr(grok.getSite()['configuration'][str(session)],
+            'remita_enabled', False)
+    except KeyError:
+        session = datetime.now().year
+        try:
+            return getattr(grok.getSite()['configuration'][str(session)],
+                'remita_enabled', False)
+        except KeyError:
+            return False
+
+# Buttons
+
+class RemitaActionButtonStudent(ManageActionButton):
+    grok.order(5)
+    grok.context(INigeriaOnlinePayment)
+    grok.view(OPDPStudent)
+    grok.require('waeup.payStudent')
+    icon = 'actionicon_pay.png'
+    text = _('Pay via Remita')
+    target = 'goto_remita'
+
+    @property
+    def target_url(self):
+        if not 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 RemitaRequestPaymentStatusActionButtonStudent(ManageActionButton):
+    grok.order(6)
+    grok.context(INigeriaOnlinePayment)
+    grok.view(OPDPStudent)
+    grok.require('waeup.payStudent')
+    icon = 'actionicon_call.png'
+    text = _('Requery Remita Payment Status')
+    target = 'request_payment_status'
+
+    @property
+    def target_url(self):
+        if not module_activated(
+            self.context.student.current_session, self.context):
+            return ''
+        if self.context.p_state in ('paid', 'waived', 'scholarship'):
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class RemitaVerifyPaymentStatusActionButtonStudent(ManageActionButton):
+    grok.order(3)
+    grok.context(INigeriaOnlinePayment)
+    grok.view(OPDPStudent)
+    grok.require('waeup.manageStudent')
+    icon = 'actionicon_call.png'
+    text = _('Verify Remita Payment Status')
+    target = 'verify_payment_status'
+
+    @property
+    def target_url(self):
+        if not module_activated(
+            self.context.student.current_session, self.context):
+            return ''
+        if self.context.p_state != 'paid' \
+            or self.context.r_company != u'remita':
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+# Webservice request views
+
+class RemitaRequestPaymentStatusPageStudent(UtilityView, grok.View):
+    """ Request webservice view for the Remita gateway.
+    """
+    grok.context(INigeriaStudentOnlinePayment)
+    grok.name('request_payment_status')
+    grok.require('waeup.payStudent')
+
+    merchantId = MERCHANTID
+    host = HOST
+    https = HTTPS
+    api_key = API_KEY
+
+    def update(self):
+        if not 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'):
+            self.flash(_('This ticket has already been paid.'), type='danger')
+            return
+        student = self.context.student
+        RRR = self.context.r_pay_reference
+        if not RRR:
+            self.flash(_('Remita Retrieval Reference not found.'), type='danger')
+            return
+        # Remita sends a POST request which may contain more information
+        # if a payment was not successful.
+        resp = self.request.form
+        if resp and resp.get('statuscode') not in (None, '025', '00', '01'):
+            self.flash('Transaction status message from Remita: %s'
+                % resp.get('status'), type='warning')
+        success, msg, log = query_remita(
+            self.context,
+            self.merchantId,
+            self.api_key,
+            RRR,
+            self.host,
+            self.https,
+            False)
+        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
+
+class RemitaVerifyPaymentStatusPageStudent(UtilityView, grok.View):
+    """ Request webservice view for the Remita gateway.
+    """
+    grok.context(INigeriaStudentOnlinePayment)
+    grok.name('verify_payment_status')
+    grok.require('waeup.manageStudent')
+
+    merchantId = MERCHANTID
+    host = HOST
+    https = HTTPS
+    api_key = API_KEY
+
+    def update(self):
+        if not 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  != 'paid' \
+            or self.context.r_company != u'remita':
+            self.flash(_('This ticket has not been paid.'), type='danger')
+            return
+        student = self.context.student
+        RRR = self.context.r_pay_reference
+        if not RRR:
+            self.flash(_('Remita Retrieval Reference not found.'), type='danger')
+            return
+        # Remita sends a POST request which may contain more information
+        # if a payment was not successful.
+        resp = self.request.form
+        if resp and resp.get('statuscode') not in (None, '025', '00', '01'):
+            self.flash('Transaction status message from Remita: %s'
+                % resp.get('status'), type='warning')
+        success, msg, log = query_remita(
+            self.context,
+            self.merchantId,
+            self.api_key,
+            RRR,
+            self.host,
+            self.https,
+            True)
+        student.writeLogMessage(self, log)
+        if not success:
+            self.flash(msg, type='danger')
+            return
+        self.flash(msg)
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, '@@index'))
+        return
+
+
+# Forwarding pages
+
+class RemitaPageStudent(KofaPage):
+    """ View which sends a POST request to the Remita payment gateway.
+    """
+    grok.context(INigeriaOnlinePayment)
+    grok.name('goto_remita')
+    grok.template('goto_remita')
+    grok.require('waeup.payStudent')
+    label = _('Pay via Remita')
+    submit_button = _('Pay now')
+
+    merchantId = MERCHANTID
+    host = HOST
+    https = HTTPS
+    api_key = API_KEY
+    serviceTypeId = SERVICETYPEID
+    gateway_amt = GATEWAY_AMT
+
+    #orderId = '3456346346'
+    init_url = '/remita/ecomm/split/init.reg'
+    amount='1000'
+    lineitems = (
+                  {"lineItemsId":"itemid1","beneficiaryName":"Klaus Mueller",
+                  "beneficiaryAccount":"6020067886","bankCode":"011",
+                  "beneficiaryAmount":"500","deductFeeFrom":"1"},
+                  {"lineItemsId":"itemid2","beneficiaryName":"Werner Rumm",
+                  "beneficiaryAccount":"0360883515","bankCode":"050",
+                  "beneficiaryAmount":"500","deductFeeFrom":"0"}
+                )
+
+    @property
+    def action(self):
+        if self.https:
+            return 'https://' + self.host + '/remita/ecomm/finalize.reg'
+        return 'http://' + self.host + '/remita/ecomm/finalize.reg'
+
+    def init_update(self):
+        if self.context.r_pay_reference or self.context.p_state == 'paid':
+            return _("Payment ticket can't be re-sent to Remita.")
+        if self.context.r_company and self.context.r_company != 'remita':
+            return _("Payment ticket has been used for another payment gateway.")
+        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.")
+        certificate = getattr(self.context.student['studycourse'],'certificate',None)
+        if certificate is None:
+            return _("Study course data are incomplete.")
+        kofa_utils = getUtility(IKofaUtils)
+        student_utils = getUtility(IStudentsUtils)
+        if student_utils.samePaymentMade(self.context.student, self.context.p_category,
+            self.context.p_item, self.context.p_session):
+            return _("This type of payment has already been made.")
+        self.responseurl = self.url(self.context, 'request_payment_status')
+        resp = get_JSON_POST_response(
+            merchantId=self.merchantId,
+            serviceTypeId=self.serviceTypeId,
+            api_key=self.api_key,
+            orderId=self.orderId,
+            amount=self.amount,
+            responseurl=self.responseurl,
+            host=self.host,
+            url=self.init_url,
+            https=self.https,
+            fullname=self.context.student.display_fullname,
+            email=self.context.student.email,
+            lineitems=self.lineitems)
+        if resp.get('error'):
+            return resp.get('error')
+        if resp.get('statuscode') not in ('021', '025', '055'):
+            return 'RRR generation message from Remita: ' + resp.get('status')
+        self.rrr = self.context.r_pay_reference = resp['RRR'].rstrip()
+        hashargs = 	self.merchantId + self.rrr + self.api_key
+        self.hashvalue = hashlib.sha512(hashargs).hexdigest()
+        self.customer = self.context.student
+        self.customer.writeLogMessage(self,
+            'RRR retrieved: %s, ServiceTypeId: %s'
+            % (self.rrr, self.serviceTypeId))
+        return
+
+    def update(self):
+        if not module_activated(
+            self.context.student.current_session, self.context):
+            self.flash(_('Forbidden'), type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        self.orderId = self.context.p_id
+        error = self.init_update()
+        if error:
+            self.flash(error, type='danger')
+            self.redirect(self.url(self.context, '@@index'))
+            return
+        # Already now it becomes a Remita 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'remita'
+        self.amount_auth = int(100 * self.context.amount_auth)
+        return
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/tests.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/tests.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/tests.py	(revision 17238)
@@ -0,0 +1,408 @@
+## $Id$
+##
+## 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 os
+import unittest
+import random
+import json
+from datetime import datetime, timedelta, date
+from zope.component import createObject, getUtility
+from zope.catalog.interfaces import ICatalog
+from hurry.workflow.interfaces import IWorkflowState
+from waeup.kofa.students.tests.test_browser import StudentsFullSetup
+from waeup.kofa.applicants.tests.test_browser import ApplicantsFullSetup
+from waeup.kofa.configuration import SessionConfiguration
+from kofacustom.nigeria.students.payments import NigeriaStudentOnlinePayment
+from kofacustom.nigeria.testing import FunctionalLayer
+
+from kofacustom.nigeria.remita.helpers import (
+    get_JSON_POST_response, get_payment_status_via_rrr, query_remita)
+
+# Also run tests that send requests to external servers?
+#   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  # URLs do no longer work
+
+MERCHANTID = '2547916'
+HOST = 'www.remitademo.net'
+HTTPS = True
+API_KEY = '1946'
+SERVICETYPEID = '4430731'
+GATEWAY_AMT = 0.0
+
+def external_test(func):
+    if not EXTERNAL_TESTS:
+        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
+
+class HelperTests(unittest.TestCase):
+
+    merchantId = MERCHANTID
+    host = HOST
+    https = HTTPS
+    api_key = API_KEY
+    serviceTypeId = SERVICETYPEID
+
+    responseurl = 'http://xxxx'
+
+    url = '/remita/ecomm/split/init.reg'  # /remita/ecomm/v2/init.reg
+    lineitems = (
+                  {"lineItemsId":"itemid1","beneficiaryName":"Oshadami Mike",
+                  "beneficiaryAccount":"6020067886","bankCode":"011",
+                  "beneficiaryAmount":"500","deductFeeFrom":"1"},
+                  {"lineItemsId":"itemid2","beneficiaryName":"Ogunseye Mujib",
+                  "beneficiaryAccount":"0360883515","bankCode":"050",
+                  "beneficiaryAmount":"500","deductFeeFrom":"0"}
+                )
+    amount = 1000.0
+
+    def _get_transaction_data(self):
+        self.orderId = str(random.randint(100000000, 999999999))
+        resp = get_JSON_POST_response(
+                merchantId=self.merchantId, serviceTypeId=self.serviceTypeId,
+                api_key=self.api_key, orderId=self.orderId,
+                amount=self.amount, responseurl=self.responseurl,
+                host=self.host, url=self.url, https=self.https,
+                fullname='Anton Meier', email='am@xxx.de',
+                lineitems=self.lineitems)
+        return resp
+
+    def test_dummytest(self):
+        return
+
+    @external_test
+    def test_get_JSON_POST_response(self):
+        resp = self._get_transaction_data()
+        self.rrr = resp['RRR']
+        assert resp == {
+            u'status': u'Payment Reference generated',
+            u'orderID': self.orderId,
+            u'RRR': self.rrr,
+            u'statuscode': u'025'}
+        resp = get_JSON_POST_response(
+                merchantId=self.merchantId, serviceTypeId=self.serviceTypeId,
+                api_key=self.api_key, orderId=self.orderId,
+                amount=self.amount, responseurl=self.responseurl,
+                host=self.host, url=self.url, https=self.https,
+                fullname='Anton Meier', email='am@xxx.de',
+                lineitems=self.lineitems)
+        assert resp == {
+            u'status': u'Duplicate Order Ref',
+            u'statuscode': u'028'}
+
+        resp = get_payment_status_via_rrr(
+                merchantId=self.merchantId,
+                api_key=self.api_key,
+                RRR=self.rrr,
+                host=self.host,
+                https=self.https,
+                )
+        assert resp['orderId'] == self.orderId
+        assert resp['status'] == '021'
+        assert resp['amount'] == self.amount
+        assert resp['RRR'] == self.rrr
+        assert resp['message'] == u'Transaction Pending'
+
+class RemitaTestsStudents(StudentsFullSetup):
+    """Tests for the Remita payment gateway.
+    """
+
+    layer = FunctionalLayer
+
+    merchantId = MERCHANTID
+    host = HOST
+    https = HTTPS
+    api_key = API_KEY
+    serviceTypeId = SERVICETYPEID
+
+    responseurl = 'http://xxxx'
+
+    # successful transaction
+    # (hopefully this transaction remains in the Remita database)
+    orderId = '3456346346'
+    rrr = u'280007640804'
+    amount = 1000.0
+
+    # pending transaction
+    #orderId_p = '987698769876'
+    rrr_p = u'320007640976'
+
+    def setUp(self):
+        super(RemitaTestsStudents, self).setUp()
+        self.app['configuration']['2004'].remita_enabled = True
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.payments_path)
+        IWorkflowState(self.student).setState('cleared')
+        self.student.nationality = u'NG'
+        self.browser.open(self.payments_path + '/addop')
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+        ctrl = self.browser.getControl(name='val_id')
+        self.value = ctrl.options[0]
+        self.browser.getLink(self.value).click()
+        self.assertMatches('...Amount Authorized...',
+                           self.browser.contents)
+        self.assertTrue('<span>40000.0</span>', self.browser.contents)
+        self.payment_url = self.browser.url
+        self.payment = self.student['payments'][self.value]
+
+    @external_test
+    def test_query_remita(self):
+        #requery pending transaction
+        qr = query_remita(
+            self.payment,
+            merchantId=self.merchantId,
+            api_key=self.api_key,
+            RRR=self.rrr_p,
+            host=self.host,
+            https=self.https,
+            verify=False)
+        assert qr == (
+            False,
+            u'Unsuccessful response: ${a}',
+            u'unsuccessful response for schoolfee payment %s: Transaction Pending'
+            % self.payment.p_id)
+
+        #requery successful transaction
+        self.payment.amount_auth = 1000.0
+        self.payment.p_id = self.orderId
+        qr = query_remita(
+            self.payment,
+            merchantId=self.merchantId,
+            api_key=self.api_key,
+            RRR=self.rrr,
+            host=self.host,
+            https=self.https,
+            verify=False)
+        assert qr[0] == True
+        assert qr[1] == u'Successful response received'
+
+    @external_test
+    def test_student_remita_form(self):
+        # Manager can access Remita form
+        self.browser.getLink("Pay via Remita").click()
+        # The RRR has been retrieved
+        self.assertTrue('<input name="rrr" type="hidden" value="%s">'
+            % self.payment.r_pay_reference
+            in self.browser.contents)
+        self.assertTrue(
+            'action="https://www.remitademo.net/remita/ecomm/finalize.reg"'
+            in self.browser.contents)
+        self.assertEqual(self.payment.r_company, 'remita')
+        # Retrieval has been logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - kofacustom.nigeria.remita.studentsbrowser.RemitaPageStudent'
+            ' - K1000000 - RRR retrieved: %s, ServiceTypeId: %s'
+            % (self.payment.r_pay_reference, self.serviceTypeId) in logcontent)
+
+        # Means of testing end here.
+        return
+
+    @external_test
+    def test_requery_verify_payment_status(self):
+        # Manager can access Remita form
+        self.browser.getLink("Requery Remita Payment Status").click()
+        self.assertTrue('Remita Retrieval Reference not found.'
+            in self.browser.contents)
+        self.payment.r_pay_reference = self.rrr
+        self.browser.getLink("Requery Remita Payment Status").click()
+        self.assertTrue('Response amount does not match.'
+            in self.browser.contents)
+        self.payment.amount_auth = self.amount
+        self.browser.getLink("Requery Remita Payment Status").click()
+        self.assertTrue('Response order id does not match.'
+            in self.browser.contents)
+        self.payment.p_id = self.orderId
+        self.browser.getLink("Requery Remita Payment Status").click()
+        self.assertTrue('Payment successfully completed' in self.browser.contents)
+        self.assertEqual(self.payment.r_desc, 'Approved')
+        self.assertEqual(self.payment.r_amount_approved , 1000.0)
+        self.assertEqual(self.payment.r_code, '01')
+        self.browser.getLink("Verify Remita Payment Status").click()
+        self.assertTrue('Successful response received' in self.browser.contents)
+        return
+
+class RemitaTestsWebserviceStudent(StudentsFullSetup):
+    """Tests for the Remita payment gateway.
+    """
+    layer = FunctionalLayer
+
+    merchantId = MERCHANTID
+    host = HOST
+    https = HTTPS
+    api_key = API_KEY
+
+    responseurl = 'http://xxxx'
+
+    # successful transaction
+    # (hopefully this transaction remains in the Remita database)
+    orderId = '3456346346'
+    rrr = u'280007640804'
+    amount = 1000.0
+
+    @external_test
+    def test_payment_notification_listener_student(self):
+        payment = createObject('waeup.StudentOnlinePayment')
+        payment.p_category = u'schoolfee'
+        payment.p_session = self.student.current_session
+        payment.p_item = u'My Certificate'
+        payment.p_id = self.orderId
+        self.student['payments']['anykey'] = payment
+        data = [{'orderRef': self.orderId, 'rrr': self.rrr},]
+        # Send POST request with wrong payment amount
+        payment.amount_auth = 2000.0
+        self.browser.post('http://localhost/app/paymentnotificationlistener',
+            json.dumps(data), 'application/json; charset=utf-8')
+        self.assertEqual('0 (1)', self.browser.contents)
+        # Send POST request with correct payment amount
+        payment.amount_auth = self.amount
+        self.browser.post('http://localhost/app/paymentnotificationlistener',
+            json.dumps(data), 'application/json; charset=utf-8')
+        self.assertEqual('1 (1)', self.browser.contents)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.anybody - kofacustom.nigeria.remita.webservices.PaymentNotificationListenerWebservice'
+            ' - K1000000 - valid response for schoolfee payment %s: '
+            % self.orderId   in logcontent)
+        self.assertTrue(
+            'zope.anybody - kofacustom.nigeria.remita.webservices.PaymentNotificationListenerWebservice'
+            ' - K1000000 - successful schoolfee payment: %s'
+            % self.orderId in logcontent)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.anybody - PaymentNotificationListenerWebservice request: '
+            '[{"rrr": "%s", "orderRef": "%s"}]'
+            % (self.rrr, self.orderId) in logcontent)
+        return
+
+class RemitaTestsWebserviceApplicant(ApplicantsFullSetup):
+    """Tests for the Remita payment gateway.
+    """
+    layer = FunctionalLayer
+
+    merchantId = MERCHANTID
+    host = HOST
+    https = HTTPS
+    api_key = API_KEY
+
+    responseurl = 'http://xxxx'
+
+    # successful transaction
+    # (hopefully this transaction remains in the Remita database)
+    orderId = '3456346346'
+    rrr = u'280007640804'
+    amount = 1000.0
+
+    @external_test
+    def test_payment_notification_listener_applicant(self):
+        self.applicantscontainer.application_fee = self.amount
+        payment = createObject('waeup.ApplicantOnlinePayment')
+        payment.p_category = u'application'
+        payment.p_session = self.applicantscontainer.year
+        payment.p_item = u'My Certificate'
+        payment.p_id = self.orderId
+        payment.amount_auth = self.amount
+        self.applicant['anykey'] = payment
+        IWorkflowState(self.applicant).setState('started')
+        # Send POST request
+        data = [{'orderRef': self.orderId, 'rrr': self.rrr},]
+        self.browser.post('http://localhost/app/paymentnotificationlistener',
+            json.dumps(data), 'application/json; charset=utf-8')
+        self.assertEqual('1 (1)', self.browser.contents)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'applicants.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.anybody - kofacustom.nigeria.remita.webservices.PaymentNotificationListenerWebservice'
+            ' - %s - valid response for application payment %s: '
+            % (self.applicant.applicant_id, self.orderId) in logcontent)
+        self.assertTrue(
+            'zope.anybody - kofacustom.nigeria.remita.webservices.PaymentNotificationListenerWebservice'
+            ' - %s - successful payment: %s'
+            % (self.applicant.applicant_id, self.orderId) in logcontent)
+        return
+
+class RemitaTestsApplicants(ApplicantsFullSetup):
+    """Tests for the Remita payment gateway.
+    """
+
+    layer = FunctionalLayer
+
+    # successful transaction
+    # (hopefully this transaction remains in the Remita database)
+    #orderId = '3456346346'
+    rrr = u'280007640804'
+    serviceTypeId = SERVICETYPEID
+    #amount = 1000.0
+
+    def setUp(self):
+        super(RemitaTestsApplicants, self).setUp()
+        configuration = SessionConfiguration()
+        configuration.academic_session = datetime.now().year - 2
+        configuration.remita_enabled = True
+        self.app['configuration'].addSessionConfiguration(configuration)
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_path)
+        #IWorkflowState(self.student).setState('started')
+        super(RemitaTestsApplicants, self).fill_correct_values()
+        self.applicantscontainer.application_fee = 1000.0
+        self.browser.getControl(name="form.nationality").value = ['NG']
+        self.browser.getControl(name="transition").value = ['start']
+        self.browser.getControl("Save").click()
+        self.browser.getControl("Add online").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+        self.payment = self.applicant.values()[0]
+        self.payment_url = self.browser.url
+
+    @external_test
+    def test_applicant_remita_form(self):
+        # Manager can access Remita form
+        self.browser.getLink("Pay via Remita").click()
+        # The RRR has been retrieved
+        self.assertTrue('<input name="rrr" type="hidden" value="%s">'
+            % self.payment.r_pay_reference
+            in self.browser.contents)
+        self.assertEqual(self.payment.r_company, 'remita')
+        # Retrieval has been logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'applicants.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - kofacustom.nigeria.remita.applicantsbrowser.RemitaPageApplicant'
+            ' - %s - RRR retrieved: %s, ServiceTypeId: %s'
+            % (self.applicant.applicant_id,
+               self.payment.r_pay_reference,
+               self.serviceTypeId)
+            in logcontent)
+        # Means of testing end here.
+        return
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/webservices.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/webservices.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/remita/webservices.py	(revision 17238)
@@ -0,0 +1,129 @@
+## $Id$
+##
+## 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 json
+from zope.component import getUtility
+from zope.catalog.interfaces import ICatalog
+from waeup.kofa.interfaces import IUniversity
+from kofacustom.nigeria.remita.helpers import query_remita, write_payments_log
+
+class PaymentNotificationListenerWebservice(grok.View):
+    """A webservice to receive payment notifications from
+    accepted IP addresses without authentication which trigger
+    a query of a Remita transaction status.
+    """
+    grok.context(IUniversity)
+    grok.name('paymentnotificationlistener')
+    grok.require('waeup.Public')
+
+    ACCEPTED_IP = ('127.0.0.1',)
+
+    # Here we use Remita test portal data
+    merchantId = '2547916'
+    host = 'www.remitademo.net'
+    https = True
+    api_key = '1946'
+
+    def update(self, P_ID=None):
+
+        # Remita sends a JSON POST request with an array with
+        # multiple JSON objects:
+        # [
+        #    {
+        #       "rrr": "A39359490",
+        #       "channnel": "BRANCH",
+        #       "amount": "6500.00",
+        #       "transactiondate": "19/01/2015",
+        #       "debitdate": "19/01/2015",
+        #       "bank": "033",
+        #       "branch": "033152763",
+        #       "serviceTypeId": "4430731",
+        #       "dateSent": "20/01/2015",
+        #       "dateRequested": "19/01/2015",
+        #       "orderRef": "4086852511",
+        #       "payerName": "mujib ishola",
+        #       "payerEmail": "ishola@specs.com",
+        #       "payerPhoneNumber": "08141376868",
+        #       "uniqueIdentifier": 10001
+        #    },
+        #    {...}
+        # ]
+
+        real_ip = self.request.get('HTTP_X_FORWARDED_FOR', None)
+        if real_ip:
+            self.context.logger.info(
+                'PaymentNotificationListenerWebservice called from %s' % real_ip)
+        if real_ip and self.ACCEPTED_IP:
+            if real_ip not in self.ACCEPTED_IP:
+                self.output = '-1'
+                return
+        requ = self.request.bodyStream.read()
+        self.context.logger.info(
+            'PaymentNotificationListenerWebservice request: %s' % requ)
+        try:
+            parsed_json = json.loads(requ)
+        except ValueError:
+            self.output = '-2'
+            return
+        successful = 0
+        failed = 0
+        for payment in parsed_json:
+            orderRef =  payment.get('orderRef')
+            rrr = payment.get('rrr')
+            if not orderRef or not rrr:
+                failed += 1
+                continue
+            cat = getUtility(ICatalog, name='payments_catalog')
+            results = list(cat.searchResults(p_id=(orderRef, orderRef)))
+            if len(results) != 1:
+                failed += 1
+                continue
+            ticket = results[0]
+            if ticket.p_state in ('waived', 'scholarship'):
+                failed += 1
+                continue
+            waspaid = (ticket.p_state == 'paid')
+            success, msg, log = query_remita(
+                ticket, self.merchantId, self.api_key,
+                rrr, self.host, self.https, False)
+            if not success:
+                failed += 1
+                continue
+            if hasattr(ticket, 'doAfterStudentPayment'):
+                ticket.student.writeLogMessage(self, log)
+                successful += 1
+                if not waspaid:
+                    write_payments_log(ticket.student.student_id, ticket)
+                    flashtype, msg, log = ticket.doAfterStudentPayment()
+                    if log is not None:
+                        ticket.student.writeLogMessage(self, log)
+            else:
+                applicant = ticket.__parent__
+                applicant.writeLogMessage(self, log)
+                successful += 1
+                if not waspaid:
+                    write_payments_log(applicant.applicant_id, ticket)
+                    flashtype, msg, log = ticket.doAfterApplicantPayment()
+                    if log is not None:
+                        applicant.writeLogMessage(self, log)
+        self.output = '%s (%s)' % (successful, failed + successful)
+        return
+
+    def render(self):
+        return self.output
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/__init__.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/__init__.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/__init__.py	(revision 17238)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/batching.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/batching.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/batching.py	(revision 17238)
@@ -0,0 +1,37 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""Batch processing components for student objects.
+
+Batch processors eat CSV files to add, update or remove large numbers
+of certain kinds of objects at once.
+
+Here we define the processors for students specific objects like
+students, studycourses, payment tickets and accommodation tickets.
+"""
+from kofacustom.nigeria.students.interfaces import (
+    INigeriaStudent, INigeriaStudentUpdateByRegNo,
+    INigeriaStudentUpdateByMatricNo
+    )
+from waeup.kofa.students.batching import StudentProcessor
+
+class NigeriaStudentProcessor(StudentProcessor):
+    """A batch processor for IStudent objects.
+    """
+    iface = INigeriaStudent
+    iface_byregnumber = INigeriaStudentUpdateByRegNo
+    iface_bymatricnumber = INigeriaStudentUpdateByMatricNo
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/browser.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/browser.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/browser.py	(revision 17238)
@@ -0,0 +1,419 @@
+## $Id$
+##
+## Copyright (C) 2012 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 zope.formlib.textwidgets import BytesDisplayWidget
+from zope.component import getUtility
+from zope.security import checkPermission
+from zope.i18n import translate
+from datetime import datetime
+from waeup.kofa.widgets.datewidget import FriendlyDatetimeDisplayWidget
+from waeup.kofa.interfaces import IExtFileStore, IObjectHistory, IKofaUtils
+from waeup.kofa.browser.layout import action, UtilityView
+from waeup.kofa.utils.helpers import get_current_principal, to_timezone
+from waeup.kofa.students.browser import (
+    StudentPersonalDisplayFormPage, StudentPersonalManageFormPage,
+    StudentClearanceManageFormPage, StudentClearanceEditFormPage,
+    StudentClearanceDisplayFormPage, OnlinePaymentFakeApproveView,
+    ExportPDFClearanceSlip, StudentBaseManageFormPage,
+    StudentBaseDisplayFormPage,
+    StudentBasePDFFormPage,
+    StudentBaseEditFormPage, StudentPersonalEditFormPage,
+    OnlinePaymentDisplayFormPage, OnlinePaymentAddFormPage,
+    OnlinePaymentBreadcrumb, ExportPDFPaymentSlip,
+    ExportPDFCourseRegistrationSlip,
+    ExportPDFBedTicketSlip,
+    StudentFilesUploadPage, emit_lock_message,
+    AccommodationManageFormPage,
+    AccommodationDisplayFormPage,
+    BedTicketAddPage)
+from waeup.kofa.students.interfaces import IStudentsUtils
+from kofacustom.nigeria.students.interfaces import (
+    INigeriaStudentBase, INigeriaStudent, INigeriaStudentPersonal,
+    INigeriaStudentPersonalEdit,
+    INigeriaUGStudentClearance,INigeriaPGStudentClearance,
+    INigeriaStudentOnlinePayment
+    )
+from waeup.kofa.students.workflow import ADMITTED
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+class NigeriaOnlinePaymentBreadcrumb(OnlinePaymentBreadcrumb):
+    """A breadcrumb for payments.
+    """
+    grok.context(INigeriaStudentOnlinePayment)
+
+class NigeriaStudentBaseDisplayFormPage(StudentBaseDisplayFormPage):
+    """ Page to display student base data
+    """
+    form_fields = grok.AutoFields(INigeriaStudentBase).omit(
+        'password', 'suspended', 'suspended_comment',
+        'flash_notice', 'provisionally_cleared')
+    form_fields[
+        'financial_clearance_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+
+class NigeriaStudentBaseManageFormPage(StudentBaseManageFormPage):
+    """ View to manage student base data
+    """
+    form_fields = grok.AutoFields(INigeriaStudentBase).omit(
+        'student_id', 'adm_code', 'suspended',
+        'financially_cleared_by', 'financial_clearance_date')
+
+class NigeriaStudentBaseEditFormPage(StudentBaseEditFormPage):
+    """ View to edit student base data
+    """
+    form_fields = grok.AutoFields(INigeriaStudentBase).select(
+        'email', 'phone')
+
+class NigeriaStudentPersonalDisplayFormPage(StudentPersonalDisplayFormPage):
+    """ Page to display student personal data
+    """
+    form_fields = grok.AutoFields(INigeriaStudentPersonal)
+    form_fields['perm_address'].custom_widget = BytesDisplayWidget
+    form_fields['next_kin_address'].custom_widget = BytesDisplayWidget
+    form_fields[
+        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+
+class NigeriaStudentPersonalEditFormPage(StudentPersonalEditFormPage):
+    """ Page to edit personal data
+    """
+    form_fields = grok.AutoFields(INigeriaStudentPersonalEdit).omit('personal_updated')
+
+class NigeriaStudentPersonalManageFormPage(StudentPersonalManageFormPage):
+    """ Page to edit personal data
+    """
+    form_fields = grok.AutoFields(INigeriaStudentPersonal)
+    form_fields['personal_updated'].for_display = True
+    form_fields[
+        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+
+class NigeriaStudentClearanceDisplayFormPage(StudentClearanceDisplayFormPage):
+    """ Page to display student clearance data
+    """
+
+    @property
+    def form_fields(self):
+        if self.context.is_postgrad:
+            form_fields = grok.AutoFields(
+                INigeriaPGStudentClearance).omit('clearance_locked')
+        else:
+            form_fields = grok.AutoFields(
+                INigeriaUGStudentClearance).omit('clearance_locked')
+        if not getattr(self.context, 'officer_comment'):
+            form_fields = form_fields.omit('officer_comment')
+        else:
+            form_fields['officer_comment'].custom_widget = BytesDisplayWidget
+        return form_fields
+
+class NigeriaExportPDFClearanceSlip(ExportPDFClearanceSlip):
+    """Deliver a PDF slip of the context.
+    """
+    omit_fields = ('password', 'suspended', 'suspended_comment',
+        'phone', 'adm_code', 'email', 'date_of_birth', 'current_level',
+        'flash_notice')
+
+    @property
+    def form_fields(self):
+        if self.context.is_postgrad:
+            form_fields = grok.AutoFields(
+                INigeriaPGStudentClearance).omit('clearance_locked')
+        else:
+            form_fields = grok.AutoFields(
+                INigeriaUGStudentClearance).omit('clearance_locked')
+        if not getattr(self.context, 'officer_comment'):
+            form_fields = form_fields.omit('officer_comment')
+        return form_fields
+
+class NigeriaStudentClearanceManageFormPage(StudentClearanceManageFormPage):
+    """ Page to edit student clearance data
+    """
+
+    @property
+    def form_fields(self):
+        if self.context.is_postgrad:
+            form_fields = grok.AutoFields(
+                INigeriaPGStudentClearance).omit('clr_code')
+        else:
+            form_fields = grok.AutoFields(
+                INigeriaUGStudentClearance).omit('clr_code')
+        return form_fields
+
+class NigeriaStudentClearanceEditFormPage(StudentClearanceEditFormPage):
+    """ View to edit student clearance data by student
+    """
+
+    @property
+    def form_fields(self):
+        if self.context.is_postgrad:
+            form_fields = grok.AutoFields(INigeriaPGStudentClearance).omit(
+            'clearance_locked', 'nysc_location', 'clr_code', 'officer_comment',
+            'physical_clearance_date')
+        else:
+            form_fields = grok.AutoFields(INigeriaUGStudentClearance).omit(
+            'clearance_locked', 'clr_code', 'officer_comment',
+            'physical_clearance_date')
+        return form_fields
+
+class NigeriaExportPDFCourseRegistrationSlip(ExportPDFCourseRegistrationSlip):
+    """Deliver a PDF slip of the context.
+    """
+    omit_fields = ('password', 'suspended', 'suspended_comment',
+        'phone', 'adm_code', 'sex', 'email', 'date_of_birth', 'current_level',
+        'flash_notice')
+
+class NigeriaOnlinePaymentDisplayFormPage(OnlinePaymentDisplayFormPage):
+    """ Page to view an online payment ticket
+    """
+    grok.context(INigeriaStudentOnlinePayment)
+    form_fields = grok.AutoFields(INigeriaStudentOnlinePayment).omit(
+        'provider_amt', 'gateway_amt', 'thirdparty_amt', 'p_item','p_combi')
+    form_fields[
+        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    form_fields[
+        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+
+class NigeriaOnlinePaymentAddFormPage(OnlinePaymentAddFormPage):
+    """ Page to add an online payment ticket
+    """
+    form_fields = grok.AutoFields(INigeriaStudentOnlinePayment).select(
+        'p_combi')
+
+class NigeriaOnlinePaymentFakeApproveView(OnlinePaymentFakeApproveView):
+    """ Disable payment approval view for students.
+
+    This view is used for browser tests only and
+    has to be neutralized here!
+    """
+    grok.name('fake_approve')
+    grok.require('waeup.managePortal')
+
+    def update(self):
+        return
+
+class NigeriaExportPDFPaymentSlip(ExportPDFPaymentSlip):
+    """Deliver a PDF slip of the context.
+    """
+    grok.context(INigeriaStudentOnlinePayment)
+    form_fields = grok.AutoFields(INigeriaStudentOnlinePayment).omit(
+        'provider_amt', 'gateway_amt', 'thirdparty_amt', 'p_item',
+        'p_split_data','p_combi')
+    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    omit_fields = ('password', 'suspended', 'suspended_comment', 'phone',
+        'adm_code', 'sex', 'email', 'date_of_birth', 'current_level',
+        'flash_notice')
+
+    def update(self):
+        if not self.context.p_state in ('paid', 'waived', 'scholarship') \
+            and not self.context.r_company:
+            self.redirect(self.url(self.context))
+            return
+        return
+
+class NigeriaAccommodationDisplayFormPage(AccommodationDisplayFormPage):
+    """ Page to view bed tickets.
+    """
+    with_hostel_selection = False
+
+class NigeriaAccommodationManageFormPage(AccommodationManageFormPage):
+    """ Page to manage bed tickets.
+    This manage form page is for both students and students officers.
+    """
+    with_hostel_selection = False
+
+class NigeriaBedTicketAddPage(BedTicketAddPage):
+    """ Page to add a bed ticket
+    """
+    with_ac = True
+    with_bedselection = False
+
+class NigeriaExportPDFBedTicketSlip(ExportPDFBedTicketSlip):
+    """Deliver a PDF slip of the context.
+    """
+    omit_fields = ('password', 'suspended', 'suspended_comment',
+        'phone', 'adm_code', 'email', 'date_of_birth', 'current_level',
+        'flash_notice')
+
+class NigeriaStudentFilesUploadPage(StudentFilesUploadPage):
+    """ View to upload passport picture.
+
+    Students are not allowed to change the picture if they
+    passed the regular Kofa application.
+    """
+
+    def update(self):
+        # Passport pictures must not be editable if application slip
+        # exists.
+        slip = getUtility(IExtFileStore).getFileByContext(
+            self.context, 'application_slip')
+        PORTRAIT_CHANGE_STATES = getUtility(IStudentsUtils).PORTRAIT_CHANGE_STATES
+        if self.context.state not in PORTRAIT_CHANGE_STATES or slip is not None:
+            emit_lock_message(self)
+            return
+        return
+
+class ClearStudentFinancially(UtilityView, grok.View):
+    """ Clear student financially by financial clearance officer
+    """
+    grok.context(INigeriaStudent)
+    grok.name('clear_financially')
+    grok.require('waeup.clearStudentFinancially')
+
+    def update(self):
+        if self.context.financially_cleared_by:
+            self.flash(_('This student has already been financially cleared.'),
+                       type="danger")
+            self.redirect(self.url(self.context))
+            return
+        user = get_current_principal()
+        if user is None:
+            usertitle = 'system'
+        else:
+            usertitle = getattr(user, 'public_name', None)
+            if not usertitle:
+                usertitle = user.title
+        self.context.financially_cleared_by = usertitle
+        self.context.financial_clearance_date = datetime.utcnow()
+        self.context.writeLogMessage(self,'financially cleared')
+        history = IObjectHistory(self.context)
+        history.addMessage('Financially cleared')
+        self.flash(_('Student has been financially cleared.'))
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+class WithdrawFinancialClearance(UtilityView, grok.View):
+    """ Withdraw financial clearance by financial clearance officer
+    """
+    grok.context(INigeriaStudent)
+    grok.name('withdraw_financial_clearance')
+    grok.require('waeup.clearStudentFinancially')
+
+    def update(self):
+        if not self.context.financially_cleared_by:
+            self.flash(_('This student has not yet been financially cleared.'),
+                       type="danger")
+            self.redirect(self.url(self.context))
+            return
+        self.context.financially_cleared_by = None
+        self.context.financial_clearance_date = None
+        self.context.writeLogMessage(self,'financial clearance withdrawn')
+        history = IObjectHistory(self.context)
+        history.addMessage('Financial clearance withdrawn')
+        self.flash(_('Financial clearance withdrawn.'))
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+cleared_note = """
+<br /><br /><br />
+<strong>Financially cleared on %s by %s.</strong>
+
+"""
+
+class NigeriaExportPDFFinancialClearancePage(UtilityView, grok.View):
+    """Deliver a PDF financial clearance slip.
+    """
+    grok.context(INigeriaStudent)
+    grok.name('fee_payment_history.pdf')
+    grok.require('waeup.viewStudent')
+    prefix = 'form'
+
+    omit_fields = (
+        'suspended', 'phone',
+        'adm_code', 'suspended_comment',
+        'date_of_birth', 'current_level',
+        'flash_notice')
+
+    form_fields = None
+
+    @property
+    def label(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        return translate(_('Fee Payment History for'),
+            'waeup.kofa', target_language=portal_language) \
+            + ' %s' % self.context.display_fullname
+
+    def _sigsInFooter(self):
+        if not checkPermission('waeup.clearStudentFinancially', self.context):
+            return ()
+        return (_('Date, Checking Officer Signature'),
+                _('Date, Approving Officer Signature'),
+                )
+
+    @property
+    def note(self):
+        if self.context.financially_cleared_by:
+            tz = getUtility(IKofaUtils).tzinfo
+            try:
+                timestamp = to_timezone(
+                    self.context.financial_clearance_date, tz).strftime(
+                        "%Y-%m-%d %H:%M:%S")
+            except ValueError:
+                return
+            return cleared_note % (
+                timestamp, self.context.financially_cleared_by)
+        return
+
+    @property
+    def tabletitle(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        tabletitle = []
+        tabletitle.append(translate(_('Successful Payments'), 'waeup.kofa',
+            target_language=portal_language))
+        return tabletitle
+
+    def render(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        P_ID = translate(_('Payment Id'), 'waeup.kofa', target_language=portal_language)
+        #CD = translate(_('Creation Date'), 'waeup.kofa', target_language=portal_language)
+        PD = translate(_('Payment Date'), 'waeup.kofa', target_language=portal_language)
+        CAT = translate(_('Payment Category'), 'waeup.kofa', target_language=portal_language)
+        ITEM = translate(_('Payment Item'), 'waeup.kofa', target_language=portal_language)
+        AMT = translate(_('Amount (Naira)'), 'waeup.kofa', target_language=portal_language)
+        SSS = translate(_('Payment Session'), 'waeup.kofa', target_language=portal_language)
+        studentview = StudentBasePDFFormPage(self.context.student,
+            self.request, self.omit_fields)
+        students_utils = getUtility(IStudentsUtils)
+
+        tabledata = []
+        tableheader = []
+        tabledata.append(sorted(
+            [value for value in self.context['payments'].values()
+             if value.p_state in ('paid', 'waived', 'scholarship')],
+             key=lambda value: value.p_session))
+        tableheader.append([(P_ID,'p_id', 4.2),
+                         #(CD,'creation_date', 3),
+                         (PD,'formatted_p_date', 3),
+                         (CAT,'category', 3),
+                         (ITEM, 'p_item', 3),
+                         (AMT, 'amount_auth', 2),
+                         (SSS, 'p_session', 2),
+                         ])
+        return students_utils.renderPDF(
+            self, 'financial_clearance_slip.pdf',
+            self.context.student, studentview,
+            tableheader=tableheader,
+            tabledata=tabledata,
+            signatures=None,
+            sigs_in_footer=self._sigsInFooter(),
+            omit_fields=self.omit_fields,
+            note=self.note
+            )
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/export.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/export.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/export.py	(revision 17238)
@@ -0,0 +1,181 @@
+## $Id$
+##
+## Copyright (C) 2011 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
+##
+"""Exporters for student related stuff.
+"""
+import grok
+from waeup.kofa.interfaces import REQUESTED
+from kofacustom.nigeria.students.interfaces import (
+    INigeriaStudent,
+    INigeriaStudentStudyCourse,
+    INigeriaStudentStudyLevel,
+    INigeriaCourseTicket,
+    INigeriaStudentOnlinePayment)
+from waeup.kofa.students.export import (
+    StudentExporter,
+    StudentStudyCourseExporter,
+    StudentStudyLevelExporter,
+    CourseTicketExporter,
+    StudentPaymentExporter,
+    DataForBursaryExporter,
+    TrimmedDataExporter)
+from waeup.kofa.utils.helpers import iface_names
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+class NigeriaStudentExporter(StudentExporter):
+    """Exporter for Students.
+    """
+
+    fields = tuple(sorted(iface_names(
+        INigeriaStudent, omit=['loggerInfo']))) + (
+        'password', 'state', 'history', 'certcode', 'is_postgrad',
+        'current_level', 'current_session')
+
+    def mangle_value(self, value, name, context=None):
+        if '_result' in name and value:
+            value = [eval(entry.to_string()) for entry in value]
+        return super(
+            NigeriaStudentExporter, self).mangle_value(
+            value, name, context=context)
+
+class NigeriaTrimmedDataExporter(TrimmedDataExporter):
+    """The Student Trimmed Data Exporter first filters the set of students
+    by searching the students catalog. Then it exports a trimmed data set
+    of this set of students.
+    """
+
+    fields = (
+        'student_id',
+        'matric_number',
+        'reg_number',
+        'firstname',
+        'middlename',
+        'lastname',
+        'sex',
+        'email',
+        'phone',
+        'nationality',
+        'date_of_birth',
+        'state',
+        'current_mode',
+        'certcode',
+        'faccode',
+        'depcode',
+        'current_level',
+        'current_session',
+        'current_verdict',
+        'entry_session',
+        'lg_state',
+        'lg_area')
+
+    def mangle_value(self, value, name, context=None):
+        if name == 'lg_state' and context.lga:
+            value = context.lga.split('_')[0]
+        if name == 'lg_area' and context.lga:
+            value = '-'.join(context.lga.split('_')[1:])
+        return super(
+            NigeriaTrimmedDataExporter, self).mangle_value(
+            value, name, context=context)
+
+class NigeriaStudentStudyCourseExporter(StudentStudyCourseExporter):
+    """Exporter for StudentStudyCourses.
+    """
+
+    fields = tuple(
+        sorted(iface_names(INigeriaStudentStudyCourse))) + (
+            'student_id', 'previous')
+
+class NigeriaStudentStudyLevelExporter(StudentStudyLevelExporter):
+    """Exporter for StudentStudyLevels.
+    """
+    fields = tuple(sorted(iface_names(
+        INigeriaStudentStudyLevel))) + (
+        'student_id', 'number_of_tickets','certcode', 'previous')
+
+class NigeriaCourseTicketExporter(CourseTicketExporter):
+    """Exporter for CourseTickets.
+    """
+
+    fields = tuple(sorted(iface_names(INigeriaCourseTicket) +
+        ['level', 'code', 'level_session'])) + ('student_id',
+        'certcode', 'display_fullname', 'previous')
+
+class NigeriaStudentPaymentExporter(StudentPaymentExporter):
+    """Exporter for OnlinePayment instances.
+    """
+
+    fields = tuple(
+        sorted(iface_names(
+            INigeriaStudentOnlinePayment, exclude_attribs=False,
+            omit=['display_item','formatted_p_date']))) + (
+            'student_id','state','current_session')
+
+class NigeriaDataForBursaryExporter(DataForBursaryExporter):
+    """Exporter for bursary data.
+    """
+
+    fields = tuple(
+        sorted(iface_names(
+            INigeriaStudentOnlinePayment, exclude_attribs=False,
+            omit=['display_item', 'certificate', 'student']))) + (
+            'student_id','matric_number','reg_number',
+            'firstname', 'middlename', 'lastname','sex',
+            'state','current_session',
+            'entry_session', 'entry_mode',
+            'faccode', 'depcode','certcode','lga')
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler fetches the student data.
+        """
+        if context is not None:
+            student = context.student
+            if name in [
+                'student_id','matric_number', 'reg_number',
+                'firstname', 'middlename', 'lastname','sex',
+                'state', 'current_session',
+                'entry_session', 'entry_mode',
+                'faccode', 'depcode', 'certcode',
+                'lga'] and student is not None:
+                value = getattr(student, name, None)
+        return super(
+            StudentPaymentExporter, self).mangle_value(
+            value, name, context=context)
+
+class ClearanceRequestedStudentExporter(StudentExporter):
+    """Exporter of data used to assign physical clearance dates.
+    """
+
+    grok.name('clearancerequested')
+
+    fields = ('student_id', 'reg_number', 'display_fullname',
+              'state', 'current_session', 'history', 'physical_clearance_date',
+              'email','phone')
+
+    title = _(u'Clearance Invitation Data')
+
+    def filter_func(self, x, **kw):
+        # We use this method to post-filter students in state
+        # 'clearance_requested'
+        return [student for student in x if student.state == REQUESTED]
+
+    def mangle_value(self, value, name, context=None):
+        if name == 'history':
+            # Only show last mesage
+            value = value.messages[-1]
+        return super(
+            StudentExporter, self).mangle_value(
+            value, name, context=context)
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/interfaces.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/interfaces.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/interfaces.py	(revision 17238)
@@ -0,0 +1,572 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+from zope import schema
+from zope.interface import Attribute, invariant, Invalid
+from waeup.kofa.schema import TextLineChoice
+from waeup.kofa.interfaces import SimpleKofaVocabulary, academic_sessions_vocab
+from waeup.kofa.schema import FormattedDate, PhoneNumber
+from waeup.kofa.schoolgrades import ResultEntryField
+from waeup.kofa.students.vocabularies import nats_vocab, StudyLevelSource
+from waeup.kofa.students.interfaces import (
+    IStudentBase, IUGStudentClearance, IPGStudentClearance,
+    IStudentPersonal, IStudentNavigation, IStudentStudyLevel,
+    IStudentStudyCourse, ICourseTicket
+    )
+from waeup.kofa.students.vocabularies import (
+    nats_vocab, contextual_reg_num_source)
+from kofacustom.nigeria.interfaces import (
+    high_qual, high_grade, exam_types, LGASource)
+from kofacustom.nigeria.interfaces import MessageFactory as _
+from kofacustom.nigeria.payments.interfaces import INigeriaOnlinePayment
+
+
+class INigeriaStudentBase(IStudentBase):
+    """Representation of student base data.
+
+    """
+
+    reg_number = TextLineChoice(
+        title = _(u'Registration Number'),
+        required = False,
+        readonly = False,
+        source = contextual_reg_num_source,
+        )
+
+    is_staff = schema.Bool(
+        title = _(u'Staff Member'),
+        required = False,
+        readonly = False,
+        default = False,
+        )
+
+    financially_cleared_by = schema.TextLine(
+        title = _(u'Financially Cleared by'),
+        default = None,
+        required = False,
+        )
+
+    financial_clearance_date = schema.Datetime(
+        title = _(u'Financial Clearance Date'),
+        required = False,
+        readonly = False,
+        )
+
+    provisionally_cleared = schema.Bool(
+        title = _(u'Provisionally Cleared'),
+        default = False,
+        required = False,
+        description = u'Student will be able to edit clearance data if set.',
+        )
+
+INigeriaStudentBase['reg_number'].order = IStudentBase[
+    'reg_number'].order
+
+
+class INigeriaUGStudentClearance(IUGStudentClearance):
+    """Representation of ug student clearance data.
+
+    """
+    officer_comment = schema.Text(
+        title = _(u"Officer's Comment"),
+        required = False,
+        )
+
+    physical_clearance_date = schema.TextLine(
+        title = _(u"Physical Clearance Date"),
+        required = False,
+        )
+
+    clr_code = schema.TextLine(
+        title = _(u'CLR Activation Code'),
+        required = False,
+        readonly = False,
+        )
+
+    date_of_birth = FormattedDate(
+        title = _(u'Date of Birth'),
+        required = False,
+        show_year = True,
+        )
+
+    nationality = schema.Choice(
+        source = nats_vocab,
+        title = _(u'Nationality'),
+        required = True,
+        )
+
+    lga = schema.Choice(
+        source = LGASource(),
+        title = _(u'State / LGA'),
+        required = False,
+        )
+
+    def_adm = schema.Bool(
+        title = _(u'Deferment of Admission'),
+        required = False,
+        readonly = False,
+        default = False,
+        )
+
+    fst_sit_fname = schema.TextLine(
+        title = _(u'Full Name'),
+        required = False,
+        readonly = False,
+        )
+    fst_sit_no = schema.TextLine(
+        title = _(u'Exam Number'),
+        required = False,
+        readonly = False,
+        )
+
+    fst_sit_date = FormattedDate(
+        title = _(u'Exam Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+
+    fst_sit_type = schema.Choice(
+        title = _(u'Exam Type'),
+        required = False,
+        readonly = False,
+        vocabulary = exam_types,
+        )
+
+    fst_sit_results = schema.List(
+        title = _(u'Exam Results'),
+        value_type = ResultEntryField(),
+        required = False,
+        readonly = False,
+        defaultFactory=list,
+        )
+
+    scd_sit_fname = schema.TextLine(
+        title = _(u'Full Name'),
+        required = False,
+        readonly = False,
+        )
+    scd_sit_no = schema.TextLine(
+        title = _(u'Exam Number'),
+        required = False,
+        readonly = False,
+        )
+
+    scd_sit_date = FormattedDate(
+        title = _(u'Exam Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+
+    scd_sit_type = schema.Choice(
+        title = _(u'Exam Type'),
+        required = False,
+        readonly = False,
+        vocabulary = exam_types,
+        )
+
+    scd_sit_results = schema.List(
+        title = _(u'Exam Results'),
+        value_type = ResultEntryField(),
+        required = False,
+        readonly = False,
+        defaultFactory=list,
+        )
+
+    alr_fname = schema.TextLine(
+        title = _(u'Full Name'),
+        required = False,
+        readonly = False,
+        )
+    alr_no = schema.TextLine(
+        title = _(u'Exam Number'),
+        required = False,
+        readonly = False,
+        )
+
+    alr_date = FormattedDate(
+        title = _(u'Exam Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+
+    alr_results = schema.List(
+        title = _(u'Exam Results'),
+        value_type = ResultEntryField(),
+        required = False,
+        readonly = False,
+        defaultFactory=list,
+        )
+
+    hq_type = schema.Choice(
+        title = _(u'Qualification Obtained'),
+        required = False,
+        readonly = False,
+        vocabulary = high_qual,
+        )
+
+    hq_fname = schema.TextLine(
+        title = _(u'Full Name'),
+        required = False,
+        readonly = False,
+        )
+
+    hq_matric_no = schema.TextLine(
+        title = _(u'Former Matric Number'),
+        required = False,
+        readonly = False,
+        )
+
+    hq_degree = schema.Choice(
+        title = _(u'Class of Degree'),
+        required = False,
+        readonly = False,
+        vocabulary = high_grade,
+        )
+
+    hq_school = schema.TextLine(
+        title = _(u'Institution Attended'),
+        required = False,
+        readonly = False,
+        )
+
+    hq_session = schema.TextLine(
+        title = _(u'Years Attended'),
+        required = False,
+        readonly = False,
+        )
+
+    hq_disc = schema.TextLine(
+        title = _(u'Discipline'),
+        required = False,
+        readonly = False,
+        )
+
+    @invariant
+    def check_lga_nationality(student):
+        if student.nationality != 'NG' and student.lga not in ('foreigner', None):
+            raise Invalid(_('Nationalty and LGA are contradictory.'))
+        if student.nationality == 'NG' and student.lga == 'foreigner':
+            raise Invalid(_('Nationalty and LGA are contradictory.'))
+
+
+class INigeriaPGStudentClearance(INigeriaUGStudentClearance):
+    """Representation of pg student clearance data.
+
+    """
+
+    hq2_type = schema.Choice(
+        title = _(u'Qualification Obtained'),
+        required = False,
+        readonly = False,
+        vocabulary = high_qual,
+        )
+
+    hq2_matric_no = schema.TextLine(
+        title = _(u'Former Matric Number'),
+        required = False,
+        readonly = False,
+        )
+
+    hq2_degree = schema.Choice(
+        title = _(u'Class of Degree'),
+        required = False,
+        readonly = False,
+        vocabulary = high_grade,
+        )
+
+    hq2_school = schema.TextLine(
+        title = _(u'Institution Attended'),
+        required = False,
+        readonly = False,
+        )
+
+    hq2_session = schema.TextLine(
+        title = _(u'Years Attended'),
+        required = False,
+        readonly = False,
+        )
+
+    hq2_disc = schema.TextLine(
+        title = _(u'Discipline'),
+        required = False,
+        readonly = False,
+        )
+
+    nysc_year = schema.Int(
+        title = _(u'Nysc Year'),
+        required = False,
+        readonly = False,
+        )
+
+    nysc_location = schema.TextLine(
+        title = _(u'Nysc Location'),
+        required = False,
+        )
+
+    nysc_lga = schema.Choice(
+        source = LGASource(),
+        title = _(u'Nysc LGA'),
+        required = False,
+        )
+
+    employer = schema.TextLine(
+        title = _(u'Employer'),
+        required = False,
+        readonly = False,
+        )
+
+    emp_position = schema.TextLine(
+        title = _(u'Employer Position'),
+        required = False,
+        readonly = False,
+        )
+
+    emp_start = FormattedDate(
+        title = _(u'Start Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+
+    emp_end = FormattedDate(
+        title = _(u'End Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+
+    emp_reason = schema.TextLine(
+        title = _(u'Reason for Leaving'),
+        required = False,
+        readonly = False,
+        )
+
+    employer2 = schema.TextLine(
+        title = _(u'2nd Employer'),
+        required = False,
+        readonly = False,
+        )
+
+    emp2_position = schema.TextLine(
+        title = _(u'2nd Employer Position'),
+        required = False,
+        readonly = False,
+        )
+
+    emp2_start = FormattedDate(
+        title = _(u'Start Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+    emp2_end = FormattedDate(
+        title = _(u'End Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+
+    emp2_reason = schema.TextLine(
+        title = _(u'Reason for Leaving'),
+        required = False,
+        readonly = False,
+        )
+
+    former_matric = schema.TextLine(
+        title = _(u'If yes, matric number'),
+        required = False,
+        readonly = False,
+        )
+
+class INigeriaStudentPersonal(IStudentPersonal):
+    """Student personal data.
+
+    """
+
+    is_foreigner = Attribute('True if student is non-Nigerian')
+
+    next_kin_name = schema.TextLine(
+        title = _(u'Next of Kin Name'),
+        required = False,
+        readonly = False,
+        )
+
+    next_kin_relation = schema.TextLine(
+        title = _(u'Next of Kin Relationship'),
+        required = False,
+        readonly = False,
+        )
+
+    next_kin_address = schema.Text(
+        title = _(u'Next of Kin Address'),
+        required = False,
+        readonly = False,
+        )
+
+    next_kin_phone = PhoneNumber(
+        title = _(u'Next of Kin Phone'),
+        description = u'',
+        required = False,
+        readonly = False,
+        )
+
+    marit_stat = schema.Choice(
+        title = u'Marital Status',
+        default = 'unmarried',
+        required = False,
+        vocabulary = SimpleKofaVocabulary(
+            (_('Unmarried'), 'unmarried'),
+            (_('Married'), 'married'),)
+        )
+
+    religion = schema.Choice(
+        title = u'Religion',
+        default = 'no_say',
+        required = False,
+        vocabulary = SimpleKofaVocabulary(
+            (_('Muslim'), 'muslim'),
+            (_('Christian'), 'christian'),
+            (_('Others'), 'others'),
+            (_('Prefer not to say'), 'no_say'),)
+        )
+
+    disabled = schema.Bool(
+        title = u'Disabled',
+        default = False,
+        required = False,
+        )
+
+
+class INigeriaStudent(INigeriaStudentBase,INigeriaUGStudentClearance,
+    INigeriaPGStudentClearance,INigeriaStudentPersonal):
+    """Representation of a student.
+
+    """
+
+class INigeriaStudentPersonalEdit(INigeriaStudentPersonal):
+    """Interface for editing personal data by students.
+
+    Here we can repeat the fields from IStudentPersonal and set the
+    `required` if necessary.
+    """
+
+    perm_address = schema.Text(
+        title = _(u'Permanent Address'),
+        required = True,
+        )
+
+    next_kin_name = schema.TextLine(
+        title = _(u'Next of Kin Name'),
+        required = True,
+        readonly = False,
+        )
+
+    next_kin_relation = schema.TextLine(
+        title = _(u'Next of Kin Relationship'),
+        required = True,
+        readonly = False,
+        )
+
+    next_kin_address = schema.Text(
+        title = _(u'Next of Kin Address'),
+        required = True,
+        readonly = False,
+        )
+
+    next_kin_phone = PhoneNumber(
+        title = _(u'Next of Kin Phone'),
+        description = u'',
+        required = True,
+        readonly = False,
+        )
+
+INigeriaStudentPersonalEdit['perm_address'].order = INigeriaStudentPersonal[
+    'perm_address'].order
+INigeriaStudentPersonalEdit['next_kin_name'].order = INigeriaStudentPersonal[
+    'next_kin_name'].order
+INigeriaStudentPersonalEdit['next_kin_relation'].order = INigeriaStudentPersonal[
+    'next_kin_relation'].order
+INigeriaStudentPersonalEdit['next_kin_address'].order = INigeriaStudentPersonal[
+    'next_kin_address'].order
+INigeriaStudentPersonalEdit['next_kin_phone'].order = INigeriaStudentPersonal[
+    'next_kin_phone'].order
+
+class INigeriaStudentStudyCourse(IStudentStudyCourse):
+    """A container for student study levels.
+
+    """
+
+class INigeriaStudentStudyLevel(IStudentStudyLevel):
+    """A container for course tickets.
+
+    """
+
+class INigeriaStudentOnlinePayment(INigeriaOnlinePayment):
+    """A student payment via payment gateways.
+
+    This Interface does not inherit from IStudentOnlinePayment.
+    Thus all fields from IStudentOnlinePayment have to be repeated here.
+    """
+
+    formatted_p_date = Attribute("Formatted pyment date")
+
+    p_current = schema.Bool(
+        title = _(u'Current Session Payment'),
+        default = True,
+        required = False,
+        )
+
+    p_level = schema.Choice(
+        title = _(u'Payment Level'),
+        source = StudyLevelSource(),
+        required = False,
+        )
+
+    p_split_data = schema.Text(
+        title = _(u'Split Payment Data'),
+        required = False,
+        readonly = False,
+        )
+
+INigeriaStudentOnlinePayment['p_level'].order = INigeriaStudentOnlinePayment[
+    'p_session'].order
+
+class INigeriaCourseTicket(ICourseTicket):
+    """A course ticket.
+
+    """
+
+class INigeriaStudentUpdateByRegNo(INigeriaStudent):
+    """Representation of a student. Skip regular reg_number validation.
+
+    """
+    reg_number = schema.TextLine(
+        title = _(u'Registration Number'),
+        required = False,
+        )
+
+class INigeriaStudentUpdateByMatricNo(INigeriaStudent):
+    """Representation of a student. Skip regular matric_number validation.
+
+    """
+    matric_number = schema.TextLine(
+        title = _(u'Matriculation Number'),
+        required = False,
+        )
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/payments.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/payments.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/payments.py	(revision 17238)
@@ -0,0 +1,77 @@
+## $Id$
+##
+## Copyright (C) 2011 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
+##
+"""
+Student payment components.
+"""
+import grok
+from datetime import datetime
+from zope.component import getUtility
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.kofa.interfaces import IKofaUtils
+from waeup.kofa.students.interfaces import IStudentNavigation
+from waeup.kofa.students.payments import (
+    StudentOnlinePayment, StudentOnlinePaymentFactory)
+from waeup.kofa.utils.helpers import attrs_to_fields, to_timezone
+from kofacustom.nigeria.students.interfaces import INigeriaStudentOnlinePayment
+
+class NigeriaStudentOnlinePayment(StudentOnlinePayment):
+    """This is a custom online payment for students.
+
+    Since this class inherits from StudentOnlinePayment, it automatically
+    implements the interfaces of the parent class which means
+    all viewlets and views meant for StudentOnlinePayment
+    can also be accessed in the customized system.
+    """
+    grok.implements(INigeriaStudentOnlinePayment, IStudentNavigation)
+    grok.provides(INigeriaStudentOnlinePayment)
+
+    def __init__(self):
+        super(NigeriaStudentOnlinePayment, self).__init__()
+        return
+
+    @property
+    def student(self):
+        return self.__parent__.__parent__
+
+
+    @property
+    def formatted_p_date(self):
+        if isinstance(self.payment_date, datetime):
+            tz = getUtility(IKofaUtils).tzinfo
+            try:
+                timestamp = to_timezone(
+                    self.payment_date, tz).strftime("%Y-%m-%d %H:%M:%S")
+            except ValueError:
+                return None
+            return timestamp
+        else:
+            return None
+
+NigeriaStudentOnlinePayment = attrs_to_fields(NigeriaStudentOnlinePayment,
+    omit=['display_item',])
+
+class NigeriaStudentOnlinePaymentFactory(StudentOnlinePaymentFactory):
+    """A factory for student online payments.
+    """
+
+    def __call__(self, *args, **kw):
+        return NigeriaStudentOnlinePayment()
+
+    def getInterfaces(self):
+        return implementedBy(NigeriaStudentOnlinePayment)
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/permissions.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/permissions.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/permissions.py	(revision 17238)
@@ -0,0 +1,39 @@
+## $Id$
+##
+## Copyright (C) 2011 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
+##
+"""
+More permissions and roles for the students section.
+"""
+import grok
+
+
+class ClearStudentFinancially(grok.Permission):
+    """The ClearStudentFinancially permission is needed to clear students
+    financially or to reject financial clearance. This permission is
+    meant for financial clearance officers.
+    """
+    grok.name('waeup.clearStudentFinancially')
+
+
+class FinancialClearanceOfficer(grok.Role):
+    """Financial Clearance Officers are also allowed to clear
+    students financially. Usually this role is assigned together
+    with the BursaryOfficer role and the StudentsOfficer role.
+    """
+    grok.name('waeup.FinancialClearanceOfficer')
+    grok.title(u'Financial Clearance Officer')
+    grok.permissions('waeup.clearStudentFinancially')
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/student.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/student.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/student.py	(revision 17238)
@@ -0,0 +1,64 @@
+## $Id$
+##
+## Copyright (C) 2011 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
+##
+"""
+Container for the various objects owned by students.
+"""
+import grok
+from zope.interface import implementedBy
+from waeup.kofa.utils.helpers import attrs_to_fields
+from waeup.kofa.students.student import Student, StudentFactory
+from waeup.kofa.interfaces import CLEARANCE
+from kofacustom.nigeria.students.interfaces import (
+    INigeriaStudent, IStudentNavigation, INigeriaStudentPersonalEdit)
+
+
+class NigeriaStudent(Student):
+    """This is a student container for the various objects
+    owned by students.
+    """
+    grok.implements(
+        INigeriaStudent, INigeriaStudentPersonalEdit, IStudentNavigation)
+    grok.provides(INigeriaStudent)
+
+    @property
+    def is_foreigner(self):
+        return getattr(self, 'nationality') != 'NG'
+
+    @property
+    def transcript_enabled(self):
+        return False
+
+    @property
+    def clearance_locked(self):
+        return self.state != CLEARANCE and not self.provisionally_cleared
+
+# Set all attributes of Student required in IStudent as field
+# properties. Doing this, we do not have to set initial attributes
+# ourselves and as a bonus we get free validation when an attribute is
+# set.
+NigeriaStudent = attrs_to_fields(NigeriaStudent)
+
+class NigeriaStudentFactory(StudentFactory):
+    """A factory for students.
+    """
+
+    def __call__(self, *args, **kw):
+        return NigeriaStudent()
+
+    def getInterfaces(self):
+        return implementedBy(NigeriaStudent)
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/studycourse.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/studycourse.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/studycourse.py	(revision 17238)
@@ -0,0 +1,73 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""
+Container which holds the data of the student study courses
+and contains the (student) study level objects.
+"""
+import grok
+from zope.interface import implementedBy
+from waeup.kofa.students.studycourse import (
+    StudentStudyCourse, StudentStudyCourseFactory)
+from waeup.kofa.utils.helpers import attrs_to_fields
+from waeup.kofa.students.workflow import CLEARED, RETURNING, PAID
+from kofacustom.nigeria.students.interfaces import (
+    INigeriaStudentStudyCourse, IStudentNavigation)
+
+class NigeriaStudentStudyCourse(StudentStudyCourse):
+    """This is a container for study levels.
+    """
+
+    grok.implements(INigeriaStudentStudyCourse, IStudentNavigation)
+    grok.provides(INigeriaStudentStudyCourse)
+
+    @property
+    def next_session_allowed(self):
+        state = self.student.state
+        certificate = getattr(self, 'certificate', None)
+        if certificate == None:
+            return False
+        if state == CLEARED:
+            return True
+        cond0 = state == RETURNING
+        cond1 = self.current_verdict in (
+            'A','B','C','F','J','L','M','N','O','X','Z')
+        cond2 = self.current_level in (0, 100)
+        cond3 = certificate.study_mode.startswith('de') and \
+            self.current_level == 200
+        cond4 = certificate.study_mode.startswith('ph') and \
+            self.current_level == 300
+        if cond0 and (cond1 or cond2 or cond3 or cond4):
+            return True
+        cond5 = self.is_postgrad
+        cond6 = state == PAID
+        cond7 = self.is_special_postgrad
+        if  cond5 and cond6 and not cond7:
+            return True
+        return False
+
+NigeriaStudentStudyCourse = attrs_to_fields(NigeriaStudentStudyCourse)
+
+class NigeriaStudentStudyCourseFactory(StudentStudyCourseFactory):
+    """A factory for student study courses.
+    """
+
+    def __call__(self, *args, **kw):
+        return NigeriaStudentStudyCourse()
+
+    def getInterfaces(self):
+        return implementedBy(NigeriaStudentStudyCourse)
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/studylevel.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/studylevel.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/studylevel.py	(revision 17238)
@@ -0,0 +1,75 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""
+Container which holds the data of a student study level
+and contains the course tickets.
+"""
+import grok
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.kofa.utils.helpers import attrs_to_fields
+from waeup.kofa.students.studylevel import (
+    StudentStudyLevel, CourseTicket,
+    CourseTicketFactory, StudentStudyLevelFactory)
+from kofacustom.nigeria.students.interfaces import (
+    INigeriaStudentStudyLevel, IStudentNavigation, INigeriaCourseTicket)
+
+
+class NigeriaStudentStudyLevel(StudentStudyLevel):
+    """This is a container for course tickets.
+    """
+    grok.implements(INigeriaStudentStudyLevel, IStudentNavigation)
+    grok.provides(INigeriaStudentStudyLevel)
+
+NigeriaStudentStudyLevel = attrs_to_fields(
+    NigeriaStudentStudyLevel, omit=['total_credits', 'gpa'])
+
+class NigeriaStudentStudyLevelFactory(StudentStudyLevelFactory):
+    """A factory for student study levels.
+    """
+
+    def __call__(self, *args, **kw):
+        return NigeriaStudentStudyLevel()
+
+    def getInterfaces(self):
+        return implementedBy(NigeriaStudentStudyLevel)
+
+class NigeriaCourseTicket(CourseTicket):
+    """This is a course ticket which allows the
+    student to attend the course. Lecturers will enter scores and more at
+    the end of the term.
+
+    A course ticket contains a copy of the original course and
+    course referrer data. If the courses and/or their referrers are removed, the
+    corresponding tickets remain unchanged. So we do not need any event
+    triggered actions on course tickets.
+    """
+    grok.implements(INigeriaCourseTicket, IStudentNavigation)
+    grok.provides(INigeriaCourseTicket)
+
+NigeriaCourseTicket = attrs_to_fields(NigeriaCourseTicket)
+
+class NigeriaCourseTicketFactory(CourseTicketFactory):
+    """A factory for student study levels.
+    """
+
+    def __call__(self, *args, **kw):
+        return NigeriaCourseTicket()
+
+    def getInterfaces(self):
+        return implementedBy(NigeriaCourseTicket)
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/tests/__init__.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/tests/__init__.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/tests/__init__.py	(revision 17238)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/tests/sample_student_data.csv
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/tests/sample_student_data.csv	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/tests/sample_student_data.csv	(revision 17238)
@@ -0,0 +1,4 @@
+firstname,lastname,reg_number,date_of_birth,matric_number,email,phone,sex,nationality,fst_sit_results
+Aaren,Pieri,1,,100000,aa@aa.ng,1234,m,DE,"[('visual_art','A1'),('applied_electricity','C6')]"
+Aaren,Finau,2,,100001,aa@aa.ng,1234,m,NG,[]
+Aaren,Berson,3,,100002,aa@aa.ng,1234,m,FR,[]
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/tests/test_browser.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/tests/test_browser.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/tests/test_browser.py	(revision 17238)
@@ -0,0 +1,481 @@
+## $Id$
+##
+## Copyright (C) 2011 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 os
+import shutil
+import tempfile
+from time import time
+from datetime import datetime, timedelta
+from StringIO import StringIO
+from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo
+from zope.component.hooks import setSite, clearSite
+from zope.component import getUtility, createObject
+from zope.interface import verify
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from waeup.kofa.app import University
+from waeup.kofa.students.tests.test_browser import (
+    StudentsFullSetup, SAMPLE_IMAGE)
+from waeup.kofa.testing import FunctionalTestCase
+from waeup.kofa.interfaces import (
+    IExtFileStore, IFileStoreNameChooser)
+from waeup.kofa.schoolgrades import ResultEntry
+from waeup.kofa.students.batching import StudentProcessor
+from waeup.kofa.students.interfaces import IStudentsUtils
+from waeup.kofa.browser.tests.test_pdf import samples_dir
+from waeup.kofa.tests.test_authentication import SECRET
+from kofacustom.nigeria.students.batching import NigeriaStudentProcessor
+from kofacustom.nigeria.testing import FunctionalLayer
+from kofacustom.nigeria.utils.utils import NigeriaKofaUtils
+from kofacustom.nigeria.students.interfaces import (
+    INigeriaStudentStudyCourse, INigeriaStudent,
+    INigeriaStudentStudyLevel, INigeriaCourseTicket)
+
+
+STUDENT_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_student_data.csv'),
+    'rb').read()
+
+STUDENT_HEADER_FIELDS = STUDENT_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+class StudentProcessorTest(FunctionalTestCase):
+    """Perform some batching tests.
+    """
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentProcessorTest, self).setUp()
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        # we add the site immediately after creation to the
+        # ZODB. Catalogs and other local utilities are not setup
+        # before that step.
+        self.app = self.getRootFolder()['app']
+        # Set site here. Some of the following setup code might need
+        # to access grok.getSite() and should get our new app then
+        setSite(app)
+
+        self.processor_base = StudentProcessor()
+        self.processor = NigeriaStudentProcessor()
+        self.workdir = tempfile.mkdtemp()
+        self.csv_file = os.path.join(self.workdir, 'sample_student_data.csv')
+        open(self.csv_file, 'wb').write(STUDENT_SAMPLE_DATA)
+
+    def tearDown(self):
+        super(StudentProcessorTest, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, STUDENT_HEADER_FIELDS)
+        #print open(fail_file).read()
+        self.assertEqual(num_warns,0)
+        assert len(self.app['students'].keys()) == 3
+        # Also fst_sit_results have been properly imported (tested only here!)
+        self.assertEqual(
+            self.app['students']['K1000000'].fst_sit_results[0].__dict__,
+            {'grade': 'A1', 'subject': 'visual_art'})
+        self.assertEqual(
+            self.app['students']['K1000000'].fst_sit_results[1].__dict__,
+            {'grade': 'C6', 'subject': 'applied_electricity'})
+        shutil.rmtree(os.path.dirname(fin_file))
+
+
+class StudentUITests(StudentsFullSetup):
+    """Tests for customized student class views and pages
+    """
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentUITests, self).setUp()
+
+    def test_classes(self):
+        # Let's see if objects created in the customized
+        # portal really implement the customized interfaces
+        verify.verifyObject(INigeriaStudent, self.student)
+        verify.verifyObject(
+            INigeriaStudentStudyCourse, self.student['studycourse'])
+        studylevel = createObject(u'waeup.StudentStudyLevel')
+        verify.verifyObject(INigeriaStudentStudyLevel, studylevel)
+        ticket = createObject(u'waeup.CourseTicket')
+        verify.verifyObject(INigeriaCourseTicket, ticket)
+        IWorkflowState(self.student).setState('returning')
+        # Let's see if next_session_allowed works as expected
+        # A, ug_ft, 100
+        self.assertTrue(self.student['studycourse'].next_session_allowed)
+        # Zero, ug_ft, 100
+        self.student['studycourse'].current_verdict = '0' # Zero!
+        self.assertTrue(self.student['studycourse'].next_session_allowed)
+        # Zero, ug_ft, 200
+        self.student['studycourse'].current_level = 200
+        self.assertFalse(self.student['studycourse'].next_session_allowed)
+        # Zero, de_ft, 200
+        self.student['studycourse'].certificate.study_mode = 'de_ft'
+        self.assertTrue(self.student['studycourse'].next_session_allowed)
+        # Zero, ph_ft, 300
+        self.student['studycourse'].certificate.study_mode = 'ph_ft'
+        self.student['studycourse'].current_level = 300
+        self.assertTrue(self.student['studycourse'].next_session_allowed)
+        # Zero, ph_ft, 400
+        self.student['studycourse'].current_level = 400
+        self.assertFalse(self.student['studycourse'].next_session_allowed)
+
+        # Now we convert the certificate into a postgraduate certificate
+        IWorkflowState(self.student).setState('school fee paid')
+        self.certificate.study_mode = 'pg_ft'
+        # ... and voila next session registration is allowed
+        self.assertTrue(self.student['studycourse'].next_session_allowed)
+
+    def test_manage_access(self):
+        self.student.nationality = u'DE'
+        # Managers can access the pages of students
+        # and can perform actions
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        # The student created in the base package is an ug student
+        self.browser.open(self.student_path)
+        self.browser.getLink("Clearance Data").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.clearance_path)
+        self.browser.getLink("Manage").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.manage_clearance_path)
+        self.browser.getControl(name="form.date_of_birth").value = '09/10/1961'
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Form has been saved...',
+                           self.browser.contents)
+        self.assertMatches('...First Sitting Record...',
+                           self.browser.contents)
+        # Managers can open clearance slip of ug students
+        self.browser.open(self.student_path + '/clearance_slip.pdf')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        # There is no pg field in the clearance form
+        self.assertFalse('Second Higher Education Record'
+            in self.browser.contents)
+        # Now we change the study mode ...
+        self.certificate.study_mode = 'pg_ft'
+        self.browser.open(self.clearance_path)
+        # ... and additional pg clearance fields appear
+        self.assertMatches('...Second Higher Education Record...',
+                           self.browser.contents)
+        # But also fields from the ug form are displayed
+        self.assertMatches('...First Sitting Record...',
+                           self.browser.contents)
+        # Managers can open clearance slip of pg students
+        self.browser.open(self.student_path + '/clearance_slip.pdf')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        # Managers can edit personal data. No fields are required.
+        self.browser.open(self.manage_personal_path)
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Form has been saved...',
+                           self.browser.contents)
+
+    def test_logging(self):
+        self.student.nationality = u'DE'
+        valid_subj = NigeriaKofaUtils().EXAM_SUBJECTS_DICT.keys()[0]
+        valid_grade = NigeriaKofaUtils().EXAM_GRADES[0][0]
+        result_entry = ResultEntry(valid_subj, valid_grade)
+        self.student.fst_sit_results = [result_entry,]
+        # Managers can access the pages of students
+        # and can perform actions
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        # The student created in the base package is an ug student
+        self.browser.open(self.student_path)
+        self.browser.getLink("Clearance Data").click()
+        self.browser.getLink("Manage").click()
+        self.browser.getControl("Save").click()
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertFalse('saved: fst_sit_results' in logcontent)
+
+    def test_student_access(self):
+        # Students can edit clearance data
+        IWorkflowState(self.student).setState('cleared')
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        # Even in state admitted students can't change the portait if
+        # application slip exists.
+        IWorkflowState(self.student).setState('admitted')
+        self.browser.open(self.student_path)
+        self.assertTrue('Change portrait' in self.browser.contents)
+        file_store = getUtility(IExtFileStore)
+        applicant_slip = 'My application slip'
+        file_id = IFileStoreNameChooser(self.student).chooseName(
+            attr="application_slip.pdf")
+        file_store.createFile(file_id, StringIO(applicant_slip))
+        self.browser.open(self.student_path)
+        self.assertFalse('Change portrait' in self.browser.contents)
+        self.browser.open(self.student_path + '/change_portrait')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+        # Student can view and edit clearance data if clearance has started ...
+        IWorkflowInfo(self.student).fireTransition('start_clearance')
+        self.student.officer_comment = u'Fill properly'
+        self.browser.getLink("Clearance Data").click()
+        self.assertTrue("Officer's Comment" in self.browser.contents)
+        # Students can't edit officer's comment
+        self.browser.getLink("Edit").click()
+        self.assertFalse("Officer's Comment" in self.browser.contents)
+        self.assertTrue('Save' in self.browser.contents)
+        # ... and request clearance if nationality field has been filled.
+        self.browser.getControl("Save and request clearance").click()
+        self.assertMatches('...Required input is missing...',
+                           self.browser.contents)
+        self.student.nationality = u'DE'
+        self.browser.open(self.edit_clearance_path)
+        self.browser.getControl("Save and request clearance").click()
+        self.assertMatches('...Clearance has been requested...',
+                           self.browser.contents)
+
+        # Students can edit personal data. Some fields are required.
+        self.browser.open(self.personal_path)
+        self.assertTrue('Updated' in self.browser.contents)
+        self.browser.getLink("Edit").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.edit_personal_path)
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Required input is missing...',
+                           self.browser.contents)
+        self.browser.getControl(name="form.perm_address").value = 'My address!'
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Required input is missing...',
+                           self.browser.contents)
+        # Ok, let's give up and fill the rest.
+        self.browser.getControl(name="form.next_kin_name").value = 'My Mutti'
+        self.browser.getControl(name="form.next_kin_relation").value = 'mother'
+        self.browser.getControl(name="form.next_kin_address").value = 'sweet home'
+        self.browser.getControl(name="form.next_kin_phone.country").value = ['+234']
+        self.browser.getControl(name="form.next_kin_phone.ext").value = '45678'
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Form has been saved...',
+                           self.browser.contents)
+
+    def test_manage_upload_file(self):
+        # Managers can upload a file via the StudentClearanceManageFormPage
+        # The image is stored even if form has errors
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_clearance_path)
+        # Managers can add and delete a file
+        self.browser.open(self.manage_clearance_path)
+        image = open(SAMPLE_IMAGE, 'rb')
+        ctrl = self.browser.getControl(name='birthcertificateupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='my_acceptance_letter.jpg')
+        self.browser.getControl(
+            name='upload_acceptanceletterupload').click()
+        # Uups, we used the wrong 'Browse' field
+        self.assertFalse(
+            '<a target="image" href="acc_let">'
+            in self.browser.contents)
+        ctrl = self.browser.getControl(name='acceptanceletterupload')
+        file_ctrl = ctrl.mech_control
+        image.seek(0)
+        file_ctrl.add_file(image, filename='my_acceptance_letter.jpg')
+        self.browser.getControl(
+            name='upload_acceptanceletterupload').click()
+        self.assertTrue(
+            'http://localhost/app/students/K1000000/acc_let'
+            in self.browser.contents)
+        self.browser.getControl(
+            name='delete_acceptanceletterupload').click()
+        self.assertTrue(
+            'acc_let deleted'
+            in self.browser.contents)
+
+    def test_student_expired_personal_data(self):
+        # Login
+        IWorkflowState(self.student).setState('school fee paid')
+        delta = timedelta(days=180)
+        self.student.personal_updated = datetime.utcnow() - delta
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertEqual(self.browser.url, self.student_path)
+        self.assertTrue(
+            'You logged in' in self.browser.contents)
+        # Students don't see personal_updated field in edit form
+        self.browser.open(self.edit_personal_path)
+        self.assertFalse('Updated' in self.browser.contents)
+        self.browser.open(self.personal_path)
+        self.assertTrue('Updated' in self.browser.contents)
+        self.browser.getLink("Logout").click()
+        delta = timedelta(days=181)
+        self.student.personal_updated = datetime.utcnow() - delta
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertEqual(self.browser.url, self.edit_personal_path)
+        self.assertTrue(
+            'Your personal data record is outdated.' in self.browser.contents)
+
+    def test_lga_nationality(self):
+        self.student.nationality = u'DE'
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_clearance_path)
+        self.browser.getControl(name="form.lga").value = ['abia_aba_north']
+        self.browser.getControl("Save").click()
+        self.assertTrue(
+            'Nationalty and LGA are contradictory.' in self.browser.contents)
+        self.browser.getControl(name="form.nationality").value = ['NG']
+        self.browser.getControl("Save").click()
+        self.assertTrue(
+            'Form has been saved' in self.browser.contents)
+
+    def test_financial_clearance(self):
+        self.app['users'].addUser('mrbursary', SECRET)
+        self.app['users']['mrbursary'].email = 'mrbursary@foo.ng'
+        self.app['users']['mrbursary'].title = u'Carlo Pitter'
+        # Clearance officers needs to get
+        # the StudentsOfficer site role
+        prmglobal = IPrincipalRoleManager(self.app)
+        prmglobal.assignRoleToPrincipal('waeup.StudentsOfficer', 'mrbursary')
+        # Assign BursaryOfficer role
+        prmglobal.assignRoleToPrincipal('waeup.BursaryOfficer', 'mrbursary')
+        # and FinancialClearanceOfficer role
+        prmglobal.assignRoleToPrincipal(
+            'waeup.FinancialClearanceOfficer', 'mrbursary')
+        # Login
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mrbursary'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        # BO can see his roles
+        self.browser.getLink("My Roles").click()
+        self.assertMatches(
+            '...<div>Bursary Officer</div>...',
+            self.browser.contents)
+        # BO can view student record
+        self.browser.open(self.student_path)
+        # BO can see clearance button ...
+        self.assertTrue(
+            'Clear student financially' in self.browser.contents)
+        # ... but not withdraw button
+        self.assertFalse(
+            'Withdraw financial clearance' in self.browser.contents)
+        # BO can clear student
+        self.browser.getLink("Clear student financially").click()
+        self.assertTrue(
+            'Student has been financially cleared' in self.browser.contents)
+        # Name of BO and date have been stored
+        self.assertEqual(self.student.financially_cleared_by, 'Carlo Pitter')
+        self.assertMatches(
+            '<YYYY-MM-DD hh:mm:ss>',
+            self.student.financial_clearance_date.strftime(
+                "%Y-%m-%d %H:%M:%S"))
+        # BO can't see clearance button ...
+        self.assertFalse(
+            'Clear student financially' in self.browser.contents)
+        # ... but withdraw button and can withdraw clearance
+        self.browser.getLink("Withdraw financial clearance").click()
+        self.assertTrue(
+            'Financial clearance withdrawn' in self.browser.contents)
+        # Name of BO and date have been deleted
+        self.assertEqual(self.student.financially_cleared_by, None)
+        self.assertEqual(self.student.financial_clearance_date, None)
+        # Clearance is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'mrbursary - kofacustom.nigeria.students.browser.ClearStudentFinancially'
+            ' - K1000000 - financially cleared' in logcontent)
+        self.assertTrue(
+            'mrbursary - kofacustom.nigeria.students.browser.WithdrawFinancialClearance'
+            ' - K1000000 - financial clearance withdrawn' in logcontent)
+        # Clearance is also stored in the history
+        self.browser.open(self.history_path)
+        self.assertMatches(
+            '...2016-01-16 15:50:48 WAT - Financially cleared by Carlo Pitter...',
+            self.browser.contents)
+        self.assertMatches(
+            '...2016-01-16 15:50:48 WAT - Financial clearance withdrawn by Carlo Pitter...',
+            self.browser.contents)
+
+    def test_fiancial_clearance_pdf_slip(self):
+        payment1 = createObject(u'waeup.StudentOnlinePayment')
+        timestamp = ("%d" % int(time()*10000))[1:]
+        payment1.p_id = "LSCNEW-2-4153206270" # the longest possible p_id
+        payment1.p_category = 'schoolfee'
+        payment1.p_item = u'My School Fee'
+        payment1.p_session = 2015
+        payment1.p_level = 100
+        payment1.p_current = True
+        payment1.amount_auth = 23456.9
+        payment1.approve()
+        payment2 = createObject(u'waeup.StudentOnlinePayment')
+        timestamp = ("%d" % int(time()*10000))[1:]
+        payment2.p_id = "p%s" % timestamp
+        payment2.p_category = 'clearance'
+        payment2.p_item = u'My Clearance Fee'
+        payment2.p_session = 2015
+        payment2.p_level = 100
+        payment2.p_current = True
+        payment2.amount_auth = 5678.6
+        payment2.approve()
+        self.student['payments'][payment1.p_id] = payment1
+        self.student['payments'][payment2.p_id] = payment2
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.student_path + '/clear_financially')
+        self.browser.getLink("Download fee payment history").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'],
+                         'application/pdf')
+        path = os.path.join(samples_dir(), 'fee_payment_history.pdf')
+        open(path, 'wb').write(self.browser.contents)
+        print "Sample PDF fee_payment_history.pdf written to %s" % path
+        return
+
+    def test_provisionally_cleared(self):
+        # Students can edit clearance data
+        IWorkflowState(self.student).setState('school fee paid')
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        # Student can't access the clearance form
+        self.browser.open(self.student_path)
+        self.browser.getLink("Clearance Data").click()
+        # Student can't open clearance edit form before starting clearance
+        self.browser.open(self.student_path + '/cedit')
+        self.assertMatches('...The requested form is locked...',
+                           self.browser.contents)
+        self.assertFalse(
+            '<h1 class="kofa-content-label">Edit clearance data</h1>'
+            in self.browser.contents)
+        # If provisionally_cleared is set they can
+        self.student.provisionally_cleared = True
+        self.browser.getLink("Clearance Data").click()
+        self.browser.getLink("Edit").click()
+        self.assertTrue(
+            '<h1 class="kofa-content-label">Edit clearance data</h1>'
+            in self.browser.contents)
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/tests/test_export.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/tests/test_export.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/tests/test_export.py	(revision 17238)
@@ -0,0 +1,237 @@
+## Copyright (C) 2011 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 datetime
+from zope.component import queryUtility
+from zope.interface.verify import verifyObject, verifyClass
+from hurry.workflow.interfaces import IWorkflowState
+from waeup.kofa.interfaces import ICSVExporter
+from waeup.kofa.schoolgrades import ResultEntry
+from waeup.kofa.students.tests.test_batching import StudentImportExportSetup
+
+from kofacustom.nigeria.utils.utils import NigeriaKofaUtils
+from kofacustom.nigeria.students.export import (
+    NigeriaStudentExporter, NigeriaStudentPaymentExporter,
+    ClearanceRequestedStudentExporter, NigeriaDataForBursaryExporter,
+    NigeriaTrimmedDataExporter)
+from kofacustom.nigeria.testing import FunctionalLayer
+
+curr_year = datetime.datetime.now().year
+
+class NigeriaStudentExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(NigeriaStudentExporterTest, self).setUp()
+        self.setup_for_export()
+        result_entry = ResultEntry(
+            sorted(NigeriaKofaUtils.EXAM_SUBJECTS_DICT.keys())[0],
+            sorted(NigeriaKofaUtils.EXAM_GRADES)[0][0]
+            )
+        self.student.alr_results = [
+            result_entry]
+        self.student.fst_sit_results = [
+            result_entry]
+        self.student.scd_sit_results = [
+            result_entry]
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = NigeriaStudentExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter, NigeriaStudentExporter)
+        return
+
+
+    def test_export_all(self):
+        # we can really export students
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        exporter = NigeriaStudentExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            'adm_code,alr_date,alr_fname,alr_no,alr_results,'
+            'clr_code,date_of_birth,def_adm,disabled,email,emp2_end,'
+            'emp2_position,emp2_reason,emp2_start,emp_end,emp_position,'
+            'emp_reason,emp_start,employer,employer2,'
+            'financial_clearance_date,financially_cleared_by,'
+            'firstname,flash_notice,former_matric,'
+            'fst_sit_date,fst_sit_fname,fst_sit_no,fst_sit_results,'
+            'fst_sit_type,hq2_degree,hq2_disc,hq2_matric_no,hq2_school,'
+            'hq2_session,hq2_type,hq_degree,hq_disc,hq_fname,hq_matric_no,'
+            'hq_school,hq_session,hq_type,is_staff,lastname,lga,'
+            'marit_stat,matric_number,middlename,nationality,'
+            'next_kin_address,next_kin_name,next_kin_phone,next_kin_relation,'
+            'nysc_lga,nysc_location,nysc_year,officer_comment,parents_email,'
+            'perm_address,personal_updated,phone,physical_clearance_date,'
+            'provisionally_cleared,reg_number,'
+            'religion,scd_sit_date,scd_sit_fname,scd_sit_no,'
+            'scd_sit_results,scd_sit_type,sex,student_id,'
+            'suspended,suspended_comment,password,state,history,certcode,is_postgrad,'
+            'current_level,current_session\r\nmy adm code,,,,'
+            '"[(\'accounts\', \'A\')]",my clr code,1981-02-04#,,,'
+            'anna@sample.com,,,,,,,,,,,,,Anna,,,,,,"[(\'accounts\', \'A\')]"'
+            ',,,,,,,,,,,,,,,,Tester,,,234,M.,NG,,,,,,,,,,'
+            '"Studentroad 21\nLagos 123456\n",,+234-123-12345#,,,123,,,,,'
+            '"[(\'accounts\', \'A\')]",,f,A111111,0,,,created,'
+            '[u\'2012-11-06 13:16:41 WAT - Record created by system\'],'
+            'CERT1,0,200,2012\r\n',
+            result
+            )
+        return
+
+class NigeriaStudentPaymentExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(NigeriaStudentPaymentExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = NigeriaStudentPaymentExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter, NigeriaStudentPaymentExporter)
+        return
+
+    def test_export_all(self):
+        # we can really export students
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        self.student['payments']['my-payment'].r_company = 'interswitch'
+        self.student['payments']['my-payment'].r_card_num = '789'
+        exporter = NigeriaStudentPaymentExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            'ac,amount_auth,creation_date,gateway_amt,net_amt,p_category,p_combi,p_current,'
+            'p_id,p_item,p_level,p_option,p_session,p_split_data,p_state,payment_date,provider_amt,'
+            'r_amount_approved,r_card_num,r_code,r_company,r_desc,'
+            'r_pay_reference,thirdparty_amt,student_id,state,'
+            'current_session\r\n'
+            '666,12.12,2012-04-01 13:12:01#,,,schoolfee,[],1,my-id,p-item,'
+            '100,,%s,,paid,2012-04-01 14:12:01#,,12.12,'
+            '789,r-code,interswitch,,,,A111111,created,2012\r\n' % (curr_year-6),
+            result
+            )
+        return
+
+class ClearanceRequestedStudentExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ClearanceRequestedStudentExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = ClearanceRequestedStudentExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter, ClearanceRequestedStudentExporter)
+        return
+
+    def test_export_all(self):
+        # we can really export students
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        self.student.physical_clearance_date = 'Come soon'
+        IWorkflowState(self.student).setState('clearance requested')
+        exporter = ClearanceRequestedStudentExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            'student_id,reg_number,display_fullname,state,current_session,'
+            'history,physical_clearance_date,email,phone\r\n'
+            'A111111,123,Anna M. Tester,clearance requested,'
+            '2012,2014-12-02 07:17:02 WAT - Record created by system,'
+            'Come soon,anna@sample.com,+234-123-12345\r\n',
+            result
+            )
+        return
+
+class BursaryDataExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(BursaryDataExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_export_all(self):
+        # we can really export all payments
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        exporter = NigeriaDataForBursaryExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,formatted_p_date,gateway_amt,net_amt,'
+            'p_category,p_combi,p_current,p_id,p_item,p_level,p_option,p_session,p_split_data,p_state,'
+            'payment_date,provider_amt,r_amount_approved,r_card_num,r_code,'
+            'r_company,r_desc,r_pay_reference,thirdparty_amt,student_id,'
+            'matric_number,reg_number,firstname,middlename,lastname,sex,state,'
+            'current_session,entry_session,entry_mode,faccode,depcode,certcode,lga\r\n'
+
+            '666,12.12,%s-04-01 13:12:01#,,,,schoolfee,[],1,my-id,p-item,'
+            '100,,%s,,paid,%s-04-01 14:12:01#,,12.12,,r-code,,,,,A111111,'
+            '234,123,Anna,M.,Tester,f,created,2012,2010,ug_ft,NA,NA,CERT1,\r\n'
+            % (curr_year-6, curr_year-6, curr_year-6)
+            )
+        return
+
+class NigeriaStudentTrimmedDataExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    std_csv_entry = (
+        'my adm code,my clr code,1981-02-04#,anna@sample.com,,'
+        'Anna,,Tester,234,M.,NG,,,"Studentroad 21\nLagos 123456\n",,'
+        '+234-123-12345#,123,f,A111111,0,,,created'
+        )
+
+    def setUp(self):
+        super(NigeriaStudentTrimmedDataExporterTest, self).setUp()
+        self.setup_for_export()
+        self.student.lga = 'abia_aba_north'
+        return
+
+    def test_export_all(self):
+        # we can really export students
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        exporter = NigeriaTrimmedDataExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'student_id,matric_number,reg_number,firstname,middlename,'
+            'lastname,sex,email,phone,nationality,date_of_birth,state,'
+            'current_mode,certcode,faccode,depcode,current_level,'
+            'current_session,current_verdict,entry_session,lg_state,lg_area\r\n'
+            'A111111,234,123,Anna,M.,Tester,f,anna@sample.com,+234-123-12345#,'
+            'NG,1981-02-04#,created,ug_ft,CERT1,NA,NA,200,2012,0,2010,'
+            'abia,aba-north'
+            in result
+            )
+        return
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/tests/test_student.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/tests/test_student.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/tests/test_student.py	(revision 17238)
@@ -0,0 +1,137 @@
+## $Id$
+##
+## Copyright (C) 2011 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
+##
+"""Tests for students and related.
+"""
+from datetime import tzinfo
+from zope.component.interfaces import IFactory
+from zope.interface import verify
+from zope.schema.interfaces import ConstraintNotSatisfied
+from waeup.kofa.testing import FunctionalTestCase
+from kofacustom.nigeria.students.student import NigeriaStudent, NigeriaStudentFactory
+from kofacustom.nigeria.students.studycourse import (
+    NigeriaStudentStudyCourse, NigeriaStudentStudyCourseFactory)
+from kofacustom.nigeria.students.studylevel import (
+    NigeriaStudentStudyLevel, NigeriaCourseTicket,
+    NigeriaStudentStudyLevelFactory,
+    NigeriaCourseTicketFactory)
+from kofacustom.nigeria.students.interfaces import (
+    INigeriaStudent, INigeriaStudentStudyCourse,
+    INigeriaStudentStudyLevel, INigeriaCourseTicket)
+from kofacustom.nigeria.testing import FunctionalLayer
+
+
+class NigeriaStudentFactoryTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(NigeriaStudentFactoryTest, self).setUp()
+        self.factory = NigeriaStudentFactory()
+
+    def tearDown(self):
+        super(NigeriaStudentFactoryTest, self).tearDown()
+
+    def test_interfaces(self):
+        verify.verifyClass(IFactory, NigeriaStudentFactory)
+        verify.verifyObject(IFactory, self.factory)
+
+    def test_factory(self):
+        obj = self.factory()
+        assert isinstance(obj, NigeriaStudent)
+
+    def test_getInterfaces(self):
+        implemented_by = self.factory.getInterfaces()
+        assert implemented_by.isOrExtends(INigeriaStudent)
+
+    def test_is_foreigner(self):
+        student = self.factory()
+        self.assertRaises(
+            ConstraintNotSatisfied, setattr, student, 'nationality', 'XX')
+        student.nationality = 'NG'
+        self.assertFalse(student.is_foreigner)
+        student.nationality = 'DE'
+        self.assertTrue(student.is_foreigner)
+
+class NigeriaStudentStudyCourseFactoryTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(NigeriaStudentStudyCourseFactoryTest, self).setUp()
+        self.factory = NigeriaStudentStudyCourseFactory()
+
+    def tearDown(self):
+        super(NigeriaStudentStudyCourseFactoryTest, self).tearDown()
+
+    def test_interfaces(self):
+        verify.verifyClass(IFactory, NigeriaStudentStudyCourseFactory)
+        verify.verifyObject(IFactory, self.factory)
+
+    def test_factory(self):
+        obj = self.factory()
+        assert isinstance(obj, NigeriaStudentStudyCourse)
+
+    def test_getInterfaces(self):
+        implemented_by = self.factory.getInterfaces()
+        assert implemented_by.isOrExtends(INigeriaStudentStudyCourse)
+
+class NigeriaStudentStudyLevelFactoryTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(NigeriaStudentStudyLevelFactoryTest, self).setUp()
+        self.factory = NigeriaStudentStudyLevelFactory()
+
+    def tearDown(self):
+        super(NigeriaStudentStudyLevelFactoryTest, self).tearDown()
+
+    def test_interfaces(self):
+        verify.verifyClass(IFactory, NigeriaStudentStudyLevelFactory)
+        verify.verifyObject(IFactory, self.factory)
+
+    def test_factory(self):
+        obj = self.factory()
+        assert isinstance(obj, NigeriaStudentStudyLevel)
+
+    def test_getInterfaces(self):
+        implemented_by = self.factory.getInterfaces()
+        assert implemented_by.isOrExtends(INigeriaStudentStudyLevel)
+
+class NigeriaCourseTicketFactoryTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(NigeriaCourseTicketFactoryTest, self).setUp()
+        self.factory = NigeriaCourseTicketFactory()
+
+    def tearDown(self):
+        super(NigeriaCourseTicketFactoryTest, self).tearDown()
+
+    def test_interfaces(self):
+        verify.verifyClass(IFactory, NigeriaCourseTicketFactory)
+        verify.verifyObject(IFactory, self.factory)
+
+    def test_factory(self):
+        obj = self.factory()
+        assert isinstance(obj, NigeriaCourseTicket)
+
+    def test_getInterfaces(self):
+        implemented_by = self.factory.getInterfaces()
+        assert implemented_by.isOrExtends(INigeriaCourseTicket)
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/utils.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/utils.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/utils.py	(revision 17238)
@@ -0,0 +1,42 @@
+## $Id$
+##
+## Copyright (C) 2011 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 waeup.kofa.students.utils import StudentsUtils
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+class NigeriaStudentsUtils(StudentsUtils):
+    """A collection of customized methods.
+
+    """
+
+    SEPARATORS_DICT = {
+        'form.fst_sit_fname': _(u'First Sitting Record'),
+        'form.scd_sit_fname': _(u'Second Sitting Record'),
+        'form.alr_fname': _(u'Advanced Level Record'),
+        'form.hq_type': _(u'Higher Education Record'),
+        'form.hq2_type': _(u'Second Higher Education Record'),
+        'form.nysc_year': _(u'NYSC Information'),
+        'form.employer': _(u'Employment History'),
+        'form.former_matric': _(u'Former Student'),
+        }
+
+    STUDENT_EXPORTER_NAMES = StudentsUtils().STUDENT_EXPORTER_NAMES + (
+            'clearancerequested',)
+
+    # Maximum size of upload files in kB
+    MAX_KB = 250
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/viewlets.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/viewlets.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/students/viewlets.py	(revision 17238)
@@ -0,0 +1,590 @@
+## $Id$
+##
+## Copyright (C) 2011 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 zope.component import getUtility
+from waeup.kofa.interfaces import IExtFileStore
+from waeup.kofa.browser.viewlets import ManageActionButton
+from waeup.kofa.students.interfaces import IStudentsUtils
+from waeup.kofa.students.viewlets import (
+    PaymentReceiptActionButton, StudentPassportActionButton)
+from waeup.kofa.students.fileviewlets import (
+    StudentFileDisplay, StudentFileUpload, StudentImage)
+from waeup.kofa.students.browser import ExportPDFClearanceSlip
+from kofacustom.nigeria.students.interfaces import (
+    INigeriaStudent, INigeriaStudentOnlinePayment)
+from kofacustom.nigeria.students.browser import NigeriaStudentBaseDisplayFormPage
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+def show_viewlet(viewlet):
+    students_utils = getUtility(IStudentsUtils)
+    if viewlet.__name__ in students_utils.SKIP_UPLOAD_VIEWLETS:
+        return False
+    cm = getattr(viewlet.context,'current_mode', None)
+    if cm is not None and cm.startswith('pg'):
+        return True
+    return False
+
+class StudentPassportActionButton(StudentPassportActionButton):
+
+    @property
+    def target_url(self):
+        # Passport pictures must not be editable if application slip
+        # exists.
+        slip = getUtility(IExtFileStore).getFileByContext(
+            self.context, 'application_slip')
+        PORTRAIT_CHANGE_STATES = getUtility(IStudentsUtils).PORTRAIT_CHANGE_STATES
+        if self.context.state not in PORTRAIT_CHANGE_STATES or slip is not None:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class PaymentReceiptActionButton(PaymentReceiptActionButton):
+    grok.order(4)
+    grok.context(INigeriaStudentOnlinePayment)
+
+    @property
+    def target_url(self):
+        if not self.context.p_state in ('paid', 'waived', 'scholarship') \
+            and not self.context.r_company:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+# Financial Clearance Officer buttons
+
+class StudentFinanciallyClearActionButton(ManageActionButton):
+    grok.order(10)
+    grok.context(INigeriaStudent)
+    grok.view(NigeriaStudentBaseDisplayFormPage)
+    grok.require('waeup.clearStudentFinancially')
+    text = _('Clear student financially')
+    target = 'clear_financially'
+    icon = 'actionicon_accept.png'
+
+    @property
+    def target_url(self):
+        if self.context.financially_cleared_by:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class StudentWithdrawFinancialClearanceActionButton(ManageActionButton):
+    grok.order(11)
+    grok.context(INigeriaStudent)
+    grok.view(NigeriaStudentBaseDisplayFormPage)
+    grok.require('waeup.clearStudentFinancially')
+    text = _('Withdraw financial clearance')
+    target = 'withdraw_financial_clearance'
+    icon = 'actionicon_reject.png'
+
+    @property
+    def target_url(self):
+        if not self.context.financially_cleared_by:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class FinancialClearanceSlipActionButton(ManageActionButton):
+    grok.order(12)
+    grok.context(INigeriaStudent)
+    grok.view(NigeriaStudentBaseDisplayFormPage)
+    grok.require('waeup.viewStudent')
+    text = _('Download fee payment history')
+    target = 'fee_payment_history.pdf'
+    icon = 'actionicon_pdf.png'
+
+# Acceptance Letter
+
+class AcceptanceLetterDisplay(StudentFileDisplay):
+    """Acceptance Letter display viewlet.
+    """
+    grok.order(2)
+    label = _(u'Acceptance Letter')
+    title = _(u'Acceptance Letter Scan')
+    download_name = u'acc_let'
+
+class AcceptanceLetterSlip(AcceptanceLetterDisplay):
+    grok.view(ExportPDFClearanceSlip)
+
+class AcceptanceLetterUpload(StudentFileUpload):
+    """AcceptanceLetter upload viewlet.
+    """
+    grok.order(2)
+    label = _(u'Acceptance Letter')
+    title = _(u'Acceptance Letter Scan')
+    download_name = u'acc_let'
+    tab_redirect = '?tab2'
+
+class AcceptanceLetterImage(StudentImage):
+    """Renders acceptance letter scan.
+    """
+    grok.name('acc_let')
+    download_name = u'acc_let'
+
+# LGA Identification
+
+class LGAIdentificationDisplay(StudentFileDisplay):
+    """LGA Identification display viewlet.
+    """
+    grok.order(3)
+    label = _(u'LGA Identification')
+    title = _(u'LGA Identification Scan')
+    download_name = u'lga_ident'
+
+class LGAIdentificationSlip(LGAIdentificationDisplay):
+    grok.view(ExportPDFClearanceSlip)
+
+class LGAIdentificationUpload(StudentFileUpload):
+    """LGA Identification upload viewlet.
+    """
+    grok.order(3)
+    label = _(u'LGA Identification')
+    title = _(u'LGA Identification Scan')
+    download_name = u'lga_ident'
+
+class LGAIdentificationImage(StudentImage):
+    """Renders LGA Identification scan.
+    """
+    grok.name('lga_ident')
+    download_name = u'lga_ident'
+
+# First Sitting Result
+
+class FirstSittingResultDisplay(StudentFileDisplay):
+    """First Sitting Result display viewlet.
+    """
+    grok.order(4)
+    label = _(u'First Sitting Result')
+    title = _(u'First Sitting Result')
+    download_name = u'fst_sit_scan'
+
+class FirstSittingResultSlip(FirstSittingResultDisplay):
+    grok.view(ExportPDFClearanceSlip)
+
+class FirstSittingResultUpload(StudentFileUpload):
+    """First Sitting Result upload viewlet.
+    """
+    grok.order(4)
+    label = _(u'First Sitting Result')
+    title = _(u'First Sitting Result Scan')
+    download_name = u'fst_sit_scan'
+
+class FirstSittingResultImage(StudentImage):
+    """Renders First Sitting Result scan.
+    """
+    grok.name('fst_sit_scan')
+    download_name = u'fst_sit_scan'
+
+# Second Sitting Result
+
+class SecondSittingResultDisplay(StudentFileDisplay):
+    """Second Sitting Result display viewlet.
+    """
+    grok.order(5)
+    label = _(u'Second Sitting Result')
+    title = _(u'Second Sitting Result')
+    download_name = u'scd_sit_scan'
+
+class SecondSittingResultSlip(SecondSittingResultDisplay):
+    grok.view(ExportPDFClearanceSlip)
+
+class SecondSittingResultUpload(StudentFileUpload):
+    """Second Sitting Result upload viewlet.
+    """
+    grok.order(5)
+    label = _(u'Second Sitting Result')
+    title = _(u'Second Sitting Result Scan')
+    download_name = u'scd_sit_scan'
+
+class SecondSittingResultImage(StudentImage):
+    """Renders Second Sitting Result scan.
+    """
+    grok.name('scd_sit_scan')
+    download_name = u'scd_sit_scan'
+
+# Higher Qualification Result
+
+class HigherQualificationResultDisplay(StudentFileDisplay):
+    """Higher Qualification Result display viewlet.
+    """
+    grok.order(6)
+    label = _(u'Higher Qualification Result')
+    title = _(u'Higher Qualification Result')
+    download_name = u'hq_scan'
+
+class HigherQualificationResultSlip(HigherQualificationResultDisplay):
+    grok.view(ExportPDFClearanceSlip)
+
+class HigherQualificationResultUpload(StudentFileUpload):
+    """Higher Qualification Result upload viewlet.
+    """
+    grok.order(6)
+    label = _(u'Higher Qualification Result')
+    title = _(u'Higher Qualification Result Scan')
+    download_name = u'hq_scan'
+
+class HigherQualificationResultImage(StudentImage):
+    """Renders Higher Qualification Result scan.
+    """
+    grok.name('hq_scan')
+    download_name = u'hq_scan'
+
+# 2nd Higher Qualification Result (PG Students only)
+
+class SecondHigherQualificationResultDisplay(StudentFileDisplay):
+    """Second Higher Qualification Result display viewlet.
+    """
+    grok.order(7)
+    label = _(u'Second Higher Qualification Result')
+    title = _(u'Second Higher Qualification Result')
+    download_name = u'hq_scan2'
+
+class SecondHigherQualificationResultSlip(SecondHigherQualificationResultDisplay):
+    grok.view(ExportPDFClearanceSlip)
+
+class SecondHigherQualificationResultUpload(StudentFileUpload):
+    """Second Higher Qualification Result upload viewlet.
+    """
+    grok.order(7)
+    label = _(u'Second Higher Qualification Result')
+    title = _(u'Second Higher Qualification Result Scan')
+    download_name = u'hq_scan2'
+
+    @property
+    def show_viewlet(self):
+        return show_viewlet(self)
+
+class SecondHigherQualificationResultImage(StudentImage):
+    """Renders Second Higher Qualification Result scan.
+    """
+    grok.name('hq_scan2')
+    download_name = u'hq_scan2'
+
+# Advanced Level Result
+
+class AdvancedLevelResultDisplay(StudentFileDisplay):
+    """Advanced Level Result display viewlet.
+    """
+    grok.order(8)
+    label = _(u'Advanced Level Result')
+    title = _(u'Advanced Level Result')
+    download_name = u'alr_scan'
+
+class AdvancedLevelResultSlip(AdvancedLevelResultDisplay):
+    grok.view(ExportPDFClearanceSlip)
+
+class AdvancedLevelResultUpload(StudentFileUpload):
+    """Advanced Level Result upload viewlet.
+    """
+    grok.order(8)
+    label = _(u'Advanced Level Result')
+    title = _(u'Advanced Level Result Scan')
+    download_name = u'alr_scan'
+
+class AdvancedLevelResultImage(StudentImage):
+    """Renders Advanced Level Result scan.
+    """
+    grok.name('alr_scan')
+    download_name = u'alr_scan'
+
+# Certificate
+
+class CertificateDisplay(StudentFileDisplay):
+    """Certificate display viewlet.
+    """
+    grok.order(9)
+    label = _(u'Certificate')
+    title = _(u'Certificate')
+    download_name = u'cert'
+
+class CertificateSlip(CertificateDisplay):
+    grok.view(ExportPDFClearanceSlip)
+
+class CertificateUpload(StudentFileUpload):
+    """Certificate upload viewlet.
+    """
+    grok.order(9)
+    label = _(u'Certificate')
+    title = _(u'Certificate Scan')
+    download_name = u'cert'
+
+class CertificateImage(StudentImage):
+    """Renders Certificate scan.
+    """
+    grok.name('cert')
+    download_name = u'cert'
+
+# Second Certificate (PG Students only)
+
+class SecondCertificateDisplay(StudentFileDisplay):
+    """ Second Certificate display viewlet.
+    """
+    grok.order(10)
+    label = _(u'Second Certificate')
+    title = _(u'Second Certificate')
+    download_name = u'cert2'
+
+class SecondCertificateSlip(SecondCertificateDisplay):
+    grok.view(ExportPDFClearanceSlip)
+
+class SecondCertificateUpload(StudentFileUpload):
+    """Second Certificate upload viewlet.
+    """
+    grok.order(10)
+    label = _(u'Second Certificate')
+    title = _(u'Second Certificate Scan')
+    download_name = u'cert2'
+
+    @property
+    def show_viewlet(self):
+        return show_viewlet(self)
+
+class SecondCertificateImage(StudentImage):
+    """Renders Second Certificate scan.
+    """
+    grok.name('cert2')
+    download_name = u'cert2'
+
+# Third Certificate (PG Students only)
+
+class ThirdCertificateDisplay(StudentFileDisplay):
+    """ Third Certificate display viewlet.
+    """
+    grok.order(11)
+    label = _(u'Third Certificate')
+    title = _(u'Third Certificate')
+    download_name = u'cert3'
+
+class ThirdCertificateSlip(ThirdCertificateDisplay):
+    grok.view(ExportPDFClearanceSlip)
+
+class ThirdCertificateUpload(StudentFileUpload):
+    """Third Certificate upload viewlet.
+    """
+    grok.order(11)
+    label = _(u'Third Certificate')
+    title = _(u'Third Certificate Scan')
+    download_name = u'cert3'
+
+    @property
+    def show_viewlet(self):
+        return show_viewlet(self)
+
+class ThirdCertificateImage(StudentImage):
+    """Renders Third Certificate scan.
+    """
+    grok.name('cert3')
+    download_name = u'cert3'
+
+# Evidence of Name
+
+class EvidenceNameDisplay(StudentFileDisplay):
+    """Evidence of Name display viewlet.
+    """
+    grok.order(12)
+    label = _(u'Evidence of Name')
+    title = _(u'Evidence of Name')
+    download_name = u'evid'
+
+class EvidenceNameSlip(EvidenceNameDisplay):
+    grok.view(ExportPDFClearanceSlip)
+
+class EvidenceNameUpload(StudentFileUpload):
+    """Evidence of Name upload viewlet.
+    """
+    grok.order(12)
+    label = _(u'Evidence of Name')
+    title = _(u'Evidence of Name Scan')
+    download_name = u'evid'
+
+class EvidenceNameImage(StudentImage):
+    """Renders Evidence of Name scan.
+    """
+    grok.name('evid')
+    download_name = u'evid'
+
+# Result Statement
+
+class ResultStatementDisplay(StudentFileDisplay):
+    """Result Statement display viewlet.
+    """
+    grok.order(13)
+    label = _(u'Result Statement')
+    title = _(u'Result Statement')
+    download_name = u'res_stat'
+
+class ResultStatementSlip(ResultStatementDisplay):
+    grok.view(ExportPDFClearanceSlip)
+
+class ResultStatementUpload(StudentFileUpload):
+    """Result Statement upload viewlet.
+    """
+    grok.order(13)
+    label = _(u'Result Statement')
+    title = _(u'Result Statement Scan')
+    download_name = u'res_stat'
+
+class ResultStatementImage(StudentImage):
+    """Renders Result Statement scan.
+    """
+    grok.name('res_stat')
+    download_name = u'res_stat'
+
+# Referee Letter
+
+class RefereeLetterDisplay(StudentFileDisplay):
+    """Referee Letter display viewlet.
+    """
+    grok.order(14)
+    label = _(u'Guarantor/Referee Letter')
+    title = _(u'Guarantor/Referee Letter')
+    download_name = u'ref_let'
+
+class RefereeLetterSlip(RefereeLetterDisplay):
+    grok.view(ExportPDFClearanceSlip)
+
+class RefereeLetterUpload(StudentFileUpload):
+    """Referee Letter upload viewlet.
+    """
+    grok.order(14)
+    label = _(u'Guarantor/Referee Letter')
+    title = _(u'Guarantor/Referee Letter Scan')
+    download_name = u'ref_let'
+
+class RefereeLetterImage(StudentImage):
+    """Renders Referee Letter scan.
+    """
+    grok.name('ref_let')
+    download_name = u'ref_let'
+
+# Second Referee Letter (PG Students only)
+
+class SecondRefereeLetterDisplay(StudentFileDisplay):
+    """Second Referee Letter display viewlet.
+    """
+    grok.order(15)
+    label = _(u'Second Referee Letter')
+    title = _(u'Second Referee Letter')
+    download_name = u'ref_let2'
+
+class SecondRefereeLetterSlip(SecondRefereeLetterDisplay):
+    grok.view(ExportPDFClearanceSlip)
+
+class SecondRefereeLetterUpload(StudentFileUpload):
+    """Referee Letter upload viewlet.
+    """
+    grok.order(15)
+    label = _(u'Second Referee Letter')
+    title = _(u'Second Referee Letter Scan')
+    download_name = u'ref_let2'
+
+    @property
+    def show_viewlet(self):
+        return show_viewlet(self)
+
+class SecondRefereeLetterImage(StudentImage):
+    """Renders Referee Letter scan.
+    """
+    grok.name('ref_let2')
+    download_name = u'ref_let2'
+
+# Third Referee Letter (PG Students only)
+
+class ThirdRefereeLetterDisplay(StudentFileDisplay):
+    """Third Referee Letter display viewlet.
+    """
+    grok.order(16)
+    label = _(u'Third Referee Letter')
+    title = _(u'Third Referee Letter')
+    download_name = u'ref_let3'
+
+class ThirdRefereeLetterSlip(ThirdRefereeLetterDisplay):
+    grok.view(ExportPDFClearanceSlip)
+
+class ThirdRefereeLetterUpload(StudentFileUpload):
+    """Referee Letter upload viewlet.
+    """
+    grok.order(16)
+    label = _(u'Third Referee Letter')
+    title = _(u'Third Referee Letter Scan')
+    download_name = u'ref_let3'
+
+    @property
+    def show_viewlet(self):
+        return show_viewlet(self)
+
+class ThirdRefereeLetterImage(StudentImage):
+    """Renders Referee Letter scan.
+    """
+    grok.name('ref_let3')
+    download_name = u'ref_let3'
+
+# Affidavit (former Statutory Declaration) of Good Conduct
+
+class StatutoryDeclarationDisplay(StudentFileDisplay):
+    """Statutory Declaration of Good Conduct display viewlet.
+    """
+    grok.order(17)
+    label = _(u'Affidavit of Good Conduct')
+    title = _(u'Affidavit of Good Conduct')
+    download_name = u'stat_dec'
+
+class StatutoryDeclarationSlip(StatutoryDeclarationDisplay):
+    grok.view(ExportPDFClearanceSlip)
+
+class StatutoryDeclarationUpload(StudentFileUpload):
+    """Statutory Declaration of Good Conduct upload viewlet.
+    """
+    grok.order(17)
+    label = _(u'Affidavit of Good Conduct')
+    title = _(u'Affidavit of Good Conduct Scan')
+    download_name = u'stat_dec'
+
+class StatutoryDeclarationImage(StudentImage):
+    """Renders Affidavit of Good Conduct scan.
+    """
+    grok.name('stat_dec')
+    download_name = u'stat_dec'
+
+# Letter of Admission (PG Students only)
+
+class LetterAdmissionDisplay(StudentFileDisplay):
+    """Letter of Admission display viewlet.
+    """
+    grok.order(18)
+    label = _(u'Letter of Admission')
+    title = _(u'Letter of Admission')
+    download_name = u'admission_let'
+
+class LetterAdmissionSlip(LetterAdmissionDisplay):
+    grok.view(ExportPDFClearanceSlip)
+
+class LetterAdmissionUpload(StudentFileUpload):
+    """Letter of Admission upload viewlet.
+    """
+    grok.order(18)
+    label = _(u'Letter of Admission')
+    title = _(u'Letter of Admission Scan')
+    download_name = u'admission_let'
+
+    @property
+    def show_viewlet(self):
+        return show_viewlet(self)
+
+class LetterAdmissionImage(StudentImage):
+    """Renders Letter of Admission scan.
+    """
+    grok.name('admission_let')
+    download_name = u'admission_let'
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/testing.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/testing.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/testing.py	(revision 17238)
@@ -0,0 +1,29 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""Testing support for :mod:`waeup.kofa`.
+"""
+import os.path
+import kofacustom.nigeria
+from zope.app.testing.functional import ZCMLLayer
+from waeup.kofa.testing import FunctionalTestCase
+
+# Some funcional layer that groks kofacustom.nigeria.
+ftesting_zcml = os.path.join(
+    os.path.dirname(kofacustom.nigeria.__file__), 'ftesting.zcml')
+FunctionalLayer = ZCMLLayer(ftesting_zcml, __name__, 'FunctionalLayer',
+                            allow_teardown=True)
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/tests/__init__.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/tests/__init__.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/tests/__init__.py	(revision 17238)
@@ -0,0 +1,1 @@
+# make this a package.
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/tests/test_foo.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/tests/test_foo.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/tests/test_foo.py	(revision 17238)
@@ -0,0 +1,27 @@
+# Make sure that test setup basically works.
+# THIS FILE CAN BE REMOVED.
+
+from kofacustom.nigeria.testing import FunctionalTestCase, FunctionalLayer
+
+class FooTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_foo(self):
+        # we can access registered components
+        from zope.component import getUtility
+        from waeup.kofa.applicants.interfaces import IApplicantsUtils
+        from kofacustom.nigeria.applicants.utils import NigeriaApplicantsUtils
+        result = getUtility(IApplicantsUtils)
+        self.assertTrue(isinstance(result, NigeriaApplicantsUtils))
+        return
+
+    def test_bar(self):
+        # we can access browser contents
+        from waeup.kofa.app import University
+        self.getRootFolder()['app'] = University()
+        from zope.testbrowser.testing import Browser
+        br = Browser()
+        br.open('http://localhost/app/')
+        self.assertTrue('WAeUP.Kofa' in br.contents)
+        return
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/utils/__init__.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/utils/__init__.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/utils/__init__.py	(revision 17238)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/utils/degrees.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/utils/degrees.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/utils/degrees.py	(revision 17238)
@@ -0,0 +1,277 @@
+DEGREES_DICT = {
+    'AGRB': 'Bachelor of Agriculture',
+    'BA': 'Bachelor of Arts',
+    'BA(ADMIN)': 'Bachelor of Arts in Administration',
+    'BACC': 'Bachelor of Accounting',
+    'BACOM': 'Bachelor of Arts in Commerce',
+    'BADMIN': 'Bachelor of Administration',
+    'BAE': 'Bachelor of Arts and Economics',
+    'BAECON': 'Bachelor of Arts in Economics',
+    'BA(ED)': 'Bachelor of Arts in Education',
+    'BA(FS)': 'Bachelor of Arts in Financial Studies',
+    'BAGR': 'Bachelor of Agriculture',
+    'BAH': 'Bachelor of Animal Health',
+    'BAI': 'Bachelor of Engineering (Arte Ingeniaria)',
+    'BAI(ELECT)': 'Bachelor of Electrical Engineering',
+    'BAI(MECH)': 'Bachelor of Mechanical Engineering',
+    'BALAW': 'Bachelor of Arts on Law',
+    'BAO': 'Bachelor of Obstetrics (Arte Obstetricia)',
+    'BAPPSC': 'Bachelor of Applied Science',
+    'BARCH': 'Bachelor of Architecture',
+    'BARCHSC': 'Bachelor of Architectural Science',
+    'BARELST': 'Bachelor of Arts on Religious Studies',
+    'BASC': 'Bachelor of Arts and Science',
+    'BASOC': 'Bachelor of Arts in Sociology',
+    'BASS': 'Bachelor of Arts in the Social Sciences',
+    'BATHEOL': 'Bachelor of Arts on Theology',
+    'BBA': 'Bachelor of Business Administration',
+    'BBLS': 'Bachelor of Business and Legal Studies',
+    'BBS': 'Bachelor of Business Studies',
+    'BBUS': 'Bachelor of Business',
+    'BCHEM': 'Bachelor of Chemistry',
+    'BCJ': 'Bachelor of Criminal Justice',
+    'BCL': 'Bachelor of Civil Law',
+    'BCLD(SOCSC)': 'Bachelor of Community Learning and Development in Social Sciences',
+    'BCLINSCI': 'Bachelor of Clinical Science',
+    'BCOM': 'Bachelor of Commerce',
+    'BCOMBST': 'Bachelor of Combined Studies',
+    'BCOMMEDCOMMDEV': 'Bachelor of Community Education and Community Development',
+    'BCOMP': 'Bachelor of Computing',
+    'BCOMSC': 'Bachelor of Commercial Science',
+    'BCOUN': 'Bachelor of Counseling',
+    'BD': 'Bachelor of Divinity',
+    'BDES': 'Bachelor of Design',
+    'BE': 'Bachelor of Engineering',
+    'BECON': 'Bachelor of Economics',
+    'BECON&FIN': 'Bachelor of Economics and Finance',
+    'BECONSCI': 'Bachelor of Science in Economics',
+    'BED': 'Bachelor of Education',
+    'BENG': 'Bachelor of Engineering',
+    'BES': 'Bachelor of Engineering Studies',
+    'BENG(TECH)': 'Bachelor of Engineering in Technology',
+    'BFA': 'Bachelor of Fine Art',
+    'BFIN': 'Bachelor of Finance',
+    'BFLS': 'Bachelor of Financial and Legal Studies',
+    'BFST': 'Bachelor of Food Science and Technology',
+    'BH': 'Bachelor of Humanities',
+    'BHEALTHSC': 'Bachelor of Health Sciences',
+    'BHSC': 'Bachelor of Health Science',
+    'BHY': 'Bachelor of Hygiene',
+    'BJUR': 'Bachelor of Jurisprudence',
+    'BL': 'Bachelor of Law',
+    'BLE': 'Bachelor of Land Economy',
+    'BLEGSC': 'Bachelor of Legal Science',
+    'BLIB': 'Bachelor of Librarianship',
+    'BLING': 'Bachelor of Linguistics',
+    'BLITT': 'Bachelor of Letters or Bachelor of Literature',
+    'BLITTCELT': 'Bachelor of Celtic Letters',
+    'BLS': 'Bachelor of Library Studies',
+    'BMEDSC': 'Bachelor of Medical Sciences or Bachelor of Biomedical Science',
+    'BMET': 'Bachelor of Metallurgy',
+    'BMID': 'Bachelor of Midwifery',
+    'BMIN': 'Bachelor of Ministry',
+    'BMS': 'Bachelor of Midwifery Studies',
+    'BMSC': 'Bachelor of Medical Science',
+    'BMSC': 'Bachelor of Biomedical science',
+    'BMS': 'Bachelor of Management Studies',
+    'BMUS': 'Bachelor of Music',
+    'BMUSED': 'Bachelor of Music Education',
+    'BMUSPERF': 'Bachelor of Music Performance',
+    'BN': 'Bachelor of Nursing',
+    'BNS': 'Bachelor of Nursing Studies',
+    'BNURS': 'Bachelor of Nursing',
+    'BOPTOM': 'Bachelor of Clinical Optometry',
+    'BPA': 'Bachelor of Public Administration',
+    'BPHARM': 'Bachelor of Pharmacy',
+    'BPHIL': 'Bachelor of Philosophy',
+    'BPHIL(ED)': 'Bachelor of Philosophy in Education',
+    'BPHYS': 'Bachelor of Physics',
+    'BPHYSIO': 'Bachelor of Physiotherapy',
+    'BPL': 'Bachelor of Planning',
+    'BRADIOG': 'Bachelor of Radiography',
+    'BSC': 'Bachelor of Science',
+    'BSCAGR': 'Bachelor of Science in Agriculture',
+    'BSC(DAIRY)': 'Bachelor of Science in Dairying',
+    'BSC(DOMSC)': 'Bachelor of Science in Domestic Science',
+    'BSCEC': 'Bachelor of Economic and Social Studies',
+    'BSCECON': 'Bachelor of Economic Science or Bachelor of Economic and Social Studies',
+    'BSC(ECON)': 'Bachelor of Science in Economics',
+    'BSC(ENG)': 'Bachelor of Science in Engineering',
+    'BSCFOR': 'Bachelor of Science in Forestry',
+    'BSC(HEALTHSC)': 'Bachelor of Science in Health Science',
+    'BSC(HORT)': 'Bachelor of Science in Horticulture',
+    'BSC(MCRM)': 'Bachelor of Science in Marine and Coastal Reserve Management',
+    'BSC(MED)': 'Bachelor of Science in Medicine',
+    'BSC(MID)': 'Bachelor of SHRMCP in Midwifery',
+    'BSC(MIN)': 'Bachelor of Science in Mining',
+    'BSCPH': 'Bachelor of Science in Public Health',
+    'BSC(PSYCH)': 'Bachelor of Science in Psychology',
+    'BSC(TECH)': 'Bachelor of Science in Technology',
+    'BSD': 'Bachelor of Science in Dentistry',
+    'BSOCSC': 'Bachelor of Social Sciences',
+    'BSS': 'Bachelor of Social Studies',
+    'BSTSU': 'Bachelor of Dead Studies',
+    'BTCHG': 'Bachelor of Teaching',
+    'BTCP': 'Bachelor of Town and County Planning',
+    'BTECH': 'Bachelor of Technology',
+    'BTECHED': 'Bachelor of Technological Education',
+    'BTH': 'Bachelor of Theology',
+    'BTHEOL': 'Bachelor of Theology',
+    'BTS': 'Bachelor of Theatre Studies',
+    'EDB': 'Bachelor of Education',
+    'LITTB': 'Bachelor of Literature or Bachelor of Letters',
+    'LLB': 'Bachelor of Laws',
+    'MA': 'Master of Arts',
+    'MUSB': 'Bachelor of Music',
+    'SCBTECH': 'Bachelor of Science in Technology',
+    'THB': 'Bachelor of Theology',
+    'BDS': 'Bachelor of Dental Surgery',
+    'BMBS': 'Bachelors of Medicine and Surgery',
+    'BMBCH': 'Bachelors of Medicine and Surgery',
+    'BVS': 'Bachelor of Veterinary Surgery',
+    'BVETM': 'Bachelor of Veterinary Medicine',
+    'BVETMED': 'Bachelor of Veterinary Medicine',
+    'BVM&S': 'Bachelor of Veterinary Medicine and Surgery',
+    'BVMEDSC OR BVSC': 'Bachelor of Veterinary Medical Science',
+    'BVSC': 'Bachelor of Veterinary Science',
+    'MACC': 'Master in Accountancy',
+    'MANTH': 'Master in Anthropology',
+    'MART': 'Master in Arts',
+    'MBBS': 'Bachelors of Medicine and Surgery',
+    'MBCHB': 'Bachelors of Medicine and Surgery',
+    'MBIOCHEM': 'Master of Biochemistry',
+    'MBIOL': 'Master of Biology',
+    'MBIOLSCI': 'Master of Biochemistry',
+    'MBUS': 'Master in Business and Management',
+    'MCHEM': 'Master of Chemistry',
+    'MCHEMPHYS': 'Master of Chemical Physics',
+    'MCHIRO': 'Master of Chiropractic',
+    'MCOMP': 'Master of Computer Science',
+    'MDIV': 'Master of Divinity',
+    'MDES': 'Master of Design',
+    'MDRAMA': 'Master of Drama',
+    'MEARTHSCI': 'MESci Master of Earth Science',
+    'MECON': 'Master of Economics',
+    'MENG': 'Master of Engineering',
+    'MENVSC': 'Master of Environmental Science',
+    'MGEOG': 'Master of Geography',
+    'MGEOL': 'Master of Geology',
+    'MGEOPHYS': 'Master of Geophysics',
+    'MSCI (MGEOSCI)': 'Master in Geoscience ',
+    'MIF': 'Master in Finance',
+    'MIM': 'Master in Management',
+    'MINF': 'Master of Informatics',
+    'MMARK': 'Master in Marketing',
+    'MMATH': 'Master of Mathematics',
+    'MMATHCOMP': 'Master of Computational Mathematics',
+    'MMATHCOMPSCI': 'Master of Mathematics and Computer Science',
+    'MMATHPHIL': 'Master of Mathematics and Philosophy',
+    'MMATHPHYS': 'Master of Mathematics and Physics, Master of Mathematical Physics',
+    'MMATHSTAT': 'Master of Mathematics and Statistics',
+    'MMET': 'Master of Meteorology',
+    'MMORSE': 'Master of Mathematics, Operational Research, Statistics and Economics',
+    'MMS': 'Master of Management Studies',
+    'MNATSC': 'Master of Natural Science',
+    'MNURSSC': 'Master of Nursing Science',
+    'MOCEAN': 'Master of Oceanography',
+    'MOPTOM': 'Master of Clinical Optometry',
+    'MPHARM': 'Master of Pharmacy',
+    'MPHYS': 'Master of Physics',
+    'MPHYSPHIL': 'Master of Physics and Philosophy',
+    'MPLAN': 'Master of Planning',
+    'MSCI': 'Master in Science',
+    'MSTAT': 'Master of Statistics',
+    'MTHEOL': 'Master of Theology',
+    'VETMB': 'Bachelor of Veterinary Medicineor BVSs',
+    'BCL': 'Bachelor of Civil Law (Oxford)',
+    'BPHIL': 'Bachelor of Philosophy (Oxford)',
+    'LLM': 'Master of Law',
+    'MA': 'Master of Arts',
+    'MARCH': 'Master of Architecture',
+    'MAST': 'Master of Advanced Study',
+    'MBA': 'Master of Business Administration',
+    'MBM': 'Master of Business and Management',
+    'MBIOLSC': 'Master of Biological Science',
+    'MCD': 'Master of Civic Design',
+    'MCH': 'Master of Surgery',
+    'MCL': 'Master of Corporate Law',
+    'MCLINDENT': 'Master of Clinical Dentistry',
+    'MDES': 'Master of Design',
+    'MED': 'Master of Education',
+    'MFA': 'Master of Fine Art',
+    'MJUR': 'Master of Jurisprudence',
+    'MLIB': 'Master of Librarianship',
+    'MLITT': 'Master of Letters',
+    'MMUS': 'Master of Music',
+    'MOST': 'Master of Osteopathy',
+    'MPA': 'Master of Public Administration',
+    'MPH': 'Master of Public Health',
+    'MPHIL': 'Master of Philosophy',
+    'MPP': 'Master of Public Policy',
+    'MPS': 'Master of Professional Studies',
+    'MPROF': 'Master of Professional Practice',
+    'MPROFST': 'Master of Professional Studies',
+    'MRES': 'Master of Research',
+    'MSC': 'Master of Science',
+    'MSOCSC': 'Master of Social Science',
+    'MSSC': 'Master of Social Science',
+    'MST': 'Master of Studies',
+    'MSW ': 'Master of Social Work',
+    'MTL': 'Master of Teaching and Learning',
+    'MTH': 'Master of Theology',
+    'MTHEOL': 'Master of Theology',
+    'MUNIV': 'Master of the University',
+    'MUSM': 'Master of Music',
+    'LLM': 'Master of Laws',
+    'DBA': 'Doctor of Business Administration',
+    'DCOUNSPYCH': 'Doctor of Counselling Psychology',
+    'DCLINPSYCH': 'Doctor of Clinical Psychology',
+    'DDS': 'Doctor of Dental Surgery',
+    'DED': 'Doctor of Education',
+    'DEDCHPSY': 'Doctor of Educational and Child Psychology',
+    'DENG': 'Doctor of Engineering',
+    'DFORENSPSY': 'Doctor of Forensic Psychology',
+    'DHEALTHPSY': 'Doctor of Health Psychology',
+    'DHSC': 'Doctor of Health Science',
+    'DHY': 'Doctor of Hygiene',
+    'DM': 'Doctor of Medicine',
+    'DMUS': 'Doctor of Music',
+    'DMIN': 'Doctor of Ministry',
+    'DNURSSC': 'Doctor of Nursing Science',
+    'DPROF': 'Doctor of Professional Studies',
+    'DPT': 'Doctor of Practical Theology',
+    'DTHM': 'Doctor of Theology and Ministry',
+    'EDCHPSYCHD': 'Doctor of Educational and Child Psychology',
+    'EDD': 'Doctor of Education',
+    'ENGD': 'Doctor of Engineering',
+    'HSCD': 'Doctor of Health Science',
+    'JD': 'Juris Doctor',
+    'MD': 'Doctor of Medicine',
+    'MUSD': 'Doctor of Music',
+    'SOCSCD': 'Doctor of Social Science',
+    'THD': 'Doctor of Theology',
+    'PHD': 'Doctor of Philosophy',
+    'DPHIL': 'Doctor of Philosophy',
+    'MD': 'Doctor of Medicine',
+    'MD(RES)': 'Doctor of Medicine (Research)',
+    'DD': 'Doctor of Divinity',
+    'DCL': 'Doctor of Civil Law',
+    'DLITT': 'Doctor of Letters',
+    'DLIT': 'Doctor of Literature',
+    'DM': 'Doctor of Medicine',
+    'DMUS': 'Doctor of Music',
+    'DSC': 'Doctor of Science',
+    'DSC(ECON)': 'Doctor of Science (Economics)',
+    'DSC(ENG)': 'Doctor of Science (Engineering)',
+    'DSC(MED)': 'Doctor of Science (Medicine)',
+    'DTECH': 'Doctor of Technology',
+    'DUNIV': 'Doctor of the University',
+    'ENGD': 'Doctor of Engineering',
+    'LITTD': 'Doctor of Letters',
+    'LLD': 'Doctor of Laws',
+    'SCD': 'Doctor of Science',
+    'PGD': 'Postgraduate Diploma',
+    'PGDE': 'Postgraduate Diploma in Education',
+    'PGDBM': 'Postgraduate Diploma in Business Administration',
+    'PGDPA': 'Post Graduate Diploma in Public Administration',
+    }
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/utils/lgas.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/utils/lgas.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/utils/lgas.py	(revision 17238)
@@ -0,0 +1,884 @@
+LGAS = (
+    ('abia_aba_north','Abia / Aba North'),
+    ('abia_aba_south','Abia / Aba South'),
+    ('abia_arochukwu','Abia / Arochukwu'),
+    ('abia_bende','Abia / Bende'),
+    ('abia_ikawuno','Abia / Ikawuno'),
+    ('abia_ikwuano','Abia / Ikwuano'),
+    ('abia_isiala-ngwa-north','Abia / Isiala-Ngwa-North'),
+    ('abia_isiala-ngwa-south','Abia / Isiala-Ngwa-South'),
+    ('abia_isikwuato','Abia / Isikwuato'),
+    ('abia_isuikwuato','Abia / Isuikwuato'),
+    ('abia_nneochi','Abia / Umu Nneochi'),
+    ('abia_obi_ngwa','Abia / Obi Ngwa'),
+    ('abia_obioma-ngwa','Abia / Obioma-Ngwa'),
+    ('abia_ohafia','Abia / Ohafia'),
+    ('abia_ohaozara','Abia / Ohaozara'),
+    ('abia_osisioma','Abia / Osisioma'),
+    ('abia_ugwunagbo','Abia / Ugwunagbo'),
+    ('abia_ukwa_west','Abia / Ukwa West'),
+    ('abia_ukwu_east','Abia / Ukwu East'),
+    ('abia_umuahia','Abia / Umuahia'),
+    ('abia_umuahia_south','Abia / Umuahia South'),
+    ('abia_umuahia-north','Abia / Umuahia-North'),
+    ('abuja_abaji','Abuja / Abaji'),
+    ('abuja_amac','Abuja / Amac'),
+    ('abuja_bwari','Abuja / Bwari'),
+    ('abuja_kuje','Abuja / Kuje'),
+    ('abuja_gwagwalada','Abuja / Gwagwalada'),
+    ('abuja_kwali','Abuja / Kwali'),
+    ('abuja_municipal','Abuja / Municipal'),
+    ('adamawa_demsa','Adamawa / Demsa'),
+    ('adamawa_fufore','Adamawa / Fufore'),
+    ('adamawa_ganye','Adamawa / Ganye'),
+    ('adamawa_girei','Adamawa / Girei'),
+    ('adamawa_gombi','Adamawa / Gombi'),
+    ('adamawa_guyuk','Adamawa / Guyuk'),
+    ('adamawa_hong','Adamawa / Hong'),
+    ('adamawa_jada','Adamawa / Jada'),
+    ('adamawa_jimeta','Adamawa / Jimeta'),
+    ('adamawa_lamurde','Adamawa / Lamurde'),
+    ('adamawa_madagali','Adamawa / Madagali'),
+    ('adamawa_maiha','Adamawa / Maiha'),
+    ('adamawa_mayo-belwa','Adamawa / Mayo-Belwa'),
+    ('adamawa_michika','Adamawa / Michika'),
+    ('adamawa_mubi-north','Adamawa / Mubi-North'),
+    ('adamawa_mubi-south','Adamawa / Mubi-South'),
+    ('adamawa_numan','Adamawa / Numan'),
+    ('adamawa_shelleng','Adamawa / Shelleng'),
+    ('adamawa_song','Adamawa / Song'),
+    ('adamawa_toungo','Adamawa / Toungo'),
+    ('adamawa_yola','Adamawa / Yola'),
+    ('akwa_ibom_abak','Akwa Ibom / Abak'),
+    ('akwa_ibom_eastern-obolo','Akwa Ibom / Eastern-Obolo'),
+    ('akwa_ibom_eket','Akwa Ibom / Eket'),
+    ('akwa_ibom_ekpe-atani','Akwa Ibom / Ekpe-Atani'),
+    ('akwa_ibom_esit_ekit','Akwa Ibom / Esit Ekit'),
+    ('akwa_ibom_essien-udim','Akwa Ibom / Essien-Udim'),
+    ('akwa_ibom_etim-ekpo','Akwa Ibom / Etim-Ekpo'),
+    ('akwa_ibom_etinam','Akwa Ibom / Etinam'),
+    ('akwa_ibom_ibeno','Akwa Ibom / Ibeno'),
+    ('akwa_ibom_ibesikpo-asutan','Akwa Ibom / Ibesikpo-Asutan'),
+    ('akwa_ibom_ibiono-ibom','Akwa Ibom / Ibiono-Ibom'),
+    ('akwa_ibom_ika','Akwa Ibom / Ika'),
+    ('akwa_ibom_ikono','Akwa Ibom / Ikono'),
+    ('akwa_ibom_ikot-abasi','Akwa Ibom / Ikot-Abasi'),
+    ('akwa_ibom_ikot-ekpene','Akwa Ibom / Ikot-Ekpene'),
+    ('akwa_ibom_ini','Akwa Ibom / Ini'),
+    ('akwa_ibom_itu','Akwa Ibom / Itu'),
+    ('akwa_ibom_mbo','Akwa Ibom / Mbo'),
+    ('akwa_ibom_mkpat-enin','Akwa Ibom / Mpat-Enin'),
+    ('akwa_ibom_nsit-atai','Akwa Ibom / Nsit-Atai'),
+    ('akwa_ibom_nsit-ibom','Akwa Ibom / Nsit-Ibom'),
+    ('akwa_ibom_nsit-ubium','Akwa Ibom / Nsit-Ubium'),
+    ('akwa_ibom_obot-akara','Akwa Ibom / Obot-Akara'),
+    ('akwa_ibom_okobo','Akwa Ibom / Okobo'),
+    ('akwa_ibom_onna','Akwa Ibom / Onna'),
+    ('akwa_ibom_oron','Akwa Ibom / Oron'),
+    ('akwa_ibom_oruk_anam','Akwa Ibom / Oruk-Anam'),
+    ('akwa_ibom_udung-uko','Akwa Ibom / Udung-Uko'),
+    ('akwa_ibom_ukanefun','Akwa Ibom / Ukanafun'),
+    ('akwa_ibom_uquo_ibeno','Akwa Ibom / Uquo Ibeno'),
+    ('akwa_ibom_urue-offong-oruko','Akwa Ibom / Urue-Offong-Oruko'),
+    ('akwa_ibom_uruan','Akwa Ibom / Uruan'),
+    ('akwa_ibom_uruk-anam','Akwa Ibom / Uruk-Anam'),
+    ('akwa_ibom_uyo','Akwa Ibom / Uyo'),
+    ('anambra_aguata','Anambra / Aguata'),
+    ('anambra_anambra-east','Anambra / Anambra-East'),
+    ('anambra_anambra-west','Anambra / Anambra-West'),
+    ('anambra_anaocha','Anambra / Anaocha'),
+    ('anambra_awka-north','Anambra / Awka-North'),
+    ('anambra_awka-south','Anambra / Awka-South'),
+    ('anambra_ayamelum','Anambra / Ayamelum'),
+    ('anambra_dunukofia','Anambra / Dunukofia'),
+    ('anambra_ekwusigo','Anambra / Ekwusigo'),
+    ('anambra_idemili','Anambra / Idemili'),
+    ('anambra_idemili-north','Anambra / Idemili-North'),
+    ('anambra_idemili-south','Anambra / Idemili-South'),
+    ('anambra_ihiala','Anambra / Ihiala'),
+    ('anambra_nibo','Anambra / Nibo'),
+    ('anambra_njikoka','Anambra / Njikoka'),
+    ('anambra_nnewi','Anambra / Nnewi'),
+    ('anambra_nnewi-north','Anambra / Nnewi-North'),
+    ('anambra_nnewi-south','Anambra / Nnewi-South'),
+    ('anambra_ogbaru','Anambra / Ogbaru'),
+    ('anambra_olumba','Anambra / Olumba'),
+    ('anambra_onitsha-north','Anambra / Onitsha-North'),
+    ('anambra_onitsha-south','Anambra / Onitsha-South'),
+    ('anambra_orumba-north','Anambra / Orumba-North'),
+    ('anambra_orumba-south','Anambra / Orumba-South'),
+    ('anambra_oti','Anambra / Oti'),
+    ('anambra_oyi','Anambra / Oyi'),
+    ('anambra_ubuluizor-ihiala','Anambra / Ubuluizor-Ihiala'),
+    ('anambra_uyi','Anambra / Uyi'),
+    ('bauchi_alkaleri','Bauchi / Alkaleri'),
+    ('bauchi_bauchi','Bauchi / Bauchi'),
+    ('bauchi_bogoro','Bauchi / Bogoro'),
+    ('bauchi_damban','Bauchi / Damban'),
+    ('bauchi_darazo','Bauchi / Darazo'),
+    ('bauchi_dass','Bauchi / Dass'),
+    ('bauchi_gamawa','Bauchi / Gamawa'),
+    ('bauchi_ganjuwa','Bauchi / Ganjuwa'),
+    ('bauchi_giade','Bauchi / Giade'),
+    ('bauchi_itas/gadau','Bauchi / Itas/Gadau'),
+    ('bauchi_jamaare','Bauchi / Jamaare'),
+    ('bauchi_katagum','Bauchi / Katagum'),
+    ('bauchi_kirfi','Bauchi / Kirfi'),
+    ('bauchi_misau','Bauchi / Misau'),
+    ('bauchi_ningi','Bauchi / Ningi'),
+    ('bauchi_shira','Bauchi / Shira'),
+    ('bauchi_tafawa-balewa','Bauchi / Tafawa-Balewa'),
+    ('bauchi_toro','Bauchi / Toro'),
+    ('bauchi_warji','Bauchi / Warji'),
+    ('bauchi_zaki','Bauchi / Zaki'),
+    ('bayelsa_brass','Bayelsa / Brass'),
+    ('bayelsa_ekeremor','Bayelsa / Ekeremor'),
+    ('bayelsa_kembe','Bayelsa / Kembe'),
+    ('bayelsa_kolokuma','Bayelsa / Kolokuma'),
+    ('bayelsa_kolokuma/opkuma','Bayelsa / Kolokuma/Opkuma'),
+    ('bayelsa_nembe','Bayelsa / Nembe'),
+    ('bayelsa_ogbia','Bayelsa / Ogbia'),
+    ('bayelsa_sagbama','Bayelsa / Sagbama'),
+    ('bayelsa_southern-ijaw','Bayelsa / Southern-Ijaw'),
+    ('bayelsa_toru-abubo','Bayelsa / Toru-Abubo'),
+    ('bayelsa_yenegoa','Bayelsa / Yenegoa'),
+    ('benue_ado','Benue / Ado'),
+    ('benue_agatu','Benue / Agatu'),
+    ('benue_apa','Benue / Apa'),
+    ('benue_buruku','Benue / Buruku'),
+    ('benue_gboko','Benue / Gboko'),
+    ('benue_guma','Benue / Guma'),
+    ('benue_gwer-east','Benue / Gwer-East'),
+    ('benue_gwer-west','Benue / Gwer-West'),
+    ('benue_katsina-ala','Benue / Katsina-Ala'),
+    ('benue_konshisha','Benue / Konshisha'),
+    ('benue_kwande','Benue / Kwande'),
+    ('benue_logo','Benue / Logo'),
+    ('benue_makurdi','Benue / Makurdi'),
+    ('benue_obi','Benue / Obi'),
+    ('benue_ogbadibo','Benue / Ogbadibo'),
+    ('benue_ohimini','Benue / Ohimini'),
+    ('benue_oju','Benue / Oju'),
+    ('benue_okpokwu','Benue / Okpokwu'),
+    ('benue_otukpo','Benue / Otukpo'),
+    ('benue_oturkpa','Benue / Oturkpa'),
+    ('benue_tarka','Benue / Tarka'),
+    ('benue_ukum','Benue / Ukum'),
+    ('benue_ushongo','Benue / Ushongo'),
+    ('benue_vandeikya','Benue / Vandeikya'),
+    ('borno_abadan','Borno / Abadan'),
+    ('borno_askira-uba','Borno / Askira-Uba'),
+    ('borno_bama','Borno / Bama'),
+    ('borno_bayo','Borno / Bayo'),
+    ('borno_biu','Borno / Biu'),
+    ('borno_chibok','Borno / Chibok'),
+    ('borno_damboa','Borno / Damboa'),
+    ('borno_dikwa','Borno / Dikwa'),
+    ('borno_gubio','Borno / Gubio'),
+    ('borno_guzamala','Borno / Guzamala'),
+    ('borno_gwoza','Borno / Gwoza'),
+    ('borno_hawul','Borno / Hawul'),
+    ('borno_jere','Borno / Jere'),
+    ('borno_kaga','Borno / Kaga'),
+    ('borno_kala/balge','Borno / Kala/Balge'),
+    ('borno_konduga','Borno / Konduga'),
+    ('borno_kukawa','Borno / Kukawa'),
+    ('borno_kwaya-kusar','Borno / Kwaya-Kusar'),
+    ('borno_mafa','Borno / Mafa'),
+    ('borno_magumeri','Borno / Magumeri'),
+    ('borno_maiduguri','Borno / Maiduguri'),
+    ('borno_marte','Borno / Marte'),
+    ('borno_mobbar','Borno / Mobbar'),
+    ('borno_mongunu','Borno / Mongunu'),
+    ('borno_ngala','Borno / Ngala'),
+    ('borno_nganzai','Borno / Nganzai'),
+    ('borno_shani','Borno / Shani'),
+    ('cross_river_abi','Cross River / Abi'),
+    ('cross_river_abuochichie','Cross River / Abuochichie'),
+    ('cross_river_akamkpa','Cross River / Akamkpa'),
+    ('cross_river_akpabuyo','Cross River / Akpabuyo'),
+    ('cross_river_bakassi','Cross River / Bakassi'),
+    ('cross_river_bekwara','Cross River / Bekwara'),
+    ('cross_river_biasi','Cross River / Biasi'),
+    ('cross_river_boki','Cross River / Boki'),
+    ('cross_river_calabar-municipal','Cross River / Calabar-Municipal'),
+    ('cross_river_calabar-south','Cross River / Calabar-South'),
+    ('cross_river_etunk','Cross River / Etunk'),
+    ('cross_river_ikom','Cross River / Ikom'),
+    ('cross_river_obanliku','Cross River / Obanliku'),
+    ('cross_river_obubra','Cross River / Obubra'),
+    ('cross_river_obudu','Cross River / Obudu'),
+    ('cross_river_odukpani','Cross River / Odukpani'),
+    ('cross_river_ogoja','Cross River / Ogoja'),
+    ('cross_river_ugep-north','Cross River / Ugep-North'),
+    ('cross_river_yakurr','Cross River / Yakurr'),
+    ('cross_river_yala','Cross River / Yala'),
+    ('delta_aniocha-north','Delta / Aniocha-North'),
+    ('delta_aniocha-south','Delta / Aniocha-South'),
+    ('delta_bomadi','Delta / Bomadi'),
+    ('delta_burutu','Delta / Burutu'),
+    ('delta_effurun','Delta / Effurun'),
+    ('delta_ethiope-east','Delta / Ethiope-East'),
+    ('delta_ethiope-west','Delta / Ethiope-West'),
+    ('delta_idu','Delta / Idu'),
+    ('delta_ika-ne','Delta / Ika-Ne'),
+    ('delta_ika-north-east','Delta / Ika-North-East'),
+    ('delta_ika-south','Delta / Ika-South'),
+    ('delta_idemili-south','Delta / Idemili-South'),
+    ('delta_isoko-north','Delta / Isoko-North'),
+    ('delta_isoko-south','Delta / Isoko-South'),
+    ('delta_ndokwa-east','Delta / Ndokwa-East'),
+    ('delta_ndokwa-north','Delta / Ndokwa-North'),
+    ('delta_ndokwa-south','Delta / Ndokwa-South'),
+    ('delta_ndokwa-west','Delta / Ndokwa-West'),
+    ('delta_okpe','Delta / Okpe'),
+    ('delta_okwuani','Delta / Okwuani'),
+    ('delta_oleh','Delta / Oleh'),
+    ('delta_oshielli-north','Delta / Oshielli-North'),
+    ('delta_oshimili','Delta / Oshimili'),
+    ('delta_oshimili-north','Delta / Oshimili-North'),
+    ('delta_oshimili-south','Delta / Oshimili-South'),
+    ('delta_patani','Delta / Patani'),
+    ('delta_sapele','Delta / Sapele'),
+    ('delta_udokwa','Delta / Udokwa'),
+    ('delta_udu','Delta / Udu'),
+    ('delta_ughelli-north','Delta / Ughelli-North'),
+    ('delta_ughelli-south','Delta / Ughelli-South'),
+    ('delta_ukwuani','Delta / Ukwuani'),
+    #('delta_urwie','Delta / Urwie'),
+    #('delta_uvie','Delta / Uvie'),
+    #('delta_uvwei','Delta / Uvwei'),
+    ('delta_uvwie','Delta / Uvwie'),
+    ('delta_warri-central','Delta / Warri-Central'),
+    ('delta_warri-north','Delta / Warri-North'),
+    ('delta_warri-south','Delta / Warri-South'),
+    ('delta_warri-south-west','Delta / Warri-South-West'),
+    ('ebonyi_abakaliki','Ebonyi / Abakaliki'),
+    ('ebonyi_afikpo-north','Ebonyi / Afikpo-North'),
+    ('ebonyi_afikpo-south','Ebonyi / Afikpo-South'),
+    ('ebonyi_bomadim','Ebonyi / Bomadim'),
+    ('ebonyi_ebonyi','Ebonyi / Ebonyi'),
+    ('ebonyi_ezza-north','Ebonyi / Ezza-North'),
+    ('ebonyi_ezza-south','Ebonyi / Ezza-South'),
+    ('ebonyi_ikwo','Ebonyi / Ikwo'),
+    ('ebonyi_ishielu','Ebonyi / Ishielu'),
+    ('ebonyi_ivo','Ebonyi / Ivo'),
+    ('ebonyi_izzi','Ebonyi / Izzi'),
+    ('ebonyi_obaukwu','Ebonyi / Obaukwu'),
+    ('ebonyi_ohakwu','Ebonyi / Ohakwu'),
+    ('ebonyi_ohaozara','Ebonyi / Ohaozara'),
+    ('ebonyi_onicha','Ebonyi / Onicha'),
+    ('ebonyi_ukaba','Ebonyi / Ukaba'),
+    ('edo_afokpella','Edo / Afokpella'),
+    ('edo_afuze','Edo / Afuze'),
+    ('edo_agbazilo','Edo / Agbazilo'),
+    ('edo_akoko-edo','Edo / Akoko-Edo'),
+    ('edo_egor','Edo / Egor'),
+    ('edo_esan-central','Edo / Esan-Central'),
+    ('edo_esan-north-east','Edo / Esan-North-East'),
+    ('edo_esan-north-west','Edo / Esan-North-West'),
+    ('edo_esan-south-east','Edo / Esan-South-East'),
+    ('edo_esan-west','Edo / Esan-West'),
+    ('edo_etsako-central','Edo / Etsako-Central'),
+    ('edo_etsako-east','Edo / Etsako-East'),
+    ('edo_etsako-west','Edo / Etsako-West'),
+    ('edo_igueben','Edo / Igueben'),
+    ('edo_ikpoba-okha','Edo / Ikpoba-Okha'),
+    #('edo_ohunmwode','Edo / Ohunmwode'), # duplicate
+    ('edo_ologbo','Edo / Ologbo'),
+    ('edo_opoji_irrua','Edo / Opoji Irrua'),
+    ('edo_oredo','Edo / Oredo'),
+    ('edo_orhionmwon','Edo / Orhionmwon'),
+    ('edo_ovia-north-east','Edo / Ovia-North-East'),
+    ('edo_ovia-north-west','Edo / Ovia-North-West'),
+    ('edo_ovia-south-east','Edo / Ovia-South-East'),
+    ('edo_ovia-south-west','Edo / Ovia-South-West'),
+    ('edo_owan','Edo / Owan'),
+    ('edo_owan-east','Edo / Owan-East'),
+    ('edo_owan-west','Edo / Owan-West'),
+    ('edo_ubiaja','Edo / Ubiaja'), 
+    ('edo_uhunmwonde','Edo / Uhunmwonde'),
+    ('ekiti_ado-ekiti','Ekiti / Ado-Ekiti'),
+    ('ekiti_aiyekire','Ekiti / Aiyekire'),
+    ('ekiti_efon','Ekiti / Efon'),
+    ('ekiti_ekiti-east','Ekiti / Ekiti-East'),
+    ('ekiti_ekiti-south-west','Ekiti / Ekiti-South-West'),
+    ('ekiti_ekiti-west','Ekiti / Ekiti-West'),
+    ('ekiti_emure/ise/orun','Ekiti / Emure/Ise/Orun'),
+    ('ekiti_gbonyin','Ekiti / Gbonyin'),
+    ('ekiti_ido-osi','Ekiti / Ido-Osi'),
+    ('ekiti_ijero','Ekiti / Ijero'),
+    ('ekiti_ikare','Ekiti / Ikare'),
+    ('ekiti_ikere','Ekiti / Ikere'),
+    ('ekiti_ikole','Ekiti / Ikole'),
+    ('ekiti_ilejemeje','Ekiti / Ilejemeje'),
+    ('ekiti_irepodun/ifelodun','Ekiti / Irepodun/Ifelodun'),
+    ('ekiti_ise-orun','Ekiti / Ise-Orun'),
+    ('ekiti_moba','Ekiti / Moba'),
+    ('ekiti_oye','Ekiti / Oye'),
+    ('enugu_aninri','Enugu / Aninri'),
+    ('enugu_awgu','Enugu / Awgu'),
+    ('enugu_enugu-east','Enugu / Enugu-East'),
+    ('enugu_enugu-north','Enugu / Enugu-North'),
+    ('enugu_enugu-south','Enugu / Enugu-South'),
+    ('enugu_ezeagu','Enugu / Ezeagu'),
+    ('enugu_igbo-etiti','Enugu / Igbo-Etiti'),
+    ('enugu_igbo-eze-north','Enugu / Igbo-Eze-North'),
+    ('enugu_igbo-eze-south','Enugu / Igbo-Eze-South'),
+    ('enugu_isi-uzo','Enugu / Isi-Uzo'),
+    ('enugu_nkanu-east','Enugu / Nkanu-East'),
+    ('enugu_nkanu-central','Enugu / Nkanu-Central'),
+    ('enugu_nkanu-west','Enugu / Nkanu-West'),
+    ('enugu_nsukka','Enugu / Nsukka'),
+    ('enugu_nukanu_east','Enugu / Nukanu East'),
+    ('enugu_oji-river','Enugu / Oji-River'),
+    ('enugu_udenu','Enugu / Udenu'),
+    ('enugu_udi','Enugu / Udi'),
+    ('enugu_uzo-uwani','Enugu / Uzo-Uwani'),
+    ('gombe_akko','Gombe / Akko'),
+    ('gombe_balanga','Gombe / Balanga'),
+    ('gombe_billiri','Gombe / Billiri'),
+    ('gombe_dukku','Gombe / Dukku'),
+    ('gombe_funakaye','Gombe / Funakaye'),
+    ('gombe_gombe','Gombe / Gombe'),
+    ('gombe_kaltungo','Gombe / Kaltungo'),
+    ('gombe_kwami','Gombe / Kwami'),
+    ('gombe_nafada/bajoga','Gombe / Nafada/Bajoga'),
+    ('gombe_shomgom','Gombe / Shomgom'),
+    ('gombe_yamaltu/deba','Gombe / Yamaltu/Deba'),
+    ('imo_aboh-mbaise','Imo / Aboh-Mbaise'),
+    ('imo_ahiazu-mbaise','Imo / Ahiazu-Mbaise'),
+    ('imo_dral-esat','Imo / Dral-Esat'),
+    ('imo_ehime-mbano','Imo / Ehime-Mbano'),
+    ('imo_ezinihitte','Imo / Ezinihitte'),
+    ('imo_ideato','Imo / Ideato'),
+    ('imo_ideato-north','Imo / Ideato-North'),
+    ('imo_ideato-south','Imo / Ideato-South'),
+    ('imo_ihitte/uboma','Imo / Ihitte/Uboma'),
+    ('imo_ikeduru','Imo / Ikeduru'),
+    ('imo_isiala-mbano','Imo / Isiala-Mbano'),
+    ('imo_isu','Imo / Isu'),
+    ('imo_mbaitoli','Imo / Mbaitoli'),
+    ('imo_mbano','Imo / Mbano'),
+    ('imo_ngor-okpala','Imo / Ngor-Okpala'),
+    ('imo_njaba','Imo / Njaba'),
+    ('imo_nkwerre','Imo / Nkwerre'),
+    ('imo_nwangele','Imo / Nwangele'),
+    ('imo_obowo','Imo / Obowo'),
+    ('imo_oguta','Imo / Oguta'),
+    ('imo_ohaji-egbema','Imo / Ohaji-Egbema'),
+    ('imo_okigwe','Imo / Okigwe'),
+    ('imo_onuimo','Imo / Onuimo'),
+    ('imo_orlu','Imo / Orlu'),
+    ('imo_orsu','Imo / Orsu'),
+    ('imo_oru-east','Imo / Oru-East'),
+    ('imo_oru-north','Imo / Oru-North'),
+    ('imo_oru-south','Imo / Oru-South'),
+    ('imo_oru-west','Imo / Oru-West'),
+    ('imo_owerri-municipal','Imo / Owerri-Municipal'),
+    ('imo_owerri-north','Imo / Owerri-North'),
+    ('imo_owerri-west','Imo / Owerri-West'),
+    ('imo_ugiri-ike_ikeduru','Imo / Ugiri-Ike Ikeduru'),
+    ('imo_ugiri-ikedikeduru','Imo / Ugiri-Ikedikeduru'),
+    ('imo_unbano','Imo / Unbano'),
+    ('imo_zinihitte','Imo / Zinihitte'),
+    ('jigawa_auyo','Jigawa / Auyo'),
+    ('jigawa_babura','Jigawa / Babura'),
+    ('jigawa_biriniwa','Jigawa / Biriniwa'),
+    ('jigawa_birnin-kudu','Jigawa / Birnin-Kudu'),
+    ('jigawa_bosuwa','Jigawa / Bosuwa'),
+    ('jigawa_buji','Jigawa / Buji'),
+    ('jigawa_dutse','Jigawa / Dutse'),
+    ('jigawa_gagarawa','Jigawa / Gagarawa'),
+    ('jigawa_garki','Jigawa / Garki'),
+    ('jigawa_gumel','Jigawa / Gumel'),
+    ('jigawa_guri','Jigawa / Guri'),
+    ('jigawa_gwaram','Jigawa / Gwaram'),
+    ('jigawa_gwiwa','Jigawa / Gwiwa'),
+    ('jigawa_hadejia','Jigawa / Hadejia'),
+    ('jigawa_jahun','Jigawa / Jahun'),
+    ('jigawa_kafin-hausa','Jigawa / Kafin-Hausa'),
+    ('jigawa_kaugama','Jigawa / Kaugama'),
+    ('jigawa_kazaure','Jigawa / Kazaure'),
+    ('jigawa_kirkasamma','Jigawa / Kirkasamma'),
+    ('jigawa_maigatari','Jigawa / Maigatari'),
+    ('jigawa_malam-maduri','Jigawa / Malam-Maduri'),
+    ('jigawa_miga','Jigawa / Miga'),
+    ('jigawa_ringim','Jigawa / Ringim'),
+    ('jigawa_roni','Jigawa / Roni'),
+    ('jigawa_sule-tankarkar','Jigawa / Sule-Tankarkar'),
+    ('jigawa_taura','Jigawa / Taura'),
+    ('jigawa_yankwashi','Jigawa / Yankwashi'),
+    ('kaduna_birnin-gwari','Kaduna / Birnin-Gwari'),
+    ('kaduna_chikun','Kaduna / Chikun'),
+    ('kaduna_giwa','Kaduna / Giwa'),
+    ('kaduna_gwagwada','Kaduna / Gwagwada'),
+    ('kaduna_igabi','Kaduna / Igabi'),
+    ('kaduna_ikara','Kaduna / Ikara'),
+    ('kaduna_jaba','Kaduna / Jaba'),
+    ('kaduna_jemaa','Kaduna / Jemaa'),
+    ('kaduna_kachia','Kaduna / Kachia'),
+    ('kaduna_kaduna-north','Kaduna / Kaduna-North'),
+    ('kaduna_kagarko','Kaduna / Kagarko'),
+    ('kaduna_kajuru','Kaduna / Kajuru'),
+    ('kaduna_kaura','Kaduna / Kaura'),
+    ('kaduna_kauru','Kaduna / Kauru'),
+    ('kaduna_koka/kawo','Kaduna / Koka/Kawo'),
+    ('kaduna_kubah','Kaduna / Kubah'),
+    ('kaduna_kudan','Kaduna / Kudan'),
+    ('kaduna_lere','Kaduna / Lere'),
+    ('kaduna_makarfi','Kaduna / Makarfi'),
+    ('kaduna_sabon-gari','Kaduna / Sabon-Gari'),
+    ('kaduna_sanga','Kaduna / Sanga'),
+    ('kaduna_soba','Kaduna / Soba'),
+    ('kaduna_tudun-wada/makera','Kaduna / Tudun-Wada/Makera'),
+    ('kaduna_zango-kataf','Kaduna / Zango-Kataf'),
+    ('kaduna_zaria','Kaduna / Zaria'),
+    ('kano_ajingi','Kano / Ajingi'),
+    ('kano_albasu','Kano / Albasu'),
+    ('kano_bagwai','Kano / Bagwai'),
+    ('kano_bebeji','Kano / Bebeji'),
+    ('kano_bichi','Kano / Bichi'),
+    ('kano_bunkure','Kano / Bunkure'),
+    ('kano_dala','Kano / Dala'),
+    ('kano_dambatta','Kano / Dambatta'),
+    ('kano_dawakin-kudu','Kano / Dawakin-Kudu'),
+    ('kano_dawakin-tofa','Kano / Dawakin-Tofa'),
+    ('kano_doguwa','Kano / Doguwa'),
+    ('kano_fagge','Kano / Fagge'),
+    ('kano_gabasawa','Kano / Gabasawa'),
+    ('kano_garko','Kano / Garko'),
+    ('kano_garun-mallam','Kano / Garun-Mallam'),
+    ('kano_gaya','Kano / Gaya'),
+    ('kano_gezawa','Kano / Gezawa'),
+    ('kano_gwale','Kano / Gwale'),
+    ('kano_gwarzo','Kano / Gwarzo'),
+    ('kano_kano-municipal','Kano / Kano-Municipal'),
+    ('kano_karaye','Kano / Karaye'),
+    ('kano_kibiya','Kano / Kibiya'),
+    ('kano_kiru','Kano / Kiru'),
+    ('kano_kumbotso','Kano / Kumbotso'),
+    ('kano_kunchi','Kano / Kunchi'),
+    ('kano_kura','Kano / Kura'),
+    ('kano_madobi','Kano / Madobi'),
+    ('kano_makoda','Kano / Makoda'),
+    ('kano_minjibir','Kano / Minjibir'),
+    ('kano_nasarawa','Kano / Nasarawa'),
+    ('kano_rano','Kano / Rano'),
+    ('kano_rimin-gado','Kano / Rimin-Gado'),
+    ('kano_rogo','Kano / Rogo'),
+    ('kano_shanono','Kano / Shanono'),
+    ('kano_sumaila','Kano / Sumaila'),
+    ('kano_takai','Kano / Takai'),
+    ('kano_tarauni','Kano / Tarauni'),
+    ('kano_tofa','Kano / Tofa'),
+    ('kano_tsanyawa','Kano / Tsanyawa'),
+    ('kano_tudun-wada','Kano / Tudun-Wada'),
+    ('kano_ungogo','Kano / Ungogo'),
+    ('kano_warawa','Kano / Warawa'),
+    ('kano_wudil','Kano / Wudil'),
+    ('katsina_bakori','Katsina / Bakori'),
+    ('katsina_batagarawa','Katsina / Batagarawa'),
+    ('katsina_batsari','Katsina / Batsari'),
+    ('katsina_baure','Katsina / Baure'),
+    ('katsina_bindawa','Katsina / Bindawa'),
+    ('katsina_charanchi','Katsina / Charanchi'),
+    ('katsina_dan-musa','Katsina / Dan-Musa'),
+    ('katsina_dandume','Katsina / Dandume'),
+    ('katsina_danji','Katsina / Danji'),
+    ('katsina_daura','Katsina / Daura'),
+    ('katsina_dutsi','Katsina / Dutsi'),
+    ('katsina_dutsinma','Katsina / Dutsinma'),
+    ('katsina_faskari','Katsina / Faskari'),
+    ('katsina_funtua','Katsina / Funtua'),
+    ('katsina_ingawa','Katsina / Ingawa'),
+    ('katsina_jibia','Katsina / Jibia'),
+    ('katsina_kafur','Katsina / Kafur'),
+    ('katsina_kaita','Katsina / Kaita'),
+    ('katsina_kankara','Katsina / Kankara'),
+    ('katsina_kankia','Katsina / Kankia'),
+    ('katsina_katsina','Katsina / Katsina'),
+    ('katsina_kurfi','Katsina / Kurfi'),
+    ('katsina_kusada','Katsina / Kusada'),
+    ('katsina_mai-adua','Katsina / Mai-Adua'),
+    ('katsina_malumfashi','Katsina / Malumfashi'),
+    ('katsina_mani','Katsina / Mani'),
+    ('katsina_mashi','Katsina / Mashi'),
+    ('katsina_matazu','Katsina / Matazu'),
+    ('katsina_musawa','Katsina / Musawa'),
+    ('katsina_rimi','Katsina / Rimi'),
+    ('katsina_sabuwa','Katsina / Sabuwa'),
+    ('katsina_safana','Katsina / Safana'),
+    ('katsina_sandamu','Katsina / Sandamu'),
+    ('katsina_zango','Katsina / Zango'),
+    ('kebbi_aleiro','Kebbi / Aleiro'),
+    ('kebbi_arewa-dandi','Kebbi / Arewa-Dandi'),
+    ('kebbi_argungu','Kebbi / Argungu'),
+    ('kebbi_augie','Kebbi / Augie'),
+    ('kebbi_bagudo','Kebbi / Bagudo'),
+    ('kebbi_birnin-kebbi','Kebbi / Birnin-Kebbi'),
+    ('kebbi_bumza','Kebbi / Bumza'),
+    ('kebbi_dandi','Kebbi / Dandi'),
+    ('kebbi_danko','Kebbi / Danko'),
+    ('kebbi_fakai','Kebbi / Fakai'),
+    ('kebbi_gwandu','Kebbi / Gwandu'),
+    ('kebbi_jega','Kebbi / Jega'),
+    ('kebbi_kalgo','Kebbi / Kalgo'),
+    ('kebbi_koko-besse','Kebbi / Koko-Besse'),
+    ('kebbi_maiyama','Kebbi / Maiyama'),
+    ('kebbi_ngaski','Kebbi / Ngaski'),
+    ('kebbi_sakaba','Kebbi / Sakaba'),
+    ('kebbi_shanga','Kebbi / Shanga'),
+    ('kebbi_suru','Kebbi / Suru'),
+    ('kebbi_wasagu','Kebbi / Wasagu'),
+    ('kebbi_yauri','Kebbi / Yauri'),
+    ('kebbi_zuru','Kebbi / Zuru'),
+    ('kogi_adavi','Kogi / Adavi'),
+    ('kogi_ajaokuta','Kogi / Ajaokuta'),
+    ('kogi_ankpa','Kogi / Ankpa'),
+    ('kogi_bassa','Kogi / Bassa'),
+    ('kogi_dekina','Kogi / Dekina'),
+    ('kogi_ibaji','Kogi / Ibaji'),
+    ('kogi_idah','Kogi / Idah'),
+    ('kogi_igalamela','Kogi / Igalamela'),
+    ('kogi_ijumu','Kogi / Ijumu'),
+    ('kogi_ikoyi-ijumu','Kogi / Ikoyi-Ijumu'),
+    ('kogi_kabba/bunu','Kogi / Kabba/Bunu'),
+    ('kogi_kogi','Kogi / Kogi'),
+    ('kogi_lokoja','Kogi / Lokoja'),
+    ('kogi_mopa-muro','Kogi / Mopa-Muro'),
+    ('kogi_obaji','Kogi / Obaji'),
+    ('kogi_ofu','Kogi / Ofu'),
+    ('kogi_ogori/magongo','Kogi / Ogori/Magongo'),
+    ('kogi_okehi','Kogi / Okehi'),
+    ('kogi_okene','Kogi / Okene'),
+    ('kogi_olamaboro','Kogi / Olamaboro'),
+    ('kogi_omala','Kogi / Omala'),
+    ('kogi_oru','Kogi / Oru'),
+    ('kogi_oyi','Kogi / Oyi'),
+    ('kogi_yagba-east','Kogi / Yagba-East'),
+    ('kogi_yagba-west','Kogi / Yagba-West'),
+    ('kwara_asa','Kwara / Asa'),
+    ('kwara_baruten','Kwara / Baruten'),
+    ('kwara_edu','Kwara / Edu'),
+    ('kwara_ekiti','Kwara / Ekiti'),
+    ('kwara_ilorin-east','Kwara / Ilorin-East'),
+    ('kwara_ilorin-south','Kwara / Ilorin-South'),
+    ('kwara_ilorin-west','Kwara / Ilorin-West'),
+    ('kwara_isin','Kwara / Isin'),
+    ('kwara_kaiama','Kwara / Kaiama'),
+    ('kwara_moro','Kwara / Moro'),
+    ('kwara_offa','Kwara / Offa'),
+    ('kwara_oke-ero','Kwara / Oke-Ero'),
+    ('kwara_oyun','Kwara / Oyun'),
+    ('kwara_pategi','Kwara / Pategi'),
+    ('kwara_ifelodun','Kwara / Ifelodun'),
+    ('kwara_irepodun','Kwara / Irepodun'),
+    ('lagos_agege','Lagos / Agege'),
+    ('lagos_ajeromi-ifelodun','Lagos / Ajeromi-Ifelodun'),
+    ('lagos_alimosho','Lagos / Alimosho'),
+    ('lagos_amuwo-odofin','Lagos / Amuwo-Odofin'),
+    ('lagos_apapa','Lagos / Apapa'),
+    ('lagos_badagry','Lagos / Badagry'),
+    ('lagos_epe','Lagos / Epe'),
+    ('lagos_eti-osa','Lagos / Eti-Osa'),
+    ('lagos_ibeju-lekki','Lagos / Ibeju-Lekki'),
+    ('lagos_ifako-ijaiye','Lagos / Ifako-Ijaiye'),
+    ('lagos_ikeja','Lagos / Ikeja'),
+    ('lagos_ikorodu','Lagos / Ikorodu'),
+    ('lagos_island','Lagos / Island'),
+    ('lagos_kosofe','Lagos / Kosofe'),
+    ('lagos_lagos-island','Lagos / Lagos-Island'),
+    ('lagos_lagos-mainland','Lagos / Lagos-Mainland'),
+    ('lagos_mushin','Lagos / Mushin'),
+    ('lagos_ojo','Lagos / Ojo'),
+    ('lagos_oshodi-isolo','Lagos / Oshodi-Isolo'),
+    ('lagos_shomolu','Lagos / Shomolu'),
+    ('lagos_somolu','Lagos / Somolu'),
+    ('lagos_suru-lere','Lagos / Suru-Lere'),
+    ('nassarawa_akwanga','Nassarawa / Akwanga'),
+    ('nassarawa_awe','Nassarawa / Awe'),
+    ('nassarawa_doma','Nassarawa / Doma'),
+    ('nassarawa_karu','Nassarawa / Karu'),
+    ('nassarawa_keana','Nassarawa / Keana'),
+    ('nassarawa_keffi','Nassarawa / Keffi'),
+    ('nassarawa_kokona','Nassarawa / Kokona'),
+    ('nassarawa_lafia','Nassarawa / Lafia'),
+    ('nassarawa_nassawara','Nassarawa / Nassawara'),
+    ('nassarawa_nassawara_eggon','Nassarawa / Nassawara Eggon'),
+    ('nassarawa_obi','Nassarawa / Obi'),
+    ('nassarawa_toto','Nassarawa / Toto'),
+    ('nassarawa_wambu','Nassarawa / Wambu'),
+    ('niger_agaie','Niger / Agaie'),
+    ('niger_agwara','Niger / Agwara'),
+    ('niger_bida','Niger / Bida'),
+    ('niger_borgu','Niger / Borgu'),
+    ('niger_bosso','Niger / Bosso'),
+    ('niger_chanchaga','Niger / Chanchaga'),
+    ('niger_edati','Niger / Edati'),
+    ('niger_gbako','Niger / Gbako'),
+    ('niger_gurara','Niger / Gurara'),
+    ('niger_katcha','Niger / Katcha'),
+    ('niger_kontagora','Niger / Kontagora'),
+    ('niger_lapai','Niger / Lapai'),
+    ('niger_lavun','Niger / Lavun'),
+    ('niger_magama','Niger / Magama'),
+    ('niger_mariga','Niger / Mariga'),
+    ('niger_mashegu','Niger / Mashegu'),
+    ('niger_mokwa','Niger / Mokwa'),
+    ('niger_muya','Niger / Muya'),
+    ('niger_paikoro','Niger / Paikoro'),
+    ('niger_rafi','Niger / Rafi'),
+    ('niger_rijau','Niger / Rijau'),
+    ('niger_shiroro','Niger / Shiroro'),
+    ('niger_suleja','Niger / Suleja'),
+    ('niger_tafa','Niger / Tafa'),
+    ('niger_wushishi','Niger / Wushishi'),
+    ('ogun_abeokuta-north','Ogun / Abeokuta-North'),
+    ('ogun_abeokuta-south','Ogun / Abeokuta-South'),
+    ('ogun_ado-odo/ota','Ogun / Ado-Odo/Ota'),
+    ('ogun_adoodo-ota','Ogun / Adoodo-Ota'),
+    ('ogun_ewekoro','Ogun / Ewekoro'),
+    ('ogun_idarapo','Ogun / Idarapo'),
+    ('ogun_iffo-otta','Ogun / Iffo-Otta'),
+    ('ogun_ifo','Ogun / Ifo'),
+    ('ogun_igbado-north','Ogun / Igbado-North'),
+    ('ogun_igbado-south','Ogun / Igbado-South'),
+    ('ogun_ijebu-east','Ogun / Ijebu-East'),
+    ('ogun_ijebu-north','Ogun / Ijebu-North'),
+    ('ogun_ijebu-north-east','Ogun / Ijebu-North-East'),
+    ('ogun_ijebu-ode','Ogun / Ijebu-Ode'),
+    ('ogun_ikenne','Ogun / Ikenne'),
+    ('ogun_ilishan_remo','Ogun / Ilishan Remo'),
+    ('ogun_ilugun-alaro','Ogun / Ilugun-Alaro'),
+    ('ogun_imeko-afon','Ogun / Imeko-Afon'),
+    ('ogun_ipokia','Ogun / Ipokia'),
+    ('ogun_lgayekpe_','Ogun / Lgayekpe'),
+    ('ogun_obafemi-owode','Ogun / Obafemi-Owode'),
+    ('ogun_odedah','Ogun / Odedah'),
+    ('ogun_odogbolu','Ogun / Odogbolu'),
+    ('ogun_ogun-waterside','Ogun / Ogun-Waterside'),
+    ('ogun_remo-north','Ogun / Remo-North'),
+    ('ogun_sagamu','Ogun / Sagamu'),
+    ('ogun_yewa','Ogun / Yewa'),
+    ('ondo_akoko','Ondo / Akoko'),
+    ('ondo_akoko-north','Ondo / Akoko-North'),
+    ('ondo_akoko-north-east','Ondo / Akoko-North-East'),
+    ('ondo_akoko-north-west','Ondo / Akoko-North-West'),
+    ('ondo_akoko-south','Ondo / Akoko-South'),
+    ('ondo_akoko-south-east','Ondo / Akoko-South-East'),
+    ('ondo_akoko-south-west','Ondo / Akoko-South-West'),
+    ('ondo_akure','Ondo / Akure'),
+    ('ondo_akure-north','Ondo / Akure-North'),
+    ('ondo_akure-south','Ondo / Akure-South'),
+    ('ondo_ese-odo','Ondo / Ese-Odo'),
+    ('ondo_idanre','Ondo / Idanre'),
+    ('ondo_ifedore','Ondo / Ifedore'),
+    ('ondo_ikale','Ondo / Ikale'),
+    ('ondo_ilaje','Ondo / Ilaje'),
+    ('ondo_ilaje-west','Ondo / Ilaje-West'),
+    ('ondo_ile-oluji-okeigbo','Ondo / Ile-Oluji-Okeigbo'),
+    ('ondo_irele','Ondo / Irele'),
+    ('ondo_odigbo','Ondo / Odigbo'),
+    ('ondo_okiti-pupa','Ondo / Okiti-Pupa'),
+    ('ondo_ondo','Ondo / Ondo'),
+    ('ondo_ondo-east','Ondo / Ondo-East'),
+    ('ondo_ondo-west','Ondo / Ondo-West'),
+    ('ondo_ose','Ondo / Ose'),
+    ('ondo_owo','Ondo / Owo'),
+    ('osun_atakumosa','Osun / Atakumosa'),
+    ('osun_atakumosa_east','Osun / Atakumosa East'),
+    ('osun_ayeda-ade','Osun / Ayeda-Ade'),
+    ('osun_ayedire','Osun / Ayedire'),
+    ('osun_boluwaduro','Osun / Boluwaduro'),
+    ('osun_boripe','Osun / Boripe'),
+    ('osun_ede','Osun / Ede'),
+    ('osun_ede-north','Osun / Ede North'),
+    ('osun_ede-south','Osun / Ede South'),
+    ('osun_egbedore','Osun / Egbedore'),
+    ('osun_ejigbo','Osun / Ejigbo'),
+    ('osun_ife','Osun / Ife'),
+    ('osun_ife-north','Osun / Ife-North'),
+    ('osun_ife-south','Osun / Ife-South'),
+    ('osun_ife-central','Osun / Ife-Central'),
+    ('osun_ife-east','Osun / Ife-East'),
+    ('osun_ifelodun','Osun / Ifelodun'),
+    ('osun_ila','Osun / Ila'),
+    ('osun_ilesa-east','Osun / Ilesa-East'),
+    ('osun_ilesa-north-east','Osun / Ilesa-North-East'),
+    ('osun_ilesa-west','Osun / Ilesa-West'),
+    ('osun_ilesha','Osun / Ilesha'),
+    ('osun_ilesha_west','Osun / Ilesha-West'),
+    ('osun_irepodun','Osun / Irepodun'),
+    ('osun_irewole','Osun / Irewole'),
+    ('osun_isokan','Osun / Isokan'),
+    ('osun_iwo','Osun / Iwo'),
+    ('osun_obokun','Osun / Obokun'),
+    ('osun_odo-otin','Osun / Odo-Otin'),
+    ('osun_ola_oluwa','Osun / Ola Oluwa'),
+    ('osun_olorunda','Osun / Olorunda'),
+    ('osun_ori-ade','Osun / Ori-Ade'),
+    ('osun_orolu','Osun / Orolu'),
+    ('osun_osogbo','Osun / Osogbo'),
+    ('oyo_afijio','Oyo / Afijio'),
+    ('oyo_akinyele','Oyo / Akinyele'),
+    ('oyo_atiba','Oyo / Atiba'),
+    ('oyo_atigbo','Oyo / Atigbo'),
+    ('oyo_egbeda','Oyo / Egbeda'),
+    ('oyo_ibadan-central','Oyo / Ibadan-Central'),
+    ('oyo_ibadan-north-east','Oyo / Ibadan-North-East'),
+    ('oyo_ibadan-north-west','Oyo / Ibadan-North-West'),
+    ('oyo_ibadan-south-east','Oyo / Ibadan-South-East'),
+    ('oyo_ibadan-south-west','Oyo / Ibadan-South-West'),
+    ('oyo_ibarapa-central','Oyo / Ibarapa-Central'),
+    ('oyo_ibarapa-east','Oyo / Ibarapa-East'),
+    ('oyo_ibarapa-north','Oyo / Ibarapa-North'),
+    ('oyo_ido','Oyo / Ido'),
+    ('oyo_ifedayo','Oyo / Ifedayo'),
+    ('oyo_ifeloju','Oyo / Ifeloju'),
+    ('oyo_irepo','Oyo / Irepo'),
+    ('oyo_iseyin','Oyo / Iseyin'),
+    ('oyo_itseiwaju','Oyo / Itseiwaju'),
+    ('oyo_iwajowa','Oyo / Iwajowa'),
+    ('oyo_kajola','Oyo / Kajola'),
+    ('oyo_lagelu','Oyo / Lagelu'),
+    ('oyo_odo-oluwa','Oyo / Odo-Oluwa'),
+    ('oyo_ogbomosho-north','Oyo / Ogbomosho-North'),
+    ('oyo_ogbomosho-south','Oyo / Ogbomosho-South'),
+    ('oyo_olorunsogo','Oyo / Olorunsogo'),
+    ('oyo_oluyole','Oyo / Oluyole'),
+    ('oyo_ona-ara','Oyo / Ona-Ara'),
+    ('oyo_orelope','Oyo / Orelope'),
+    ('oyo_ori-ire','Oyo / Ori-Ire'),
+    ('oyo_oyo-west','Oyo / Oyo-West'),
+    ('oyo_saki-east','Oyo / Saki-East'),
+    ('oyo_saki-west','Oyo / Saki-West'),
+    ('oyo_surulere','Oyo / Surulere'),
+    ('plateau_barkin-ladi','Plateau / Barkin-Ladi'),
+    ('plateau_bassa','Plateau / Bassa'),
+    ('plateau_bokkos','Plateau / Bokkos'),
+    ('plateau_jos-east','Plateau / Jos-East'),
+    ('plateau_jos-north','Plateau / Jos-North'),
+    ('plateau_jos-south','Plateau / Jos-South'),
+    ('plateau_kanam','Plateau / Kanam'),
+    ('plateau_kanke','Plateau / Kanke'),
+    ('plateau_langtang-north','Plateau / Langtang-North'),
+    ('plateau_langtang-south','Plateau / Langtang-South'),
+    ('plateau_mangu','Plateau / Mangu'),
+    ('plateau_mikang','Plateau / Mikang'),
+    ('plateau_pankshin','Plateau / Pankshin'),
+    ('plateau_quananpan','Plateau / Quananpan'),
+    ('plateau_riyom','Plateau / Riyom'),
+    ('plateau_shendam','Plateau / Shendam'),
+    ('plateau_wase','Plateau / Wase'),
+    ('rivers_aboa/odual','Rivers / Aboa/Odual'),
+    ('rivers_ahoada-east','Rivers / Ahoada-East'),
+    ('rivers_ahoada-west','Rivers / Ahoada-West'),
+    ('rivers_akukutoru','Rivers / Akukutoru'),
+    ('rivers_andoni','Rivers / Andoni'),
+    ('rivers_asari-toru','Rivers / Asari-Toru'),
+    ('rivers_bonny','Rivers / Bonny'),
+    ('rivers_buguma','Rivers / Buguma'),
+    ('rivers_degema','Rivers / Degema'),
+    ('rivers_eleme','Rivers / Eleme'),
+    ('rivers_elfane','Rivers / Elfane'),
+    ('rivers_emuoha','Rivers / Emuoha'),
+    ('rivers_etche','Rivers / Etche'),
+    ('rivers_gokana','Rivers / Gokana'),
+    ('rivers_ikwerre','Rivers / Ikwerre'),
+    ('rivers_khana','Rivers / Khana'),
+    ('rivers_obia/akpor','Rivers / Obia/Akpor'),
+    ('rivers_ogba-egbema-ndoni','Rivers / Ogba-Egbema-Ndoni'),
+    ('rivers_ogu/bolo','Rivers / Ogu/Bolo'),
+    ('rivers_okirika','Rivers / Okirika'),
+    ('rivers_omuma','Rivers / Omuma'),
+    ('rivers_opobo/nkoro','Rivers / Opobo/Nkoro'),
+    ('rivers_oyigbo','Rivers / Oyigbo'),
+    ('rivers_port-harcourt','Rivers / Port-Harcourt'),
+    ('rivers_tai','Rivers / Tai'),
+    ('sokoto_binji','Sokoto / Binji'),
+    ('sokoto_bodinga','Sokoto / Bodinga'),
+    ('sokoto_dange-shuni','Sokoto / Dange-Shuni'),
+    ('sokoto_gada','Sokoto / Gada'),
+    ('sokoto_goronyo','Sokoto / Goronyo'),
+    ('sokoto_gudu','Sokoto / Gudu'),
+    ('sokoto_gwadabawa','Sokoto / Gwadabawa'),
+    ('sokoto_illela','Sokoto / Illela'),
+    ('sokoto_kebbe','Sokoto / Kebbe'),
+    ('sokoto_kware','Sokoto / Kware'),
+    ('sokoto_raba','Sokoto / Raba'),
+    ('sokoto_sabo-birni','Sokoto / Sabo-Birni'),
+    ('sokoto_shagari','Sokoto / Shagari'),
+    ('sokoto_silame','Sokoto / Silame'),
+    ('sokoto_sokoto-north','Sokoto / Sokoto-North'),
+    ('sokoto_sokoto-south','Sokoto / Sokoto-South'),
+    ('sokoto_tambuwal','Sokoto / Tambuwal'),
+    ('sokoto_tangaza','Sokoto / Tangaza'),
+    ('sokoto_tureta','Sokoto / Tureta'),
+    ('sokoto_wamakko','Sokoto / Wamakko'),
+    ('sokoto_wurno','Sokoto / Wurno'),
+    ('sokoto_yabo','Sokoto / Yabo'),
+    ('taraba_ardo-kola','Taraba / Ardo-Kola'),
+    ('taraba_bali','Taraba / Bali'),
+    ('taraba_donga','Taraba / Donga'),
+    ('taraba_gashaka','Taraba / Gashaka'),
+    ('taraba_gassol','Taraba / Gassol'),
+    ('taraba_ibi','Taraba / Ibi'),
+    ('taraba_jalingo','Taraba / Jalingo'),
+    ('taraba_karim-lamido','Taraba / Karim-Lamido'),
+    ('taraba_kurmi','Taraba / Kurmi'),
+    ('taraba_lau','Taraba / Lau'),
+    ('taraba_oilingo','Taraba / Oilingo'),
+    ('taraba_sardauna','Taraba / Sardauna'),
+    ('taraba_takum','Taraba / Takum'),
+    ('taraba_ussa','Taraba / Ussa'),
+    ('taraba_wukari','Taraba / Wukari'),
+    ('taraba_yorro','Taraba / Yorro'),
+    ('taraba_zing','Taraba / Zing'),
+    ('yobe_bade','Yobe / Bade'),
+    ('yobe_borsali','Yobe / Borsali'),
+    ('yobe_damaturu','Yobe / Damaturu'),
+    ('yobe_fika','Yobe / Fika'),
+    ('yobe_fune','Yobe / Fune'),
+    ('yobe_geidam','Yobe / Geidam'),
+    ('yobe_gogaram','Yobe / Gogaram'),
+    ('yobe_gujba','Yobe / Gujba'),
+    ('yobe_gulani','Yobe / Gulani'),
+    ('yobe_jakusko','Yobe / Jakusko'),
+    ('yobe_karasuwa','Yobe / Karasuwa'),
+    ('yobe_machina','Yobe / Machina'),
+    ('yobe_nangere','Yobe / Nangere'),
+    ('yobe_nguru','Yobe / Nguru'),
+    ('yobe_potiskum','Yobe / Potiskum'),
+    ('yobe_tarmua','Yobe / Tarmua'),
+    ('yobe_yunusari','Yobe / Yunusari'),
+    ('zamfara_anka','Zamfara / Anka'),
+    ('zamfara_bakura','Zamfara / Bakura'),
+    ('zamfara_bukkuyum','Zamfara / Bukkuyum'),
+    ('zamfara_bungudu','Zamfara / Bungudu'),
+    ('zamfara_gumi','Zamfara / Gumi'),
+    ('zamfara_gusau','Zamfara / Gusau'),
+    ('zamfara_isa','Zamfara / Isa'),
+    ('zamfara_kaura-namoda','Zamfara / Kaura-Namoda'),
+    ('zamfara_kiyawa','Zamfara / Kiyawa'),
+    ('zamfara_maradun','Zamfara / Maradun'),
+    ('zamfara_maru','Zamfara / Maru'),
+    ('zamfara_shinkafi','Zamfara / Shinkafi'),
+    ('zamfara_talata-mafara','Zamfara / Talata-Mafara'),
+    ('zamfara_tsafe','Zamfara / Tsafe'),
+    ('zamfara_zurmi','Zamfara / Zurmi'),
+    ('osun_aiyedire','Osun / Aiyedire'),
+    ('cross_river_biase','Cross River / Biase'),
+    ('ogun_egbado-north','Ogun / Egbado-North'),
+    ('ogun_egbado-south','Ogun / Egbado-South'),
+    ('oyo_itesiwaju','Oyo / Itesiwaju'),
+    ('oyo_ogo-oluwa','Oyo / Ogo-Oluwa'),
+    ('ebonyi_ohaukwu','Ebonyi / Ohaukwu'),
+    ('ondo_okitipupa','Ondo / Okitipupa'),
+    ('osun_oriade','Osun / Oriade'),
+    ('benue_oturkpo','Benue / Oturkpo'),
+    ('oyo_oyo-east','Oyo / Oyo-East'),
+    ('abia_ukwa-east','Abia / Ukwa-East'),
+    ('akwa_ibom_urue-ffong-oruko','Akwa Ibom / Urue-Offong-Oruko'),
+    ('foreigner','Foreigner')
+    )
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/utils/utils.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/utils/utils.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/utils/utils.py	(revision 17238)
@@ -0,0 +1,367 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""Customize general helper utilities for Kofa.
+"""
+import os
+import pytz
+from waeup.kofa.utils.utils import KofaUtils, sorted_phone_prefixes
+from kofacustom.nigeria.interfaces import MessageFactory as _
+from kofacustom.nigeria.utils.degrees import DEGREES_DICT
+
+INT_PHONE_PREFIXES = [
+    (1, _('Nigeria'), '234'),
+    (99, _('Germany'), '49'),
+    (99, _('United States'), '1'),
+    (99, _('Niger'), '227'),
+    (99, _('Benin'), '229'),
+    (99, _('Cameroon'), '237'),
+    (99, _('United Kingdom'), '44'),
+    (99, _('France'), '33'),
+    (99, _('Australia'), '61'),
+    (99, _('Austria'), '43'),
+    (99, _('Canada'), '1'),
+    (99, _('China'), '86'),
+    (99, _('Cyprus'), '357'),
+    (99, _('Denmark'), '45'),
+    (99, _('Egypt'), '20'),
+    (99, _('Estonia'), '372'),
+    (99, _('Ethiopia'), '251'),
+    (99, _('Finland'), '358'),
+    (99, _('Gambia'), '220'),
+    (99, _('Georgia'), '995'),
+    (99, _('Ghana'), '233'),
+    (99, _('Greece'), '30'),
+    (99, _('Hong Kong'), '852'),
+    (99, _('Hungary'), '36'),
+    (99, _('Iceland'), '354'),
+    (99, _('India'), '91'),
+    (99, _('Ireland'), '353'),
+    (99, _('Italy'), '39'),
+    (99, _('Japan'), '81'),
+    (99, _('Kenya'), '254'),
+    (99, _('Luxembourg'), '352'),
+    (99, _('Malaysia'), '60'),
+    (99, _('Malta'), '356'),
+    (99, _('Netherlands'), '31'),
+    (99, _('New Zealand'), '64'),
+    (99, _('Norway'), '47'),
+    (99, _('Qatar'), '974'),
+    (99, _('South Africa'), '27'),
+    (99, _('Spain'), '34'),
+    (99, _('Sweden'), '46'),
+    (99, _('Switzerland'), '41'),
+    (99, _('Togo'), '228'),
+    (99, _('Turkey'), '90'),
+    (99, _('Ukraine'), '380'),
+    (99, _('United Arab Emirates'), '971'),
+    (99, _('Zambia'), '260'),
+    (99, _('Zimbabwe'), '263'),
+    ]
+
+class NigeriaKofaUtils(KofaUtils):
+    """A collection of methods subject to customization.
+    """
+    PORTAL_LANGUAGE = 'en'
+
+    PREFERRED_LANGUAGES_DICT = {
+        'en':(1, u'English'),
+        'fr':(2, u'Fran&ccedil;ais'),
+        'de':(3, u'Deutsch'),
+        'ha':(4, u'Hausa'),
+        'yo':(5, u'Yoruba'),
+        'ig':(6, u'Igbo'),
+        }
+
+    #: A function to return
+    @classmethod
+    def sorted_phone_prefixes(cls, data=INT_PHONE_PREFIXES, request=None):
+        return sorted_phone_prefixes(data, request)
+
+    DEGREES_DICT = DEGREES_DICT
+
+    EXAM_SUBJECTS_DICT = {
+        'accounts': 'Accounts',
+        'add_general_science': 'Add General Science',
+        'add_maths': 'Add Maths',
+        'adv_elect_install_maint': 'Adv. Electrical Installation and Maintenance',
+        'agricultural_science': 'Agricultural Science',
+        'applied_electricity': 'Applied Electricity',
+        'arabi': 'Arabic',
+        'arithmetics': 'Arithmetics',
+        'art': 'Art',
+        'auto_mechanics': 'Auto Mechanics',
+        'awaiting_results': 'Awaiting Results',
+        'basic_electricity': 'Basic Electricity',
+        'bible_knowledge': 'Bible Knowledge',
+        'biology': 'Biology',
+        'biology_alt_syl': 'Biology (Alt.Syl)',
+        'block_brick_concrete': 'Block Laying, Brick Laying and Concreting',
+        'book_keeping': 'Book Keeping',
+        'building_construction': 'Building Construction',
+        'business_management': 'Business Management',
+        'business_construction_management': 'Business/Construction Management (Adv)',
+        'chemistry': 'Chemistry',
+        'chemistry_alt_syl': 'Chemistry (Alt.Syl)',
+        'christian_religious_studies': 'Christian Religious Studies',
+        'class_teaching': 'Class Teaching',
+        'clerical_office_duties': 'Clerical Office Duties',
+        'clothing_and_textiles': 'Clothing and Textiles',
+        'commerce': 'Commerce',
+        'economics': 'Economics',
+        'edo': 'Edo Language',
+        'education': 'Education',
+        'efik': 'Efik',
+        'electronics': 'Electronics',
+        'elementary_surveying': 'Elementary Surveying',
+        'english_language': 'English Language',
+        'engineering_science': 'Engineering Science',
+        'financial_accounting': 'Financial Accounting',
+        'fine_art': 'Fine Art',
+        'food_and_nutrition': 'Food and Nutrition',
+        'french': 'French',
+        'further_mathematics': 'Further Mathematics',
+        'general_science': 'General Science',
+        'geography': 'Geography',
+        'german': 'German',
+        'government': 'Government',
+        'hausa': 'Hausa',
+        'hausa_literature': 'Hausa Literature',
+        'health_science': 'Health Science',
+        'history': 'History',
+        'home_economics': 'Home Economics',
+        'home_management': 'Home Management',
+        'human_biology': 'Human Biology',
+        'industrial_electrical_installation': 'Industrial Elect Installation (Adv)',
+        'intergrated_science': 'Intergrated Science',
+        'islamic_studies': 'Islamic Studies',
+        'literature_in_english': 'Literature in English',
+        'literature_in_nigerian_languages': 'Literature in Nigerian Languages',
+        'igbo': 'Igbo',
+        'igbo_literature': 'Igbo Literature',
+        'yoruba': 'Yoruba',
+        'yoruba_literature': 'Yoruba Literature',
+        'management_in_living': 'Management in Living',
+        'mathematics': 'Mathematics',
+        'metalwork': 'Metalwork',
+        'music': 'Music',
+        'nigerian_language': 'Nigerian Language',
+        'office_practice': 'Office Practice',
+        'physical_education': 'Physical Education',
+        'physical_health_education': 'Physical and Health Education',
+        'physics': 'Physics',
+        'physics_alt_syl': 'Physics (Alt.Syl)',
+        'principles_and_practice_of_education': 'Principles and Practice of Education',
+        'principles_of_cost_accounting': 'Principles of Cost Accounting',
+        'rural_science': 'Rural Science',
+        'science': 'Science',
+        'secretarial_duties': 'Secretarial Duties',
+        'shorthand': 'Shorthand',
+        'sierra_leone_studies': 'Sierra Leone Studies',
+        'sierra_leone_languages': 'Sierra Leone Languages',
+        'social_studies': 'Social Studies',
+        'statistics': 'Statistics',
+        'teaching_practice': 'Teaching Practice',
+        'technical_drawing': 'Technical Drawing',
+        'typewriting': 'Typewriting',
+        'visual_art': 'Visual Art',
+        'woodwork': 'Woodwork',
+        'winding_elect_machines': 'Winding of Elect Machines and Elect Eng Sc',
+        'first_written': 'First Written',
+        'second_written': 'Second Written',
+        'pract_tests': 'Practical Tests',
+        # Added 25/08/2014
+        'animal_husbandry_(alt_a)': 'Animal Husbandry (Alt A)',
+        'animal_husbandry_(alt_b)': 'Animal Husbandry (Alt B)',
+        'auto_electrical_work': 'Auto Electrical Work',
+        'auto_mechanical_work': 'Auto Mechanical Work',
+        'catering_craft_practice': 'Catering Craft Practice',
+        'civic_education': 'Civic Education',
+        'computer_studies': 'Computer Studies',
+        'cosmetology': 'Cosmetology',
+        'data_processing': 'Data Processing',
+        'dyeing_&_bleaching': 'Dyeing & Bleaching',
+        'electrical_installation_and_maintenance_work': 'Electrical Installation and Maintenance Work',
+        'fisheries_(alt_a)_ghana_candidates': 'Fisheries (Alt A) Ghana Candidates',
+        'fisheries_(alt_b)_nigeria_candidates': 'Fisheries (Alt B) Nigeria Candidates',
+        'garment_making': 'Garment Making',
+        'general_agriculture': 'General Agriculture',
+        'gsm_phones_maintenance_and_repairs': 'Gsm Phones Maintenance and Repairs',
+        'health_education': 'Health Education',
+        'insurance': 'Insurance',
+        'painting_and_decorating': 'Painting and Decorating',
+        'photography': 'Photography',
+        'printing_craft_practice': 'Printing Craft Practice',
+        'store_management': 'Store Management',
+        'textiles': 'Textiles',
+        # Added after 19/01/2015
+        'marketing': 'Marketing',
+        'carpentry_joinery': 'Carpentry and Joinery',
+        'tourism': 'Tourism',
+        'mining': 'Mining',
+        'furniture_making': 'Furniture Making',
+        'information_communication_technology': 'Information and Communication Technology',
+        'salesmanship': 'Salesmanship',
+        'welding_and_fabrication': 'Welding and Fabrication',
+        }
+
+    EXAM_GRADES = (
+        ('A1', 'Excellent (A1)'),
+        ('A2', 'Very Good (A2)'),
+        ('A3', 'Good (A3)'),
+        ('B2', 'Very Good (B2)'),
+        ('B3', 'Good (B3)'),
+        ('C4', 'Credit (C4)'),
+        ('C5', 'Credit (C5)'),
+        ('C6', 'Credit (C6)'),
+        ('A', 'Pass (A)'),
+        ('B', 'Pass (B)'),
+        ('C', 'Pass (C)'),
+        ('U', 'Pass (U)'),
+        ('D7', 'Pass (D7)'),
+        ('P7', 'Pass (P7)'),
+        ('P8', 'Pass (P8)'),
+        ('E8', 'Pass (E8)'),
+        ('F9', 'Fail (F9)'),
+        ('Aa', 'A (a)'),
+        ('Bb', 'B (b)'),
+        ('Cc', 'C (c)'),
+        ('Dd', 'D (d)'),
+        ('Ee', 'E (e)'),
+        ('Ff', 'F (f)'),
+        ('Gg', 'G (g)'),
+        ('credit-1', 'Credit-1'),
+        ('credit-2', 'Credit-2'),
+        ('dist', 'Distinction'),
+        ('pass', 'Pass'),  
+        ('AR', 'Awaiting Results'),
+        )
+
+    INST_TYPES_DICT = {
+        'none': '',
+        'faculty': 'Faculty of',
+        'department': 'Department of',
+        'school': 'School of',
+        'office': 'Office for',
+        'centre': 'Centre for',
+        'centre_of': 'Centre of',
+        'institute': 'Institute of',
+        'school_for': 'School for',
+        'college': 'College of',
+        'directorate': 'Directorate of',
+        }
+
+    STUDY_MODES_DICT = {
+        'ume_ft': 'UME Full Time',
+        'ug_ft': 'Undergraduate Full Time',
+        'ug_pt': 'Undergraduate Part Time',
+        'dp_pt': 'Diploma Part Time',
+        'ct_ft': 'Certificate Full Time',
+        'dp_ft': 'Diploma Full Time',
+        'de_ft': 'Direct Entry Full Time',
+        'de_pt': 'Direct Entry Part Time',
+        'nd_ft': 'National Diploma Full Time',
+        'nd_pt': 'National Diploma Part Time',
+        'hnd_ft': 'Higher National Diploma Full Time',
+        'hnd_pt': 'Higher National Diploma Part Time',
+        'pg_ft': 'Postgraduate Full Time',
+        'pg_pt': 'Postgraduate Part Time',
+        'pgd_ft': 'Postgraduate Diploma Full Time',
+        'pgd_pt': 'Postgraduate Diploma Part Time',
+        'pgphd': 'Postgraduate PhD/MPhil',
+        'special_pg_pt': 'Special Postgraduate Part Time',
+        'nce_ft': 'NCE Full Time',
+        'nce_pt': 'NCE Part Time',
+        'ug_sw': 'Undergraduate Sandwich',
+        'nce_sw': 'NCE Sandwich',
+        'jm_ft': 'Joint Matriculation Full Time',
+        'ph_ft': 'Post Higher Education Full Time',
+        'transfer_pt': 'Transfer Part Time',
+        'transfer_ft': 'Transfer Full Time',
+        'ct_pt': 'Certificate Part Time',
+        'rmd_ft': 'Remedial with deficiencies',
+        'rm_ft': 'Remedial',
+        'transfer': 'Transfer',
+        'prence': 'Pre-NCE',
+        'prend': 'Pre-ND',
+        'pce': 'PCE',
+        'pd_ft': 'Professional Diploma in Education',
+        'found': 'Foundation',
+        'no': 'no application',
+        }
+
+    APP_CATS_DICT = {
+        'basic': 'UAS, PUTME, PUDE, PCE, PRENCE',
+        'no': 'No Application',
+        'pg_ft': 'Postgraduate Full-Time',
+        'pg_pt': 'Postgraduate Part-Time',
+        'sandwich': 'Sandwich',
+        'cest': 'Part-Time, Diploma, Certificate',
+        'pre': 'Pre-Degree Studies',
+        }
+
+    VERDICTS_DICT = {
+        '0': 'not yet',
+        'A': 'Successful student',
+        'B': 'Student with carryover courses',
+        'C': 'Student on probation',
+        'D': 'Withdrawn from the faculty',
+        'E': 'Student who were previously on probation',
+        'F': 'Medical case',
+        'G': 'Absent from examination',
+        'H': 'Withheld results',
+        'I': 'Expelled/rusticated/suspended student',
+        'J': 'Temporary withdrawn from the university',
+        'K': 'Unregistered student',
+        'L': 'Referred student',
+        'M': 'Reinstatement',
+        'N': 'Student on transfer',
+        #'O': 'NCE-III repeater', # FCEOkene only
+        'P': 'Exhausted maximum years in the university',
+        'R': 'Register Excess Credit',
+        'Y': 'No previous verdict',
+        'X': 'New 300 level student (Uniben)',
+        'Z': 'Successful student (provisional)',
+        'A1': 'First Class',
+        'A2': 'Second Class Upper',
+        'A3': 'Second Class Lower',
+        'A4': 'Third Class',
+        'A5': 'Pass',
+        'A6': 'Distinction',
+        'A7': 'Credit',
+        'A8': 'Merit',
+        'A9': 'Cert Not Classified',
+        'A10': 'Upper Credit',
+        'A11': 'Lower Credit',
+        }
+
+    DISABILITIES_DICT = {
+        'cripple': 'Paraplegia (cripple)',
+        'deaf': 'Deaf',
+        'dumb': 'Dumb',
+        'blind': 'Blind',
+        'others': 'Others',
+        }
+
+    GRADING_SYSTEM_DICT = {
+        'A': 'Regular Course Grading System (2018/2019 till date)',
+        'B': 'Old Course Grading System (2012/2013 - 2017/2018)',
+        }
+
+    @property
+    def tzinfo(self):
+        return pytz.timezone('Africa/Lagos')
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/widgets/__init__.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/widgets/__init__.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/widgets/__init__.py	(revision 17238)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/widgets/phonewidget.py
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/widgets/phonewidget.py	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/src/kofacustom/nigeria/widgets/phonewidget.py	(revision 17238)
@@ -0,0 +1,28 @@
+## $Id$
+##
+## Copyright (C) 2011 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
+##
+"""
+A customized phone widget
+"""
+
+from waeup.kofa.widgets.phonewidget import PhoneWidget
+
+class NigeriaPhoneWidget(PhoneWidget):
+    """A phone widget without area prefix.
+    """
+    subwidget_names = ('country', 'ext')
+    bootstrap_span = 'span3'
Index: main/kofacustom.nigeria/branches/uli-paypal/versions.cfg
===================================================================
--- main/kofacustom.nigeria/branches/uli-paypal/versions.cfg	(revision 17238)
+++ main/kofacustom.nigeria/branches/uli-paypal/versions.cfg	(revision 17238)
@@ -0,0 +1,6 @@
+# Versions of packages used by kofacustom.nigeria
+[buildout]
+extends = sources/waeup.kofa/versions.cfg
+versions = versions
+
+[versions]
