Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/__init__.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/__init__.py	(revision 12288)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/__init__.py	(revision 12311)
@@ -3,8 +3,8 @@
 # Make this a package.
 from waeup.ikoba.payments.container import PaymentsContainer
-from waeup.ikoba.payments.payment import OnlinePayment
+from waeup.ikoba.payments.payment import Payment
 
 __all__ = [
     'PaymentsContainer',
-    'OnlinePayment',
+    'Payment',
     ]
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/catalog.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/catalog.py	(revision 12288)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/catalog.py	(revision 12311)
@@ -22,4 +22,5 @@
 from waeup.ikoba.payments.interfaces import IPayment
 
+
 class PaymentIndexes(grok.Indexes):
     """A catalog for all payments.
@@ -29,6 +30,7 @@
     grok.context(IPayment)
 
-    p_id = grok.index.Field(attribute='p_id')
-    p_category = grok.index.Field(attribute='p_category')
-    p_item = grok.index.Field(attribute='p_item')
-    p_state = grok.index.Field(attribute='p_state')
+    payment_id = grok.index.Field(attribute='payment_id')
+    payer_id = grok.index.Field(attribute='payer_id')
+    payed_item_id = grok.index.Field(attribute='payed_item_id')
+    state = grok.index.Field(attribute='state')
+    amount = grok.index.Field(attribute='amount')
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/container.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/container.py	(revision 12288)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/container.py	(revision 12311)
@@ -20,7 +20,7 @@
 """
 import grok
-from grok import index
 from waeup.ikoba.payments.interfaces import IPaymentsContainer
 from waeup.ikoba.utils.helpers import attrs_to_fields
+
 
 class PaymentsContainer(grok.Container):
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/currencies.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/currencies.py	(revision 12311)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/currencies.py	(revision 12311)
@@ -0,0 +1,226 @@
+"""Currencies
+"""
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.interfaces import SimpleIkobaVocabulary
+
+#: Currencies according to ISO 4217
+#:
+#: Compiled from:
+#:   http://www.currency-iso.org/dam/downloads/table_a1.xml
+#: Entries without decimal units (such as funds or not officially
+#: traded currencies) were removed.
+#:
+#: Data published: 2014-08-15
+#:
+#: Keys are the currency codes (as given by ISO)
+#: Entries have four items:
+#:   0: Authority maintaining the currency (normally a country)
+#:   1: Name of currency
+#:   2: Number of decimals after decimal separator
+#:   3: Currency nuber in ISO listing
+#:
+ISO_4217_CURRENCIES = {
+    'AED': ('UNITED ARAB EMIRATES', _('UAE Dirham'), 2, '784'),
+    'AFN': ('AFGHANISTAN', _('Afghani'), 2, '971'),
+    'ALL': ('ALBANIA', _('Lek'), 2, '008'),
+    'AMD': ('ARMENIA', _('Armenian Dram'), 2, '051'),
+    'ANG': ('SINT MAARTEN (DUTCH PART)',
+            _('Netherlands Antillean Guilder'),
+            2,
+            '532'),
+    'AOA': ('ANGOLA', _('Kwanza'), 2, '973'),
+    'ARS': ('ARGENTINA', _('Argentine Peso'), 2, '032'),
+    'AUD': ('TUVALU', _('Australian Dollar'), 2, '036'),
+    'AWG': ('ARUBA', _('Aruban Florin'), 2, '533'),
+    'AZN': ('AZERBAIJAN', _('Azerbaijanian Manat'), 2, '944'),
+    'BAM': ('BOSNIA AND HERZEGOVINA', _('Convertible Mark'), 2, '977'),
+    'BBD': ('BARBADOS', _('Barbados Dollar'), 2, '052'),
+    'BDT': ('BANGLADESH', _('Taka'), 2, '050'),
+    'BGN': ('BULGARIA', _('Bulgarian Lev'), 2, '975'),
+    'BHD': ('BAHRAIN', _('Bahraini Dinar'), 3, '048'),
+    'BIF': ('BURUNDI', _('Burundi Franc'), 0, '108'),
+    'BMD': ('BERMUDA', _('Bermudian Dollar'), 2, '060'),
+    'BND': ('BRUNEI DARUSSALAM', _('Brunei Dollar'), 2, '096'),
+    'BOB': ('BOLIVIA, PLURINATIONAL STATE OF', _('Boliviano'), 2, '068'),
+    'BOV': ('BOLIVIA, PLURINATIONAL STATE OF', _('Mvdol'), 2, '984'),
+    'BRL': ('BRAZIL', _('Brazilian Real'), 2, '986'),
+    'BSD': ('BAHAMAS', _('Bahamian Dollar'), 2, '044'),
+    'BTN': ('BHUTAN', _('Ngultrum'), 2, '064'),
+    'BWP': ('BOTSWANA', _('Pula'), 2, '072'),
+    'BYR': ('BELARUS', _('Belarussian Ruble'), 0, '974'),
+    'BZD': ('BELIZE', _('Belize Dollar'), 2, '084'),
+    'CAD': ('CANADA', _('Canadian Dollar'), 2, '124'),
+    'CDF': ('CONGO, DEMOCRATIC REPUBLIC OF THE ',
+            _('Congolese Franc'),
+            2,
+            '976'),
+    'CHE': ('SWITZERLAND', _('WIR Euro'), 2, '947'),
+    'CHF': ('SWITZERLAND', _('Swiss Franc'), 2, '756'),
+    'CHW': ('SWITZERLAND', _('WIR Franc'), 2, '948'),
+    'CLF': ('CHILE', _('Unidad de Fomento'), 4, '990'),
+    'CLP': ('CHILE', _('Chilean Peso'), 0, '152'),
+    'CNY': ('CHINA', _('Yuan Renminbi'), 2, '156'),
+    'COP': ('COLOMBIA', _('Colombian Peso'), 2, '170'),
+    'COU': ('COLOMBIA', _('Unidad de Valor Real'), 2, '970'),
+    'CRC': ('COSTA RICA', _('Costa Rican Colon'), 2, '188'),
+    'CUC': ('CUBA', _('Peso Convertible'), 2, '931'),
+    'CUP': ('CUBA', _('Cuban Peso'), 2, '192'),
+    'CVE': ('CABO VERDE', _('Cabo Verde Escudo'), 2, '132'),
+    'CZK': ('CZECH REPUBLIC', _('Czech Koruna'), 2, '203'),
+    'DJF': ('DJIBOUTI', _('Djibouti Franc'), 0, '262'),
+    'DKK': ('GREENLAND', _('Danish Krone'), 2, '208'),
+    'DOP': ('DOMINICAN REPUBLIC', _('Dominican Peso'), 2, '214'),
+    'DZD': ('ALGERIA', _('Algerian Dinar'), 2, '012'),
+    'EGP': ('EGYPT', _('Egyptian Pound'), 2, '818'),
+    'ERN': ('ERITREA', _('-Nakfa'), 2, '232'),
+    'ETB': ('ETHIOPIA', _('Ethiopian Birr'), 2, '230'),
+    'EUR': ('SPAIN', _('Euro'), 2, '978'),
+    'FJD': ('FIJI', _('Fiji Dollar'), 2, '242'),
+    'FKP': ('FALKLAND ISLANDS (MALVINAS)',
+            _('Falkland Islands Pound'),
+            2,
+            '238'),
+    'GBP': ('UNITED KINGDOM', _('Pound Sterling'), 2, '826'),
+    'GEL': ('GEORGIA', _('Lari'), 2, '981'),
+    'GHS': ('GHANA', _('Ghana Cedi'), 2, '936'),
+    'GIP': ('GIBRALTAR', _('Gibraltar Pound'), 2, '292'),
+    'GMD': ('GAMBIA', _('Dalasi'), 2, '270'),
+    'GNF': ('GUINEA', _('Guinea Franc'), 0, '324'),
+    'GTQ': ('GUATEMALA', _('Quetzal'), 2, '320'),
+    'GYD': ('GUYANA', _('Guyana Dollar'), 2, '328'),
+    'HKD': ('HONG KONG', _('Hong Kong Dollar'), 2, '344'),
+    'HNL': ('HONDURAS', _('Lempira'), 2, '340'),
+    'HRK': ('CROATIA', _('Croatian Kuna'), 2, '191'),
+    'HTG': ('HAITI', _('Gourde'), 2, '332'),
+    'HUF': ('HUNGARY', _('Forint'), 2, '348'),
+    'IDR': ('INDONESIA', _('Rupiah'), 2, '360'),
+    'ILS': ('ISRAEL', _('New Israeli Sheqel'), 2, '376'),
+    'INR': ('INDIA', _('Indian Rupee'), 2, '356'),
+    'IQD': ('IRAQ', _('Iraqi Dinar'), 3, '368'),
+    'IRR': ('IRAN, ISLAMIC REPUBLIC OF', _('Iranian Rial'), 2, '364'),
+    'ISK': ('ICELAND', _('Iceland Krona'), 0, '352'),
+    'JMD': ('JAMAICA', _('Jamaican Dollar'), 2, '388'),
+    'JOD': ('JORDAN', _('Jordanian Dinar'), 3, '400'),
+    'JPY': ('JAPAN', _('Yen'), 0, '392'),
+    'KES': ('KENYA', _('Kenyan Shilling'), 2, '404'),
+    'KGS': ('KYRGYZSTAN', _('Som'), 2, '417'),
+    'KHR': ('CAMBODIA', _('Riel'), 2, '116'),
+    'KMF': ('COMOROS', _('Comoro Franc'), 0, '174'),
+    'KPW': (u"KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF",
+            _('North Korean Won'),
+            2,
+            '408'),
+    'KRW': ('KOREA, REPUBLIC OF', _('Won'), 0, '410'),
+    'KWD': ('KUWAIT', _('Kuwaiti Dinar'), 3, '414'),
+    'KYD': ('CAYMAN ISLANDS', _('Cayman Islands Dollar'), 2, '136'),
+    'KZT': ('KAZAKHSTAN', _('Tenge'), 2, '398'),
+    'LAK': (u"LAO PEOPLE'S DEMOCRATIC REPUBLIC", _('Kip'), 2, '418'),
+    'LBP': ('LEBANON', _('Lebanese Pound'), 2, '422'),
+    'LKR': ('SRI LANKA', _('Sri Lanka Rupee'), 2, '144'),
+    'LRD': ('LIBERIA', _('Liberian Dollar'), 2, '430'),
+    'LSL': ('LESOTHO', _('Loti'), 2, '426'),
+    'LTL': ('LITHUANIA', _('Lithuanian Litas'), 2, '440'),
+    'LYD': ('LIBYA', _('Libyan Dinar'), 3, '434'),
+    'MAD': ('WESTERN SAHARA', _('Moroccan Dirham'), 2, '504'),
+    'MDL': ('MOLDOVA, REPUBLIC OF', _('Moldovan Leu'), 2, '498'),
+    'MGA': ('MADAGASCAR', _('Malagasy Ariary'), 2, '969'),
+    'MKD': ('MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF',
+            _('Denar'),
+            2,
+            '807'),
+    'MMK': ('MYANMAR', _('Kyat'), 2, '104'),
+    'MNT': ('MONGOLIA', _('Tugrik'), 2, '496'),
+    'MOP': ('MACAO', _('Pataca'), 2, '446'),
+    'MRO': ('MAURITANIA', _('Ouguiya'), 2, '478'),
+    'MUR': ('MAURITIUS', _('Mauritius Rupee'), 2, '480'),
+    'MVR': ('MALDIVES', _('Rufiyaa'), 2, '462'),
+    'MWK': ('MALAWI', _('Kwacha'), 2, '454'),
+    'MXN': ('MEXICO', _('Mexican Peso'), 2, '484'),
+    'MXV': ('MEXICO', _('Mexican Unidad de Inversion (UDI)'), 2, '979'),
+    'MYR': ('MALAYSIA', _('Malaysian Ringgit'), 2, '458'),
+    'MZN': ('MOZAMBIQUE', _('Mozambique Metical'), 2, '943'),
+    'NAD': ('NAMIBIA', _('Namibia Dollar'), 2, '516'),
+    'NGN': ('NIGERIA', _('Naira'), 2, '566'),
+    'NIO': ('NICARAGUA', _('Cordoba Oro'), 2, '558'),
+    'NOK': ('SVALBARD AND JAN MAYEN', _('Norwegian Krone'), 2, '578'),
+    'NPR': ('NEPAL', _('Nepalese Rupee'), 2, '524'),
+    'NZD': ('TOKELAU', _('New Zealand Dollar'), 2, '554'),
+    'OMR': ('OMAN', _('Rial Omani'), 3, '512'),
+    'PAB': ('PANAMA', _('Balboa'), 2, '590'),
+    'PEN': ('PERU', _('Nuevo Sol'), 2, '604'),
+    'PGK': ('PAPUA NEW GUINEA', _('Kina'), 2, '598'),
+    'PHP': ('PHILIPPINES', _('Philippine Peso'), 2, '608'),
+    'PKR': ('PAKISTAN', _('Pakistan Rupee'), 2, '586'),
+    'PLN': ('POLAND', _('Zloty'), 2, '985'),
+    'PYG': ('PARAGUAY', _('Guarani'), 0, '600'),
+    'QAR': ('QATAR', _('Qatari Rial'), 2, '634'),
+    'RON': ('ROMANIA', _('New Romanian Leu'), 2, '946'),
+    'RSD': ('SERBIA', _('Serbian Dinar'), 2, '941'),
+    'RUB': ('RUSSIAN FEDERATION', _('Russian Ruble'), 2, '643'),
+    'RWF': ('RWANDA', _('Rwanda Franc'), 0, '646'),
+    'SAR': ('SAUDI ARABIA', _('Saudi Riyal'), 2, '682'),
+    'SBD': ('SOLOMON ISLANDS', _('Solomon Islands Dollar'), 2, '090'),
+    'SCR': ('SEYCHELLES', _('Seychelles Rupee'), 2, '690'),
+    'SDG': ('SUDAN', _('Sudanese Pound'), 2, '938'),
+    'SEK': ('SWEDEN', _('Swedish Krona'), 2, '752'),
+    'SGD': ('SINGAPORE', _('Singapore Dollar'), 2, '702'),
+    'SHP': ('SAINT HELENA, ASCENSION AND TRISTAN DA CUNHA',
+            _('Saint Helena Pound'),
+            2,
+            '654'),
+    'SLL': ('SIERRA LEONE', _('Leone'), 2, '694'),
+    'SOS': ('SOMALIA', _('Somali Shilling'), 2, '706'),
+    'SRD': ('SURINAME', _('Surinam Dollar'), 2, '968'),
+    'SSP': ('SOUTH SUDAN', _('South Sudanese Pound'), 2, '728'),
+    'STD': ('SAO TOME AND PRINCIPE', _('Dobra'), 2, '678'),
+    'SVC': ('EL SALVADOR', _('El Salvador Colon'), 2, '222'),
+    'SYP': ('SYRIAN ARAB REPUBLIC', _('Syrian Pound'), 2, '760'),
+    'SZL': ('SWAZILAND', _('Lilangeni'), 2, '748'),
+    'THB': ('THAILAND', _('Baht'), 2, '764'),
+    'TJS': ('TAJIKISTAN', _('Somoni'), 2, '972'),
+    'TMT': ('TURKMENISTAN', _('Turkmenistan New Manat'), 2, '934'),
+    'TND': ('TUNISIA', _('Tunisian Dinar'), 3, '788'),
+    'TOP': ('TONGA', _(u"Pa'anga"), 2, '776'),
+    'TRY': ('TURKEY', _('Turkish Lira'), 2, '949'),
+    'TTD': ('TRINIDAD AND TOBAGO', _('Trinidad and Tobago Dollar'), 2, '780'),
+    'TWD': ('TAIWAN, PROVINCE OF CHINA', _('New Taiwan Dollar'), 2, '901'),
+    'TZS': ('TANZANIA, UNITED REPUBLIC OF', _('Tanzanian Shilling'), 2, '834'),
+    'UAH': ('UKRAINE', _('Hryvnia'), 2, '980'),
+    'UGX': ('UGANDA', _('Uganda Shilling'), 0, '800'),
+    'USD': ('VIRGIN ISLANDS (U.S.)', _('US Dollar'), 2, '840'),
+    'USN': ('UNITED STATES', _('US Dollar (Next day)'), 2, '997'),
+    'UYI': ('URUGUAY',
+            _('Uruguay Peso en Unidades Indexadas (URUIURUI)'),
+            0,
+            '940'),
+    'UYU': ('URUGUAY', _('Peso Uruguayo'), 2, '858'),
+    'UZS': ('UZBEKISTAN', _('Uzbekistan Sum'), 2, '860'),
+    'VEF': ('VENEZUELA, BOLIVARIAN REPUBLIC OF', _('Bolivar'), 2, '937'),
+    'VND': ('VIET NAM', _('Dong'), 0, '704'),
+    'VUV': ('VANUATU', _('Vatu'), 0, '548'),
+    'WST': ('SAMOA', _('Tala'), 2, '882'),
+    'XAF': ('GABON', _('CFA Franc BEAC'), 0, '950'),
+    'XCD': ('SAINT VINCENT AND THE GRENADINES',
+            _('East Caribbean Dollar'),
+            2,
+            '951'),
+    'XOF': ('TOGO', _('CFA Franc BCEAO'), 0, '952'),
+    'XPF': ('WALLIS AND FUTUNA', _('CFP Franc'), 0, '953'),
+    'YER': ('YEMEN', _('Yemeni Rial'), 2, '886'),
+    'ZAR': ('SOUTH AFRICA', _('Rand'), 2, '710'),
+    'ZMW': ('ZAMBIA', _('Zambian Kwacha'), 2, '967'),
+    'ZWL': ('ZIMBABWE', _('Zimbabwe Dollar'), 2, '932')
+    }
+
+
+def get_decimal_units(currency_code):
+    """Get the number of decimals after decimal separator for a currency.
+
+    Pass in a three-letter currency code. Returns a number.
+    """
+    return ISO_4217_CURRENCIES[currency_code][2]
+
+
+#: A vocabulary of ISO 4217 currencies
+ISO_4217_CURRENCIES_VOCAB = SimpleIkobaVocabulary(
+    *[(val[1], key) for key, val in ISO_4217_CURRENCIES.items()])
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/interfaces.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/interfaces.py	(revision 12288)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/interfaces.py	(revision 12311)
@@ -16,16 +16,88 @@
 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 ##
