## $Id: paypal.py 12741 2015-03-12 05:29:43Z uli $
##
## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
##
"""Support for PayPal payments.
"""
import decimal
import grok
import ConfigParser
import inspect
import paypalrestsdk
import re
import uuid
from zope import schema
from zope.component import queryUtility
from zope.container.interfaces import IContainer
from zope.interface import Interface, Attribute
from waeup.ikoba.interfaces import MessageFactory as _
from waeup.ikoba.interfaces import IPayPalConfig, SimpleIkobaVocabulary
from waeup.ikoba.utils.helpers import attrs_to_fields
from waeup.ikoba.payments.interfaces import (
    IPayment, IPaymentGatewayService, IPayer, IPaymentItem, IPayee,
    )
from waeup.ikoba.payments.payment import PaymentProviderServiceBase
from waeup.ikoba.payments.paypal_countries import COUNTRIES_VOCAB
from waeup.ikoba.payments.paypal_currencies import CURRENCIES_VOCAB

#: Intents allowed for paypal based payments
PAYMENT_INTENTS = ('sale', 'authorize', 'order')

#: Payment methods allowed by PayPal
PAYMENT_METHODS = ('credit_card', 'paypal')

#: Payer status set/accepted by PayPal
PAYER_STATUS = ('VERIFIED', 'UNVERIFIED')

#: Tax ID types accepted by PayPal (yes, this list is complete)
TAX_ID_TYPES = ('BR_CPF', 'BR_CNPJ')

#: Address types accepted by PayPal
ADDRESS_TYPES = ('residential', 'business', 'mailbox')

#: A vocabulary with address types
ADDRESS_TYPES_VOCAB = SimpleIkobaVocabulary(
    *[(_(x), x) for x in ADDRESS_TYPES])

#: Credit card types accepted by PayPal
CREDIT_CARD_TYPES = ('visa', 'mastercard', 'discover', 'amex')

CREDIT_CARD_TYPES_VOCAB = SimpleIkobaVocabulary(
    *[(_(x), x) for x in CREDIT_CARD_TYPES])

#: Credit card status accepted by PayPal
CREDIT_CARD_STATUS = ('expired', 'ok')

CREDIT_CARD_STATUS_VOCAB = SimpleIkobaVocabulary(
    *[(_(x), x) for x in CREDIT_CARD_STATUS])

#: Stock keeping units we support.
STOCK_KEEPING_UNITS = {
    "pcs": _("pieces"),
    "license": _("license"),
    }

STOCK_KEEPING_UNITS_VOCAB = SimpleIkobaVocabulary(
    *[(value, key) for key, value in STOCK_KEEPING_UNITS.items()])

#: Payment methods (flags) used by PayPal
PAYMENT_OPTION_METHODS = ('INSTANT_FUNDING_SOURCE', )
PAYMENT_OPTION_METHODS_VOCAB = SimpleIkobaVocabulary(
    *[(_(x), x) for x in PAYMENT_OPTION_METHODS])


def to_dict(obj, name_map={}):
    """Turn `obj` into some dict representation.
    """
    result = dict()
    for name in dir(obj):
        if name.startswith('_'):
            continue
        value = getattr(obj, name)
        if value is None:
            continue
        if inspect.ismethod(value):
            continue
        if hasattr(value, 'to_dict'):
            value = value.to_dict()
        elif isinstance(value, list):
            value = [x.to_dict() for x in value]
        elif isinstance(value, decimal.Decimal):
            value = u"%.2f" % (value, )
        else:
            value = unicode(value)
        name = name_map.get(name, name)
        result[name] = value
    return result


def parse_paypal_config(path):
    """Get paypal credentials from config file.

    Returns a dict with following keys set:

      ``client_id``, ``client_secret``, ``mode``.

    Please note, that values might be `None`.
    """
    parser = ConfigParser.SafeConfigParser(
        {'id': None, 'secret': None, 'mode': 'sandbox', }
        )
    parser.readfp(open(path))
    return {
        'client_id': parser.get('rest-client', 'id', None),
        'client_secret': parser.get('rest-client', 'secret', None),
        'mode': parser.get('rest-client', 'mode', 'sandbox')
        }


