Index: main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser.py
===================================================================
--- main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser.py	(revision 12739)
+++ main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/browser.py	(revision 12740)
@@ -1532,5 +1532,4 @@
             payment, view_name = service.next_step(payment.payment_id)
             url = self.url(payment, view_name)
-            IWorkflowInfo(self.context).fireTransition('await')
             self.redirect(url)
             return
Index: main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/contracts.py
===================================================================
--- main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/contracts.py	(revision 12739)
+++ main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/customers/contracts.py	(revision 12740)
@@ -20,4 +20,5 @@
 """
 import grok
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
 from zope.catalog.interfaces import ICatalog
 from zope.component import getUtility, queryUtility
@@ -25,5 +26,4 @@
 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 (
@@ -33,6 +33,11 @@
     IContractSelectProduct, ICustomersUtils, ISampleContract,
     ISampleContractProcess, ISampleContractEdit, ISampleContractOfficialUse)
-from waeup.ikoba.payments.interfaces import IPayer, IPayableFinder, IPayable
-from waeup.ikoba.payments.payment import PaymentItem
+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
 
@@ -248,4 +253,24 @@
 
 
+@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')
Index: main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/demo_provider.py
===================================================================
--- main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/demo_provider.py	(revision 12739)
+++ main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/demo_provider.py	(revision 12740)
@@ -1,10 +1,16 @@
+import datetime
 import grok
+import random
+import re
+from zope.event import notify
 from waeup.ikoba.interfaces import MessageFactory as _
 from waeup.ikoba.browser.layout import IkobaEditFormPage, action
 from waeup.ikoba.payments.interfaces import (
-    IPaymentGatewayService, IPayment, IPayable, IPayer, IPayee,)
+    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,
-    PaymentProviderServiceBase)
+    Payment, get_payment, find_payable_from_payable_id, format_amount,
+    find_payer_from_payer_id, PaymentProviderServiceBase,
+    PaymentWaitingForGatewayEvent, PaymentFinishedEvent)
 
 
@@ -16,4 +22,8 @@
     'Do you really want to submit?'
 )
+
+
+RE_CC_NUMBER = re.compile('^[0-9]{9,25}$')
+RE_CSC = re.compile('^[0-9]{3,4}$')
 
 
@@ -49,4 +59,6 @@
         if payment is None:
             return None, None
+        if payment.state == STATE_AWAITING_GATEWAY_CONFIRM:
+            return payment, 'demo_cc2'
         return payment, 'demo_cc1'
 
@@ -61,11 +73,61 @@
     pnav = 4
 
-    def update(self):
-        pass
+    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):
-        print "AUTH!"
+        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'))
@@ -78,6 +140,37 @@
         if payed_item is not None:
             # remove context/payment?
-            target = self.url(payed_item)
+            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/branches/uli-payments/src/waeup/ikoba/payments/interfaces.py
===================================================================
--- main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/interfaces.py	(revision 12739)
+++ main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/interfaces.py	(revision 12740)
@@ -20,4 +20,5 @@
 from zope import schema
 from zope.component import getUtilitiesFor
+from zope.component.interfaces import IObjectEvent
 from zope.interface import Interface, Attribute
 from waeup.ikoba.interfaces import (
@@ -28,12 +29,34 @@
 #: 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),
     )
+
+
+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.""")
 
 
Index: main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/payment.py
===================================================================
--- main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/payment.py	(revision 12739)
+++ main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/payment.py	(revision 12740)
@@ -32,6 +32,15 @@
     IPayment, STATE_UNPAID, STATE_FAILED, STATE_PAID,
     IPaymentGatewayService, IPayer, IPaymentItem, IPayee,
-    IPaymentGatewayServicesLister, IPayableFinder,
+    IPaymentGatewayServicesLister, IPayableFinder, IPayerFinder,
+    IPaymentWaitingForGatewayEvent, IPaymentFinishedEvent,
     )
+
+
+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))
 
 
@@ -57,4 +66,17 @@
     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
@@ -89,4 +111,18 @@
         getUtilitiesFor(IPaymentGatewayService)
     )
+
+
+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
 
 
Index: main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/templates/demo_cc_step1.pt
===================================================================
--- main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/templates/demo_cc_step1.pt	(revision 12739)
+++ main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/templates/demo_cc_step1.pt	(revision 12740)
@@ -1,4 +1,5 @@
 <form class="form-horizontal">
