source: main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/paypal.py @ 12305

Last change on this file since 12305 was 12305, checked in by uli, 10 years ago

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

  • Property svn:keywords set to Id
File size: 33.8 KB
Line 
1## $Id: paypal.py 12305 2014-12-23 13:06:04Z uli $
2##
3## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""Support for PayPal payments.
19"""
20import decimal
21import grok
22import ConfigParser
23import inspect
24import paypalrestsdk
25import re
26import uuid
27from zope import schema
28from zope.component import queryUtility
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
107
108
109def parse_paypal_config(path):
110    """Get paypal credentials from config file.
111
112    Returns a dict with following keys set:
113
114      ``client_id``, ``client_secret``, ``mode``.
115
116    Please note, that values might be `None`.
117    """
118    parser = ConfigParser.SafeConfigParser(
119        {'id': None, 'secret': None, 'mode': 'sandbox', }
120        )
121    parser.readfp(open(path))
122    return {
123        'client_id': parser.get('rest-client', 'id', None),
124        'client_secret': parser.get('rest-client', 'secret', None),
125        'mode': parser.get('rest-client', 'mode', 'sandbox')
126        }
127
128
129def get_paypal_config_file_path():
130    """Get the path of the configuration file (if any).
131
132    If no such file is registered, we get `None`.
133    """
134    util = queryUtility(IPayPalConfig)
135    if util is None:
136        return None
137    return util['path']
138
139
140def configure_sdk(path):
141    """Configure the PayPal SDK with values from config in `path`.
142
143    Parse paypal configuration from file in `path` and set
144    configuration in SDK accordingly.
145
146    Returns a dict with values from config file and path set.
147
148    This function will normally be called during start-up and only
149    this one time.
150
151    It is neccessary to authorize later calls to other part of the
152    PayPal API.
153    """
154    conf = parse_paypal_config(path)
155    paypalrestsdk.configure({
156        'mode': conf['mode'],
157        'client_id': conf['client_id'],
158        'client_secret': conf['client_secret'],
159        })
160    conf['path'] = path
161    return conf
162
163
164def get_access_token():
165    """Get an access token for further calls.
166    """
167    conf = parse_paypal_config(get_paypal_config_file_path())
168    api = paypalrestsdk.set_config(
169        mode=conf['mode'],  # sandbox or live
170        client_id=conf['client_id'],
171        client_secret=conf['client_secret'],
172        )
173    return api.get_access_token()
174
175
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'):
1057    """Construct a payment.
1058
1059    You have to `create()` the payment yourself.
1060
1061    Returns a paypalrestsdk Payment object, not an Ikoba payment.
1062
1063    As `intent` currently only the string ``'sale'`` is supported.
1064
1065    XXX: Just some sampledata yet.
1066    """
1067    if intent != "sale":
1068        raise ValueError(
1069            "Currently, only 'sale' is allowed as type of paypal"
1070            "payment.")
1071    payment = paypalrestsdk.Payment(
1072        {
1073            "intent": intent,
1074            "payer": {
1075                "payment_method": "credit_card",
1076                "funding_instruments": [
1077                    {
1078                        "credit_card": {
1079                            "type": "visa",
1080                            "number": "4417119669820331",
1081                            "expire_month": "11",
1082                            "expire_year": "2018",
1083                            "cvv2": "874",
1084                            "first_name": "Joe",
1085                            "last_name": "Shopper",
1086                            "billing_address": {
1087                                "line1": "52 N Main ST",
1088                                "city": "Johnstown",
1089                                "state": "OH",
1090                                "postal_code": "43210",
1091                                "country_code": "US"}
1092                            }
1093                        }
1094                    ]},
1095            "transactions": [{
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                }]
1106            }
1107        )
1108    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 TracBrowser for help on using the repository browser.