Index: main/waeup.ikoba/trunk/src/waeup/ikoba/app.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/app.py	(revision 12706)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/app.py	(revision 12741)
@@ -35,4 +35,5 @@
 from waeup.ikoba.products.container import ProductsContainer
 from waeup.ikoba.documents.container import DocumentsContainer
+from waeup.ikoba.payments.container import PaymentsContainer
 
 
@@ -59,5 +60,4 @@
         the like.
         """
-
         self['users'] = UsersContainer()
         self['datacenter'] = DataCenter()
@@ -67,4 +67,5 @@
         self['products'] = ProductsContainer()
         self['documents'] = DocumentsContainer()
+        self['payments'] = PaymentsContainer()
         self._createPlugins()
 
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser.py	(revision 12706)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser.py	(revision 12741)
@@ -18,17 +18,11 @@
 """UI components for customers and related components.
 """
-
-import sys
 import grok
-import pytz
 import os
 from urllib import urlencode
-from datetime import datetime
 from zope.event import notify
 from zope.i18n import translate
 from zope.catalog.interfaces import ICatalog
 from zope.component import queryUtility, getUtility, createObject
-from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
-from zope.formlib.textwidgets import BytesDisplayWidget
 from zope.security import checkPermission
 from hurry.workflow.interfaces import (
@@ -42,23 +36,25 @@
 from waeup.ikoba.browser.layout import (
     IkobaPage, IkobaEditFormPage, IkobaAddFormPage, IkobaDisplayFormPage,
-    IkobaForm, NullValidator, jsaction, action, UtilityView)
-from waeup.ikoba.widgets.datewidget import (
-    FriendlyDateWidget, FriendlyDateDisplayWidget,
-    FriendlyDatetimeDisplayWidget)
+    NullValidator, jsaction, action, UtilityView)
 from waeup.ikoba.browser.pages import ContactAdminForm
 from waeup.ikoba.browser.breadcrumbs import Breadcrumb
 from waeup.ikoba.browser.interfaces import ICaptchaManager
 from waeup.ikoba.mandates.mandate import PasswordMandate
+from waeup.ikoba.payments.payment import format_payment_item_values
+from waeup.ikoba.payments.interfaces import (
+    IPaymentGatewayServicesLister, IPaymentGatewayService, IPayer, IPayable
+    )
 from waeup.ikoba.widgets.hrefwidget import HREFDisplayWidget
 from waeup.ikoba.utils.helpers import (
-    get_current_principal, to_timezone, now, format_date)
+    get_current_principal, format_date)
 from waeup.ikoba.customers.interfaces import (
     ICustomer, ICustomersContainer, ICustomerRequestPW, ICustomersUtils,
     ICustomerDocument, ICustomerDocumentsContainer, ICustomerCreate,
     ICustomerPDFDocument, IContractsContainer, IContract,
-    IContractSelectProduct, ISampleContract,
+    IContractSelectProduct,
     )
 from waeup.ikoba.customers.catalog import search
 from waeup.ikoba.customers.workflow import PAYMENT_TRANSITIONS
+
 
 grok.context(IIkobaObject)
@@ -927,5 +923,4 @@
     def createDocument(self, **data):
         form = self.request.form
-        customer = self.context.__parent__
         doctype = form.get('doctype', None)
         # Here we can create various instances of CustomerDocument derived 
@@ -1098,5 +1093,4 @@
         tableheader = []
         tabledata = []
-        contenttitle = []
         for i in range(1,3):
             tabledata.append(sorted(
@@ -1134,5 +1128,4 @@
     @property
     def label(self):
-        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
         return '%s of %s\nTitle: %s' % (
             self.context.translated_class_name,
@@ -1141,5 +1134,4 @@
 
     def render(self):
-        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
         customerview = CustomerBasePDFFormPage(self.context.customer,
             self.request, self.omit_fields)
@@ -1157,5 +1149,4 @@
 
     def render(self):
-        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
         customerview = CustomerBasePDFFormPage(self.context.customer,
             self.request, self.omit_fields)
@@ -1295,5 +1286,4 @@
     def createContract(self, **data):
         form = self.request.form
-        customer = self.context.__parent__
         contype = form.get('contype', None)
         # Here we can create various instances of Contract derived
@@ -1506,17 +1496,49 @@
     label = _('Select payment method')
 
-    def update(self, CANCEL=None):
+    @property
+    def payment_gateways(self):
+        """Get an iter over registered and enabled gateway service providers.
+
+        We provide tuples ``(value, description)`` for each supported
+        payment gateway.
+        """
+        lister = getUtility(IPaymentGatewayServicesLister)
+        for name, service in lister().items():
+            yield {'name': name, 'title': service.title}
+
+    def update(self, CANCEL=None, gw=None):
         if self.context.state != CREATED or not self.context.fee_based:
             emit_lock_message(self)
             return
+        self.gw = gw
         super(SelectPaymentMethodPage, self).update()
         return
 
-    @action(_('Select payment method and proceed to payment gateway (final submission)'),
-            style='primary', warning=WARNING_CON,)
+    @action(_('Select payment method (final submission)'),
+            style='primary')
     def confirm(self, **data):
-        IWorkflowInfo(self.context).fireTransition('await')
-        self.flash(_('Payment has been initiated.'))
-        return
+        if self.gw is None:
+            self.flash(_('Please pick a payment method.'),
+                       type='warning')
+        else:
+            service = queryUtility(IPaymentGatewayService, name=self.gw)
+            if service is None:
+                self.flash(_('Invalid payment gateway.'), type='danger')
+                return
+            payer = IPayer(self.context)
+            payable = IPayable(self.context)
+            payment = service.create_payment(payer, payable)
+            service.store(payment)
+            payment, view_name = service.next_step(payment.payment_id)
+            url = self.url(payment, view_name)
+            self.redirect(url)
+            return
+        return
+
+    @action(_('Cancel'))
+    def cancel(self, **data):
+        self.redirect(self.url(self.context, 'edit'))
+        return
+
 
 class ContractTriggerTransitionFormPage(IkobaEditFormPage):
@@ -1600,5 +1622,4 @@
         tableheader = []
         tabledata = []
-        contenttitle = []
         for i in range(1,3):
             tabledata.append(sorted(
@@ -1655,5 +1676,4 @@
     @property
     def label(self):
-        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
         return self.context.title
 
@@ -1670,5 +1690,4 @@
 
     def render(self):
-        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
         customerview = CustomerBasePDFFormPage(self.context.customer,
             self.request, self.omit_fields)
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/selectpaymentmethodpage.pt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/selectpaymentmethodpage.pt	(revision 12706)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/selectpaymentmethodpage.pt	(revision 12741)
@@ -1,20 +1,17 @@
+
 <form action="." tal:attributes="action request/URL" method="post"
       i18n:domain="waeup.ikoba" enctype="multipart/form-data">
 
-  <table class="form-table">
-    <tbody>
-      <tr>
-        <td class="fieldname" i18n:translate=""></td>
-        <td>
-        </td>
-      </tr>
-      <tr>
-        <td class="fieldname" i18n:translate=""></td>
-        <td>
+  <div class="form-group">
 
-        </td>
-      </tr>
-    </tbody>
-  </table>
+    <div class="radio" tal:repeat="service view/payment_gateways">
+      <label>
+        <input type="radio" name="gw" tal:attributes="value service/name" />
+        <b><span tal:replace="service/title" /></b>
+      </label>
+    </div>
+
+  </div>
+
   <br />
 
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/contracts.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/contracts.py	(revision 12706)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/contracts.py	(revision 12741)
@@ -19,23 +19,27 @@
 Customer contract components.
 """
-import os
 import grok
-from zope.component import queryUtility, getUtility
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from zope.catalog.interfaces import ICatalog
+from zope.component import getUtility, queryUtility
 from zope.component.interfaces import IFactory
 from zope.interface import implementedBy
 from zope.schema import getFields
-from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
 from waeup.ikoba.interfaces import MessageFactory as _
 from waeup.ikoba.interfaces import (
-    IIkobaUtils, IObjectHistory,
-    VERIFIED, APPROVED, PROVISIONALLY,
-    IIDSource)
+    IObjectHistory, VERIFIED, APPROVED, PROVISIONALLY, IIDSource)
 from waeup.ikoba.customers.interfaces import (
-    IContractsContainer, ICustomerNavigation,
-    IContract, IContractSelectProduct, ICustomersUtils,
-    ISampleContract, ISampleContractProcess, ISampleContractEdit,
-    ISampleContractOfficialUse)
-from waeup.ikoba.customers.utils import generate_contract_id
+    IContractsContainer, ICustomerNavigation, IContract,
+    IContractSelectProduct, ICustomersUtils, ISampleContract,
+    ISampleContractProcess, ISampleContractEdit, ISampleContractOfficialUse)
+from waeup.ikoba.payments.interfaces import (
+    IPayer, IPayableFinder, IPayable, IPaymentWaitingForGatewayEvent,
+    STATE_PAID, STATE_FAILED, IPaymentFinishedEvent
+    )
+from waeup.ikoba.payments.payment import (
+    PaymentItem, find_payable_from_payable_id,
+    )
 from waeup.ikoba.utils.helpers import attrs_to_fields
+
 
 class ContractsContainer(grok.Container):
@@ -61,9 +65,36 @@
 ContractsContainer = attrs_to_fields(ContractsContainer)
 
+
+class ContractPayer(grok.Adapter):
+    """Adapter to turn contracts into IPayers.
+    """
+    grok.implements(IPayer)
+    grok.context(IContract)
+
+    def __init__(self, context):
+        self.context = context
+
+    @property
+    def _customer(self):
+        return self.context.customer
+
+    @property
+    def first_name(self):
+        return getattr(self._customer, 'firstname', None)
+
+    @property
+    def last_name(self):
+        return getattr(self._customer, 'lastname', None)
+
+    @property
+    def payer_id(self):
+        return getattr(self._customer, 'customer_id', None)
+
+
 class ContractBase(grok.Container):
     """This is a customer contract baseclass.
     """
-    grok.implements(IContractSelectProduct)  # Necesary for the selectproduct page
-
+    grok.implements(IContractSelectProduct)  # Neccessary for the
+                                             # selectproduct page (why?)
     grok.baseclass()
 
@@ -150,5 +181,6 @@
                 state = getattr(obj, 'state', None)
                 if state and state != VERIFIED:
-                    return False, _("Attached documents must be verified first.")
+                    return False, _(
+                        "Attached documents must be verified first.")
         return True, None
 
@@ -177,5 +209,5 @@
 
     grok.implements(
-        ISampleContractProcess, # must come before ISampleContract
+        ISampleContractProcess,  # must come before ISampleContract
         ISampleContract,
         ISampleContractEdit,
@@ -210,4 +242,5 @@
         return implementedBy(SampleContract)
 
+
 @grok.subscribe(IContract, grok.IObjectAddedEvent)
 def handle_contract_added(contract, event):
@@ -218,2 +251,80 @@
         IWorkflowInfo(contract).fireTransition('create')
     return
+
+
+@grok.subscribe(IPaymentWaitingForGatewayEvent)
+def handle_payment_waiting_for_gw(event):
+    maybe_contract = find_payable_from_payable_id(
+        event.object.payable_id)
+    if IContract.providedBy(maybe_contract):
+        IWorkflowInfo(maybe_contract).fireTransition('await')
+
+
+@grok.subscribe(IPaymentFinishedEvent)
+def handle_payment_finished(event):
+    payment = event.object
+    maybe_contract = find_payable_from_payable_id(payment.payable_id)
+    if not IContract.providedBy(maybe_contract):
+        return
+    if payment.state == STATE_PAID:
+        IWorkflowInfo(maybe_contract).fireTransition('confirm')
+    else:
+        IWorkflowInfo(maybe_contract).fireTransition('discard')
+
+
+class ContractFinder(grok.GlobalUtility):
+    grok.name('contracts_finder')
+    grok.implements(IPayableFinder)
+
+    def get_payable_by_id(self, contract_id):
+        catalog = queryUtility(ICatalog, 'contracts_catalog')
+        if catalog is None:
+            return None
+        result = catalog.searchResults(
+            contract_id=(contract_id, contract_id))
+        result = [x for x in result]
+        if not result:
+            return None
+        # there should not be more than one result really.
+        return result[0]
+
+
+class PayableContract(grok.Adapter):
+    """Adapter to adapt IContracts to IPayable.
+    """
+
+    grok.context(IContract)
+    grok.implements(IPayable)
+
+    def __init__(self, context):
+        self.context = context
+        currencies = set([x.currency for x in context.product_options])
+        if len(currencies) > 1:
+            raise ValueError(
+                "Only contracts with same currency for all options allowed.")
+        return
+
+    @property
+    def payable_id(self):
+        return self.context.contract_id
+
+    @property
+    def title(self):
+        return self.context.title or u''
+
+    @property
+    def currency(self):
+        if not len(self.context.product_options):
+            return None
+        return self.context.product_options[0].currency
+
+    @property
+    def payment_items(self):
+        result = []
+        for num, option in enumerate(self.context.product_options):
+            item = PaymentItem()
+            item.item_id = u'%s' % num
+            item.title = option.title
+            item.amount = option.fee
+            result.append(item)
+        return tuple(result)
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/customer.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/customer.py	(revision 12706)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/customer.py	(revision 12741)
@@ -25,9 +25,9 @@
 from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo
 from zope.password.interfaces import IPasswordManager
-from zope.component import getUtility, createObject
+from zope.catalog.interfaces import ICatalog
+from zope.component import getUtility, queryUtility
 from zope.component.interfaces import IFactory
 from zope.interface import implementedBy
 from zope.securitypolicy.interfaces import IPrincipalRoleManager
-from zope.schema.interfaces import ConstraintNotSatisfied
 
 from waeup.ikoba.image import IkobaImageFile
@@ -35,6 +35,5 @@
 from waeup.ikoba.interfaces import (
     IObjectHistory, IUserAccount, IFileStoreNameChooser, IFileStoreHandler,
-    IIkobaUtils, IExtFileStore,
-    CREATED, REQUESTED, APPROVED)
+    IIkobaUtils, IExtFileStore, )
 from waeup.ikoba.customers.interfaces import (
     ICustomer, ICustomerNavigation, ICSVCustomerExporter,
@@ -43,5 +42,8 @@
 from waeup.ikoba.customers.documents import CustomerDocumentsContainer
 from waeup.ikoba.customers.contracts import ContractsContainer
-from waeup.ikoba.utils.helpers import attrs_to_fields, now, copy_filesystem_tree
+from waeup.ikoba.payments.interfaces import IPayer, IPayerFinder
+from waeup.ikoba.utils.helpers import (
+    attrs_to_fields, now, copy_filesystem_tree)
+
 
 class Customer(grok.Container):
@@ -77,5 +79,6 @@
             'password'] = passwordmanager.encodePassword(password)
         self.temp_password['user'] = user
