Index: main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/interfaces.py
===================================================================
--- main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/interfaces.py	(revision 12296)
+++ main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/interfaces.py	(revision 12305)
@@ -86,6 +86,16 @@
         )
 
-    def create_payment(payer_id, payee_id, payment_item_id):
+    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.
         """
 
@@ -236,4 +246,15 @@
 
 
+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.
@@ -242,4 +263,14 @@
         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,
         )
 
Index: main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/payment.py
===================================================================
--- main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/payment.py	(revision 12296)
+++ main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/payment.py	(revision 12305)
@@ -27,5 +27,5 @@
 from waeup.ikoba.payments.interfaces import (
     IPayment, STATE_UNPAID, STATE_FAILED, STATE_PAID,
-    IPaymentGatewayService,
+    IPaymentGatewayService, IPayer,
     )
 from waeup.ikoba.utils.logger import Logger
@@ -63,4 +63,5 @@
         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
Index: main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/paypal.py
===================================================================
--- main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/paypal.py	(revision 12296)
+++ main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/paypal.py	(revision 12305)
@@ -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/branches/uli-payments/src/waeup/ikoba/payments/tests/test_paypal.py
===================================================================
--- main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_paypal.py	(revision 12296)
+++ main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_paypal.py	(revision 12305)
@@ -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)