-from zope.interface import Attribute
+import decimal
+from zc.sourcefactory.basic import BasicSourceFactory
 from zope import schema
+from zope.component import getUtilitiesFor
+from zope.interface import Interface
 from waeup.ikoba.interfaces import (
-    IIkobaObject, SimpleIkobaVocabulary,
-    ContextualDictSourceFactoryBase)
+    IIkobaObject, SimpleIkobaVocabulary, ContextualDictSourceFactoryBase)
 from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.payments.currencies import ISO_4217_CURRENCIES_VOCAB
+
+#: Possible states of payments
+STATE_UNPAID = 1
+STATE_PAID = 2
+STATE_FAILED = 4
 
 payment_states = SimpleIkobaVocabulary(
-    (_('Not yet paid'),'unpaid'),
-    (_('Paid'),'paid'),
-    (_('Failed'),'failed'),
+    (_('Not yet paid'), STATE_UNPAID),
+    (_('Paid'), STATE_PAID),
+    (_('Failed'), STATE_FAILED),
     )
+
+
+class PaymentGatewayServicesSource(BasicSourceFactory):
+    """A source that lists available payment services.
+
+    Suitable for forms etc. Token and value correspond to the name the
+    respective IPaymentGatewayService utility is registered with.
+    """
+
+    _services = None
+
+    @classmethod
+    def services(cls):
+        """Cache the services registered on startup.
+
+        We assume that services do not change after startup.
+        """
+        if cls._services is None:
+            cls._services = dict(getUtilitiesFor(IPaymentGatewayService))
+        return cls._services
+
+    def getValues(self):
+        """Get payment gateway registration names.
+        """
+        return sorted(PaymentGatewayServicesSource.services().keys())
+
+    def getTitle(self, value):
+        """Get title of the respective service, if it exists.
+        """
+        service = PaymentGatewayServicesSource.services().get(value, None)
+        if service is not None:
+            return service.title
+
+
+class IPaymentGatewayService(Interface):
+    """A financial gateway service.
+
+    Any gateway provider might provide several services. For instance
+    payments by credit card, scratch card, bank transfer, etc. An
+    `IPaymentGatewayService` represents one of those services.
+
+    Payment services are normally registered as a named global
+    utility.
+    """
+    title = schema.TextLine(
+        title=u'Title',
+        description=u'Human readable name of gateway service.',
+        required=True,
+        )
+
+    def create_payment(payer, payment_item,  payee):
+        """Create a payment.
+
+        For all parameters we expect an object, that implements
+        `IPayer`, `IPaymentItem`, or `IPayee` respectively. If not,
+        then the given objects must be at least adaptable to the
+        respective interface.
+
+        Therfore you can pass in some `Customer` as long as there is
+        some `IPayer` adapter for `Customer` objects defined.
+
+        Returns an `IPayment` object.
+        """
+
 
 class PaymentCategorySource(ContextualDictSourceFactoryBase):
@@ -36,4 +108,5 @@
     DICT_NAME = 'PAYMENT_CATEGORIES'
 
+
 class IPaymentsContainer(IIkobaObject):
     """A container for all kind of payment objects.
@@ -41,54 +114,98 @@
     """
 
+
 class IPayment(IIkobaObject):
     """A base representation of payments.
 
-    """
-    p_id = Attribute('Payment identifier')
-
-    p_category = schema.Choice(
-        title = _(u'Payment Category'),
-        #default = u'schoolfee',
-        source = PaymentCategorySource(),
-        required = True,
-        )
-
-    p_item = schema.TextLine(
-        title = u'',
-        default = None,
-        required = False,
-        )
-
-    display_item = schema.TextLine(
-        title = _(u'Payment Item'),
-        required = False,
-        readonly = True,
-        )
-
-    p_state = schema.Choice(
-        title = _(u'Payment State'),
-        default = u'unpaid',
-        vocabulary = payment_states,
-        required = True,
+    In a payment, a payer payes someone (the payee) for something, the
+    item to pay.
+
+    We currently support only the case where one payer pays one payee
+    for one item. The item might include taxes, handling,
+    shipping, etc.
+
+    As in RL any payment is actually performed by some financial
+    service provider (like paypal, interswitch, etc.), each of which
+    might provide several types of payments (credit card, scratch
+    card, you name it).
+
+    In Ikoba we call financial service providers 'gateway' and their
+    types of services are handled as gateway types. Therefore PayPal
+    handling a credit card payment is different from PayPal handling a
+    regular PayPal account transaction.
+
+    A payment can be approve()d, which means the act of paying was
+    really performed. It can also fail for any reason, in which case
+    we mark the payment 'failed'.
+    """
+    payment_id = schema.TextLine(
+        title=u'Payment Identifier',
+        default=None,
+        required=True,
+        )
+
+    payer_id = schema.TextLine(
+        title=u'Payer',
+        default=None,
+        required=True,
+        )
+
+    payed_item_id = schema.TextLine(
+        title=u'Payed Item ID',
+        default=None,
+        required=True,
+        )
+
+    gateway_service = schema.Choice(
+        title=u'Payment Gateway',
+        description=u'Payment gateway that handles this transaction.',
+        source=PaymentGatewayServicesSource(),
+        default=None,
+        required=True,
+        )
+
+    state = schema.Choice(
+        title=_(u'Payment State'),
+        default=STATE_UNPAID,
+        vocabulary=payment_states,
+        required=True,
         )
 
     creation_date = schema.Datetime(
-        title = _(u'Ticket Creation Date'),
-        readonly = False,
-        required = False,
+        title=_(u'Creation Datetime'),
+        readonly=False,
+        required=False,
         )
 
     payment_date = schema.Datetime(
-        title = _(u'Payment Date'),
-        required = False,
-        readonly = False,
-        )
-
-    amount_auth = schema.Float(
-        title = _(u'Amount Authorized'),
-        default = 0.0,
-        required = True,
-        readonly = False,
-        )
+        title=_(u'Payment Datetime'),
+        required=False,
+        readonly=False,
+        )
+
+    amount = schema.Decimal(
+        title=_(u'Amount'),
+        description=_(
+            'The overall sum payed, including all taxes fees, etc.'),
+        default=decimal.Decimal("0.00"),
+        required=True,
+        readonly=False,
+        )
+
+    def approve():
+        """Approve a payment.
+
+        The payment was approved and can now be considered payed. This
+        kind of approvement means the final one (in case there are
+        several instances to ask).
+        """
+
+    def mark_failed(reason=None):
+        """Mark the payment as failed.
+
+        A failed payment was canceled due to technical problems,
+        insufficient funds, etc.
+        """
+
 
 class IOnlinePayment(IPayment):
@@ -98,31 +215,100 @@
 
     ac = schema.TextLine(
-        title = _(u'Activation Code'),
-        default = None,
-        required = False,
-        readonly = False,
+        title=_(u'Activation Code'),
+        default=None,
+        required=False,
+        readonly=False,
         )
 
     r_amount_approved = schema.Float(
-        title = _(u'Response Amount Approved'),
-        default = 0.0,
-        required = False,
-        readonly = False,
+        title=_(u'Response Amount Approved'),
+        default=0.0,
+        required=False,
+        readonly=False,
         )
 
     r_code = schema.TextLine(
-        title = _(u'Response Code'),
-        default = None,
-        required = False,
-        readonly = False,
+        title=_(u'Response Code'),
+        default=None,
+        required=False,
+        readonly=False,
         )
 
     r_desc = schema.TextLine(
-        title = _(u'Response Description'),
-        default = None,
-        required = False,
-        readonly = False,
+        title=_(u'Response Description'),
+        default=None,
+        required=False,
+        readonly=False,
         )
 
     def approve():
         "Approve an online payment and set to paid."
+
+
+class ICreditCard(Interface):
+    """A credit card.
+
+    A credit card is connected to a Payer.
+    """
+    credit_card_id = schema.TextLine(
+        title=u'Internal Credit Card ID',
+        required=True,
+        )
+
+
+class IPayer(Interface):
+    """A payer.
+    """
+    payer_id = schema.TextLine(
+        title=u'Payer ID',
+        required=True,
+        )
+
+    first_name = schema.TextLine(
+        title=u'First Name',
+        required=False,
+        )
+
+    last_name = schema.TextLine(
+        title=u'Last Name',
+        required=False,
+        )
+
+
+class IPayee(Interface):
+    """A person or institution being paid.
+    """
+    payee_id = schema.TextLine(
+        title=u'Payee ID',
+        required=True
+        )
+
+
+class IPaymentItem(Interface):
+    """Something to sell.
+    """
+    item_id = schema.TextLine(
+        title=u'Payment Item ID',
+        required=True,
+        )
+
+    title = schema.TextLine(
+        title=u'Title',
+        description=u'A short title of the good sold.',
+        required=True,
+        default=u'Unnamed'
+        )
+
+    amount = schema.Decimal(
+        title=u'Amount',
+        description=u'Total amount, includung any taxes, fees, etc.',
+        required=True,
+        default=decimal.Decimal('0.00'),
+        )
+
+    currency = schema.Choice(
+        title=u'Currency',
+        source=ISO_4217_CURRENCIES_VOCAB,
+        required=True,
+        default='USD',
+        )
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/payment.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/payment.py	(revision 12288)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/payment.py	(revision 12311)
@@ -19,24 +19,38 @@
 These are the payment tickets.
 """
+import decimal
 import grok
+import uuid
 from datetime import datetime
-from grok import index
+from zope.component import getUtilitiesFor
 from zope.event import notify
-from zope.component import getUtility
-from zope.i18n import translate
-from waeup.ikoba.interfaces import IIkobaUtils
-from waeup.ikoba.interfaces import MessageFactory as _
 from waeup.ikoba.payments.interfaces import (
-    IPayment, IOnlinePayment,
-    payment_states)
-from waeup.ikoba.utils.helpers import attrs_to_fields, get_current_principal
+    IPayment, STATE_UNPAID, STATE_FAILED, STATE_PAID,
+    IPaymentGatewayService, IPayer,
+    )
 from waeup.ikoba.utils.logger import Logger
 
-class Payment(grok.Container, Logger):
+
+def get_payment_providers():
+    """Get all services of payment gateways registered.
+    """
+    return dict(
+        getUtilitiesFor(IPaymentGatewayService)
+        )
+
+
+class PaymentProviderServiceBase(grok.GlobalUtility):
+
+    grok.baseclass()
+    grok.implements(IPaymentGatewayService)
+
+    title = u'Sample Credit Card Service'
+
+
+class Payment(grok.Model, Logger):
     """This is a payment.
     """
     grok.implements(IPayment)
     grok.provides(IPayment)
-    grok.baseclass()
 
     logger_name = 'waeup.ikoba.${sitename}.payments'
@@ -44,62 +58,37 @@
     logger_format_str = '"%(asctime)s","%(user)s",%(message)s'
 
-    def logger_info(self, comment=None):
-        """Get the logger's info method.
-        """
-        self.logger.info('%s' % comment)
-        return
-
     def __init__(self):
         super(Payment, self).__init__()
         self.creation_date = datetime.utcnow()
-        self.p_id = None
+        self.payment_date = None
+        self.payment_id = u'PAY_' + unicode(uuid.uuid4().hex)
+        self.gateway_service = None
+        self.amount = decimal.Decimal("0.00")
+        self.payed_item_id = None
+        self.payer_id = None
+        self.state = STATE_UNPAID
         return
 