-        self.temp_password['timestamp'] = datetime.utcnow()  # offset-naive datetime
+        self.temp_password[
+            'timestamp'] = datetime.utcnow()  # offset-naive datetime
 
     def getTempPassword(self):
@@ -89,6 +92,6 @@
         if temp_password_dict is not None:
             delta = timedelta(minutes=self.temp_password_minutes)
-            now = datetime.utcnow()
-            if now < temp_password_dict.get('timestamp') + delta:
+            dt_now = datetime.utcnow()
+            if dt_now < temp_password_dict.get('timestamp') + delta:
                 return temp_password_dict.get('password')
             else:
@@ -98,5 +101,6 @@
 
     def writeLogMessage(self, view, message):
-        ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
+        ob_class = view.__implemented__.__name__.replace(
+            'waeup.ikoba.', '')
         self.__parent__.logger.info(
             '%s - %s - %s' % (ob_class, self.__name__, message))
@@ -196,4 +200,5 @@
     return
 
+
 def move_customer_files(customer, del_dir):
     """Move files belonging to `customer` to `del_dir`.
@@ -246,5 +251,5 @@
         timestamp = str(now().replace(microsecond=0))  # store UTC timestamp
         for num, row in enumerate(csv_data[1:-1]):
-            csv_data[num+1] = csv_data[num+1] + ',' + timestamp
+            csv_data[num + 1] = csv_data[num + 1] + ',' + timestamp
         csv_path = os.path.join(del_dir, '%s.csv' % name)
 
@@ -399,2 +404,40 @@
         return file, path, IkobaImageFile(
             file_obj.filename, file_obj.data)
+
+
+class CustomerPayer(grok.Adapter):
+    """Adapter to turn customers into IPayers.
+    """
+    grok.implements(IPayer)
+    grok.context(ICustomer)
+
+    @property
+    def first_name(self):
+        return getattr(self.context, 'firstname', None)
+
+    @property
+    def last_name(self):
+        return getattr(self.context, 'lastname', None)
+
+    @property
+    def payer_id(self):
+        return getattr(self.context, 'customer_id', None)
+
+
+class CustomerFinder(grok.GlobalUtility):
+    """Find customers.
+    """
+    grok.name('customer_finder')
+    grok.implements(IPayerFinder)
+
+    def get_payer_by_id(self, customer_id):
+        catalog = queryUtility(ICatalog, 'customers_catalog')
+        if catalog is None:
+            return None
+        result = catalog.searchResults(
+            customer_id=(customer_id, customer_id))
+        result = [x for x in result]
+        if not result:
+            return None
+        # there should not be more than one result really.
+        return result[0]
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/export.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/export.py	(revision 12706)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/export.py	(revision 12741)
@@ -215,5 +215,6 @@
                       'translated_class_name',
                       'is_editable',
-                      'is_approvable'])))
+                      'is_approvable',
+                      'customer'])))
 
     def filter_func(self, x, **kw):
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/interfaces.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/interfaces.py	(revision 12706)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/interfaces.py	(revision 12741)
@@ -298,4 +298,5 @@
     is_approvable = Attribute('Contract approvable by officer')
     translated_class_name = Attribute('Translatable class name')
+    customer = Attribute('Customer object of context.')
     user_id = Attribute('Id of a user, actually the id of the customer')
     title = Attribute('Title generated by the associated product')
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/tests/test_browser.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/tests/test_browser.py	(revision 12706)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/tests/test_browser.py	(revision 12741)
@@ -1,4 +1,4 @@
 ## $Id$
-## 
+##
 ## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
 ## This program is free software; you can redistribute it and/or modify
@@ -6,10 +6,10 @@
 ## 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
@@ -22,5 +22,4 @@
 import tempfile
 import logging
-import pytz
 import base64
 from decimal import Decimal
@@ -42,13 +41,8 @@
 from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
 from waeup.ikoba.app import Company
-from waeup.ikoba.customers.interfaces import ICustomersUtils
-from waeup.ikoba.customers.customer import Customer
 from waeup.ikoba.interfaces import (
     IUserAccount, IJobManager, APPROVED, SUBMITTED,
-    IFileStoreNameChooser, IExtFileStore, IFileStoreHandler, NotIdValue)
-from waeup.ikoba.imagestorage import (
-    FileStoreNameChooser, ExtFileStore, DefaultFileStoreHandler,
-    DefaultStorage)
-from waeup.ikoba.authentication import LocalRoleSetEvent
+    IFileStoreNameChooser, NotIdValue)
+from waeup.ikoba.imagestorage import ExtFileStore
 from waeup.ikoba.tests.test_async import FunctionalAsyncTestCase
 from waeup.ikoba.interfaces import VERIFIED
@@ -61,4 +55,5 @@
 SAMPLE_IMAGE_BMP = os.path.join(os.path.dirname(__file__), 'test_image.bmp')
 SAMPLE_PDF = os.path.join(os.path.dirname(__file__), 'test_pdf.pdf')
+
 
 def lookup_submit_value(name, value, browser):
@@ -72,4 +67,5 @@
             break
     return None
+
 
 class CustomersFullSetup(FunctionalTestCase):
@@ -119,5 +115,5 @@
         prodoption.fee = Decimal('99.9')
         prodoption.currency = 'USD'
-        self.product.options = [prodoption,]
+        self.product.options = [prodoption, ]
         self.app['products'].addProduct(self.product)
 
@@ -138,10 +134,12 @@
         self.document.document_id = u'DOC1'
         self.assertRaises(
-            NotIdValue, setattr, self.document, 'document_id', u'id with spaces')
+            NotIdValue, setattr, self.document, 'document_id',
+            u'id with spaces')
         self.customer['documents'].addDocument(self.document)
         self.contract = createObject(self._contract_factory)
         self.contract.contract_id = u'CON1'
         self.assertRaises(
-            NotIdValue, setattr, self.contract, 'contract_id', u'id with spaces')
+            NotIdValue, setattr, self.contract, 'contract_id',
+            u'id with spaces')
         self.customer['contracts'].addContract(self.contract)
 
@@ -299,7 +297,7 @@
         return
 
+
 class OfficerUITests(CustomersFullSetup):
     # Tests for Customer class views and pages
-
 
     def setup_logging(self):
@@ -331,12 +329,14 @@
         self.browser.getLink("Logout").click()
         self.assertTrue('You have been logged out' in self.browser.contents)
-        # But we are still logged in since we've used basic authentication here.
-        # Wikipedia says: Existing browsers retain authentication information
-        # until the tab or browser is closed or the user clears the history.
-        # HTTP does not provide a method for a server to direct clients to
-        # discard these cached credentials. This means that there is no
-        # effective way for a server to "log out" the user without closing
-        # the browser. This is a significant defect that requires browser
-        # manufacturers to support a "logout" user interface element ...
+        # But we are still logged in since we've used basic
+        # authentication here.  Wikipedia says: Existing browsers
+        # retain authentication information until the tab or browser
+        # is closed or the user clears the history.  HTTP does not
+        # provide a method for a server to direct clients to discard
+        # these cached credentials. This means that there is no
+        # effective way for a server to "log out" the user without
+        # closing the browser. This is a significant defect that
+        # requires browser manufacturers to support a "logout" user
+        # interface element ...
         self.assertTrue('Manager' in self.browser.contents)
 
@@ -378,5 +378,6 @@
         self.browser.open(self.customer_path)
         self.browser.getLink("Send email").click()
-        self.browser.getControl(name="form.subject").value = 'Important subject'
+        self.browser.getControl(
+            name="form.subject").value = 'Important subject'
         self.browser.getControl(name="form.body").value = 'Hello!'
         self.browser.getControl("Send message now").click()
@@ -410,5 +411,4 @@
         self.assertTrue('passport.jpg deleted' in self.browser.contents)
 
-
     def test_manage_workflow_send_transition_information(self):
         # Managers can pass through the whole workflow
@@ -417,5 +417,5 @@
         self.setup_logging()
         self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
-        customer = self.app['customers'][self.customer_id]
+        self.customer = self.app['customers'][self.customer_id]
         self.browser.open(self.trigtrans_path)
         self.browser.getControl(name="transition").value = ['start']
@@ -459,5 +459,6 @@
         self.browser.getControl("Apply").click()
         self.browser.open(self.trigtrans_path)
-        self.browser.getControl(name="transition").value = ['approve_provisionally']
+        self.browser.getControl(name="transition").value = [
+            'approve_provisionally']
         self.browser.getControl("Apply").click()
         self.browser.open(self.trigtrans_path)
@@ -499,5 +500,6 @@
         self.browser.getControl('Perform import').click()
         self.assertTrue('Processing of 1 rows failed' in self.browser.contents)
-        self.assertTrue('Successfully processed 2 rows' in self.browser.contents)
+        self.assertTrue(
+            'Successfully processed 2 rows' in self.browser.contents)
         self.assertTrue('Batch processing finished' in self.browser.contents)
 
@@ -535,16 +537,19 @@
         self.browser.getLink("History").click()
         self.assertTrue(
-            'Customer account deactivated by Manager<br />' in self.browser.contents)
-        self.assertTrue(
-            'Customer account activated by Manager<br />' in self.browser.contents)
+            'Customer account deactivated by Manager<br />'
+            in self.browser.contents)
+        self.assertTrue(
+            'Customer account activated by Manager<br />'
+            in self.browser.contents)
         # ... and actions have been logged.
         logfile = os.path.join(
             self.app['datacenter'].storage, 'logs', 'customers.log')
         logcontent = open(logfile).read()
-        self.assertTrue('zope.mgr - customers.browser.CustomerDeactivatePage - '
-                        'K1000000 - account deactivated' in logcontent)
-        self.assertTrue('zope.mgr - customers.browser.CustomerActivatePage - '
-                        'K1000000 - account activated' in logcontent)
-
+        self.assertTrue(
+            'zope.mgr - customers.browser.CustomerDeactivatePage - '
+            'K1000000 - account deactivated' in logcontent)
+        self.assertTrue(
+            'zope.mgr - customers.browser.CustomerActivatePage - '
+            'K1000000 - account activated' in logcontent)
 
     def test_login_as_customer(self):
@@ -554,5 +559,6 @@
         self.app['users']['mrofficer'].title = 'Harry Actor'
         prmglobal = IPrincipalRoleManager(self.app)
-        prmglobal.assignRoleToPrincipal('waeup.CustomerImpersonator', 'mrofficer')
+        prmglobal.assignRoleToPrincipal(
+            'waeup.CustomerImpersonator', 'mrofficer')
         prmglobal.assignRoleToPrincipal('waeup.CustomersManager', 'mrofficer')
         self.assertEqual(self.customer.state, 'created')
@@ -574,5 +580,6 @@
         # We are logged in as customer and can see the 'My Data' tab
         self.assertMatches(
-            '...<a href="#" class="dropdown-toggle" data-toggle="dropdown">...',
+            '...<a href="#" class="dropdown-toggle"'
+            ' data-toggle="dropdown">...',
             self.browser.contents)
         self.assertMatches(
@@ -724,5 +731,6 @@
         self.assertMatches(
             '...<div class="alert alert-warning">'
-            'Your account has been deactivated.</div>...', self.browser.contents)
+            'Your account has been deactivated.</div>...',
+            self.browser.contents)
         # If suspended_comment is set this message will be flashed instead
         self.customer.suspended_comment = u'Aetsch baetsch!'
@@ -778,4 +786,5 @@
         self.assertTrue('An email with' in self.browser.contents)
 
+
 class CustomerRegistrationTests(CustomersFullSetup):
     # Tests for customer registration
@@ -823,10 +832,11 @@
             cat.searchResults(
             email=('new@yy.zz', 'new@yy.zz')))
-        self.assertEqual(self.customer,results[0])
+        self.assertEqual(self.customer, results[0])
         logfile = os.path.join(
             self.app['datacenter'].storage, 'logs', 'main.log')
         logcontent = open(logfile).read()
-        self.assertTrue('zope.anybody - customers.browser.CustomerRequestPasswordPage - '
-                        '123 (K1000000) - new@yy.zz' in logcontent)
+        self.assertTrue(
+            'zope.anybody - customers.browser.CustomerRequestPasswordPage - '
+            '123 (K1000000) - new@yy.zz' in logcontent)
         return
 
@@ -841,5 +851,6 @@
         self.browser.getControl(name="form.email").value = 'newcustomer@xx.zz'
         self.browser.getControl("Send login credentials").click()
-        self.assertTrue('Your request was successful.' in self.browser.contents)
+        self.assertTrue(
+            'Your request was successful.' in self.browser.contents)
         # Customer can be found in the catalog via the email address
         cat = queryUtility(ICatalog, name='customers_catalog')
@@ -849,11 +860,14 @@
         self.assertEqual(self.app['customers']['K1000001'], results[0])
         self.assertEqual(self.app['customers']['K1000001'].firstname, 'Ruben')
-        self.assertEqual(self.app['customers']['K1000001'].lastname, 'Gonzales')
+        self.assertEqual(
+            self.app['customers']['K1000001'].lastname, 'Gonzales')
         logfile = os.path.join(
             self.app['datacenter'].storage, 'logs', 'main.log')
         logcontent = open(logfile).read()
-        self.assertTrue('zope.anybody - customers.browser.CustomerCreateAccountPage - '
-                        'K1000001 - newcustomer@xx.zz' in logcontent)
+        self.assertTrue(
+            'zope.anybody - customers.browser.CustomerCreateAccountPage - '
+            'K1000001 - newcustomer@xx.zz' in logcontent)
         return
+
 
 class CustomerDataExportTests(CustomersFullSetup, FunctionalAsyncTestCase):
@@ -929,5 +943,6 @@
         self.browser.getControl("Add document").click()
         self.assertTrue('PDF Document added.' in self.browser.contents)
-        docid = [i for i in self.customer['documents'].keys() if len(i) > 10][0]
+        docid = [i for i in self.customer['documents'].keys()
+                 if len(i) > 10][0]
         document = self.customer['documents'][docid]
 
@@ -950,5 +965,6 @@
         self.browser.getControl(name="transition").value = ['verify']
         self.browser.getControl("Apply").click()
-        self.assertTrue('Customer has not yet been approved' in self.browser.contents)
+        self.assertTrue(
+            'Customer has not yet been approved' in self.browser.contents)
         IWorkflowState(self.customer).setState(APPROVED)
         # Document can only be verified if files have been uploaded before
@@ -958,5 +974,6 @@
         self.assertTrue('No file uploaded' in self.browser.contents)
         self.assertEqual(document.state, 'submitted')
-        # We set state here manually (verification is tested in test_verify_document)
+        # We set state here manually (verification is tested in
+        # test_verify_document)
         IWorkflowState(document).setState(VERIFIED)
 
@@ -964,5 +981,6 @@
         self.browser.open(self.documents_path + '/' + docid + '/index')
         self.assertFalse(
-            'href="http://localhost/app/customers/K1000000/documents/%s/manage"'
+            'href="http://localhost/app/customers/K1000000/'
+            'documents/%s/manage"'
             % docid in self.browser.contents)
         self.browser.open(self.documents_path + '/' + docid + '/manage')
@@ -1012,13 +1030,16 @@
         self.browser.getLink("Documents").click()
         self.browser.getControl("Add document").click()
-        self.assertTrue('The requested form is locked' in self.browser.contents)
+        self.assertTrue(
+            'The requested form is locked' in self.browser.contents)
         # Customer is in wrong state
         IWorkflowState(self.customer).setState(APPROVED)
         self.browser.getControl("Add document").click()
-        self.browser.getControl(name="doctype").value = ['CustomerSampleDocument']
+        self.browser.getControl(name="doctype").value = [
+            'CustomerSampleDocument']
         self.browser.getControl(name="form.title").value = 'My Sample Document'
         self.browser.getControl("Add document").click()
         self.assertTrue('Sample Document added.' in self.browser.contents)
-        docid = [i for i in self.customer['documents'].keys() if len(i) > 10][0]
+        docid = [i for i in self.customer['documents'].keys()
+                 if len(i) > 10][0]
         document = self.customer['documents'][docid]
         self.browser.getControl(name="form.title").value = 'My second doc'
@@ -1038,5 +1059,6 @@
             name='upload_samplescaneditupload').click()
         self.assertTrue(
-            'href="http://localhost/app/customers/K1000000/documents/%s/sample"'
+            'href="http://localhost/app/customers/K1000000/'
+            'documents/%s/sample"'
             % docid in self.browser.contents)
         # Customer can submit the form. The form is also saved.
@@ -1045,8 +1067,11 @@
         self.assertEqual(document.title, 'My third doc')
         self.assertEqual(document.state, 'submitted')
-        self.assertTrue('Document State: submitted for verification' in self.browser.contents)
+        self.assertTrue(
+            'Document State: submitted for verification'
+            in self.browser.contents)
         # Customer can't edit the document once it has been submitted
         self.browser.open(self.documents_path + '/%s/edit' % docid)
-        self.assertTrue('The requested form is locked' in self.browser.contents)
+        self.assertTrue(
+            'The requested form is locked' in self.browser.contents)
 
     def test_manage_upload_sample_file(self):
@@ -1055,5 +1080,5 @@
         self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
         self.browser.open(self.customer_path + '/documents/DOC1/manage')
-        # Create a pseudo image file and select it to be uploaded 
+        # Create a pseudo image file and select it to be uploaded
         image = open(SAMPLE_IMAGE, 'rb')
         ctrl = self.browser.getControl(name='samplescanmanageupload')
@@ -1061,7 +1086,8 @@
         file_ctrl.add_file(image, filename='my_sample_scan.jpg')
         # The Save action does not upload files
-        self.browser.getControl("Save").click() # submit form
+        self.browser.getControl("Save").click()  # submit form
         self.assertFalse(
-            'href="http://localhost/app/customers/K1000000/documents/DOC1/sample"'
+            'href="http://localhost/app/customers/K1000000/'
+            'documents/DOC1/sample"'
             in self.browser.contents)
         # ... but the correct upload submit button does
@@ -1073,5 +1099,6 @@
             name='upload_samplescanmanageupload').click()
         self.assertTrue(
-            'href="http://localhost/app/customers/K1000000/documents/DOC1/sample"'
+            'href="http://localhost/app/customers/K1000000/'
+            'documents/DOC1/sample"'
             in self.browser.contents)
         # Browsing the link shows a real image
@@ -1098,5 +1125,5 @@
             'Uploaded file is too big' in self.browser.contents)
         # We do not rely on filename extensions given by uploaders
-        image = open(SAMPLE_IMAGE, 'rb') # a jpg-file
+        image = open(SAMPLE_IMAGE, 'rb')  # a jpg-file
         ctrl = self.browser.getControl(name='samplescanmanageupload')
         file_ctrl = ctrl.mech_control
@@ -1152,5 +1179,6 @@
         self.browser.getControl(name="form.title").value = 'My PDF Document'
         self.browser.getControl("Add document").click()
-        docid = [i for i in self.customer['documents'].keys() if len(i) > 10][0]
+        docid = [
+            i for i in self.customer['documents'].keys() if len(i) > 10][0]
         self.browser.open(self.documents_path + '/%s/manage' % docid)
         # Create a pseudo image file and select it to be uploaded
@@ -1177,5 +1205,6 @@
             name='upload_pdfscanmanageupload').click()
         self.assertTrue(
-            'href="http://localhost/app/customers/K1000000/documents/%s/sample.pdf">%s.pdf</a>'
+            'href="http://localhost/app/customers/K1000000/'
+            'documents/%s/sample.pdf">%s.pdf</a>'
             % (docid, docid[:9]) in self.browser.contents)
         # Browsing the link shows a real pdf
@@ -1194,5 +1223,6 @@
         self.browser.getLink("Download documents overview").click()
         self.assertEqual(self.browser.headers['Status'], '200 Ok')
-        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        self.assertEqual(
+            self.browser.headers['Content-Type'], 'application/pdf')
         path = os.path.join(samples_dir(), 'documents_overview_slip.pdf')
         open(path, 'wb').write(self.browser.contents)
@@ -1200,5 +1230,6 @@
         # Officers can open document slips which shows a thumbnail of
         # the jpeg file attached.
-        file_id = IFileStoreNameChooser(self.document).chooseName(attr='sample.jpg')
+        file_id = IFileStoreNameChooser(self.document).chooseName(
+            attr='sample.jpg')
         fs = ExtFileStore(root=self.dc_root)
         jpegfile = open(SAMPLE_IMAGE, 'rb')
@@ -1207,5 +1238,6 @@
         self.browser.getLink("Download document slip").click()
         self.assertEqual(self.browser.headers['Status'], '200 Ok')
-        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        self.assertEqual(
+            self.browser.headers['Content-Type'], 'application/pdf')
         path = os.path.join(samples_dir(), 'document_slip.pdf')
         open(path, 'wb').write(self.browser.contents)
@@ -1216,13 +1248,16 @@
         self.customer['documents'].addDocument(pdfdocument)
         # Add pdf file
-        file_id = IFileStoreNameChooser(pdfdocument).chooseName(attr='sample.pdf')
+        file_id = IFileStoreNameChooser(pdfdocument).chooseName(
+            attr='sample.pdf')
         fs = ExtFileStore(root=self.dc_root)
         pdffile = open(SAMPLE_PDF, 'rb')
         fs.createFile(file_id, pdffile)
-        docid = [i for i in self.customer['documents'].keys() if len(i) > 10][0]
+        docid = [i for i in self.customer['documents'].keys()
+                 if len(i) > 10][0]
         self.browser.open(self.customer_path + '/documents/' + docid)
         self.browser.getLink("Download document slip").click()
         self.assertEqual(self.browser.headers['Status'], '200 Ok')
-        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        self.assertEqual(
+            self.browser.headers['Content-Type'], 'application/pdf')
         path = os.path.join(samples_dir(), 'pdfdocument_slip.pdf')
         open(path, 'wb').write(self.browser.contents)
@@ -1232,13 +1267,18 @@
         # A proper file name chooser is registered for customer documents.
         # This is not a UI test. It's just a functional test.
-        file_id = IFileStoreNameChooser(self.document).chooseName(attr='sample')
+        file_id = IFileStoreNameChooser(self.document).chooseName(
+            attr='sample')
         fs = ExtFileStore(root=self.dc_root)
         fs.createFile(file_id, StringIO('my sample 1'))
         result = fs.getFileByContext(self.document, attr='sample')
-        self.assertEqual(file_id, '__file-customerdocument__01000/K1000000/sample_DOC1_K1000000')
+        self.assertEqual(
+            file_id, '__file-customerdocument__01000/'
+            'K1000000/sample_DOC1_K1000000')
         self.assertEqual(result.read(), 'my sample 1')
-        self.assertEqual(self.document.connected_files[0][1].read(), 'my sample 1')
+        self.assertEqual(
+            self.document.connected_files[0][1].read(), 'my sample 1')
         self.document.setMD5()
-        self.assertEqual(self.document.sample_md5, 'a406995ee8eb6772bacf51aa4b0caa24')
+        self.assertEqual(
+            self.document.sample_md5, 'a406995ee8eb6772bacf51aa4b0caa24')
         return
 
@@ -1246,4 +1286,26 @@
 class ContractUITests(CustomersFullSetup):
     # Tests for contract related views and pages
+
+    never_ending_button_text = (
+        'Select payment method (final submission)')
+
+    def add_product_option(self, contract):
+        prodoption = ProductOption()
+        prodoption.title = u'Any product option'
+        prodoption.fee = Decimal('88.8')
+        prodoption.currency = 'EUR'
+        contract.product_options = [prodoption, ]
+
+    def prepare_payment_select(self):
+        IWorkflowState(self.customer).setState('approved')
+        IWorkflowState(self.document).setState('verified')
+        self.contract.document_object = self.document
+        self.add_product_option(self.contract)
+        IWorkflowState(self.contract).setState('created')
+        # login as customer
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'cpwd'
+        self.browser.getControl("Login").click()
 
     def test_manage_contract(self):
@@ -1261,12 +1323,15 @@
         self.browser.getControl("Add contract").click()
         self.assertTrue('Sample Contract added.' in self.browser.contents)
-        conid = [i for i in self.customer['contracts'].keys() if len(i) > 10][0]
+        conid = [i for i in self.customer['contracts'].keys()
+                 if len(i) > 10][0]
         contract = self.customer['contracts'][conid]
         self.assertEqual(
-            self.browser.url, self.contracts_path + '/%s/selectproduct' % conid)
+            self.browser.url,
+            self.contracts_path + '/%s/selectproduct' % conid)
         # SAM is in the correct contract_category
         self.assertTrue('<option value="SAM">' in self.browser.contents)
         # So far last_product_id is None.
-        self.assertTrue(self.customer['contracts'][conid].last_product_id is None)
+        self.assertTrue(
+            self.customer['contracts'][conid].last_product_id is None)
         self.browser.getControl(name="form.product_object").value = ['SAM']
         self.browser.getControl("Save and proceed").click()
@@ -1274,5 +1339,6 @@
             self.browser.url, self.contracts_path + '/%s/manage' % conid)
         self.browser.getLink("View").click()
-        self.assertEqual(self.browser.url, self.contracts_path + '/%s/index' % conid)
+        self.assertEqual(
+            self.browser.url, self.contracts_path + '/%s/index' % conid)
         self.assertEqual(contract.tc_dict, {'en': u'Hello world'})
 
@@ -1342,5 +1408,6 @@
         self.assertFalse('Add contract' in self.browser.contents)
         self.browser.open(self.contracts_path + '/addcontract')
-        self.assertTrue('The requested form is locked' in self.browser.contents)
+        self.assertTrue(
+            'The requested form is locked' in self.browser.contents)
         IWorkflowState(self.customer).setState(APPROVED)
         self.browser.open(self.contracts_path)
@@ -1350,8 +1417,10 @@
         self.browser.getControl("Add contract").click()
         self.assertTrue('Sample Contract added.' in self.browser.contents)
-        conid = [i for i in self.customer['contracts'].keys() if len(i) > 10][0]
+        conid = [i for i in self.customer['contracts'].keys()
+                 if len(i) > 10][0]
         contract = self.customer['contracts'][conid]
         self.assertEqual(
-            self.browser.url, self.contracts_path + '/%s/selectproduct' % conid)
+            self.browser.url,
+            self.contracts_path + '/%s/selectproduct' % conid)
         # SAM is in the correct contract_category ...
         self.assertTrue('<option value="SAM">' in self.browser.contents)
@@ -1359,5 +1428,6 @@
         self.assertFalse('<option value="LIC">' in self.browser.contents)
         # So far last_product_id is None.
-        self.assertTrue(self.customer['contracts'][conid].last_product_id is None)
+        self.assertTrue(
+            self.customer['contracts'][conid].last_product_id is None)
         self.browser.getControl(name="form.product_object").value = ['SAM']
         self.browser.getControl("Save and proceed").click()
@@ -1366,5 +1436,6 @@
         # Document is a required field on edit form page.
         self.browser.getControl("Save").click()
-        self.assertTrue('Document: <span class="error">Required input is missing.</span>'
+        self.assertTrue(
+            'Document: <span class="error">Required input is missing.</span>'
             in self.browser.contents)
         # But our document can't be selected because it's not submitted
@@ -1376,5 +1447,6 @@
         # After saving the form, last_product_id and other attributes are set
         self.assertTrue('Form has been saved.' in self.browser.contents)
-        self.assertEqual(self.customer['contracts'][conid].last_product_id, 'SAM')
+        self.assertEqual(
+            self.customer['contracts'][conid].last_product_id, 'SAM')
         self.assertEqual(contract.title, 'Our Sample Product')
         self.assertEqual(contract.product_object, self.product)
@@ -1382,5 +1454,6 @@
         # Saving the form again does not unset last_product_id
         self.browser.getControl("Save").click()
-        self.assertEqual(self.customer['contracts'][conid].last_product_id, 'SAM')
+        self.assertEqual(
+            self.customer['contracts'][conid].last_product_id, 'SAM')
         self.assertTrue('Form has been saved.' in self.browser.contents)
         # So far we have not yet set product options.
@@ -1390,11 +1463,12 @@
         prodoption.fee = Decimal('88.8')
         prodoption.currency = 'EUR'
-        contract.product_options = [prodoption,]
+        contract.product_options = [prodoption, ]
         self.browser.open(self.contracts_path + '/%s/edit' % conid)
         # We can see both the stored and the recent product options
         # from the chosen product.
-        self.assertTrue('<option selected="selected" value="Any product option">'
-                        'Any product option @ 88.8 Euro</option>'
-                        in self.browser.contents)
+        self.assertTrue(
+            '<option selected="selected" value="Any product option">'
+            'Any product option @ 88.8 Euro</option>'
+            in self.browser.contents)
         self.assertTrue('<option value="First option">First option '
                         '@ 99.9 US Dollar</option>' in self.browser.contents)
@@ -1402,15 +1476,20 @@
         self.browser.getControl(
             name="form.product_options.0.").value = ['First option']
-        self.assertEqual(contract.product_options[0].title, 'Any product option')
+        self.assertEqual(
+            contract.product_options[0].title, 'Any product option')
         self.browser.getControl("Save").click()
         self.assertEqual(contract.product_options[0].title, 'First option')
         self.browser.getLink("View").click()
-        self.assertTrue('<span>First option @ 99.9 US Dollar</span>' in self.browser.contents)
-        self.assertEqual(self.browser.url, self.contracts_path + '/%s/index' % conid)
+        self.assertTrue(
+            '<span>First option @ 99.9 US Dollar</span>'
+            in self.browser.contents)
+        self.assertEqual(
+            self.browser.url, self.contracts_path + '/%s/index' % conid)
         # An href attribute is referring to the document and product objects
         self.assertTrue('<a href="http://localhost/app/products/SAM">SAM -'
             in self.browser.contents)
         self.assertTrue(
-            '<a href="http://localhost/app/customers/K1000000/documents/DOC1">DOC1 -'
+            '<a href="http://localhost/app/customers/K1000000/'
+            'documents/DOC1">DOC1 -'
             in self.browser.contents)
         # Customer can submit the form if confirmation box is ticket.
@@ -1418,14 +1497,19 @@
         self.browser.getLink("Edit").click()
         self.browser.getControl("Proceed to checkout").click()
-        self.assertTrue('confirm your acceptance of these by ticking' in self.browser.contents)
+        self.assertTrue(
+            'confirm your acceptance of these by ticking'
+            in self.browser.contents)
         self.assertEqual(contract.state, 'created')
         self.browser.getControl(name="confirm_tc").value = True
         self.browser.getControl("Proceed to checkout").click()
         self.assertEqual(contract.state, 'created')
+        radio_ctrl = self.browser.getControl(name='gw')
+        radio_ctrl.value = [radio_ctrl.options[0]]  # pick first payment opt
         self.browser.getControl("Select payment method").click()
         self.assertEqual(contract.state, 'awaiting')
         # Customer can't edit the contract once it has been submitted
         self.browser.open(self.contracts_path + '/%s/edit' % conid)
-        self.assertTrue('The requested form is locked' in self.browser.contents)
+        self.assertTrue(
+            'The requested form is locked' in self.browser.contents)
 
     def test_view_slips(self):
@@ -1435,5 +1519,6 @@
         self.browser.getLink("Download contracts overview").click()
         self.assertEqual(self.browser.headers['Status'], '200 Ok')
-        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        self.assertEqual(
+            self.browser.headers['Content-Type'], 'application/pdf')
         path = os.path.join(samples_dir(), 'contracts_overview_slip.pdf')
         open(path, 'wb').write(self.browser.contents)
@@ -1451,5 +1536,6 @@
         self.browser.getLink("Download contract slip").click()
         self.assertEqual(self.browser.headers['Status'], '200 Ok')
-        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        self.assertEqual(
+            self.browser.headers['Content-Type'], 'application/pdf')
         path = os.path.join(samples_dir(), 'contract_slip.pdf')
         open(path, 'wb').write(self.browser.contents)
@@ -1486,5 +1572,6 @@
         # InvalidTransitionError is catched
         self.assertTrue(
-            '<div class="alert alert-warning">Attached documents must be verified first.</div>'
+            '<div class="alert alert-warning">Attached documents '
+            'must be verified first.</div>'
             in self.browser.contents)
         self.browser.open(self.contracts_path + '/CON1/trigtrans')
@@ -1493,2 +1580,30 @@
         self.browser.getControl("Apply").click()
         self.assertEqual(IWorkflowState(self.contract).getState(), 'approved')
+
+    def test_select_payment(self):
+        # select payment
+        self.prepare_payment_select()
+        self.browser.open('%s/CON1/edit' % self.contracts_path)
+        self.browser.getControl("Proceed to checkout").click()
+        self.assertTrue(
+            "Select payment method" in self.browser.contents)
+        self.assertTrue(
+            'Credit Card (Demo Payments)' in self.browser.contents)
+
+    def test_select_payment_no_choice(self):
+        # we get warned if no payment was selected
+        self.prepare_payment_select()
+        self.browser.open(
+            '%s/CON1/select_payment_method' % self.contracts_path)
+        self.browser.getControl(self.never_ending_button_text).click()
+        self.assertTrue(
+            'Please pick a payment method' in self.browser.contents)
+
+    def test_select_payment_demo_provider(self):
+        # we can proceed with payments if we select a payment method
+        self.prepare_payment_select()
+        self.browser.open(
+            '%s/CON1/select_payment_method' % self.contracts_path)
+        radio_ctrl = self.browser.getControl(name='gw')
+        radio_ctrl.displayValue = ['Credit Card (Demo Payments)']
+        self.browser.getControl(self.never_ending_button_text).click()
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/tests/test_contract.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/tests/test_contract.py	(revision 12706)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/tests/test_contract.py	(revision 12741)
@@ -19,14 +19,24 @@
 Tests for contracts.
 """
