- Timestamp:
- 23 Dec 2014, 13:06:04 (10 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/paypal.py
r12060 r12305 18 18 """Support for PayPal payments. 19 19 """ 20 import decimal 21 import grok 20 22 import ConfigParser 23 import inspect 21 24 import paypalrestsdk 25 import re 26 import uuid 27 from zope import schema 22 28 from zope.component import queryUtility 23 from waeup.ikoba.interfaces import IPayPalConfig 29 from zope.interface import Interface, Attribute 30 from waeup.ikoba.interfaces import MessageFactory as _ 31 from waeup.ikoba.interfaces import IPayPalConfig, SimpleIkobaVocabulary 32 from waeup.ikoba.utils.helpers import attrs_to_fields 33 from waeup.ikoba.payments.interfaces import ( 34 IPayment, IPaymentGatewayService, IPayer, IPaymentItem, IPayee, 35 ) 36 from waeup.ikoba.payments.paypal_countries import COUNTRIES_VOCAB 37 from waeup.ikoba.payments.paypal_currencies import CURRENCIES_VOCAB 38 39 #: Intents allowed for paypal based payments 40 PAYMENT_INTENTS = ('sale', 'authorize', 'order') 41 42 #: Payment methods allowed by PayPal 43 PAYMENT_METHODS = ('credit_card', 'paypal') 44 45 #: Payer status set/accepted by PayPal 46 PAYER_STATUS = ('VERIFIED', 'UNVERIFIED') 47 48 #: Tax ID types accepted by PayPal (yes, this list is complete) 49 TAX_ID_TYPES = ('BR_CPF', 'BR_CNPJ') 50 51 #: Address types accepted by PayPal 52 ADDRESS_TYPES = ('residential', 'business', 'mailbox') 53 54 #: A vocabulary with address types 55 ADDRESS_TYPES_VOCAB = SimpleIkobaVocabulary( 56 *[(_(x), x) for x in ADDRESS_TYPES]) 57 58 #: Credit card types accepted by PayPal 59 CREDIT_CARD_TYPES = ('visa', 'mastercard', 'discover', 'amex') 60 61 CREDIT_CARD_TYPES_VOCAB = SimpleIkobaVocabulary( 62 *[(_(x), x) for x in CREDIT_CARD_TYPES]) 63 64 #: Credit card status accepted by PayPal 65 CREDIT_CARD_STATUS = ('expired', 'ok') 66 67 CREDIT_CARD_STATUS_VOCAB = SimpleIkobaVocabulary( 68 *[(_(x), x) for x in CREDIT_CARD_STATUS]) 69 70 #: Stock keeping units we support. 71 STOCK_KEEPING_UNITS = { 72 "pcs": _("pieces"), 73 } 74 75 STOCK_KEEPING_UNITS_VOCAB = SimpleIkobaVocabulary( 76 *[(value, key) for key, value in STOCK_KEEPING_UNITS.items()]) 77 78 #: Payment methods (flags) used by PayPal 79 PAYMENT_OPTION_METHODS = ('INSTANT_FUNDING_SOURCE', ) 80 PAYMENT_OPTION_METHODS_VOCAB = SimpleIkobaVocabulary( 81 *[(_(x), x) for x in PAYMENT_OPTION_METHODS]) 82 83 84 def 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 24 107 25 108 … … 91 174 92 175 93 def get_payment(): 176 class 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 212 class 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 267 class 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 290 class 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 380 class 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 452 class 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 508 class 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 524 class 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 594 class 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 616 class 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 672 class 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 693 class 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 758 class 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 786 class 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 799 class 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 862 class 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 881 class 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 908 class 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 926 class 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 948 class 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 966 class 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 1033 class 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 1056 def get_payment(intent='sale', payment_method='credit_card'): 94 1057 """Construct a payment. 95 1058 96 1059 You have to `create()` the payment yourself. 97 1060 1061 Returns a paypalrestsdk Payment object, not an Ikoba payment. 1062 1063 As `intent` currently only the string ``'sale'`` is supported. 1064 98 1065 XXX: Just some sampledata yet. 99 1066 """ 1067 if intent != "sale": 1068 raise ValueError( 1069 "Currently, only 'sale' is allowed as type of paypal" 1070 "payment.") 100 1071 payment = paypalrestsdk.Payment( 101 1072 { 102 "intent": "sale",1073 "intent": intent, 103 1074 "payer": { 104 1075 "payment_method": "credit_card", … … 123 1094 ]}, 124 1095 "transactions": [{ 125 126 127 128 129 130 131 132 133 134 1096 "amount": { 1097 "total": "7.47", 1098 "currency": "USD", 1099 "details": { 1100 "subtotal": "7.41", 1101 "tax": "0.03", 1102 "shipping": "0.03"}}, 1103 "description": ("This is the payment " 1104 "transaction description.") 1105 }] 135 1106 } 136 1107 ) 137 1108 return payment 1109 1110 1111 class IPayPalPayment(IPayment): 1112 """A paypal payment. 1113 """ 1114 1115 1116 class PayPalPayment(grok.Model): 1117 """A paypal payment. 1118 """ 1119 pass 1120 1121 1122 class 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 1138 class PayPalRegularPaymentService(grok.GlobalUtility): 1139 grok.implements(IPaymentGatewayService) 1140 grok.name('paypal_regular') 1141 1142 title = _(u'PayPal Payment')
Note: See TracChangeset for help on using the changeset viewer.