-    @property
-    def p_state_title(self):
-        return payment_states.getTermByToken(self.p_state).title
+    def approve(self, payment_date=None):
+        """A payment was approved.
 
-    @property
-    def category(self):
-        utils = getUtility(IIkobaUtils)
-        return utils.PAYMENT_CATEGORIES.get(self.p_category, None)
+        Successful ending; the payment is marked as payed.
 
-    @property
-    def display_item(self):
-        ikoba_utils = getUtility(IIkobaUtils)
-        return ikoba_utils.getPaymentItem(self)
+        If `payment_date` is given, it must be a datetime object
+        giving a datetime in UTC timezone.
 
-class OnlinePayment(Payment):
-    """This is an online payment.
-    """
-    grok.implements(IOnlinePayment)
-    grok.provides(IOnlinePayment)
+        Raises ObjectModifiedEvent.
+        """
+        if payment_date is None:
+            payment_date = datetime.utcnow()
+        self.payment_date = payment_date
+        self.state = STATE_PAID
+        notify(grok.ObjectModifiedEvent(self))
 
-    def __init__(self):
-        super(OnlinePayment, self).__init__()
-        p_id = None
-        return
+    def mark_failed(self, reason=None):
+        """Mark payment as failed.
 
-    def approve(self):
-        "Approve online payment and set to paid."
-        self.r_amount_approved = self.amount_auth
-        self.r_code = u'AP'
-        self.p_state = 'paid'
-        user = get_current_principal()
-        if user is None:
-            # in tests
-            usertitle = 'system'
-        else:
-            usertitle = getattr(user, 'public_name', None)
-            if not usertitle:
-                usertitle = user.title
-        r_desc = _('Payment approved by ${a}', mapping = {'a': usertitle})
-        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
-        self.r_desc = translate(r_desc, 'waeup.ikoba',
-            target_language=portal_language)
-        self.payment_date = datetime.utcnow()
-        # Update catalog
+        Raises ObjectModifiedEvent.
+        """
+        self.state = STATE_FAILED
         notify(grok.ObjectModifiedEvent(self))
-        return
-
-OnlinePayment = attrs_to_fields(OnlinePayment, omit=['display_item'])
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/paypal.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/paypal.py	(revision 12288)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/paypal.py	(revision 12311)
@@ -18,8 +18,91 @@
 """Support for PayPal payments.
 """
+import decimal
+import grok
 import ConfigParser
+import inspect
 import paypalrestsdk
+import re
+import uuid
+from zope import schema
 from zope.component import queryUtility
