source: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/paypal.py @ 12761

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

Merge changes from uli-payments back into trunk.

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