def get_paypal_config_file_path():
    """Get the path of the configuration file (if any).

    If no such file is registered, we get `None`.
    """
    util = queryUtility(IPayPalConfig)
    if util is None:
        return None
    return util['path']


def configure_sdk(path):
    """Configure the PayPal SDK with values from config in `path`.

    Parse paypal configuration from file in `path` and set
    configuration in SDK accordingly.

    Returns a dict with values from config file and path set.

    This function will normally be called during start-up and only
    this one time.

    It is neccessary to authorize later calls to other part of the
    PayPal API.
    """
    conf = parse_paypal_config(path)
    paypalrestsdk.configure({
        'mode': conf['mode'],
        'client_id': conf['client_id'],
        'client_secret': conf['client_secret'],
        })
    conf['path'] = path
    return conf


def get_access_token():
    """Get an access token for further calls.
    """
    conf = parse_paypal_config(get_paypal_config_file_path())
    api = paypalrestsdk.set_config(
        mode=conf['mode'],  # sandbox or live
        client_id=conf['client_id'],
        client_secret=conf['client_secret'],
        )
    return api.get_access_token()


class Payer(object):
    """A payer as required in paypal payments.

    According to Paypal docs:

    `payment_method` must be one of ``'paypal'`` or ``'credit_card'``
    as stored in `PAYMENT_METHODS`.

    `funding_instruments` is a list of `FundingInstrument` objects. I
    think the list must be empty for ``paypal`` payments and must
    contain at least one entry for ``credit_card`` payments.

    `payer_info` must be provided for ``paypal`` payments and might be
    provided for ``credit_card`` payments. It's a `PayerInfo` object.

    `status` reflects the payer's PayPal account status and is
    currently supported for ``paypal`` payments. Allowed values are
    ``'VERIFIED'`` and ``'UNVERIFIED'`` as stored in `PAYER_STATUS`.
    """
    def __init__(self, payment_method, funding_instruments=[],
                 payer_info=None, status=None):
        if payment_method not in PAYMENT_METHODS:
            raise ValueError(
                "Invalid payment method: use one of %s" %
                (PAYMENT_METHODS, )
                )
        if status and status not in PAYER_STATUS:
            raise ValueError(
                "Invalid status: use one of %s" % (PAYER_STATUS, )
                )
        self.payment_method = payment_method
        self.funding_instruments = funding_instruments
        self.payer_info = payer_info
        self.status = status


class PayerInfo(object):
    """Payer infos as required by Paypal payers.

    Normally used with a `Payer` instance (which in turn is part of a
    `payment`).

    According to PayPal docs:

    Pre-filled by PayPal when `payment_method` is ``'paypal'``.

    `email`: 127 chars max. I don't think, this value is pre-filled.

    `first_name`: First name of payer. Assigned by PayPal.

    `last_naem`: Last name of payer. Assigned by PayPal.

    `payer_id`: Payer ID as assigned by PayPal. Do not mix up with any
    Ikoba Payer IDs.

    `phone`: Phone number representing the payer. 20 chars max.

    `shipping_address`: a shipping address object of payer. Assigned
    by PayPal.

    `tax_id_type`: Payer's tax ID type. Allowed values: ``'BR_CPF'``,
    ``'BR_CNPJ'`` (I have not the slightest clue what that means).
    Supported (PayPal-wise) with ``paypal`` payment method only (not
    with ``credit_card``).

    `tax_id`: Payer's tax ID. Here the same as for `tax_id_type`
    applies (except that also other values are accepted).

    By default all values are set to the empty string and shipping
    address to `None`.

    See also: :class:`Payer`
    """
    def __init__(self, email='', first_name='', last_name='',
                 payer_id='', phone='', shipping_address=None,
                 tax_id_type='', tax_id=''):
        if tax_id_type and tax_id_type not in TAX_ID_TYPES:
            raise ValueError(
                "Invalid tax id type: use one of %s" %
                (TAX_ID_TYPES, )
                )
        self.email = email
        self.first_name = first_name
        self.last_name = last_name
        self.payer_id = payer_id
        self.phone = phone
        self.shipping_address = shipping_address
        self.tax_id_type = tax_id_type
        self.tax_id = tax_id