-from waeup.ikoba.interfaces import IPayPalConfig
+from zope.interface import Interface, Attribute
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.interfaces import IPayPalConfig, SimpleIkobaVocabulary
+from waeup.ikoba.utils.helpers import attrs_to_fields
+from waeup.ikoba.payments.interfaces import (
+    IPayment, IPaymentGatewayService, IPayer, IPaymentItem, IPayee,
+    )
+from waeup.ikoba.payments.paypal_countries import COUNTRIES_VOCAB
+from waeup.ikoba.payments.paypal_currencies import CURRENCIES_VOCAB
+
+#: Intents allowed for paypal based payments
+PAYMENT_INTENTS = ('sale', 'authorize', 'order')
+
+#: Payment methods allowed by PayPal
+PAYMENT_METHODS = ('credit_card', 'paypal')
+
+#: Payer status set/accepted by PayPal
+PAYER_STATUS = ('VERIFIED', 'UNVERIFIED')
+
+#: Tax ID types accepted by PayPal (yes, this list is complete)
+TAX_ID_TYPES = ('BR_CPF', 'BR_CNPJ')
+
+#: Address types accepted by PayPal
+ADDRESS_TYPES = ('residential', 'business', 'mailbox')
+
+#: A vocabulary with address types
+ADDRESS_TYPES_VOCAB = SimpleIkobaVocabulary(
+    *[(_(x), x) for x in ADDRESS_TYPES])
+
+#: Credit card types accepted by PayPal
+CREDIT_CARD_TYPES = ('visa', 'mastercard', 'discover', 'amex')
+
+CREDIT_CARD_TYPES_VOCAB = SimpleIkobaVocabulary(
+    *[(_(x), x) for x in CREDIT_CARD_TYPES])
+
+#: Credit card status accepted by PayPal
+CREDIT_CARD_STATUS = ('expired', 'ok')
+
+CREDIT_CARD_STATUS_VOCAB = SimpleIkobaVocabulary(
+    *[(_(x), x) for x in CREDIT_CARD_STATUS])
+
+#: Stock keeping units we support.
+STOCK_KEEPING_UNITS = {
+    "pcs": _("pieces"),
+    }
+
+STOCK_KEEPING_UNITS_VOCAB = SimpleIkobaVocabulary(
+    *[(value, key) for key, value in STOCK_KEEPING_UNITS.items()])
+
+#: Payment methods (flags) used by PayPal
+PAYMENT_OPTION_METHODS = ('INSTANT_FUNDING_SOURCE', )
+PAYMENT_OPTION_METHODS_VOCAB = SimpleIkobaVocabulary(
+    *[(_(x), x) for x in PAYMENT_OPTION_METHODS])
+
+
+def to_dict(obj, name_map={}):
+    """Turn `obj` into some dict representation.
+    """
+    result = dict()
+    for name in dir(obj):
+        if name.startswith('_'):
+            continue
+        value = getattr(obj, name)
+        if value is None:
+            continue
+        if inspect.ismethod(value):
+            continue
+        if hasattr(value, 'to_dict'):
+            value = value.to_dict()
+        elif isinstance(value, list):
+            value = [x.to_dict() for x in value]
+        elif isinstance(value, decimal.Decimal):
+            value = u"%.2f" % (value, )
+        else:
+            value = unicode(value)
+        name = name_map.get(name, name)
+        result[name] = value
+    return result
 
 
@@ -91,14 +174,902 @@
 
 
-def get_payment():
+class Payer(object):
+    """A payer as required in paypal payments.
+
+    According to Paypal docs:
+
+    `payment_method` must be one of ``'paypal'`` or ``'credit_card'``
+    as stored in `PAYMENT_METHODS`.
+
+    `funding_instruments` is a list of `FundingInstrument` objects. I
+    think the list must be empty for ``paypal`` payments and must
+    contain at least one entry for ``credit_card`` payments.
+
+    `payer_info` must be provided for ``paypal`` payments and might be
+    provided for ``credit_card`` payments. It's a `PayerInfo` object.
+
+    `status` reflects the payer's PayPal account status and is
+    currently supported for ``paypal`` payments. Allowed values are
+    ``'VERIFIED'`` and ``'UNVERIFIED'`` as stored in `PAYER_STATUS`.
+    """
+    def __init__(self, payment_method, funding_instruments=[],
+                 payer_info=None, status=None):
+        if payment_method not in PAYMENT_METHODS:
+            raise ValueError(
+                "Invalid payment method: use one of %s" %
+                (PAYMENT_METHODS, )
+                )
+        if status and status not in PAYER_STATUS:
+            raise ValueError(
+                "Invalid status: use one of %s" % (PAYER_STATUS, )
+                )
+        self.payment_method = payment_method
+        self.funding_instruments = funding_instruments
+        self.payer_info = payer_info
+        self.status = status
+
+
+class PayerInfo(object):
+    """Payer infos as required by Paypal payers.
+
+    Normally used with a `Payer` instance (which in turn is part of a
+    `payment`).
+
+    According to PayPal docs:
+
+    Pre-filled by PayPal when `payment_method` is ``'paypal'``.
+
+    `email`: 127 chars max. I don't think, this value is pre-filled.
+
+    `first_name`: First name of payer. Assigned by PayPal.
+
+    `last_naem`: Last name of payer. Assigned by PayPal.
+
+    `payer_id`: Payer ID as assigned by PayPal. Do not mix up with any
+    Ikoba Payer IDs.
+
+    `phone`: Phone number representing the payer. 20 chars max.
+
+    `shipping_address`: a shipping address object of payer. Assigned
+    by PayPal.
+
+    `tax_id_type`: Payer's tax ID type. Allowed values: ``'BR_CPF'``,
+    ``'BR_CNPJ'`` (I have not the slightest clue what that means).
+    Supported (PayPal-wise) with ``paypal`` payment method only (not
+    with ``credit_card``).
+
+    `tax_id`: Payer's tax ID. Here the same as for `tax_id_type`
+    applies (except that also other values are accepted).
+
+    By default all values are set to the empty string and shipping
+    address to `None`.
+
+    See also: :class:`Payer`
+    """
+    def __init__(self, email='', first_name='', last_name='',
+                 payer_id='', phone='', shipping_address=None,
+                 tax_id_type='', tax_id=''):
+        if tax_id_type and tax_id_type not in TAX_ID_TYPES:
+            raise ValueError(
+                "Invalid tax id type: use one of %s" %
+                (TAX_ID_TYPES, )
+                )
+        self.email = email
+        self.first_name = first_name
+        self.last_name = last_name
+        self.payer_id = payer_id
+        self.phone = phone
+        self.shipping_address = shipping_address
+        self.tax_id_type = tax_id_type
+        self.tax_id = tax_id
+
+
+class FundingInstrument(object):
+    """Representation of a payer's funding instrument as required by PayPal.
+
+    Represents always a credit card. Either by a complete set of
+    credit card infos or by a credit card token, which contains only a
+    limited set of credit card data and represents a full set stored
+    in PayPal vault.
+    """
+
+    def __init__(self, credit_card=None, credit_card_token=None):
+        if credit_card is None and credit_card_token is None:
+            raise ValueError(
+                "Must provide credit card data or a token")
+        if credit_card is not None and credit_card_token is not None:
+            raise ValueError(
+                "Must provide credit card data *or* a token, not both.")
+        self.credit_card = credit_card
+        self.credit_card_token = credit_card_token
+
+    def to_dict(self):
+        return to_dict(self)
+
+
+class ICreditCard(Interface):
+    """A credit card (full data set) as accepted by PayPal.
+    """
+
+    paypal_id = schema.TextLine(
+        title=u'PayPal ID',
+        description=u'ID of the credit card provided by PayPal.',
+        required=False,
+        )
+
+    payer_id = schema.TextLine(
+        title=u'Payer ID',
+        description=(u'A unique identifier for the credit card. This '
+                     u'identifier is provided by Ikoba and must be set '
+                     u'with all future transactions, once put into '
+                     u'PayPal vault.'),
+        required=True,
+        )
+
+    number = schema.TextLine(
+        title=u'Credit Card Number',
+        description=u'Numeric characters only w/o spaces or punctuation',
+        required=True,
+        )
+
+    credit_card_type = schema.Choice(
+        title=u'Credit Card Type',
+        description=u'Credit card types supported by PayPal.',
+        required=True,
+        source=CREDIT_CARD_TYPES_VOCAB,
+        )
+
+    expire_month = schema.Int(
+        title=u'Expiration Month',
+        description=u"Month, the credit card expires.",
+        required=True,
+        default=1,
+        min=1,
+        max=12,
+        )
+
+    expire_year = schema.Int(
+        title=u'Expiration Year',
+        description=u'4-digit expiration year.',
+        required=True,
+        default=2020,
+        min=1900,
+        max=9999,
+        )
+
+    cvv2 = schema.TextLine(
+        title=u'CVV2',
+        description=u'3-4 digit card validation code',
+        required=False,
+        )
+
+    first_name = schema.TextLine(
+        title=u'First Name',
+        description=u"Cardholder's first name.",
+        required=False,
+        )
+
+    last_name = schema.TextLine(
+        title=u'Last Name',
+        description=u"Cardholder's last name.",
+        required=False,
+        )
+
+    billing_address = Attribute(
+        "Billing address associated with card.")
+
+    state = schema.Choice(
+        title=u"Status",
+        description=(u"Status of credit card funding instrument. "
+                     u"Value assigned by PayPal."
+                     ),
+        required=False,
+        source=CREDIT_CARD_STATUS_VOCAB,
+        default=None,
+        )
+
+    valid_unti = schema.TextLine(
+        title=u'Valid until',
+        description=(u'Funding instrument expiratiopn date, '
+                     u'assigned by PayPal'),
+        required=False,
+        )
+
+
+@attrs_to_fields
+class CreditCard(object):
+    """A credit card (full info set) as used by PayPal.
+
+    Normally used with a `FundingInstrument` instance.
+
+    According to PayPal docs:
+
+    `paypal_id`: provided by PayPal when storing credit card
+          data. Required if using a stored credit card.
+
+    `payer_id`: Unique identifier. If none is given, we assign a
+          uuid. The uuid reads 'PAYER_<32 hex digits>'.
+
+    `number`: Credit card number. Numeric characters only with no
+          spaces or punctuation. The string must conform with modulo
+          and length required by each credit card type. Redacted in
+          responses. Required.
+
+    `credit_card_type`: One of ``'visa'``, ``'mastercard'``,
+          ``'discover'``, ``'amex'``. Required.
+
+    `expire_month`: Expiration month. A number from 1 through
+          12. Required.
+
+    `expire_year`: 4-digit expiration year. Required.
+
+    `cvv2`: 3-4 digit card validation code.
+
+    `first_name`: card holders first name.
+
+    `last_name`: card holders last name.
+
+    `billing_address`: Billing address associated with card. A
+      `Billing` instance.
+
+    `state`: state of the credit card funding instrument. Valid values
+      are ``'expired'`` and ``'ok'``. *Value assigned by PayPal.*
+
+    `paypal_valid_until`: Funding instrument expiration date.
+       *Value assigned by PayPal.* Not to confuse with the credit cards
+       expiration date, which is set via `expire_month` and
+       `expire_year`.
+
+    """
+    grok.implements(ICreditCard)
+
+    def __init__(self, paypal_id=None, payer_id=None, number=None,
+                 credit_card_type=None, expire_month=1, expire_year=2000,
+                 cvv2=None, first_name=None, last_name=None,
+                 billing_address=None, state=None, paypal_valid_until=None):
+        if not re.match('^[0-9]+$', number):
+            raise ValueError("Credit card number may "
+                             "not contain non-numbers.")
+        if payer_id is None:
+            payer_id = u'PAYER_' + unicode(uuid.uuid4().hex)
+        self.paypal_id = paypal_id
+        self.payer_id = payer_id
+        self.number = number
+        self.credit_card_type = credit_card_type
+        self.expire_month = expire_month
+        self.expire_year = expire_year
+        self.cvv2 = cvv2
+        self.first_name = first_name
+        self.last_name = last_name
+        self.billing_address = billing_address
+        self.state = state
+        self.paypal_valid_until = paypal_valid_until
+
+    def to_dict(self):
+        return to_dict(self, name_map={'credit_card_type': 'type'})
+
+
+class ICreditCardToken(Interface):
+    """A credit card token corresponding to a credit card stored with PayPal.
+    """
+    credit_card_id = schema.TextLine(
+        title=u"Credit Card ID",
+        description=u"ID if credit card previously stored with PayPal",
+        required=True,
+        )
+
+    payer_id = schema.TextLine(
+        title=u'Payer ID',
+        description=(u'A unique identifier for the credit card. This '
+                     u'identifier is provided by Ikoba and must be set '
+                     u'with all future transactions, once put into '
+                     u'PayPal vault.'),
+        required=True,
+        )
+
+    last4 = schema.TextLine(
+        title=u"Credit Card's last 4 numbers",
+        description=(
+            u"Last four digits of the stored credit card number. "
+            u"Value assigned by PayPal."),
+        required=False,
+        min_length=4,
+        max_length=4
+        )
+
+    credit_card_type = schema.Choice(
+        title=u'Credit Card Type',
+        description=(
+            u'Credit card type supported by PayPal. Value assigned '
+            u'by PayPal.'),
+        required=False,
+        source=CREDIT_CARD_TYPES_VOCAB,
+        )
+
+    expire_month = schema.Int(
+        title=u'Expiration Month',
+        description=u"Month, the credit card expires. Assigned by PayPal.",
+        required=True,
+        default=1,
+        min=1,
+        max=12,
+        )
+
+    expire_year = schema.Int(
+        title=u'Expiration Year',
+        description=u'4-digit expiration year. Assigned by PayPal.',
+        required=True,
+        default=2020,
+        min=1900,
+        max=9999,
+        )
+
+
+class CreditCardToken(object):
+    grok.implements(ICreditCardToken)
+
+    def __init__(self, credit_card_id, payer_id=None, last4=None,
+                 credit_card_type=None, expire_month=None, expire_year=None):
+        self.credit_card_id = credit_card_id
+        self.payer_id = payer_id
+        self.last4 = last4
+        self.credit_card_type = credit_card_type
+        self.expire_month = expire_month
+        self.expire_year = expire_year
+
+    def to_dict(self):
+        return to_dict(self, name_map={'credit_card_type': 'type'})
+
+
+class IShippingAddress(Interface):
+    """A shipping address as accepted by PayPal.
+    """
+    recipient_name = schema.TextLine(
+        title=u'Recipient Name',
+        required=True,
+        description=u'Name of the recipient at this address.',
+        max_length=50,
+        )
+
+    type = schema.Choice(
+        title=u'Address Type',
+        description=u'Address Type.',
+        required=False,
+        source=ADDRESS_TYPES_VOCAB,
+        )
+
+    line1 = schema.TextLine(
+        title=u'Address Line 1',
+        required=True,
+        description=u'Line 1 of the address (e.g., Number, street, etc.)',
+        max_length=100,
+        )
+
+    line2 = schema.TextLine(
+        title=u'Address Line 2',
+        required=False,
+        description=u'Line 2 of the address (e.g., Suite, apt #, etc.)',
+        max_length=100,
+        )
+
+    city = schema.TextLine(
+        title=u'City',
+        required=True,
+        description=u'City name',
+        max_length=50,
+        )
+
+    country_code = schema.Choice(
+        title=u'Country',
+        required=True,
+        description=u'2-letter country code',
+        source=COUNTRIES_VOCAB,
+        )
+
+    postal_code = schema.TextLine(
+        title=u'Postal code',
+        required=False,
+        description=(u'Zip code or equivalent is usually required '
+                     u'for countries that have them.'),
+        max_length=20,
+        )
+
+    state = schema.TextLine(
+        title=u'State',
+        required=False,
+        description=(u'2-letter code for US stated, and the '
+                     u'equivalent for other countries.'),
+        max_length=100,
+        )
+
+    phone = schema.TextLine(
+        title=u'Phone',
+        required=False,
+        description=u'Phone number in E.123 format.',
+        max_length=50,
+        )
+
+
+@attrs_to_fields
+class ShippingAddress(object):
+    """A shipping address as used in PayPal transactions.
+    """
+    grok.implements(IShippingAddress)
+
+    def __init__(self, recipient_name, type='residential', line1='',
+                 line2=None, city='', country_code=None,
+                 postal_code=None, state=None, phone=None):
+        self.recipient_name = recipient_name
+        self.type = type
+        self.line1 = line1
+        self.line2 = line2
+        self.city = city
+        self.country_code = country_code
+        self.postal_code = postal_code
+        self.state = state
+        self.phone = phone
+
+    def to_dict(self):
+        return to_dict(self)
+
+
+class IAddress(Interface):
+    """An address as accepted by PayPal.
+    """
+    line1 = schema.TextLine(
+        title=u'Address Line 1',
+        required=True,
+        description=u'Line 1 of the address (e.g., Number, street, etc.)',
+        max_length=100,
+        )
+
+    line2 = schema.TextLine(
+        title=u'Address Line 2',
+        required=False,
+        description=u'Line 2 of the address (e.g., Suite, apt #, etc.)',
+        max_length=100,
+        )
+
+    city = schema.TextLine(
+        title=u'City',
+        required=True,
+        description=u'City name',
+        max_length=50,
+        )
+
+    country_code = schema.Choice(
+        title=u'Country',
+        required=True,
+        description=u'2-letter country code',
+        source=COUNTRIES_VOCAB,
+        )
+
+    postal_code = schema.TextLine(
+        title=u'Postal code',
+        required=False,
+        description=(u'Zip code or equivalent is usually required '
+                     u'for countries that have them.'),
+        max_length=20,
+        )
+
+    state = schema.TextLine(
+        title=u'State',
+        required=False,
+        description=(u'2-letter code for US stated, and the '
+                     u'equivalent for other countries.'),
+        max_length=100,
+        )
+
+    phone = schema.TextLine(
+        title=u'Phone',
+        required=False,
+        description=u'Phone number in E.123 format.',
+        max_length=50,
+        )
+
+
+@attrs_to_fields
+class Address(object):
+    """A postal address as used in PayPal transactions.
+    """
+    grok.implements(IAddress)
+
+    def __init__(self, line1='', line2=None, city='', country_code=None,
+                 postal_code=None, state=None, phone=None):
+        self.line1 = line1
+        self.line2 = line2
+        self.city = city
+        self.country_code = country_code
+        self.postal_code = postal_code
+        self.state = state
+        self.phone = phone
+
+    def to_dict(self):
+        """Turn Adress into a dict that can be fed to PayPal classes.
+        """
+        return to_dict(self)
+
+
+class AmountDetails(object):
+    """Amount details can be given with Amount objects.
+
+    All parameters are passed in as decimals (`decimal.Decimal`).
+
+    All numbers stored here, might have 10 characters max with
+    support for two decimal places.
+
+    No parameter is strictly required, except `subtotal`, which must
+    be set if any of the other values is set.
+
+    `shipping`: Amount charged for shipping.
+
+    `subtotal`: Amount for subtotal of the items. Automatically
+      computed. If no other item was set, subtotal is `None`.
+
+    `tax`: Amount charged for tax.
+
+    `fee`: Fee charged by PayPal. In case of a refund, this is the fee
+      amount refunded to the original recipient of the payment. Value
+      assigned by PayPal.
+
+    `handling_fee`: Amount being charged for the handling
+      fee. Currently supported with paypal payment_method only, but
+      available for credit_card payment_method at a later date.
+
+    `insurance`: Amount being charged for the insurance fee. Currently
+      supported with paypal payment_method only, but available for
+      credit_card payment_method at a later date.
+
+    `shipping_discount`: Amount being discounted for the shipping
+      fee. Currently supported with paypal payment_method only, but
+      available for credit_card payment_method at a later date.
+    """
+    def __init__(self, shipping=None, tax=None, fee=None,
+                 handling_fee=None, insurance=None, shipping_discount=None):
+        self.shipping = shipping
+        self.tax = tax
+        self.fee = fee
+        self.handling_fee = handling_fee
+        self.insurance = insurance
+        self.shipping_discount = shipping_discount
+
+    @property
+    def subtotal(self):
+        names = (
+            'shipping', 'tax', 'fee', 'handling_fee', 'insurance',
+            'shipping_discount'
+            )
+        result = None
+        for name in names:
+            val = getattr(self, name)
+            if name == 'shipping_discount' and val is not None:
+                val = -val
+            if val is not None:
+                if result is None:
+                    result = val
+                else:
+                    result += val
+        return result
+
+    def to_dict(self):
+        return to_dict(self)
+
+
+class IAmount(Interface):
+    """An amount as used by PayPal in payments.
+    """
+    currency = schema.Choice(
+        title=u'Currency',
+        description=u'PayPal does not support all currencies. Required.',
+        required=True,
+        source=CURRENCIES_VOCAB,
+        default=u'USD',
+        )
+
+    total = schema.Decimal(
+        title=u'Total',
+        description=(
+            u'Total amount charged from the payer to the payee. '
+            u'In case of a refund, this is the refunded amount to '
+            u'the original payer from the payee.'),
+        required=True,
+        default=decimal.Decimal("0.00"),
+        max=decimal.Decimal("9999999.99")
+        )
+
+    details = Attribute(
+        """Additional details related to a payment amount.
+        """)
+
+
+@attrs_to_fields
+class Amount(object):
+    grok.implements(IAmount)
+
+    def __init__(self, currency="USD", total=decimal.Decimal("0.00"),
+                 details=None):
+        self.currency = currency
+        self.total = total
+        self.details = details
+
+    def to_dict(self):
+        return to_dict(self)
+
+
+class IItem(Interface):
+    """PayPal Item.
+
+    Items in a PayPal context are goods sold to customers.
+    """
+    quantity = schema.Int(
+        title=u"Quantity",
+        description=u"Number of this particular items.",
+        required=True,
+        default=1,
+        max=9999999999,
+        )
+
+    name = schema.TextLine(
+        title=u"Name",
+        description=u"Item name",
+        required=True,
+        max_length=127,
+        )
+
+    price = schema.Decimal(
+        title=u"Price",
+        description=u"Price",
+        required=True,
+        max=decimal.Decimal("9999999.99"),
+        )
+
+    currency = schema.Choice(
+        title=u"Currency",
+        description=u"Currency",
+        source=CURRENCIES_VOCAB,
+        default=u'USD',
+        required=True,
+        )
+
+    sku = schema.Choice(
+        title=u"SKU",
+        description=u"Stock keeping unit corresponding to item.",
+        source=STOCK_KEEPING_UNITS_VOCAB,
+        required=False,
+        )
+
+    description = schema.TextLine(
+        title=u"Description",
+        description=(
+            u"Description of Item. Currently supported with paypal "
+            u"payments only."
+            ),
+        max_length=127,
+        required=False,
+        )
+
+    tax = schema.Decimal(
+        title=u"Tax",
+        description=(
+            u"Tax of the item. Currently supported with paypal "
+            u"payments only."
+            ),
+        required=False,
+        )
+
+
+@attrs_to_fields
+class Item(object):
+    """See IItem for docs.
+    """
+    grok.implements(IItem)
+
+    def __init__(self, name, quantity=1, price=decimal.Decimal("0.00"),
+                 currency="USD", sku=None, description=None, tax=None):
+        self.name = name
+        self.quantity = quantity
+        self.price = price
+        self.currency = currency
+        self.sku = sku
+        self.description = description
+        self.tax = tax
+
+    def to_dict(self):
+        return to_dict(self)
+
+
+class IItemList(Interface):
+    """List of `Item` objects and a related `ShippingAddress`.
+
+    `items`: can be a simple list of `Item` objects.
+
+    `shipping_address`: a `ShippingAddress` object. Only needed if
+      different from Payer address.
+    """
+    items = schema.List(
+        title=u"Items",
+        description=u"PayPal Items are sold goods",
+        value_type=schema.Object(
+            title=u"Item",
+            description=u"Item in a list",
+            schema=IItem,
+            )
+        )
+
+    shipping_address = schema.Object(
+        title=u"Shipping Address",
+        description=u"Shipping address of receiver if different from payee.",
+        schema=IShippingAddress,
+        required=False,
+        )
+
+
+@attrs_to_fields
+class ItemList(object):
+    """List of `Item` objects and a related `ShippingAddress`.
+
+    `items`: can be a simple list of `Item` objects.
+
+    `shipping_address`: a `ShippingAddress` object. Only needed if
+      different from Payer address.
+    """
+    grok.implements(IItemList)
+
+    def __init__(self, items=[], shipping_address=None):
+        self.items = items
+        self.shipping_address = shipping_address
+
+    def to_dict(self):
+        return to_dict(self)
+
+
+class IPaymentOptions(Interface):
+    """Payment options requested for a certain purchase unit.
+
+    `allowed_payment_method`: Optional payment method type. If
+      specified, the transaction will go through for only instant
+      payment. Allowed values: ``INSTANT_FUNDING_SOURCE``. Only for
+      use with the ``paypal`` payment_method, not relevant for the
+      ``credit_card`` payment_method.
+    """
+    allowed_payment_method = schema.Choice(
+        title=u"Allowed payment method",
+        description=(
+            u"Optional payment type. If specified, the transaction "
+            u"will go through for only instant payment. Only for use "
+            u"with paypal payment method, not relevant for credit cards."
+            ),
+        required=False,
+        source=PAYMENT_OPTION_METHODS_VOCAB,
+        )
+
+
+@attrs_to_fields
+class PaymentOptions(object):
+    """Payment options requested for a certain purchase unit.
+    """
+    grok.implements(IPaymentOptions)
+
+    def __init__(self, allowed_payment_method=None):
+        if allowed_payment_method not in (
+            None, 'INSTANT_FUNDING_SOURCE'):
+            raise ValueError(
+                "allowed_payment_method of PaymentOptions must be None or "
+                "'INSTANT_FUNDING_SOURCE'"
+                )
+        self.allowed_payment_method = allowed_payment_method
+
+    def to_dict(self):
+        return to_dict(self)
+
+
+class ITransaction(Interface):
+    """PayPal transactions provide payment transaction details.
+    """
+    amount = schema.Object(
+        title=u"Amount",
+        description=u"Amount being collected.",
+        schema=IAmount,
+        required=True,
+        )
+
+    description = schema.TextLine(
+        title=u"Description",
+        description=u"Description of transaction",
+        required=False,
+        max_length=127,
+        )
+
+    item_list = schema.Object(
+        title=u"Item List",
+        description=u"List of items",
+        required=False,
+        schema=IItemList,
+        )
+
+    # XXX: This should be defined more precisely: What kind of objects, etc.
+    #      PayPal docs say: "array of sale, authorization, capture, or refund,
+    #      objects"
+    related_resources = Attribute("Arbitrary objects")
+
+    invoice_number = schema.TextLine(
+        title=u"Invoice Number",
+        description=(
+            u"Invoice number used to track the payment. "
+            u"Currently supported with paypal payment_method only."
+            ),
+        required=False,
+        max_length=256,
+        )
+
+    custom = schema.TextLine(
+        title=u"Custom text",
+        description=(
+            u"Free-form field for the use of clients. Currently "
+            u"supported with paypal payment_method only."),
+        required=False,
+        max_length=256,
+        )
+
+    soft_descriptor = schema.TextLine(
+        title=u"Soft descriptor",
+        description=(
+            u"Soft descriptor used when charging this funding "
+            u"source. Currently supported with paypal payment_method only"
+            ),
+        required=False,
+        max_length=22,
+        )
+
+    payment_options = schema.Object(
+        title=u"Payment Options",
+        description=u"Payment options requested for this purchase unit.",
+        required=False,
+        schema=IPaymentOptions,
+        )
+
+
+@attrs_to_fields
+class Transaction(object):
+    # See ITransaction for description
+
+    grok.implements(ITransaction)
+
+    def __init__(self, amount, description=None, item_list=None,
+                 related_resources=[], invoice_number=None, custom=None,
+                 soft_descriptor=None, payment_options=None):
+        self.amount = amount
+        self.description = description
+        self.item_list = item_list
+        self.related_resources = related_resources
+        self.invoice_number = invoice_number
+        self.custom = custom
+        self.soft_descriptor = soft_descriptor
+        self.payment_options = payment_options
+
+    def to_dict(self):
+        """Give a `dict` representation of this `Transaction`.
+        """
+        return to_dict(self)
+
+
+def get_payment(intent='sale', payment_method='credit_card'):
     """Construct a payment.
 
     You have to `create()` the payment yourself.
 
+    Returns a paypalrestsdk Payment object, not an Ikoba payment.
+
+    As `intent` currently only the string ``'sale'`` is supported.
+
     XXX: Just some sampledata yet.
     """