+import decimal
 from zope.interface.verify import verifyClass, verifyObject
-from zope.component import createObject
+from zope.component import createObject, getUtility, getUtilitiesFor
+from zope.component.hooks import setSite
 from hurry.workflow.interfaces import (
     IWorkflowInfo, IWorkflowState, InvalidTransitionError)
 from waeup.ikoba.customers.interfaces import (
     IContractsContainer, IContract)
-from waeup.ikoba.interfaces import IObjectHistory
 from waeup.ikoba.customers.contracts import (
-    ContractsContainer, SampleContract)
+    ContractsContainer, SampleContract, ContractPayer, ContractFinder,
+    PayableContract,
+    )
+from waeup.ikoba.app import Company
+from waeup.ikoba.customers.customer import Customer
+from waeup.ikoba.payments.interfaces import (
+    IPaymentItem, IPayer, IPayableFinder, IPayable,
+    )
+from waeup.ikoba.products.productoptions import ProductOption
 from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase)
+
 
 class ContractsContainerTestCase(FunctionalTestCase):
@@ -89,2 +99,133 @@
         self.assertTrue('Contract created by system' in messages)
         return
+
+
+class TestContractHelpers(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_payer_adapter(self):
+        # we can adapt IContract to IPayer (i.e. create a payer)
+        customer = Customer()
+        customer.firstname, customer.lastname = u'Anna', u'Tester'
+        contract = createObject(u'waeup.SampleContract')
+        customer['contracts'] = ContractsContainer()
+        customer['contracts'].addContract(contract)
+        result = IPayer(contract)
+        self.assertTrue(isinstance(result, ContractPayer))
+        verifyObject(IPayer, result)
+        self.assertEqual(result.first_name, u'Anna')
+        self.assertEqual(result.last_name, u'Tester')
+        self.assertEqual(result.payer_id, customer.customer_id)
+
+    def test_contract_finder_iface(self):
+        # we have a contract finder that returns IPayableFinder data.
+        verifyClass(IPayableFinder, ContractFinder)
+
+    def test_contract_finder_registered(self):
+        # the contract finder is a utility registered on startup
+        util = getUtility(IPayableFinder, name='contracts_finder')
+        self.assertTrue(isinstance(util, ContractFinder))
+        utils = [util for name, util in getUtilitiesFor(IPayableFinder)
+                 if isinstance(util, ContractFinder)]
+        self.assertEqual(len(utils), 1)
+
+    def create_contract_and_site(self):
+        contract = SampleContract()
+        option1 = ProductOption(u"Fee 1", decimal.Decimal("31.10"), "USD")
+        option2 = ProductOption(u"Fee 2", decimal.Decimal("12.12"), "USD")
+        contract.product_options = [option1, option2]
+        contract.contract_id = u'CON1234'
+        self.getRootFolder()['app'] = Company()
+        app = self.getRootFolder()['app']
+        setSite(app)
+        return contract, app
+
+    def test_contract_finder(self):
+        # the contract finder can really find contracts
+        contract, app = self.create_contract_and_site()
+        app['mycontract'] = contract  # trigger cataloging
+        finder = ContractFinder()
+        result = finder.get_payable_by_id('CON1234')
+        self.assertTrue(result is contract)
+
+    def test_contract_finder_not_stored(self):
+        # we get none if an id is not stored
+        contract, app = self.create_contract_and_site()
+        app['mycontract'] = contract  # trigger cataloging
+        finder = ContractFinder()
+        result = finder.get_payable_by_id('Not-a-valid-id')
+        self.assertTrue(result is None)
+
+    def test_contract_finder_no_catalog(self):
+        # contract finder does not complain about missing catalog
+        finder = ContractFinder()
+        result = finder.get_payable_by_id('CON1234')
+        self.assertTrue(result is None)
+
+
+class TestContractAsPayable(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_adaptable(self):
+        # we can turn contracts into payables.
+        contract = SampleContract()
+        payable = IPayable(contract)
+        self.assertTrue(payable is not None)
+        self.assertTrue(isinstance(payable, PayableContract))
+
+    def test_payable_iface(self):
+        # PayableContracts really provide IPayable
+        contract = SampleContract()
+        option1 = ProductOption(u"Fee 1", decimal.Decimal("31.10"), "USD")
+        option2 = ProductOption(u"Fee 2", decimal.Decimal("12.12"), "USD")
+        contract.product_options = [option1, option2]
+        payable = PayableContract(contract)
+        verifyObject(IPayable, payable)
+        verifyClass(IPayable, PayableContract)
+
+    def test_payable_simple_attributes(self):
+        # the simple attribs are set correctly, according to context contract
+        contract = SampleContract()
+        contract.title = u'the title'
+        option1 = ProductOption(u"Fee 1", decimal.Decimal("31.10"), "EUR")
+        option2 = ProductOption(u"Fee 2", decimal.Decimal("12.12"), "EUR")
+        contract.product_options = [option1, option2]
+        payable = PayableContract(contract)
+        self.assertTrue(contract.contract_id, payable.payable_id)
+        self.assertEqual(payable.title, contract.title)
+        self.assertEqual(payable.currency, 'EUR')
+
+    def test_payable_items(self):
+        # we can get payment items from payable
+        contract = SampleContract()
+        option1 = ProductOption(u"Fee 1", decimal.Decimal("31.10"), "EUR")
+        option2 = ProductOption(u"Fee 2", decimal.Decimal("12.12"), "EUR")
+        contract.product_options = [option1, option2]
+        payable = PayableContract(contract)
+        items = payable.payment_items
+        self.assertTrue(isinstance(items, tuple))
+        self.assertEqual(len(items), 2)
+        verifyObject(IPaymentItem, items[0])
+        verifyObject(IPaymentItem, items[1])
+        self.assertEqual(items[0].item_id, '0')
+        self.assertEqual(items[0].title, u'Fee 1')
+        self.assertEqual(items[0].amount, decimal.Decimal("31.10"))
+
+    def test_payable_no_items(self):
+        # payables work also with no options set on contract
+        contract = SampleContract()
+        payable = PayableContract(contract)
+        items = payable.payment_items
+        self.assertTrue(isinstance(items, tuple))
+        self.assertEqual(len(items), 0)
+        self.assertEqual(payable.currency, None)
+
+    def test_different_currencies_forbiddedn(self):
+        # we do not accept different currencies in payment items
+        contract = SampleContract()
+        option1 = ProductOption(u"Fee 1", decimal.Decimal("31.10"), "EUR")
+        option2 = ProductOption(u"Fee 2", decimal.Decimal("12.12"), "USD")
+        contract.product_options = [option1, option2]
+        self.assertRaises(ValueError, PayableContract, contract)
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/tests/test_customer.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/tests/test_customer.py	(revision 12706)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/tests/test_customer.py	(revision 12741)
@@ -21,20 +21,21 @@
 import re
 import unittest
-import grok
 from cStringIO import StringIO
-from datetime import tzinfo
-from zope.component import getUtility, queryUtility, createObject
-from zope.catalog.interfaces import ICatalog
+from zope.component import getUtility, getUtilitiesFor
+from zope.component.hooks import setSite
 from zope.component.interfaces import IFactory
-from zope.event import notify
 from zope.interface import verify
-from zope.schema.interfaces import RequiredMissing
 from waeup.ikoba.interfaces import IExtFileStore, IFileStoreNameChooser
+from waeup.ikoba.app import Company
 from waeup.ikoba.customers.customer import (
-    Customer, CustomerFactory, handle_customer_removed, path_from_custid)
+    Customer, CustomerFactory, handle_customer_removed, path_from_custid,
+    CustomerPayer, CustomerFinder,
+    )
 from waeup.ikoba.customers.interfaces import (
     ICustomer, ICustomerNavigation, ICustomersUtils)
 from waeup.ikoba.customers.tests.test_batching import CustomerImportExportSetup
+from waeup.ikoba.payments.interfaces import IPayer, IPayerFinder
 from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
+
 
 class HelperTests(unittest.TestCase):
@@ -59,4 +60,5 @@
             path_from_custid('KM123456'), u'00120/KM123456')
         return