class FundingInstrument(object):
    """Representation of a payer's funding instrument as required by PayPal.

    Represents always a credit card. Either by a complete set of
    credit card infos or by a credit card token, which contains only a
    limited set of credit card data and represents a full set stored
    in PayPal vault.
    """

    def __init__(self, credit_card=None, credit_card_token=None):
        if credit_card is None and credit_card_token is None:
            raise ValueError(
                "Must provide credit card data or a token")
        if credit_card is not None and credit_card_token is not None:
            raise ValueError(
                "Must provide credit card data *or* a token, not both.")
        self.credit_card = credit_card
        self.credit_card_token = credit_card_token

    def to_dict(self):
        return to_dict(self)


class ICreditCard(Interface):
    """A credit card (full data set) as accepted by PayPal.
    """

    paypal_id = schema.TextLine(
        title=u'PayPal ID',
        description=u'ID of the credit card provided by PayPal.',
        required=False,
        )

    external_customer_id = schema.TextLine(
        title=u'Payer ID',
        description=(u'A unique identifier for the credit card. This '
                     u'identifier is provided by Ikoba and must be set '
                     u'with all future transactions, once put into '
                     u'PayPal vault.'),
        required=True,
        )

    number = schema.TextLine(
        title=u'Credit Card Number',
        description=u'Numeric characters only w/o spaces or punctuation',
        required=True,
        )

    credit_card_type = schema.Choice(
        title=u'Credit Card Type',
        description=u'Credit card types supported by PayPal.',
        required=True,
        source=CREDIT_CARD_TYPES_VOCAB,
        )

    expire_month = schema.Int(
        title=u'Expiration Month',
        description=u"Month, the credit card expires.",
        required=True,
        default=1,
        min=1,
        max=12,
        )

    expire_year = schema.Int(
        title=u'Expiration Year',
        description=u'4-digit expiration year.',
        required=True,
        default=2020,
        min=1900,
        max=9999,
        )

    cvv2 = schema.TextLine(
        title=u'CVV2',
        description=u'3-4 digit card validation code',
        required=False,
        )

    first_name = schema.TextLine(
        title=u'First Name',
        description=u"Cardholder's first name.",
        required=False,
        )

    last_name = schema.TextLine(
        title=u'Last Name',
        description=u"Cardholder's last name.",
        required=False,
        )

    billing_address = Attribute(
        "Billing address associated with card.")

    state = schema.Choice(
        title=u"Status",
        description=(u"Status of credit card funding instrument. "
                     u"Value assigned by PayPal."
                     ),
        required=False,
        source=CREDIT_CARD_STATUS_VOCAB,
        default=None,
        )

    valid_unti = schema.TextLine(
        title=u'Valid until',
        description=(u'Funding instrument expiratiopn date, '
                     u'assigned by PayPal'),
        required=False,
        )


@attrs_to_fields
class CreditCard(object):
    """A credit card (full info set) as used by PayPal.

    Normally used with a `FundingInstrument` instance.

    According to PayPal docs:

    `paypal_id`: provided by PayPal when storing credit card
          data. Required if using a stored credit card.

    `external_customer_id`: Unique identifier. If none is given, we
          assign a uuid. The uuid reads 'PAYER_<32 hex digits>'.

    `number`: Credit card number. Numeric characters only with no
          spaces or punctuation. The string must conform with modulo
          and length required by each credit card type. Redacted in
          responses. Required.

    `credit_card_type`: One of ``'visa'``, ``'mastercard'``,
          ``'discover'``, ``'amex'``. Required.

    `expire_month`: Expiration month. A number from 1 through
          12. Required.

    `expire_year`: 4-digit expiration year. Required.

    `cvv2`: 3-4 digit card validation code.

    `first_name`: card holders first name.

    `last_name`: card holders last name.

    `billing_address`: Billing address associated with card. A
      `Billing` instance.

    `state`: state of the credit card funding instrument. Valid values
      are ``'expired'`` and ``'ok'``. *Value assigned by PayPal.*

    `paypal_valid_until`: Funding instrument expiration date.
       *Value assigned by PayPal.* Not to confuse with the credit cards
       expiration date, which is set via `expire_month` and
       `expire_year`.

    """
    grok.implements(ICreditCard)

    def __init__(self, paypal_id=None, external_customer_id=None,
                 number=None, credit_card_type=None, expire_month=1,
                 expire_year=2000, cvv2=None, first_name=None,
                 last_name=None, billing_address=None, state=None,
                 paypal_valid_until=None):
        if not re.match('^[0-9]+$', number):
            raise ValueError("Credit card number may "
                             "not contain non-numbers.")
        if external_customer_id is None:
            external_customer_id = u'PAYER_' + unicode(uuid.uuid4().hex)
        self.paypal_id = paypal_id
        self.external_customer_id = external_customer_id
        self.number = number
        self.credit_card_type = credit_card_type
        self.expire_month = expire_month
        self.expire_year = expire_year
        self.cvv2 = cvv2
        self.first_name = first_name
        self.last_name = last_name
        self.billing_address = billing_address
        self.state = state
        self.paypal_valid_until = paypal_valid_until

    def to_dict(self):
        return to_dict(self, name_map={'credit_card_type': 'type'})


