diff --git a/CHANGELOG.md b/CHANGELOG.md index 8acb0e0..aaabb0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 4.26.0 +* Remove usage of standard library deprecated `cgi` module. _Note: this will break integrations on versions of Python below 3.2. However, this is NOT a breaking change to this library, due to our current support of Python 3.5+._ +* Add `PackageDetails` class. +* Add `packages` to `Transaction` attributes. +* Add `package_tracking` method to `TransactionGateway` to make request to add tracking information to transactions. +* Add `process_debit_as_credit` to `credit_card` field in `options` field during Transaction create. +* Deprecate `three_d_secure_token` in favor of `three_d_secure_authentication_id` +* Add `upc_code`, `upc_type`, and `image_url` to `line_items` in `transaction` +* Deprecate `venmo_sdk_session` and `venmo_sdk_payment_method_code` + ## 4.25.0 * Add `PickupInStore` to `ShippingMethod` enum * Add `external_vault` and `risk_data` to `CreditCardVerification.create` request diff --git a/braintree/apple_pay_gateway.py b/braintree/apple_pay_gateway.py index bb5cea6..0c2dfff 100644 --- a/braintree/apple_pay_gateway.py +++ b/braintree/apple_pay_gateway.py @@ -1,8 +1,4 @@ -try: - from html import escape -except ImportError: - from cgi import escape - +from html import escape from braintree.apple_pay_options import ApplePayOptions from braintree.error_result import ErrorResult from braintree.successful_result import SuccessfulResult diff --git a/braintree/credit_card.py b/braintree/credit_card.py index 7b9820c..bf59976 100644 --- a/braintree/credit_card.py +++ b/braintree/credit_card.py @@ -238,7 +238,7 @@ def signature(type): "fail_on_duplicate_payment_method", "make_default", "skip_advanced_fraud_checking", - "venmo_sdk_session", + "venmo_sdk_session", # NEXT_MJOR_VERSION remove venmo_sdk_session "verification_account_type", "verification_amount", "verification_merchant_account_id", @@ -268,7 +268,7 @@ def signature(type): "expiration_year", "number", "token", - "venmo_sdk_payment_method_code", + "venmo_sdk_payment_method_code", # NEXT_MJOR_VERSION remove venmo_sdk_payment_method_code "device_data", "payment_method_nonce", "device_session_id", "fraud_merchant_id", # NEXT_MAJOR_VERSION remove device_session_id and fraud_merchant_id diff --git a/braintree/credit_card_gateway.py b/braintree/credit_card_gateway.py index 18adec5..673d33e 100644 --- a/braintree/credit_card_gateway.py +++ b/braintree/credit_card_gateway.py @@ -89,8 +89,11 @@ def _post(self, url, params=None): elif "api_error_response" in response: return ErrorResult(self.gateway, response["api_error_response"]) + # NEXT_MAJOR_VERSION remove these checks when the attributes are removed def __check_for_deprecated_attributes(self, params): if "device_session_id" in params.keys(): warnings.warn("device_session_id is deprecated, use device_data parameter instead", DeprecationWarning) if "fraud_merchant_id" in params.keys(): warnings.warn("fraud_merchant_id is deprecated, use device_data parameter instead", DeprecationWarning) + if "venmo_sdk_payment_method_code" in params.keys() or "venmo_sdk_session" in params.keys(): + warnings.warn("The Venmo SDK integration is Unsupported. Please update your integration to use Pay with Venmo instead.", DeprecationWarning) diff --git a/braintree/error_codes.py b/braintree/error_codes.py index 202dd8c..620ba8c 100644 --- a/braintree/error_codes.py +++ b/braintree/error_codes.py @@ -107,7 +107,7 @@ class CreditCard(object): ExpirationMonthIsInvalid = "81712" ExpirationYearIsInvalid = "81713" InvalidParamsForCreditCardUpdate = "91745" - InvalidVenmoSDKPaymentMethodCode = "91727" + InvalidVenmoSDKPaymentMethodCode = "91727" # NEXT_MJOR_VERSION remove this code NumberHasInvalidLength = NumberLengthIsInvalid = "81716" NumberIsInvalid = "81715" NumberIsProhibited = "81750" @@ -129,7 +129,7 @@ class CreditCard(object): TokenIsNotAllowed = "91721" TokenIsRequired = "91722" TokenIsTooLong = "91720" - VenmoSDKPaymentMethodCodeCardTypeIsNotAccepted = "91726" + VenmoSDKPaymentMethodCodeCardTypeIsNotAccepted = "91726" # NEXT_MJOR_VERSION remove this code VerificationNotSupportedOnThisMerchantAccount = "91730" VerificationAccountTypeIsInvald = "91757" VerificationAccountTypeNotSupported = "91758" @@ -558,7 +558,7 @@ class Transaction(object): PaymentInstrumentTypeIsNotAccepted = "915101" PaymentInstrumentWithExternalVaultIsInvalid = "915176" PaymentMethodConflict = "91515" - PaymentMethodConflictWithVenmoSDK = "91549" + PaymentMethodConflictWithVenmoSDK = "91549" # NEXT_MJOR_VERSION remove this code PaymentMethodDoesNotBelongToCustomer = "91516" PaymentMethodDoesNotBelongToSubscription = "91527" PaymentMethodNonceCardTypeIsNotAccepted = "91567" @@ -740,9 +740,9 @@ class AdditionalCharge(object): class LineItem(object): CommodityCodeIsTooLong = "95801" DescriptionIsTooLong = "95803" + DiscountAmountCannotBeNegative = "95806" DiscountAmountFormatIsInvalid = "95804" DiscountAmountIsTooLarge = "95805" - DiscountAmountCannotBeNegative = "95806" KindIsInvalid = "95807" KindIsRequired = "95808" NameIsRequired = "95822" @@ -751,21 +751,25 @@ class LineItem(object): QuantityFormatIsInvalid = "95810" QuantityIsRequired = "95811" QuantityIsTooLarge = "95812" + TaxAmountCannotBeNegative = "95829" + TaxAmountFormatIsInvalid = "95827" + TaxAmountIsTooLarge = "95828" TotalAmountFormatIsInvalid = "95813" TotalAmountIsRequired = "95814" TotalAmountIsTooLarge = "95815" TotalAmountMustBeGreaterThanZero = "95816" + UPCCodeIsMissing = "95830" + UPCCodeIsTooLong = "95831" + UPCTypeIsInvalid = "95833" + UPCTypeIsMissing = "95832" UnitAmountFormatIsInvalid = "95817" UnitAmountIsRequired = "95818" UnitAmountIsTooLarge = "95819" UnitAmountMustBeGreaterThanZero = "95820" UnitOfMeasureIsTooLarge = "95821" + UnitTaxAmountCannotBeNegative = "95826" UnitTaxAmountFormatIsInvalid = "95824" UnitTaxAmountIsTooLarge = "95825" - UnitTaxAmountCannotBeNegative = "95826" - TaxAmountFormatIsInvalid = "95827" - TaxAmountIsTooLarge = "95828" - TaxAmountCannotBeNegative = "95829" class UsBankAccountVerification(object): NotConfirmable = "96101" diff --git a/braintree/package_details.py b/braintree/package_details.py new file mode 100644 index 0000000..9c95927 --- /dev/null +++ b/braintree/package_details.py @@ -0,0 +1,25 @@ +from braintree.attribute_getter import AttributeGetter + +class PackageDetails(AttributeGetter): + """ + A class representing the package tracking information of a transaction. + + An example of package details including all available fields:: + + result = braintree.PackageDetails.create({ + "id": "my_id", + "carrier": "a_carrier", + "tracking_number": "my_tracking_number", + "paypal_tracking_id": "my_paypal_tracking_id", + }) + + """ + detail_list = [ + "id", + "carrier", + "tracking_number", + "paypal_tracking_id", + ] + + def __init__(self, attributes): + AttributeGetter.__init__(self, attributes) diff --git a/braintree/payment_method.py b/braintree/payment_method.py index 6997745..718ae47 100644 --- a/braintree/payment_method.py +++ b/braintree/payment_method.py @@ -115,14 +115,14 @@ def update_signature(): "number", "payment_method_nonce", "token", - "venmo_sdk_payment_method_code", + "venmo_sdk_payment_method_code", # NEXT_MJOR_VERSION remove venmo_sdk_payment_method_code "device_session_id", "fraud_merchant_id", # NEXT_MAJOR_VERSION remove device_session_id and fraud_merchant_id { "options": [ "make_default", "skip_advanced_fraud_checking", "us_bank_account_verification_method", - "venmo_sdk_session", + "venmo_sdk_session", # NEXT_MJOR_VERSION remove venmo_sdk_session "verification_account_type", "verification_add_ons", "verification_amount", diff --git a/braintree/payment_method_gateway.py b/braintree/payment_method_gateway.py index 5cd475c..84005d1 100644 --- a/braintree/payment_method_gateway.py +++ b/braintree/payment_method_gateway.py @@ -151,8 +151,11 @@ def _parse_payment_method_nonce(self, response): return PaymentMethodNonce(self.gateway, response["payment_method_nonce"]) raise ValueError("payment_method_nonce not present in response") + # NEXT_MAJOR_VERSION remove these checks when the attributes are removed def __check_for_deprecated_attributes(self, params): if "device_session_id" in params.keys(): warnings.warn("device_session_id is deprecated, use device_data parameter instead", DeprecationWarning) if "fraud_merchant_id" in params.keys(): warnings.warn("fraud_merchant_id is deprecated, use device_data parameter instead", DeprecationWarning) + if "venmo_sdk_payment_method_code" in params.keys() or "venmo_sdk_session" in params.keys(): + warnings.warn("The Venmo SDK integration is Unsupported. Please update your integration to use Pay with Venmo instead.", DeprecationWarning) diff --git a/braintree/transaction.py b/braintree/transaction.py index a1ea623..d99b24f 100644 --- a/braintree/transaction.py +++ b/braintree/transaction.py @@ -32,6 +32,7 @@ from braintree.risk_data import RiskData from braintree.samsung_pay_card import SamsungPayCard from braintree.sepa_direct_debit_account import SepaDirectDebitAccount +from braintree.package_details import PackageDetails from braintree.status_event import StatusEvent from braintree.subscription_details import SubscriptionDetails from braintree.successful_result import SuccessfulResult @@ -132,6 +133,7 @@ def __repr__(self): "network_response_text", "network_transaction_id", "order_id", + "packages", "payment_instrument_type", "payment_method_token", "plan_id", @@ -529,8 +531,9 @@ def create_signature(): "device_data", "billing_address_id", "payment_method_nonce", "product_sku", "tax_amount", "shared_payment_method_token", "shared_customer_id", "shared_billing_address_id", "shared_shipping_address_id", "shared_payment_method_nonce", "discount_amount", "shipping_amount", "ships_from_postal_code", - "tax_exempt", "three_d_secure_authentication_id", "three_d_secure_token", "type", "venmo_sdk_payment_method_code", "service_fee_amount", - "sca_exemption","exchange_rate_quote_id", + "tax_exempt", "three_d_secure_authentication_id", "three_d_secure_token", # NEXT_MAJOR_VERSION Remove three_d_secure_token + "type", "venmo_sdk_payment_method_code", # NEXT_MJOR_VERSION remove venmo_sdk_payment_method_code + "service_fee_amount", "sca_exemption","exchange_rate_quote_id", "device_session_id", "fraud_merchant_id", # NEXT_MAJOR_VERSION remove device_session_id and fraud_merchant_id { "risk_data": [ @@ -582,7 +585,7 @@ def create_signature(): "store_in_vault_on_success", "store_shipping_address_in_vault", "submit_for_settlement", - "venmo_sdk_session", + "venmo_sdk_session", # NEXT_MJOR_VERSION remove venmo_sdk_session "payee_id", "payee_email", "skip_advanced_fraud_checking", @@ -590,7 +593,8 @@ def create_signature(): "skip_cvv", { "credit_card": [ - "account_type" + "account_type", + "process_debit_as_credit" ], "paypal": [ "payee_id", @@ -663,7 +667,7 @@ def create_signature(): }, {"line_items": [ - "quantity", "name", "description", "kind", "unit_amount", "unit_tax_amount", "total_amount", "discount_amount", "tax_amount", "unit_of_measure", "product_code", "commodity_code", "url", + "commodity_code", "description", "discount_amount", "image_url", "kind", "name", "product_code", "quantity", "tax_amount", "total_amount", "unit_amount", "unit_of_measure", "unit_tax_amount", "upc_code", "upc_type", "url", ] }, {"apple_pay_card": ["number", "cardholder_name", "cryptogram", "expiration_month", "expiration_year", "eci_indicator"]}, @@ -708,7 +712,7 @@ def submit_for_settlement_signature(): }, {"line_items": [ - "quantity", "name", "description", "kind", "unit_amount", "unit_tax_amount", "total_amount", "discount_amount", "tax_amount", "unit_of_measure", "product_code", "commodity_code", "url", + "commodity_code", "description", "discount_amount", "image_url", "kind", "name", "product_code", "quantity", "tax_amount", "total_amount", "unit_amount", "unit_of_measure", "unit_tax_amount", "upc_code", "upc_type", "url," ] }, {"shipping": @@ -743,6 +747,32 @@ def submit_for_settlement_signature(): }, ] + @staticmethod + def package_tracking_signature(): + return [ "carrier", "notify_payer", "tracking_number", + { "line_items": [ + "commodity_code", "description", "discount_amount", "image_url", "kind", "name", + "product_code", "quantity", "tax_amount", "total_amount", "unit_amount", "unit_of_measure", + "unit_tax_amount", "upc_code", "upc_type", "url" + ] + }, + ] + + @staticmethod + def package_tracking(transaction_id, params=None): + """ + Creates a request to send package tracking information for a transaction which has already submitted for settlement. + + Requires the transaction id of the transaction and the package tracking request details:: + + result = braintree.Transaction.package_tracking("my_transaction_id", params ) + + """ + if params is None: + params = {} + return Configuration.gateway().transaction.package_tracking(transaction_id, params) + + @staticmethod def update_details_signature(): return ["amount", "order_id", {"descriptor": ["name", "phone", "url"]}] @@ -779,6 +809,8 @@ def __init__(self, gateway, attributes): self.billing_details = Address(gateway, attributes.pop("billing")) if "credit_card" in attributes: self.credit_card_details = CreditCard(gateway, attributes.pop("credit_card")) + if "shipments" in attributes: + self.packages = [PackageDetails(detail) for detail in self.shipments] if "paypal" in attributes: self.paypal_details = PayPalAccount(gateway, attributes.pop("paypal")) if "paypal_here" in attributes: diff --git a/braintree/transaction_gateway.py b/braintree/transaction_gateway.py index 258a7fa..2a595d3 100644 --- a/braintree/transaction_gateway.py +++ b/braintree/transaction_gateway.py @@ -145,6 +145,21 @@ def submit_for_partial_settlement(self, transaction_id, amount, params=None): elif "api_error_response" in response: return ErrorResult(self.gateway, response["api_error_response"]) + def package_tracking(self, transaction_id, params=None): + try: + if params is None: + params = {} + if transaction_id is None or transaction_id.strip() == "": + raise NotFoundError() + Resource.verify_keys(params, Transaction.package_tracking_signature()) + response = self.config.http().post(self.config.base_merchant_path() + "/transactions/" + transaction_id + "/shipments", {"shipment": params}) + if "transaction" in response: + return SuccessfulResult({"transaction": Transaction(self.gateway, response["transaction"])}) + elif "api_error_response" in response: + return ErrorResult(self.gateway, response["api_error_response"]) + except NotFoundError: + raise NotFoundError("transaction with id " + repr(transaction_id) + " not found") + def void(self, transaction_id): response = self.config.http().put(self.config.base_merchant_path() + "/transactions/" + transaction_id + "/void") if "transaction" in response: @@ -179,8 +194,13 @@ def _post(self, url, params=None): elif "api_error_response" in response: return ErrorResult(self.gateway, response["api_error_response"]) + # NEXT_MAJOR_VERSION remove these checks when the attributes are removed def __check_for_deprecated_attributes(self, params): if "device_session_id" in params.keys(): warnings.warn("device_session_id is deprecated, use device_data parameter instead", DeprecationWarning) if "fraud_merchant_id" in params.keys(): warnings.warn("fraud_merchant_id is deprecated, use device_data parameter instead", DeprecationWarning) + if "three_d_secure_token" in params.keys(): + warnings.warn("three_d_secure_token is deprecated, use three_d_secure_authentication_id parameter instead", DeprecationWarning) + if "venmo_sdk_payment_method_code" in params.keys() or "venmo_sdk_session" in params.keys(): + warnings.warn("The Venmo SDK integration is Unsupported. Please update your integration to use Pay with Venmo instead.", DeprecationWarning) diff --git a/braintree/version.py b/braintree/version.py index 68911b9..bacec5b 100644 --- a/braintree/version.py +++ b/braintree/version.py @@ -1 +1 @@ -Version = "4.25.0" +Version = "4.26.0" diff --git a/setup.py b/setup.py index 80d3762..36f8d3c 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ setup( name="braintree", - version="4.25.0", + version="4.26.0", description="Braintree Python Library", long_description=long_description, author="Braintree", diff --git a/tests/integration/test_package_tracking.py b/tests/integration/test_package_tracking.py new file mode 100644 index 0000000..c40c50a --- /dev/null +++ b/tests/integration/test_package_tracking.py @@ -0,0 +1,110 @@ +import json +from tests.test_helper import * +from braintree.transaction import Transaction + +class PackageTracking(unittest.TestCase): + def setUp(self): + # Create successful transaction to obtain id + result = Transaction.sale({ + "amount": "100", + "options": { + "submit_for_settlement": True + }, + "paypal_account": { + "payer_id": "fake-payer-id", + "payment_id": "fake-payment-id", + }, + }) + self.transaction = result.transaction + + def test_package_tracking_returns_error_when_transaction_id_is_not_present(self): + with self.assertRaisesRegex(NotFoundError, "transaction with id ' ' not found"): + Transaction.package_tracking(" ") + + def test_package_tracking_returns_api_error_when_carrier_is_not_present(self): + # Create package without carrier + package_result = Transaction.package_tracking( + self.transaction.id, + { + "notify_payer": True, + "tracking_number": "1Z5338FF0107231059", + "line_items": [ + { + "product_code": "ABC 01", + "name": "Best Product Ever", + "quantity": "1", + "description": "Best Description Ever", + }, + ], + }) + self.assertFalse(package_result.is_success) + self.assertEqual(package_result.message, 'Carrier name is required.') + + def test_package_tracking_returns_api_error_when_tracking_number_is_not_present(self): + # Create package without tracking_number + package_result = Transaction.package_tracking( + self.transaction.id, + { + "notify_payer": True, + "carrier": "UPS", + "line_items": [ ], + }) + self.assertFalse(package_result.is_success) + self.assertEqual(package_result.message, 'Tracking number is required.') + + + def test_package_tracking_adds_information_and_returns_valid_response(self): + # Create first package with 2 products + package_result_1 = Transaction.package_tracking( + self.transaction.id, + { + "carrier": "UPS", + "notify_payer": True, + "tracking_number": "1Z5338FF0107231059", + "line_items": [ + { + "product_code": "ABC 01", + "name": "Best Product Ever", + "quantity": "1", + "description": "Best Description Ever", + "upc_type": "UPC-A", + "upc_code": "9248093u5", + "image_url": "https://example.com/image.png" + }, + { + "product_code": "ABC 02", + "name": "Second best product ever", + "quantity": "2", + "description": "Second best description ever", + "upc_type": "UPC-B", + "upc_code": "0586967ABD", + "image_url": "https://example.com/image2.png" + } + ], + }) + self.assertTrue(package_result_1.is_success) + self.assertIsNotNone(package_result_1.transaction.packages[0]) + self.assertEqual("UPS", package_result_1.transaction.packages[0].carrier) + self.assertEqual("1Z5338FF0107231059", package_result_1.transaction.packages[0].tracking_number) + + # Create second package with 1 more product + package_result_2 = Transaction.package_tracking( + self.transaction.id, + { + "carrier": "FEDEX", + "notify_payer": True, + "tracking_number": "08594809767HGH0L", + "line_items": [ + { + "product_code": "ABC 03", + "name": "Worst Product Ever", + "quantity": "25", + "description": "Worst Description Ever", + } + ], + }) + self.assertTrue(package_result_2.is_success) + self.assertIsNotNone(package_result_2.transaction.packages[1]) + self.assertEqual("FEDEX", package_result_2.transaction.packages[1].carrier) + self.assertEqual("08594809767HGH0L", package_result_2.transaction.packages[1].tracking_number) + self.assertIsNotNone(Transaction.find(package_result_2.transaction.id)) diff --git a/tests/integration/test_payment_method_nonce.py b/tests/integration/test_payment_method_nonce.py index 5b7227a..f4626b1 100644 --- a/tests/integration/test_payment_method_nonce.py +++ b/tests/integration/test_payment_method_nonce.py @@ -1,5 +1,6 @@ from tests.test_helper import * from braintree.test.nonces import Nonces +from datetime import date class TestPaymentMethodNonce(unittest.TestCase): indian_payment_token = "india_visa_credit" @@ -143,7 +144,7 @@ def test_find_nonce_shows_meta_checkout_card_details(self): self.assertEqual("1881", found_nonce.details["last_four"]) self.assertEqual("Visa", found_nonce.details["card_type"]) self.assertEqual("Meta Checkout Card Cardholder", found_nonce.details["cardholder_name"]) - self.assertEqual("2024", found_nonce.details["expiration_year"]) + self.assertEqual(str(date.today().year + 1), found_nonce.details["expiration_year"]) self.assertEqual("12", found_nonce.details["expiration_month"]) def test_find_nonce_shows_meta_checkout_token_details(self): @@ -154,7 +155,7 @@ def test_find_nonce_shows_meta_checkout_token_details(self): self.assertEqual("1881", found_nonce.details["last_four"]) self.assertEqual("Visa", found_nonce.details["card_type"]) self.assertEqual("Meta Checkout Token Cardholder", found_nonce.details["cardholder_name"]) - self.assertEqual("2024", found_nonce.details["expiration_year"]) + self.assertEqual(str(date.today().year + 1), found_nonce.details["expiration_year"]) self.assertEqual("12", found_nonce.details["expiration_month"]) def test_exposes_null_3ds_info_if_none_exists(self): diff --git a/tests/integration/test_transaction.py b/tests/integration/test_transaction.py index 05f1346..5ae32de 100644 --- a/tests/integration/test_transaction.py +++ b/tests/integration/test_transaction.py @@ -4,6 +4,7 @@ from braintree.test.nonces import Nonces from braintree.dispute import Dispute from braintree.payment_instrument_type import PaymentInstrumentType +from datetime import date class TestTransaction(unittest.TestCase): @@ -2417,6 +2418,126 @@ def test_sale_with_line_items_validation_error_unit_amount_must_be_greater_than_ result.errors.for_object("transaction").for_object("line_items").for_object("index_1").on("unit_amount")[0].code ) + def test_sale_with_line_items_accepts_valid_image_url_and_upc_code_and_type(self): + result = Transaction.sale({ + "amount": "35.05", + "credit_card": { + "number": CreditCardNumbers.Visa, + "expiration_date": "05/2009", + }, + "line_items": [{ + "quantity": "1.0232", + "name": "Name #1", + "kind": TransactionLineItem.Kind.Debit, + "unit_amount": "45.1232", + "unit_of_measure": "gallon", + "discount_amount": "1.02", + "total_amount": "45.15", + "product_code": "23434", + "commodity_code": "9SAASSD8724", + "upc_code": "1234567890", + "upc_type": "UPC-A", + "image_url": "https://google.com/image.png", + }] + }) + self.assertTrue(result.is_success) + line_items = result.transaction.line_items + + lineItem = line_items[0] + self.assertEqual("1234567890", lineItem.upc_code) + self.assertEqual("UPC-A", lineItem.upc_type) + + def test_sale_with_line_items_returns_validation_errors_for_upc_code_too_long_and_type_invalid(self): + result = Transaction.sale({ + "amount": "35.05", + "credit_card": { + "number": CreditCardNumbers.Visa, + "expiration_date": "05/2009", + }, + "line_items": [{ + "quantity": "1.0232", + "name": "Name #1", + "kind": TransactionLineItem.Kind.Debit, + "unit_amount": "45.1232", + "unit_of_measure": "gallon", + "discount_amount": "1.02", + "total_amount": "45.15", + "product_code": "23434", + "commodity_code": "9SAASSD8724", + "upc_code": "THISCODEISABITTOOLONG", + "upc_type": "invalid", + "image_url": "https://google.com/image.png", + }] + }) + + self.assertFalse(result.is_success) + + self.assertEqual( + ErrorCodes.Transaction.LineItem.UPCCodeIsTooLong, + result.errors.for_object("transaction").for_object("line_items").for_object("index_0").on("upc_code")[0].code + ) + self.assertEqual( + ErrorCodes.Transaction.LineItem.UPCTypeIsInvalid, + result.errors.for_object("transaction").for_object("line_items").for_object("index_0").on("upc_type")[0].code + ) + + def test_sale_with_line_items_returns_UPC_code_missing_error(self): + result = Transaction.sale({ + "amount": "35.05", + "credit_card": { + "number": CreditCardNumbers.Visa, + "expiration_date": "05/2009", + }, + "line_items": [{ + "quantity": "1.0232", + "name": "Name #1", + "kind": TransactionLineItem.Kind.Debit, + "unit_amount": "45.1232", + "unit_of_measure": "gallon", + "discount_amount": "1.02", + "total_amount": "45.15", + "product_code": "23434", + "commodity_code": "9SAASSD8724", + "upc_type": "UPC-B", + }] + }) + + self.assertFalse(result.is_success) + + self.assertEqual( + ErrorCodes.Transaction.LineItem.UPCCodeIsMissing, + result.errors.for_object("transaction").for_object("line_items").for_object("index_0").on("upc_code")[0].code + ) + + def test_sale_with_line_items_returns_UPC_type_missing_error(self): + result = Transaction.sale({ + "amount": "35.05", + "credit_card": { + "number": CreditCardNumbers.Visa, + "expiration_date": "05/2009", + }, + "line_items": [{ + "quantity": "1.0232", + "name": "Name #1", + "kind": TransactionLineItem.Kind.Debit, + "unit_amount": "45.1232", + "unit_of_measure": "gallon", + "discount_amount": "1.02", + "total_amount": "45.15", + "product_code": "23434", + "commodity_code": "9SAASSD8724", + "upc_code": "00000000000000", + }] + }) + + self.assertFalse(result.is_success) + + self.assertEqual( + ErrorCodes.Transaction.LineItem.UPCTypeIsMissing, + result.errors.for_object("transaction").for_object("line_items").for_object("index_0").on("upc_type")[0].code + ) + + def test_sale_with_amount_not_supported_by_processor(self): result = Transaction.sale({ "amount": "0.2", @@ -2624,7 +2745,6 @@ def test_sale_with_debit_network(self): "amount": TransactionAmounts.Authorize, "merchant_account_id": TestHelper.pinless_debit_merchant_account_id, - #"currency_iso_code": "USD", "payment_method_nonce": Nonces.TransactablePinlessDebitVisa, "options": { "submit_for_settlement": True @@ -2632,10 +2752,25 @@ def test_sale_with_debit_network(self): }) self.assertTrue(result.is_success) transaction = result.transaction - self.assertEqual(Transaction.Status.SubmittedForSettlement, transaction.status) self.assertTrue(hasattr(transaction, 'debit_network')) self.assertIsNotNone(transaction.debit_network) + def test_pinless_eligible_sale_with_process_debit_as_credit(self): + result = Transaction.sale({ + "amount": TransactionAmounts.Authorize, + "merchant_account_id": TestHelper.pinless_debit_merchant_account_id, + "payment_method_nonce": Nonces.TransactablePinlessDebitVisa, + "options": { + "submit_for_settlement": True, + "credit_card":{ + "process_debit_as_credit": True, + } + } + }) + self.assertTrue(result.is_success) + transaction = result.transaction + self.assertIsNone(transaction.debit_network) + def test_validation_error_on_invalid_custom_fields(self): result = Transaction.sale({ "amount": TransactionAmounts.Authorize, @@ -4615,6 +4750,8 @@ def test_sale_with_three_d_secure_option(self): self.assertEqual(Transaction.Status.GatewayRejected, result.transaction.status) self.assertEqual(Transaction.GatewayRejectionReason.ThreeDSecure, result.transaction.gateway_rejection_reason) + # NEXT_MAJOR_VERSION Remove this test + # three_d_secure_token is deprecated in favor of three_d_secure_authentication_id def test_sale_with_three_d_secure_token(self): three_d_secure_token = TestHelper.create_3ds_verification(TestHelper.three_d_secure_merchant_account_id, { "number": "4111111111111111", @@ -4634,6 +4771,8 @@ def test_sale_with_three_d_secure_token(self): self.assertTrue(result.is_success) + # NEXT_MAJOR_VERSION Remove this test + # three_d_secure_token is deprecated in favor of three_d_secure_authentication_id def test_sale_without_three_d_secure_token(self): result = Transaction.sale({ "merchant_account_id": TestHelper.three_d_secure_merchant_account_id, @@ -4646,6 +4785,8 @@ def test_sale_without_three_d_secure_token(self): self.assertTrue(result.is_success) + # NEXT_MAJOR_VERSION Remove this test + # three_d_secure_token is deprecated in favor of three_d_secure_authentication_id def test_sale_returns_error_with_none_three_d_secure_token(self): result = Transaction.sale({ "merchant_account_id": TestHelper.three_d_secure_merchant_account_id, @@ -4663,6 +4804,8 @@ def test_sale_returns_error_with_none_three_d_secure_token(self): result.errors.for_object("transaction").on("three_d_secure_token")[0].code ) + # NEXT_MAJOR_VERSION Remove this test + # three_d_secure_token is deprecated in favor of three_d_secure_authentication_id def test_sale_returns_error_with_mismatched_3ds_verification_data(self): three_d_secure_token = TestHelper.create_3ds_verification(TestHelper.three_d_secure_merchant_account_id, { "number": "4111111111111111", @@ -5657,14 +5800,16 @@ def test_creating_meta_checkout_card_transaction_with_fake_nonce(self): transaction = result.transaction details = transaction.meta_checkout_card_details + next_year = str(date.today().year + 1) + self.assertEqual(details.bin, "401288") self.assertEqual(details.card_type, "Visa") self.assertEqual(details.cardholder_name, "Meta Checkout Card Cardholder") self.assertEqual(details.container_id, "container123") self.assertEqual(details.customer_location, "US") - self.assertEqual(details.expiration_date, "12/2024") + self.assertEqual(details.expiration_date, "12/" + next_year) self.assertEqual(details.expiration_month, "12") - self.assertEqual(details.expiration_year, "2024") + self.assertEqual(details.expiration_year, next_year) self.assertEqual(details.image_url, "https://assets.braintreegateway.com/payment_method_logo/visa.png?environment=development") self.assertEqual(details.is_network_tokenized, False) self.assertEqual(details.last_4, "1881") @@ -5681,6 +5826,8 @@ def test_creating_meta_checkout_token_transaction_with_fake_nonce(self): transaction = result.transaction details = transaction.meta_checkout_token_details + next_year = str(date.today().year + 1) + self.assertEqual(details.bin, "401288") self.assertEqual(details.card_type, "Visa") self.assertEqual(details.cardholder_name, "Meta Checkout Token Cardholder") @@ -5688,9 +5835,9 @@ def test_creating_meta_checkout_token_transaction_with_fake_nonce(self): self.assertEqual(details.cryptogram, "AlhlvxmN2ZKuAAESNFZ4GoABFA==") self.assertEqual(details.customer_location, "US") self.assertEqual(details.ecommerce_indicator, "07") - self.assertEqual(details.expiration_date, "12/2024") + self.assertEqual(details.expiration_date, "12/" + next_year) self.assertEqual(details.expiration_month, "12") - self.assertEqual(details.expiration_year, "2024") + self.assertEqual(details.expiration_year, next_year) self.assertEqual(details.image_url, "https://assets.braintreegateway.com/payment_method_logo/visa.png?environment=development") self.assertEqual(details.is_network_tokenized, True) self.assertEqual(details.last_4, "1881") @@ -6396,19 +6543,6 @@ def test_adjust_authorization_when_amount_submitted_same_as_authorized(self): error_code = adjusted_authorization_result.errors.for_object("authorization_adjustment").on("base")[0].code self.assertEqual(ErrorCodes.Transaction.NoNetAmountToPerformAuthAdjustment, error_code) - def test_adjust_authorization_when_transaction_status_is_not_authorized(self): - additional_params = { "options": { "submit_for_settlement": True } } - merchant_params = { **self.__first_data_transaction_params(), **additional_params } - initial_transaction_sale = Transaction.sale(merchant_params) - self.assertTrue(initial_transaction_sale.is_success) - adjusted_authorization_result = Transaction.adjust_authorization(initial_transaction_sale.transaction.id, "85.50") - - self.assertFalse(adjusted_authorization_result.is_success) - self.assertEqual(adjusted_authorization_result.transaction.amount, Decimal("75.50")) - - error_code = adjusted_authorization_result.errors.for_object("transaction").on("base")[0].code - self.assertEqual(ErrorCodes.Transaction.TransactionMustBeInStateAuthorized, error_code) - def test_adjust_authorization_when_transaction_authorization_type_is_undfined_or_final(self): additional_params = { "transaction_source": "recurring" } merchant_params = { **self.__first_data_transaction_params(), **additional_params } diff --git a/tests/integration/test_transaction_line_item.py b/tests/integration/test_transaction_line_item.py index db9f3d2..4a8e0ae 100644 --- a/tests/integration/test_transaction_line_item.py +++ b/tests/integration/test_transaction_line_item.py @@ -16,6 +16,9 @@ def test_transaction_line_item_find_all_returns_line_items(self): "kind": TransactionLineItem.Kind.Debit, "unit_amount": "45.1232", "total_amount": "45.15", + "upc_code": "123456789", + "upc_type": "UPC-A", + "image_url": "https://google.com/image.png", }] }).transaction @@ -27,3 +30,7 @@ def test_transaction_line_item_find_all_returns_line_items(self): self.assertEqual(TransactionLineItem.Kind.Debit, lineItem.kind) self.assertEqual("45.1232", lineItem.unit_amount) self.assertEqual("45.15", lineItem.total_amount) + self.assertEqual("123456789", lineItem.upc_code) + self.assertEqual("UPC-A", lineItem.upc_type) + self.assertEqual("https://google.com/image.png",lineItem.image_url) + diff --git a/tests/unit/test_credit_card.py b/tests/unit/test_credit_card.py index 5724a25..dec2668 100644 --- a/tests/unit/test_credit_card.py +++ b/tests/unit/test_credit_card.py @@ -12,7 +12,7 @@ def test_update_raises_exception_with_bad_keys(self): def test_create_signature(self): expected = ["billing_address_id", "cardholder_name", "cvv", "expiration_date", "expiration_month", - "expiration_year", "number", "token", "venmo_sdk_payment_method_code", + "expiration_year", "number", "token", "venmo_sdk_payment_method_code", # NEXT_MJOR_VERSION remove venmo_sdk_payment_method_code "device_data", "payment_method_nonce", "device_session_id", "fraud_merchant_id", { @@ -26,7 +26,7 @@ def test_create_signature(self): "fail_on_duplicate_payment_method", "make_default", "skip_advanced_fraud_checking", - "venmo_sdk_session", + "venmo_sdk_session", # NEXT_MJOR_VERSION remove venmo_sdk_session "verification_account_type", "verification_amount", "verification_merchant_account_id", @@ -44,7 +44,7 @@ def test_create_signature(self): def test_update_signature(self): expected = ["billing_address_id", "cardholder_name", "cvv", "expiration_date", "expiration_month", - "expiration_year", "number", "token", "venmo_sdk_payment_method_code", + "expiration_year", "number", "token", "venmo_sdk_payment_method_code", # NEXT_MJOR_VERSION remove venmo_sdk_payment_method_code "device_data", "payment_method_nonce", "device_session_id", "fraud_merchant_id", { @@ -58,7 +58,7 @@ def test_update_signature(self): "fail_on_duplicate_payment_method", "make_default", "skip_advanced_fraud_checking", - "venmo_sdk_session", + "venmo_sdk_session", # NEXT_MJOR_VERSION remove venmo_sdk_session "verification_account_type", "verification_amount", "verification_merchant_account_id", diff --git a/tests/unit/test_payment_method_gateway.py b/tests/unit/test_payment_method_gateway.py index baad300..9e5edb0 100644 --- a/tests/unit/test_payment_method_gateway.py +++ b/tests/unit/test_payment_method_gateway.py @@ -95,7 +95,7 @@ def test_update_signature(self): "number", "payment_method_nonce", "token", - "venmo_sdk_payment_method_code", + "venmo_sdk_payment_method_code", # NEXT_MJOR_VERSION remove venmo_sdk_payment_method_code "device_session_id", "fraud_merchant_id", { @@ -103,7 +103,7 @@ def test_update_signature(self): "make_default", "skip_advanced_fraud_checking", "us_bank_account_verification_method", - "venmo_sdk_session", + "venmo_sdk_session", # NEXT_MJOR_VERSION remove venmo_sdk_session "verification_account_type", "verification_add_ons", "verification_amount", diff --git a/tests/unit/test_transaction.py b/tests/unit/test_transaction.py index 760f0c5..e94b166 100644 --- a/tests/unit/test_transaction.py +++ b/tests/unit/test_transaction.py @@ -204,6 +204,59 @@ def test_constructor_includes_auth_adjustments(self): self.assertEqual(transaction_adjustment.processor_response_code, "1000") self.assertEqual(transaction_adjustment.processor_response_text, "Approved") + def test_constructor_parses_shipments_into_packages(self): + attributes = { + 'amount': '27.00', + 'customer_id': '4096', + 'merchant_account_id': '8192', + 'payment_method_token': 'sometoken', + 'purchase_order_number': '20202', + 'recurring': 'False', + 'tax_amount': '1.00', + 'shipments': [ + { + 'id': 'id1', + 'carrier': 'UPS', + 'tracking_number': 'tracking_number_1', + 'paypal_tracking_id': 'pp_tracking_number_1', + }, + { + 'id': 'id2', + 'carrier': 'FEDEX', + 'tracking_number': 'tracking_number_2', + 'paypal_tracking_id': 'pp_tracking_number_2', + }, + ], + } + + transaction = Transaction(None, attributes) + package_detail_1 = transaction.packages[0] + self.assertEqual(package_detail_1.id, "id1") + self.assertEqual(package_detail_1.carrier, "UPS") + self.assertEqual(package_detail_1.tracking_number, "tracking_number_1") + self.assertEqual(package_detail_1.paypal_tracking_id, "pp_tracking_number_1") + + package_detail_2 = transaction.packages[1] + self.assertEqual(package_detail_2.id, "id2") + self.assertEqual(package_detail_2.carrier, "FEDEX") + self.assertEqual(package_detail_2.tracking_number, "tracking_number_2") + self.assertEqual(package_detail_2.paypal_tracking_id, "pp_tracking_number_2") + + def test_constructor_works_with_empty_shipments_list(self): + attributes = { + 'amount': '27.00', + 'customer_id': '4096', + 'merchant_account_id': '8192', + 'payment_method_token': 'sometoken', + 'purchase_order_number': '20202', + 'recurring': 'False', + 'tax_amount': '1.00', + 'shipments': [], + } + + transaction = Transaction(None, attributes) + self.assertEqual(len(transaction.packages), 0) + def test_constructor_includes_network_transaction_id_and_response_code_and_response_text(self): attributes = { 'amount': '27.00',