+
 
 class CustomerTest(FunctionalTestCase):
@@ -92,4 +94,62 @@
 
 
+class TestCustomerHelpers(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_payer_adapter(self):
+        # we can adapt ICustomer to IPayer (i.e. create a payer)
+        customer = Customer()
+        customer.firstname, customer.lastname = u'Anna', u'Tester'
+        result = IPayer(customer)
+        self.assertTrue(isinstance(result, CustomerPayer))
+        verify.verifyObject(IPayer, result)
+        self.assertEqual(result.first_name, u'Anna')
+        self.assertEqual(result.last_name, u'Tester')
+        self.assertEqual(result.payer_id, customer.customer_id)
+
+    def test_customer_finder_iface(self):
+        # we have a customer finder that returns IPayableFinder data.
+        verify.verifyClass(IPayerFinder, CustomerFinder)
+
+    def test_customer_finder_registered(self):
+        # the customer finder is a utility registered on startup
+        util = getUtility(IPayerFinder, name='customer_finder')
+        self.assertTrue(isinstance(util, CustomerFinder))
+        utils = [util for name, util in getUtilitiesFor(IPayerFinder)
+                 if isinstance(util, CustomerFinder)]
+        self.assertEqual(len(utils), 1)
+
+    def create_customer_and_site(self):
+        customer = Customer()
+        customer.customer_id = u'CUST1'
+        self.getRootFolder()['app'] = Company()
+        app = self.getRootFolder()['app']
+        setSite(app)
+        return customer, app
+
+    def test_customer_finder(self):
+        # the customer finder can really find customers
+        customer, app = self.create_customer_and_site()
+        app['mycustomer'] = customer  # trigger cataloging
+        finder = CustomerFinder()
+        result = finder.get_payer_by_id('CUST1')
+        self.assertTrue(result is customer)
+
+    def test_customer_finder_not_stored(self):
+        # we get none if an id is not stored
+        customer, app = self.create_customer_and_site()
+        app['mycustomer'] = customer  # trigger cataloging
+        finder = CustomerFinder()
+        result = finder.get_payer_by_id('Not-a-valid-id')
+        self.assertTrue(result is None)
+
+    def test_customer_finder_no_catalog(self):
+        # customer finder does not complain about missing catalog
+        finder = CustomerFinder()
+        result = finder.get_payer_by_id('CUST1')
+        self.assertTrue(result is None)
+
+
 class CustomerRemovalTests(CustomerImportExportSetup):
     # Test handle_customer_removed
@@ -192,4 +252,5 @@
         return
 
+
 class CustomerFactoryTest(FunctionalTestCase):
 
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/demo_provider.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/demo_provider.py	(revision 12706)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/demo_provider.py	(revision 12741)
@@ -1,9 +1,32 @@
+import datetime
 import grok
+import random
+import re
+from zope.event import notify
 from waeup.ikoba.interfaces import MessageFactory as _
-from waeup.ikoba.payments.interfaces import IPaymentGatewayService
-from waeup.ikoba.payments.payment import Payment
+from waeup.ikoba.browser.layout import IkobaEditFormPage, action
+from waeup.ikoba.payments.interfaces import (
+    IPaymentGatewayService, IPayment, IPayable, IPayer, IPayee,
+    STATE_AWAITING_GATEWAY_CONFIRM, STATE_PAID)
+from waeup.ikoba.payments.payment import (
+    Payment, get_payment, find_payable_from_payable_id, format_amount,
+    find_payer_from_payer_id, PaymentProviderServiceBase,
+    PaymentWaitingForGatewayEvent, PaymentFinishedEvent)
 
 
-class DemoCreditcardPaymentService(grok.GlobalUtility):
+grok.templatedir('templates')
+
+
+WARN_FINAL_SUBMIT = _(
+    'You can not edit your contract after final submission. '
+    'Do you really want to submit?'
+)
+
+
+RE_CC_NUMBER = re.compile('^[0-9]{9,25}$')
+RE_CSC = re.compile('^[0-9]{3,4}$')
+
+
+class DemoCreditcardPaymentService(PaymentProviderServiceBase):
     """A demo payment gateway service.
 
@@ -13,13 +36,141 @@
     grok.name('demo_creditcard')
 
-    title = _(u'Demo Creditcard')
+    title = _(u'Credit Card (Demo Payments)')
 
-    def create_payment(self, payer, payment_item_list=[],  payee=None):
+    def create_payment(self, payer, payable,  payee=None):
         """Create a payment.
         """
-        payment = Payment()
+        if not IPayer.providedBy(payer):
+            payer = IPayer(payer)
+        if not IPayable.providedBy(payable):
+            payable = IPayable(payable)
+        if (payee is not None) and (not IPayee.providedBy(payee)):
+            payee = IPayee(payee)
+        payment = Payment(payer, payable, payee)
         payment.gateway_service = 'demo_creditcard'  # must be grok.name above
-        payment.payer_id = payer.payer_id
-        for item in payment_item_list:
-            payment.add_payment_item(item)
         return payment
+
+    def next_step(self, payment_id):
+        """Tell where to go next.
+
+        Returns (context, view_name). Both may be none.
+        """
+        payment = get_payment(payment_id)
+        if payment is None:
+            return None, None
+        if payment.state == STATE_AWAITING_GATEWAY_CONFIRM:
+            return payment, 'demo_cc2'
+        return payment, 'demo_cc1'
+
+
+class CreditCardStep1(IkobaEditFormPage):
+    grok.context(IPayment)
+    grok.name('demo_cc1')
+    # XXX: Use own permissions for payments
+    grok.require('waeup.Authenticated')
+    label = "Enter Credit Card Details"
+    grok.template('demo_cc_step1')
+    pnav = 4
+
+    def validate_form(self):
+        fields = ['first_name', 'last_name', 'cc_number', 'csc', 'exp_date']
+        cls = 'form-group'
+        if len(self.request.form):
+            cls += ' has-success'
+        result = dict([(x, cls) for x in fields])
+        if not len(self.request.form):
+            return True, result
+        err = 'form-group has-error'
+        if not self.first_name:
+            result['first_name'] = err
+        if not self.last_name:
+            result['last_name'] = err
+        if not RE_CC_NUMBER.match(self.cc_number):
+            result['cc_number'] = err
+        if not RE_CSC.match(self.csc):
+            result['csc'] = err
+        if err in result.values():
+            return False, result
+        return True, result
+
+    def update(self, first_name=None, last_name=None, cc_number=None,
+               month=None, year=None):
+        self.payer = IPayer(find_payer_from_payer_id(self.context.payer_id))
+        self.payable = IPayable(find_payable_from_payable_id(
+            self.context.payable_id))
+        form = self.request.form
+        self.first_name = form.get('first_name', self.payer.first_name)
+        self.last_name = form.get('last_name', self.payer.last_name)
+        self.cc_number = form.get('cc_number', '')
+        self.month = int(form.get('exp_month', datetime.datetime.now().month))
+        self.year = int(form.get('exp_year', datetime.datetime.now().year))
+        self.csc = form.get('csc', '')
+        self.amount = format_amount(
+            self.context.amount, self.context.currency)
+        self.months = ''.join([
+            '<option%s>%s</option>' % (
+                (x == self.month) and ' selected="selected"' or '',
+                x) for x in range(1, 13)])
+        self.years = ''.join([
+            '<option%s>%s</option>' % (
+                (x == self.year + 1) and ' selected="selected"' or '',
+                x) for x in range(self.year - 1, self.year + 15)])
+        self.ok, self.validations = self.validate_form()
+
+    @action(_('Authorize Payment'), warning=WARN_FINAL_SUBMIT,
+            style="primary")
+    def authorize(self, **data):
+        if not self.ok:
+            self.flash(_("Please review (red) entries below!"),
+                       type='warning')
+            return
+        # XXX: payment really started, do lots of logging
+        self.context.state = STATE_AWAITING_GATEWAY_CONFIRM
+        notify(PaymentWaitingForGatewayEvent(self.context))
+        self.redirect(self.url(self.context, 'demo_cc2'))
+        return
+
+    @action(_('Cancel'))
+    def cancel(self, **data):
+        """Go back to the payable (if possible) or site home.
+        """
+        payable_id = getattr(self.context, 'payable_id', '')
+        payed_item = find_payable_from_payable_id(payable_id)
+        self.flash(_("Payment cancelled."))
+        if payed_item is not None:
+            # remove context/payment?
+            target = payed_item
+        else:
+            target = grok.getSite()
+        self.redirect(self.url(target))
+
+
+class CreditCardStep2(IkobaEditFormPage):
+    grok.context(IPayment)
+    grok.name('demo_cc2')
+    # XXX: Use own permissions for payments
+    grok.require('waeup.Authenticated')
+    label = "&nbsp;"
+    grok.template('demo_cc_step2')
+    pnav = 4
+
+    def update(self):
+        cnt = int(self.request.form.get('cnt', '0'))
+        self.cnt = cnt + 1
+        threshold = random.choice(range(10))
+        self.success = False
+        # HERE WOULD WE REALLY ASK FOR VERIFICATION
+        if threshold <= cnt:
+            self.success = True
+            self.flash(_("Your payment was finished successfully."))
+        if self.request.form.get('SUBMIT', None):
+            self.renew()
+
+    def renew(self):
+        if not self.success:
+            return
+        payable_id = getattr(self.context, 'payable_id', '')
+        payed_item = find_payable_from_payable_id(payable_id)
+        self.context.state = STATE_PAID
+        notify(PaymentFinishedEvent(self.context))
+        self.redirect(self.url(payed_item))
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/interfaces.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/interfaces.py	(revision 12706)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/interfaces.py	(revision 12741)
@@ -20,6 +20,5 @@
 from zope import schema
 from zope.component import getUtilitiesFor
-from zope.container.interfaces import IContainer
-from zope.container.constraints import contains
+from zope.component.interfaces import IObjectEvent
 from zope.interface import Interface, Attribute
 from waeup.ikoba.interfaces import (
@@ -30,9 +29,13 @@
 #: Possible states of payments
 STATE_UNPAID = 1
-STATE_PAID = 2
-STATE_FAILED = 4
+STATE_AWAITING_USER_CONFIRM = 2
+STATE_AWAITING_GATEWAY_CONFIRM = 4
+STATE_PAID = 64
+STATE_FAILED = 128
 
 payment_states = SimpleIkobaVocabulary(
     (_('Not yet paid'), STATE_UNPAID),
+    (_('Waiting for user confirm'), STATE_AWAITING_USER_CONFIRM),
+    (_('Waiting for verification'), STATE_AWAITING_GATEWAY_CONFIRM),
     (_('Paid'), STATE_PAID),
     (_('Failed'), STATE_FAILED),
@@ -40,4 +43,22 @@
 
 
+class IPaymentWaitingForGatewayEvent(IObjectEvent):
+    """Fired when a payment starts waiting for verification.
+    """
+    object = Attribute("""The payment waiting.""")
+
+
+class IPaymentFinishedEvent(IObjectEvent):
+    """Fired when a payment failed or succeeded.
+    """
+    object = Attribute("""The payment finished.""")
+
+
+class IPaymentAborted(IObjectEvent):
+    """Fired when a payment was aborted before external transactions.
+    """
+    object = Attribute("""The payment aborted.""")
+
+
 class PaymentGatewayServicesSource(BasicSourceFactory):
     """A source that lists available payment services.
@@ -88,9 +109,9 @@
         )
 