class ICreditCardToken(Interface):
    """A credit card token corresponding to a credit card stored with PayPal.
    """
    credit_card_id = schema.TextLine(
        title=u"Credit Card ID",
        description=u"ID if credit card previously stored with PayPal",
        required=True,
        )

    external_customer_id = schema.TextLine(
        title=u'Payer ID',
        description=(u'A unique identifier for the credit card. This '
                     u'identifier is provided by Ikoba and must be set '
                     u'with all future transactions, once put into '
                     u'PayPal vault.'),
        required=True,
        )

    last4 = schema.TextLine(
        title=u"Credit Card's last 4 numbers",
        description=(
            u"Last four digits of the stored credit card number. "
            u"Value assigned by PayPal."),
        required=False,
        min_length=4,
        max_length=4
        )

    credit_card_type = schema.Choice(
        title=u'Credit Card Type',
        description=(
            u'Credit card type supported by PayPal. Value assigned '
            u'by PayPal.'),
        required=False,
        source=CREDIT_CARD_TYPES_VOCAB,
        )

    expire_month = schema.Int(
        title=u'Expiration Month',
        description=u"Month, the credit card expires. Assigned by PayPal.",
        required=True,
        default=1,
        min=1,
        max=12,
        )

    expire_year = schema.Int(
        title=u'Expiration Year',
        description=u'4-digit expiration year. Assigned by PayPal.',
        required=True,
        default=2020,
        min=1900,
        max=9999,
        )


class CreditCardToken(object):
    grok.implements(ICreditCardToken)

    def __init__(self, credit_card_id, external_customer_id=None, last4=None,
                 credit_card_type=None, expire_month=None, expire_year=None):
        self.credit_card_id = credit_card_id
        self.external_customer_id = external_customer_id
        self.last4 = last4
        self.credit_card_type = credit_card_type
        self.expire_month = expire_month
        self.expire_year = expire_year

    def to_dict(self):
        return to_dict(self, name_map={'credit_card_type': 'type'})


class IShippingAddress(Interface):
    """A shipping address as accepted by PayPal.
    """
    recipient_name = schema.TextLine(
        title=u'Recipient Name',
        required=True,
        description=u'Name of the recipient at this address.',
        max_length=50,
        )

    type = schema.Choice(
        title=u'Address Type',
        description=u'Address Type.',
        required=False,
        source=ADDRESS_TYPES_VOCAB,
        )

    line1 = schema.TextLine(
        title=u'Address Line 1',
        required=True,
        description=u'Line 1 of the address (e.g., Number, street, etc.)',
        max_length=100,
        )

    line2 = schema.TextLine(
        title=u'Address Line 2',
        required=False,
        description=u'Line 2 of the address (e.g., Suite, apt #, etc.)',
        max_length=100,
        )

    city = schema.TextLine(
        title=u'City',
        required=True,
        description=u'City name',
        max_length=50,
        )

    country_code = schema.Choice(
        title=u'Country',
        required=True,
        description=u'2-letter country code',
        source=COUNTRIES_VOCAB,
        )

    postal_code = schema.TextLine(
        title=u'Postal code',
        required=False,
        description=(u'Zip code or equivalent is usually required '
                     u'for countries that have them.'),
        max_length=20,
        )

    state = schema.TextLine(
        title=u'State',
        required=False,
        description=(u'2-letter code for US stated, and the '
                     u'equivalent for other countries.'),
        max_length=100,
        )

    phone = schema.TextLine(
        title=u'Phone',
        required=False,
        description=u'Phone number in E.123 format.',
        max_length=50,
        )


