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)