-    def create_payment(payer, payment_item_list,  payee):
+    def create_payment(payer, payable, payee):
         """Create a payment.
 
         For all parameters we expect an object, that implements
-        `IPayer`, `IPaymentItem`, or `IPayee` respectively. If not,
+        `IPayer`, `IPayable`, or `IPayee` respectively. If not,
         then the given objects must be at least adaptable to the
         respective interface.
@@ -102,4 +123,14 @@
         """
 
+    def next_step(payment_id):
+        """Returns a payment (as context) and a view name.
+
+        May result in (None, None).
+        """
+
+    def store(payment):
+        """Store `payment` in site.
+        """
+
 
 class IPaymentGatewayServicesLister(Interface):
@@ -136,4 +167,41 @@
         required=True,
         )
+
+
+class IPayableFinder(Interface):
+    """Finds payables.
+
+    For each type of content you want to make payable, you should
+    define an IPayableFinder that can lookup payables in the
+    site.
+
+    This enables access from payments (which store payable ids only)
+    to arbitrary content objects (for which a payable finder is
+    registered under some name).
+
+    The other way round (getting an id and other relevant data from
+    any content object) is ensured by IPayable adapters.
+    """
+    def get_payable_by_id(item_id):
+        """Get an item by its Id, or none.
+        """
+
+
+class IPayerFinder(Interface):
+    """Finds payers.
+
+    For each type of content you understand as payer, you should
+    define an IPayrtFinder that can lookup payers in the site.
+
+    This enables access from payments (which store payer ids only)
+    to arbitrary content objects (for which a payer finder is
+    registered under some name.
+
+    The other way round (getting an id and other relevant data from
+    any content object) is ensured by IPayer adapters.
+    """
+    def get_payer_by_id(item_id):
+        """Get a payer by its Id, or none.
+        """
 
 
@@ -161,5 +229,51 @@
 
 
-class IPayment(IContainer):
+class IPayable(Interface):
+    """Something that can be payed.
+
+    Designed to serve as adapter. IPayables turn arbitrary content
+    objects into something with a standarized interfaces for use with
+    payments.
+
+    While currency is important to tell about the amount currency, the
+    total amount is computed on-demand from payment items.
+    """
+    payable_id = schema.TextLine(
+        title=u'ID of a payable',
+        description=(u'It should be possible to lookup the payable id '
+                     u'by some registered IPayableFinder later on'),
+        required=True,
+        readonly=True,
+        )
+
+    title = schema.TextLine(
+        title=u'Title',
+        description=u'A short description of the payed item.',
+        required=True,
+        default=u'',
+        readonly=True,
+        )
+
+    currency = schema.Choice(
+        title=u'Currency',
+        source=ISO_4217_CURRENCIES_VOCAB,
+        required=True,
+        default='USD',
+        readonly=True,
+        )
+
+    payment_items = schema.Tuple(
+        title=u'Tuple of IPaymentItems.',
+        value_type=schema.Object(
+            title=u'Payment Item',
+            schema=IPaymentItem,
+            ),
+        required=False,
+        default=(),
+        readonly=True,
+        )
+
+
+class IPayment(IIkobaObject):
     """A base representation of payments.
 
@@ -185,9 +299,13 @@
     we mark the payment 'failed'.
     """