@attrs_to_fields
class ShippingAddress(object):
    """A shipping address as used in PayPal transactions.
    """
    grok.implements(IShippingAddress)

    def __init__(self, recipient_name, type='residential', line1='',
                 line2=None, city='', country_code=None,
                 postal_code=None, state=None, phone=None):
        self.recipient_name = recipient_name
        self.type = type
        self.line1 = line1
        self.line2 = line2
        self.city = city
        self.country_code = country_code
        self.postal_code = postal_code
        self.state = state
        self.phone = phone

    def to_dict(self):
        return to_dict(self)


class IAddress(Interface):
    """An address as accepted by PayPal.
    """
    line1 = schema.TextLine(
        title=u'Address Line 1',
        required=True,
        description=u'Line 1 of the address (e.g., Number, street, etc.)',
        max_length=100,
        )

    line2 = schema.TextLine(
        title=u'Address Line 2',
        required=False,
        description=u'Line 2 of the address (e.g., Suite, apt #, etc.)',
        max_length=100,
        )

    city = schema.TextLine(
        title=u'City',
        required=True,
        description=u'City name',
        max_length=50,
        )

    country_code = schema.Choice(
        title=u'Country',
        required=True,
        description=u'2-letter country code',
        source=COUNTRIES_VOCAB,
        )

    postal_code = schema.TextLine(
        title=u'Postal code',
        required=False,
        description=(u'Zip code or equivalent is usually required '
                     u'for countries that have them.'),
        max_length=20,
        )

    state = schema.TextLine(
        title=u'State',
        required=False,
        description=(u'2-letter code for US stated, and the '
                     u'equivalent for other countries.'),
        max_length=100,
        )

    phone = schema.TextLine(
        title=u'Phone',
        required=False,
        description=u'Phone number in E.123 format.',
        max_length=50,
        )


@attrs_to_fields
class Address(object):
    """A postal address as used in PayPal transactions.
    """
    grok.implements(IAddress)

    def __init__(self, line1='', line2=None, city='', country_code=None,
                 postal_code=None, state=None, phone=None):
        self.line1 = line1
        self.line2 = line2
        self.city = city
        self.country_code = country_code
        self.postal_code = postal_code
        self.state = state
        self.phone = phone

    def to_dict(self):
        """Turn Adress into a dict that can be fed to PayPal classes.
        """
        return to_dict(self)


class AmountDetails(object):
    """Amount details can be given with Amount objects.

    All parameters are passed in as decimals (`decimal.Decimal`).

    All numbers stored here, might have 10 characters max with
    support for two decimal places.

    No parameter is strictly required, except `subtotal`, which must
    be set if any of the other values is set.

    `shipping`: Amount charged for shipping.

    `subtotal`: Amount for subtotal of the items. Automatically
      computed. If no other item was set, subtotal is `None`.

    `tax`: Amount charged for tax.

    `fee`: Fee charged by PayPal. In case of a refund, this is the fee
      amount refunded to the original recipient of the payment. Value
      assigned by PayPal.

    `handling_fee`: Amount being charged for the handling
      fee. Currently supported with paypal payment_method only, but
      available for credit_card payment_method at a later date.

    `insurance`: Amount being charged for the insurance fee. Currently
      supported with paypal payment_method only, but available for
      credit_card payment_method at a later date.

    `shipping_discount`: Amount being discounted for the shipping
      fee. Currently supported with paypal payment_method only, but
      available for credit_card payment_method at a later date.
    """
    def __init__(self, shipping=None, tax=None, fee=None,
                 handling_fee=None, insurance=None, shipping_discount=None):
        self.shipping = shipping
        self.tax = tax
        self.fee = fee
        self.handling_fee = handling_fee
        self.insurance = insurance
        self.shipping_discount = shipping_discount

    @property
    def subtotal(self):
        names = (
            'shipping', 'tax', 'fee', 'handling_fee', 'insurance',
            'shipping_discount'
            )
        result = None
        for name in names:
            val = getattr(self, name)
            if name == 'shipping_discount' and val is not None:
                val = -val
            if val is not None:
                if result is None:
                    result = val
                else:
                    result += val
        return result

    def to_dict(self):
        return to_dict(self)