-  <div class="form-group">
+  <div class="form-group"
+       tal:attributes="class view/validations/first_name">
     <label for="firstname" class="col-sm-3 control-label">
       Cardholder's First Name:
@@ -6,9 +7,10 @@
     <div class="col-sm-5">
       <input type="text" class="form-control" placeholder="John"
-             id="firstname" />
+             id="firstname" name="first_name"
+             tal:attributes="value view/first_name" />
     </div>
   </div>
 
-  <div class="form-group">
+  <div tal:attributes="class view/validations/last_name" class="form-group">
     <label for="lastname" class="col-sm-3 control-label">
       Cardholder's Last Name:
@@ -16,18 +18,22 @@
     <div class="col-sm-5">
       <input type="text" class="form-control" placeholder="Smith"
-             id="lastname" />
+             id="lastname" name="last_name"
+             tal:attributes="value view/last_name" />
     </div>
   </div>
 
-  <div class="form-group">
+  <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" />
+      <input type="text" class="form-control" id="ccnumber"
+             name="cc_number" tal:attributes="value view/cc_number" />
     </div>
   </div>
 
-  <div class="form-group">
+  <div class="form-group"
+       tal:attributes="class view/validations/exp_date">
     <label for="exp_date" class="col-sm-3 control-label">
       Expiration Date:
@@ -35,6 +41,7 @@
 
     <div class="col-sm-2">
-      <select class="form-control text-right" id="exp_date_month">
-        <option class="text-right">1</option>
+      <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>
@@ -52,17 +59,18 @@
 
     <div class="col-sm-2">
-      <select class="form-control" id="exp_date_month">
-        <option>2015</option>
+      <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>
-        <option>2020</option>
-        <option>2021</option>
       </select>
     </div>
   </div>
 
-  <div class="form-group">
+  <div class="form-group"
+       tal:attributes="class view/validations/csc">
     <label for="sec_code" class="col-sm-3 control-label">
       Security Code (CSC):
@@ -75,5 +83,6 @@
     </label>
     <div class="col-sm-3">
-      <input type="text" class="form-control" maxlength="4" id="sec_code" />
+      <input type="password" class="form-control" maxlength="4" id="sec_code"
+             name="csc" tal:attributes="value view/csc" />
     </div>
 
@@ -123,5 +132,5 @@
     </div>
     <div class="col-sm-9 text-left">
-      <b>$&nbsp;3,012.40&nbsp;USD</b>
+      <b tal:content="view/amount">$&nbsp;3,012.40&nbsp;USD</b>
     </div>
   </div>
Index: main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/templates/demo_cc_step2.pt
===================================================================
--- main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/templates/demo_cc_step2.pt	(revision 12740)
+++ main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/templates/demo_cc_step2.pt	(revision 12740)
@@ -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/branches/uli-payments/src/waeup/ikoba/payments/tests/test_demo_provider.py
===================================================================
--- main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_demo_provider.py	(revision 12739)
+++ main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_demo_provider.py	(revision 12740)
@@ -1,2 +1,3 @@
+import unittest
 from zope.component import queryUtility
 from zope.component.hooks import setSite
@@ -11,15 +12,34 @@
 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(DemoCreditcarPaymentServiceTests, self).setUp()
+        super(DemoCreditcardPaymentServiceTests, self).setUp()
         self.app = Company()
         self.getRootFolder()['app'] = self.app
Index: main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_payment.py
===================================================================
--- main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_payment.py	(revision 12739)
+++ main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_payment.py	(revision 12740)
@@ -1,2 +1,3 @@
+# -*- coding: utf-8 -*-
 ## $Id$
 ##
@@ -29,10 +30,11 @@
     IPayment, STATE_UNPAID, STATE_PAID, STATE_FAILED,
     IPaymentGatewayService, IPaymentItem, IPaymentGatewayServicesLister,
-    IPayableFinder, IPayable, IPayer,
+    IPayableFinder, IPayerFinder, IPayable, IPayer,
     )
 from waeup.ikoba.app import Company
 from waeup.ikoba.payments.payment import (
     Payment, get_payment_providers, PaymentItem, format_payment_item_values,
-    get_payment, find_payable_from_payable_id,
+    get_payment, find_payable_from_payable_id, find_payer_from_payer_id,
+    format_amount,
     )
 from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase)
@@ -120,4 +122,12 @@
              ],
             '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")
 
 
@@ -177,4 +187,33 @@
         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):
