## $Id: paypal.py 12741 2015-03-12 05:29:43Z uli $ ## ## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with this program; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ## """Support for PayPal payments. """ import decimal import grok import ConfigParser import inspect import paypalrestsdk import re import uuid from zope import schema from zope.component import queryUtility from zope.container.interfaces import IContainer from zope.interface import Interface, Attribute from waeup.ikoba.interfaces import MessageFactory as _ from waeup.ikoba.interfaces import IPayPalConfig, SimpleIkobaVocabulary from waeup.ikoba.utils.helpers import attrs_to_fields from waeup.ikoba.payments.interfaces import ( IPayment, IPaymentGatewayService, IPayer, IPaymentItem, IPayee, ) from waeup.ikoba.payments.payment import PaymentProviderServiceBase from waeup.ikoba.payments.paypal_countries import COUNTRIES_VOCAB from waeup.ikoba.payments.paypal_currencies import CURRENCIES_VOCAB #: Intents allowed for paypal based payments PAYMENT_INTENTS = ('sale', 'authorize', 'order') #: Payment methods allowed by PayPal PAYMENT_METHODS = ('credit_card', 'paypal') #: Payer status set/accepted by PayPal PAYER_STATUS = ('VERIFIED', 'UNVERIFIED') #: Tax ID types accepted by PayPal (yes, this list is complete) TAX_ID_TYPES = ('BR_CPF', 'BR_CNPJ') #: Address types accepted by PayPal ADDRESS_TYPES = ('residential', 'business', 'mailbox') #: A vocabulary with address types ADDRESS_TYPES_VOCAB = SimpleIkobaVocabulary( *[(_(x), x) for x in ADDRESS_TYPES]) #: Credit card types accepted by PayPal CREDIT_CARD_TYPES = ('visa', 'mastercard', 'discover', 'amex') CREDIT_CARD_TYPES_VOCAB = SimpleIkobaVocabulary( *[(_(x), x) for x in CREDIT_CARD_TYPES]) #: Credit card status accepted by PayPal CREDIT_CARD_STATUS = ('expired', 'ok') CREDIT_CARD_STATUS_VOCAB = SimpleIkobaVocabulary( *[(_(x), x) for x in CREDIT_CARD_STATUS]) #: Stock keeping units we support. STOCK_KEEPING_UNITS = { "pcs": _("pieces"), "license": _("license"), } STOCK_KEEPING_UNITS_VOCAB = SimpleIkobaVocabulary( *[(value, key) for key, value in STOCK_KEEPING_UNITS.items()]) #: Payment methods (flags) used by PayPal PAYMENT_OPTION_METHODS = ('INSTANT_FUNDING_SOURCE', ) PAYMENT_OPTION_METHODS_VOCAB = SimpleIkobaVocabulary( *[(_(x), x) for x in PAYMENT_OPTION_METHODS]) def to_dict(obj, name_map={}): """Turn `obj` into some dict representation. """ result = dict() for name in dir(obj): if name.startswith('_'): continue value = getattr(obj, name) if value is None: continue if inspect.ismethod(value): continue if hasattr(value, 'to_dict'): value = value.to_dict() elif isinstance(value, list): value = [x.to_dict() for x in value] elif isinstance(value, decimal.Decimal): value = u"%.2f" % (value, ) else: value = unicode(value) name = name_map.get(name, name) result[name] = value return result def parse_paypal_config(path): """Get paypal credentials from config file. Returns a dict with following keys set: ``client_id``, ``client_secret``, ``mode``. Please note, that values might be `None`. """ parser = ConfigParser.SafeConfigParser( {'id': None, 'secret': None, 'mode': 'sandbox', } ) parser.readfp(open(path)) return { 'client_id': parser.get('rest-client', 'id', None), 'client_secret': parser.get('rest-client', 'secret', None), 'mode': parser.get('rest-client', 'mode', 'sandbox') } def get_paypal_config_file_path(): """Get the path of the configuration file (if any). If no such file is registered, we get `None`. """ util = queryUtility(IPayPalConfig) if util is None: return None return util['path'] def configure_sdk(path): """Configure the PayPal SDK with values from config in `path`. Parse paypal configuration from file in `path` and set configuration in SDK accordingly. Returns a dict with values from config file and path set. This function will normally be called during start-up and only this one time. It is neccessary to authorize later calls to other part of the PayPal API. """ conf = parse_paypal_config(path) paypalrestsdk.configure({ 'mode': conf['mode'], 'client_id': conf['client_id'], 'client_secret': conf['client_secret'], }) conf['path'] = path return conf def get_access_token(): """Get an access token for further calls. """ conf = parse_paypal_config(get_paypal_config_file_path()) api = paypalrestsdk.set_config( mode=conf['mode'], # sandbox or live client_id=conf['client_id'], client_secret=conf['client_secret'], ) return api.get_access_token() class Payer(object): """A payer as required in paypal payments. According to Paypal docs: `payment_method` must be one of ``'paypal'`` or ``'credit_card'`` as stored in `PAYMENT_METHODS`. `funding_instruments` is a list of `FundingInstrument` objects. I think the list must be empty for ``paypal`` payments and must contain at least one entry for ``credit_card`` payments. `payer_info` must be provided for ``paypal`` payments and might be provided for ``credit_card`` payments. It's a `PayerInfo` object. `status` reflects the payer's PayPal account status and is currently supported for ``paypal`` payments. Allowed values are ``'VERIFIED'`` and ``'UNVERIFIED'`` as stored in `PAYER_STATUS`. """ def __init__(self, payment_method, funding_instruments=[], payer_info=None, status=None): if payment_method not in PAYMENT_METHODS: raise ValueError( "Invalid payment method: use one of %s" % (PAYMENT_METHODS, ) ) if status and status not in PAYER_STATUS: raise ValueError( "Invalid status: use one of %s" % (PAYER_STATUS, ) ) self.payment_method = payment_method self.funding_instruments = funding_instruments self.payer_info = payer_info self.status = status class PayerInfo(object): """Payer infos as required by Paypal payers. Normally used with a `Payer` instance (which in turn is part of a `payment`). According to PayPal docs: Pre-filled by PayPal when `payment_method` is ``'paypal'``. `email`: 127 chars max. I don't think, this value is pre-filled. `first_name`: First name of payer. Assigned by PayPal. `last_naem`: Last name of payer. Assigned by PayPal. `payer_id`: Payer ID as assigned by PayPal. Do not mix up with any Ikoba Payer IDs. `phone`: Phone number representing the payer. 20 chars max. `shipping_address`: a shipping address object of payer. Assigned by PayPal. `tax_id_type`: Payer's tax ID type. Allowed values: ``'BR_CPF'``, ``'BR_CNPJ'`` (I have not the slightest clue what that means). Supported (PayPal-wise) with ``paypal`` payment method only (not with ``credit_card``). `tax_id`: Payer's tax ID. Here the same as for `tax_id_type` applies (except that also other values are accepted). By default all values are set to the empty string and shipping address to `None`. See also: :class:`Payer` """ def __init__(self, email='', first_name='', last_name='', payer_id='', phone='', shipping_address=None, tax_id_type='', tax_id=''): if tax_id_type and tax_id_type not in TAX_ID_TYPES: raise ValueError( "Invalid tax id type: use one of %s" % (TAX_ID_TYPES, ) ) self.email = email self.first_name = first_name self.last_name = last_name self.payer_id = payer_id self.phone = phone self.shipping_address = shipping_address self.tax_id_type = tax_id_type self.tax_id = tax_id class FundingInstrument(object): """Representation of a payer's funding instrument as required by PayPal. Represents always a credit card. Either by a complete set of credit card infos or by a credit card token, which contains only a limited set of credit card data and represents a full set stored in PayPal vault. """ def __init__(self, credit_card=None, credit_card_token=None): if credit_card is None and credit_card_token is None: raise ValueError( "Must provide credit card data or a token") if credit_card is not None and credit_card_token is not None: raise ValueError( "Must provide credit card data *or* a token, not both.") self.credit_card = credit_card self.credit_card_token = credit_card_token def to_dict(self): return to_dict(self) class ICreditCard(Interface): """A credit card (full data set) as accepted by PayPal. """ paypal_id = schema.TextLine( title=u'PayPal ID', description=u'ID of the credit card provided by PayPal.', required=False, ) external_customer_id = schema.TextLine( title=u'Payer ID', description=(u'A unique identifier for the credit card. This ' u'identifier is provided by Ikoba and must be set ' u'with all future transactions, once put into ' u'PayPal vault.'), required=True, ) number = schema.TextLine( title=u'Credit Card Number', description=u'Numeric characters only w/o spaces or punctuation', required=True, ) credit_card_type = schema.Choice( title=u'Credit Card Type', description=u'Credit card types supported by PayPal.', required=True, source=CREDIT_CARD_TYPES_VOCAB, ) expire_month = schema.Int( title=u'Expiration Month', description=u"Month, the credit card expires.", required=True, default=1, min=1, max=12, ) expire_year = schema.Int( title=u'Expiration Year', description=u'4-digit expiration year.', required=True, default=2020, min=1900, max=9999, ) cvv2 = schema.TextLine( title=u'CVV2', description=u'3-4 digit card validation code', required=False, ) first_name = schema.TextLine( title=u'First Name', description=u"Cardholder's first name.", required=False, ) last_name = schema.TextLine( title=u'Last Name', description=u"Cardholder's last name.", required=False, ) billing_address = Attribute( "Billing address associated with card.") state = schema.Choice( title=u"Status", description=(u"Status of credit card funding instrument. " u"Value assigned by PayPal." ), required=False, source=CREDIT_CARD_STATUS_VOCAB, default=None, ) valid_unti = schema.TextLine( title=u'Valid until', description=(u'Funding instrument expiratiopn date, ' u'assigned by PayPal'), required=False, ) @attrs_to_fields class CreditCard(object): """A credit card (full info set) as used by PayPal. Normally used with a `FundingInstrument` instance. According to PayPal docs: `paypal_id`: provided by PayPal when storing credit card data. Required if using a stored credit card. `external_customer_id`: Unique identifier. If none is given, we assign a uuid. The uuid reads 'PAYER_<32 hex digits>'. `number`: Credit card number. Numeric characters only with no spaces or punctuation. The string must conform with modulo and length required by each credit card type. Redacted in responses. Required. `credit_card_type`: One of ``'visa'``, ``'mastercard'``, ``'discover'``, ``'amex'``. Required. `expire_month`: Expiration month. A number from 1 through 12. Required. `expire_year`: 4-digit expiration year. Required. `cvv2`: 3-4 digit card validation code. `first_name`: card holders first name. `last_name`: card holders last name. `billing_address`: Billing address associated with card. A `Billing` instance. `state`: state of the credit card funding instrument. Valid values are ``'expired'`` and ``'ok'``. *Value assigned by PayPal.* `paypal_valid_until`: Funding instrument expiration date. *Value assigned by PayPal.* Not to confuse with the credit cards expiration date, which is set via `expire_month` and `expire_year`. """ grok.implements(ICreditCard) def __init__(self, paypal_id=None, external_customer_id=None, number=None, credit_card_type=None, expire_month=1, expire_year=2000, cvv2=None, first_name=None, last_name=None, billing_address=None, state=None, paypal_valid_until=None): if not re.match('^[0-9]+$', number): raise ValueError("Credit card number may " "not contain non-numbers.") if external_customer_id is None: external_customer_id = u'PAYER_' + unicode(uuid.uuid4().hex) self.paypal_id = paypal_id self.external_customer_id = external_customer_id self.number = number self.credit_card_type = credit_card_type self.expire_month = expire_month self.expire_year = expire_year self.cvv2 = cvv2 self.first_name = first_name self.last_name = last_name self.billing_address = billing_address self.state = state self.paypal_valid_until = paypal_valid_until def to_dict(self): return to_dict(self, name_map={'credit_card_type': 'type'}) class ICreditCardToken(Interface): """A credit card token corresponding to a credit card stored with PayPal. """ credit_card_id = schema.TextLine( title=u"Credit Card ID", description=u"ID if credit card previously stored with PayPal", required=True, ) external_customer_id = schema.TextLine( title=u'Payer ID', description=(u'A unique identifier for the credit card. This ' u'identifier is provided by Ikoba and must be set ' u'with all future transactions, once put into ' u'PayPal vault.'), required=True, ) last4 = schema.TextLine( title=u"Credit Card's last 4 numbers", description=( u"Last four digits of the stored credit card number. " u"Value assigned by PayPal."), required=False, min_length=4, max_length=4 ) credit_card_type = schema.Choice( title=u'Credit Card Type', description=( u'Credit card type supported by PayPal. Value assigned ' u'by PayPal.'), required=False, source=CREDIT_CARD_TYPES_VOCAB, ) expire_month = schema.Int( title=u'Expiration Month', description=u"Month, the credit card expires. Assigned by PayPal.", required=True, default=1, min=1, max=12, ) expire_year = schema.Int( title=u'Expiration Year', description=u'4-digit expiration year. Assigned by PayPal.', required=True, default=2020, min=1900, max=9999, ) class CreditCardToken(object): grok.implements(ICreditCardToken) def __init__(self, credit_card_id, external_customer_id=None, last4=None, credit_card_type=None, expire_month=None, expire_year=None): self.credit_card_id = credit_card_id self.external_customer_id = external_customer_id self.last4 = last4 self.credit_card_type = credit_card_type self.expire_month = expire_month self.expire_year = expire_year def to_dict(self): return to_dict(self, name_map={'credit_card_type': 'type'}) class IShippingAddress(Interface): """A shipping address as accepted by PayPal. """ recipient_name = schema.TextLine( title=u'Recipient Name', required=True, description=u'Name of the recipient at this address.', max_length=50, ) type = schema.Choice( title=u'Address Type', description=u'Address Type.', required=False, source=ADDRESS_TYPES_VOCAB, ) line1 = schema.TextLine( title=u'Address Line 1', required=True, description=u'Line 1 of the address (e.g., Number, street, etc.)', max_length=100, ) line2 = schema.TextLine( title=u'Address Line 2', required=False, description=u'Line 2 of the address (e.g., Suite, apt #, etc.)', max_length=100, ) city = schema.TextLine( title=u'City', required=True, description=u'City name', max_length=50, ) country_code = schema.Choice( title=u'Country', required=True, description=u'2-letter country code', source=COUNTRIES_VOCAB, ) postal_code = schema.TextLine( title=u'Postal code', required=False, description=(u'Zip code or equivalent is usually required ' u'for countries that have them.'), max_length=20, ) state = schema.TextLine( title=u'State', required=False, description=(u'2-letter code for US stated, and the ' u'equivalent for other countries.'), max_length=100, ) phone = schema.TextLine( title=u'Phone', required=False, description=u'Phone number in E.123 format.', max_length=50, ) @attrs_to_fields class ShippingAddress(object): """A shipping address as used in PayPal transactions. """ grok.implements(IShippingAddress) def __init__(self, recipient_name, type='residential', line1='', line2=None, city='', country_code=None, postal_code=None, state=None, phone=None): self.recipient_name = recipient_name self.type = type self.line1 = line1 self.line2 = line2 self.city = city self.country_code = country_code self.postal_code = postal_code self.state = state self.phone = phone def to_dict(self): return to_dict(self) class IAddress(Interface): """An address as accepted by PayPal. """ line1 = schema.TextLine( title=u'Address Line 1', required=True, description=u'Line 1 of the address (e.g., Number, street, etc.)', max_length=100, ) line2 = schema.TextLine( title=u'Address Line 2', required=False, description=u'Line 2 of the address (e.g., Suite, apt #, etc.)', max_length=100, ) city = schema.TextLine( title=u'City', required=True, description=u'City name', max_length=50, ) country_code = schema.Choice( title=u'Country', required=True, description=u'2-letter country code', source=COUNTRIES_VOCAB, ) postal_code = schema.TextLine( title=u'Postal code', required=False, description=(u'Zip code or equivalent is usually required ' u'for countries that have them.'), max_length=20, ) state = schema.TextLine( title=u'State', required=False, description=(u'2-letter code for US stated, and the ' u'equivalent for other countries.'), max_length=100, ) phone = schema.TextLine( title=u'Phone', required=False, description=u'Phone number in E.123 format.', max_length=50, ) @attrs_to_fields class Address(object): """A postal address as used in PayPal transactions. """ grok.implements(IAddress) def __init__(self, line1='', line2=None, city='', country_code=None, postal_code=None, state=None, phone=None): self.line1 = line1 self.line2 = line2 self.city = city self.country_code = country_code self.postal_code = postal_code self.state = state self.phone = phone def to_dict(self): """Turn Adress into a dict that can be fed to PayPal classes. """ return to_dict(self) class AmountDetails(object): """Amount details can be given with Amount objects. All parameters are passed in as decimals (`decimal.Decimal`). All numbers stored here, might have 10 characters max with support for two decimal places. No parameter is strictly required, except `subtotal`, which must be set if any of the other values is set. `shipping`: Amount charged for shipping. `subtotal`: Amount for subtotal of the items. Automatically computed. If no other item was set, subtotal is `None`. `tax`: Amount charged for tax. `fee`: Fee charged by PayPal. In case of a refund, this is the fee amount refunded to the original recipient of the payment. Value assigned by PayPal. `handling_fee`: Amount being charged for the handling fee. Currently supported with paypal payment_method only, but available for credit_card payment_method at a later date. `insurance`: Amount being charged for the insurance fee. Currently supported with paypal payment_method only, but available for credit_card payment_method at a later date. `shipping_discount`: Amount being discounted for the shipping fee. Currently supported with paypal payment_method only, but available for credit_card payment_method at a later date. """ def __init__(self, shipping=None, tax=None, fee=None, handling_fee=None, insurance=None, shipping_discount=None): self.shipping = shipping self.tax = tax self.fee = fee self.handling_fee = handling_fee self.insurance = insurance self.shipping_discount = shipping_discount @property def subtotal(self): names = ( 'shipping', 'tax', 'fee', 'handling_fee', 'insurance', 'shipping_discount' ) result = None for name in names: val = getattr(self, name) if name == 'shipping_discount' and val is not None: val = -val if val is not None: if result is None: result = val else: result += val return result def to_dict(self): return to_dict(self) class IAmount(Interface): """An amount as used by PayPal in payments. """ currency = schema.Choice( title=u'Currency', description=u'PayPal does not support all currencies. Required.', required=True, source=CURRENCIES_VOCAB, default=u'USD', ) total = schema.Decimal( title=u'Total', description=( u'Total amount charged from the payer to the payee. ' u'In case of a refund, this is the refunded amount to ' u'the original payer from the payee.'), required=True, default=decimal.Decimal("0.00"), max=decimal.Decimal("9999999.99") ) details = Attribute( """Additional details related to a payment amount. """) @attrs_to_fields class Amount(object): grok.implements(IAmount) def __init__(self, currency="USD", total=decimal.Decimal("0.00"), details=None): self.currency = currency self.total = total self.details = details def to_dict(self): return to_dict(self) class IItem(Interface): """PayPal Item. Items in a PayPal context are goods sold to customers. """ quantity = schema.Int( title=u"Quantity", description=u"Number of this particular items.", required=True, default=1, max=9999999999, ) name = schema.TextLine( title=u"Name", description=u"Item name", required=True, max_length=127, ) price = schema.Decimal( title=u"Price", description=u"Price", required=True, max=decimal.Decimal("9999999.99"), ) currency = schema.Choice( title=u"Currency", description=u"Currency", source=CURRENCIES_VOCAB, default=u'USD', required=True, ) sku = schema.Choice( title=u"SKU", description=u"Stock keeping unit corresponding to item.", source=STOCK_KEEPING_UNITS_VOCAB, required=False, ) description = schema.TextLine( title=u"Description", description=( u"Description of Item. Currently supported with paypal " u"payments only." ), max_length=127, required=False, ) tax = schema.Decimal( title=u"Tax", description=( u"Tax of the item. Currently supported with paypal " u"payments only." ), required=False, ) @attrs_to_fields class Item(object): """See IItem for docs. """ grok.implements(IItem) def __init__(self, name, quantity=1, price=decimal.Decimal("0.00"), currency="USD", sku=None, description=None, tax=None): self.name = name self.quantity = quantity self.price = price self.currency = currency self.sku = sku self.description = description self.tax = tax def to_dict(self): return to_dict(self) class IItemList(Interface): """List of `Item` objects and a related `ShippingAddress`. `items`: can be a simple list of `Item` objects. `shipping_address`: a `ShippingAddress` object. Only needed if different from Payer address. """ items = schema.List( title=u"Items", description=u"PayPal Items are sold goods", value_type=schema.Object( title=u"Item", description=u"Item in a list", schema=IItem, ) ) shipping_address = schema.Object( title=u"Shipping Address", description=u"Shipping address of receiver if different from payee.", schema=IShippingAddress, required=False, ) @attrs_to_fields class ItemList(object): """List of `Item` objects and a related `ShippingAddress`. `items`: can be a simple list of `Item` objects. `shipping_address`: a `ShippingAddress` object. Only needed if different from Payer address. """ grok.implements(IItemList) def __init__(self, items=[], shipping_address=None): self.items = items self.shipping_address = shipping_address def to_dict(self): return to_dict(self) class IPaymentOptions(Interface): """Payment options requested for a certain purchase unit. `allowed_payment_method`: Optional payment method type. If specified, the transaction will go through for only instant payment. Allowed values: ``INSTANT_FUNDING_SOURCE``. Only for use with the ``paypal`` payment_method, not relevant for the ``credit_card`` payment_method. """ allowed_payment_method = schema.Choice( title=u"Allowed payment method", description=( u"Optional payment type. If specified, the transaction " u"will go through for only instant payment. Only for use " u"with paypal payment method, not relevant for credit cards." ), required=False, source=PAYMENT_OPTION_METHODS_VOCAB, ) @attrs_to_fields class PaymentOptions(object): """Payment options requested for a certain purchase unit. """ grok.implements(IPaymentOptions) def __init__(self, allowed_payment_method=None): if allowed_payment_method not in ( None, 'INSTANT_FUNDING_SOURCE'): raise ValueError( "allowed_payment_method of PaymentOptions must be None or " "'INSTANT_FUNDING_SOURCE'" ) self.allowed_payment_method = allowed_payment_method def to_dict(self): return to_dict(self) class ITransaction(Interface): """PayPal transactions provide payment transaction details. """ amount = schema.Object( title=u"Amount", description=u"Amount being collected.", schema=IAmount, required=True, ) description = schema.TextLine( title=u"Description", description=u"Description of transaction", required=False, max_length=127, ) item_list = schema.Object( title=u"Item List", description=u"List of items", required=False, schema=IItemList, ) # XXX: This should be defined more precisely: What kind of objects, etc. # PayPal docs say: "array of sale, authorization, capture, or refund, # objects" related_resources = Attribute("Arbitrary objects") invoice_number = schema.TextLine( title=u"Invoice Number", description=( u"Invoice number used to track the payment. " u"Currently supported with paypal payment_method only." ), required=False, max_length=256, ) custom = schema.TextLine( title=u"Custom text", description=( u"Free-form field for the use of clients. Currently " u"supported with paypal payment_method only."), required=False, max_length=256, ) soft_descriptor = schema.TextLine( title=u"Soft descriptor", description=( u"Soft descriptor used when charging this funding " u"source. Currently supported with paypal payment_method only" ), required=False, max_length=22, ) payment_options = schema.Object( title=u"Payment Options", description=u"Payment options requested for this purchase unit.", required=False, schema=IPaymentOptions, ) @attrs_to_fields class Transaction(object): # See ITransaction for description grok.implements(ITransaction) def __init__(self, amount, description=None, item_list=None, related_resources=[], invoice_number=None, custom=None, soft_descriptor=None, payment_options=None): self.amount = amount self.description = description self.item_list = item_list self.related_resources = related_resources self.invoice_number = invoice_number self.custom = custom self.soft_descriptor = soft_descriptor self.payment_options = payment_options def to_dict(self): """Give a `dict` representation of this `Transaction`. """ return to_dict(self) def get_payment(intent='sale', payment_method='credit_card'): """Construct a payment. You have to `create()` the payment yourself. Returns a paypalrestsdk Payment object, not an Ikoba payment. As `intent` currently only the string ``'sale'`` is supported. XXX: Just some sampledata yet. """ if intent != "sale": raise ValueError( "Currently, only 'sale' is allowed as type of paypal" "payment.") payment = paypalrestsdk.Payment( { "intent": intent, "payer": { "payment_method": "credit_card", "funding_instruments": [ { "credit_card": { "type": "visa", "number": "4417119669820331", "expire_month": "11", "expire_year": "2018", "cvv2": "874", "first_name": "Joe", "last_name": "Shopper", "billing_address": { "line1": "52 N Main ST", "city": "Johnstown", "state": "OH", "postal_code": "43210", "country_code": "US"} } } ]}, "transactions": [{ "amount": { "total": "7.47", "currency": "USD", "details": { "subtotal": "7.41", "tax": "0.03", "shipping": "0.03"}}, "description": ("This is the payment " "transaction description.") }] } ) return payment class IPayPalPayment(IPayment): """A paypal payment. """ class PayPalPayment(grok.Model): """A paypal payment. """ pass class StandardItemAdapter(grok.Adapter): grok.implements(ITransaction) grok.adapts(IPaymentItem) def payment_item_to_transaction(item): """Turn an IPaymentItem into an ITransaction """ amount = Amount(currency=item.currency, total=item.amount) description = item.title item = Item(name=item.title, price=item.amount, currency=item.currency, quantity=1, sku=_("license")) item_list = ItemList(items=[item, ]) transaction = Transaction( amount=amount, description=description, item_list=item_list) return transaction class PayPalCreditCardService(PaymentProviderServiceBase): grok.implements(IPaymentGatewayService) grok.name('paypal_creditcard') title = _(u'Credit Card (PayPal)') def get_credit_card(self, customer_id): """Get an ICreditCard for `customer_id`. `None` if the payer has (yet) no credit card stored with ikoba. """ site = grok.getSite() if not IContainer.providedBy(site): return None if not 'creditcards' in site: return None return site['creditcards'].get(customer_id, None) def store_credit_card(self, paypal_credit_card): """Store `paypal_credit_card` in vault. Returns a credit card token in case of success. Otherwise an IOError is raised. `paypal_credit_card` must provide `ICreditCard`. The credit card created is stored at PayPal and gets a token stored locally (in site['creditcards']). If no `creditcards` folder is available in local site, we create a new container. """ site = grok.getSite() if not 'creditcards' in site: site['creditcards'] = grok.Container() pp_credit_card = paypalrestsdk.CreditCard( paypal_credit_card.to_dict()) result = pp_credit_card.create() if not result: # error raise IOError(pp_credit_card.error) result = CreditCardToken( pp_credit_card.id, pp_credit_card.external_customer_id, pp_credit_card.number, pp_credit_card.type, pp_credit_card.expire_month, pp_credit_card.expire_year ) site['creditcards'][pp_credit_card.external_customer_id] = result return result def create_payment(self, payer, payment_item, payee=None): """Create a creditcard payment. """ if not IPayer.providedBy(payer): payer = IPayer(payer) if not IPaymentItem.providedBy(payment_item): payment_item = IPaymentItem(payment_item) if (payee is not None) and (not IPayee.providedBy(payee)): payee = IPayee(payee) credit_card = self.get_credit_card(payer.payer_id) if credit_card is None: raise ValueError("Payer %s has no credit card." % payer.payer_id) transaction = payment_item_to_transaction(payment_item) payment_dict = { "intent": "sale", "payer": { "payment_method": "credit_card", "funding_instruments": [{ "credit_card_token": { "credit_card_id": credit_card.credit_card_id, }}]}, "transactions": [ transaction.to_dict()] } payment = paypalrestsdk.Payment(payment_dict) return payment def next_step(self, payment_id): raise NotImplemented("next_steo() not implemented") class PayPalRegularPaymentService(PaymentProviderServiceBase): grok.implements(IPaymentGatewayService) grok.name('paypal_regular') title = _(u'PayPal Payment')