class IAmount(Interface):
    """An amount as used by PayPal in payments.
    """
    currency = schema.Choice(
        title=u'Currency',
        description=u'PayPal does not support all currencies. Required.',
        required=True,
        source=CURRENCIES_VOCAB,
        default=u'USD',
        )

    total = schema.Decimal(
        title=u'Total',
        description=(
            u'Total amount charged from the payer to the payee. '
            u'In case of a refund, this is the refunded amount to '
            u'the original payer from the payee.'),
        required=True,
        default=decimal.Decimal("0.00"),
        max=decimal.Decimal("9999999.99")
        )

    details = Attribute(
        """Additional details related to a payment amount.
        """)


@attrs_to_fields
class Amount(object):
    grok.implements(IAmount)

    def __init__(self, currency="USD", total=decimal.Decimal("0.00"),
                 details=None):
        self.currency = currency
        self.total = total
        self.details = details

    def to_dict(self):
        return to_dict(self)


class IItem(Interface):
    """PayPal Item.

    Items in a PayPal context are goods sold to customers.
    """
    quantity = schema.Int(
        title=u"Quantity",
        description=u"Number of this particular items.",
        required=True,
        default=1,
        max=9999999999,
        )

    name = schema.TextLine(
        title=u"Name",
        description=u"Item name",
        required=True,
        max_length=127,
        )

    price = schema.Decimal(
        title=u"Price",
        description=u"Price",
        required=True,
        max=decimal.Decimal("9999999.99"),
        )

    currency = schema.Choice(
        title=u"Currency",
        description=u"Currency",
        source=CURRENCIES_VOCAB,
        default=u'USD',
        required=True,
        )

    sku = schema.Choice(
        title=u"SKU",
        description=u"Stock keeping unit corresponding to item.",
        source=STOCK_KEEPING_UNITS_VOCAB,
        required=False,
        )

    description = schema.TextLine(
        title=u"Description",
        description=(
            u"Description of Item. Currently supported with paypal "
            u"payments only."
            ),
        max_length=127,
        required=False,
        )

    tax = schema.Decimal(
        title=u"Tax",
        description=(
            u"Tax of the item. Currently supported with paypal "
            u"payments only."
            ),
        required=False,
        )


@attrs_to_fields
class Item(object):
    """See IItem for docs.
    """
    grok.implements(IItem)

    def __init__(self, name, quantity=1, price=decimal.Decimal("0.00"),
                 currency="USD", sku=None, description=None, tax=None):
        self.name = name
        self.quantity = quantity
        self.price = price
        self.currency = currency
        self.sku = sku
        self.description = description
        self.tax = tax

    def to_dict(self):
        return to_dict(self)


class IItemList(Interface):
    """List of `Item` objects and a related `ShippingAddress`.

    `items`: can be a simple list of `Item` objects.

    `shipping_address`: a `ShippingAddress` object. Only needed if
      different from Payer address.
    """
    items = schema.List(
        title=u"Items",
        description=u"PayPal Items are sold goods",
        value_type=schema.Object(
            title=u"Item",
            description=u"Item in a list",
            schema=IItem,
            )
        )

    shipping_address = schema.Object(
        title=u"Shipping Address",
        description=u"Shipping address of receiver if different from payee.",
        schema=IShippingAddress,
        required=False,
        )


@attrs_to_fields
class ItemList(object):
    """List of `Item` objects and a related `ShippingAddress`.

    `items`: can be a simple list of `Item` objects.

    `shipping_address`: a `ShippingAddress` object. Only needed if
      different from Payer address.
    """
    grok.implements(IItemList)

    def __init__(self, items=[], shipping_address=None):
        self.items = items
        self.shipping_address = shipping_address

    def to_dict(self):
        return to_dict(self)