+    if intent != "sale":
+        raise ValueError(
+            "Currently, only 'sale' is allowed as type of paypal"
+            "payment.")
     payment = paypalrestsdk.Payment(
         {
-            "intent": "sale",
+            "intent": intent,
             "payer": {
                 "payment_method": "credit_card",
@@ -123,15 +1094,49 @@
                     ]},
             "transactions": [{
-                    "amount": {
-                        "total": "7.47",
-                        "currency": "USD",
-                        "details": {
-                            "subtotal": "7.41",
-                            "tax": "0.03",
-                            "shipping": "0.03"}},
-                    "description": ("This is the payment "
-                                    "transaction description.")
-                    }]
+                "amount": {
+                    "total": "7.47",
+                    "currency": "USD",
+                    "details": {
+                        "subtotal": "7.41",
+                        "tax": "0.03",
+                        "shipping": "0.03"}},
+                "description": ("This is the payment "
+                                "transaction description.")
+                }]
             }
         )
     return payment
+
+
+class IPayPalPayment(IPayment):
+    """A paypal payment.
+    """
+
+
+class PayPalPayment(grok.Model):
+    """A paypal payment.
+    """
+    pass
+
+
+class PayPalCreditCardService(grok.GlobalUtility):
+    grok.implements(IPaymentGatewayService)
+    grok.name('paypal_creditcard')
+
+    title = _(u'Credit Card (PayPal)')
+
+    def create_payment(self, payer, payment_item,  payee=None):
+        if not IPayer.providedBy(payer):
+            payer = IPayer(payer)
+        if not IPaymentItem.providedBy(payment_item):
+            payment_item = IPaymentItem(payment_item)
+        return None
+        payment = get_payment()
+        return payment
+
+
+class PayPalRegularPaymentService(grok.GlobalUtility):
+    grok.implements(IPaymentGatewayService)
+    grok.name('paypal_regular')
+
+    title = _(u'PayPal Payment')
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/paypal_countries.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/paypal_countries.py	(revision 12311)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/paypal_countries.py	(revision 12311)
@@ -0,0 +1,206 @@
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.interfaces import SimpleIkobaVocabulary
+
+
+#: A list of country codes as accepted by PayPal
+COUNTRIES = {
+    'AD': _('ANDORRA'),
+    'AE': _('UNITED ARAB EMIRATES'),
+    'AG': _('ANTIGUA AND BARBUDA'),
+    'AI': _('ANGUILLA'),
+    'AL': _('ALBANIA'),
+    'AM': _('ARMENIA'),
+    'AN': _('NETHERLANDS ANTILLES'),
+    'AO': _('ANGOLA'),
+    'AR': _('ARGENTINA'),
+    'AT': _('AUSTRIA'),
+    'AU': _('AUSTRALIA'),
+    'AW': _('ARUBA'),
+    'AZ': _('AZERBAIJAN'),
+    'BA': _('BOSNIA-HERZEGOVINA'),
+    'BB': _('BARBADOS'),
+    'BE': _('BELGIUM'),
+    'BF': _('BURKINA FASO'),
+    'BG': _('BULGARIA'),
+    'BH': _('BAHRAIN'),
+    'BI': _('BURUNDI'),
+    'BJ': _('BENIN'),
+    'BM': _('BERMUDA'),
+    'BN': _('BRUNEI DARUSSALAM'),
+    'BO': _('BOLIVIA'),
+    'BR': _('BRAZIL'),
+    'BS': _('BAHAMAS'),
+    'BT': _('BHUTAN'),
+    'BW': _('BOTSWANA'),
+    'BZ': _('BELIZE'),
+    'CA': _('CANADA'),
+    'CD': _('DEMOCRATIC REPUBLIC OF CONGO'),
+    'CG': _('CONGO'),
+    'CH': _('SWITZERLAND'),
+    'CK': _('COOK ISLANDS'),
+    'CL': _('CHILE'),
+    'CN': _('CHINA (domestic Chinese bank transactions only)'),
+    'C2': _('CHINA (For CUP, bank card and cross-border transactions)'),
+    'CO': _('COLOMBIA'),
+    'CR': _('COSTA RICA'),
+    'CV': _('CAPE VERDE'),
+    'CY': _('CYPRUS'),
+    'CZ': _('CZECH REPUBLIC'),
+    'DE': _('GERMANY'),
+    'DJ': _('DJIBOUTI'),
+    'DK': _('DENMARK'),
+    'DM': _('DOMINICA'),
+    'DO': _('DOMINICAN REPUBLIC'),
+    'DZ': _('ALGERIA'),
+    'EC': _('ECUADOR'),
+    'EE': _('ESTONIA'),
+    'EG': _('EGYPT'),
+    'ER': _('ERITERIA'),
+    'ES': _('SPAIN'),
+    'ET': _('ETHIOPIA'),
+    'FI': _('FINLAND'),
+    'FJ': _('FIJI'),
+    'FK': _('FALKLAND ISLANDS (MALVINAS)'),
+    'FM': _('MICRONESIA, FEDERATED STATES OF'),
+    'FR': _('FRANCE'),
+    'GA': _('GABON'),
+    'GB': _('UNITED KINGDOM'),
+    'GD': _('GRENADA'),
+    'GE': _('GEORGIA'),
+    'GF': _('FRENCH GUIANA'),
+    'GI': _('GIBRALTAR'),
+    'GL': _('GREENLAND'),
+    'GM': _('GAMBIA'),
+    'GN': _('GUINEA'),
+    'GP': _('GUADELOUPE'),
+    'GR': _('GREECE'),
+    'GT': _('GUATEMALA'),
+    'GU': _('GUAM'),
+    'GW': _('GUINEA BISSAU'),
+    'GY': _('GUYANA'),
+    'HK': _('HONG KONG'),
+    'HN': _('HONDURAS'),
+    'HR': _('CROATIA'),
+    'HU': _('HUNGARY'),
+    'ID': _('INDONESIA'),
+    'IE': _('IRELAND'),
+    'IL': _('ISRAEL'),
+    'IN': _('INDIA'),
+    'IS': _('ICELAND'),
+    'IT': _('ITALY'),
+    'JM': _('JAMAICA'),
+    'JO': _('JORDAN'),
+    'JP': _('JAPAN'),
+    'KE': _('KENYA'),
+    'KG': _('KYRGYZSTAN'),
+    'KH': _('CAMBODIA'),
+    'KI': _('KIRIBATI'),
+    'KM': _('COMOROS'),
+    'KN': _('SAINT KITTS AND NEVIS'),
+    'KR': _('SOUTH KOREA'),
+    'KW': _('KUWAIT'),
+    'KY': _('CAYMAN ISLANDS'),
+    'KZ': _('KAZAKHSTAN'),
+    'LA': _('LAOS'),
+    'LC': _('SAINT LUCIA'),
+    'LI': _('LIECHTENSTEIN'),
+    'LK': _('SRI LANKA'),
+    'LS': _('LESOTHO'),
+    'LT': _('LITHUANIA'),
+    'LU': _('LUXEMBOURG'),
+    'LV': _('LATVIA'),
+    'MA': _('MOROCCO'),
+    'MG': _('MADAGASCAR'),
+    'MH': _('MARSHALL ISLANDS'),
+    'ML': _('MALI'),
+    'MN': _('MONGOLIA'),
+    'MQ': _('MARTINIQUE'),
+    'MR': _('MAURITANIA'),
+    'MS': _('MONTSERRAT'),
+    'MT': _('MALTA'),
+    'MU': _('MAURITIUS'),
+    'MV': _('MALDIVES'),
+    'MW': _('MALAWI'),
+    'MX': _('MEXICO'),
+    'MY': _('MALAYSIA'),
+    'MZ': _('MOZAMBIQUE'),
+    'NA': _('NAMIBIA'),
+    'NC': _('NEW CALEDONIA'),
+    'NE': _('NIGER'),
+    'NF': _('NORFOLK ISLAND'),
+    'NI': _('NICARAGUA'),
+    'NL': _('NETHERLANDS'),
+    'NO': _('NORWAY'),
+    'NP': _('NEPAL'),
+    'NR': _('NAURU'),
+    'NU': _('NIUE'),
+    'NZ': _('NEW ZEALAND'),
+    'OM': _('OMAN'),
+    'PA': _('PANAMA'),
+    'PE': _('PERU'),
+    'PF': _('FRENCH POLYNESIA'),
+    'PG': _('PAPUA NEW GUINEA'),
+    'PH': _('PHILIPPINES'),
+    'PL': _('POLAND'),
+    'PM': _('SAINT PIERRE AND MIQUELON'),
+    'PN': _('PITCAIRN'),
+    'PT': _('PORTUGAL'),
+    'PW': _('PALAU'),
+    'QA': _('QATAR'),
+    'RE': _('REUNION'),
+    'RO': _('ROMANIA'),
+    'RS': _('SERBIA'),
+    'RU': _('RUSSIAN FEDERATION'),
+    'RW': _('RWANDA'),
+    'SA': _('SAUDI ARABIA'),
+    'SB': _('SOLOMON ISLANDS'),
+    'SC': _('SEYCHELLES'),
+    'SE': _('SWEDEN'),
+    'SG': _('SINGAPORE'),
+    'SH': _('SAINT HELENA'),
+    'SI': _('SLOVENIA'),
+    'SJ': _('SVALBARD AND JAN MAYEN'),
+    'SK': _('SLOVAKIA'),
+    'SL': _('SIERRA LEONE'),
+    'SM': _('SAN MARINO'),
+    'SN': _('SENEGAL'),
+    'SO': _('SOMALIA'),
+    'SR': _('SURINAME'),
+    'ST': _('SAO TOME AND PRINCIPE'),
+    'SV': _('EL SALVADOR'),
+    'SZ': _('SWAZILAND'),
+    'TC': _('TURKS AND CAICOS ISLANDS'),
+    'TD': _('CHAD'),
+    'TG': _('TOGO'),
+    'TH': _('THAILAND'),
+    'TJ': _('TAJIKISTAN'),
+    'TM': _('TURKMENISTAN'),
+    'TN': _('TUNISIA'),
+    'TO': _('TONGA'),
+    'TR': _('TURKEY'),
+    'TT': _('TRINIDAD AND TOBAGO'),
+    'TV': _('TUVALU'),
+    'TW': _('TAIWAN, PROVINCE OF CHINA'),
+    'TZ': _('TANZANIA, UNITED REPUBLIC OF'),
+    'UA': _('UKRAINE'),
+    'UG': _('UGANDA'),
+    'US': _('UNITED STATES'),
+    'UY': _('URUGUAY'),
+    'VA': _('HOLY SEE (VATICAN CITY STATE)'),
+    'VC': _('SAINT VINCENT AND THE GRENADINES'),
+    'VE': _('VENEZUELA'),
+    'VG': _('VIRGIN ISLANDS, BRITISH'),
+    'VN': _('VIETNAM'),
+    'VU': _('VANUATU'),
+    'WF': _('WALLIS AND FUTUNA'),
+    'WS': _('SAMOA'),
+    'YE': _('YEMEN'),
+    'YT': _('MAYOTTE'),
+    'ZA': _('SOUTH AFRICA'),
+    'ZM': _('ZAMBIA'),
+    }
+
+
+#: A vocabulary of paypal supported countries
+COUNTRIES_VOCAB = SimpleIkobaVocabulary(
+    *[(val, key) for key, val in COUNTRIES.items()])
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/paypal_currencies.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/paypal_currencies.py	(revision 12311)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/paypal_currencies.py	(revision 12311)
@@ -0,0 +1,63 @@
+# $Id$
+#
+# Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# 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
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.interfaces import SimpleIkobaVocabulary
+
+#: The currencies accepted by PayPal.
+#: The entries represent:
+#:   ( title (i18nized), domestic_only, decimals_allowed)
+#:
+#: If `domestic_only` is True, then this currency is supported as a
+#: payment currency and a currency balance for in-country PayPal
+#: accounts only.
+#:
+#: If `decimals_allowed` is *not* True, then you cannot pass amounts
+#: with decimals.
+#:
+#: Status of 2014-12-11
+CURRENCIES = {
+    'AUD': (_(u'Australian dollar'), False, True),
+    'BRL': (_(u'Brazilian real'), True, True),
+    'CAD': (_(u'Canadian dollar'), False, True),
+    'CHF': (_(u'Swiss franc'), False, True),
+    'CZK': (_(u'Czech koruna'), False, True),
+    'DKK': (_(u'Danish krone'), False, True),
+    'EUR': (_(u'Euro'), False, True),
+    'GBP': (_(u'Pound sterling'), False, True),
+    'HKD': (_(u'Hong Kong dollar'), False, True),
+    'HUF': (_(u'Hungarian forint'), False, True),
+    'ILS': (_(u'Israeli new shekel'), False, True),
+    'JPY': (_(u'Japanese yen'), False, False),
+    'MXN': (_(u'Mexican peso'), False, True),
+    'MYR': (_(u'Malaysian ringgit'), True, True),
+    'NOK': (_(u'Norwegian krone'), False, True),
+    'NZD': (_(u'New Zealand dollar'), False, True),
+    'PHP': (_(u'Philippine peso'), False, True),
+    'PLN': (_(u'Polish z\u0142oty'), False, True),
+    'SEK': (_(u'Swedish krona'), False, True),
+    'SGD': (_(u'Singapore dollar'), False, True),
+    'THB': (_(u'Thai baht'), False, True),
+    'TRY': (_(u'Turkish lira'), True, True),
+    'TWD': (_(u'New Taiwan dollar'), False, False),
+    'USD': (_(u'United States dollar'), False, True)
+    }
+
+
+#: A vocabulary of paypal supported currencies
+CURRENCIES_VOCAB = SimpleIkobaVocabulary(
+    *[(val[0], key) for key, val in CURRENCIES.items()])
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_container.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_container.py	(revision 12311)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_container.py	(revision 12311)
@@ -0,0 +1,49 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## 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
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Test the payments/container module.
+"""
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.ikoba.payments.interfaces import IPaymentsContainer
+from waeup.ikoba.payments.container import PaymentsContainer
+from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase)
+
+
+class PaymentsContainerTestCase(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verifyClass(
+                IPaymentsContainer, PaymentsContainer)
+            )
+        self.assertTrue(
+            verifyObject(
+                IPaymentsContainer, PaymentsContainer())
+            )
+        return
+
+    def test_base(self):
+        # We cannot call the fundamental methods of a base in that case
+        container = PaymentsContainer()
+        self.assertRaises(
+            NotImplementedError, container.archive)
+        self.assertRaises(
+            NotImplementedError, container.clear)
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_currencies.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_currencies.py	(revision 12311)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_currencies.py	(revision 12311)
@@ -0,0 +1,49 @@
+import unittest
+from zope.i18nmessageid.message import Message as i18nMessage
+from zope.interface.verify import verifyObject
+from zope.schema.interfaces import IVocabularyTokenized, ITerm
+from waeup.ikoba.payments.currencies import (
+    get_decimal_units, ISO_4217_CURRENCIES, ISO_4217_CURRENCIES_VOCAB,
+    )
+
+
+class CurrenciesTests(unittest.TestCase):
+
+    def test_get_decimal_units(self):
+        # we can get the number of decimals for a currency code
+        assert get_decimal_units('USD') == 2
+        assert get_decimal_units('JPY') == 0
+
+    def test_iso_currencies_no_linebreak(self):
+        # sanity check: make sure we didn't compile linebreaks
+        for key, value in ISO_4217_CURRENCIES.items():
+            assert '\n' not in key
+            assert '\n' not in value[0]
+            assert '\n' not in value[1]
+
+    def test_iso_currencies_types(self):
+        # sanity check: make sure we have the correct types in all tuples
+        for key, value in ISO_4217_CURRENCIES.items():
+            assert isinstance(value[1], i18nMessage)
+            assert isinstance(value[2], int)
+            assert isinstance(value[3], str)
+
+    def test_currencies_vocab_tokenized(self):
+        # we can get ISO currencies as a source suitable for forms etc.
+        verifyObject(IVocabularyTokenized, ISO_4217_CURRENCIES_VOCAB)
+
+    def test_currencies_vocab_i18nized(self):
+        # vocab titles are i18nized
+        result = ISO_4217_CURRENCIES_VOCAB.getTerm('USD')
+        assert ITerm.providedBy(result)
+        self.assertEqual(result.title, u'US Dollar')
+        assert isinstance(result.title, i18nMessage)
+
+    def test_currencies_vocab_tokens_are_string(self):
+        # vocab tokens are simple strings
+        result = ISO_4217_CURRENCIES_VOCAB.getTerm('USD')
+        assert ITerm.providedBy(result)
+        assert result.token == result.value
+        assert result.value == 'USD'
+        assert isinstance(result.token, str)
+        assert isinstance(result.value, str)
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_interfaces.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_interfaces.py	(revision 12288)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_interfaces.py	(revision 12311)
@@ -17,11 +17,7 @@
 ##
 """
