Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser.py	(revision 12663)
+++ 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 12663)
+++ 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 12663)
+++ 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 12663)
+++ 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 12663)
+++ 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 12663)
+++ 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 12663)
+++ 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 12663)
+++ 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 12663)
+++ 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):
 