class IPaymentOptions(Interface):
    """Payment options requested for a certain purchase unit.

    `allowed_payment_method`: Optional payment method type. If
      specified, the transaction will go through for only instant
      payment. Allowed values: ``INSTANT_FUNDING_SOURCE``. Only for
      use with the ``paypal`` payment_method, not relevant for the
      ``credit_card`` payment_method.
    """
    allowed_payment_method = schema.Choice(
        title=u"Allowed payment method",
        description=(
            u"Optional payment type. If specified, the transaction "
            u"will go through for only instant payment. Only for use "
            u"with paypal payment method, not relevant for credit cards."
            ),
        required=False,
        source=PAYMENT_OPTION_METHODS_VOCAB,
        )


@attrs_to_fields
class PaymentOptions(object):
    """Payment options requested for a certain purchase unit.
    """
    grok.implements(IPaymentOptions)

    def __init__(self, allowed_payment_method=None):
        if allowed_payment_method not in (
            None, 'INSTANT_FUNDING_SOURCE'):
            raise ValueError(
                "allowed_payment_method of PaymentOptions must be None or "
                "'INSTANT_FUNDING_SOURCE'"
                )
        self.allowed_payment_method = allowed_payment_method

    def to_dict(self):
        return to_dict(self)


class ITransaction(Interface):
    """PayPal transactions provide payment transaction details.
    """
    amount = schema.Object(
        title=u"Amount",
        description=u"Amount being collected.",
        schema=IAmount,
        required=True,
        )

    description = schema.TextLine(
        title=u"Description",
        description=u"Description of transaction",
        required=False,
        max_length=127,
        )

    item_list = schema.Object(
        title=u"Item List",
        description=u"List of items",
        required=False,
        schema=IItemList,
        )

    # XXX: This should be defined more precisely: What kind of objects, etc.
    #      PayPal docs say: "array of sale, authorization, capture, or refund,
    #      objects"
    related_resources = Attribute("Arbitrary objects")

    invoice_number = schema.TextLine(
        title=u"Invoice Number",
        description=(
            u"Invoice number used to track the payment. "
            u"Currently supported with paypal payment_method only."
            ),
        required=False,
        max_length=256,
        )

    custom = schema.TextLine(
        title=u"Custom text",
        description=(
            u"Free-form field for the use of clients. Currently "
            u"supported with paypal payment_method only."),
        required=False,
        max_length=256,
        )

    soft_descriptor = schema.TextLine(
        title=u"Soft descriptor",
        description=(
            u"Soft descriptor used when charging this funding "
            u"source. Currently supported with paypal payment_method only"
            ),
        required=False,
        max_length=22,
        )

    payment_options = schema.Object(
        title=u"Payment Options",
        description=u"Payment options requested for this purchase unit.",
        required=False,
        schema=IPaymentOptions,
        )


@attrs_to_fields
class Transaction(object):
    # See ITransaction for description

    grok.implements(ITransaction)

    def __init__(self, amount, description=None, item_list=None,
                 related_resources=[], invoice_number=None, custom=None,
                 soft_descriptor=None, payment_options=None):
        self.amount = amount
        self.description = description
        self.item_list = item_list
        self.related_resources = related_resources
        self.invoice_number = invoice_number
        self.custom = custom
        self.soft_descriptor = soft_descriptor
        self.payment_options = payment_options

    def to_dict(self):
        """Give a `dict` representation of this `Transaction`.
        """
        return to_dict(self)


def get_payment(intent='sale', payment_method='credit_card'):
    """Construct a payment.

    You have to `create()` the payment yourself.

    Returns a paypalrestsdk Payment object, not an Ikoba payment.

    As `intent` currently only the string ``'sale'`` is supported.

    XXX: Just some sampledata yet.
    """
    if intent != "sale":
        raise ValueError(
            "Currently, only 'sale' is allowed as type of paypal"
            "payment.")
    payment = paypalrestsdk.Payment(
        {
            "intent": intent,
            "payer": {
                "payment_method": "credit_card",
                "funding_instruments": [
                    {
                        "credit_card": {
                            "type": "visa",
                            "number": "4417119669820331",
                            "expire_month": "11",
                            "expire_year": "2018",
                            "cvv2": "874",
                            "first_name": "Joe",
                            "last_name": "Shopper",
                            "billing_address": {
                                "line1": "52 N Main ST",
                                "city": "Johnstown",
                                "state": "OH",
                                "postal_code": "43210",
                                "country_code": "US"}
                            }
                        }
                    ]},
            "transactions": [{
                "amount": {
                    "total": "7.47",
                    "currency": "USD",
                    "details": {
                        "subtotal": "7.41",
                        "tax": "0.03",
                        "shipping": "0.03"}},
                "description": ("This is the payment "
                                "transaction description.")
                }]
            }
        )
    return payment