-    contains(IPaymentItem)
-
     payment_id = schema.TextLine(
         title=u'Payment Identifier',
         default=None,
+        required=True,
+        )
+
+    title = schema.TextLine(
+        title=u'Payment description.',
+        default=u'',
         required=True,
         )
@@ -203,5 +321,11 @@
         default=None,
         required=False,
-    )
+        )
+
+    payable_id = schema.TextLine(
+        title=u'ID of item/good being paid',
+        default=None,
+        required=False,
+        )
 
     gateway_service = schema.Choice(
@@ -256,10 +380,4 @@
         """
 
-    def add_payment_item(item):
-        """Payments contain payment items.
-
-        Add one
-        """
-
 
 class IPayer(Interface):
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/payment.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/payment.py	(revision 12706)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/payment.py	(revision 12741)
@@ -23,13 +23,84 @@
 import uuid
 from datetime import datetime
-from zope.component import getUtilitiesFor
+from zope.catalog.interfaces import ICatalog
+from zope.component import getUtilitiesFor, getUtility
 from zope.event import notify
+from waeup.ikoba.interfaces import MessageFactory as _
 from waeup.ikoba.utils.helpers import attrs_to_fields
+from waeup.ikoba.utils.logger import Logger
 from waeup.ikoba.payments.interfaces import (
     IPayment, STATE_UNPAID, STATE_FAILED, STATE_PAID,
     IPaymentGatewayService, IPayer, IPaymentItem, IPayee,
-    IPaymentGatewayServicesLister,
+    IPaymentGatewayServicesLister, IPayableFinder, IPayerFinder,
+    IPaymentWaitingForGatewayEvent, IPaymentFinishedEvent,
     )
-from waeup.ikoba.utils.logger import Logger
+
+
+def format_amount(amount, currency):
+    """Turn `amount`, `currency` into a readable string.
+    """
+    cncy_map = {'USD': u'US$', 'EUR': u'\u20ac', 'NGN': u'\u20a6'}
+    currency = cncy_map.get(currency, currency)
+    return '%s %s' % (currency, '{:,.2f}'.format(amount))
+
+
+def get_payment(payment_id):
+    """Get payment by payment id.
+
+    If no such payment can be found in catalog, return none.
+    """
+    cat = getUtility(ICatalog, name='payments_catalog')
+    result_set = [x for x in cat.searchResults(
+        payment_id=(payment_id, payment_id))]
+    if len(result_set):
+        return result_set[0]
+    return None
+
+
+def find_payable_from_payable_id(payable_id):
+    """Find a payable from its id.
+
+    Looks up all registered IPayableFinders and returns the first
+    positive result found.
+    """
+    for name, util in getUtilitiesFor(IPayableFinder):
+        result = util.get_payable_by_id(payable_id)
+        if result is not None:
+            return result
+    return None
+
+
+def find_payer_from_payer_id(payer_id):
+    """Find a payer from its id.
+
+    Looks up all registered IPayerFinders and returns the first
+    positive result found.
+    """
+    for name, util in getUtilitiesFor(IPayerFinder):
+        result = util.get_payer_by_id(payer_id)
+        if result is not None:
+            return result
+    return None
+
+
+def format_payment_item_values(payment_item_values, currency):
+    """Format tuples (description, currency, amount) for output.
+
+    `currency` passed in is the 'target' currency.
+
+    Returns a list of formated values. Last item is total sum.
+    XXX: we do not really respect currency. If different items
+         have different currencies, we are choked.
+    """
+    result = []
+    total = decimal.Decimal("0.00")
+    for descr, item_currency, amount in payment_item_values:
+        total += amount
+        if item_currency != currency:
+            raise ValueError(
+                "Different currencies in payment items not supported.")
+        result.append((descr, '%s %0.2f' % (item_currency, amount)))
+    result.append((_('Total'), '%s %0.2f' % (currency, total)))
+    return result
 
 
@@ -42,4 +113,18 @@
 
 
+class PaymentWaitingForGatewayEvent(object):
+    grok.implements(IPaymentWaitingForGatewayEvent)
+
+    def __init__(self, obj):
+        self.object = obj
+
+
+class PaymentFinishedEvent(object):
+    grok.implements(IPaymentFinishedEvent)
+
+    def __init__(self, obj):
+        self.object = obj
+
+
 class PaymentGatewayServicesLister(grok.GlobalUtility):
     grok.implements(IPaymentGatewayServicesLister)
@@ -52,5 +137,6 @@
 
 class PaymentProviderServiceBase(grok.GlobalUtility):
-
+    """Base for IPaymentGatewayServices.
+    """
     grok.baseclass()
     grok.implements(IPaymentGatewayService)
@@ -58,7 +144,16 @@
     title = u'Sample Credit Card Service'
 
-
-@attrs_to_fields
-class Payment(grok.Container, Logger):
+    def store(self, payment):
+        """Store `payment` in site.
+        """
+        site = grok.getSite()
+        payments = site['payments']
+        if payment.payment_id in payments:
+            del site['payments'][payment.payment_id]
+        site['payments'][payment.payment_id] = payment
+
+
+@attrs_to_fields
+class Payment(grok.Model, Logger):
     """This is a payment.
     """
@@ -70,21 +165,19 @@
     logger_format_str = '"%(asctime)s","%(user)s",%(message)s'
 
-    @property
-    def amount(self):
-        """The amount of a payment.
-
-        Equals the sum of items contained.
-        """
-        return sum(
-            [item.amount for item in self.values()],
-            decimal.Decimal("0.00")  # default value
-        )
-
-    def __init__(self):
+    def __init__(self, payer, payable, payee=None):
         super(Payment, self).__init__()
+        item_amounts = [decimal.Decimal("0.00"), ]
+        item_amounts += [item.amount for item in payable.payment_items]
+        self.amount = sum(item_amounts)
+        self.payer_id = payer.payer_id
+        self.payable_id = payable.payable_id
+        self.title = payable.title
         self.creation_date = datetime.utcnow()
         self.payment_date = None
         self.payment_id = u'PAY_' + unicode(uuid.uuid4().hex)
         self.state = STATE_UNPAID
+        self.currency = payable.currency
+        if payee is not None:
+            self.payee_id = payee.payee_id
         return
 
@@ -113,18 +206,4 @@
         notify(grok.ObjectModifiedEvent(self))
 
-    def add_payment_item(self, item):
-        """Add `item`
-
-        Returns the key under which the `item` was stored. Please do
-        not make anby assumptions about the key. It will be a
-        string. That is all we can tell.
-
-        """
-        cnt = 0
-        while str(cnt) in self:
-            cnt += 1
-        self[str(cnt)] = item
-        return str(cnt)
-
 
 @attrs_to_fields
@@ -138,10 +217,14 @@
 
 @attrs_to_fields
-class PaymentItem(grok.Model):
+class PaymentItem(object):
 
     grok.implements(IPaymentItem)
 
-    def __init__(self):
+    def __init__(
+            self, item_id=u"0", title=u"", amount=decimal.Decimal("0.00")):
         super(PaymentItem, self).__init__()
+        self.item_id = item_id
+        self.title = title
+        self.amount = amount
 
 
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/paypal.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/paypal.py	(revision 12706)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/paypal.py	(revision 12741)
@@ -35,4 +35,5 @@
     IPayment, IPaymentGatewayService, IPayer, IPaymentItem, IPayee,
     )
+from waeup.ikoba.payments.payment import PaymentProviderServiceBase
 from waeup.ikoba.payments.paypal_countries import COUNTRIES_VOCAB
 from waeup.ikoba.payments.paypal_currencies import CURRENCIES_VOCAB
@@ -1141,5 +1142,5 @@
 
 
-class PayPalCreditCardService(grok.GlobalUtility):
+class PayPalCreditCardService(PaymentProviderServiceBase):
     grok.implements(IPaymentGatewayService)
     grok.name('paypal_creditcard')
@@ -1216,6 +1217,9 @@
         return payment
 
-
-class PayPalRegularPaymentService(grok.GlobalUtility):
+    def next_step(self, payment_id):
+        raise NotImplemented("next_steo() not implemented")
+
+
+class PayPalRegularPaymentService(PaymentProviderServiceBase):
     grok.implements(IPaymentGatewayService)
     grok.name('paypal_regular')
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/templates/demo_cc_step1.pt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/templates/demo_cc_step1.pt	(revision 12741)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/templates/demo_cc_step1.pt	(revision 12741)
@@ -0,0 +1,148 @@
+<form class="form-horizontal">
+  <div class="form-group"
+       tal:attributes="class view/validations/first_name">
+    <label for="firstname" class="col-sm-3 control-label">
+      Cardholder's First Name:
+    </label>
+    <div class="col-sm-5">
+      <input type="text" class="form-control" placeholder="John"
+             id="firstname" name="first_name"
+             tal:attributes="value view/first_name" />
+    </div>
+  </div>
+
+  <div tal:attributes="class view/validations/last_name" class="form-group">
+    <label for="lastname" class="col-sm-3 control-label">
+      Cardholder's Last Name:
+    </label>
+    <div class="col-sm-5">
+      <input type="text" class="form-control" placeholder="Smith"
+             id="lastname" name="last_name"
+             tal:attributes="value view/last_name" />
+    </div>
+  </div>
+
+  <div class="form-group"
+       tal:attributes="class view/validations/cc_number">
+    <label for="ccnumber" class="col-sm-3 control-label">
+      Credit Card Number:
+    </label>
+    <div class="col-sm-5">
+      <input type="text" class="form-control" id="ccnumber"
+             name="cc_number" tal:attributes="value view/cc_number" />
+    </div>
+  </div>
+
+  <div class="form-group"
+       tal:attributes="class view/validations/exp_date">
+    <label for="exp_date" class="col-sm-3 control-label">
+      Expiration Date:
+    </label>
+
+    <div class="col-sm-2">
+      <select class="form-control text-right" id="exp_date_month"
+              name="exp_month" tal:content="structure view/months">
+        <option>1</option>
+        <option>2</option>
+        <option selected="selected">3</option>
+        <option>4</option>
+        <option>5</option>
+        <option>6</option>
+        <option>7</option>
+        <option>8</option>
+        <option>9</option>
+        <option>10</option>
+        <option>11</option>
+        <option>12</option>
+      </select>
+    </div>
+
+    <div class="col-sm-2">
+      <select class="form-control" id="exp_date_month" name="exp_year"
+              tal:content="structure view/years">
+        <option>2014</option>
+        <option selected="selected">2015</option>
+        <option>2016</option>
+        <option>2017</option>
+        <option>2018</option>
+        <option>2019</option>
+      </select>
+    </div>
+  </div>
+
+  <div class="form-group"
+       tal:attributes="class view/validations/csc">
+    <label for="sec_code" class="col-sm-3 control-label">
+      Security Code (CSC):
+      <div class="small">
+        <a href="#collapseExample" data-toggle="collapse"
+           aria-expanded="false" aria-controls="collapseExample">
+          What is a CSC?
+        </a>
+      </div>
+    </label>
+    <div class="col-sm-3">
+      <input type="password" class="form-control" maxlength="4" id="sec_code"
+             name="csc" tal:attributes="value view/csc" />
+    </div>
+
+  </div>
+
+  <div class="form-group">
+    <div class="collapse col-sm-12" id="collapseExample">
+      <div class="well">
+        <p class="strong"><b>Card Security Code (CSC)</b></p>
+        <p>
+          The card security code, also known as card verification
+          value (CVV or CVV2) is a counter-measure to protect you from
+          credit card fraud.
+        </p>
+
+        <p>
+            The security code is located on the back of MasterCard,
+            Visa and Discover credit or debit cards and is typically a
+            separate group of 3 digits within or to the right of the
+            signature strip.
+        </p>
+        <div class="col-sm-12">
+          <div>
+            <img src="/static/img/cvv2-sample-visa.png"
+                 class="img-responsive col-sm-3" />
+            </div><div>
+            <img src="/static/img/cvv2-sample-amex.png"
+                 class="img-responsive col-sm-3" />
+          </div>
+        </div>
+
+        <p>
+          On American Express cards, the card security code is a
+          printed, not embossed, group of four digits on the front
+          towards the right.
+        </p>
+      </div>
+    </div>
+
+  </div>
+
+  <hr />
+
+  <div class="form-group">
+    <div class="col-sm-offset-1 col-sm-2">
+      <b>Payment Total:</b>
+    </div>
+    <div class="col-sm-9 text-left">
+      <b tal:content="view/amount">$&nbsp;3,012.40&nbsp;USD</b>
+    </div>
+  </div>
+
+  <div class="form-group">
+    <div class="col-sm-offset-2 col-sm-10">
+      <div tal:condition="view/availableActions">
+        <input tal:repeat="action view/actions"
+               tal:replace="structure action/render"
+               />
+      </div>
+    </div>
+  </div>
+
+</form>
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/templates/demo_cc_step2.pt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/templates/demo_cc_step2.pt	(revision 12741)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/templates/demo_cc_step2.pt	(revision 12741)
@@ -0,0 +1,17 @@
+<form class="form-horizontal">
+  <div class="jumbotron">
+    <h1>Thank you!</h1>
+    <p>
+      Your payment will now be verified by our financial
+      partners. Please bear with us.
+    </p>
+    <p>
+      <input type="submit" name="SUBMIT" class="btn btn-primary btn-lg"
+             role="button" value="Update Status" />
+    </p>
+    <input type="hidden" name="cnt"
+           tal:attributes="value view/cnt" />
+
+  </div>
+
+</form>
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_demo_provider.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_demo_provider.py	(revision 12706)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_demo_provider.py	(revision 12741)
@@ -1,3 +1,5 @@
-import decimal
+import unittest
+from zope.component import queryUtility
+from zope.component.hooks import setSite
 from zope.interface.verify import verifyClass, verifyObject
 from waeup.ikoba.testing import (
@@ -7,13 +9,40 @@
     IPaymentGatewayService, IPayment, STATE_UNPAID,
     )
-from waeup.ikoba.payments.payment import Payer, PaymentItem, Payee
+from waeup.ikoba.app import Company
+from waeup.ikoba.payments.payment import Payer, Payee, Payment
 from waeup.ikoba.payments.demo_provider import (
-    DemoCreditcardPaymentService,
+    DemoCreditcardPaymentService, RE_CC_NUMBER, RE_CSC,
     )
+from waeup.ikoba.payments.tests.test_payment import FakePayer, FakePayable
 
 
-class DemoCreditcarPaymentServiceTests(FunctionalTestCase):
+class TestDemoProviderHelpers(unittest.TestCase):
+
+    def test_re_cc_number(self):
+        # we recognize valid numbers
+        assert RE_CC_NUMBER.match('a') is None
+        assert RE_CC_NUMBER.match('12345678') is None
+        assert RE_CC_NUMBER.match('1234a5678') is None
+        assert RE_CC_NUMBER.match('132456789') is not None
+        assert RE_CC_NUMBER.match('123456789012345') is not None
+
+    def test_re_csc(self):
+        # we recognize security numbers
+        assert RE_CSC.match('12') is None
+        assert RE_CSC.match('123') is not None
+        assert RE_CSC.match('1234') is not None
+        assert RE_CSC.match('12345') is None
+        assert RE_CSC.match('12A2') is None
+
+
+class DemoCreditcardPaymentServiceTests(FunctionalTestCase):
 
     layer = FunctionalLayer
+
+    def setUp(self):
+        super(DemoCreditcardPaymentServiceTests, self).setUp()
+        self.app = Company()
+        self.getRootFolder()['app'] = self.app
+        setSite(self.app)
 
     def test_iface(self):
@@ -26,36 +55,37 @@
         # we can get payments from payment gateways
         service = DemoCreditcardPaymentService()
-        payer, payment_item, payee = Payer(), PaymentItem(), Payee()
+        payer, payable, payee = Payer(), FakePayable(), Payee()
         payer.payer_id = u'SOME_PAYER_ID'
-        result = service.create_payment(payer, [], payee)
+        result = service.create_payment(payer, payable, payee)
         assert IPayment.providedBy(result)
         assert result.gateway_service == u'demo_creditcard'
         assert result.state == STATE_UNPAID
-        assert len(result) == 0  # no items stored
-
-    def test_create_payment_honors_payment_item(self):
-        # we inspect payment items and take their values
-        service = DemoCreditcardPaymentService()
-        payer, payment_item, payee = Payer(), PaymentItem(), Payee()
-        payment_item.item_id = u'SOME_ITEM_ID'
-        payer.payer_id = u'SOME_PAYER_ID'
-        payment_item.amount = decimal.Decimal("300.99")
-        result = service.create_payment(payer, [payment_item], payee)
-        self.assertEqual(result.amount, payment_item.amount)
-        assert len(result) == 1
 
     def test_create_payment_honors_payer(self):
         # we inspect payers when creating their payments
         service = DemoCreditcardPaymentService()
-        payer, payment_item, payee = Payer(), PaymentItem(), Payee()
-        payment_item.item_id = u'SOME_ITEM_ID'
+        payer, payable, payee = Payer(), FakePayable(), Payee()
         payer.payer_id = u'SOME_PAYER_ID'
-        result = service.create_payment(payer, [payment_item], payee)
+        result = service.create_payment(payer, payable, payee)
         assert result.payer_id == payer.payer_id
 
     def test_get_service_by_utility_name(self):
         # we can get the demo credit card service by its utility name
-        from zope.component import queryUtility
         service = queryUtility(
             IPaymentGatewayService, name="demo_creditcard")
         assert service is not None
+
+    def test_next_step_invalid_id(self):
+        # we cannot go to non-existent payments
+        service = DemoCreditcardPaymentService()
+        result = service.next_step('not-existent-payment-id')
+        assert result == (None, None)
+
+    def test_next_step(self):
+        # we are redirected to 'index' in the beginning
+        service = DemoCreditcardPaymentService()
+        p1 = Payment(FakePayer(), FakePayable())
+        self.app['payments']['1'] = p1
+        p_id = p1.payment_id
+        result = service.next_step(p_id)
+        assert result == (p1, 'demo_cc1')
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_payment.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_payment.py	(revision 12706)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_payment.py	(revision 12741)
@@ -1,2 +1,3 @@
+# -*- coding: utf-8 -*-
 ## $Id$
 ##
@@ -20,15 +21,55 @@
 import re
 import unittest
-from zope.component import getUtilitiesFor, getSiteManager, queryUtility
-from zope.interface import implements
+from zope.component import (
+    getUtilitiesFor, getSiteManager, queryUtility, getGlobalSiteManager,
+    )
+from zope.component.hooks import setSite
+from zope.interface import implements, implementer
 from zope.interface.verify import verifyClass, verifyObject
 from waeup.ikoba.payments.interfaces import (
     IPayment, STATE_UNPAID, STATE_PAID, STATE_FAILED,
     IPaymentGatewayService, IPaymentItem, IPaymentGatewayServicesLister,
+    IPayableFinder, IPayerFinder, IPayable, IPayer,
     )
+from waeup.ikoba.app import Company
 from waeup.ikoba.payments.payment import (
-    Payment, get_payment_providers, PaymentItem,
+    Payment, get_payment_providers, PaymentItem, format_payment_item_values,
+    get_payment, find_payable_from_payable_id, find_payer_from_payer_id,
+    format_amount,
     )
 from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase)
+
+
+@implementer(IPayer)
+class FakePayer(object):
+
+    def __init__(
+        self, payer_id=u'PAYER_01', first_name=u'Anna', last_name='Tester'):
+        self.payer_id = payer_id
+        self.first_name = first_name
+        self.last_name = last_name
+
+
+FAKE_PAYMENT_ITEMS = (
+    PaymentItem(u'ITEM1', u'Item title 1', decimal.Decimal("1.00")),
+    PaymentItem(u'ITEM2', u'Item title 2', decimal.Decimal("2.2")),
+    )
+
+
+@implementer(IPayable)
+class FakePayable(object):
+
+    payable_id = u'id1'
+    items = (
+        (u'item 1', decimal.Decimal("1.00")),
+        (u'item 2', decimal.Decimal("2.12")),
+        )
+
+    def __init__(self, payable_id=u'PAYABLE_01', title=u'title',
+                 currency=u'USD', payment_items=FAKE_PAYMENT_ITEMS):
+        self.payable_id = payable_id
+        self.title = title
+        self.currency = currency
+        self.payment_items = payment_items
 
 
@@ -61,4 +102,33 @@
         assert result['some_name'] is fake_util
 
+    def test_format_payment_item_values(self):
+        # we can format lists of payment item values
+        result = format_payment_item_values(
+            [(u'Item 1', 'USD', decimal.Decimal("12.123")),
+             (u'Item 2', 'USD', decimal.Decimal("12.002")),
+             ], 'USD')
+        self.assertEqual(
+            result, [(u'Item 1', 'USD 12.12'),
+                     (u'Item 2', 'USD 12.00'),
+                     (u'Total', 'USD 24.12')]
+            )
+
+    def test_format_payment_item_values_req_single_currency(self):
+        # we require one currency for all items, yet.
+        self.assertRaises(
+            ValueError, format_payment_item_values,
+            [(u'Item 1', 'USD', decimal.Decimal("12.12")),
+             (u'Item 2', 'EUR', decimal.Decimal("50")),
+             ],
+            'USD')
+
+    def test_format_amount(self):
+        # we can make amounts readable
+        D = decimal.Decimal
+        self.assertEqual(format_amount(D("0"), 'USD'), u"US$ 0.00")
+        self.assertEqual(format_amount(D("0.1"), 'EUR'), u"€ 0.10")
+        self.assertEqual(format_amount(D("-1.2"), 'NGN'), u"₦ -1.20")
+        self.assertEqual(format_amount(D("1234.5"), 'YEN'), u"YEN 1,234.50")
+
 
 class FunctionalHelperTests(FunctionalTestCase):
@@ -76,16 +146,110 @@
         assert len(util()) > 0
 
-
-class PaymentTests(unittest.TestCase):
+    def test_get_payment(self):
+        # we can lookup payments.
+        self.getRootFolder()['app'] = Company()
+        app = self.getRootFolder()['app']
+        setSite(app)
+        p1 = Payment(FakePayer(), FakePayable())
+        app['payments']['1'] = p1
+        p_id = p1.payment_id
+        result = get_payment(p_id)
+        self.assertTrue(result is p1)
+        self.assertTrue(get_payment('not-valid') is None)
+
+    def test_find_payable_from_payable_id(self):
+        # we can find payables.
+        obj1 = object()
+        obj2 = object()
+
+        class FakeFinder(object):
+            valid = {'id1': obj1, 'id3': obj2}
+
+            def get_payable_by_id(self, the_id):
+                return self.valid.get(the_id)
+
+        finder1 = FakeFinder()
+        finder1.valid = {'id1': obj1}
+        finder2 = FakeFinder()
+        finder2.valid = {'id2': obj2}
+        gsm = getGlobalSiteManager()
+        try:
+            gsm.registerUtility(finder1, provided=IPayableFinder, name='f1')
+            gsm.registerUtility(finder2, provided=IPayableFinder, name='f2')
+            result1 = find_payable_from_payable_id('id1')
+            result2 = find_payable_from_payable_id('id2')
+            result3 = find_payable_from_payable_id('id3')
+        finally:
+            gsm.unregisterUtility(finder1, IPayableFinder)
+            gsm.unregisterUtility(finder2, IPayableFinder)
+        self.assertTrue(result1 is obj1)
+        self.assertTrue(result2 is obj2)
+        self.assertTrue(result3 is None)
+
+    def test_find_payer_from_payer_id(self):
+        # we can find payables.
+        obj1 = object()
+        obj2 = object()
+
+        class FakeFinder(object):
+            valid = {'id1': obj1, 'id3': obj2}
+
+            def get_payer_by_id(self, the_id):
+                return self.valid.get(the_id)
+
+        finder1 = FakeFinder()
+        finder1.valid = {'id1': obj1}
+        finder2 = FakeFinder()
+        finder2.valid = {'id2': obj2}
+        gsm = getGlobalSiteManager()
+        try:
+            gsm.registerUtility(finder1, provided=IPayerFinder, name='f1')
+            gsm.registerUtility(finder2, provided=IPayerFinder, name='f2')
+            result1 = find_payer_from_payer_id('id1')
+            result2 = find_payer_from_payer_id('id2')
+            result3 = find_payer_from_payer_id('id3')
+        finally:
+            gsm.unregisterUtility(finder1, IPayerFinder)
+            gsm.unregisterUtility(finder2, IPayerFinder)
+        self.assertTrue(result1 is obj1)
+        self.assertTrue(result2 is obj2)
+        self.assertTrue(result3 is None)
+
+
+class PaymentTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(PaymentTests, self).setUp()
+        self.payer = FakePayer()
+        self.payable = FakePayable()
 
     def test_iface(self):
         # Payments fullfill any interface contracts
-        obj = Payment()
+        obj = Payment(self.payer, self.payable)
         verifyClass(IPayment, Payment)
         verifyObject(IPayment, obj)
 
+    def test_initial_values(self):
+        # important attributes are set initially
+        payer = self.payer
+        payer.payer_id = u'PAYER_ID'
+        payable = self.payable
+        payable.payable_id = u'PAYABLE_ID'
+        payable.title = u'PAYABLE-TITLE'
+        payable.currency = 'NGN'
+        payment = Payment(payer, payable)
+        assert payment.payer_id == u'PAYER_ID'
+        assert payment.payable_id == u'PAYABLE_ID'
+        assert payment.title == u'PAYABLE-TITLE'
+        assert payment.currency == 'NGN'
+        assert isinstance(payment.creation_date, datetime.datetime)
+        assert payment.payment_date is None
+
     def test_payment_id_unique(self):
         # we get unique payment ids
-        p1, p2 = Payment(), Payment()
+        p1 = Payment(self.payer, self.payable)
+        p2 = Payment(self.payer, self.payable)
         id1, id2 = p1.payment_id, p2.payment_id
         assert id1 != id2
@@ -93,5 +257,5 @@
     def test_payment_id_format(self):
         # payment ids have a special format: "PAY_<32 hex digits>"
-        id1 = Payment().payment_id
+        id1 = Payment(self.payer, self.payable).payment_id
         assert isinstance(id1, basestring)
         assert re.match('PAY_[0-9a-f]{32}', id1)
@@ -99,10 +263,10 @@
     def test_initial_state_is_unpaid(self):
         # the initial state of payments is <unpaid>
-        p1 = Payment()
+        p1 = Payment(self.payer, self.payable)
         assert p1.state == STATE_UNPAID
 
     def test_approve(self):
         # we can approve payments
-        p1 = Payment()
+        p1 = Payment(self.payer, self.payable)
         p1.approve()
         assert p1.state == STATE_PAID
@@ -112,5 +276,5 @@
     def test_approve_datetime_given(self):
         # we can give a datetime
-        p1 = Payment()
+        p1 = Payment(self.payer, self.payable)
         some_datetime = datetime.datetime(2014, 1, 1, 0, 0, 0)
         p1.approve(payment_date=some_datetime)
@@ -120,5 +284,5 @@
         # if we do not give a datetime, current one will be used
         current = datetime.datetime.utcnow()
-        p1 = Payment()
+        p1 = Payment(self.payer, self.payable)
         p1.approve()
         assert p1.payment_date >= current
@@ -126,54 +290,29 @@
     def test_mark_failed(self):
         # we can mark payments as failed
-        p1 = Payment()
+        p1 = Payment(self.payer, self.payable)
         p1.mark_failed()
         assert p1.state == STATE_FAILED
 
-    def test_add_payment_item(self):
-        # we can add payment items
-        p1 = Payment()
-        item1 = PaymentItem()
-        result = p1.add_payment_item(item1)
-        assert len(p1) == 1  # do not make assumptions about result content
-        assert isinstance(result, basestring)
-
-    def test_add_payment_item_multiple(self):
-        # we can add several items
-        p1 = Payment()
-        item1 = PaymentItem()
-        item2 = PaymentItem()
-        result1 = p1.add_payment_item(item1)
-        result2 = p1.add_payment_item(item2)
-        assert len(p1) == 2  # do not make assumptions about result content
-        assert isinstance(result1, basestring)
-        assert isinstance(result2, basestring)
-
     def test_amount(self):
         # the amount of a payment is the sum of amounts of its items
-        p1 = Payment()
-        item1 = PaymentItem()
-        item2 = PaymentItem()
-        p1.add_payment_item(item1)
-        p1.add_payment_item(item2)
-        item1.amount = decimal.Decimal("12.25")
-        item2.amount = decimal.Decimal("0.5")
+        payable = self.payable
+        payable.payment_items[0].amount = decimal.Decimal("12.25")
+        payable.payment_items[1].amount = decimal.Decimal("0.5")
+        p1 = Payment(self.payer, self.payable)
         assert p1.amount == decimal.Decimal("12.75")
 
     def test_amount_negative(self):
         # we can sum up negative numbers
-        p1 = Payment()
-        item1 = PaymentItem()
-        item2 = PaymentItem()
-        p1.add_payment_item(item1)
-        p1.add_payment_item(item2)
-        item1.amount = decimal.Decimal("2.21")
-        item2.amount = decimal.Decimal("-3.23")
+        payable = self.payable
+        payable.payment_items[0].amount = decimal.Decimal("2.21")
+        payable.payment_items[1].amount = decimal.Decimal("-3.23")
+        p1 = Payment(self.payer, payable)
         assert p1.amount == decimal.Decimal("-1.02")
 
     def test_amount_empty(self):
-        # the amount of zero items is 0.00.
-        p1 = Payment()
-        assert p1.amount == decimal.Decimal("0.00")
-        assert isinstance(p1.amount, decimal.Decimal)
+        # the amount of zero items is None.
+        payable = FakePayable(payment_items=())
+        p1 = Payment(self.payer, payable)
+        self.assertEqual(p1.amount, decimal.Decimal("0.00"))
 
 
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_paypal.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_paypal.py	(revision 12706)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_paypal.py	(revision 12741)
@@ -1174,5 +1174,5 @@
     def test_store_credit_card_invalid(self):
         # an exception is raised with invalid credit cards.
-        site = self.create_site()
+        self.create_site()
         service = PayPalCreditCardService()
         credit_card = CreditCard(
@@ -1203,5 +1203,5 @@
         # we can actually create payments
         service = PayPalCreditCardService()
-        site = self.create_site()
+        self.create_site()
         credit_card = self.get_credit_card()
         result = service.store_credit_card(credit_card)
