Changeset 12311


Ignore:
Timestamp:
24 Dec 2014, 09:17:05 (10 years ago)
Author:
uli
Message:

Merge changes from uli-paypal back into trunk.

Location:
main/waeup.ikoba/trunk
Files:
10 edited
8 copied

Legend:

Unmodified
Added
Removed
  • main/waeup.ikoba/trunk

  • main/waeup.ikoba/trunk/src/waeup/ikoba

  • main/waeup.ikoba/trunk/src/waeup/ikoba/payments/__init__.py

    r11949 r12311  
    33# Make this a package.
    44from waeup.ikoba.payments.container import PaymentsContainer
    5 from waeup.ikoba.payments.payment import OnlinePayment
     5from waeup.ikoba.payments.payment import Payment
    66
    77__all__ = [
    88    'PaymentsContainer',
    9     'OnlinePayment',
     9    'Payment',
    1010    ]
  • main/waeup.ikoba/trunk/src/waeup/ikoba/payments/catalog.py

    r12186 r12311  
    2222from waeup.ikoba.payments.interfaces import IPayment
    2323
     24
    2425class PaymentIndexes(grok.Indexes):
    2526    """A catalog for all payments.
     
    2930    grok.context(IPayment)
    3031
    31     p_id = grok.index.Field(attribute='p_id')
    32     p_category = grok.index.Field(attribute='p_category')
    33     p_item = grok.index.Field(attribute='p_item')
    34     p_state = grok.index.Field(attribute='p_state')
     32    payment_id = grok.index.Field(attribute='payment_id')
     33    payer_id = grok.index.Field(attribute='payer_id')
     34    payed_item_id = grok.index.Field(attribute='payed_item_id')
     35    state = grok.index.Field(attribute='state')
     36    amount = grok.index.Field(attribute='amount')
  • main/waeup.ikoba/trunk/src/waeup/ikoba/payments/container.py

    r11949 r12311  
    2020"""
    2121import grok
    22 from grok import index
    2322from waeup.ikoba.payments.interfaces import IPaymentsContainer
    2423from waeup.ikoba.utils.helpers import attrs_to_fields
     24
    2525
    2626class PaymentsContainer(grok.Container):
  • main/waeup.ikoba/trunk/src/waeup/ikoba/payments/interfaces.py

    r12186 r12311  
    1616## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
    1717##
    18 from zope.interface import Attribute
     18import decimal
     19from zc.sourcefactory.basic import BasicSourceFactory
    1920from zope import schema
     21from zope.component import getUtilitiesFor
     22from zope.interface import Interface
    2023from waeup.ikoba.interfaces import (
    21     IIkobaObject, SimpleIkobaVocabulary,
    22     ContextualDictSourceFactoryBase)
     24    IIkobaObject, SimpleIkobaVocabulary, ContextualDictSourceFactoryBase)
    2325from waeup.ikoba.interfaces import MessageFactory as _
     26from waeup.ikoba.payments.currencies import ISO_4217_CURRENCIES_VOCAB
     27
     28#: Possible states of payments
     29STATE_UNPAID = 1
     30STATE_PAID = 2
     31STATE_FAILED = 4
    2432
    2533payment_states = SimpleIkobaVocabulary(
    26     (_('Not yet paid'),'unpaid'),
    27     (_('Paid'),'paid'),
    28     (_('Failed'),'failed'),
     34    (_('Not yet paid'), STATE_UNPAID),
     35    (_('Paid'), STATE_PAID),
     36    (_('Failed'), STATE_FAILED),
    2937    )
     38
     39
     40class PaymentGatewayServicesSource(BasicSourceFactory):
     41    """A source that lists available payment services.
     42
     43    Suitable for forms etc. Token and value correspond to the name the
     44    respective IPaymentGatewayService utility is registered with.
     45    """
     46
     47    _services = None
     48
     49    @classmethod
     50    def services(cls):
     51        """Cache the services registered on startup.
     52
     53        We assume that services do not change after startup.
     54        """
     55        if cls._services is None:
     56            cls._services = dict(getUtilitiesFor(IPaymentGatewayService))
     57        return cls._services
     58
     59    def getValues(self):
     60        """Get payment gateway registration names.
     61        """
     62        return sorted(PaymentGatewayServicesSource.services().keys())
     63
     64    def getTitle(self, value):
     65        """Get title of the respective service, if it exists.
     66        """
     67        service = PaymentGatewayServicesSource.services().get(value, None)
     68        if service is not None:
     69            return service.title
     70
     71
     72class IPaymentGatewayService(Interface):
     73    """A financial gateway service.
     74
     75    Any gateway provider might provide several services. For instance
     76    payments by credit card, scratch card, bank transfer, etc. An
     77    `IPaymentGatewayService` represents one of those services.
     78
     79    Payment services are normally registered as a named global
     80    utility.
     81    """
     82    title = schema.TextLine(
     83        title=u'Title',
     84        description=u'Human readable name of gateway service.',
     85        required=True,
     86        )
     87
     88    def create_payment(payer, payment_item,  payee):
     89        """Create a payment.
     90
     91        For all parameters we expect an object, that implements
     92        `IPayer`, `IPaymentItem`, or `IPayee` respectively. If not,
     93        then the given objects must be at least adaptable to the
     94        respective interface.
     95
     96        Therfore you can pass in some `Customer` as long as there is
     97        some `IPayer` adapter for `Customer` objects defined.
     98
     99        Returns an `IPayment` object.
     100        """
     101
    30102
    31103class PaymentCategorySource(ContextualDictSourceFactoryBase):
     
    36108    DICT_NAME = 'PAYMENT_CATEGORIES'
    37109
     110
    38111class IPaymentsContainer(IIkobaObject):
    39112    """A container for all kind of payment objects.
     
    41114    """
    42115
     116
    43117class IPayment(IIkobaObject):
    44118    """A base representation of payments.
    45119
    46     """
    47     p_id = Attribute('Payment identifier')
    48 
    49     p_category = schema.Choice(
    50         title = _(u'Payment Category'),
    51         #default = u'schoolfee',
    52         source = PaymentCategorySource(),
    53         required = True,
    54         )
    55 
    56     p_item = schema.TextLine(
    57         title = u'',
    58         default = None,
    59         required = False,
    60         )
    61 
    62     display_item = schema.TextLine(
    63         title = _(u'Payment Item'),
    64         required = False,
    65         readonly = True,
    66         )
    67 
    68     p_state = schema.Choice(
    69         title = _(u'Payment State'),
    70         default = u'unpaid',
    71         vocabulary = payment_states,
    72         required = True,
     120    In a payment, a payer payes someone (the payee) for something, the
     121    item to pay.
     122
     123    We currently support only the case where one payer pays one payee
     124    for one item. The item might include taxes, handling,
     125    shipping, etc.
     126
     127    As in RL any payment is actually performed by some financial
     128    service provider (like paypal, interswitch, etc.), each of which
     129    might provide several types of payments (credit card, scratch
     130    card, you name it).
     131
     132    In Ikoba we call financial service providers 'gateway' and their
     133    types of services are handled as gateway types. Therefore PayPal
     134    handling a credit card payment is different from PayPal handling a
     135    regular PayPal account transaction.
     136
     137    A payment can be approve()d, which means the act of paying was
     138    really performed. It can also fail for any reason, in which case
     139    we mark the payment 'failed'.
     140    """
     141    payment_id = schema.TextLine(
     142        title=u'Payment Identifier',
     143        default=None,
     144        required=True,
     145        )
     146
     147    payer_id = schema.TextLine(
     148        title=u'Payer',
     149        default=None,
     150        required=True,
     151        )
     152
     153    payed_item_id = schema.TextLine(
     154        title=u'Payed Item ID',
     155        default=None,
     156        required=True,
     157        )
     158
     159    gateway_service = schema.Choice(
     160        title=u'Payment Gateway',
     161        description=u'Payment gateway that handles this transaction.',
     162        source=PaymentGatewayServicesSource(),
     163        default=None,
     164        required=True,
     165        )
     166
     167    state = schema.Choice(
     168        title=_(u'Payment State'),
     169        default=STATE_UNPAID,
     170        vocabulary=payment_states,
     171        required=True,
    73172        )
    74173
    75174    creation_date = schema.Datetime(
    76         title = _(u'Ticket Creation Date'),
    77         readonly = False,
    78         required = False,
     175        title=_(u'Creation Datetime'),
     176        readonly=False,
     177        required=False,
    79178        )
    80179
    81180    payment_date = schema.Datetime(
    82         title = _(u'Payment Date'),
    83         required = False,
    84         readonly = False,
    85         )
    86 
    87     amount_auth = schema.Float(
    88         title = _(u'Amount Authorized'),
    89         default = 0.0,
    90         required = True,
    91         readonly = False,
    92         )
     181        title=_(u'Payment Datetime'),
     182        required=False,
     183        readonly=False,
     184        )
     185
     186    amount = schema.Decimal(
     187        title=_(u'Amount'),
     188        description=_(
     189            'The overall sum payed, including all taxes fees, etc.'),
     190        default=decimal.Decimal("0.00"),
     191        required=True,
     192        readonly=False,
     193        )
     194
     195    def approve():
     196        """Approve a payment.
     197
     198        The payment was approved and can now be considered payed. This
     199        kind of approvement means the final one (in case there are
     200        several instances to ask).
     201        """
     202
     203    def mark_failed(reason=None):
     204        """Mark the payment as failed.
     205
     206        A failed payment was canceled due to technical problems,
     207        insufficient funds, etc.
     208        """
     209
    93210
    94211class IOnlinePayment(IPayment):
     
    98215
    99216    ac = schema.TextLine(
    100         title = _(u'Activation Code'),
    101         default = None,
    102         required = False,
    103         readonly = False,
     217        title=_(u'Activation Code'),
     218        default=None,
     219        required=False,
     220        readonly=False,
    104221        )
    105222
    106223    r_amount_approved = schema.Float(
    107         title = _(u'Response Amount Approved'),
    108         default = 0.0,
    109         required = False,
    110         readonly = False,
     224        title=_(u'Response Amount Approved'),
     225        default=0.0,
     226        required=False,
     227        readonly=False,
    111228        )
    112229
    113230    r_code = schema.TextLine(
    114         title = _(u'Response Code'),
    115         default = None,
    116         required = False,
    117         readonly = False,
     231        title=_(u'Response Code'),
     232        default=None,
     233        required=False,
     234        readonly=False,
    118235        )
    119236
    120237    r_desc = schema.TextLine(
    121         title = _(u'Response Description'),
    122         default = None,
    123         required = False,
    124         readonly = False,
     238        title=_(u'Response Description'),
     239        default=None,
     240        required=False,
     241        readonly=False,
    125242        )
    126243
    127244    def approve():
    128245        "Approve an online payment and set to paid."
     246
     247
     248class ICreditCard(Interface):
     249    """A credit card.
     250
     251    A credit card is connected to a Payer.
     252    """
     253    credit_card_id = schema.TextLine(
     254        title=u'Internal Credit Card ID',
     255        required=True,
     256        )
     257
     258
     259class IPayer(Interface):
     260    """A payer.
     261    """
     262    payer_id = schema.TextLine(
     263        title=u'Payer ID',
     264        required=True,
     265        )
     266
     267    first_name = schema.TextLine(
     268        title=u'First Name',
     269        required=False,
     270        )
     271
     272    last_name = schema.TextLine(
     273        title=u'Last Name',
     274        required=False,
     275        )
     276
     277
     278class IPayee(Interface):
     279    """A person or institution being paid.
     280    """
     281    payee_id = schema.TextLine(
     282        title=u'Payee ID',
     283        required=True
     284        )
     285
     286
     287class IPaymentItem(Interface):
     288    """Something to sell.
     289    """
     290    item_id = schema.TextLine(
     291        title=u'Payment Item ID',
     292        required=True,
     293        )
     294
     295    title = schema.TextLine(
     296        title=u'Title',
     297        description=u'A short title of the good sold.',
     298        required=True,
     299        default=u'Unnamed'
     300        )
     301
     302    amount = schema.Decimal(
     303        title=u'Amount',
     304        description=u'Total amount, includung any taxes, fees, etc.',
     305        required=True,
     306        default=decimal.Decimal('0.00'),
     307        )
     308
     309    currency = schema.Choice(
     310        title=u'Currency',
     311        source=ISO_4217_CURRENCIES_VOCAB,
     312        required=True,
     313        default='USD',
     314        )
  • main/waeup.ikoba/trunk/src/waeup/ikoba/payments/payment.py

    r11949 r12311  
    1919These are the payment tickets.
    2020"""
     21import decimal
    2122import grok
     23import uuid
    2224from datetime import datetime
    23 from grok import index
     25from zope.component import getUtilitiesFor
    2426from zope.event import notify
    25 from zope.component import getUtility
    26 from zope.i18n import translate
    27 from waeup.ikoba.interfaces import IIkobaUtils
    28 from waeup.ikoba.interfaces import MessageFactory as _
    2927from waeup.ikoba.payments.interfaces import (
    30     IPayment, IOnlinePayment,
    31     payment_states)
    32 from waeup.ikoba.utils.helpers import attrs_to_fields, get_current_principal
     28    IPayment, STATE_UNPAID, STATE_FAILED, STATE_PAID,
     29    IPaymentGatewayService, IPayer,
     30    )
    3331from waeup.ikoba.utils.logger import Logger
    3432
    35 class Payment(grok.Container, Logger):
     33
     34def get_payment_providers():
     35    """Get all services of payment gateways registered.
     36    """
     37    return dict(
     38        getUtilitiesFor(IPaymentGatewayService)
     39        )
     40
     41
     42class PaymentProviderServiceBase(grok.GlobalUtility):
     43
     44    grok.baseclass()
     45    grok.implements(IPaymentGatewayService)
     46
     47    title = u'Sample Credit Card Service'
     48
     49
     50class Payment(grok.Model, Logger):
    3651    """This is a payment.
    3752    """
    3853    grok.implements(IPayment)
    3954    grok.provides(IPayment)
    40     grok.baseclass()
    4155
    4256    logger_name = 'waeup.ikoba.${sitename}.payments'
     
    4458    logger_format_str = '"%(asctime)s","%(user)s",%(message)s'
    4559
    46     def logger_info(self, comment=None):
    47         """Get the logger's info method.
    48         """
    49         self.logger.info('%s' % comment)
    50         return
    51 
    5260    def __init__(self):
    5361        super(Payment, self).__init__()
    5462        self.creation_date = datetime.utcnow()
    55         self.p_id = None
     63        self.payment_date = None
     64        self.payment_id = u'PAY_' + unicode(uuid.uuid4().hex)
     65        self.gateway_service = None
     66        self.amount = decimal.Decimal("0.00")
     67        self.payed_item_id = None
     68        self.payer_id = None
     69        self.state = STATE_UNPAID
    5670        return
    5771
    58     @property
    59     def p_state_title(self):
    60         return payment_states.getTermByToken(self.p_state).title
     72    def approve(self, payment_date=None):
     73        """A payment was approved.
    6174
    62     @property
    63     def category(self):
    64         utils = getUtility(IIkobaUtils)
    65         return utils.PAYMENT_CATEGORIES.get(self.p_category, None)
     75        Successful ending; the payment is marked as payed.
    6676
    67     @property
    68     def display_item(self):
    69         ikoba_utils = getUtility(IIkobaUtils)
    70         return ikoba_utils.getPaymentItem(self)
     77        If `payment_date` is given, it must be a datetime object
     78        giving a datetime in UTC timezone.
    7179
    72 class OnlinePayment(Payment):
    73     """This is an online payment.
    74     """
    75     grok.implements(IOnlinePayment)
    76     grok.provides(IOnlinePayment)
     80        Raises ObjectModifiedEvent.
     81        """
     82        if payment_date is None:
     83            payment_date = datetime.utcnow()
     84        self.payment_date = payment_date
     85        self.state = STATE_PAID
     86        notify(grok.ObjectModifiedEvent(self))
    7787
    78     def __init__(self):
    79         super(OnlinePayment, self).__init__()
    80         p_id = None
    81         return
     88    def mark_failed(self, reason=None):
     89        """Mark payment as failed.
    8290
    83     def approve(self):
    84         "Approve online payment and set to paid."
    85         self.r_amount_approved = self.amount_auth
    86         self.r_code = u'AP'
    87         self.p_state = 'paid'
    88         user = get_current_principal()
    89         if user is None:
    90             # in tests
    91             usertitle = 'system'
    92         else:
    93             usertitle = getattr(user, 'public_name', None)
    94             if not usertitle:
    95                 usertitle = user.title
    96         r_desc = _('Payment approved by ${a}', mapping = {'a': usertitle})
    97         portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
    98         self.r_desc = translate(r_desc, 'waeup.ikoba',
    99             target_language=portal_language)
    100         self.payment_date = datetime.utcnow()
    101         # Update catalog
     91        Raises ObjectModifiedEvent.
     92        """
     93        self.state = STATE_FAILED
    10294        notify(grok.ObjectModifiedEvent(self))
    103         return
    104 
    105 OnlinePayment = attrs_to_fields(OnlinePayment, omit=['display_item'])
  • main/waeup.ikoba/trunk/src/waeup/ikoba/payments/paypal.py

    r12060 r12311  
    1818"""Support for PayPal payments.
    1919"""
     20import decimal
     21import grok
    2022import ConfigParser
     23import inspect
    2124import paypalrestsdk
     25import re
     26import uuid
     27from zope import schema
    2228from zope.component import queryUtility
    23 from waeup.ikoba.interfaces import IPayPalConfig
     29from zope.interface import Interface, Attribute
     30from waeup.ikoba.interfaces import MessageFactory as _
     31from waeup.ikoba.interfaces import IPayPalConfig, SimpleIkobaVocabulary
     32from waeup.ikoba.utils.helpers import attrs_to_fields
     33from waeup.ikoba.payments.interfaces import (
     34    IPayment, IPaymentGatewayService, IPayer, IPaymentItem, IPayee,
     35    )
     36from waeup.ikoba.payments.paypal_countries import COUNTRIES_VOCAB
     37from waeup.ikoba.payments.paypal_currencies import CURRENCIES_VOCAB
     38
     39#: Intents allowed for paypal based payments
     40PAYMENT_INTENTS = ('sale', 'authorize', 'order')
     41
     42#: Payment methods allowed by PayPal
     43PAYMENT_METHODS = ('credit_card', 'paypal')
     44
     45#: Payer status set/accepted by PayPal
     46PAYER_STATUS = ('VERIFIED', 'UNVERIFIED')
     47
     48#: Tax ID types accepted by PayPal (yes, this list is complete)
     49TAX_ID_TYPES = ('BR_CPF', 'BR_CNPJ')
     50
     51#: Address types accepted by PayPal
     52ADDRESS_TYPES = ('residential', 'business', 'mailbox')
     53
     54#: A vocabulary with address types
     55ADDRESS_TYPES_VOCAB = SimpleIkobaVocabulary(
     56    *[(_(x), x) for x in ADDRESS_TYPES])
     57
     58#: Credit card types accepted by PayPal
     59CREDIT_CARD_TYPES = ('visa', 'mastercard', 'discover', 'amex')
     60
     61CREDIT_CARD_TYPES_VOCAB = SimpleIkobaVocabulary(
     62    *[(_(x), x) for x in CREDIT_CARD_TYPES])
     63
     64#: Credit card status accepted by PayPal
     65CREDIT_CARD_STATUS = ('expired', 'ok')
     66
     67CREDIT_CARD_STATUS_VOCAB = SimpleIkobaVocabulary(
     68    *[(_(x), x) for x in CREDIT_CARD_STATUS])
     69
     70#: Stock keeping units we support.
     71STOCK_KEEPING_UNITS = {
     72    "pcs": _("pieces"),
     73    }
     74
     75STOCK_KEEPING_UNITS_VOCAB = SimpleIkobaVocabulary(
     76    *[(value, key) for key, value in STOCK_KEEPING_UNITS.items()])
     77
     78#: Payment methods (flags) used by PayPal
     79PAYMENT_OPTION_METHODS = ('INSTANT_FUNDING_SOURCE', )
     80PAYMENT_OPTION_METHODS_VOCAB = SimpleIkobaVocabulary(
     81    *[(_(x), x) for x in PAYMENT_OPTION_METHODS])
     82
     83
     84def to_dict(obj, name_map={}):
     85    """Turn `obj` into some dict representation.
     86    """
     87    result = dict()
     88    for name in dir(obj):
     89        if name.startswith('_'):
     90            continue
     91        value = getattr(obj, name)
     92        if value is None:
     93            continue
     94        if inspect.ismethod(value):
     95            continue
     96        if hasattr(value, 'to_dict'):
     97            value = value.to_dict()
     98        elif isinstance(value, list):
     99            value = [x.to_dict() for x in value]
     100        elif isinstance(value, decimal.Decimal):
     101            value = u"%.2f" % (value, )
     102        else:
     103            value = unicode(value)
     104        name = name_map.get(name, name)
     105        result[name] = value
     106    return result
    24107
    25108
     
    91174
    92175
    93 def get_payment():
     176class Payer(object):
     177    """A payer as required in paypal payments.
     178
     179    According to Paypal docs:
     180
     181    `payment_method` must be one of ``'paypal'`` or ``'credit_card'``
     182    as stored in `PAYMENT_METHODS`.
     183
     184    `funding_instruments` is a list of `FundingInstrument` objects. I
     185    think the list must be empty for ``paypal`` payments and must
     186    contain at least one entry for ``credit_card`` payments.
     187
     188    `payer_info` must be provided for ``paypal`` payments and might be
     189    provided for ``credit_card`` payments. It's a `PayerInfo` object.
     190
     191    `status` reflects the payer's PayPal account status and is
     192    currently supported for ``paypal`` payments. Allowed values are
     193    ``'VERIFIED'`` and ``'UNVERIFIED'`` as stored in `PAYER_STATUS`.
     194    """
     195    def __init__(self, payment_method, funding_instruments=[],
     196                 payer_info=None, status=None):
     197        if payment_method not in PAYMENT_METHODS:
     198            raise ValueError(
     199                "Invalid payment method: use one of %s" %
     200                (PAYMENT_METHODS, )
     201                )
     202        if status and status not in PAYER_STATUS:
     203            raise ValueError(
     204                "Invalid status: use one of %s" % (PAYER_STATUS, )
     205                )
     206        self.payment_method = payment_method
     207        self.funding_instruments = funding_instruments
     208        self.payer_info = payer_info
     209        self.status = status
     210
     211
     212class PayerInfo(object):
     213    """Payer infos as required by Paypal payers.
     214
     215    Normally used with a `Payer` instance (which in turn is part of a
     216    `payment`).
     217
     218    According to PayPal docs:
     219
     220    Pre-filled by PayPal when `payment_method` is ``'paypal'``.
     221
     222    `email`: 127 chars max. I don't think, this value is pre-filled.
     223
     224    `first_name`: First name of payer. Assigned by PayPal.
     225
     226    `last_naem`: Last name of payer. Assigned by PayPal.
     227
     228    `payer_id`: Payer ID as assigned by PayPal. Do not mix up with any
     229    Ikoba Payer IDs.
     230
     231    `phone`: Phone number representing the payer. 20 chars max.
     232
     233    `shipping_address`: a shipping address object of payer. Assigned
     234    by PayPal.
     235
     236    `tax_id_type`: Payer's tax ID type. Allowed values: ``'BR_CPF'``,
     237    ``'BR_CNPJ'`` (I have not the slightest clue what that means).
     238    Supported (PayPal-wise) with ``paypal`` payment method only (not
     239    with ``credit_card``).
     240
     241    `tax_id`: Payer's tax ID. Here the same as for `tax_id_type`
     242    applies (except that also other values are accepted).
     243
     244    By default all values are set to the empty string and shipping
     245    address to `None`.
     246
     247    See also: :class:`Payer`
     248    """
     249    def __init__(self, email='', first_name='', last_name='',
     250                 payer_id='', phone='', shipping_address=None,
     251                 tax_id_type='', tax_id=''):
     252        if tax_id_type and tax_id_type not in TAX_ID_TYPES:
     253            raise ValueError(
     254                "Invalid tax id type: use one of %s" %
     255                (TAX_ID_TYPES, )
     256                )
     257        self.email = email
     258        self.first_name = first_name
     259        self.last_name = last_name
     260        self.payer_id = payer_id
     261        self.phone = phone
     262        self.shipping_address = shipping_address
     263        self.tax_id_type = tax_id_type
     264        self.tax_id = tax_id
     265
     266
     267class FundingInstrument(object):
     268    """Representation of a payer's funding instrument as required by PayPal.
     269
     270    Represents always a credit card. Either by a complete set of
     271    credit card infos or by a credit card token, which contains only a
     272    limited set of credit card data and represents a full set stored
     273    in PayPal vault.
     274    """
     275
     276    def __init__(self, credit_card=None, credit_card_token=None):
     277        if credit_card is None and credit_card_token is None:
     278            raise ValueError(
     279                "Must provide credit card data or a token")
     280        if credit_card is not None and credit_card_token is not None:
     281            raise ValueError(
     282                "Must provide credit card data *or* a token, not both.")
     283        self.credit_card = credit_card
     284        self.credit_card_token = credit_card_token
     285
     286    def to_dict(self):
     287        return to_dict(self)
     288
     289
     290class ICreditCard(Interface):
     291    """A credit card (full data set) as accepted by PayPal.
     292    """
     293
     294    paypal_id = schema.TextLine(
     295        title=u'PayPal ID',
     296        description=u'ID of the credit card provided by PayPal.',
     297        required=False,
     298        )
     299
     300    payer_id = schema.TextLine(
     301        title=u'Payer ID',
     302        description=(u'A unique identifier for the credit card. This '
     303                     u'identifier is provided by Ikoba and must be set '
     304                     u'with all future transactions, once put into '
     305                     u'PayPal vault.'),
     306        required=True,
     307        )
     308
     309    number = schema.TextLine(
     310        title=u'Credit Card Number',
     311        description=u'Numeric characters only w/o spaces or punctuation',
     312        required=True,
     313        )
     314
     315    credit_card_type = schema.Choice(
     316        title=u'Credit Card Type',
     317        description=u'Credit card types supported by PayPal.',
     318        required=True,
     319        source=CREDIT_CARD_TYPES_VOCAB,
     320        )
     321
     322    expire_month = schema.Int(
     323        title=u'Expiration Month',
     324        description=u"Month, the credit card expires.",
     325        required=True,
     326        default=1,
     327        min=1,
     328        max=12,
     329        )
     330
     331    expire_year = schema.Int(
     332        title=u'Expiration Year',
     333        description=u'4-digit expiration year.',
     334        required=True,
     335        default=2020,
     336        min=1900,
     337        max=9999,
     338        )
     339
     340    cvv2 = schema.TextLine(
     341        title=u'CVV2',
     342        description=u'3-4 digit card validation code',
     343        required=False,
     344        )
     345
     346    first_name = schema.TextLine(
     347        title=u'First Name',
     348        description=u"Cardholder's first name.",
     349        required=False,
     350        )
     351
     352    last_name = schema.TextLine(
     353        title=u'Last Name',
     354        description=u"Cardholder's last name.",
     355        required=False,
     356        )
     357
     358    billing_address = Attribute(
     359        "Billing address associated with card.")
     360
     361    state = schema.Choice(
     362        title=u"Status",
     363        description=(u"Status of credit card funding instrument. "
     364                     u"Value assigned by PayPal."
     365                     ),
     366        required=False,
     367        source=CREDIT_CARD_STATUS_VOCAB,
     368        default=None,
     369        )
     370
     371    valid_unti = schema.TextLine(
     372        title=u'Valid until',
     373        description=(u'Funding instrument expiratiopn date, '
     374                     u'assigned by PayPal'),
     375        required=False,
     376        )
     377
     378
     379@attrs_to_fields
     380class CreditCard(object):
     381    """A credit card (full info set) as used by PayPal.
     382
     383    Normally used with a `FundingInstrument` instance.
     384
     385    According to PayPal docs:
     386
     387    `paypal_id`: provided by PayPal when storing credit card
     388          data. Required if using a stored credit card.
     389
     390    `payer_id`: Unique identifier. If none is given, we assign a
     391          uuid. The uuid reads 'PAYER_<32 hex digits>'.
     392
     393    `number`: Credit card number. Numeric characters only with no
     394          spaces or punctuation. The string must conform with modulo
     395          and length required by each credit card type. Redacted in
     396          responses. Required.
     397
     398    `credit_card_type`: One of ``'visa'``, ``'mastercard'``,
     399          ``'discover'``, ``'amex'``. Required.
     400
     401    `expire_month`: Expiration month. A number from 1 through
     402          12. Required.
     403
     404    `expire_year`: 4-digit expiration year. Required.
     405
     406    `cvv2`: 3-4 digit card validation code.
     407
     408    `first_name`: card holders first name.
     409
     410    `last_name`: card holders last name.
     411
     412    `billing_address`: Billing address associated with card. A
     413      `Billing` instance.
     414
     415    `state`: state of the credit card funding instrument. Valid values
     416      are ``'expired'`` and ``'ok'``. *Value assigned by PayPal.*
     417
     418    `paypal_valid_until`: Funding instrument expiration date.
     419       *Value assigned by PayPal.* Not to confuse with the credit cards
     420       expiration date, which is set via `expire_month` and
     421       `expire_year`.
     422
     423    """
     424    grok.implements(ICreditCard)
     425
     426    def __init__(self, paypal_id=None, payer_id=None, number=None,
     427                 credit_card_type=None, expire_month=1, expire_year=2000,
     428                 cvv2=None, first_name=None, last_name=None,
     429                 billing_address=None, state=None, paypal_valid_until=None):
     430        if not re.match('^[0-9]+$', number):
     431            raise ValueError("Credit card number may "
     432                             "not contain non-numbers.")
     433        if payer_id is None:
     434            payer_id = u'PAYER_' + unicode(uuid.uuid4().hex)
     435        self.paypal_id = paypal_id
     436        self.payer_id = payer_id
     437        self.number = number
     438        self.credit_card_type = credit_card_type
     439        self.expire_month = expire_month
     440        self.expire_year = expire_year
     441        self.cvv2 = cvv2
     442        self.first_name = first_name
     443        self.last_name = last_name
     444        self.billing_address = billing_address
     445        self.state = state
     446        self.paypal_valid_until = paypal_valid_until
     447
     448    def to_dict(self):
     449        return to_dict(self, name_map={'credit_card_type': 'type'})
     450
     451
     452class ICreditCardToken(Interface):
     453    """A credit card token corresponding to a credit card stored with PayPal.
     454    """
     455    credit_card_id = schema.TextLine(
     456        title=u"Credit Card ID",
     457        description=u"ID if credit card previously stored with PayPal",
     458        required=True,
     459        )
     460
     461    payer_id = schema.TextLine(
     462        title=u'Payer ID',
     463        description=(u'A unique identifier for the credit card. This '
     464                     u'identifier is provided by Ikoba and must be set '
     465                     u'with all future transactions, once put into '
     466                     u'PayPal vault.'),
     467        required=True,
     468        )
     469
     470    last4 = schema.TextLine(
     471        title=u"Credit Card's last 4 numbers",
     472        description=(
     473            u"Last four digits of the stored credit card number. "
     474            u"Value assigned by PayPal."),
     475        required=False,
     476        min_length=4,
     477        max_length=4
     478        )
     479
     480    credit_card_type = schema.Choice(
     481        title=u'Credit Card Type',
     482        description=(
     483            u'Credit card type supported by PayPal. Value assigned '
     484            u'by PayPal.'),
     485        required=False,
     486        source=CREDIT_CARD_TYPES_VOCAB,
     487        )
     488
     489    expire_month = schema.Int(
     490        title=u'Expiration Month',
     491        description=u"Month, the credit card expires. Assigned by PayPal.",
     492        required=True,
     493        default=1,
     494        min=1,
     495        max=12,
     496        )
     497
     498    expire_year = schema.Int(
     499        title=u'Expiration Year',
     500        description=u'4-digit expiration year. Assigned by PayPal.',
     501        required=True,
     502        default=2020,
     503        min=1900,
     504        max=9999,
     505        )
     506
     507
     508class CreditCardToken(object):
     509    grok.implements(ICreditCardToken)
     510
     511    def __init__(self, credit_card_id, payer_id=None, last4=None,
     512                 credit_card_type=None, expire_month=None, expire_year=None):
     513        self.credit_card_id = credit_card_id
     514        self.payer_id = payer_id
     515        self.last4 = last4
     516        self.credit_card_type = credit_card_type
     517        self.expire_month = expire_month
     518        self.expire_year = expire_year
     519
     520    def to_dict(self):
     521        return to_dict(self, name_map={'credit_card_type': 'type'})
     522
     523
     524class IShippingAddress(Interface):
     525    """A shipping address as accepted by PayPal.
     526    """
     527    recipient_name = schema.TextLine(
     528        title=u'Recipient Name',
     529        required=True,
     530        description=u'Name of the recipient at this address.',
     531        max_length=50,
     532        )
     533
     534    type = schema.Choice(
     535        title=u'Address Type',
     536        description=u'Address Type.',
     537        required=False,
     538        source=ADDRESS_TYPES_VOCAB,
     539        )
     540
     541    line1 = schema.TextLine(
     542        title=u'Address Line 1',
     543        required=True,
     544        description=u'Line 1 of the address (e.g., Number, street, etc.)',
     545        max_length=100,
     546        )
     547
     548    line2 = schema.TextLine(
     549        title=u'Address Line 2',
     550        required=False,
     551        description=u'Line 2 of the address (e.g., Suite, apt #, etc.)',
     552        max_length=100,
     553        )
     554
     555    city = schema.TextLine(
     556        title=u'City',
     557        required=True,
     558        description=u'City name',
     559        max_length=50,
     560        )
     561
     562    country_code = schema.Choice(
     563        title=u'Country',
     564        required=True,
     565        description=u'2-letter country code',
     566        source=COUNTRIES_VOCAB,
     567        )
     568
     569    postal_code = schema.TextLine(
     570        title=u'Postal code',
     571        required=False,
     572        description=(u'Zip code or equivalent is usually required '
     573                     u'for countries that have them.'),
     574        max_length=20,
     575        )
     576
     577    state = schema.TextLine(
     578        title=u'State',
     579        required=False,
     580        description=(u'2-letter code for US stated, and the '
     581                     u'equivalent for other countries.'),
     582        max_length=100,
     583        )
     584
     585    phone = schema.TextLine(
     586        title=u'Phone',
     587        required=False,
     588        description=u'Phone number in E.123 format.',
     589        max_length=50,
     590        )
     591
     592
     593@attrs_to_fields
     594class ShippingAddress(object):
     595    """A shipping address as used in PayPal transactions.
     596    """
     597    grok.implements(IShippingAddress)
     598
     599    def __init__(self, recipient_name, type='residential', line1='',
     600                 line2=None, city='', country_code=None,
     601                 postal_code=None, state=None, phone=None):
     602        self.recipient_name = recipient_name
     603        self.type = type
     604        self.line1 = line1
     605        self.line2 = line2
     606        self.city = city
     607        self.country_code = country_code
     608        self.postal_code = postal_code
     609        self.state = state
     610        self.phone = phone
     611
     612    def to_dict(self):
     613        return to_dict(self)
     614
     615
     616class IAddress(Interface):
     617    """An address as accepted by PayPal.
     618    """
     619    line1 = schema.TextLine(
     620        title=u'Address Line 1',
     621        required=True,
     622        description=u'Line 1 of the address (e.g., Number, street, etc.)',
     623        max_length=100,
     624        )
     625
     626    line2 = schema.TextLine(
     627        title=u'Address Line 2',
     628        required=False,
     629        description=u'Line 2 of the address (e.g., Suite, apt #, etc.)',
     630        max_length=100,
     631        )
     632
     633    city = schema.TextLine(
     634        title=u'City',
     635        required=True,
     636        description=u'City name',
     637        max_length=50,
     638        )
     639
     640    country_code = schema.Choice(
     641        title=u'Country',
     642        required=True,
     643        description=u'2-letter country code',
     644        source=COUNTRIES_VOCAB,
     645        )
     646
     647    postal_code = schema.TextLine(
     648        title=u'Postal code',
     649        required=False,
     650        description=(u'Zip code or equivalent is usually required '
     651                     u'for countries that have them.'),
     652        max_length=20,
     653        )
     654
     655    state = schema.TextLine(
     656        title=u'State',
     657        required=False,
     658        description=(u'2-letter code for US stated, and the '
     659                     u'equivalent for other countries.'),
     660        max_length=100,
     661        )
     662
     663    phone = schema.TextLine(
     664        title=u'Phone',
     665        required=False,
     666        description=u'Phone number in E.123 format.',
     667        max_length=50,
     668        )
     669
     670
     671@attrs_to_fields
     672class Address(object):
     673    """A postal address as used in PayPal transactions.
     674    """
     675    grok.implements(IAddress)
     676
     677    def __init__(self, line1='', line2=None, city='', country_code=None,
     678                 postal_code=None, state=None, phone=None):
     679        self.line1 = line1
     680        self.line2 = line2
     681        self.city = city
     682        self.country_code = country_code
     683        self.postal_code = postal_code
     684        self.state = state
     685        self.phone = phone
     686
     687    def to_dict(self):
     688        """Turn Adress into a dict that can be fed to PayPal classes.
     689        """
     690        return to_dict(self)
     691
     692
     693class AmountDetails(object):
     694    """Amount details can be given with Amount objects.
     695
     696    All parameters are passed in as decimals (`decimal.Decimal`).
     697
     698    All numbers stored here, might have 10 characters max with
     699    support for two decimal places.
     700
     701    No parameter is strictly required, except `subtotal`, which must
     702    be set if any of the other values is set.
     703
     704    `shipping`: Amount charged for shipping.
     705
     706    `subtotal`: Amount for subtotal of the items. Automatically
     707      computed. If no other item was set, subtotal is `None`.
     708
     709    `tax`: Amount charged for tax.
     710
     711    `fee`: Fee charged by PayPal. In case of a refund, this is the fee
     712      amount refunded to the original recipient of the payment. Value
     713      assigned by PayPal.
     714
     715    `handling_fee`: Amount being charged for the handling
     716      fee. Currently supported with paypal payment_method only, but
     717      available for credit_card payment_method at a later date.
     718
     719    `insurance`: Amount being charged for the insurance fee. Currently
     720      supported with paypal payment_method only, but available for
     721      credit_card payment_method at a later date.
     722
     723    `shipping_discount`: Amount being discounted for the shipping
     724      fee. Currently supported with paypal payment_method only, but
     725      available for credit_card payment_method at a later date.
     726    """
     727    def __init__(self, shipping=None, tax=None, fee=None,
     728                 handling_fee=None, insurance=None, shipping_discount=None):
     729        self.shipping = shipping
     730        self.tax = tax
     731        self.fee = fee
     732        self.handling_fee = handling_fee
     733        self.insurance = insurance
     734        self.shipping_discount = shipping_discount
     735
     736    @property
     737    def subtotal(self):
     738        names = (
     739            'shipping', 'tax', 'fee', 'handling_fee', 'insurance',
     740            'shipping_discount'
     741            )
     742        result = None
     743        for name in names:
     744            val = getattr(self, name)
     745            if name == 'shipping_discount' and val is not None:
     746                val = -val
     747            if val is not None:
     748                if result is None:
     749                    result = val
     750                else:
     751                    result += val
     752        return result
     753
     754    def to_dict(self):
     755        return to_dict(self)
     756
     757
     758class IAmount(Interface):
     759    """An amount as used by PayPal in payments.
     760    """
     761    currency = schema.Choice(
     762        title=u'Currency',
     763        description=u'PayPal does not support all currencies. Required.',
     764        required=True,
     765        source=CURRENCIES_VOCAB,
     766        default=u'USD',
     767        )
     768
     769    total = schema.Decimal(
     770        title=u'Total',
     771        description=(
     772            u'Total amount charged from the payer to the payee. '
     773            u'In case of a refund, this is the refunded amount to '
     774            u'the original payer from the payee.'),
     775        required=True,
     776        default=decimal.Decimal("0.00"),
     777        max=decimal.Decimal("9999999.99")
     778        )
     779
     780    details = Attribute(
     781        """Additional details related to a payment amount.
     782        """)
     783
     784
     785@attrs_to_fields
     786class Amount(object):
     787    grok.implements(IAmount)
     788
     789    def __init__(self, currency="USD", total=decimal.Decimal("0.00"),
     790                 details=None):
     791        self.currency = currency
     792        self.total = total
     793        self.details = details
     794
     795    def to_dict(self):
     796        return to_dict(self)
     797
     798
     799class IItem(Interface):
     800    """PayPal Item.
     801
     802    Items in a PayPal context are goods sold to customers.
     803    """
     804    quantity = schema.Int(
     805        title=u"Quantity",
     806        description=u"Number of this particular items.",
     807        required=True,
     808        default=1,
     809        max=9999999999,
     810        )
     811
     812    name = schema.TextLine(
     813        title=u"Name",
     814        description=u"Item name",
     815        required=True,
     816        max_length=127,
     817        )
     818
     819    price = schema.Decimal(
     820        title=u"Price",
     821        description=u"Price",
     822        required=True,
     823        max=decimal.Decimal("9999999.99"),
     824        )
     825
     826    currency = schema.Choice(
     827        title=u"Currency",
     828        description=u"Currency",
     829        source=CURRENCIES_VOCAB,
     830        default=u'USD',
     831        required=True,
     832        )
     833
     834    sku = schema.Choice(
     835        title=u"SKU",
     836        description=u"Stock keeping unit corresponding to item.",
     837        source=STOCK_KEEPING_UNITS_VOCAB,
     838        required=False,
     839        )
     840
     841    description = schema.TextLine(
     842        title=u"Description",
     843        description=(
     844            u"Description of Item. Currently supported with paypal "
     845            u"payments only."
     846            ),
     847        max_length=127,
     848        required=False,
     849        )
     850
     851    tax = schema.Decimal(
     852        title=u"Tax",
     853        description=(
     854            u"Tax of the item. Currently supported with paypal "
     855            u"payments only."
     856            ),
     857        required=False,
     858        )
     859
     860
     861@attrs_to_fields
     862class Item(object):
     863    """See IItem for docs.
     864    """
     865    grok.implements(IItem)
     866
     867    def __init__(self, name, quantity=1, price=decimal.Decimal("0.00"),
     868                 currency="USD", sku=None, description=None, tax=None):
     869        self.name = name
     870        self.quantity = quantity
     871        self.price = price
     872        self.currency = currency
     873        self.sku = sku
     874        self.description = description
     875        self.tax = tax
     876
     877    def to_dict(self):
     878        return to_dict(self)
     879
     880
     881class IItemList(Interface):
     882    """List of `Item` objects and a related `ShippingAddress`.
     883
     884    `items`: can be a simple list of `Item` objects.
     885
     886    `shipping_address`: a `ShippingAddress` object. Only needed if
     887      different from Payer address.
     888    """
     889    items = schema.List(
     890        title=u"Items",
     891        description=u"PayPal Items are sold goods",
     892        value_type=schema.Object(
     893            title=u"Item",
     894            description=u"Item in a list",
     895            schema=IItem,
     896            )
     897        )
     898
     899    shipping_address = schema.Object(
     900        title=u"Shipping Address",
     901        description=u"Shipping address of receiver if different from payee.",
     902        schema=IShippingAddress,
     903        required=False,
     904        )
     905
     906
     907@attrs_to_fields
     908class ItemList(object):
     909    """List of `Item` objects and a related `ShippingAddress`.
     910
     911    `items`: can be a simple list of `Item` objects.
     912
     913    `shipping_address`: a `ShippingAddress` object. Only needed if
     914      different from Payer address.
     915    """
     916    grok.implements(IItemList)
     917
     918    def __init__(self, items=[], shipping_address=None):
     919        self.items = items
     920        self.shipping_address = shipping_address
     921
     922    def to_dict(self):
     923        return to_dict(self)
     924
     925
     926class IPaymentOptions(Interface):
     927    """Payment options requested for a certain purchase unit.
     928
     929    `allowed_payment_method`: Optional payment method type. If
     930      specified, the transaction will go through for only instant
     931      payment. Allowed values: ``INSTANT_FUNDING_SOURCE``. Only for
     932      use with the ``paypal`` payment_method, not relevant for the
     933      ``credit_card`` payment_method.
     934    """
     935    allowed_payment_method = schema.Choice(
     936        title=u"Allowed payment method",
     937        description=(
     938            u"Optional payment type. If specified, the transaction "
     939            u"will go through for only instant payment. Only for use "
     940            u"with paypal payment method, not relevant for credit cards."
     941            ),
     942        required=False,
     943        source=PAYMENT_OPTION_METHODS_VOCAB,
     944        )
     945
     946
     947@attrs_to_fields
     948class PaymentOptions(object):
     949    """Payment options requested for a certain purchase unit.
     950    """
     951    grok.implements(IPaymentOptions)
     952
     953    def __init__(self, allowed_payment_method=None):
     954        if allowed_payment_method not in (
     955            None, 'INSTANT_FUNDING_SOURCE'):
     956            raise ValueError(
     957                "allowed_payment_method of PaymentOptions must be None or "
     958                "'INSTANT_FUNDING_SOURCE'"
     959                )
     960        self.allowed_payment_method = allowed_payment_method
     961
     962    def to_dict(self):
     963        return to_dict(self)
     964
     965
     966class ITransaction(Interface):
     967    """PayPal transactions provide payment transaction details.
     968    """
     969    amount = schema.Object(
     970        title=u"Amount",
     971        description=u"Amount being collected.",
     972        schema=IAmount,
     973        required=True,
     974        )
     975
     976    description = schema.TextLine(
     977        title=u"Description",
     978        description=u"Description of transaction",
     979        required=False,
     980        max_length=127,
     981        )
     982
     983    item_list = schema.Object(
     984        title=u"Item List",
     985        description=u"List of items",
     986        required=False,
     987        schema=IItemList,
     988        )
     989
     990    # XXX: This should be defined more precisely: What kind of objects, etc.
     991    #      PayPal docs say: "array of sale, authorization, capture, or refund,
     992    #      objects"
     993    related_resources = Attribute("Arbitrary objects")
     994
     995    invoice_number = schema.TextLine(
     996        title=u"Invoice Number",
     997        description=(
     998            u"Invoice number used to track the payment. "
     999            u"Currently supported with paypal payment_method only."
     1000            ),
     1001        required=False,
     1002        max_length=256,
     1003        )
     1004
     1005    custom = schema.TextLine(
     1006        title=u"Custom text",
     1007        description=(
     1008            u"Free-form field for the use of clients. Currently "
     1009            u"supported with paypal payment_method only."),
     1010        required=False,
     1011        max_length=256,
     1012        )
     1013
     1014    soft_descriptor = schema.TextLine(
     1015        title=u"Soft descriptor",
     1016        description=(
     1017            u"Soft descriptor used when charging this funding "
     1018            u"source. Currently supported with paypal payment_method only"
     1019            ),
     1020        required=False,
     1021        max_length=22,
     1022        )
     1023
     1024    payment_options = schema.Object(
     1025        title=u"Payment Options",
     1026        description=u"Payment options requested for this purchase unit.",
     1027        required=False,
     1028        schema=IPaymentOptions,
     1029        )
     1030
     1031
     1032@attrs_to_fields
     1033class Transaction(object):
     1034    # See ITransaction for description
     1035
     1036    grok.implements(ITransaction)
     1037
     1038    def __init__(self, amount, description=None, item_list=None,
     1039                 related_resources=[], invoice_number=None, custom=None,
     1040                 soft_descriptor=None, payment_options=None):
     1041        self.amount = amount
     1042        self.description = description
     1043        self.item_list = item_list
     1044        self.related_resources = related_resources
     1045        self.invoice_number = invoice_number
     1046        self.custom = custom
     1047        self.soft_descriptor = soft_descriptor
     1048        self.payment_options = payment_options
     1049
     1050    def to_dict(self):
     1051        """Give a `dict` representation of this `Transaction`.
     1052        """
     1053        return to_dict(self)
     1054
     1055
     1056def get_payment(intent='sale', payment_method='credit_card'):
    941057    """Construct a payment.
    951058
    961059    You have to `create()` the payment yourself.
    971060
     1061    Returns a paypalrestsdk Payment object, not an Ikoba payment.
     1062
     1063    As `intent` currently only the string ``'sale'`` is supported.
     1064
    981065    XXX: Just some sampledata yet.
    991066    """
     1067    if intent != "sale":
     1068        raise ValueError(
     1069            "Currently, only 'sale' is allowed as type of paypal"
     1070            "payment.")
    1001071    payment = paypalrestsdk.Payment(
    1011072        {
    102             "intent": "sale",
     1073            "intent": intent,
    1031074            "payer": {
    1041075                "payment_method": "credit_card",
     
    1231094                    ]},
    1241095            "transactions": [{
    125                     "amount": {
    126                         "total": "7.47",
    127                         "currency": "USD",
    128                         "details": {
    129                             "subtotal": "7.41",
    130                             "tax": "0.03",
    131                             "shipping": "0.03"}},
    132                     "description": ("This is the payment "
    133                                     "transaction description.")
    134                     }]
     1096                "amount": {
     1097                    "total": "7.47",
     1098                    "currency": "USD",
     1099                    "details": {
     1100                        "subtotal": "7.41",
     1101                        "tax": "0.03",
     1102                        "shipping": "0.03"}},
     1103                "description": ("This is the payment "
     1104                                "transaction description.")
     1105                }]
    1351106            }
    1361107        )
    1371108    return payment
     1109
     1110
     1111class IPayPalPayment(IPayment):
     1112    """A paypal payment.
     1113    """
     1114
     1115
     1116class PayPalPayment(grok.Model):
     1117    """A paypal payment.
     1118    """
     1119    pass
     1120
     1121
     1122class PayPalCreditCardService(grok.GlobalUtility):
     1123    grok.implements(IPaymentGatewayService)
     1124    grok.name('paypal_creditcard')
     1125
     1126    title = _(u'Credit Card (PayPal)')
     1127
     1128    def create_payment(self, payer, payment_item,  payee=None):
     1129        if not IPayer.providedBy(payer):
     1130            payer = IPayer(payer)
     1131        if not IPaymentItem.providedBy(payment_item):
     1132            payment_item = IPaymentItem(payment_item)
     1133        return None
     1134        payment = get_payment()
     1135        return payment
     1136
     1137
     1138class PayPalRegularPaymentService(grok.GlobalUtility):
     1139    grok.implements(IPaymentGatewayService)
     1140    grok.name('paypal_regular')
     1141
     1142    title = _(u'PayPal Payment')
  • main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_interfaces.py

    r12060 r12311  
    1717##
    1818"""
    19 Tests for payments.
     19Tests for payments interfaces.
    2020"""
    21 from zope.interface.verify import verifyClass, verifyObject
    22 from waeup.ikoba.payments.interfaces import (
    23     IPaymentsContainer, IOnlinePayment)
    24 from waeup.ikoba.payments.container import PaymentsContainer
    25 from waeup.ikoba.payments.payment import OnlinePayment
     21from waeup.ikoba.payments.interfaces import PaymentGatewayServicesSource
    2622from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase)
    2723
     
    3127    layer = FunctionalLayer
    3228
    33     def test_interfaces(self):
    34         # Make sure the correct interfaces are implemented.
    35         self.assertTrue(
    36             verifyClass(
    37                 IPaymentsContainer, PaymentsContainer)
    38             )
    39         self.assertTrue(
    40             verifyObject(
    41                 IPaymentsContainer, PaymentsContainer())
    42             )
    43         self.assertTrue(
    44             verifyClass(
    45                 IOnlinePayment, OnlinePayment)
    46             )
    47         self.assertTrue(
    48             verifyObject(
    49                 IOnlinePayment, OnlinePayment())
    50             )
    51         return
     29    def test_payment_gateway_services_source(self):
     30        # the payment gateway services source provides a list of registered
     31        # payment gateways
     32        source = PaymentGatewayServicesSource()
     33        services = list(source)
     34        assert len(services) > 0
    5235
    53     def test_base(self):
    54         # We cannot call the fundamental methods of a base in that case
    55         container = PaymentsContainer()
    56         self.assertRaises(
    57             NotImplementedError, container.archive)
    58         self.assertRaises(
    59             NotImplementedError, container.clear)
     36    def test_payment_gateway_services_source_title(self):
     37        # we can get titles from gateway sources
     38        source = PaymentGatewayServicesSource()
     39        service1 = list(source)[0]
     40        title = source.factory.getTitle(service1)
     41        assert title != service1
     42        assert isinstance(title, basestring)
  • main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_paypal.py

    r12060 r12311  
    1616## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
    1717##
     18import decimal
    1819import os
     20import re
    1921import shutil
    2022import tempfile
    2123import unittest
    2224import paypalrestsdk
    23 from zope.component import getGlobalSiteManager, queryUtility
     25from zope.component import (
     26    getGlobalSiteManager, queryUtility, getUtilitiesFor,
     27    )
     28from zope.i18nmessageid.message import Message as i18nMessage
     29from zope.interface import implements
     30from zope.interface.verify import verifyObject, verifyClass
     31from zope.schema.interfaces import IVocabularyTokenized, ITerm
    2432from waeup.ikoba.interfaces import IPayPalConfig
     33from waeup.ikoba.payments.interfaces import (
     34    IPaymentGatewayService, IPayer, IPayee, IPaymentItem, IPayment,
     35    )
    2536from waeup.ikoba.payments.paypal import (
    2637    get_paypal_config_file_path, parse_paypal_config, get_access_token,
    27     configure_sdk, get_payment,
     38    configure_sdk, get_payment, Payer, PayerInfo, ShippingAddress,
     39    ADDRESS_TYPES_VOCAB, IShippingAddress, Address, IAddress, to_dict,
     40    CreditCard, CREDIT_CARD_TYPES_VOCAB, CREDIT_CARD_STATUS_VOCAB,
     41    ICreditCard, ICreditCardToken, CreditCardToken, FundingInstrument,
     42    AmountDetails, IAmount, Amount, IItem, Item, STOCK_KEEPING_UNITS_VOCAB,
     43    IItemList, ItemList, IPaymentOptions, PaymentOptions, ITransaction,
     44    Transaction, PAYMENT_OPTION_METHODS_VOCAB, PayPalCreditCardService,
     45    PayPalRegularPaymentService,
    2846    )
    2947from waeup.ikoba.testing import (
    3048    FunctionalLayer, FunctionalTestCase,
    3149    )
     50
    3251
    3352#
     
    118137        assert paypalrestsdk.api.__api__.client_id == 'my-special-id'
    119138
     139    def test_get_payment_invalid_intent(self):
     140        # only 'sale' is currently allowed
     141        self.assertRaises(
     142            ValueError, get_payment, intent='invalid')
     143        self.assertRaises(
     144            ValueError, get_payment, intent='order')
     145
     146    def test_to_dict(self):
     147        # we can turn objects into dicts
     148        class C1(object):
     149            a = 1
     150            b = "somestring"
     151            c = u"unicodestring"
     152            d = None
     153        obj = C1()
     154        self.assertEqual(
     155            to_dict(obj),
     156            {'a': u"1",
     157             'b': u"somestring",
     158             'c': u"unicodestring",
     159             }
     160            )
     161
     162    def test_to_dict_map(self):
     163        # we can map attribute names
     164        class C1(object):
     165            a = 1
     166            b = "somestring"
     167        obj = C1()
     168        self.assertEqual(
     169            to_dict(obj, name_map={'a': 'replaced_a'}),
     170            {'replaced_a': u"1",
     171             'b': u"somestring",
     172             })
     173
     174    def test_to_dict_lists(self):
     175        # to_dict can handle lists
     176        class C1(object):
     177            a = 1
     178
     179            def to_dict(self):
     180                return to_dict(self)
     181
     182        obj1 = C1()
     183        obj2 = C1()
     184        obj2.a = 2
     185        obj3 = C1()
     186        obj3.a = 3
     187        obj1.foo = [obj2, obj3]
     188        self.assertEqual(
     189            to_dict(obj1),
     190            {
     191                'a': u'1',
     192                'foo': [
     193                    {'a': u'2'},
     194                    {'a': u'3'},
     195                    ]
     196                }
     197            )
     198
     199    def test_to_dict_decimals(self):
     200        # decimals are converted to numbers with 2 decimals
     201        class C1(object):
     202            a = decimal.Decimal("0.1")
     203        self.assertEqual(
     204            to_dict(C1()),
     205            {
     206                'a': u"0.10",
     207                }
     208            )
     209
     210
     211class PayerTests(unittest.TestCase):
     212
     213    def test_create(self):
     214        # we can create payer objects
     215        payer = Payer(payment_method='paypal')
     216        assert payer.payment_method == 'paypal'
     217        assert payer.funding_instruments == []
     218        assert payer.payer_info is None
     219        assert payer.status is None
     220
     221    def test_init_invalid_payment_meth(self):
     222        # we must provide a valid payment method
     223        payer = Payer(payment_method='paypal')
     224        assert payer is not None
     225        payer = Payer(payment_method='credit_card')
     226        assert payer is not None
     227        self.assertRaises(
     228            ValueError, Payer, payment_method='invalid')
     229
     230    def test_init_invalid_payer_state(self):
     231        # only certain values are allowed as payer states
     232        payer = Payer(payment_method='paypal', status='VERIFIED')
     233        assert payer is not None
     234        payer = Payer(payment_method='paypal', status='UNVERIFIED')
     235        assert payer is not None
     236        self.assertRaises(
     237            ValueError, Payer, payment_method='paypal', status='InVaLiD')
     238
     239
     240class PayerInfoTests(unittest.TestCase):
     241
     242    def test_create(self):
     243        # we can create payer infos
     244        info = PayerInfo()
     245        assert info.email == ''
     246        assert info.first_name == ''
     247        assert info.last_name == ''
     248        assert info.payer_id == ''
     249        assert info.phone == ''
     250        assert info.shipping_address is None
     251        assert info.tax_id_type == ''
     252        assert info.tax_id == ''
     253
     254    def test_init_invalid_tax_id_type(self):
     255        # onyl certain tax id types are allowed
     256        info = PayerInfo(tax_id_type='BR_CPF')
     257        assert info is not None
     258        info = PayerInfo(tax_id_type='BR_CNPJ')
     259        assert info is not None
     260        self.assertRaises(
     261            ValueError, PayerInfo, tax_id_type='INVALID_TYPE')
     262
     263
     264class CreditCardTests(unittest.TestCase):
     265
     266    def test_iface(self):
     267        # we fullfill any interface contracts
     268        credit_card = CreditCard(
     269            number=u"12345678",
     270            credit_card_type="visa",
     271            expire_month=4,
     272            expire_year=2012,
     273            )
     274        verifyClass(ICreditCard, CreditCard)
     275        verifyObject(ICreditCard, credit_card)
     276
     277    def test_create(self):
     278        # we can create CreditCard objects
     279        credit_card = CreditCard(
     280            number=u"12345678",
     281            credit_card_type="visa",
     282            expire_month=4,
     283            expire_year=2012,
     284            )
     285        assert credit_card.paypal_id is None
     286        assert credit_card.payer_id is not None
     287        assert credit_card.number == u"12345678"
     288        assert credit_card.credit_card_type == "visa"
     289        assert credit_card.expire_month == 4
     290        assert credit_card.expire_year == 2012
     291        assert credit_card.cvv2 is None
     292        assert credit_card.first_name is None
     293        assert credit_card.last_name is None
     294        assert credit_card.billing_address is None
     295        assert credit_card.state is None
     296        assert credit_card.paypal_valid_until is None
     297
     298    def test_payer_id_given(self):
     299        # we do not override given payer ids
     300        credit_card = CreditCard(
     301            number=u"12345678",
     302            credit_card_type="visa",
     303            expire_month=4,
     304            expire_year=2012,
     305            payer_id=u'MySpecialPayerId',
     306            )
     307        assert credit_card.payer_id == u'MySpecialPayerId'
     308
     309    def test_payer_id_not_given(self):
     310        # in case no payer id is given, we generate one
     311        credit_card = CreditCard(
     312            number=u"12345678",
     313            credit_card_type="visa",
     314            expire_month=4,
     315            expire_year=2012,
     316            )
     317        # our payer ids contain a leading 'PAYER_' and 32 hex digits
     318        assert re.match('PAYER_[0-9a-f]{32}$', credit_card.payer_id)
     319
     320    def test_number_is_checked(self):
     321        # we do not accept invalid numbers
     322        self.assertRaises(
     323            ValueError, CreditCard,
     324            number=u"not-a-number",
     325            credit_card_type="visa",
     326            expire_month=4,
     327            expire_year=2012,
     328            )
     329
     330    def test_to_str(self):
     331        # we can turn CreditCard objects into dicts.
     332        addr = Address(
     333            line1=u"52 N Main ST",
     334            city=u"Johnstown",
     335            state=u"OH",
     336            postal_code=u"43210",
     337            country_code=u"US")
     338        credit_card = CreditCard(
     339            credit_card_type=u"visa",
     340            payer_id=u"PAYER_0123456789012345678901",
     341            number=u"4417119669820331",
     342            expire_month=11,
     343            expire_year=2018,
     344            cvv2=u"874",
     345            first_name=u"Joe",
     346            last_name=u"Shopper",
     347            billing_address=addr)
     348        self.assertEqual(
     349            credit_card.to_dict(),
     350            {
     351                "type": u"visa",
     352                "number": u"4417119669820331",
     353                "payer_id": u"PAYER_0123456789012345678901",
     354                "expire_month": u"11",
     355                "expire_year": u"2018",
     356                "cvv2": u"874",
     357                "first_name": u"Joe",
     358                "last_name": u"Shopper",
     359                "billing_address": {
     360                    "line1": u"52 N Main ST",
     361                    "city": u"Johnstown",
     362                    "state": u"OH",
     363                    "postal_code": u"43210",
     364                    "country_code": u"US"}
     365                }
     366            )
     367
     368
     369class CreditCardTokenTests(unittest.TestCase):
     370
     371    def test_iface(self):
     372        # we fullfill any interface contracts
     373        token = CreditCardToken(
     374            credit_card_id=u"12345678",
     375            )
     376        verifyClass(ICreditCardToken, CreditCardToken)
     377        verifyObject(ICreditCardToken, token)
     378
     379    def test_create(self):
     380        # we can create CreditCardToken objects
     381        token = CreditCardToken(
     382            credit_card_id=u"12345678",
     383            )
     384        assert token.credit_card_id == u"12345678"
     385        assert token.payer_id is None
     386        assert token.credit_card_type is None
     387        assert token.expire_month is None
     388        assert token.expire_year is None
     389        assert token.last4 is None
     390
     391    def test_payer_id_given(self):
     392        # we do not override given payer ids
     393        token = CreditCardToken(
     394            credit_card_id=u"12345678",
     395            payer_id=u'MySpecialPayerId',
     396            )
     397        assert token.payer_id == u'MySpecialPayerId'
     398
     399    def test_to_str(self):
     400        # we can turn CreditCardToken objects into dicts.
     401        token = CreditCardToken(
     402            credit_card_type=u"visa",
     403            payer_id=u"PAYER_0123456789012345678901",
     404            credit_card_id=u"12345678",
     405            last4="8901",
     406            expire_month=11,
     407            expire_year=2018,
     408            )
     409        self.assertEqual(
     410            token.to_dict(),
     411            {
     412                "credit_card_id": u"12345678",
     413                "payer_id": u"PAYER_0123456789012345678901",
     414                "last4": u"8901",
     415                "type": u"visa",
     416                "expire_month": u"11",
     417                "expire_year": u"2018",
     418                }
     419            )
     420
     421
     422class FundingInstrumentTests(unittest.TestCase):
     423
     424    def test_create(self):
     425        # we can create FundingInstrument objects
     426        token = CreditCardToken(
     427            credit_card_id=u"12345678",
     428            )
     429        instr = FundingInstrument(credit_card_token=token)
     430        assert instr.credit_card_token is token
     431
     432    def test_require_credit_card_or_token(self):
     433        # we require a credit card object or a token.
     434        credit_card = CreditCard(
     435            number=u"12345678",
     436            credit_card_type="visa",
     437            expire_month=4,
     438            expire_year=2012,
     439            )
     440        token = CreditCardToken(
     441            credit_card_id=u"12345678",
     442            )
     443        self.assertRaises(
     444            ValueError, FundingInstrument,
     445            credit_card=credit_card,
     446            credit_card_token=token
     447            )
     448        self.assertRaises(
     449            ValueError, FundingInstrument
     450            )
     451
     452    def test_to_dict(self):
     453        # we can turn Funding instruments into dicts
     454        token = CreditCardToken(
     455            credit_card_type=u"visa",
     456            payer_id=u"PAYER_0123456789012345678901",
     457            credit_card_id=u"12345678",
     458            last4="8901",
     459            expire_month=11,
     460            expire_year=2018,
     461            )
     462        instr = FundingInstrument(credit_card_token=token)
     463        result = instr.to_dict()
     464        self.assertEqual(
     465            result,
     466            {
     467                "credit_card_token": {
     468                    "credit_card_id": u"12345678",
     469                    "payer_id": u"PAYER_0123456789012345678901",
     470                    "last4": u"8901",
     471                    "type": u"visa",
     472                    "expire_month": u"11",
     473                    "expire_year": u"2018",
     474                    }
     475                }
     476            )
     477
     478
     479class AddressTests(unittest.TestCase):
     480
     481    def test_iface(self):
     482        # we fullfill any interface contracts
     483        addr = Address(
     484            line1=u'Address Line 1',
     485            city=u'Somecity',
     486            country_code=u'AT',
     487            )
     488        verifyClass(IAddress, Address)
     489        verifyObject(IAddress, addr)
     490
     491    def test_create(self):
     492        # we can create addresses
     493        addr = Address(
     494            line1=u'Honey Street 1',
     495            city=u'Beartown',
     496            country_code=u'GB',
     497            )
     498        assert addr.line1 == u'Honey Street 1'
     499        assert addr.line2 is None
     500        assert addr.city == u'Beartown'
     501        assert addr.country_code == u'GB'
     502        assert addr.postal_code is None
     503        assert addr.state is None
     504        assert addr.phone is None
     505
     506    def test_to_dict(self):
     507        # we can turn addresses into dicts
     508        addr = Address(
     509            line1=u'Honey Street 1',
     510            city=u'Beartown',
     511            country_code=u'GB',
     512            )
     513        self.assertEqual(
     514            addr.to_dict(),
     515            {
     516                'line1': u'Honey Street 1',
     517                'city': u'Beartown',
     518                'country_code': u'GB',
     519                }
     520            )
     521        addr.line2 = u"Behind little tree"
     522        self.assertEqual(
     523            addr.to_dict(),
     524            {
     525                'line1': u'Honey Street 1',
     526                'line2': u'Behind little tree',
     527                'city': u'Beartown',
     528                'country_code': u'GB',
     529                }
     530            )
     531
     532
     533class ShippingAddressTests(unittest.TestCase):
     534
     535    def test_iface(self):
     536        # we fullfill any interface contracts
     537        addr = ShippingAddress(
     538            recipient_name=u'Foo Bar',
     539            line1=u'Address Line 1',
     540            city=u'Somecity',
     541            country_code=u'AT',
     542            )
     543        verifyClass(IShippingAddress, ShippingAddress)
     544        verifyObject(IShippingAddress, addr)
     545
     546    def test_create(self):
     547        # we can create shipping addresses
     548        addr = ShippingAddress(
     549            recipient_name=u'Rob Receiver',
     550            line1=u'Honey Street 1',
     551            city=u'Beartown',
     552            country_code=u'GB',
     553            )
     554        assert addr.recipient_name == u'Rob Receiver'
     555        assert addr.type == u'residential'
     556        assert addr.line1 == u'Honey Street 1'
     557        assert addr.line2 is None
     558        assert addr.city == u'Beartown'
     559        assert addr.country_code == u'GB'
     560        assert addr.postal_code is None
     561        assert addr.state is None
     562        assert addr.phone is None
     563
     564    def test_to_dict(self):
     565        # we can turn shipping addresses into dicts
     566        addr = ShippingAddress(
     567            recipient_name=u'Rob Receiver',
     568            line1=u'Honey Street 1',
     569            city=u'Beartown',
     570            country_code=u'GB',
     571            )
     572        self.assertEqual(
     573            addr.to_dict(),
     574            {
     575                'recipient_name': u'Rob Receiver',
     576                'type': u'residential',
     577                'line1': u'Honey Street 1',
     578                'city': u'Beartown',
     579                'country_code': u'GB',
     580                }
     581            )
     582        addr.line2 = u"Behind little tree"
     583        self.assertEqual(
     584            addr.to_dict(),
     585            {
     586                'recipient_name': u'Rob Receiver',
     587                'type': u'residential',
     588                'line1': u'Honey Street 1',
     589                'line2': u'Behind little tree',
     590                'city': u'Beartown',
     591                'country_code': u'GB',
     592                }
     593            )
     594
     595
     596class AmountDetailsTests(unittest.TestCase):
     597
     598    def test_create(self):
     599        # we can create AmountDetail objects
     600        details = AmountDetails()
     601        assert details.shipping is None
     602        assert details.subtotal is None
     603        assert details.tax is None
     604        assert details.fee is None
     605        assert details.handling_fee is None
     606        assert details.insurance is None
     607        assert details.shipping_discount is None
     608
     609    def test_to_dict(self):
     610        # we can turn AmountDetails into a dict
     611        details = AmountDetails(
     612            shipping=decimal.Decimal("0.10"),
     613            tax=decimal.Decimal("0.30"),
     614            fee=decimal.Decimal("0.40"),
     615            handling_fee=decimal.Decimal("0.50"),
     616            insurance=decimal.Decimal("0.60"),
     617            shipping_discount=decimal.Decimal("0.70")
     618            )
     619        self.assertEqual(
     620            details.to_dict(),
     621            {
     622                'shipping': u"0.10",
     623                'subtotal': u"1.20",
     624                'tax': u"0.30",
     625                'fee': u"0.40",
     626                'handling_fee': u"0.50",
     627                'insurance': u"0.60",
     628                'shipping_discount': u"0.70"
     629                }
     630            )
     631
     632    def test_subtotal_all_none(self):
     633        # if all items are none, also subtotal is none
     634        details = AmountDetails(
     635            shipping=None, tax=None, fee=None, handling_fee=None,
     636            insurance=None, shipping_discount=None,
     637            )
     638        assert details.subtotal is None
     639        details.shipping_discount = decimal.Decimal("1.00")
     640        assert details.subtotal == decimal.Decimal("-1.00")
     641
     642    def test_subtotal_sum(self):
     643        # subtotal sums up correctly
     644        details = AmountDetails(
     645            shipping=decimal.Decimal("0.05"),
     646            tax=decimal.Decimal("0.40"),
     647            fee=decimal.Decimal("3.00"),
     648            handling_fee=decimal.Decimal("20.00"),
     649            insurance=decimal.Decimal("100.00"),
     650            shipping_discount=None
     651            )
     652        self.assertEqual(details.subtotal, decimal.Decimal("123.45"))
     653        details.shipping_discount = decimal.Decimal("0.00")
     654        self.assertEqual(details.subtotal, decimal.Decimal("123.45"))
     655        details.shipping_discount = decimal.Decimal("23.45")
     656        self.assertEqual(details.subtotal, decimal.Decimal("100.00"))
     657
     658
     659class AmountTests(unittest.TestCase):
     660
     661    def test_iface(self):
     662        # we fullfill any interface contracts.
     663        amount = Amount()
     664        verifyClass(IAmount, Amount)
     665        verifyObject(IAmount, amount)
     666
     667    def test_create(self):
     668        # we can create amount objects
     669        details = AmountDetails(
     670            shipping=decimal.Decimal("0.05"),
     671            tax=decimal.Decimal("0.40"),
     672            fee=decimal.Decimal("3.00"),
     673            handling_fee=decimal.Decimal("20.00"),
     674            insurance=decimal.Decimal("100.00"),
     675            shipping_discount=None
     676            )
     677        amount = Amount(
     678            total=decimal.Decimal("12.12"),
     679            currency="USD",
     680            details=details
     681            )
     682        assert amount.total == decimal.Decimal("12.12")
     683        assert amount.currency == "USD"
     684        assert amount.details is details
     685
     686    def test_to_dict(self):
     687        # we can turn Amount objects into dicts
     688        self.maxDiff = None
     689        details = AmountDetails(
     690            shipping=decimal.Decimal("0.05"),
     691            tax=decimal.Decimal("0.40"),
     692            fee=decimal.Decimal("3.00"),
     693            handling_fee=decimal.Decimal("20.00"),
     694            insurance=decimal.Decimal("100.00"),
     695            shipping_discount=None
     696            )
     697        amount = Amount(
     698            total=decimal.Decimal("12.12"),
     699            currency="USD",
     700            details=details
     701            )
     702        self.assertEqual(
     703            amount.to_dict(),
     704            {
     705                'total': u'12.12',
     706                'currency': u'USD',
     707                'details': {
     708                    'shipping': u'0.05',
     709                    'subtotal': u'123.45',
     710                    'tax': u'0.40',
     711                    'fee': u'3.00',
     712                    'handling_fee': u'20.00',
     713                    'insurance': u'100.00',
     714                    }
     715                }
     716            )
     717
     718
     719class ItemTests(unittest.TestCase):
     720
     721    def test_iface(self):
     722        # we fullfill all interface contracts
     723        item = Item(name=u"Splendid Item")
     724        verifyClass(IItem, Item)
     725        verifyObject(IItem, item)
     726
     727    def test_create(self):
     728        # we can create Item objects
     729        item = Item(
     730            quantity=3,
     731            name=u"Splendid Thing",
     732            price=decimal.Decimal("1.1"),
     733            currency="USD",
     734            sku="pcs",
     735            description=u"Soo splendid!",
     736            tax=decimal.Decimal("0.1"),
     737            )
     738        assert item.quantity == 3
     739        assert item.name == u"Splendid Thing"
     740        assert item.price == decimal.Decimal("1.1")
     741        assert item.currency == "USD"
     742        assert item.sku == "pcs"
     743        assert item.description == u"Soo splendid!"
     744        assert item.tax == decimal.Decimal("0.1")
     745
     746    def test_to_dict(self):
     747        # we can turn Item objects into dicts
     748        item = Item(
     749            quantity=3,
     750            name=u"Splendid Thing",
     751            price=decimal.Decimal("1.1"),
     752            currency="USD",
     753            sku="pcs",
     754            description=u"Soo splendid!",
     755            tax=decimal.Decimal("0.1"),
     756            )
     757        self.assertEqual(
     758            item.to_dict(),
     759            {
     760                "quantity": u"3",
     761                "name": u"Splendid Thing",
     762                "price": u"1.10",
     763                "currency": u"USD",
     764                "sku": u"pcs",
     765                "description": u"Soo splendid!",
     766                "tax": u"0.10"
     767                }
     768            )
     769
     770
     771class ItemListTests(unittest.TestCase):
     772
     773    def test_iface(self):
     774        # we fullfill all interface contracts
     775        item_list = ItemList()
     776        verifyClass(IItemList, ItemList)
     777        verifyObject(IItemList, item_list)
     778
     779    def test_create_minimal(self):
     780        # we can create ItemLists with a minimum of params
     781        item_list = ItemList()
     782        assert item_list.shipping_address is None
     783        assert item_list.items == []
     784
     785    def test_create(self):
     786        # we can create ItemLists
     787        item1 = Item(
     788            name=u"Splendid Thing",
     789            )
     790        item2 = Item(
     791            name=u"Other Splendid Thing",
     792            )
     793        addr = ShippingAddress(
     794            recipient_name=u'Rob Receiver',
     795            line1=u'Honey Street 1',
     796            city=u'Beartown',
     797            country_code=u'GB',
     798            )
     799        item_list = ItemList(
     800            shipping_address=addr,
     801            items=[item1, item2])
     802        assert item_list.shipping_address is addr
     803        assert item_list.items == [item1, item2]
     804
     805    def test_to_dict(self):
     806        # we can turn ITemLists into dicts
     807        item = Item(
     808            quantity=3,
     809            name=u"Splendid Thing",
     810            price=decimal.Decimal("1.1"),
     811            currency="USD",
     812            sku="pcs",
     813            description=u"Soo splendid!",
     814            tax=decimal.Decimal("0.1"),
     815            )
     816        addr = ShippingAddress(
     817            recipient_name=u'Rob Receiver',
     818            line1=u'Honey Street 1',
     819            city=u'Beartown',
     820            country_code=u'GB',
     821            )
     822        item_list = ItemList(items=[item, ], shipping_address=addr)
     823        self.assertEqual(
     824            item_list.to_dict(),
     825            {
     826                "items": [
     827                    {
     828                        "quantity": u"3",
     829                        "name": u"Splendid Thing",
     830                        "price": u"1.10",
     831                        "currency": u"USD",
     832                        "sku": u"pcs",
     833                        "description": u"Soo splendid!",
     834                        "tax": u"0.10"
     835                        }
     836                    ],
     837                "shipping_address":
     838                {
     839                    'recipient_name': u'Rob Receiver',
     840                    'type': u'residential',
     841                    'line1': u'Honey Street 1',
     842                    'city': u'Beartown',
     843                    'country_code': u'GB',
     844                    }
     845                }
     846            )
     847
     848
     849class PaymentOptionsTests(unittest.TestCase):
     850
     851    def test_iface(self):
     852        # we fullfill all interface contracts
     853        opts = PaymentOptions()
     854        verifyClass(IPaymentOptions, PaymentOptions)
     855        verifyObject(IPaymentOptions, opts)
     856
     857    def test_create(self):
     858        # we can create PaymentOptions objects
     859        opts = PaymentOptions()
     860        assert opts.allowed_payment_method is None
     861
     862    def test_allowed_payment_method_checked_in_init(self):
     863        # any value apart from None, INSTANT... is rejected in __init__
     864        self.assertRaises(
     865            ValueError,
     866            PaymentOptions, allowed_payment_method='NoTvAlID')
     867
     868    def test_to_dict(self):
     869        # we can turn PaymentOptions into dicts
     870        opts = PaymentOptions(
     871            allowed_payment_method="INSTANT_FUNDING_SOURCE")
     872        self.assertEqual(
     873            opts.to_dict(),
     874            {
     875                'allowed_payment_method': "INSTANT_FUNDING_SOURCE",
     876                }
     877            )
     878
     879
     880class TransactionTests(unittest.TestCase):
     881
     882    def test_iface(self):
     883        # we fullfill all interface contracts
     884        amount = Amount()
     885        transaction = Transaction(amount=amount)
     886        verifyClass(ITransaction, Transaction)
     887        verifyObject(ITransaction, transaction)
     888
     889    def test_create(self):
     890        # we can create transacions
     891        amount = Amount()
     892        transaction = Transaction(amount=amount)
     893        assert transaction.amount is amount
     894        assert transaction.description is None
     895        assert transaction.item_list is None
     896        assert transaction.related_resources == []
     897        assert transaction.invoice_number is None
     898        assert transaction.custom is None
     899        assert transaction.soft_descriptor is None
     900        assert transaction.payment_options is None
     901
     902    def test_to_dict(self):
     903        # we can turn Transaction objects into dicts
     904        transaction = Transaction(
     905            amount=Amount(),
     906            description=u"My description",
     907            item_list=ItemList(),
     908            related_resources=[],
     909            invoice_number=u"12345",
     910            custom=u"Some custom remark",
     911            soft_descriptor=u"softdescriptor?",
     912            payment_options=PaymentOptions(
     913                allowed_payment_method="INSTANT_FUNDING_SOURCE"),
     914            )
     915        self.assertEqual(
     916            transaction.to_dict(), {
     917                'amount': {'currency': u'USD', 'total': u'0.00'},
     918                'custom': u'Some custom remark',
     919                'description': u'My description',
     920                'invoice_number': u'12345',
     921                'item_list': {
     922                    'items': []
     923                    },
     924                'payment_options': {
     925                    'allowed_payment_method': u'INSTANT_FUNDING_SOURCE'
     926                    },
     927                'related_resources': [],
     928                'soft_descriptor': u'softdescriptor?'
     929                }
     930            )
     931
     932
     933class AddressTypesVocabTests(unittest.TestCase):
     934
     935    def test_address_types_vocab_tokenized(self):
     936        # we can get a countries source suitable for forms etc.
     937        verifyObject(IVocabularyTokenized, ADDRESS_TYPES_VOCAB)
     938
     939    def test_address_types_vocab_i18nized(self):
     940        # vocab titles are i18nized
     941        result = ADDRESS_TYPES_VOCAB.getTerm('residential')
     942        assert ITerm.providedBy(result)
     943        self.assertEqual(result.title, u'residential')
     944        assert isinstance(result.title, i18nMessage)
     945
     946    def test_address_types_vocab_tokens_are_string(self):
     947        # vocab tokens are simple strings
     948        result = ADDRESS_TYPES_VOCAB.getTerm('residential')
     949        assert ITerm.providedBy(result)
     950        assert result.token == result.value
     951        assert result.value == 'residential'
     952        assert isinstance(result.token, str)
     953        assert isinstance(result.value, str)
     954
     955
     956class CreditCardTypesVocabTests(unittest.TestCase):
     957
     958    def test_credit_card_types_vocab_tokenized(self):
     959        # we can get a countries source suitable for forms etc.
     960        verifyObject(IVocabularyTokenized, CREDIT_CARD_TYPES_VOCAB)
     961
     962    def test_credit_cards_types_vocab_i18nized(self):
     963        # vocab titles are i18nized
     964        result = CREDIT_CARD_TYPES_VOCAB.getTerm('visa')
     965        assert ITerm.providedBy(result)
     966        self.assertEqual(result.title, u'visa')
     967        assert isinstance(result.title, i18nMessage)
     968
     969    def test_credit_cards_types_vocab_tokens_are_string(self):
     970        # vocab tokens are simple strings
     971        result = CREDIT_CARD_TYPES_VOCAB.getTerm('visa')
     972        assert ITerm.providedBy(result)
     973        assert result.token == result.value
     974        assert result.value == 'visa'
     975        assert isinstance(result.token, str)
     976        assert isinstance(result.value, str)
     977
     978
     979class CreditcardstatusVocabTests(unittest.TestCase):
     980
     981    def test_credit_card_status_vocab_tokenized(self):
     982        # we can get a countries source suitable for forms etc.
     983        verifyObject(IVocabularyTokenized, CREDIT_CARD_STATUS_VOCAB)
     984
     985    def test_credit_cards_status_vocab_i18nized(self):
     986        # vocab titles are i18nized
     987        result = CREDIT_CARD_STATUS_VOCAB.getTerm('ok')
     988        assert ITerm.providedBy(result)
     989        self.assertEqual(result.title, u'ok')
     990        assert isinstance(result.title, i18nMessage)
     991
     992    def test_credit_cards_status_vocab_tokens_are_string(self):
     993        # vocab tokens are simple strings
     994        result = CREDIT_CARD_STATUS_VOCAB.getTerm('expired')
     995        assert ITerm.providedBy(result)
     996        assert result.token == result.value
     997        assert result.value == 'expired'
     998        assert isinstance(result.token, str)
     999        assert isinstance(result.value, str)
     1000
     1001
     1002class StockKeepingUnitsVocabTests(unittest.TestCase):
     1003
     1004    def test_sku_vocab_tokenized(self):
     1005        # we can get a tokenzed vocab for stock keeping units
     1006        verifyObject(IVocabularyTokenized, STOCK_KEEPING_UNITS_VOCAB)
     1007
     1008    def test_sku_vocab_i18nized(self):
     1009        # vocab titles are i18nized
     1010        result = STOCK_KEEPING_UNITS_VOCAB.getTerm('pcs')
     1011        assert ITerm.providedBy(result)
     1012        self.assertEqual(result.title, u'pieces')
     1013        assert isinstance(result.title, i18nMessage)
     1014
     1015    def test_sku_vocab_tokens_are_string(self):
     1016        # vocab tokens are simple strings
     1017        result = STOCK_KEEPING_UNITS_VOCAB.getTerm('pcs')
     1018        assert ITerm.providedBy(result)
     1019        assert result.token == result.value
     1020        assert result.value == 'pcs'
     1021        assert isinstance(result.token, str)
     1022        assert isinstance(result.value, str)
     1023
     1024
     1025class PaymentOptionMethodsVocabTests(unittest.TestCase):
     1026
     1027    def test_payment_option_methods_vocab_tokenized(self):
     1028        # we can get a countries source suitable for forms etc.
     1029        verifyObject(IVocabularyTokenized, PAYMENT_OPTION_METHODS_VOCAB)
     1030
     1031    def test_payment_option_methods_vocab_i18nized(self):
     1032        # vocab titles are i18nized
     1033        result = PAYMENT_OPTION_METHODS_VOCAB.getTerm('INSTANT_FUNDING_SOURCE')
     1034        assert ITerm.providedBy(result)
     1035        self.assertEqual(result.title, u'INSTANT_FUNDING_SOURCE')
     1036        assert isinstance(result.title, i18nMessage)
     1037
     1038    def test_payment_option_methods_vocab_tokens_are_string(self):
     1039        # vocab tokens are simple strings
     1040        result = PAYMENT_OPTION_METHODS_VOCAB.getTerm('INSTANT_FUNDING_SOURCE')
     1041        assert ITerm.providedBy(result)
     1042        assert result.token == result.value
     1043        assert result.value == 'INSTANT_FUNDING_SOURCE'
     1044        assert isinstance(result.token, str)
     1045        assert isinstance(result.value, str)
     1046
     1047
     1048class FakePayer(object):
     1049
     1050    implements(IPayer)
     1051
     1052    payer_id = 'PAYER-123'
     1053
     1054
     1055class FakePayee(object):
     1056
     1057     implements(IPayee)
     1058
     1059     payee_id = 'PAYEE-456'
     1060
     1061
     1062class FakePaymentItem(object):
     1063
     1064    implements(IPaymentItem)
     1065
     1066    item_id = 'BILL-123456'
     1067    amount = decimal.Decimal("12.10")
     1068    currency = 'EUR'
     1069
     1070
     1071class PayPalCreditCardServiceTests(unittest.TestCase):
     1072
     1073    def test_iface(self):
     1074        # we fullfill all interface contracts
     1075        service = PayPalCreditCardService()
     1076        verifyClass(IPaymentGatewayService, PayPalCreditCardService)
     1077        verifyObject(IPaymentGatewayService, service)
     1078
     1079    def DIStest_creditcard_service_can_create_payment(self):
     1080        # we can create IPayment objects with creditcard service
     1081        service = PayPalCreditCardService()
     1082        payment = service.create_payment(
     1083            payer=FakePayer(),
     1084            payment_item=FakePaymentItem(),
     1085            payee=FakePayee()
     1086            )
     1087        assert IPayment.providedBy(payment)
     1088
    1201089
    1211090class FunctionalPaypalTests(FunctionalTestCase):
     
    1361105        result = payment.create()
    1371106        assert result is True
     1107
     1108    def test_paypal_services_registered(self):
     1109        # the PayPal gateway services are all registered
     1110        creditcard_service = queryUtility(
     1111            IPaymentGatewayService, name=u'paypal_creditcard')
     1112        assert creditcard_service is not None
     1113        assert isinstance(creditcard_service, PayPalCreditCardService)
     1114        paypal_regular_service = queryUtility(
     1115            IPaymentGatewayService, name=u'paypal_regular')
     1116        assert paypal_regular_service is not None
     1117        assert isinstance(paypal_regular_service, PayPalRegularPaymentService)
Note: See TracChangeset for help on using the changeset viewer.