class IPayPalPayment(IPayment):
    """A paypal payment.
    """


class PayPalPayment(grok.Model):
    """A paypal payment.
    """
    pass


class StandardItemAdapter(grok.Adapter):
    grok.implements(ITransaction)
    grok.adapts(IPaymentItem)


def payment_item_to_transaction(item):
    """Turn an IPaymentItem into an ITransaction
    """
    amount = Amount(currency=item.currency, total=item.amount)
    description = item.title
    item = Item(name=item.title, price=item.amount, currency=item.currency,
                quantity=1, sku=_("license"))
    item_list = ItemList(items=[item, ])
    transaction = Transaction(
        amount=amount, description=description, item_list=item_list)
    return transaction


class PayPalCreditCardService(PaymentProviderServiceBase):
    grok.implements(IPaymentGatewayService)
    grok.name('paypal_creditcard')

    title = _(u'Credit Card (PayPal)')

    def get_credit_card(self, customer_id):
        """Get an ICreditCard for `customer_id`.

        `None` if the payer has (yet) no credit card stored with ikoba.
        """
        site = grok.getSite()
        if not IContainer.providedBy(site):
            return None
        if not 'creditcards' in site:
            return None
        return site['creditcards'].get(customer_id, None)

    def store_credit_card(self, paypal_credit_card):
        """Store `paypal_credit_card` in vault.

        Returns a credit card token in case of success. Otherwise an
        IOError is raised.

        `paypal_credit_card` must provide `ICreditCard`. The credit
        card created is stored at PayPal and gets a token stored
        locally (in site['creditcards']).

        If no `creditcards` folder is available in local site, we
        create a new container.
        """
        site = grok.getSite()
        if not 'creditcards' in site:
            site['creditcards'] = grok.Container()
        pp_credit_card = paypalrestsdk.CreditCard(
            paypal_credit_card.to_dict())
        result = pp_credit_card.create()
        if not result:
            # error
            raise IOError(pp_credit_card.error)
        result = CreditCardToken(
            pp_credit_card.id, pp_credit_card.external_customer_id,
            pp_credit_card.number, pp_credit_card.type,
            pp_credit_card.expire_month, pp_credit_card.expire_year
            )
        site['creditcards'][pp_credit_card.external_customer_id] = result
        return result

    def create_payment(self, payer, payment_item,  payee=None):
        """Create a creditcard payment.
        """
        if not IPayer.providedBy(payer):
            payer = IPayer(payer)
        if not IPaymentItem.providedBy(payment_item):
            payment_item = IPaymentItem(payment_item)
        if (payee is not None) and (not IPayee.providedBy(payee)):
            payee = IPayee(payee)
        credit_card = self.get_credit_card(payer.payer_id)
        if credit_card is None:
            raise ValueError("Payer %s has no credit card." % payer.payer_id)
        transaction = payment_item_to_transaction(payment_item)
        payment_dict = {
            "intent": "sale",
            "payer": {
                "payment_method": "credit_card",
                "funding_instruments": [{
                    "credit_card_token": {
                        "credit_card_id": credit_card.credit_card_id,
                        }}]},
            "transactions": [
                transaction.to_dict()]
            }
        payment = paypalrestsdk.Payment(payment_dict)
        return payment

    def next_step(self, payment_id):
        raise NotImplemented("next_steo() not implemented")


class PayPalRegularPaymentService(PaymentProviderServiceBase):
    grok.implements(IPaymentGatewayService)
    grok.name('paypal_regular')

    title = _(u'PayPal Payment')
