Changeset 12305 for main/waeup.ikoba/branches
- Timestamp:
- 23 Dec 2014, 13:06:04 (10 years ago)
- Location:
- main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments
- Files:
-
- 4 edited
Legend:
- Unmodified
- Added
- Removed
-
main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/interfaces.py
r12295 r12305 86 86 ) 87 87 88 def create_payment(payer _id, payee_id, payment_item_id):88 def create_payment(payer, payment_item, payee): 89 89 """Create a payment. 90 91 For all parameters we expect an object, that implements 92 `IPayer`, `IPaymentItem`, or `IPayee` respectively. If not, 93 then the given objects must be at least adaptable to the 94 respective interface. 95 96 Therfore you can pass in some `Customer` as long as there is 97 some `IPayer` adapter for `Customer` objects defined. 98 99 Returns an `IPayment` object. 90 100 """ 91 101 … … 236 246 237 247 248 class ICreditCard(Interface): 249 """A credit card. 250 251 A credit card is connected to a Payer. 252 """ 253 credit_card_id = schema.TextLine( 254 title=u'Internal Credit Card ID', 255 required=True, 256 ) 257 258 238 259 class IPayer(Interface): 239 260 """A payer. … … 242 263 title=u'Payer ID', 243 264 required=True, 265 ) 266 267 first_name = schema.TextLine( 268 title=u'First Name', 269 required=False, 270 ) 271 272 last_name = schema.TextLine( 273 title=u'Last Name', 274 required=False, 244 275 ) 245 276 -
main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/payment.py
r12277 r12305 27 27 from waeup.ikoba.payments.interfaces import ( 28 28 IPayment, STATE_UNPAID, STATE_FAILED, STATE_PAID, 29 IPaymentGatewayService, 29 IPaymentGatewayService, IPayer, 30 30 ) 31 31 from waeup.ikoba.utils.logger import Logger … … 63 63 self.payment_date = None 64 64 self.payment_id = u'PAY_' + unicode(uuid.uuid4().hex) 65 self.gateway_service = None 65 66 self.amount = decimal.Decimal("0.00") 66 67 self.payed_item_id = None -
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') -
main/waeup.ikoba/branches/uli-payments/src/waeup/ikoba/payments/tests/test_paypal.py
r12060 r12305 16 16 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 17 17 ## 18 import decimal 18 19 import os 20 import re 19 21 import shutil 20 22 import tempfile 21 23 import unittest 22 24 import paypalrestsdk 23 from zope.component import getGlobalSiteManager, queryUtility 25 from zope.component import ( 26 getGlobalSiteManager, queryUtility, getUtilitiesFor, 27 ) 28 from zope.i18nmessageid.message import Message as i18nMessage 29 from zope.interface import implements 30 from zope.interface.verify import verifyObject, verifyClass 31 from zope.schema.interfaces import IVocabularyTokenized, ITerm 24 32 from waeup.ikoba.interfaces import IPayPalConfig 33 from waeup.ikoba.payments.interfaces import ( 34 IPaymentGatewayService, IPayer, IPayee, IPaymentItem, IPayment, 35 ) 25 36 from waeup.ikoba.payments.paypal import ( 26 37 get_paypal_config_file_path, parse_paypal_config, get_access_token, 27 configure_sdk, get_payment, 38 configure_sdk, get_payment, Payer, PayerInfo, ShippingAddress, 39 ADDRESS_TYPES_VOCAB, IShippingAddress, Address, IAddress, to_dict, 40 CreditCard, CREDIT_CARD_TYPES_VOCAB, CREDIT_CARD_STATUS_VOCAB, 41 ICreditCard, ICreditCardToken, CreditCardToken, FundingInstrument, 42 AmountDetails, IAmount, Amount, IItem, Item, STOCK_KEEPING_UNITS_VOCAB, 43 IItemList, ItemList, IPaymentOptions, PaymentOptions, ITransaction, 44 Transaction, PAYMENT_OPTION_METHODS_VOCAB, PayPalCreditCardService, 45 PayPalRegularPaymentService, 28 46 ) 29 47 from waeup.ikoba.testing import ( 30 48 FunctionalLayer, FunctionalTestCase, 31 49 ) 50 32 51 33 52 # … … 118 137 assert paypalrestsdk.api.__api__.client_id == 'my-special-id' 119 138 139 def test_get_payment_invalid_intent(self): 140 # only 'sale' is currently allowed 141 self.assertRaises( 142 ValueError, get_payment, intent='invalid') 143 self.assertRaises( 144 ValueError, get_payment, intent='order') 145 146 def test_to_dict(self): 147 # we can turn objects into dicts 148 class C1(object): 149 a = 1 150 b = "somestring" 151 c = u"unicodestring" 152 d = None 153 obj = C1() 154 self.assertEqual( 155 to_dict(obj), 156 {'a': u"1", 157 'b': u"somestring", 158 'c': u"unicodestring", 159 } 160 ) 161 162 def test_to_dict_map(self): 163 # we can map attribute names 164 class C1(object): 165 a = 1 166 b = "somestring" 167 obj = C1() 168 self.assertEqual( 169 to_dict(obj, name_map={'a': 'replaced_a'}), 170 {'replaced_a': u"1", 171 'b': u"somestring", 172 }) 173 174 def test_to_dict_lists(self): 175 # to_dict can handle lists 176 class C1(object): 177 a = 1 178 179 def to_dict(self): 180 return to_dict(self) 181 182 obj1 = C1() 183 obj2 = C1() 184 obj2.a = 2 185 obj3 = C1() 186 obj3.a = 3 187 obj1.foo = [obj2, obj3] 188 self.assertEqual( 189 to_dict(obj1), 190 { 191 'a': u'1', 192 'foo': [ 193 {'a': u'2'}, 194 {'a': u'3'}, 195 ] 196 } 197 ) 198 199 def test_to_dict_decimals(self): 200 # decimals are converted to numbers with 2 decimals 201 class C1(object): 202 a = decimal.Decimal("0.1") 203 self.assertEqual( 204 to_dict(C1()), 205 { 206 'a': u"0.10", 207 } 208 ) 209 210 211 class PayerTests(unittest.TestCase): 212 213 def test_create(self): 214 # we can create payer objects 215 payer = Payer(payment_method='paypal') 216 assert payer.payment_method == 'paypal' 217 assert payer.funding_instruments == [] 218 assert payer.payer_info is None 219 assert payer.status is None 220 221 def test_init_invalid_payment_meth(self): 222 # we must provide a valid payment method 223 payer = Payer(payment_method='paypal') 224 assert payer is not None 225 payer = Payer(payment_method='credit_card') 226 assert payer is not None 227 self.assertRaises( 228 ValueError, Payer, payment_method='invalid') 229 230 def test_init_invalid_payer_state(self): 231 # only certain values are allowed as payer states 232 payer = Payer(payment_method='paypal', status='VERIFIED') 233 assert payer is not None 234 payer = Payer(payment_method='paypal', status='UNVERIFIED') 235 assert payer is not None 236 self.assertRaises( 237 ValueError, Payer, payment_method='paypal', status='InVaLiD') 238 239 240 class PayerInfoTests(unittest.TestCase): 241 242 def test_create(self): 243 # we can create payer infos 244 info = PayerInfo() 245 assert info.email == '' 246 assert info.first_name == '' 247 assert info.last_name == '' 248 assert info.payer_id == '' 249 assert info.phone == '' 250 assert info.shipping_address is None 251 assert info.tax_id_type == '' 252 assert info.tax_id == '' 253 254 def test_init_invalid_tax_id_type(self): 255 # onyl certain tax id types are allowed 256 info = PayerInfo(tax_id_type='BR_CPF') 257 assert info is not None 258 info = PayerInfo(tax_id_type='BR_CNPJ') 259 assert info is not None 260 self.assertRaises( 261 ValueError, PayerInfo, tax_id_type='INVALID_TYPE') 262 263 264 class CreditCardTests(unittest.TestCase): 265 266 def test_iface(self): 267 # we fullfill any interface contracts 268 credit_card = CreditCard( 269 number=u"12345678", 270 credit_card_type="visa", 271 expire_month=4, 272 expire_year=2012, 273 ) 274 verifyClass(ICreditCard, CreditCard) 275 verifyObject(ICreditCard, credit_card) 276 277 def test_create(self): 278 # we can create CreditCard objects 279 credit_card = CreditCard( 280 number=u"12345678", 281 credit_card_type="visa", 282 expire_month=4, 283 expire_year=2012, 284 ) 285 assert credit_card.paypal_id is None 286 assert credit_card.payer_id is not None 287 assert credit_card.number == u"12345678" 288 assert credit_card.credit_card_type == "visa" 289 assert credit_card.expire_month == 4 290 assert credit_card.expire_year == 2012 291 assert credit_card.cvv2 is None 292 assert credit_card.first_name is None 293 assert credit_card.last_name is None 294 assert credit_card.billing_address is None 295 assert credit_card.state is None 296 assert credit_card.paypal_valid_until is None 297 298 def test_payer_id_given(self): 299 # we do not override given payer ids 300 credit_card = CreditCard( 301 number=u"12345678", 302 credit_card_type="visa", 303 expire_month=4, 304 expire_year=2012, 305 payer_id=u'MySpecialPayerId', 306 ) 307 assert credit_card.payer_id == u'MySpecialPayerId' 308 309 def test_payer_id_not_given(self): 310 # in case no payer id is given, we generate one 311 credit_card = CreditCard( 312 number=u"12345678", 313 credit_card_type="visa", 314 expire_month=4, 315 expire_year=2012, 316 ) 317 # our payer ids contain a leading 'PAYER_' and 32 hex digits 318 assert re.match('PAYER_[0-9a-f]{32}$', credit_card.payer_id) 319 320 def test_number_is_checked(self): 321 # we do not accept invalid numbers 322 self.assertRaises( 323 ValueError, CreditCard, 324 number=u"not-a-number", 325 credit_card_type="visa", 326 expire_month=4, 327 expire_year=2012, 328 ) 329 330 def test_to_str(self): 331 # we can turn CreditCard objects into dicts. 332 addr = Address( 333 line1=u"52 N Main ST", 334 city=u"Johnstown", 335 state=u"OH", 336 postal_code=u"43210", 337 country_code=u"US") 338 credit_card = CreditCard( 339 credit_card_type=u"visa", 340 payer_id=u"PAYER_0123456789012345678901", 341 number=u"4417119669820331", 342 expire_month=11, 343 expire_year=2018, 344 cvv2=u"874", 345 first_name=u"Joe", 346 last_name=u"Shopper", 347 billing_address=addr) 348 self.assertEqual( 349 credit_card.to_dict(), 350 { 351 "type": u"visa", 352 "number": u"4417119669820331", 353 "payer_id": u"PAYER_0123456789012345678901", 354 "expire_month": u"11", 355 "expire_year": u"2018", 356 "cvv2": u"874", 357 "first_name": u"Joe", 358 "last_name": u"Shopper", 359 "billing_address": { 360 "line1": u"52 N Main ST", 361 "city": u"Johnstown", 362 "state": u"OH", 363 "postal_code": u"43210", 364 "country_code": u"US"} 365 } 366 ) 367 368 369 class CreditCardTokenTests(unittest.TestCase): 370 371 def test_iface(self): 372 # we fullfill any interface contracts 373 token = CreditCardToken( 374 credit_card_id=u"12345678", 375 ) 376 verifyClass(ICreditCardToken, CreditCardToken) 377 verifyObject(ICreditCardToken, token) 378 379 def test_create(self): 380 # we can create CreditCardToken objects 381 token = CreditCardToken( 382 credit_card_id=u"12345678", 383 ) 384 assert token.credit_card_id == u"12345678" 385 assert token.payer_id is None 386 assert token.credit_card_type is None 387 assert token.expire_month is None 388 assert token.expire_year is None 389 assert token.last4 is None 390 391 def test_payer_id_given(self): 392 # we do not override given payer ids 393 token = CreditCardToken( 394 credit_card_id=u"12345678", 395 payer_id=u'MySpecialPayerId', 396 ) 397 assert token.payer_id == u'MySpecialPayerId' 398 399 def test_to_str(self): 400 # we can turn CreditCardToken objects into dicts. 401 token = CreditCardToken( 402 credit_card_type=u"visa", 403 payer_id=u"PAYER_0123456789012345678901", 404 credit_card_id=u"12345678", 405 last4="8901", 406 expire_month=11, 407 expire_year=2018, 408 ) 409 self.assertEqual( 410 token.to_dict(), 411 { 412 "credit_card_id": u"12345678", 413 "payer_id": u"PAYER_0123456789012345678901", 414 "last4": u"8901", 415 "type": u"visa", 416 "expire_month": u"11", 417 "expire_year": u"2018", 418 } 419 ) 420 421 422 class FundingInstrumentTests(unittest.TestCase): 423 424 def test_create(self): 425 # we can create FundingInstrument objects 426 token = CreditCardToken( 427 credit_card_id=u"12345678", 428 ) 429 instr = FundingInstrument(credit_card_token=token) 430 assert instr.credit_card_token is token 431 432 def test_require_credit_card_or_token(self): 433 # we require a credit card object or a token. 434 credit_card = CreditCard( 435 number=u"12345678", 436 credit_card_type="visa", 437 expire_month=4, 438 expire_year=2012, 439 ) 440 token = CreditCardToken( 441 credit_card_id=u"12345678", 442 ) 443 self.assertRaises( 444 ValueError, FundingInstrument, 445 credit_card=credit_card, 446 credit_card_token=token 447 ) 448 self.assertRaises( 449 ValueError, FundingInstrument 450 ) 451 452 def test_to_dict(self): 453 # we can turn Funding instruments into dicts 454 token = CreditCardToken( 455 credit_card_type=u"visa", 456 payer_id=u"PAYER_0123456789012345678901", 457 credit_card_id=u"12345678", 458 last4="8901", 459 expire_month=11, 460 expire_year=2018, 461 ) 462 instr = FundingInstrument(credit_card_token=token) 463 result = instr.to_dict() 464 self.assertEqual( 465 result, 466 { 467 "credit_card_token": { 468 "credit_card_id": u"12345678", 469 "payer_id": u"PAYER_0123456789012345678901", 470 "last4": u"8901", 471 "type": u"visa", 472 "expire_month": u"11", 473 "expire_year": u"2018", 474 } 475 } 476 ) 477 478 479 class AddressTests(unittest.TestCase): 480 481 def test_iface(self): 482 # we fullfill any interface contracts 483 addr = Address( 484 line1=u'Address Line 1', 485 city=u'Somecity', 486 country_code=u'AT', 487 ) 488 verifyClass(IAddress, Address) 489 verifyObject(IAddress, addr) 490 491 def test_create(self): 492 # we can create addresses 493 addr = Address( 494 line1=u'Honey Street 1', 495 city=u'Beartown', 496 country_code=u'GB', 497 ) 498 assert addr.line1 == u'Honey Street 1' 499 assert addr.line2 is None 500 assert addr.city == u'Beartown' 501 assert addr.country_code == u'GB' 502 assert addr.postal_code is None 503 assert addr.state is None 504 assert addr.phone is None 505 506 def test_to_dict(self): 507 # we can turn addresses into dicts 508 addr = Address( 509 line1=u'Honey Street 1', 510 city=u'Beartown', 511 country_code=u'GB', 512 ) 513 self.assertEqual( 514 addr.to_dict(), 515 { 516 'line1': u'Honey Street 1', 517 'city': u'Beartown', 518 'country_code': u'GB', 519 } 520 ) 521 addr.line2 = u"Behind little tree" 522 self.assertEqual( 523 addr.to_dict(), 524 { 525 'line1': u'Honey Street 1', 526 'line2': u'Behind little tree', 527 'city': u'Beartown', 528 'country_code': u'GB', 529 } 530 ) 531 532 533 class ShippingAddressTests(unittest.TestCase): 534 535 def test_iface(self): 536 # we fullfill any interface contracts 537 addr = ShippingAddress( 538 recipient_name=u'Foo Bar', 539 line1=u'Address Line 1', 540 city=u'Somecity', 541 country_code=u'AT', 542 ) 543 verifyClass(IShippingAddress, ShippingAddress) 544 verifyObject(IShippingAddress, addr) 545 546 def test_create(self): 547 # we can create shipping addresses 548 addr = ShippingAddress( 549 recipient_name=u'Rob Receiver', 550 line1=u'Honey Street 1', 551 city=u'Beartown', 552 country_code=u'GB', 553 ) 554 assert addr.recipient_name == u'Rob Receiver' 555 assert addr.type == u'residential' 556 assert addr.line1 == u'Honey Street 1' 557 assert addr.line2 is None 558 assert addr.city == u'Beartown' 559 assert addr.country_code == u'GB' 560 assert addr.postal_code is None 561 assert addr.state is None 562 assert addr.phone is None 563 564 def test_to_dict(self): 565 # we can turn shipping addresses into dicts 566 addr = ShippingAddress( 567 recipient_name=u'Rob Receiver', 568 line1=u'Honey Street 1', 569 city=u'Beartown', 570 country_code=u'GB', 571 ) 572 self.assertEqual( 573 addr.to_dict(), 574 { 575 'recipient_name': u'Rob Receiver', 576 'type': u'residential', 577 'line1': u'Honey Street 1', 578 'city': u'Beartown', 579 'country_code': u'GB', 580 } 581 ) 582 addr.line2 = u"Behind little tree" 583 self.assertEqual( 584 addr.to_dict(), 585 { 586 'recipient_name': u'Rob Receiver', 587 'type': u'residential', 588 'line1': u'Honey Street 1', 589 'line2': u'Behind little tree', 590 'city': u'Beartown', 591 'country_code': u'GB', 592 } 593 ) 594 595 596 class AmountDetailsTests(unittest.TestCase): 597 598 def test_create(self): 599 # we can create AmountDetail objects 600 details = AmountDetails() 601 assert details.shipping is None 602 assert details.subtotal is None 603 assert details.tax is None 604 assert details.fee is None 605 assert details.handling_fee is None 606 assert details.insurance is None 607 assert details.shipping_discount is None 608 609 def test_to_dict(self): 610 # we can turn AmountDetails into a dict 611 details = AmountDetails( 612 shipping=decimal.Decimal("0.10"), 613 tax=decimal.Decimal("0.30"), 614 fee=decimal.Decimal("0.40"), 615 handling_fee=decimal.Decimal("0.50"), 616 insurance=decimal.Decimal("0.60"), 617 shipping_discount=decimal.Decimal("0.70") 618 ) 619 self.assertEqual( 620 details.to_dict(), 621 { 622 'shipping': u"0.10", 623 'subtotal': u"1.20", 624 'tax': u"0.30", 625 'fee': u"0.40", 626 'handling_fee': u"0.50", 627 'insurance': u"0.60", 628 'shipping_discount': u"0.70" 629 } 630 ) 631 632 def test_subtotal_all_none(self): 633 # if all items are none, also subtotal is none 634 details = AmountDetails( 635 shipping=None, tax=None, fee=None, handling_fee=None, 636 insurance=None, shipping_discount=None, 637 ) 638 assert details.subtotal is None 639 details.shipping_discount = decimal.Decimal("1.00") 640 assert details.subtotal == decimal.Decimal("-1.00") 641 642 def test_subtotal_sum(self): 643 # subtotal sums up correctly 644 details = AmountDetails( 645 shipping=decimal.Decimal("0.05"), 646 tax=decimal.Decimal("0.40"), 647 fee=decimal.Decimal("3.00"), 648 handling_fee=decimal.Decimal("20.00"), 649 insurance=decimal.Decimal("100.00"), 650 shipping_discount=None 651 ) 652 self.assertEqual(details.subtotal, decimal.Decimal("123.45")) 653 details.shipping_discount = decimal.Decimal("0.00") 654 self.assertEqual(details.subtotal, decimal.Decimal("123.45")) 655 details.shipping_discount = decimal.Decimal("23.45") 656 self.assertEqual(details.subtotal, decimal.Decimal("100.00")) 657 658 659 class AmountTests(unittest.TestCase): 660 661 def test_iface(self): 662 # we fullfill any interface contracts. 663 amount = Amount() 664 verifyClass(IAmount, Amount) 665 verifyObject(IAmount, amount) 666 667 def test_create(self): 668 # we can create amount objects 669 details = AmountDetails( 670 shipping=decimal.Decimal("0.05"), 671 tax=decimal.Decimal("0.40"), 672 fee=decimal.Decimal("3.00"), 673 handling_fee=decimal.Decimal("20.00"), 674 insurance=decimal.Decimal("100.00"), 675 shipping_discount=None 676 ) 677 amount = Amount( 678 total=decimal.Decimal("12.12"), 679 currency="USD", 680 details=details 681 ) 682 assert amount.total == decimal.Decimal("12.12") 683 assert amount.currency == "USD" 684 assert amount.details is details 685 686 def test_to_dict(self): 687 # we can turn Amount objects into dicts 688 self.maxDiff = None 689 details = AmountDetails( 690 shipping=decimal.Decimal("0.05"), 691 tax=decimal.Decimal("0.40"), 692 fee=decimal.Decimal("3.00"), 693 handling_fee=decimal.Decimal("20.00"), 694 insurance=decimal.Decimal("100.00"), 695 shipping_discount=None 696 ) 697 amount = Amount( 698 total=decimal.Decimal("12.12"), 699 currency="USD", 700 details=details 701 ) 702 self.assertEqual( 703 amount.to_dict(), 704 { 705 'total': u'12.12', 706 'currency': u'USD', 707 'details': { 708 'shipping': u'0.05', 709 'subtotal': u'123.45', 710 'tax': u'0.40', 711 'fee': u'3.00', 712 'handling_fee': u'20.00', 713 'insurance': u'100.00', 714 } 715 } 716 ) 717 718 719 class ItemTests(unittest.TestCase): 720 721 def test_iface(self): 722 # we fullfill all interface contracts 723 item = Item(name=u"Splendid Item") 724 verifyClass(IItem, Item) 725 verifyObject(IItem, item) 726 727 def test_create(self): 728 # we can create Item objects 729 item = Item( 730 quantity=3, 731 name=u"Splendid Thing", 732 price=decimal.Decimal("1.1"), 733 currency="USD", 734 sku="pcs", 735 description=u"Soo splendid!", 736 tax=decimal.Decimal("0.1"), 737 ) 738 assert item.quantity == 3 739 assert item.name == u"Splendid Thing" 740 assert item.price == decimal.Decimal("1.1") 741 assert item.currency == "USD" 742 assert item.sku == "pcs" 743 assert item.description == u"Soo splendid!" 744 assert item.tax == decimal.Decimal("0.1") 745 746 def test_to_dict(self): 747 # we can turn Item objects into dicts 748 item = Item( 749 quantity=3, 750 name=u"Splendid Thing", 751 price=decimal.Decimal("1.1"), 752 currency="USD", 753 sku="pcs", 754 description=u"Soo splendid!", 755 tax=decimal.Decimal("0.1"), 756 ) 757 self.assertEqual( 758 item.to_dict(), 759 { 760 "quantity": u"3", 761 "name": u"Splendid Thing", 762 "price": u"1.10", 763 "currency": u"USD", 764 "sku": u"pcs", 765 "description": u"Soo splendid!", 766 "tax": u"0.10" 767 } 768 ) 769 770 771 class ItemListTests(unittest.TestCase): 772 773 def test_iface(self): 774 # we fullfill all interface contracts 775 item_list = ItemList() 776 verifyClass(IItemList, ItemList) 777 verifyObject(IItemList, item_list) 778 779 def test_create_minimal(self): 780 # we can create ItemLists with a minimum of params 781 item_list = ItemList() 782 assert item_list.shipping_address is None 783 assert item_list.items == [] 784 785 def test_create(self): 786 # we can create ItemLists 787 item1 = Item( 788 name=u"Splendid Thing", 789 ) 790 item2 = Item( 791 name=u"Other Splendid Thing", 792 ) 793 addr = ShippingAddress( 794 recipient_name=u'Rob Receiver', 795 line1=u'Honey Street 1', 796 city=u'Beartown', 797 country_code=u'GB', 798 ) 799 item_list = ItemList( 800 shipping_address=addr, 801 items=[item1, item2]) 802 assert item_list.shipping_address is addr 803 assert item_list.items == [item1, item2] 804 805 def test_to_dict(self): 806 # we can turn ITemLists into dicts 807 item = Item( 808 quantity=3, 809 name=u"Splendid Thing", 810 price=decimal.Decimal("1.1"), 811 currency="USD", 812 sku="pcs", 813 description=u"Soo splendid!", 814 tax=decimal.Decimal("0.1"), 815 ) 816 addr = ShippingAddress( 817 recipient_name=u'Rob Receiver', 818 line1=u'Honey Street 1', 819 city=u'Beartown', 820 country_code=u'GB', 821 ) 822 item_list = ItemList(items=[item, ], shipping_address=addr) 823 self.assertEqual( 824 item_list.to_dict(), 825 { 826 "items": [ 827 { 828 "quantity": u"3", 829 "name": u"Splendid Thing", 830 "price": u"1.10", 831 "currency": u"USD", 832 "sku": u"pcs", 833 "description": u"Soo splendid!", 834 "tax": u"0.10" 835 } 836 ], 837 "shipping_address": 838 { 839 'recipient_name': u'Rob Receiver', 840 'type': u'residential', 841 'line1': u'Honey Street 1', 842 'city': u'Beartown', 843 'country_code': u'GB', 844 } 845 } 846 ) 847 848 849 class PaymentOptionsTests(unittest.TestCase): 850 851 def test_iface(self): 852 # we fullfill all interface contracts 853 opts = PaymentOptions() 854 verifyClass(IPaymentOptions, PaymentOptions) 855 verifyObject(IPaymentOptions, opts) 856 857 def test_create(self): 858 # we can create PaymentOptions objects 859 opts = PaymentOptions() 860 assert opts.allowed_payment_method is None 861 862 def test_allowed_payment_method_checked_in_init(self): 863 # any value apart from None, INSTANT... is rejected in __init__ 864 self.assertRaises( 865 ValueError, 866 PaymentOptions, allowed_payment_method='NoTvAlID') 867 868 def test_to_dict(self): 869 # we can turn PaymentOptions into dicts 870 opts = PaymentOptions( 871 allowed_payment_method="INSTANT_FUNDING_SOURCE") 872 self.assertEqual( 873 opts.to_dict(), 874 { 875 'allowed_payment_method': "INSTANT_FUNDING_SOURCE", 876 } 877 ) 878 879 880 class TransactionTests(unittest.TestCase): 881 882 def test_iface(self): 883 # we fullfill all interface contracts 884 amount = Amount() 885 transaction = Transaction(amount=amount) 886 verifyClass(ITransaction, Transaction) 887 verifyObject(ITransaction, transaction) 888 889 def test_create(self): 890 # we can create transacions 891 amount = Amount() 892 transaction = Transaction(amount=amount) 893 assert transaction.amount is amount 894 assert transaction.description is None 895 assert transaction.item_list is None 896 assert transaction.related_resources == [] 897 assert transaction.invoice_number is None 898 assert transaction.custom is None 899 assert transaction.soft_descriptor is None 900 assert transaction.payment_options is None 901 902 def test_to_dict(self): 903 # we can turn Transaction objects into dicts 904 transaction = Transaction( 905 amount=Amount(), 906 description=u"My description", 907 item_list=ItemList(), 908 related_resources=[], 909 invoice_number=u"12345", 910 custom=u"Some custom remark", 911 soft_descriptor=u"softdescriptor?", 912 payment_options=PaymentOptions( 913 allowed_payment_method="INSTANT_FUNDING_SOURCE"), 914 ) 915 self.assertEqual( 916 transaction.to_dict(), { 917 'amount': {'currency': u'USD', 'total': u'0.00'}, 918 'custom': u'Some custom remark', 919 'description': u'My description', 920 'invoice_number': u'12345', 921 'item_list': { 922 'items': [] 923 }, 924 'payment_options': { 925 'allowed_payment_method': u'INSTANT_FUNDING_SOURCE' 926 }, 927 'related_resources': [], 928 'soft_descriptor': u'softdescriptor?' 929 } 930 ) 931 932 933 class AddressTypesVocabTests(unittest.TestCase): 934 935 def test_address_types_vocab_tokenized(self): 936 # we can get a countries source suitable for forms etc. 937 verifyObject(IVocabularyTokenized, ADDRESS_TYPES_VOCAB) 938 939 def test_address_types_vocab_i18nized(self): 940 # vocab titles are i18nized 941 result = ADDRESS_TYPES_VOCAB.getTerm('residential') 942 assert ITerm.providedBy(result) 943 self.assertEqual(result.title, u'residential') 944 assert isinstance(result.title, i18nMessage) 945 946 def test_address_types_vocab_tokens_are_string(self): 947 # vocab tokens are simple strings 948 result = ADDRESS_TYPES_VOCAB.getTerm('residential') 949 assert ITerm.providedBy(result) 950 assert result.token == result.value 951 assert result.value == 'residential' 952 assert isinstance(result.token, str) 953 assert isinstance(result.value, str) 954 955 956 class CreditCardTypesVocabTests(unittest.TestCase): 957 958 def test_credit_card_types_vocab_tokenized(self): 959 # we can get a countries source suitable for forms etc. 960 verifyObject(IVocabularyTokenized, CREDIT_CARD_TYPES_VOCAB) 961 962 def test_credit_cards_types_vocab_i18nized(self): 963 # vocab titles are i18nized 964 result = CREDIT_CARD_TYPES_VOCAB.getTerm('visa') 965 assert ITerm.providedBy(result) 966 self.assertEqual(result.title, u'visa') 967 assert isinstance(result.title, i18nMessage) 968 969 def test_credit_cards_types_vocab_tokens_are_string(self): 970 # vocab tokens are simple strings 971 result = CREDIT_CARD_TYPES_VOCAB.getTerm('visa') 972 assert ITerm.providedBy(result) 973 assert result.token == result.value 974 assert result.value == 'visa' 975 assert isinstance(result.token, str) 976 assert isinstance(result.value, str) 977 978 979 class CreditcardstatusVocabTests(unittest.TestCase): 980 981 def test_credit_card_status_vocab_tokenized(self): 982 # we can get a countries source suitable for forms etc. 983 verifyObject(IVocabularyTokenized, CREDIT_CARD_STATUS_VOCAB) 984 985 def test_credit_cards_status_vocab_i18nized(self): 986 # vocab titles are i18nized 987 result = CREDIT_CARD_STATUS_VOCAB.getTerm('ok') 988 assert ITerm.providedBy(result) 989 self.assertEqual(result.title, u'ok') 990 assert isinstance(result.title, i18nMessage) 991 992 def test_credit_cards_status_vocab_tokens_are_string(self): 993 # vocab tokens are simple strings 994 result = CREDIT_CARD_STATUS_VOCAB.getTerm('expired') 995 assert ITerm.providedBy(result) 996 assert result.token == result.value 997 assert result.value == 'expired' 998 assert isinstance(result.token, str) 999 assert isinstance(result.value, str) 1000 1001 1002 class StockKeepingUnitsVocabTests(unittest.TestCase): 1003 1004 def test_sku_vocab_tokenized(self): 1005 # we can get a tokenzed vocab for stock keeping units 1006 verifyObject(IVocabularyTokenized, STOCK_KEEPING_UNITS_VOCAB) 1007 1008 def test_sku_vocab_i18nized(self): 1009 # vocab titles are i18nized 1010 result = STOCK_KEEPING_UNITS_VOCAB.getTerm('pcs') 1011 assert ITerm.providedBy(result) 1012 self.assertEqual(result.title, u'pieces') 1013 assert isinstance(result.title, i18nMessage) 1014 1015 def test_sku_vocab_tokens_are_string(self): 1016 # vocab tokens are simple strings 1017 result = STOCK_KEEPING_UNITS_VOCAB.getTerm('pcs') 1018 assert ITerm.providedBy(result) 1019 assert result.token == result.value 1020 assert result.value == 'pcs' 1021 assert isinstance(result.token, str) 1022 assert isinstance(result.value, str) 1023 1024 1025 class PaymentOptionMethodsVocabTests(unittest.TestCase): 1026 1027 def test_payment_option_methods_vocab_tokenized(self): 1028 # we can get a countries source suitable for forms etc. 1029 verifyObject(IVocabularyTokenized, PAYMENT_OPTION_METHODS_VOCAB) 1030 1031 def test_payment_option_methods_vocab_i18nized(self): 1032 # vocab titles are i18nized 1033 result = PAYMENT_OPTION_METHODS_VOCAB.getTerm('INSTANT_FUNDING_SOURCE') 1034 assert ITerm.providedBy(result) 1035 self.assertEqual(result.title, u'INSTANT_FUNDING_SOURCE') 1036 assert isinstance(result.title, i18nMessage) 1037 1038 def test_payment_option_methods_vocab_tokens_are_string(self): 1039 # vocab tokens are simple strings 1040 result = PAYMENT_OPTION_METHODS_VOCAB.getTerm('INSTANT_FUNDING_SOURCE') 1041 assert ITerm.providedBy(result) 1042 assert result.token == result.value 1043 assert result.value == 'INSTANT_FUNDING_SOURCE' 1044 assert isinstance(result.token, str) 1045 assert isinstance(result.value, str) 1046 1047 1048 class FakePayer(object): 1049 1050 implements(IPayer) 1051 1052 payer_id = 'PAYER-123' 1053 1054 1055 class FakePayee(object): 1056 1057 implements(IPayee) 1058 1059 payee_id = 'PAYEE-456' 1060 1061 1062 class FakePaymentItem(object): 1063 1064 implements(IPaymentItem) 1065 1066 item_id = 'BILL-123456' 1067 amount = decimal.Decimal("12.10") 1068 currency = 'EUR' 1069 1070 1071 class PayPalCreditCardServiceTests(unittest.TestCase): 1072 1073 def test_iface(self): 1074 # we fullfill all interface contracts 1075 service = PayPalCreditCardService() 1076 verifyClass(IPaymentGatewayService, PayPalCreditCardService) 1077 verifyObject(IPaymentGatewayService, service) 1078 1079 def DIStest_creditcard_service_can_create_payment(self): 1080 # we can create IPayment objects with creditcard service 1081 service = PayPalCreditCardService() 1082 payment = service.create_payment( 1083 payer=FakePayer(), 1084 payment_item=FakePaymentItem(), 1085 payee=FakePayee() 1086 ) 1087 assert IPayment.providedBy(payment) 1088 120 1089 121 1090 class FunctionalPaypalTests(FunctionalTestCase): … … 136 1105 result = payment.create() 137 1106 assert result is True 1107 1108 def test_paypal_services_registered(self): 1109 # the PayPal gateway services are all registered 1110 creditcard_service = queryUtility( 1111 IPaymentGatewayService, name=u'paypal_creditcard') 1112 assert creditcard_service is not None 1113 assert isinstance(creditcard_service, PayPalCreditCardService) 1114 paypal_regular_service = queryUtility( 1115 IPaymentGatewayService, name=u'paypal_regular') 1116 assert paypal_regular_service is not None 1117 assert isinstance(paypal_regular_service, PayPalRegularPaymentService)
Note: See TracChangeset for help on using the changeset viewer.