Ignore:
Timestamp:
23 Dec 2014, 13:06:04 (10 years ago)
Author:
uli
Message:

Put all local changes into repos. Sorry for the mess!

File:
1 edited

Legend:

Unmodified
Added
Removed
  • main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/paypal.py

    r12060 r12305  
    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')
Note: See TracChangeset for help on using the changeset viewer.