Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/demo_provider.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/demo_provider.py	(revision 12671)
+++ 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 12671)
+++ 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 12671)
+++ 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 12671)
+++ 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 12671)
+++ 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 12671)
+++ 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 12671)
+++ 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)