-Tests for payments.
+Tests for payments interfaces.
 """
-from zope.interface.verify import verifyClass, verifyObject
-from waeup.ikoba.payments.interfaces import (
-    IPaymentsContainer, IOnlinePayment)
-from waeup.ikoba.payments.container import PaymentsContainer
-from waeup.ikoba.payments.payment import OnlinePayment
+from waeup.ikoba.payments.interfaces import PaymentGatewayServicesSource
 from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase)
 
@@ -31,29 +27,16 @@
     layer = FunctionalLayer
 
-    def test_interfaces(self):
-        # Make sure the correct interfaces are implemented.
-        self.assertTrue(
-            verifyClass(
-                IPaymentsContainer, PaymentsContainer)
-            )
-        self.assertTrue(
-            verifyObject(
-                IPaymentsContainer, PaymentsContainer())
-            )
-        self.assertTrue(
-            verifyClass(
-                IOnlinePayment, OnlinePayment)
-            )
-        self.assertTrue(
-            verifyObject(
-                IOnlinePayment, OnlinePayment())
-            )
-        return
+    def test_payment_gateway_services_source(self):
+        # the payment gateway services source provides a list of registered
+        # payment gateways
+        source = PaymentGatewayServicesSource()
+        services = list(source)
+        assert len(services) > 0
 
-    def test_base(self):
-        # We cannot call the fundamental methods of a base in that case
-        container = PaymentsContainer()
-        self.assertRaises(
-            NotImplementedError, container.archive)
-        self.assertRaises(
-            NotImplementedError, container.clear)
+    def test_payment_gateway_services_source_title(self):
+        # we can get titles from gateway sources
+        source = PaymentGatewayServicesSource()
+        service1 = list(source)[0]
+        title = source.factory.getTitle(service1)
+        assert title != service1
+        assert isinstance(title, basestring)
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 12311)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_payment.py	(revision 12311)
@@ -0,0 +1,112 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## 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
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import datetime
+import re
+import unittest
+from zope.component import getUtilitiesFor, getSiteManager
+from zope.interface import implements
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.ikoba.payments.interfaces import (
+    IPayment, STATE_UNPAID, STATE_PAID, STATE_FAILED,
+    IPaymentGatewayService,
+    )
+from waeup.ikoba.payments.payment import Payment, get_payment_providers
+
+
+class HelperTests(unittest.TestCase):
+
+    def tearDown(self):
+        # unregister any IPaymentGatewayServices
+        sm = getSiteManager(None)
+        for name, util in getUtilitiesFor(
+            IPaymentGatewayService):
+            sm.unregisterUtility(util, name=name)
+
+    def test_get_payment_providers_no_providers(self):
+        # we can get a dict of all payment providers
+        result = get_payment_providers()
+        assert isinstance(result, dict)
+        assert result == {}
+
+    def test_get_payment_providers(self):
+        # we get any payment providers registered
+        sm = getSiteManager(None)
+
+        class FakeUtil(object):
+            implements(IPaymentGatewayService)
+
+        fake_util = FakeUtil()
+        sm.registerUtility(fake_util, name=u'some_name')
+        result = get_payment_providers()
+        assert isinstance(result, dict)
+        assert result.keys() == ['some_name', ]
+        assert result['some_name'] is fake_util
+
+
+class PaymentTests(unittest.TestCase):
+
+    def test_iface(self):
+        # Payments fullfill any interface contracts
+        obj = Payment()
+        verifyClass(IPayment, Payment)
+        verifyObject(IPayment, obj)
+
+    def test_payment_id_unique(self):
+        # we get unique payment ids
+        p1, p2 = Payment(), Payment()
+        id1, id2 = p1.payment_id, p2.payment_id
+        assert id1 != id2
+
+    def test_payment_id_format(self):
+        # payment ids have a special format: "PAY_<32 hex digits>"
+        id1 = Payment().payment_id
+        assert isinstance(id1, basestring)
+        assert re.match('PAY_[0-9a-f]{32}', id1)
+
+    def test_initial_state_is_unpaid(self):
+        # the initial state of payments is <unpaid>
+        p1 = Payment()
+        assert p1.state == STATE_UNPAID
+
+    def test_approve(self):
+        # we can approve payments
+        p1 = Payment()
+        p1.approve()
+        assert p1.state == STATE_PAID
+        assert p1.payment_date is not None
+        assert isinstance(p1.payment_date, datetime.datetime)
+
+    def test_approve_datetime_given(self):
+        # we can give a datetime
+        p1 = Payment()
+        some_datetime = datetime.datetime(2014, 1, 1, 0, 0, 0)
+        p1.approve(payment_date=some_datetime)
+        assert p1.payment_date == some_datetime
+
+    def test_approve_datetime_automatic(self):
+        # if we do not give a datetime, current one will be used
+        current = datetime.datetime.utcnow()
+        p1 = Payment()
+        p1.approve()
+        assert p1.payment_date >= current
+
+    def test_mark_failed(self):
+        # we can mark payments as failed
+        p1 = Payment()
+        p1.mark_failed()
+        assert p1.state == STATE_FAILED
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 12288)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_paypal.py	(revision 12311)
@@ -16,18 +16,37 @@
 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 ##
+import decimal
 import os
+import re
 import shutil
 import tempfile
 import unittest
 import paypalrestsdk
-from zope.component import getGlobalSiteManager, queryUtility
+from zope.component import (
+    getGlobalSiteManager, queryUtility, getUtilitiesFor,
+    )
+from zope.i18nmessageid.message import Message as i18nMessage
+from zope.interface import implements
+from zope.interface.verify import verifyObject, verifyClass
+from zope.schema.interfaces import IVocabularyTokenized, ITerm
 from waeup.ikoba.interfaces import IPayPalConfig
+from waeup.ikoba.payments.interfaces import (
+    IPaymentGatewayService, IPayer, IPayee, IPaymentItem, IPayment,
+    )
 from waeup.ikoba.payments.paypal import (
     get_paypal_config_file_path, parse_paypal_config, get_access_token,
-    configure_sdk, get_payment,
+    configure_sdk, get_payment, Payer, PayerInfo, ShippingAddress,
+    ADDRESS_TYPES_VOCAB, IShippingAddress, Address, IAddress, to_dict,
+    CreditCard, CREDIT_CARD_TYPES_VOCAB, CREDIT_CARD_STATUS_VOCAB,
+    ICreditCard, ICreditCardToken, CreditCardToken, FundingInstrument,
+    AmountDetails, IAmount, Amount, IItem, Item, STOCK_KEEPING_UNITS_VOCAB,
+    IItemList, ItemList, IPaymentOptions, PaymentOptions, ITransaction,
+    Transaction, PAYMENT_OPTION_METHODS_VOCAB, PayPalCreditCardService,
+    PayPalRegularPaymentService,
     )
 from waeup.ikoba.testing import (
     FunctionalLayer, FunctionalTestCase,
     )
+
 
 #
@@ -118,4 +137,954 @@
         assert paypalrestsdk.api.__api__.client_id == 'my-special-id'
 
+    def test_get_payment_invalid_intent(self):
+        # only 'sale' is currently allowed
+        self.assertRaises(
+            ValueError, get_payment, intent='invalid')
+        self.assertRaises(
+            ValueError, get_payment, intent='order')
+
+    def test_to_dict(self):
+        # we can turn objects into dicts
+        class C1(object):
+            a = 1
+            b = "somestring"
+            c = u"unicodestring"
+            d = None
+        obj = C1()
+        self.assertEqual(
+            to_dict(obj),
+            {'a': u"1",
+             'b': u"somestring",
+             'c': u"unicodestring",
+             }
+            )
+
+    def test_to_dict_map(self):
+        # we can map attribute names
+        class C1(object):
+            a = 1
+            b = "somestring"
+        obj = C1()
+        self.assertEqual(
+            to_dict(obj, name_map={'a': 'replaced_a'}),
+            {'replaced_a': u"1",
+             'b': u"somestring",
+             })
+
+    def test_to_dict_lists(self):
+        # to_dict can handle lists
+        class C1(object):
+            a = 1
+
+            def to_dict(self):
+                return to_dict(self)
+
+        obj1 = C1()
+        obj2 = C1()
+        obj2.a = 2
+        obj3 = C1()
+        obj3.a = 3
+        obj1.foo = [obj2, obj3]
+        self.assertEqual(
+            to_dict(obj1),
+            {
+                'a': u'1',
+                'foo': [
+                    {'a': u'2'},
+                    {'a': u'3'},
+                    ]
+                }
+            )
+
+    def test_to_dict_decimals(self):
+        # decimals are converted to numbers with 2 decimals
+        class C1(object):
+            a = decimal.Decimal("0.1")
+        self.assertEqual(
+            to_dict(C1()),
+            {
+                'a': u"0.10",
+                }
+            )
+
+
+class PayerTests(unittest.TestCase):
+
+    def test_create(self):
+        # we can create payer objects
+        payer = Payer(payment_method='paypal')
+        assert payer.payment_method == 'paypal'
+        assert payer.funding_instruments == []
+        assert payer.payer_info is None
+        assert payer.status is None
+
+    def test_init_invalid_payment_meth(self):
+        # we must provide a valid payment method
+        payer = Payer(payment_method='paypal')
+        assert payer is not None
+        payer = Payer(payment_method='credit_card')
+        assert payer is not None
+        self.assertRaises(
+            ValueError, Payer, payment_method='invalid')
+
+    def test_init_invalid_payer_state(self):
+        # only certain values are allowed as payer states
+        payer = Payer(payment_method='paypal', status='VERIFIED')
+        assert payer is not None
+        payer = Payer(payment_method='paypal', status='UNVERIFIED')
+        assert payer is not None
+        self.assertRaises(
+            ValueError, Payer, payment_method='paypal', status='InVaLiD')
+
+
+class PayerInfoTests(unittest.TestCase):
+
+    def test_create(self):
+        # we can create payer infos
+        info = PayerInfo()
+        assert info.email == ''
+        assert info.first_name == ''
+        assert info.last_name == ''
+        assert info.payer_id == ''
+        assert info.phone == ''
+        assert info.shipping_address is None
+        assert info.tax_id_type == ''
+        assert info.tax_id == ''
+
+    def test_init_invalid_tax_id_type(self):
+        # onyl certain tax id types are allowed
+        info = PayerInfo(tax_id_type='BR_CPF')
+        assert info is not None
+        info = PayerInfo(tax_id_type='BR_CNPJ')
+        assert info is not None
+        self.assertRaises(
+            ValueError, PayerInfo, tax_id_type='INVALID_TYPE')
+
+
+class CreditCardTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill any interface contracts
+        credit_card = CreditCard(
+            number=u"12345678",
+            credit_card_type="visa",
+            expire_month=4,
+            expire_year=2012,
+            )
+        verifyClass(ICreditCard, CreditCard)
+        verifyObject(ICreditCard, credit_card)
+
+    def test_create(self):
+        # we can create CreditCard objects
+        credit_card = CreditCard(
+            number=u"12345678",
+            credit_card_type="visa",
+            expire_month=4,
+            expire_year=2012,
+            )
+        assert credit_card.paypal_id is None
+        assert credit_card.payer_id is not None
+        assert credit_card.number == u"12345678"
+        assert credit_card.credit_card_type == "visa"
+        assert credit_card.expire_month == 4
+        assert credit_card.expire_year == 2012
+        assert credit_card.cvv2 is None
+        assert credit_card.first_name is None
+        assert credit_card.last_name is None
+        assert credit_card.billing_address is None
+        assert credit_card.state is None
+        assert credit_card.paypal_valid_until is None
+
+    def test_payer_id_given(self):
+        # we do not override given payer ids
+        credit_card = CreditCard(
+            number=u"12345678",
+            credit_card_type="visa",
+            expire_month=4,
+            expire_year=2012,
+            payer_id=u'MySpecialPayerId',
+            )
+        assert credit_card.payer_id == u'MySpecialPayerId'
+
+    def test_payer_id_not_given(self):
+        # in case no payer id is given, we generate one
+        credit_card = CreditCard(
+            number=u"12345678",
+            credit_card_type="visa",
+            expire_month=4,
+            expire_year=2012,
+            )
+        # our payer ids contain a leading 'PAYER_' and 32 hex digits
+        assert re.match('PAYER_[0-9a-f]{32}$', credit_card.payer_id)
+
+    def test_number_is_checked(self):
+        # we do not accept invalid numbers
+        self.assertRaises(
+            ValueError, CreditCard,
+            number=u"not-a-number",
+            credit_card_type="visa",
+            expire_month=4,
+            expire_year=2012,
+            )
+
+    def test_to_str(self):
+        # we can turn CreditCard objects into dicts.
+        addr = Address(
+            line1=u"52 N Main ST",
+            city=u"Johnstown",
+            state=u"OH",
+            postal_code=u"43210",
+            country_code=u"US")
+        credit_card = CreditCard(
+            credit_card_type=u"visa",
+            payer_id=u"PAYER_0123456789012345678901",
+            number=u"4417119669820331",
+            expire_month=11,
+            expire_year=2018,
+            cvv2=u"874",
+            first_name=u"Joe",
+            last_name=u"Shopper",
+            billing_address=addr)
+        self.assertEqual(
+            credit_card.to_dict(),
+            {
+                "type": u"visa",
+                "number": u"4417119669820331",
+                "payer_id": u"PAYER_0123456789012345678901",
+                "expire_month": u"11",
+                "expire_year": u"2018",
+                "cvv2": u"874",
+                "first_name": u"Joe",
+                "last_name": u"Shopper",
+                "billing_address": {
+                    "line1": u"52 N Main ST",
+                    "city": u"Johnstown",
+                    "state": u"OH",
+                    "postal_code": u"43210",
+                    "country_code": u"US"}
+                }
+            )
+
+
+class CreditCardTokenTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill any interface contracts
+        token = CreditCardToken(
+            credit_card_id=u"12345678",
+            )
+        verifyClass(ICreditCardToken, CreditCardToken)
+        verifyObject(ICreditCardToken, token)
+
+    def test_create(self):
+        # we can create CreditCardToken objects
+        token = CreditCardToken(
+            credit_card_id=u"12345678",
+            )
+        assert token.credit_card_id == u"12345678"
+        assert token.payer_id is None
+        assert token.credit_card_type is None
+        assert token.expire_month is None
+        assert token.expire_year is None
+        assert token.last4 is None
+
+    def test_payer_id_given(self):
+        # we do not override given payer ids
+        token = CreditCardToken(
+            credit_card_id=u"12345678",
+            payer_id=u'MySpecialPayerId',
+            )
+        assert token.payer_id == u'MySpecialPayerId'
+
+    def test_to_str(self):
+        # we can turn CreditCardToken objects into dicts.
+        token = CreditCardToken(
+            credit_card_type=u"visa",
+            payer_id=u"PAYER_0123456789012345678901",
+            credit_card_id=u"12345678",
+            last4="8901",
+            expire_month=11,
+            expire_year=2018,
+            )
+        self.assertEqual(
+            token.to_dict(),
+            {
+                "credit_card_id": u"12345678",
+                "payer_id": u"PAYER_0123456789012345678901",
+                "last4": u"8901",
+                "type": u"visa",
+                "expire_month": u"11",
+                "expire_year": u"2018",
+                }
+            )
+
+
+class FundingInstrumentTests(unittest.TestCase):
+
+    def test_create(self):
+        # we can create FundingInstrument objects
+        token = CreditCardToken(
+            credit_card_id=u"12345678",
+            )
+        instr = FundingInstrument(credit_card_token=token)
+        assert instr.credit_card_token is token
+
+    def test_require_credit_card_or_token(self):
+        # we require a credit card object or a token.
+        credit_card = CreditCard(
+            number=u"12345678",
+            credit_card_type="visa",
+            expire_month=4,
+            expire_year=2012,
+            )
+        token = CreditCardToken(
+            credit_card_id=u"12345678",
+            )
+        self.assertRaises(
+            ValueError, FundingInstrument,
+            credit_card=credit_card,
+            credit_card_token=token
+            )
+        self.assertRaises(
+            ValueError, FundingInstrument
+            )
+
+    def test_to_dict(self):
+        # we can turn Funding instruments into dicts
+        token = CreditCardToken(
+            credit_card_type=u"visa",
+            payer_id=u"PAYER_0123456789012345678901",
+            credit_card_id=u"12345678",
+            last4="8901",
+            expire_month=11,
+            expire_year=2018,
+            )
+        instr = FundingInstrument(credit_card_token=token)
+        result = instr.to_dict()
+        self.assertEqual(
+            result,
+            {
+                "credit_card_token": {
+                    "credit_card_id": u"12345678",
+                    "payer_id": u"PAYER_0123456789012345678901",
+                    "last4": u"8901",
+                    "type": u"visa",
+                    "expire_month": u"11",
+                    "expire_year": u"2018",
+                    }
+                }
+            )
+
+
+class AddressTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill any interface contracts
+        addr = Address(
+            line1=u'Address Line 1',
+            city=u'Somecity',
+            country_code=u'AT',
+            )
+        verifyClass(IAddress, Address)
+        verifyObject(IAddress, addr)
+
+    def test_create(self):
+        # we can create addresses
+        addr = Address(
+            line1=u'Honey Street 1',
+            city=u'Beartown',
+            country_code=u'GB',
+            )
+        assert addr.line1 == u'Honey Street 1'
+        assert addr.line2 is None
+        assert addr.city == u'Beartown'
+        assert addr.country_code == u'GB'
+        assert addr.postal_code is None
+        assert addr.state is None
+        assert addr.phone is None
+
+    def test_to_dict(self):
+        # we can turn addresses into dicts
+        addr = Address(
+            line1=u'Honey Street 1',
+            city=u'Beartown',
+            country_code=u'GB',
+            )
+        self.assertEqual(
+            addr.to_dict(),
+            {
+                'line1': u'Honey Street 1',
+                'city': u'Beartown',
+                'country_code': u'GB',
+                }
+            )
+        addr.line2 = u"Behind little tree"
+        self.assertEqual(
+            addr.to_dict(),
+            {
+                'line1': u'Honey Street 1',
+                'line2': u'Behind little tree',
+                'city': u'Beartown',
+                'country_code': u'GB',
+                }
+            )
+
+
+class ShippingAddressTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill any interface contracts
+        addr = ShippingAddress(
+            recipient_name=u'Foo Bar',
+            line1=u'Address Line 1',
+            city=u'Somecity',
+            country_code=u'AT',
+            )
+        verifyClass(IShippingAddress, ShippingAddress)
+        verifyObject(IShippingAddress, addr)
+
+    def test_create(self):
+        # we can create shipping addresses
+        addr = ShippingAddress(
+            recipient_name=u'Rob Receiver',
+            line1=u'Honey Street 1',
+            city=u'Beartown',
+            country_code=u'GB',
+            )
+        assert addr.recipient_name == u'Rob Receiver'
+        assert addr.type == u'residential'
+        assert addr.line1 == u'Honey Street 1'
+        assert addr.line2 is None
+        assert addr.city == u'Beartown'
+        assert addr.country_code == u'GB'
+        assert addr.postal_code is None
+        assert addr.state is None
+        assert addr.phone is None
+
+    def test_to_dict(self):
+        # we can turn shipping addresses into dicts
+        addr = ShippingAddress(
+            recipient_name=u'Rob Receiver',
+            line1=u'Honey Street 1',
+            city=u'Beartown',
+            country_code=u'GB',
+            )
+        self.assertEqual(
+            addr.to_dict(),
+            {
+                'recipient_name': u'Rob Receiver',
+                'type': u'residential',
+                'line1': u'Honey Street 1',
+                'city': u'Beartown',
+                'country_code': u'GB',
+                }
+            )
+        addr.line2 = u"Behind little tree"
+        self.assertEqual(
+            addr.to_dict(),
+            {
+                'recipient_name': u'Rob Receiver',
+                'type': u'residential',
+                'line1': u'Honey Street 1',
+                'line2': u'Behind little tree',
+                'city': u'Beartown',
+                'country_code': u'GB',
+                }
+            )
+
+
+class AmountDetailsTests(unittest.TestCase):
+
+    def test_create(self):
+        # we can create AmountDetail objects
+        details = AmountDetails()
+        assert details.shipping is None
+        assert details.subtotal is None
+        assert details.tax is None
+        assert details.fee is None
+        assert details.handling_fee is None
+        assert details.insurance is None
+        assert details.shipping_discount is None
+
+    def test_to_dict(self):
+        # we can turn AmountDetails into a dict
+        details = AmountDetails(
+            shipping=decimal.Decimal("0.10"),
+            tax=decimal.Decimal("0.30"),
+            fee=decimal.Decimal("0.40"),
+            handling_fee=decimal.Decimal("0.50"),
+            insurance=decimal.Decimal("0.60"),
+            shipping_discount=decimal.Decimal("0.70")
+            )
+        self.assertEqual(
+            details.to_dict(),
+            {
+                'shipping': u"0.10",
+                'subtotal': u"1.20",
+                'tax': u"0.30",
+                'fee': u"0.40",
+                'handling_fee': u"0.50",
+                'insurance': u"0.60",
+                'shipping_discount': u"0.70"
+                }
+            )
+
+    def test_subtotal_all_none(self):
+        # if all items are none, also subtotal is none
+        details = AmountDetails(
+            shipping=None, tax=None, fee=None, handling_fee=None,
+            insurance=None, shipping_discount=None,
+            )
+        assert details.subtotal is None
+        details.shipping_discount = decimal.Decimal("1.00")
+        assert details.subtotal == decimal.Decimal("-1.00")
+
+    def test_subtotal_sum(self):
+        # subtotal sums up correctly
+        details = AmountDetails(
+            shipping=decimal.Decimal("0.05"),
+            tax=decimal.Decimal("0.40"),
+            fee=decimal.Decimal("3.00"),
+            handling_fee=decimal.Decimal("20.00"),
+            insurance=decimal.Decimal("100.00"),
+            shipping_discount=None
+            )
+        self.assertEqual(details.subtotal, decimal.Decimal("123.45"))
+        details.shipping_discount = decimal.Decimal("0.00")
+        self.assertEqual(details.subtotal, decimal.Decimal("123.45"))
+        details.shipping_discount = decimal.Decimal("23.45")
+        self.assertEqual(details.subtotal, decimal.Decimal("100.00"))
+
+
+class AmountTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill any interface contracts.
+        amount = Amount()
+        verifyClass(IAmount, Amount)
+        verifyObject(IAmount, amount)
+
+    def test_create(self):
+        # we can create amount objects
+        details = AmountDetails(
+            shipping=decimal.Decimal("0.05"),
+            tax=decimal.Decimal("0.40"),
+            fee=decimal.Decimal("3.00"),
+            handling_fee=decimal.Decimal("20.00"),
+            insurance=decimal.Decimal("100.00"),
+            shipping_discount=None
+            )
+        amount = Amount(
+            total=decimal.Decimal("12.12"),
+            currency="USD",
+            details=details
+            )
+        assert amount.total == decimal.Decimal("12.12")
+        assert amount.currency == "USD"
+        assert amount.details is details
+
+    def test_to_dict(self):
+        # we can turn Amount objects into dicts
+        self.maxDiff = None
+        details = AmountDetails(
+            shipping=decimal.Decimal("0.05"),
+            tax=decimal.Decimal("0.40"),
+            fee=decimal.Decimal("3.00"),
+            handling_fee=decimal.Decimal("20.00"),
+            insurance=decimal.Decimal("100.00"),
+            shipping_discount=None
+            )
+        amount = Amount(
+            total=decimal.Decimal("12.12"),
+            currency="USD",
+            details=details
+            )
+        self.assertEqual(
+            amount.to_dict(),
+            {
+                'total': u'12.12',
+                'currency': u'USD',
+                'details': {
+                    'shipping': u'0.05',
+                    'subtotal': u'123.45',
+                    'tax': u'0.40',
+                    'fee': u'3.00',
+                    'handling_fee': u'20.00',
+                    'insurance': u'100.00',
+                    }
+                }
+            )
+
+
+class ItemTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill all interface contracts
+        item = Item(name=u"Splendid Item")
+        verifyClass(IItem, Item)
+        verifyObject(IItem, item)
+
+    def test_create(self):
+        # we can create Item objects
+        item = Item(
+            quantity=3,
+            name=u"Splendid Thing",
+            price=decimal.Decimal("1.1"),
+            currency="USD",
+            sku="pcs",
+            description=u"Soo splendid!",
+            tax=decimal.Decimal("0.1"),
+            )
+        assert item.quantity == 3
+        assert item.name == u"Splendid Thing"
+        assert item.price == decimal.Decimal("1.1")
+        assert item.currency == "USD"
+        assert item.sku == "pcs"
+        assert item.description == u"Soo splendid!"
+        assert item.tax == decimal.Decimal("0.1")
+
+    def test_to_dict(self):
+        # we can turn Item objects into dicts
+        item = Item(
+            quantity=3,
+            name=u"Splendid Thing",
+            price=decimal.Decimal("1.1"),
+            currency="USD",
+            sku="pcs",
+            description=u"Soo splendid!",
+            tax=decimal.Decimal("0.1"),
+            )
+        self.assertEqual(
+            item.to_dict(),
+            {
+                "quantity": u"3",
+                "name": u"Splendid Thing",
+                "price": u"1.10",
+                "currency": u"USD",
+                "sku": u"pcs",
+                "description": u"Soo splendid!",
+                "tax": u"0.10"
+                }
+            )
+
+
+class ItemListTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill all interface contracts
+        item_list = ItemList()
+        verifyClass(IItemList, ItemList)
+        verifyObject(IItemList, item_list)
+
+    def test_create_minimal(self):
+        # we can create ItemLists with a minimum of params
+        item_list = ItemList()
+        assert item_list.shipping_address is None
+        assert item_list.items == []
+
+    def test_create(self):
+        # we can create ItemLists
+        item1 = Item(
+            name=u"Splendid Thing",
+            )
+        item2 = Item(
+            name=u"Other Splendid Thing",
+            )
+        addr = ShippingAddress(
+            recipient_name=u'Rob Receiver',
+            line1=u'Honey Street 1',
+            city=u'Beartown',
+            country_code=u'GB',
+            )
+        item_list = ItemList(
+            shipping_address=addr,
+            items=[item1, item2])
+        assert item_list.shipping_address is addr
+        assert item_list.items == [item1, item2]
+
+    def test_to_dict(self):
+        # we can turn ITemLists into dicts
+        item = Item(
+            quantity=3,
+            name=u"Splendid Thing",
+            price=decimal.Decimal("1.1"),
+            currency="USD",
+            sku="pcs",
+            description=u"Soo splendid!",
+            tax=decimal.Decimal("0.1"),
+            )
+        addr = ShippingAddress(
+            recipient_name=u'Rob Receiver',
+            line1=u'Honey Street 1',
+            city=u'Beartown',
+            country_code=u'GB',
+            )
+        item_list = ItemList(items=[item, ], shipping_address=addr)
+        self.assertEqual(
+            item_list.to_dict(),
+            {
+                "items": [
+                    {
+                        "quantity": u"3",
+                        "name": u"Splendid Thing",
+                        "price": u"1.10",
+                        "currency": u"USD",
+                        "sku": u"pcs",
+                        "description": u"Soo splendid!",
+                        "tax": u"0.10"
+                        }
+                    ],
+                "shipping_address":
+                {
+                    'recipient_name': u'Rob Receiver',
+                    'type': u'residential',
+                    'line1': u'Honey Street 1',
+                    'city': u'Beartown',
+                    'country_code': u'GB',
+                    }
+                }
+            )
+
+
+class PaymentOptionsTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill all interface contracts
+        opts = PaymentOptions()
+        verifyClass(IPaymentOptions, PaymentOptions)
+        verifyObject(IPaymentOptions, opts)
+
+    def test_create(self):
+        # we can create PaymentOptions objects
+        opts = PaymentOptions()
+        assert opts.allowed_payment_method is None
+
+    def test_allowed_payment_method_checked_in_init(self):
+        # any value apart from None, INSTANT... is rejected in __init__
+        self.assertRaises(
+            ValueError,
+            PaymentOptions, allowed_payment_method='NoTvAlID')
+
+    def test_to_dict(self):
+        # we can turn PaymentOptions into dicts
+        opts = PaymentOptions(
+            allowed_payment_method="INSTANT_FUNDING_SOURCE")
+        self.assertEqual(
+            opts.to_dict(),
+            {
+                'allowed_payment_method': "INSTANT_FUNDING_SOURCE",
+                }
+            )
+
+
+class TransactionTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill all interface contracts
+        amount = Amount()
+        transaction = Transaction(amount=amount)
+        verifyClass(ITransaction, Transaction)
+        verifyObject(ITransaction, transaction)
+
+    def test_create(self):
+        # we can create transacions
+        amount = Amount()
+        transaction = Transaction(amount=amount)
+        assert transaction.amount is amount
+        assert transaction.description is None
+        assert transaction.item_list is None
+        assert transaction.related_resources == []
+        assert transaction.invoice_number is None
+        assert transaction.custom is None
+        assert transaction.soft_descriptor is None
+        assert transaction.payment_options is None
+
+    def test_to_dict(self):
+        # we can turn Transaction objects into dicts
+        transaction = Transaction(
+            amount=Amount(),
+            description=u"My description",
+            item_list=ItemList(),
+            related_resources=[],
+            invoice_number=u"12345",
+            custom=u"Some custom remark",
+            soft_descriptor=u"softdescriptor?",
+            payment_options=PaymentOptions(
+                allowed_payment_method="INSTANT_FUNDING_SOURCE"),
+            )
+        self.assertEqual(
+            transaction.to_dict(), {
+                'amount': {'currency': u'USD', 'total': u'0.00'},
+                'custom': u'Some custom remark',
+                'description': u'My description',
+                'invoice_number': u'12345',
+                'item_list': {
+                    'items': []
+                    },
+                'payment_options': {
+                    'allowed_payment_method': u'INSTANT_FUNDING_SOURCE'
+                    },
+                'related_resources': [],
+                'soft_descriptor': u'softdescriptor?'
+                }
+            )
+
+
+class AddressTypesVocabTests(unittest.TestCase):
+
+    def test_address_types_vocab_tokenized(self):
+        # we can get a countries source suitable for forms etc.
+        verifyObject(IVocabularyTokenized, ADDRESS_TYPES_VOCAB)
+
+    def test_address_types_vocab_i18nized(self):
+        # vocab titles are i18nized
+        result = ADDRESS_TYPES_VOCAB.getTerm('residential')
+        assert ITerm.providedBy(result)
+        self.assertEqual(result.title, u'residential')
+        assert isinstance(result.title, i18nMessage)
+
+    def test_address_types_vocab_tokens_are_string(self):
+        # vocab tokens are simple strings
+        result = ADDRESS_TYPES_VOCAB.getTerm('residential')
+        assert ITerm.providedBy(result)
+        assert result.token == result.value
+        assert result.value == 'residential'
+        assert isinstance(result.token, str)
+        assert isinstance(result.value, str)
+
+
+class CreditCardTypesVocabTests(unittest.TestCase):
+
+    def test_credit_card_types_vocab_tokenized(self):
+        # we can get a countries source suitable for forms etc.
+        verifyObject(IVocabularyTokenized, CREDIT_CARD_TYPES_VOCAB)
+
+    def test_credit_cards_types_vocab_i18nized(self):
+        # vocab titles are i18nized
+        result = CREDIT_CARD_TYPES_VOCAB.getTerm('visa')
+        assert ITerm.providedBy(result)
+        self.assertEqual(result.title, u'visa')
+        assert isinstance(result.title, i18nMessage)
+
+    def test_credit_cards_types_vocab_tokens_are_string(self):
+        # vocab tokens are simple strings
+        result = CREDIT_CARD_TYPES_VOCAB.getTerm('visa')
+        assert ITerm.providedBy(result)
+        assert result.token == result.value
+        assert result.value == 'visa'
+        assert isinstance(result.token, str)
+        assert isinstance(result.value, str)
+
+
+class CreditcardstatusVocabTests(unittest.TestCase):
+
+    def test_credit_card_status_vocab_tokenized(self):
+        # we can get a countries source suitable for forms etc.
+        verifyObject(IVocabularyTokenized, CREDIT_CARD_STATUS_VOCAB)
+
+    def test_credit_cards_status_vocab_i18nized(self):
+        # vocab titles are i18nized
+        result = CREDIT_CARD_STATUS_VOCAB.getTerm('ok')
+        assert ITerm.providedBy(result)
+        self.assertEqual(result.title, u'ok')
+        assert isinstance(result.title, i18nMessage)
+
+    def test_credit_cards_status_vocab_tokens_are_string(self):
+        # vocab tokens are simple strings
+        result = CREDIT_CARD_STATUS_VOCAB.getTerm('expired')
+        assert ITerm.providedBy(result)
+        assert result.token == result.value
+        assert result.value == 'expired'
+        assert isinstance(result.token, str)
+        assert isinstance(result.value, str)
+
+
+class StockKeepingUnitsVocabTests(unittest.TestCase):
+
+    def test_sku_vocab_tokenized(self):
+        # we can get a tokenzed vocab for stock keeping units
+        verifyObject(IVocabularyTokenized, STOCK_KEEPING_UNITS_VOCAB)
+
+    def test_sku_vocab_i18nized(self):
+        # vocab titles are i18nized
+        result = STOCK_KEEPING_UNITS_VOCAB.getTerm('pcs')
+        assert ITerm.providedBy(result)
+        self.assertEqual(result.title, u'pieces')
+        assert isinstance(result.title, i18nMessage)
+
+    def test_sku_vocab_tokens_are_string(self):
+        # vocab tokens are simple strings
+        result = STOCK_KEEPING_UNITS_VOCAB.getTerm('pcs')
+        assert ITerm.providedBy(result)
+        assert result.token == result.value
+        assert result.value == 'pcs'
+        assert isinstance(result.token, str)
+        assert isinstance(result.value, str)
+
+
+class PaymentOptionMethodsVocabTests(unittest.TestCase):
+
+    def test_payment_option_methods_vocab_tokenized(self):
+        # we can get a countries source suitable for forms etc.
+        verifyObject(IVocabularyTokenized, PAYMENT_OPTION_METHODS_VOCAB)
+
+    def test_payment_option_methods_vocab_i18nized(self):
+        # vocab titles are i18nized
+        result = PAYMENT_OPTION_METHODS_VOCAB.getTerm('INSTANT_FUNDING_SOURCE')
+        assert ITerm.providedBy(result)
+        self.assertEqual(result.title, u'INSTANT_FUNDING_SOURCE')
+        assert isinstance(result.title, i18nMessage)
+
+    def test_payment_option_methods_vocab_tokens_are_string(self):
+        # vocab tokens are simple strings
+        result = PAYMENT_OPTION_METHODS_VOCAB.getTerm('INSTANT_FUNDING_SOURCE')
+        assert ITerm.providedBy(result)
+        assert result.token == result.value
+        assert result.value == 'INSTANT_FUNDING_SOURCE'
+        assert isinstance(result.token, str)
+        assert isinstance(result.value, str)
+
+
+class FakePayer(object):
+
+    implements(IPayer)
+
+    payer_id = 'PAYER-123'
+
+
+class FakePayee(object):
+
+     implements(IPayee)
+
+     payee_id = 'PAYEE-456'
+
+
+class FakePaymentItem(object):
+
+    implements(IPaymentItem)
+
+    item_id = 'BILL-123456'
+    amount = decimal.Decimal("12.10")
+    currency = 'EUR'
+
+
+class PayPalCreditCardServiceTests(unittest.TestCase):
+
+    def test_iface(self):
+        # we fullfill all interface contracts
+        service = PayPalCreditCardService()
+        verifyClass(IPaymentGatewayService, PayPalCreditCardService)
+        verifyObject(IPaymentGatewayService, service)
+
+    def DIStest_creditcard_service_can_create_payment(self):
+        # we can create IPayment objects with creditcard service
+        service = PayPalCreditCardService()
+        payment = service.create_payment(
+            payer=FakePayer(),
+            payment_item=FakePaymentItem(),
+            payee=FakePayee()
+            )
+        assert IPayment.providedBy(payment)
+
 
 class FunctionalPaypalTests(FunctionalTestCase):
@@ -136,2 +1105,13 @@
         result = payment.create()
         assert result is True
+
+    def test_paypal_services_registered(self):
+        # the PayPal gateway services are all registered
+        creditcard_service = queryUtility(
+            IPaymentGatewayService, name=u'paypal_creditcard')
+        assert creditcard_service is not None
+        assert isinstance(creditcard_service, PayPalCreditCardService)
+        paypal_regular_service = queryUtility(
+            IPaymentGatewayService, name=u'paypal_regular')
+        assert paypal_regular_service is not None
+        assert isinstance(paypal_regular_service, PayPalRegularPaymentService)
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_paypal_countries.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_paypal_countries.py	(revision 12311)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_paypal_countries.py	(revision 12311)
@@ -0,0 +1,38 @@
+import unittest
+from zope.i18nmessageid.message import Message as i18nMessage
+from zope.interface.verify import verifyObject
+from zope.schema.interfaces import IVocabularyTokenized, ITerm
+from waeup.ikoba.payments.paypal_countries import COUNTRIES, COUNTRIES_VOCAB
+
+
+class CountryTests(unittest.TestCase):
+
+    def test_countries_is_dict(self):
+        # COUNTRIES is available
+        assert isinstance(COUNTRIES, dict)
+
+    def test_countries_contains_i18n_messages(self):
+        # the COUNTRIES values are i18n messages
+        assert 'DE' in COUNTRIES.keys()
+        assert isinstance(COUNTRIES['DE'], i18nMessage)
+
+
+class CountriesVocabTests(unittest.TestCase):
+
+    def test_countries_vocab_tokenized(self):
+        # we can get a countries source suitable for forms etc.
+        verifyObject(IVocabularyTokenized, COUNTRIES_VOCAB)
+
+    def test_countries_vocab_i18nized(self):
+        # vocab titles are i18nized
+        result = COUNTRIES_VOCAB.getTerm('DE')
+        assert ITerm.providedBy(result)
+        self.assertEqual(result.title, u'GERMANY')
+        assert isinstance(result.title, i18nMessage)
+
+    def test_countries_vocab_tokens_are_string(self):
+        # vocab tokens are simple strings
+        result = COUNTRIES_VOCAB.getTerm('DE')
+        assert ITerm.providedBy(result)
+        assert result.token == 'DE'
+        assert result.value == 'DE'
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_paypal_currencies.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_paypal_currencies.py	(revision 12311)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_paypal_currencies.py	(revision 12311)
@@ -0,0 +1,63 @@
+# $Id$
+#
+# Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# 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
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import unittest
+from zope.i18nmessageid.message import Message as i18nMessage
+from zope.interface.verify import verifyObject
+from zope.schema.interfaces import IVocabularyTokenized, ITerm
+from waeup.ikoba.payments.paypal_currencies import (
+    CURRENCIES, CURRENCIES_VOCAB,
+    )
+
+
+class CurrencyTests(unittest.TestCase):
+
+    def test_currencies_is_dict(self):
+        # CURRENCIES is available
+        assert isinstance(CURRENCIES, dict)
+
+    def test_currencies_contains_i18n_messages(self):
+        # the CURRENCIES values are i18n messages
+        assert 'USD' in CURRENCIES.keys()
+        assert isinstance(CURRENCIES['USD'][0], i18nMessage)
+
+    def test_currencies_contain_domestic_infos(self):
+        # there is info about currencies valid domestic only avail.
+        CURRENCIES['USD'][1] is False
+        # there is info about whether the currency support decimals
+        CURRENCIES['USD'][2] is True
+
+
+class CurrenciesVocabTests(unittest.TestCase):
+
+    def test_currencies_vocab_tokenized(self):
+        # we can get a currencies source suitable for forms etc.
+        verifyObject(IVocabularyTokenized, CURRENCIES_VOCAB)
+
+    def test_currencies_vocab_i18nized(self):
+        # vocab titles are i18nized
+        result = CURRENCIES_VOCAB.getTerm('USD')
+        assert ITerm.providedBy(result)
+        self.assertEqual(result.title, u'United States dollar')
+        assert isinstance(result.title, i18nMessage)
+
+    def test_currencies_vocab_tokens_are_string(self):
+        # vocab tokens are simple strings
+        result = CURRENCIES_VOCAB.getTerm('USD')
+        assert ITerm.providedBy(result)
+        assert result.token == 'USD'
+        assert result.value == 'USD'
