New Upstream Release - python-braintree

Ready changes

Summary

Merged new upstream version: 4.19.0 (was: 4.18.1).

Resulting package

Built on 2023-05-20T07:08 (took 4m47s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases python3-braintree

Lintian Result

Diff

diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 2654cd8..af7b9ca 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1 +1 @@
-* @braintree/sdk
+* @braintree/team-sdk-server
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index cb3a1ee..2d4f046 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -1,3 +1,5 @@
+<!-- Only open an issue here if you think you've found an issue with our SDK. If you need help troubleshooting your integration, reach out to Braintree Support at https://help.braintreepayments.com. -->
+
 ### General information
 
 * SDK/Library version: <!-- Example: 3.37.0 -->
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index de9aa9e..7e12aae 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -3,4 +3,9 @@
 # Checklist
 
 - [ ] Added changelog entry
-- [ ] Ran unit tests (`nosetests tests/unit`)
+- [ ] Ran unit tests (`python3 -m unittest discover tests/unit`)
+- [ ] I understand that unless this is a Draft PR or has a DO NOT MERGE label, this PR is considered to be in a deploy ready state and can be deployed if merged to main
+
+<!-- **For Braintree Developers only, don't forget:**
+- [ ] Does this change require work to be done to the GraphQL API? If you have questions check with the GraphQL team.
+- [ ] Add & Run integration tests -->
diff --git a/.gitignore b/.gitignore
index e4b4fae..660b4cf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@
 MANIFEST
 build
 /venv
+.idea
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 0000000..65b390f
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,5 @@
+version: 2
+
+python:
+   install:
+   - requirements: docs/requirements.txt
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b3003c..c045a0e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,177 @@
+# Changelog
+
+## 4.19.0
+* Add `intended_transaction_source` to `CreditCardVerification`
+* Add `three_d_secure_pass_thru` to `CreditCardVerification`
+* Add `payment_method_nonce` to `CreditCardVerification`
+* Add `three_d_secure_authentication_id` to `CreditCardVerification`
+* Add support for subscriptions in SEPA direct debit accounts
+
+## 4.18.1
+* Fixup issue where request sessions weren't including proxy settings (see [#5677](https://github.com/psf/requests/issues/5677) for details).
+
+## 4.18.0
+* Replace nose usage for tests with unittest (Thanks @arthurzam)
+* Remove mock dev dependency (Thanks @arthurzam)
+* Add `ExcessiveRetry` to `GatewayRejectionReason`
+* Add `pre_dispute_program` to `Dispute` and `DisputeSearch`
+* Add `AutoAccepted` status to `Dispute`
+* Add `DisputeAutoAccepted` to `WebhookNotification.Kind`
+* Deprecate `chargeback_protection_level` and add `protection_level` to `Dispute` and `DisputeSearch`
+* Add `shipping` object to `submit_for_settlement_signature`
+* Add `SEPADirectDebitAccount` payment method
+* Add `SEPADirectDebitAccount` to transaction object
+* Add `SEPA_DIRECT_DEBIT_ACCOUNT` to `PaymentInstrumentType`
+* Add `sepa_debit_paypal_v2_order_id` to `TransactionSearch`
+* Add `sepa_direct_debit_accounts` to `Customer`
+* Add SEPA Direct Debit specific error codes
+
+## 4.17.1
+* Prepare http request before setting url to resolve issue where dot segments get normalized
+
+## 4.17.0
+* Fix `DeprecationWarning` on invalid escape sequences (thanks @DavidCain)
+* Add validation for arguments in Address.delete, Address.find, and Address.update
+
+## 4.16.0
+* Add `LiabilityShift` class and `liability_shift` to RiskData
+* Add ExchangeRateQuote API
+* Add `ach_return_responses_created_at` and `reason_code` fields in TransactionSearch
+* Allow vaulting of raw ApplePayCards with billing address via Customer.create/update
+
+## 4.15.2
+* Add `retried` to `Transaction`
+
+## 4.14.0
+* Add `PaymentMethodCustomerDataUpdated` webhook
+
+## 4.13.0
+* Add plan create/update/find API endpoint
+* Add `TransactionReview` webhook notification
+* Fix typos (@timgates42)
+
+## 4.12.0
+* Add `localPaymentFunded` and `localPaymentExpired` webhooks
+
+## 4.11.0
+* Add `exchange_rate_quote_id` to `Transaction.sale`
+* Add validation error code `ExchangeRateQuoteIdIsTooLong` to `Transaction`
+* Add the following fields to `ApplePayCard` and `AndroidPayCard`:
+  * `commercial`
+  * `debit`
+  * `durbin_regulated`
+  * `healthcare`
+  * `payroll`
+  * `prepaid`
+  * `product_id`
+  * `country_of_issuance`
+  * `issuing_bank`
+* Add error code `Transaction.TaxAmountIsRequiredForAibSwedish` for attribute `tax_amount` to handle validation for AIB:Domestic Transactions in Sweden
+
+
+## 4.10.0
+* Add `payment_reader_card_details` parameter to `Transaction.sale`
+* Add webhook sample for `GrantedPaymentMethodRevoked`
+* Add `chargeback_protection_level` to `DisputeSearch`
+* Add `skip_advanced_fraud_checking` to:
+  * `PaymentMethod.create` and `PaymentMethod.update`
+  * `CreditCard.create` and `CreditCard.update`
+
+## 4.9.0
+* Add `paypal_messages` to `Dispute`
+* Add `tax_identifiers` parameter to `Customer.create` and `Customer.update`
+
+## 4.8.0
+* Add `LocalPaymentReversed` webhook
+* Add `store_id` and `store_ids` to `Transaction.search`
+
+## 4.7.0
+* Add `merchant_account_id` to `Transaction.refund`
+* Add `Transaction.adjust_authorization` method to support for multiple authorizations for a single transaction
+
+## 4.6.0
+* Add `installments` to `Transaction` requests
+* Add `count` to `installments`
+* Deprecate `device_session_id` and `fraud_merchant_id` in `CreditCardGateway`, `CustomerGateway`, `PaymentMethodGateway`, and `TransactionGateway` classes
+* Add `sca_exemption` to Transaction.sale request
+
+## 4.5.0
+* Add `acquirer_reference_number` to `Transaction`
+* Deprecate `recurring` in Transaction sale requests
+
+## 4.4.0
+* Deprecate `masterpass_card` and `amex_checkout_card` payment methods
+* Fix issue where `transaction.credit` could not be called using a gateway object
+
+## 4.3.0
+* Add validation error code `Transaction.ProductSkuIsInvalid`
+* Add 'RiskThreshold' gateway rejection reason
+* Add `processed_with_network_token` to `Transaction`
+* Add `is_network_tokenized` to `CreditCard`
+
+## 4.2.0
+* Add `retrieval_reference_number` to `Transaction`
+* Add `network_transaction_id` to `CreditCardVerification`
+* Add `product_sku` to `Transaction`
+* Add `customer_device_id`, `customer_location_zip`, and `customer_tenure` to `RiskData`
+* Add `phone_number` and `shipping_method` to `Address`
+* Add validation error codes:
+  * `Transaction.ShippingMethodIsInvalid`
+  * `Transaction.ShippingPhoneNumberIsInvalid`
+  * `Transaction.BillingPhoneNumberIsInvalid`
+  * `RiskData.CustomerBrowserIsTooLong`
+  * `RiskData.CustomerDeviceIdIsTooLong`
+  * `RiskData.CustomerLocationZipInvalidCharacters`
+  * `RiskData.CustomerLocationZipIsInvalid`
+  * `RiskData.CustomerLocationZipIsTooLong`
+  * `RiskData.CustomerTenureIsTooLong`
+
+## 4.1.0
+* Add `DisputeAccepted`, `DisputeDisputed`, and `DisputeExpired` webhook constants
+* Add `three_d_secure_pass_thru` to `CreditCard.create`, `CreditCard.update`, `PaymentMethod.create`, `PaymentMethod.update`, `Customer.create`, and `Customer.update`.
+* Add `Verification` validation errors for 3D Secure
+* Add `payment_method_token` to `CreditCardVerificationSearch`
+* Add `recurring_customer_consent` and `recurring_max_amount` to `authentication_insight_options` for `PaymentMethodNonce.create`
+* Add `FileIsEmpty` error code
+* Eliminates usage of mutable objects for function parameters. Resolves #113 Thank you @maneeshd!
+
+## 4.0.0
+* Split development and deployments requirements files out
+* Add `Authentication Insight` to payment method nonce create
+* Add ThreeDSecure test payment method nonces
+* Add test `AuthenticationId`s
+* Add `three_d_secure_authentication_id` to `three_d_secure_info`
+* Add `three_d_secure_authentication_id` support for transaction sale
+* Breaking Changes
+  * Require Python 3.5+
+  * Remove deprecated Transparent Redirect
+  * Remove deprecated iDeal payment method
+  * Apple Pay register_domains returns an ApplePayOptions object
+  * Remove `unrecognized` status from Transaction, Subscription, and CreditCardVerification
+  * Remove `GrantedPaymentInstrumentUpdate` kind from Webhook
+  * Remove Coinbase references
+  * Add GatewayTimeoutError, RequestTimeoutError exceptions
+  * Rename DownForMaintenanceError exception to ServiceUnavailableError
+  * Transaction `line_items` only returns the line items for a transaction response object. Use TransactionLineItem `find_all` to search all line items on a transaction, given a transaction_id
+  * Upgrade API version to retrieve declined refund transactions
+  * Remove all deprecated parameters, errors, and methods
+
+## 3.59.0
+* Add `RefundAuthHardDeclined` and `RefundAuthSoftDeclined` to validation errors
+* Fix issue where managing Apple Pay domains would fail in Python 3.8+
+* Add level 2 processing options `purchase_order_number`, `tax_amount`, and `tax_exempt` to `Transaction.submit_for_settlement`
+* Add level 3 processing options `discount_amount`, `shipping_amount`, `ships_from_postal_code`, `line_items` to `Transaction.submit_for_settlement`
+
+## 3.58.0
+* Add support for managing Apple Pay domains (thanks @ethier #117)
+* Fix error when running against Python 3.8 (thanks @felixonmars #114)
+* Add `ProcessorDoesNotSupportMotoForCardType` to validation errors
+* Add Graphql ID to `CreditCardVerification`, `Customer`, `Dispute`, and `Transaction`
+
 ## 3.57.1
 * Set correct version for PyPi
 
-## 3.57.0 
+## 3.57.0
 * Forward `processor_comments` to `forwarded_comments`
 * Add Venmo 'TokenIssuance' gateway rejection reason
 * Add `AmountNotSupportedByProcessor` to validation error
diff --git a/Dockerfile b/Dockerfile
index 1211b35..dc4441c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,9 +1,10 @@
 FROM debian:stretch
 
 RUN apt-get update
-RUN apt-get -y install python rake
-RUN apt-get -y install python-pip
-RUN pip install --upgrade pip
-RUN pip install --upgrade distribute
+RUN apt-get -y install python3 rake
+RUN apt-get -y install python3-pip
+RUN pip3 install --upgrade pip
+RUN pip3 install --upgrade distribute
 
+RUN echo 'alias python=python3' >> ~/.bashrc
 WORKDIR /braintree-python
diff --git a/Makefile b/Makefile
index f4cbed5..2b16d76 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
 .PHONY: console build
 
 console: build
-	docker run -it -v="$(PWD):/braintree-python" --net="host" braintree-python /bin/bash -l -c "pip install -r requirements.txt;bash"
+	docker run -it -v="$(PWD):/braintree-python" --net="host" braintree-python /bin/bash -l -c "pip3 install -r dev_requirements.txt;pip3 install pylint;bash"
 
 build:
 	docker build -t braintree-python .
diff --git a/README.md b/README.md
index 67b96a0..627480b 100644
--- a/README.md
+++ b/README.md
@@ -2,70 +2,66 @@
 
 The Braintree Python library provides integration access to the Braintree Gateway.
 
-## Please Note
-> **The Payment Card Industry (PCI) Council has [mandated](https://blog.pcisecuritystandards.org/migrating-from-ssl-and-early-tls) that early versions of TLS be retired from service.  All organizations that handle credit card information are required to comply with this standard. As part of this obligation, Braintree is updating its services to require TLS 1.2 for all HTTPS connections. Braintree will also require HTTP/1.1 for all connections. Please see our [technical documentation](https://github.com/paypal/tls-update) for more information.**
+## TLS 1.2 required
+> **The Payment Card Industry (PCI) Council has [mandated](https://blog.pcisecuritystandards.org/migrating-from-ssl-and-early-tls) that early versions of TLS be retired from service.  All organizations that handle credit card information are required to comply with this standard. As part of this obligation, Braintree has updated its services to require TLS 1.2 for all HTTPS connections. Braintrees require HTTP/1.1 for all connections. Please see our [technical documentation](https://github.com/paypal/tls-update) for more information.**
 
 ## Dependencies
 
 * [requests](http://docs.python-requests.org/en/latest/)
 
-The Braintree Python SDK is tested against Python versions 2.7.9, 3.3.5 and 3.7.0.
+The Braintree Python SDK is tested against Python versions 3.5.3 and 3.8.0.
 
-## Upgrading from 2.x.x to 3.x.x
+_The Python core development community has released [End-of-Life branches](https://devguide.python.org/devcycle/#end-of-life-branches) for Python versions 2.7 - 3.4, and are no longer receiving [security updates](https://devguide.python.org/#branchstatus). As a result, Braintree no longer supports these versions of Python._
 
-On Python 2.6 or 2.7 with default settings / requests:
+## Versions
 
-No changes are required to upgrade to version 3.
+Braintree employs a deprecation policy for our SDKs. For more information on the statuses of an SDK check our [developer docs](https://developer.paypal.com/braintree/docs/reference/general/server-sdk-deprecation-policy).
 
-On Python 2.6 or 2.7 with pycurl, httplib, or use_unsafe_ssl = True:
-
-Install requests and test that you are able to connect to the Sandbox
-environment with version 3 and without specifying an HTTP strategy.
-The use_unsafe_ssl parameter will be ignored.
-
-On Python 2.5:
-
-Python 2.5 isn't supported by version 3 of the library.
-Most code that runs on 2.5 will work unmodified on Python 2.6.
-After making sure your code works on Python 2.6, follow the
-instructions above for upgrading from pycurl / httplib to requests.
+| Major version number | Status | Released | Deprecated | Unsupported |
+| -------------------- | ------ | -------- | ---------- | ----------- |
+| 4.x.x | Active | March 2020 | TBA | TBA |
+| 3.x.x | Inactive | June 2014 | March 2022 | March 2023 |
 
 ## Documentation
 
- * [Official documentation](https://developers.braintreepayments.com/ios+python/start/hello-server)
+ * [Official documentation](https://developer.paypal.com/braintree/docs/start/hello-server/python)
+
+Updating from an Inactive, Deprecated, or Unsupported version of this SDK? Check our [Migration Guide](https://developer.paypal.com/braintree/docs/reference/general/server-sdk-migration-guide/python) for tips.
 
 ## Quick Start Example
 
-    import braintree
+```python
+import braintree
 
-    gateway = braintree.BraintreeGateway(
-        braintree.Configuration(
-            environment=braintree.Environment.Sandbox
-            merchant_id="your_merchant_id",
-            public_key="your_public_key",
-            private_key="your_private_key",
-        )
+gateway = braintree.BraintreeGateway(
+    braintree.Configuration(
+        environment=braintree.Environment.Sandbox
+        merchant_id="your_merchant_id",
+        public_key="your_public_key",
+        private_key="your_private_key",
     )
-
-    result = gateway.transaction.sale({
-        "amount": "1000.00",
-        "payment_method_nonce": nonce_from_the_client,
-        "options": {
-            "submit_for_settlement": True
-        }
-    })
-
-    if result.is_success:
-        print("success!: " + result.transaction.id)
-    elif result.transaction:
-        print("Error processing transaction:")
-        print("  code: " + result.transaction.processor_response_code)
-        print("  text: " + result.transaction.processor_response_text)
-    else:
-        for error in result.errors.deep_errors:
-            print("attribute: " + error.attribute)
-            print("  code: " + error.code)
-            print("  message: " + error.message)
+)
+
+result = gateway.transaction.sale({
+    "amount": "1000.00",
+    "payment_method_nonce": nonce_from_the_client,
+    "options": {
+        "submit_for_settlement": True
+    }
+})
+
+if result.is_success:
+    print("success!: " + result.transaction.id)
+elif result.transaction:
+    print("Error processing transaction:")
+    print("  code: " + result.transaction.processor_response_code)
+    print("  text: " + result.transaction.processor_response_text)
+else:
+    for error in result.errors.deep_errors:
+        print("attribute: " + error.attribute)
+        print("  code: " + error.code)
+        print("  message: " + error.message)
+```
 
 ## Developing
 
@@ -84,7 +80,7 @@ instructions above for upgrading from pycurl / httplib to requests.
 3. Install dependencies:
 
    ```
-   pip install -r requirements.txt
+   pip3 install -r dev_requirements.txt
    ```
 
 ## Developing (Docker)
@@ -99,7 +95,7 @@ make
 
 Our friends at [Venmo](https://venmo.com) have [an open source library](https://github.com/venmo/btnamespace) designed to simplify testing of applications using this library.
 
-If you wish to run the tests, make sure you are set up for development (see instructions above). The unit specs can be run by anyone on any system, but the integration specs are meant to be run against a local development server of our gateway code. These integration specs are not meant for public consumption and will likely fail if run on your system. To run unit tests use rake (`rake test:unit`) or nose (`nosetests tests/unit`).
+If you wish to run the tests, make sure you are set up for development (see instructions above). The unit specs can be run by anyone on any system, but the integration specs are meant to be run against a local development server of our gateway code. These integration specs are not meant for public consumption and will likely fail if run on your system. To run unit tests use rake (`rake test:unit`) or unittest (`python3 -m unittest discover tests/unit`).
 
 ## License
 
diff --git a/Rakefile b/Rakefile
index e45b219..f1e54b3 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,22 +1,40 @@
 task :default => :test
 
-task :test => ["test:unit", "test:integration"]
+task :test => ["test:all"]
 
 namespace :test do
+
+  # Usage:
+  #   rake test:unit
+  #   rake test:unit[test_configuration]
+  #   rake test:unit[test_configuration,test_base_merchant_path_for_development]
   desc "run unit tests"
-  task :unit do
-    sh "nosetests tests/unit"
+  task :unit, [:file_name, :test_name] do |task, args|
+    if args.file_name.nil?
+      sh "python3 -m unittest tests/unit"
+    elsif args.test_name.nil?
+      sh "python3 -m unittest tests/unit/#{args.file_name}.py"
+    else
+      sh "python3 -m unittest tests/unit/#{args.file_name}.py -m #{args.test_name}"
+    end
   end
 
+  # Usage:
+  #   rake test:integration
+  #   rake test:integration[test_plan]
+  #   rake test:integration[test_plan,test_all_returns_all_the_plans]
   desc "run integration tests"
-  task :integration do
-    sh "env nosetests tests/integration"
+  task :integration, [:file_name, :test_name] do |task, args|
+    if args.file_name.nil?
+      sh "python3 -m unittest tests/integration"
+    elsif args.test_name.nil?
+      sh "python3 -m unittest tests/integration/#{args.file_name}.py"
+    else
+      sh "python3 -m unittest tests/integration/#{args.file_name}.py -m #{args.test_name}"
+    end
   end
 
-  desc "run single test (example: rake test:single[tests/integration/test_paypal_account.py:TestPayPalAccount.test_find_returns_paypal_account])"
-  task :single, [:test_name] do |t, args|
-      sh "nosetests #{args[:test_name]}"
-  end
+  task :all => [:unit, :integration]
 end
 
 task :clean do
@@ -26,32 +44,29 @@ task :clean do
 end
 
 namespace :pypi do
-  desc "Register the package with PyPI"
-  task :register => :clean do
-    sh "python setup.py register"
-  end
-
   desc "Upload a new version to PyPI"
   task :upload => :clean do
-    sh "python setup.py sdist bdist_wheel"
+    sh "python3 setup.py sdist bdist_wheel"
     sh "twine upload dist/*"
   end
 end
 
 namespace :lint do
+  # We are only checking linting errors (for now),
+  # so we use --disable to skip refactor(R), convention(C), and warning(W) messages
   desc "Evaluate test code quality using pylintrc file"
   task :tests do
-    puts `pylint tests --rcfile=.pylintrc --disable=R0801 --disable=W0232`
+    puts `pylint --disable=R,C,W tests --rcfile=.pylintrc --disable=R0801 --disable=W0232`
   end
 
   desc "Evaluate app code quality using pylintrc file"
   task :code do
-    puts `pylint braintree --rcfile=.pylintrc`
+    puts `pylint --disable=R,C,W braintree --rcfile=.pylintrc`
   end
 
   desc "Evaluate library code quality using pylintrc file"
   task :all do
-    puts `pylint braintree tests --rcfile=.pylintrc`
+    puts `pylint --disable=R,C,W braintree tests --rcfile=.pylintrc`
   end
 end
 
diff --git a/braintree/__init__.py b/braintree/__init__.py
index 47188bf..c30d401 100644
--- a/braintree/__init__.py
+++ b/braintree/__init__.py
@@ -6,11 +6,12 @@ from braintree.address_gateway import AddressGateway
 from braintree.amex_express_checkout_card import AmexExpressCheckoutCard
 from braintree.android_pay_card import AndroidPayCard
 from braintree.apple_pay_card import ApplePayCard
+from braintree.apple_pay_gateway import ApplePayGateway
 from braintree.braintree_gateway import BraintreeGateway
 from braintree.client_token import ClientToken
 from braintree.configuration import Configuration
-from braintree.connected_merchant_status_transitioned import ConnectedMerchantStatusTransitioned
 from braintree.connected_merchant_paypal_status_changed import ConnectedMerchantPayPalStatusChanged
+from braintree.connected_merchant_status_transitioned import ConnectedMerchantStatusTransitioned
 from braintree.credentials_parser import CredentialsParser
 from braintree.credit_card import CreditCard
 from braintree.credit_card_gateway import CreditCardGateway
@@ -21,19 +22,21 @@ from braintree.customer_gateway import CustomerGateway
 from braintree.customer_search import CustomerSearch
 from braintree.descriptor import Descriptor
 from braintree.disbursement import Disbursement
-from braintree.document_upload import DocumentUpload
-from braintree.document_upload_gateway import DocumentUploadGateway
 from braintree.discount import Discount
 from braintree.discount_gateway import DiscountGateway
 from braintree.dispute import Dispute
 from braintree.dispute_search import DisputeSearch
+from braintree.document_upload import DocumentUpload
+from braintree.document_upload_gateway import DocumentUploadGateway
+from braintree.enriched_customer_data import EnrichedCustomerData
 from braintree.environment import Environment
 from braintree.error_codes import ErrorCodes
 from braintree.error_result import ErrorResult
 from braintree.errors import Errors
 from braintree.europe_bank_account import EuropeBankAccount
-from braintree.us_bank_account import UsBankAccount
-from braintree.ideal_payment import IdealPayment
+from braintree.liability_shift import LiabilityShift
+from braintree.local_payment_completed import LocalPaymentCompleted
+from braintree.local_payment_reversed import LocalPaymentReversed
 from braintree.merchant import Merchant
 from braintree.merchant_account import MerchantAccount
 from braintree.merchant_account_gateway import MerchantAccountGateway
@@ -41,6 +44,7 @@ from braintree.oauth_access_revocation import OAuthAccessRevocation
 from braintree.partner_merchant import PartnerMerchant
 from braintree.payment_instrument_type import PaymentInstrumentType
 from braintree.payment_method import PaymentMethod
+from braintree.payment_method_customer_data_updated_metadata import PaymentMethodCustomerDataUpdatedMetadata
 from braintree.payment_method_nonce import PaymentMethodNonce
 from braintree.payment_method_parser import parse_payment_method
 from braintree.paypal_account import PayPalAccount
@@ -51,6 +55,7 @@ from braintree.resource_collection import ResourceCollection
 from braintree.risk_data import RiskData
 from braintree.samsung_pay_card import SamsungPayCard
 from braintree.search import Search
+from braintree.sepa_direct_debit_account import SepaDirectDebitAccount
 from braintree.settlement_batch_summary import SettlementBatchSummary
 from braintree.signature_service import SignatureService
 from braintree.status_event import StatusEvent
@@ -67,11 +72,11 @@ from braintree.transaction_details import TransactionDetails
 from braintree.transaction_gateway import TransactionGateway
 from braintree.transaction_line_item import TransactionLineItem
 from braintree.transaction_search import TransactionSearch
-from braintree.transparent_redirect import TransparentRedirect
-from braintree.transparent_redirect_gateway import TransparentRedirectGateway
 from braintree.unknown_payment_method import UnknownPaymentMethod
+from braintree.us_bank_account import UsBankAccount
 from braintree.validation_error_collection import ValidationErrorCollection
 from braintree.venmo_account import VenmoAccount
+from braintree.venmo_profile_data import VenmoProfileData
 from braintree.version import Version
 from braintree.webhook_notification import WebhookNotification
 from braintree.webhook_notification_gateway import WebhookNotificationGateway
diff --git a/braintree/address.py b/braintree/address.py
index 5717950..c82b55e 100644
--- a/braintree/address.py
+++ b/braintree/address.py
@@ -42,12 +42,31 @@ class Address(Resource):
             "postal_code",
             "region",
             "street_address",
+            "shipping_method",
         ]
         return super(Address, self).__repr__(detail_list)
 
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
+    class ShippingMethod(object):
+        """
+        Constants representing shipping methods for shipping addresses. Available types are:
+
+        * braintree.Address.ShippingMethod.SameDay
+        * braintree.Address.ShippingMethod.NextDay
+        * braintree.Address.ShippingMethod.Priority
+        * braintree.Address.ShippingMethod.Ground
+        * braintree.Address.ShippingMethod.Electronic
+        * braintree.Address.ShippingMethod.ShipToStore
+        """
+        SameDay     = "same_day"
+        NextDay     = "next_day"
+        Priority    = "priority"
+        Ground      = "ground"
+        Electronic  = "electronic"
+        ShipToStore = "ship_to_store"
 
     @staticmethod
-    def create(params={}):
+    def create(params=None):
         """
         Create an Address.
 
@@ -61,7 +80,8 @@ class Address(Resource):
             })
 
         """
-
+        if params is None:
+            params = {}
         return Configuration.gateway().address.create(params)
 
     @staticmethod
@@ -89,7 +109,7 @@ class Address(Resource):
         return Configuration.gateway().address.find(customer_id, address_id)
 
     @staticmethod
-    def update(customer_id, address_id, params={}):
+    def update(customer_id, address_id, params=None):
         """
         Update an existing Address.
 
@@ -100,14 +120,15 @@ class Address(Resource):
             })
 
         """
-
+        if params is None:
+            params = {}
         return Configuration.gateway().address.update(customer_id, address_id, params)
 
     @staticmethod
     def create_signature():
         return ["company", "country_code_alpha2", "country_code_alpha3", "country_code_numeric",
                 "country_name", "customer_id", "extended_address", "first_name",
-                "last_name", "locality", "postal_code", "region", "street_address"]
+                "last_name", "locality", "phone_number", "postal_code", "region", "street_address"]
 
     @staticmethod
     def update_signature():
diff --git a/braintree/address_gateway.py b/braintree/address_gateway.py
index 99fc90f..7eeabdf 100644
--- a/braintree/address_gateway.py
+++ b/braintree/address_gateway.py
@@ -11,11 +11,19 @@ class AddressGateway(object):
         self.gateway = gateway
         self.config = gateway.config
 
-    def create(self, params={}):
+    def __validate_chars_in_args(self, customer_id, address_id):
+        if not re.search(r"\A[0-9A-Za-z_-]+\Z", customer_id):
+            raise KeyError("customer_id contains invalid characters")
+        if not re.search(r"\A[0-9A-Za-z]+\Z", address_id):
+            raise KeyError("address_id contains invalid characters")
+
+    def create(self, params=None):
+        if params is None:
+            params = {}
         Resource.verify_keys(params, Address.create_signature())
-        if not "customer_id" in params:
+        if "customer_id" not in params:
             raise KeyError("customer_id must be provided")
-        if not re.search("\A[0-9A-Za-z_-]+\Z", params["customer_id"]):
+        if not re.search(r"\A[0-9A-Za-z_-]+\Z", params["customer_id"]):
             raise KeyError("customer_id contains invalid characters")
 
         response = self.config.http().post(self.config.base_merchant_path() + "/customers/" + params.pop("customer_id") + "/addresses", {"address": params})
@@ -25,20 +33,26 @@ class AddressGateway(object):
             return ErrorResult(self.gateway, response["api_error_response"])
 
     def delete(self, customer_id, address_id):
+        self.__validate_chars_in_args(customer_id, address_id)
         self.config.http().delete(self.config.base_merchant_path() + "/customers/" + customer_id + "/addresses/" + address_id)
         return SuccessfulResult()
 
     def find(self, customer_id, address_id):
         try:
+            # NEXT_MAJOR_VERSION return KeyError instead of NotFoundError, it's a more helpful error message to the developer that way
             if customer_id is None or customer_id.strip() == "" or address_id is None or address_id.strip() == "":
                 raise NotFoundError()
+            self.__validate_chars_in_args(customer_id, address_id)
             response = self.config.http().get(self.config.base_merchant_path() + "/customers/" + customer_id + "/addresses/" + address_id)
             return Address(self.gateway, response["address"])
         except NotFoundError:
             raise NotFoundError("address for customer " + repr(customer_id) + " with id " + repr(address_id) + " not found")
 
-    def update(self, customer_id, address_id, params={}):
+    def update(self, customer_id, address_id, params=None):
+        if params is None:
+            params = {}
         Resource.verify_keys(params, Address.update_signature())
+        self.__validate_chars_in_args(customer_id, address_id)
         response = self.config.http().put(
             self.config.base_merchant_path() + "/customers/" + customer_id + "/addresses/" + address_id,
             {"address": params}
diff --git a/braintree/amex_express_checkout_card.py b/braintree/amex_express_checkout_card.py
index 42af24d..3cf8f2c 100644
--- a/braintree/amex_express_checkout_card.py
+++ b/braintree/amex_express_checkout_card.py
@@ -1,11 +1,13 @@
 import braintree
 from braintree.resource import Resource
+from warnings import warn
 
 class AmexExpressCheckoutCard(Resource):
     """
-    A class representing Braintree Amex Express Checkout card objects.
+    A class representing Braintree Amex Express Checkout card objects. Deprecated
     """
     def __init__(self, gateway, attributes):
+        warn("AmexExpressCheckoutCard is deprecated")
         Resource.__init__(self, gateway, attributes)
 
         if "subscriptions" in attributes:
@@ -14,4 +16,3 @@ class AmexExpressCheckoutCard(Resource):
     @property
     def expiration_date(self):
         return self.expiration_month + "/" + self.expiration_year
-
diff --git a/braintree/android_pay_card.py b/braintree/android_pay_card.py
index 33c56f0..a8f27de 100644
--- a/braintree/android_pay_card.py
+++ b/braintree/android_pay_card.py
@@ -1,6 +1,7 @@
 import braintree
 from braintree.resource import Resource
 
+# NEXT_MAJOR_VERSION - rename to GooglePayCard
 class AndroidPayCard(Resource):
     """
     A class representing Braintree Android Pay card objects.
diff --git a/braintree/apple_pay_card.py b/braintree/apple_pay_card.py
index 0c16fa0..ce00c6a 100644
--- a/braintree/apple_pay_card.py
+++ b/braintree/apple_pay_card.py
@@ -30,3 +30,40 @@ class ApplePayCard(Resource):
     def expiration_date(self):
         return self.expiration_month + "/" + self.expiration_year
 
+    @staticmethod
+    def signature():
+        options = ["make_default"]
+
+        signature = [
+            "customer_id",
+            "cardholder_name",
+            "expiration_month",
+            "expiration_year",
+            "number",
+            "cryptogram",
+            "eci_indicator",
+            "token",
+            {
+                "options": options
+            },
+            {
+                "billing_address": [
+                    "company",
+                    "country_code_alpha2",
+                    "country_code_alpha3",
+                    "country_code_numeric",
+                    "country_name",
+                    "extended_address",
+                    "first_name",
+                    "last_name",
+                    "locality",
+                    "postal_code",
+                    "phone_number",
+                    "region",
+                    "street_address"
+                ]
+            }
+        ]
+
+        return signature
+
diff --git a/braintree/apple_pay_gateway.py b/braintree/apple_pay_gateway.py
new file mode 100644
index 0000000..bb5cea6
--- /dev/null
+++ b/braintree/apple_pay_gateway.py
@@ -0,0 +1,34 @@
+try:
+    from html import escape
+except ImportError:
+    from cgi import escape
+
+from braintree.apple_pay_options import ApplePayOptions
+from braintree.error_result import ErrorResult
+from braintree.successful_result import SuccessfulResult
+from braintree.exceptions.unexpected_error import UnexpectedError
+
+class ApplePayGateway(object):
+    def __init__(self, gateway):
+        self.gateway = gateway
+        self.config = gateway.config
+
+    def register_domain(self, domain):
+        response = self.config.http().post(self.config.base_merchant_path() + "/processing/apple_pay/validate_domains", {'url': domain})
+
+        if "response" in response and response["response"]["success"]:
+            return SuccessfulResult()
+        elif response["api_error_response"]:
+            return ErrorResult(self.gateway, response["api_error_response"])
+
+    def unregister_domain(self, domain):
+        self.config.http().delete(self.config.base_merchant_path() + "/processing/apple_pay/unregister_domain?url=" + escape(domain))
+        return SuccessfulResult()
+
+    def registered_domains(self):
+        response = self.config.http().get(self.config.base_merchant_path() + "/processing/apple_pay/registered_domains")
+
+        if "response" in response:
+            response = ApplePayOptions(response.pop("response"))
+
+        return response.domains
diff --git a/braintree/apple_pay_options.py b/braintree/apple_pay_options.py
new file mode 100644
index 0000000..24bf37e
--- /dev/null
+++ b/braintree/apple_pay_options.py
@@ -0,0 +1,4 @@
+from braintree.attribute_getter import AttributeGetter
+
+class ApplePayOptions(AttributeGetter):
+    pass
diff --git a/braintree/attribute_getter.py b/braintree/attribute_getter.py
index a4067c0..43049a8 100644
--- a/braintree/attribute_getter.py
+++ b/braintree/attribute_getter.py
@@ -1,23 +1,28 @@
 class AttributeGetter(object):
     """
-    Helper class for objects that define their attributes from dictionaries 
+    Helper class for objects that define their attributes from dictionaries
     passed in during instantiation.
-    
+
     Example:
-    
+
     a = AttributeGetter({'foo': 'bar', 'baz': 5})
     a.foo
     >> 'bar'
     a.baz
     >> 5
-    
+
     Typically inherited by subclasses instead of directly instantiated.
     """
-    def __init__(self, attributes={}):
+    def __init__(self, attributes=None):
+        if attributes is None:
+            attributes = {}
         self._setattrs = []
         for key, val in attributes.items():
             setattr(self, key, val)
             self._setattrs.append(key)
+            if key == "global_id":
+                setattr(self, "graphql_id", val)
+                self._setattrs.append("graphql_id")
 
     def __repr__(self, detail_list=None):
         if detail_list is None:
diff --git a/braintree/authorization_adjustment.py b/braintree/authorization_adjustment.py
index fafd692..bb395eb 100644
--- a/braintree/authorization_adjustment.py
+++ b/braintree/authorization_adjustment.py
@@ -4,5 +4,5 @@ from braintree.attribute_getter import AttributeGetter
 class AuthorizationAdjustment(AttributeGetter):
     def __init__(self, attributes):
         AttributeGetter.__init__(self, attributes)
-        if hasattr(self, 'amount') and self.amount is not None:
+        if getattr(self, "amount", None) is not None:
             self.amount = Decimal(self.amount)
diff --git a/braintree/braintree_gateway.py b/braintree/braintree_gateway.py
index a0a4619..66744c8 100644
--- a/braintree/braintree_gateway.py
+++ b/braintree/braintree_gateway.py
@@ -1,33 +1,32 @@
 from braintree.add_on_gateway import AddOnGateway
 from braintree.address_gateway import AddressGateway
+from braintree.apple_pay_gateway import ApplePayGateway
 from braintree.client_token_gateway import ClientTokenGateway
 from braintree.configuration import Configuration
 from braintree.credit_card_gateway import CreditCardGateway
 from braintree.credit_card_verification_gateway import CreditCardVerificationGateway
 from braintree.customer_gateway import CustomerGateway
-from braintree.document_upload_gateway import DocumentUploadGateway
 from braintree.discount_gateway import DiscountGateway
 from braintree.dispute_gateway import DisputeGateway
+from braintree.document_upload_gateway import DocumentUploadGateway
+from braintree.exchange_rate_quote_gateway import ExchangeRateQuoteGateway
 from braintree.merchant_account_gateway import MerchantAccountGateway
 from braintree.merchant_gateway import MerchantGateway
 from braintree.oauth_gateway import OAuthGateway
 from braintree.payment_method_gateway import PaymentMethodGateway
 from braintree.payment_method_nonce_gateway import PaymentMethodNonceGateway
 from braintree.paypal_account_gateway import PayPalAccountGateway
+from braintree.sepa_direct_debit_account_gateway import SepaDirectDebitAccountGateway
 from braintree.plan_gateway import PlanGateway
 from braintree.settlement_batch_summary_gateway import SettlementBatchSummaryGateway
 from braintree.subscription_gateway import SubscriptionGateway
 from braintree.testing_gateway import TestingGateway
 from braintree.transaction_gateway import TransactionGateway
 from braintree.transaction_line_item_gateway import TransactionLineItemGateway
-from braintree.transparent_redirect_gateway import TransparentRedirectGateway
 from braintree.us_bank_account_gateway import UsBankAccountGateway
 from braintree.us_bank_account_verification_gateway import UsBankAccountVerificationGateway
 from braintree.webhook_notification_gateway import WebhookNotificationGateway
 from braintree.webhook_testing_gateway import WebhookTestingGateway
-# NEXT_MAJOR_VERSION Remove this class as legacy Ideal has been removed/disabled in the Braintree Gateway
-# DEPRECATED If you're looking to accept iDEAL as a payment method contact accounts@braintreepayments.com for a solution.
-from braintree.ideal_payment_gateway import IdealPaymentGateway
 import braintree.configuration
 
 class BraintreeGateway(object):
@@ -41,33 +40,32 @@ class BraintreeGateway(object):
                 access_token=kwargs.get("access_token"),
                 http_strategy=kwargs.get("http_strategy")
             )
-        self.graphql_client = self.config.graphql_client()
         self.add_on = AddOnGateway(self)
         self.address = AddressGateway(self)
+        self.apple_pay = ApplePayGateway(self)
         self.client_token = ClientTokenGateway(self)
         self.credit_card = CreditCardGateway(self)
         self.customer = CustomerGateway(self)
-        self.document_upload = DocumentUploadGateway(self)
         self.discount = DiscountGateway(self)
         self.dispute = DisputeGateway(self)
-        self.merchant_account = MerchantAccountGateway(self)
+        self.document_upload = DocumentUploadGateway(self)
+        self.exchange_rate_quote = ExchangeRateQuoteGateway(self)
+        self.graphql_client = self.config.graphql_client()
         self.merchant = MerchantGateway(self)
+        self.merchant_account = MerchantAccountGateway(self)
         self.oauth = OAuthGateway(self)
+        self.payment_method = PaymentMethodGateway(self)
+        self.payment_method_nonce = PaymentMethodNonceGateway(self)
+        self.paypal_account = PayPalAccountGateway(self)
         self.plan = PlanGateway(self)
+        self.sepa_direct_debit_account = SepaDirectDebitAccountGateway(self)
         self.settlement_batch_summary = SettlementBatchSummaryGateway(self)
         self.subscription = SubscriptionGateway(self)
+        self.testing = TestingGateway(self)
         self.transaction = TransactionGateway(self)
         self.transaction_line_item = TransactionLineItemGateway(self)
-        self.transparent_redirect = TransparentRedirectGateway(self)
+        self.us_bank_account = UsBankAccountGateway(self)
+        self.us_bank_account_verification = UsBankAccountVerificationGateway(self)
         self.verification = CreditCardVerificationGateway(self)
         self.webhook_notification = WebhookNotificationGateway(self)
         self.webhook_testing = WebhookTestingGateway(self)
-        self.payment_method = PaymentMethodGateway(self)
-        self.payment_method_nonce = PaymentMethodNonceGateway(self)
-        self.paypal_account = PayPalAccountGateway(self)
-        self.testing = TestingGateway(self)
-        self.us_bank_account = UsBankAccountGateway(self)
-        self.us_bank_account_verification = UsBankAccountVerificationGateway(self)
-        # NEXT_MAJOR_VERSION Remove this class as legacy Ideal has been removed/disabled in the Braintree Gateway
-        # DEPRECATED If you're looking to accept iDEAL as a payment method contact accounts@braintreepayments.com for a solution.
-        self.ideal_payment = IdealPaymentGateway(self)
diff --git a/braintree/client_token.py b/braintree/client_token.py
index eb85235..1be04cc 100644
--- a/braintree/client_token.py
+++ b/braintree/client_token.py
@@ -6,10 +6,13 @@ from braintree.signature_service import SignatureService
 from braintree.util.crypto import Crypto
 from braintree import exceptions
 
+
 class ClientToken(object):
 
     @staticmethod
-    def generate(params={}, gateway=None):
+    def generate(params=None, gateway=None):
+        if params is None:
+            params = {}
         if gateway is None:
             gateway = Configuration.gateway().client_token
 
diff --git a/braintree/client_token_gateway.py b/braintree/client_token_gateway.py
index ecea6a2..8273d49 100644
--- a/braintree/client_token_gateway.py
+++ b/braintree/client_token_gateway.py
@@ -3,14 +3,16 @@ from braintree.resource import Resource
 from braintree.client_token import ClientToken
 from braintree import exceptions
 
+
 class ClientTokenGateway(object):
     def __init__(self, gateway):
         self.gateway = gateway
         self.config = gateway.config
 
-
-    def generate(self, params={}):
-        if "options" in params and not "customer_id" in params:
+    def generate(self, params=None):
+        if params is None:
+            params = {}
+        if "options" in params and "customer_id" not in params:
             for option in ["verify_card", "make_default", "fail_on_duplicate_payment_method"]:
                 if option in params["options"]:
                     raise exceptions.InvalidSignatureError("cannot specify %s without a customer_id" % option)
diff --git a/braintree/coinbase_account.py b/braintree/coinbase_account.py
deleted file mode 100644
index 3544cd7..0000000
--- a/braintree/coinbase_account.py
+++ /dev/null
@@ -1,8 +0,0 @@
-import braintree
-from braintree.resource import Resource
-
-class CoinbaseAccount(Resource):
-    def __init__(self, gateway, attributes):
-        Resource.__init__(self, gateway, attributes)
-        if "subscriptions" in attributes:
-            self.subscriptions = [braintree.subscription.Subscription(gateway, subscription) for subscription in self.subscriptions]
diff --git a/braintree/configuration.py b/braintree/configuration.py
index 42fd84a..45bca10 100644
--- a/braintree/configuration.py
+++ b/braintree/configuration.py
@@ -58,7 +58,7 @@ class Configuration(object):
 
     @staticmethod
     def api_version():
-        return "5"
+        return "6"
 
     @staticmethod
     def graphql_api_version():
diff --git a/braintree/credit_card.py b/braintree/credit_card.py
index 3931ef0..7f5a0c5 100644
--- a/braintree/credit_card.py
+++ b/braintree/credit_card.py
@@ -3,9 +3,9 @@ import warnings
 from braintree.resource import Resource
 from braintree.address import Address
 from braintree.configuration import Configuration
-from braintree.transparent_redirect import TransparentRedirect
 from braintree.credit_card_verification import CreditCardVerification
 
+
 class CreditCard(Resource):
     """
     A class representing Braintree CreditCard objects.
@@ -38,7 +38,7 @@ class CreditCard(Resource):
         print(result.credit_card.token)
         print(result.credit_card.masked_number)
 
-    For more information on CreditCards, see https://developers.braintreepayments.com/reference/request/credit-card/create/python
+    For more information on CreditCards, see https://developer.paypal.com/braintree/docs/reference/request/credit-card/create/python
 
     """
     class CardType(object):
@@ -95,6 +95,7 @@ class CreditCard(Resource):
         International = "international"
         US = "us"
 
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
     class CardTypeIndicator(object):
         """
         Constants representing the three states for the card type indicator attributes
@@ -111,19 +112,7 @@ class CreditCard(Resource):
             CountryOfIssuance = IssuingBank = Payroll = Prepaid = ProductId = CardTypeIndicator
 
     @staticmethod
-    def confirm_transparent_redirect(query_string):
-        """
-        Confirms a transparent redirect request. It expects the query string from the
-        redirect request. The query string should _not_ include the leading "?" character. ::
-
-            result = braintree.CreditCard.confirm_transparent_redirect_request("foo=bar&id=12345")
-        """
-
-        warnings.warn("Please use TransparentRedirect.confirm instead", DeprecationWarning)
-        return Configuration.gateway().credit_card.confirm_transparent_redirect(query_string)
-
-    @staticmethod
-    def create(params={}):
+    def create(params=None):
         """
         Create a CreditCard.
 
@@ -135,11 +124,12 @@ class CreditCard(Resource):
             })
 
         """
-
+        if params is None:
+            params = {}
         return Configuration.gateway().credit_card.create(params)
 
     @staticmethod
-    def update(credit_card_token, params={}):
+    def update(credit_card_token, params=None):
         """
         Update an existing CreditCard
 
@@ -150,7 +140,8 @@ class CreditCard(Resource):
             })
 
         """
-
+        if params is None:
+            params = {}
         return Configuration.gateway().credit_card.update(credit_card_token, params)
 
     @staticmethod
@@ -187,14 +178,6 @@ class CreditCard(Resource):
         """
         return Configuration.gateway().credit_card.find(credit_card_token)
 
-    @staticmethod
-    def forward(credit_card_token, receiving_merchant_id):
-        """
-        This method has been deprecated. Please consider the Grant API instead.
-        """
-
-        return Configuration.gateway().credit_card.forward(credit_card_token, receiving_merchant_id)
-
     @staticmethod
     def from_nonce(nonce):
         """
@@ -232,13 +215,14 @@ class CreditCard(Resource):
         ]
 
         options = [
+            "fail_on_duplicate_payment_method",
             "make_default",
+            "skip_advanced_fraud_checking",
+            "venmo_sdk_session",
+            "verification_account_type",
+            "verification_amount",
             "verification_merchant_account_id",
             "verify_card",
-            "verification_amount",
-            "verification_account_type",
-            "venmo_sdk_session",
-            "fail_on_duplicate_payment_method",
             {
                 "adyen":[
                     "overwrite_brand",
@@ -247,6 +231,14 @@ class CreditCard(Resource):
             }
         ]
 
+        three_d_secure_pass_thru = [
+            "cavv",
+            "ds_transaction_id",
+            "eci_flag",
+            "three_d_secure_version",
+            "xid"
+        ]
+
         signature = [
             "billing_address_id",
             "cardholder_name",
@@ -254,18 +246,20 @@ class CreditCard(Resource):
             "expiration_date",
             "expiration_month",
             "expiration_year",
-            "device_session_id",
-            "fraud_merchant_id",
             "number",
             "token",
             "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
             {
                 "billing_address": billing_address_params
             },
             {
                 "options": options
+            },
+            {
+                "three_d_secure_pass_thru": three_d_secure_pass_thru
             }
         ]
 
@@ -281,38 +275,6 @@ class CreditCard(Resource):
 
         return signature
 
-    @staticmethod
-    def transparent_redirect_create_url():
-        """
-        Returns the url to use for creating CreditCards through transparent redirect.
-        """
-        warnings.warn("Please use TransparentRedirect.url instead", DeprecationWarning)
-        return Configuration.gateway().credit_card.transparent_redirect_create_url()
-
-    @staticmethod
-    def tr_data_for_create(tr_data, redirect_url):
-        """
-        Builds tr_data for CreditCard creation.
-        """
-
-        return Configuration.gateway().credit_card.tr_data_for_create(tr_data, redirect_url)
-
-    @staticmethod
-    def tr_data_for_update(tr_data, redirect_url):
-        """
-        Builds tr_data for CreditCard updating.
-        """
-
-        return Configuration.gateway().credit_card.tr_data_for_update(tr_data, redirect_url)
-
-    @staticmethod
-    def transparent_redirect_update_url():
-        """
-        Returns the url to be used for updating CreditCards through transparent redirect.
-        """
-        warnings.warn("Please use TransparentRedirect.url instead", DeprecationWarning)
-        return Configuration.gateway().credit_card.transparent_redirect_update_url()
-
     def __init__(self, gateway, attributes):
         Resource.__init__(self, gateway, attributes)
         self.is_expired = self.expired
@@ -339,4 +301,3 @@ class CreditCard(Resource):
         Returns the masked number of the CreditCard.
         """
         return self.bin + "******" + self.last_4
-
diff --git a/braintree/credit_card_gateway.py b/braintree/credit_card_gateway.py
index 4002701..18adec5 100644
--- a/braintree/credit_card_gateway.py
+++ b/braintree/credit_card_gateway.py
@@ -1,4 +1,5 @@
 import braintree
+import warnings
 from braintree.credit_card import CreditCard
 from braintree.error_result import ErrorResult
 from braintree.exceptions.not_found_error import NotFoundError
@@ -6,19 +7,18 @@ from braintree.ids_search import IdsSearch
 from braintree.resource import Resource
 from braintree.resource_collection import ResourceCollection
 from braintree.successful_result import SuccessfulResult
-from braintree.transparent_redirect import TransparentRedirect
+
 
 class CreditCardGateway(object):
     def __init__(self, gateway):
         self.gateway = gateway
         self.config = gateway.config
 
-    def confirm_transparent_redirect(self, query_string):
-        id = self.gateway.transparent_redirect._parse_and_validate_query_string(query_string)["id"][0]
-        return self._post("/payment_methods/all/confirm_transparent_redirect_request", {"id": id})
-
-    def create(self, params={}):
+    def create(self, params=None):
+        if params is None:
+            params = {}
         Resource.verify_keys(params, CreditCard.create_signature())
+        self.__check_for_deprecated_attributes(params)
         return self._post("/payment_methods", {"credit_card": params})
 
     def delete(self, credit_card_token):
@@ -57,24 +57,11 @@ class CreditCardGateway(object):
         except NotFoundError:
             raise NotFoundError("payment method with nonce " + repr(nonce) + " locked, consumed or not found")
 
-    def tr_data_for_create(self, tr_data, redirect_url):
-        Resource.verify_keys(tr_data, [{"credit_card": CreditCard.create_signature()}])
-        tr_data["kind"] = TransparentRedirect.Kind.CreatePaymentMethod
-        return self.gateway.transparent_redirect.tr_data(tr_data, redirect_url)
-
-    def tr_data_for_update(self, tr_data, redirect_url):
-        Resource.verify_keys(tr_data, ["payment_method_token", {"credit_card": CreditCard.update_signature()}])
-        tr_data["kind"] = TransparentRedirect.Kind.UpdatePaymentMethod
-        return self.gateway.transparent_redirect.tr_data(tr_data, redirect_url)
-
-    def transparent_redirect_create_url(self):
-        return self.config.base_url() + self.config.base_merchant_path() + "/payment_methods/all/create_via_transparent_redirect_request"
-
-    def transparent_redirect_update_url(self):
-        return self.config.base_url() + self.config.base_merchant_path() + "/payment_methods/all/update_via_transparent_redirect_request"
-
-    def update(self, credit_card_token, params={}):
+    def update(self, credit_card_token, params=None):
+        if params is None:
+            params = {}
         Resource.verify_keys(params, CreditCard.update_signature())
+        self.__check_for_deprecated_attributes(params)
         response = self.config.http().put(self.config.base_merchant_path() + "/payment_methods/credit_card/" + credit_card_token, {"credit_card": params})
         if "credit_card" in response:
             return SuccessfulResult({"credit_card": CreditCard(self.gateway, response["credit_card"])})
@@ -93,10 +80,17 @@ class CreditCardGateway(object):
         response = self.config.http().post(self.config.base_merchant_path() + "/payment_methods/all/expiring?" + query, {"search": criteria})
         return [CreditCard(self.gateway, item) for item in ResourceCollection._extract_as_array(response["payment_methods"], "credit_card")]
 
-    def _post(self, url, params={}):
+    def _post(self, url, params=None):
+        if params is None:
+            params = {}
         response = self.config.http().post(self.config.base_merchant_path() + url, params)
         if "credit_card" in response:
             return SuccessfulResult({"credit_card": CreditCard(self.gateway, response["credit_card"])})
         elif "api_error_response" in response:
             return ErrorResult(self.gateway, response["api_error_response"])
 
+    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)
diff --git a/braintree/credit_card_verification.py b/braintree/credit_card_verification.py
index b081433..9643f55 100644
--- a/braintree/credit_card_verification.py
+++ b/braintree/credit_card_verification.py
@@ -7,6 +7,7 @@ from braintree.resource import Resource
 
 class CreditCardVerification(AttributeGetter):
 
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
     class Status(object):
         """
         Constants representing transaction statuses. Available statuses are:
@@ -14,20 +15,18 @@ class CreditCardVerification(AttributeGetter):
         * braintree.CreditCardVerification.Status.Failed
         * braintree.CreditCardVerification.Status.GatewayRejected
         * braintree.CreditCardVerification.Status.ProcessorDeclined
-        * braintree.CreditCardVerification.Status.Unrecognized
         * braintree.CreditCardVerification.Status.Verified
         """
 
         Failed                 = "failed"
         GatewayRejected        = "gateway_rejected"
         ProcessorDeclined      = "processor_declined"
-        Unrecognized           = "unrecognized"
         Verified               = "verified"
 
     def __init__(self, gateway, attributes):
         AttributeGetter.__init__(self, attributes)
 
-        if "amount" in attributes and self.amount:
+        if "amount" in attributes and getattr(self, "amount", None):
             self.amount = Decimal(self.amount)
         else:
             self.amount = None
@@ -82,7 +81,24 @@ class CreditCardVerification(AttributeGetter):
         options_params = [
                 "account_type", "amount", "merchant_account_id"
             ]
-        return [{"credit_card": credit_card_params}, {"options": options_params}]
+        three_d_secure_pass_thru_params = [
+                "eci_flag",
+                "cavv",
+                "xid",
+                "authentication_response",
+                "directory_response",
+                "cavv_algorithm",
+                "ds_transaction_id",
+                "three_d_secure_version"
+                ]
+
+        return [
+                {"credit_card": credit_card_params},
+                "intended_transaction_source",
+                {"options": options_params},
+                "payment_method_nonce",
+                "three_d_secure_authentication_id",
+                {"three_d_secure_pass_thru": three_d_secure_pass_thru_params}]
 
     def __eq__(self, other):
         if not isinstance(other, CreditCardVerification):
diff --git a/braintree/credit_card_verification_gateway.py b/braintree/credit_card_verification_gateway.py
index 40b69e8..3025bc7 100644
--- a/braintree/credit_card_verification_gateway.py
+++ b/braintree/credit_card_verification_gateway.py
@@ -6,6 +6,7 @@ from braintree.resource_collection import ResourceCollection
 from braintree.error_result import ErrorResult
 from braintree.successful_result import SuccessfulResult
 
+
 class CreditCardVerificationGateway(object):
     def __init__(self, gateway):
         self.gateway = gateway
@@ -51,8 +52,8 @@ class CreditCardVerificationGateway(object):
         return [CreditCardVerification(self.gateway, item) for item in ResourceCollection._extract_as_array(response["credit_card_verifications"], "verification")]
 
     def create(self, params):
-       response = self.config.http().post(self.config.base_merchant_path() + "/verifications", {"verification": params})
-       if "verification" in response:
-           return SuccessfulResult({"verification": CreditCardVerification(self.gateway, response["verification"])})
-       elif "api_error_response" in response:
-           return ErrorResult(self.gateway, response["api_error_response"])
+        response = self.config.http().post(self.config.base_merchant_path() + "/verifications", {"verification": params})
+        if "verification" in response:
+            return SuccessfulResult({"verification": CreditCardVerification(self.gateway, response["verification"])})
+        elif "api_error_response" in response:
+            return ErrorResult(self.gateway, response["api_error_response"])
diff --git a/braintree/credit_card_verification_search.py b/braintree/credit_card_verification_search.py
index af26c8b..8a89cf2 100644
--- a/braintree/credit_card_verification_search.py
+++ b/braintree/credit_card_verification_search.py
@@ -15,3 +15,4 @@ class CreditCardVerificationSearch:
     billing_postal_code          = Search.TextNodeBuilder("billing_address_details_postal_code")
     customer_email               = Search.TextNodeBuilder("customer_email")
     customer_id                  = Search.TextNodeBuilder("customer_id")
+    payment_method_token         = Search.TextNodeBuilder("payment_method_token")
diff --git a/braintree/customer.py b/braintree/customer.py
index 4c9b0ec..2d76c1e 100644
--- a/braintree/customer.py
+++ b/braintree/customer.py
@@ -8,9 +8,9 @@ from braintree.android_pay_card import AndroidPayCard
 from braintree.amex_express_checkout_card import AmexExpressCheckoutCard
 from braintree.credit_card import CreditCard
 from braintree.paypal_account import PayPalAccount
+from braintree.sepa_direct_debit_account import SepaDirectDebitAccount
 from braintree.europe_bank_account import EuropeBankAccount
 from braintree.us_bank_account import UsBankAccount
-from braintree.coinbase_account import CoinbaseAccount
 from braintree.venmo_account import VenmoAccount
 from braintree.visa_checkout_card import VisaCheckoutCard
 from braintree.masterpass_card import MasterpassCard
@@ -19,9 +19,9 @@ from braintree.configuration import Configuration
 from braintree.ids_search import IdsSearch
 from braintree.exceptions.not_found_error import NotFoundError
 from braintree.resource_collection import ResourceCollection
-from braintree.transparent_redirect import TransparentRedirect
 from braintree.samsung_pay_card import SamsungPayCard
 
+
 class Customer(Resource):
     """
     A class representing a customer.
@@ -67,13 +67,14 @@ class Customer(Resource):
         print(result.customer.id)
         print(result.customer.first_name)
 
-    For more information on Customers, see https://developers.braintreepayments.com/reference/request/customer/create/python
+    For more information on Customers, see https://developer.paypal.com/braintree/docs/reference/request/customer/create/python
 
     """
 
     def __repr__(self):
         detail_list = [
             "id",
+            "graphql_id",
             "company",
             "created_at",
             "email",
@@ -94,19 +95,7 @@ class Customer(Resource):
         return Configuration.gateway().customer.all()
 
     @staticmethod
-    def confirm_transparent_redirect(query_string):
-        """
-        Confirms a transparent redirect request.  It expects the query string from the
-        redirect request.  The query string should _not_ include the leading "?" character. ::
-
-            result = braintree.Customer.confirm_transparent_redirect_request("foo=bar&id=12345")
-        """
-
-        warnings.warn("Please use TransparentRedirect.confirm instead", DeprecationWarning)
-        return Configuration.gateway().customer.confirm_transparent_redirect(query_string)
-
-    @staticmethod
-    def create(params={}):
+    def create(params=None):
         """
         Create a Customer
 
@@ -118,7 +107,8 @@ class Customer(Resource):
             })
 
         """
-
+        if params is None:
+            params = {}
         return Configuration.gateway().customer.create(params)
 
     @staticmethod
@@ -151,33 +141,7 @@ class Customer(Resource):
         return Configuration.gateway().customer.search(*query)
 
     @staticmethod
-    def tr_data_for_create(tr_data, redirect_url):
-        """ Builds tr_data for creating a Customer. """
-
-        return Configuration.gateway().customer.tr_data_for_create(tr_data, redirect_url)
-
-    @staticmethod
-    def tr_data_for_update(tr_data, redirect_url):
-        """ Builds tr_data for updating a Customer. """
-
-        return Configuration.gateway().customer.tr_data_for_update(tr_data, redirect_url)
-
-    @staticmethod
-    def transparent_redirect_create_url():
-        """ Returns the url to use for creating Customers through transparent redirect. """
-
-        warnings.warn("Please use TransparentRedirect.url instead", DeprecationWarning)
-        return Configuration.gateway().customer.transparent_redirect_create_url()
-
-    @staticmethod
-    def transparent_redirect_update_url():
-        """ Returns the url to use for updating Customers through transparent redirect. """
-
-        warnings.warn("Please use TransparentRedirect.url instead", DeprecationWarning)
-        return Configuration.gateway().customer.transparent_redirect_update_url()
-
-    @staticmethod
-    def update(customer_id, params={}):
+    def update(customer_id, params=None):
         """
         Update an existing Customer
 
@@ -188,16 +152,27 @@ class Customer(Resource):
             })
 
         """
-
+        if params is None:
+            params = {}
         return Configuration.gateway().customer.update(customer_id, params)
 
     @staticmethod
     def create_signature():
         return [
-            "company", "email", "fax", "first_name", "id", "last_name", "phone", "website", "device_data", "device_session_id", "fraud_merchant_id", "payment_method_nonce",
-            {"risk_data": ["customer_browser", "customer_ip"]},
+            "company", "email", "fax", "first_name", "id", "last_name", "phone", "website", "device_data", "payment_method_nonce",
+            "device_session_id", "fraud_merchant_id", # NEXT_MAJOR_VERSION remove device_session_id and fraud_merchant_id
+            {"risk_data": ["customer_browser", "customer_device_id", "customer_ip", "customer_location_zip", "customer_tenure"]},
             {"credit_card": CreditCard.create_signature()},
+            {"apple_pay_card": ApplePayCard.signature()},
             {"custom_fields": ["__any_key__"]},
+            {"three_d_secure_pass_thru": [
+                "cavv",
+                "ds_transaction_id",
+                "eci_flag",
+                "three_d_secure_version",
+                "xid",
+                ]},
+            {"tax_identifiers": ["country_code", "identifier"]},
             {"options": [{"paypal": [
                 "payee_email",
                 "order_id",
@@ -213,7 +188,16 @@ class Customer(Resource):
         return [
             "company", "email", "fax", "first_name", "id", "last_name", "phone", "website", "device_data", "device_session_id", "fraud_merchant_id", "payment_method_nonce", "default_payment_method_token",
             {"credit_card": CreditCard.signature("update_via_customer")},
+            {"apple_pay_card": ApplePayCard.signature()},
+            {"three_d_secure_pass_thru": [
+                "cavv",
+                "ds_transaction_id",
+                "eci_flag",
+                "three_d_secure_version",
+                "xid",
+                ]},
             {"custom_fields": ["__any_key__"]},
+            {"tax_identifiers": ["country_code", "identifier"]},
             {"options": [{"paypal": [
                 "payee_email",
                 "order_id",
@@ -246,6 +230,7 @@ class Customer(Resource):
             self.android_pay_cards  = [AndroidPayCard(gateway, android_pay_card) for android_pay_card in self.android_pay_cards]
             self.payment_methods += self.android_pay_cards
 
+        # NEXT_MAJOR_VERSION remove deprecated amex express checkout
         if "amex_express_checkout_cards" in attributes:
             self.amex_express_checkout_cards  = [AmexExpressCheckoutCard(gateway, amex_express_checkout_card) for amex_express_checkout_card in self.amex_express_checkout_cards]
             self.payment_methods += self.amex_express_checkout_cards
@@ -254,14 +239,14 @@ class Customer(Resource):
             self.europe_bank_accounts = [EuropeBankAccount(gateway, europe_bank_account) for europe_bank_account in self.europe_bank_accounts]
             self.payment_methods += self.europe_bank_accounts
 
-        if "coinbase_accounts" in attributes:
-            self.coinbase_accounts = [CoinbaseAccount(gateway, coinbase_account) for coinbase_account in self.coinbase_accounts]
-            self.payment_methods += self.coinbase_accounts
-
         if "venmo_accounts" in attributes:
             self.venmo_accounts = [VenmoAccount(gateway, venmo_account) for venmo_account in self.venmo_accounts]
             self.payment_methods += self.venmo_accounts
 
+        if "sepa_debit_accounts" in attributes:
+            self.sepa_direct_debit_accounts  = [SepaDirectDebitAccount(gateway, sepa_direct_debit_account) for sepa_direct_debit_account in self.sepa_debit_accounts]
+            self.payment_methods += self.sepa_direct_debit_accounts
+
         if "us_bank_accounts" in attributes:
             self.us_bank_accounts = [UsBankAccount(gateway, us_bank_account) for us_bank_account in self.us_bank_accounts]
             self.payment_methods += self.us_bank_accounts
@@ -270,6 +255,7 @@ class Customer(Resource):
             self.visa_checkout_cards = [VisaCheckoutCard(gateway, visa_checkout_card) for visa_checkout_card in self.visa_checkout_cards]
             self.payment_methods += self.visa_checkout_cards
 
+        # NEXT_MAJOR_VERSION remove deprecated masterpass
         if "masterpass_cards" in attributes:
             self.masterpass_cards = [MasterpassCard(gateway, masterpass_card) for masterpass_card in self.masterpass_cards]
             self.payment_methods += self.masterpass_cards
diff --git a/braintree/customer_gateway.py b/braintree/customer_gateway.py
index 6ea6b64..3c494ea 100644
--- a/braintree/customer_gateway.py
+++ b/braintree/customer_gateway.py
@@ -1,4 +1,5 @@
 import braintree
+import warnings
 from braintree.customer import Customer
 from braintree.error_result import ErrorResult
 from braintree.exceptions.not_found_error import NotFoundError
@@ -6,7 +7,7 @@ from braintree.ids_search import IdsSearch
 from braintree.resource import Resource
 from braintree.resource_collection import ResourceCollection
 from braintree.successful_result import SuccessfulResult
-from braintree.transparent_redirect import TransparentRedirect
+
 
 class CustomerGateway(object):
     def __init__(self, gateway):
@@ -17,12 +18,11 @@ class CustomerGateway(object):
         response = self.config.http().post(self.config.base_merchant_path() + "/customers/advanced_search_ids")
         return ResourceCollection({}, response, self.__fetch)
 
-    def confirm_transparent_redirect(self, query_string):
-        id = self.gateway.transparent_redirect._parse_and_validate_query_string(query_string)["id"][0]
-        return self._post("/customers/all/confirm_transparent_redirect_request", {"id": id})
-
-    def create(self, params={}):
+    def create(self, params=None):
+        if params is None:
+            params = {}
         Resource.verify_keys(params, Customer.create_signature())
+        self.__check_for_deprecated_attributes(params)
         return self._post("/customers", {"customer": params})
 
     def delete(self, customer_id):
@@ -50,24 +50,11 @@ class CustomerGateway(object):
         response = self.config.http().post(self.config.base_merchant_path() + "/customers/advanced_search_ids", {"search": self.__criteria(query)})
         return ResourceCollection(query, response, self.__fetch)
 
-    def tr_data_for_create(self, tr_data, redirect_url):
-        Resource.verify_keys(tr_data, [{"customer": Customer.create_signature()}])
-        tr_data["kind"] = TransparentRedirect.Kind.CreateCustomer
-        return self.gateway.transparent_redirect.tr_data(tr_data, redirect_url)
-
-    def tr_data_for_update(self, tr_data, redirect_url):
-        Resource.verify_keys(tr_data, ["customer_id", {"customer": Customer.update_signature()}])
-        tr_data["kind"] = TransparentRedirect.Kind.UpdateCustomer
-        return self.gateway.transparent_redirect.tr_data(tr_data, redirect_url)
-
-    def transparent_redirect_create_url(self):
-        return self.config.base_url() + self.config.base_merchant_path() + "/customers/all/create_via_transparent_redirect_request"
-
-    def transparent_redirect_update_url(self):
-        return self.config.base_url() + self.config.base_merchant_path() + "/customers/all/update_via_transparent_redirect_request"
-
-    def update(self, customer_id, params={}):
+    def update(self, customer_id, params=None):
+        if params is None:
+            params = {}
         Resource.verify_keys(params, Customer.update_signature())
+        self.__check_for_deprecated_attributes(params)
         response = self.config.http().put(self.config.base_merchant_path() + "/customers/" + customer_id, {"customer": params})
         if "customer" in response:
             return SuccessfulResult({"customer": Customer(self.gateway, response["customer"])})
@@ -89,7 +76,9 @@ class CustomerGateway(object):
         response = self.config.http().post(self.config.base_merchant_path() + "/customers/advanced_search", {"search": criteria})
         return [Customer(self.gateway, item) for item in ResourceCollection._extract_as_array(response["customers"], "customer")]
 
-    def _post(self, url, params={}):
+    def _post(self, url, params=None):
+        if params is None:
+            params = {}
         response = self.config.http().post(self.config.base_merchant_path() + url, params)
         if "customer" in response:
             return SuccessfulResult({"customer": Customer(self.gateway, response["customer"])})
@@ -98,3 +87,8 @@ class CustomerGateway(object):
         else:
             pass
 
+    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)
diff --git a/braintree/disbursement_detail.py b/braintree/disbursement_detail.py
index b8c9a84..e1bf221 100644
--- a/braintree/disbursement_detail.py
+++ b/braintree/disbursement_detail.py
@@ -5,9 +5,9 @@ class DisbursementDetail(AttributeGetter):
     def __init__(self, attributes):
         AttributeGetter.__init__(self, attributes)
 
-        if self.settlement_amount is not None:
+        if getattr(self, "settlement_amount", None) is not None:
             self.settlement_amount = Decimal(self.settlement_amount)
-        if self.settlement_currency_exchange_rate is not None:
+        if getattr(self, "settlement_currency_exchange_rate", None) is not None:
             self.settlement_currency_exchange_rate = Decimal(self.settlement_currency_exchange_rate)
 
     @property
diff --git a/braintree/dispute.py b/braintree/dispute.py
index 2049bf9..179e4c2 100644
--- a/braintree/dispute.py
+++ b/braintree/dispute.py
@@ -1,27 +1,32 @@
+import warnings
 from decimal import Decimal
 from braintree.attribute_getter import AttributeGetter
 from braintree.transaction_details import TransactionDetails
-from braintree.dispute_details import DisputeEvidence, DisputeStatusHistory
+from braintree.dispute_details import DisputeEvidence, DisputeStatusHistory, DisputePayPalMessage
 from braintree.configuration import Configuration
 
 class Dispute(AttributeGetter):
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
     class Status(object):
         """
         Constants representing dispute statuses. Available types are:
 
         * braintree.Dispute.Status.Accepted
+        * braintree.Dispute.Status.AutoAccepted
         * braintree.Dispute.Status.Disputed
         * braintree.Dispute.Status.Open
         * braintree.Dispute.Status.Won
         * braintree.Dispute.Status.Lost
         """
         Accepted = "accepted"
+        AutoAccepted = "auto_accepted"
         Disputed = "disputed"
         Expired = "expired"
         Open  = "open"
         Won  = "won"
         Lost = "lost"
 
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
     class Reason(object):
         """
         Constants representing dispute reasons. Available types are:
@@ -50,6 +55,7 @@ class Dispute(AttributeGetter):
         Retrieval                     = "retrieval"
         TransactionAmountDiffers      = "transaction_amount_differs"
 
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
     class Kind(object):
         """
         Constants representing dispute kinds. Available types are:
@@ -62,6 +68,44 @@ class Dispute(AttributeGetter):
         PreArbitration = "pre_arbitration"
         Retrieval      = "retrieval"
 
+    # NEXT_MAJOR_VERSION Remove this enum
+    class ChargebackProtectionLevel(object):
+        """
+        Constants representing dispute ChargebackProtectionLevel. Available types are:
+
+        * braintree.Dispute.ChargebackProtectionLevel.EFFORTLESS
+        * braintree.Dispute.ChargebackProtectionLevel.STANDARD
+        * braintree.Dispute.ChargebackProtectionLevel.NOT_PROTECTED
+        """
+        warnings.warn("Use ProtectionLevel enum instead", DeprecationWarning)
+        Effortless     = "effortless"
+        Standard       = "standard"
+        NotProtected   = "not_protected"
+
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
+    class PreDisputeProgram(object):
+        """
+        Constants representing dispute pre-dispute programs. Available types are:
+
+        * braintree.Dispute.PreDisputeProgram.NONE
+        * braintree.Dispute.PreDisputeProgram.VisaRdr
+        """
+        NONE = "none"
+        VisaRdr = "visa_rdr"
+
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
+    class ProtectionLevel(object):
+        """
+        Constants representing dispute ProtectionLevel. Available types are:
+
+        * braintree.Dispute.ProtectionLevel.EffortlessCBP
+        * braintree.Dispute.ProtectionLevel.StandardCBP
+        * braintree.Dispute.ProtectionLevel.NoProtection
+        """
+        EffortlessCBP  = "Effortless Chargeback Protection tool"
+        StandardCBP    = "Chargeback Protection tool"
+        NoProtection   = "No Protection"
+
     @staticmethod
     def accept(id):
         """
@@ -160,20 +204,28 @@ class Dispute(AttributeGetter):
         return Configuration.gateway().dispute.search(*query)
 
     def __init__(self, attributes):
+        if "chargeback_protection_level" in attributes:
+            warnings.warn("Use protection_level attribute instead", DeprecationWarning)
         AttributeGetter.__init__(self, attributes)
 
-        if "amount" in attributes and self.amount is not None:
+        if "amount" in attributes and getattr(self, "amount", None) is not None:
             self.amount = Decimal(self.amount)
-        if "amount_disputed" in attributes and self.amount_disputed is not None:
+        if "amount_disputed" in attributes and getattr(self, "amount_disputed", None) is not None:
             self.amount_disputed = Decimal(self.amount_disputed)
-        if "amount_won" in attributes and self.amount_won is not None:
+        if "amount_won" in attributes and getattr(self, "amount_won", None) is not None:
             self.amount_won = Decimal(self.amount_won)
+        if "chargeback_protection_level" in attributes and getattr(self, "chargeback_protection_level", None) in [self.ChargebackProtectionLevel.Effortless, self.ChargebackProtectionLevel.Standard]:
+            self.protection_level = eval("self.ProtectionLevel.{0}CBP".format(self.chargeback_protection_level.capitalize()))
+        else:
+            self.protection_level = self.ProtectionLevel.NoProtection
         if "transaction" in attributes:
             self.transaction_details = TransactionDetails(attributes.pop("transaction"))
             self.transaction = self.transaction_details
-        if "evidence" in attributes and self.evidence is not None:
+        if "evidence" in attributes and getattr(self, "evidence", None) is not None:
             self.evidence = [DisputeEvidence(evidence) for evidence in self.evidence]
-        if "status_history" in attributes and self.status_history is not None:
+        if "paypal_messages" in attributes and getattr(self, "paypal_messages", None) is not None:
+            self.paypal_messages = [DisputePayPalMessage(paypal_message) for paypal_message in self.paypal_messages]
+        if "status_history" in attributes and getattr(self, "status_history", None) is not None:
             self.status_history = [DisputeStatusHistory(status_history) for status_history in self.status_history]
         if "processor_comments" in attributes and self.processor_comments is not None:
             self.forwarded_comments = self.processor_comments
diff --git a/braintree/dispute_details/__init__.py b/braintree/dispute_details/__init__.py
index 5cde7c2..696bfe9 100644
--- a/braintree/dispute_details/__init__.py
+++ b/braintree/dispute_details/__init__.py
@@ -1,2 +1,3 @@
 from braintree.dispute_details.evidence import DisputeEvidence
+from braintree.dispute_details.paypal_message import DisputePayPalMessage
 from braintree.dispute_details.status_history import DisputeStatusHistory
diff --git a/braintree/dispute_details/paypal_message.py b/braintree/dispute_details/paypal_message.py
new file mode 100644
index 0000000..2b447ed
--- /dev/null
+++ b/braintree/dispute_details/paypal_message.py
@@ -0,0 +1,5 @@
+from braintree.attribute_getter import AttributeGetter
+
+class DisputePayPalMessage(AttributeGetter):
+    def __init__(self, attributes):
+        AttributeGetter.__init__(self, attributes)
diff --git a/braintree/dispute_gateway.py b/braintree/dispute_gateway.py
index 3cfe88e..902dcf1 100644
--- a/braintree/dispute_gateway.py
+++ b/braintree/dispute_gateway.py
@@ -73,19 +73,14 @@ class DisputeGateway(object):
         except ValueError:
             raise ValueError("sequence_number must be an integer")
 
-        category = request.get("category", request.get("tag"))
-
-        if "tag" in request.keys():
-            warnings.warn("Please use category instead", DeprecationWarning)
-
-        if category is not None and not isinstance(category, str):
+        if request.get("category") is not None and not isinstance(request.get("category"), str):
             raise ValueError("category must be a string")
 
         try:
             response = self.config.http().post(self.config.base_merchant_path() + "/disputes/" + dispute_id + "/evidence", {
                 "evidence": {
                     "comments": request.get("content"),
-                    "category": category,
+                    "category": request.get("category"),
                     "sequence_number": request.get("sequence_number")
                 }
             })
diff --git a/braintree/dispute_search.py b/braintree/dispute_search.py
index 11f53df..821d71b 100644
--- a/braintree/dispute_search.py
+++ b/braintree/dispute_search.py
@@ -1,20 +1,25 @@
 from braintree.search import Search
 
 class DisputeSearch:
-    amount_disputed     = Search.RangeNodeBuilder("amount_disputed")
-    amount_won          = Search.RangeNodeBuilder("amount_won")
-    case_number         = Search.TextNodeBuilder("case_number")
-    customer_id         = Search.TextNodeBuilder("customer_id")
-    disbursement_date   = Search.RangeNodeBuilder("disbursement_date")
-    effective_date      = Search.RangeNodeBuilder("effective_date")
-    id                  = Search.TextNodeBuilder("id")
-    kind                = Search.MultipleValueNodeBuilder("kind")
-    merchant_account_id = Search.MultipleValueNodeBuilder("merchant_account_id")
-    reason              = Search.MultipleValueNodeBuilder("reason")
-    reason_code         = Search.MultipleValueNodeBuilder("reason_code")
-    received_date       = Search.RangeNodeBuilder("received_date")
-    reference_number    = Search.TextNodeBuilder("reference_number")
-    reply_by_date       = Search.RangeNodeBuilder("reply_by_date")
-    status              = Search.MultipleValueNodeBuilder("status")
-    transaction_id      = Search.TextNodeBuilder("transaction_id")
-    transaction_source  = Search.MultipleValueNodeBuilder("transaction_source")
+    amount_disputed             =   Search.RangeNodeBuilder("amount_disputed")
+    amount_won                  =   Search.RangeNodeBuilder("amount_won")
+    case_number                 =   Search.TextNodeBuilder("case_number")
+    # NEXT_MAJOR_VERSION Remove this attribute
+    # DEPRECATED The chargeback_protection_level attribute is deprecated in favor of protection_level
+    chargeback_protection_level =   Search.MultipleValueNodeBuilder("chargeback_protection_level")
+    protection_level            =   Search.MultipleValueNodeBuilder("protection_level")
+    customer_id                 =   Search.TextNodeBuilder("customer_id")
+    disbursement_date           =   Search.RangeNodeBuilder("disbursement_date")
+    effective_date              =   Search.RangeNodeBuilder("effective_date")
+    id                          =   Search.TextNodeBuilder("id")
+    kind                        =   Search.MultipleValueNodeBuilder("kind")
+    merchant_account_id         =   Search.MultipleValueNodeBuilder("merchant_account_id")
+    pre_dispute_program         =   Search.MultipleValueNodeBuilder("pre_dispute_program")
+    reason                      =   Search.MultipleValueNodeBuilder("reason")
+    reason_code                 =   Search.MultipleValueNodeBuilder("reason_code")
+    received_date               =   Search.RangeNodeBuilder("received_date")
+    reference_number            =   Search.TextNodeBuilder("reference_number")
+    reply_by_date               =   Search.RangeNodeBuilder("reply_by_date")
+    status                      =   Search.MultipleValueNodeBuilder("status")
+    transaction_id              =   Search.TextNodeBuilder("transaction_id")
+    transaction_source          =   Search.MultipleValueNodeBuilder("transaction_source")
diff --git a/braintree/document_upload.py b/braintree/document_upload.py
index 4a93f4e..55b794f 100644
--- a/braintree/document_upload.py
+++ b/braintree/document_upload.py
@@ -3,6 +3,7 @@ from braintree.successful_result import SuccessfulResult
 from braintree.resource import Resource
 from braintree.configuration import Configuration
 
+
 class DocumentUpload(Resource):
     """
     A class representing a DocumentUpload.
@@ -16,7 +17,7 @@ class DocumentUpload(Resource):
             }
         )
 
-    For more information on DocumentUploads, see https://developers.braintreepayments.com/reference/request/document_upload/create
+    For more information on DocumentUploads, see https://developer.paypal.com/braintree/docs/reference/request/document-upload/create
 
     """
 
@@ -24,7 +25,7 @@ class DocumentUpload(Resource):
         EvidenceDocument = "evidence_document"
 
     @staticmethod
-    def create(params={}):
+    def create(params=None):
         """
         Create a DocumentUpload
 
@@ -38,6 +39,8 @@ class DocumentUpload(Resource):
             )
 
         """
+        if params is None:
+            params = {}
         return Configuration.gateway().document_upload.create(params)
 
     @staticmethod
diff --git a/braintree/document_upload_gateway.py b/braintree/document_upload_gateway.py
index 15aea8c..ddfcd54 100644
--- a/braintree/document_upload_gateway.py
+++ b/braintree/document_upload_gateway.py
@@ -5,12 +5,15 @@ from braintree.error_result import ErrorResult
 from braintree.resource import Resource
 from braintree.successful_result import SuccessfulResult
 
+
 class DocumentUploadGateway(object):
     def __init__(self, gateway):
         self.gateway = gateway
         self.config = gateway.config
 
-    def create(self, params={}):
+    def create(self, params=None):
+        if params is None:
+            params = {}
         Resource.verify_keys(params, DocumentUpload.create_signature())
 
         if "file" in params and not hasattr(params["file"], "read"):
diff --git a/braintree/enriched_customer_data.py b/braintree/enriched_customer_data.py
new file mode 100644
index 0000000..5d1cc74
--- /dev/null
+++ b/braintree/enriched_customer_data.py
@@ -0,0 +1,10 @@
+from braintree.resource import Resource
+from braintree.venmo_profile_data import VenmoProfileData
+
+class EnrichedCustomerData(Resource):
+    """
+    A class representing Braintree EnrichedCustomerData object.
+    """
+    def __init__(self, gateway, attributes):
+        Resource.__init__(self, gateway, attributes)
+        self.profile_data = VenmoProfileData(gateway, attributes.pop("profile_data"))
diff --git a/braintree/error_codes.py b/braintree/error_codes.py
index c5e0949..202dd8c 100644
--- a/braintree/error_codes.py
+++ b/braintree/error_codes.py
@@ -18,7 +18,6 @@ class ErrorCodes(object):
         CountryCodeAlpha3IsNotAccepted = "91816"
         CountryCodeNumericIsNotAccepted = "91817"
         CountryNameIsNotAccepted = "91803"
-        ExtedAddressIsTooLong = "81804" # Deprecated
         ExtendedAddressIsInvalid = "91823"
         ExtendedAddressIsTooLong = "81804"
         FirstNameIsInvalid = "91819"
@@ -65,6 +64,8 @@ class ErrorCodes(object):
         InvalidToken = "83520"
         PrivateKeyMismatch = "93521"
         KeyMismatchStoringCertificate = "93522"
+        CustomerIdIsInvalid = "93528"
+        BillingAddressFormatIsInvalid = "93529"
 
     class AuthorizationFingerprint(object):
         MissingFingerprint = "93201"
@@ -156,7 +157,6 @@ class ErrorCodes(object):
         FaxIsTooLong = "81607"
         FirstNameIsTooLong = "81608"
         IdIsInUse = "91609"
-        IdIsInvaild = "91610" # Deprecated
         IdIsInvalid = "91610"
         IdIsNotAllowed = "91611"
         IdIsRequired = "91613"
@@ -206,6 +206,7 @@ class ErrorCodes(object):
         FileTypeIsInvalid = "84903"
         FileIsMalformedOrEncrypted = "84904"
         FileIsTooLong = "84905"
+        FileIsEmpty = "84906"
 
     class Merchant(object):
         CountryCannotBeBlank = "83603"
@@ -337,6 +338,27 @@ class ErrorCodes(object):
         UnsupportedGrantType = "93805"
 
     class Verification(object):
+        ThreeDSecureAuthenticationIdIsInvalid = "942196"
+        ThreeDSecureAuthenticationIdDoesntMatchNonceThreeDSecureAuthentication = "942198"
+        ThreeDSecureTransactionPaymentMethodDoesntMatchThreeDSecureAuthenticationPaymentMethod = "942197"
+        ThreeDSecureAuthenticationIdWithThreeDSecurePassThruIsInvalid = "942199"
+        ThreeDSecureAuthenticationFailed = "94271"
+        ThreeDSecureTokenIsInvalid = "94268"
+        ThreeDSecureVerificationDataDoesntMatchVerify = "94270"
+        MerchantAccountDoesNotSupport3DSecure = "942169"
+        MerchantAcountDoesNotMatch3DSecureMerchantAccount = "94284"
+        AmountDoesNotMatch3DSecureAmount = "94285"
+        class ThreeDSecurePassThru(object):
+            EciFlagIsRequired = "942113"
+            EciFlagIsInvalid = "942114"
+            CavvIsRequired = "942116"
+            ThreeDSecureVersionIsRequired = "942117"
+            ThreeDSecureVersionIsInvalid = "942119"
+            AuthenticationResponseIsInvalid = "942120"
+            DirectoryResponseIsInvalid = "942121"
+            CavvAlgorithmIsInvalid = "942122"
+
+
         class Options(object):
             AmountCannotBeNegative = "94201"
             AmountFormatIsInvalid = "94202"
@@ -403,6 +425,11 @@ class ErrorCodes(object):
         IBANIsRequired = "83303"
         AccountHolderNameIsRequired = "83301"
 
+    class SepaDirectDebitAccount(object):
+        SepaDebitAccountPaymentMethodMandateTypeIsNotSupported = "87115"
+        SepaDebitAccountPaymentMethodCustomerIdIsInvalid = "87116"
+        SepaDebitAccountPaymentMethodCustomerIdIsRequired = "87117"
+
     class Subscription(object):
         BillingDayOfMonthCannotBeUpdated = "91918"
         BillingDayOfMonthIsInvalid = "91914"
@@ -469,15 +496,16 @@ class ErrorCodes(object):
             IdToRemoveIsInvalid = "92025"
 
     class Transaction(object):
+        AdjustmentAmountMustBeGreaterThanZero = "95605"
         AmountCannotBeNegative = "81501"
         AmountDoesNotMatch3DSecureAmount = "91585"
-        AmountDoesNotMatchIdealPaymentAmount = "915144"
         AmountIsInvalid = AmountFormatIsInvalid = "81503"
         AmountIsRequired = "81502"
         AmountIsTooLarge = "81528"
         AmountMustBeGreaterThanZero = "81531"
         AmountNotSupportedByProcessor = "815193"
         BillingAddressConflict = "91530"
+        BillingPhoneNumberIsInvalid = "915206"
         CannotBeVoided = "91504"
         CannotCancelRelease = "91562"
         CannotCloneCredit = "91543"
@@ -506,39 +534,29 @@ class ErrorCodes(object):
         CustomerDefaultPaymentMethodCardTypeIsNotAccepted = "81509"
         CustomerDoesNotHaveCreditCard = "91511"
         CustomerIdIsInvalid = "91510"
+        DiscountAmountCannotBeNegative = "915160"
+        DiscountAmountFormatIsInvalid = "915159"
+        DiscountAmountIsTooLarge = "915161"
+        ExchangeRateQuoteIdIsTooLong = "915229"
         FailedAuthAdjustmentAllowRetry = "95603"
         FailedAuthAdjustmentHardDecline = "95602"
         FinalAuthSubmitForSettlementForDifferentAmount = "95601"
         HasAlreadyBeenRefunded = "91512"
-        IdealPaymentNotComplete = "815141"
-        IdealPaymentsCannotBeVaulted = "915150"
-        PaymentInstrumentWithExternalVaultIsInvalid = "915176"
-        DiscountAmountFormatIsInvalid = "915159"
-        DiscountAmountCannotBeNegative = "915160"
-        DiscountAmountIsTooLarge = "915161"
-        ShippingAmountFormatIsInvalid = "915162"
-        ShippingAmountCannotBeNegative = "915163"
-        ShippingAmountIsTooLarge = "915164"
-        ShipsFromPostalCodeIsTooLong = "915165"
-        ShipsFromPostalCodeIsInvalid = "915166"
-        ShipsFromPostalCodeInvalidCharacters = "915167"
+        LineItemsExpected = "915158"
         MerchantAccountDoesNotMatch3DSecureMerchantAccount = "91584"
-        MerchantAccountDoesNotMatchIdealPaymentMerchantAccount = "915143"
         MerchantAccountDoesNotSupportMOTO = "91558"
         MerchantAccountDoesNotSupportRefunds = "91547"
         MerchantAccountIdDoesNotMatchSubscription = "915180"
         MerchantAccountIdIsInvalid = "91513"
-        MerchantAccountIsSusped = "91514" # Deprecated
         MerchantAccountIsSuspended = "91514"
-        MerchantAccountNameIsInvalid = "91513" # Deprecated
-        OrderIdDoesNotMatchIdealPaymentOrderId = "91503"
-        OrderIdIsRequiredWithIdealPayment = "91502"
+        NoNetAmountToPerformAuthAdjustment = "95606"
         OrderIdIsTooLong = "91501"
         PayPalAuthExpired = "91579"
         PayPalNotEnabled = "91576"
         PayPalVaultRecordMissingData = "91583"
         PaymentInstrumentNotSupportedByMerchantAccount = "91577"
         PaymentInstrumentTypeIsNotAccepted = "915101"
+        PaymentInstrumentWithExternalVaultIsInvalid = "915176"
         PaymentMethodConflict = "91515"
         PaymentMethodConflictWithVenmoSDK = "91549"
         PaymentMethodDoesNotBelongToCustomer = "91516"
@@ -553,15 +571,23 @@ class ErrorCodes(object):
         ProcessorAuthorizationCodeCannotBeSet = "91519"
         ProcessorAuthorizationCodeIsInvalid = "81520"
         ProcessorDoesNotSupportAuths = "915104"
+        ProcessorDoesNotSupportAuthAdjustment = "915222"
         ProcessorDoesNotSupportCredits = "91546"
+        ProcessorDoesNotSupportIncrementalAuth = "915220"
+        ProcessorDoesNotSupportMotoForCardType = "915195"
+        ProcessorDoesNotSupportPartialAuthReversal = "915221"
         ProcessorDoesNotSupportPartialSettlement = "915102"
-        ProcessorDoesNotSupportUpdatingOrderId = "915107"
         ProcessorDoesNotSupportUpdatingDescriptor = "915108"
+        ProcessorDoesNotSupportUpdatingOrderId = "915107"
         ProcessorDoesNotSupportUpdatingTransactionDetails = "915130"
         ProcessorDoesNotSupportVoiceAuthorizations = "91545"
+        ProductSkuIsInvalid = "915202"
         PurchaseOrderNumberIsInvalid = "91548"
         PurchaseOrderNumberIsTooLong = "91537"
         RefundAmountIsTooLarge = "91521"
+        RefundAuthHardDeclined = "915200"
+        RefundAuthSoftDeclined = "915201"
+        ScaExemptionInvalid = "915213"
         ServiceFeeAmountCannotBeNegative = "91554"
         ServiceFeeAmountFormatIsInvalid = "91555"
         ServiceFeeAmountIsTooLarge = "91556"
@@ -571,36 +597,51 @@ class ErrorCodes(object):
         SettlementAmountIsLessThanServiceFeeAmount = "91551"
         SettlementAmountIsTooLarge = "91522"
         ShippingAddressDoesntMatchCustomer = "91581"
+        ShippingAmountCannotBeNegative = "915163"
+        ShippingAmountFormatIsInvalid = "915162"
+        ShippingAmountIsTooLarge = "915164"
+        ShippingMethodIsInvalid = "915203"
+        ShippingPhoneNumberIsInvalid = "915204"
+        ShipsFromPostalCodeInvalidCharacters = "915167"
+        ShipsFromPostalCodeIsInvalid = "915166"
+        ShipsFromPostalCodeIsTooLong = "915165"
         SubMerchantAccountRequiresServiceFeeAmount = "91553"
         SubscriptionDoesNotBelongToCustomer = "91529"
         SubscriptionIdIsInvalid = "91528"
         SubscriptionStatusMustBePastDue = "91531"
         TaxAmountCannotBeNegative = "81534"
         TaxAmountFormatIsInvalid = "81535"
+        TaxAmountIsRequiredForAibSwedish = "815224"
         TaxAmountIsTooLarge = "81536"
         ThreeDSecureAuthenticationFailed = "81571"
-        ThreeDSecureTokenIsInvalid = "91568"
-        ThreeDSecureTransactionDataDoesntMatchVerify = "91570"
-        ThreeDSecureEciFlagIsRequired = "915113"
-        ThreeDSecureCavvIsRequired = "915116"
-        ThreeDSecureXidIsRequired = "915115"
-        ThreeDSecureEciFlagIsInvalid = "915114"
+        ThreeDSecureAuthenticationIdDoesntMatchNonceThreeDSecureAuthentication = "915198"
+        ThreeDSecureAuthenticationIdIsInvalid = "915196"
+        ThreeDSecureAuthenticationIdWithThreeDSecurePassThruIsInvalid = "915199"
         ThreeDSecureAuthenticationResponseIsInvalid = "915120"
-        ThreeDSecureDirectoryResponseIsInvalid = "915121"
         ThreeDSecureCavvAlgorithmIsInvalid = "915122"
+        ThreeDSecureCavvIsRequired = "915116"
+        ThreeDSecureDirectoryResponseIsInvalid = "915121"
+        ThreeDSecureEciFlagIsInvalid = "915114"
+        ThreeDSecureEciFlagIsRequired = "915113"
         ThreeDSecureMerchantAccountDoesNotSupportCardType = "915131"
+        ThreeDSecureTokenIsInvalid = "91568"
+        ThreeDSecureTransactionDataDoesntMatchVerify = "91570"
+        ThreeDSecureTransactionPaymentMethodDoesntMatchThreeDSecureAuthenticationPaymentMethod = "915197"
+        ThreeDSecureXidIsRequired = "915115"
         TooManyLineItems = "915157"
-        LineItemsExpected = "915158"
+        TransactionIsNotEligibleForAdjustment = "915219"
+        TransactionMustBeInStateAuthorized = "915218"
+        TransactionSourceIsInvalid = "915133"
         TypeIsInvalid = "91523"
         TypeIsRequired = "91524"
         UnsupportedVoiceAuthorization = "91539"
         UsBankAccountNonceMustBePlaidVerified = "915171"
         UsBankAccountNotVerified = "915172"
-        TransactionSourceIsInvalid = "915133"
 
         class ExternalVault(object):
             StatusIsInvalid = "915175"
             StatusWithPreviousNetworkTransactionIdIsInvalid = "915177"
+            # NEXT_MAJOR_VERSION remove this validation error as it is no longer returned by the gateway
             CardTypeIsInvalid = "915178"
             PreviousNetworkTransactionIdIsInvalid = "915179"
 
@@ -701,7 +742,6 @@ class ErrorCodes(object):
             DescriptionIsTooLong = "95803"
             DiscountAmountFormatIsInvalid = "95804"
             DiscountAmountIsTooLarge = "95805"
-            DiscountAmountMustBeGreaterThanZero = "95806" # Deprecated as the amount may be zero.
             DiscountAmountCannotBeNegative = "95806"
             KindIsInvalid = "95807"
             KindIsRequired = "95808"
@@ -722,7 +762,6 @@ class ErrorCodes(object):
             UnitOfMeasureIsTooLarge = "95821"
             UnitTaxAmountFormatIsInvalid = "95824"
             UnitTaxAmountIsTooLarge = "95825"
-            UnitTaxAmountMustBeGreaterThanZero = "95826" # Deprecated as the amount may be zero.
             UnitTaxAmountCannotBeNegative = "95826"
             TaxAmountFormatIsInvalid = "95827"
             TaxAmountIsTooLarge = "95828"
@@ -735,3 +774,12 @@ class ErrorCodes(object):
         TooManyConfirmationAttempts = "96104"
         UnableToConfirmDepositAmounts = "96105"
         InvalidDepositAmounts = "96106"
+
+    class RiskData(object):
+        # NEXT_MAJOR_VERSION Remove CustomerBrowserIsTooLong code as it is no longer used
+        CustomerBrowserIsTooLong = "94701"
+        CustomerDeviceIdIsTooLong = "94702"
+        CustomerLocationZipInvalidCharacters = "94703"
+        CustomerLocationZipIsInvalid = "94704"
+        CustomerLocationZipIsTooLong = "94705"
+        CustomerTenureIsTooLong = "94706"
diff --git a/braintree/error_result.py b/braintree/error_result.py
index 43f8cd0..c98cbfb 100644
--- a/braintree/error_result.py
+++ b/braintree/error_result.py
@@ -40,6 +40,11 @@ class ErrorResult(object):
         else:
             self.subscription = None
 
+        if "plan" in attributes:
+            self.plan = braintree.plan.Plan(gateway, attributes["plan"])
+        else:
+            self.plan = None
+
         if "merchant_account" in attributes:
             self.merchant_account = braintree.merchant_account.MerchantAccount(gateway, attributes["merchant_account"])
         else:
diff --git a/braintree/europe_bank_account.py b/braintree/europe_bank_account.py
index 7540990..331755d 100644
--- a/braintree/europe_bank_account.py
+++ b/braintree/europe_bank_account.py
@@ -2,6 +2,7 @@ import braintree
 from braintree.resource import Resource
 from braintree.configuration import Configuration
 
+#NEXT_MAJOR_VERSION this was specific to iDEAL integrations and can be removed
 class EuropeBankAccount(Resource):
     class MandateType(object):
         """
diff --git a/braintree/exceptions/__init__.py b/braintree/exceptions/__init__.py
index 1daf76d..eb9de84 100644
--- a/braintree/exceptions/__init__.py
+++ b/braintree/exceptions/__init__.py
@@ -1,13 +1,14 @@
 from braintree.exceptions.authentication_error import AuthenticationError
 from braintree.exceptions.authorization_error import AuthorizationError
-from braintree.exceptions.down_for_maintenance_error import DownForMaintenanceError
-from braintree.exceptions.forged_query_string_error import ForgedQueryStringError
+from braintree.exceptions.configuration_error import ConfigurationError
+from braintree.exceptions.gateway_timeout_error import GatewayTimeoutError
 from braintree.exceptions.invalid_challenge_error import InvalidChallengeError
 from braintree.exceptions.invalid_signature_error import InvalidSignatureError
 from braintree.exceptions.not_found_error import NotFoundError
+from braintree.exceptions.request_timeout_error import RequestTimeoutError
 from braintree.exceptions.server_error import ServerError
+from braintree.exceptions.service_unavailable_error import ServiceUnavailableError
+from braintree.exceptions.test_operation_performed_in_production_error import TestOperationPerformedInProductionError
 from braintree.exceptions.too_many_requests_error import TooManyRequestsError
 from braintree.exceptions.unexpected_error import UnexpectedError
 from braintree.exceptions.upgrade_required_error import UpgradeRequiredError
-from braintree.exceptions.test_operation_performed_in_production_error import TestOperationPerformedInProductionError
-from braintree.exceptions.configuration_error import ConfigurationError
diff --git a/braintree/exceptions/authentication_error.py b/braintree/exceptions/authentication_error.py
index 65dbd6b..b034ad0 100644
--- a/braintree/exceptions/authentication_error.py
+++ b/braintree/exceptions/authentication_error.py
@@ -4,6 +4,6 @@ class AuthenticationError(BraintreeError):
     """
     Raised when the client library cannot authenticate with the gateway.  This generally means the public_key/private key are incorrect, or the user is not active.
 
-    See https://developers.braintreepayments.com/reference/general/exceptions/python#authentication-error
+    See https://developer.paypal.com/braintree/docs/reference/general/exceptions/python#authentication-error
     """
     pass
diff --git a/braintree/exceptions/authorization_error.py b/braintree/exceptions/authorization_error.py
index 12fdeca..fcfb014 100644
--- a/braintree/exceptions/authorization_error.py
+++ b/braintree/exceptions/authorization_error.py
@@ -4,6 +4,6 @@ class AuthorizationError(BraintreeError):
     """
     Raised when the user does not have permission to complete the requested operation.
 
-    See https://developers.braintreepayments.com/reference/general/exceptions/python#authorization-error
+    See https://developer.paypal.com/braintree/docs/reference/general/exceptions/python#authorization-error
     """
     pass
diff --git a/braintree/exceptions/down_for_maintenance_error.py b/braintree/exceptions/down_for_maintenance_error.py
deleted file mode 100644
index d0aa166..0000000
--- a/braintree/exceptions/down_for_maintenance_error.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from braintree.exceptions.braintree_error import BraintreeError
-
-class DownForMaintenanceError(BraintreeError):
-    """
-    Raised when the gateway is down for maintenance.
-
-    See https://developers.braintreepayments.com/reference/general/exceptions/python#down-for-maintenance
-    """
-    pass
diff --git a/braintree/exceptions/forged_query_string_error.py b/braintree/exceptions/forged_query_string_error.py
deleted file mode 100644
index 5dbd7cd..0000000
--- a/braintree/exceptions/forged_query_string_error.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from braintree.exceptions.braintree_error import BraintreeError
-
-class ForgedQueryStringError(BraintreeError):
-    """
-    Raised when the query string has been forged or tampered with during a transparent redirect.
-
-    See https://developers.braintreepayments.com/reference/general/exceptions/python#forged-query-string
-    """
-    pass
diff --git a/braintree/exceptions/gateway_timeout_error.py b/braintree/exceptions/gateway_timeout_error.py
new file mode 100644
index 0000000..31d1a82
--- /dev/null
+++ b/braintree/exceptions/gateway_timeout_error.py
@@ -0,0 +1,7 @@
+from braintree.exceptions.braintree_error import BraintreeError
+
+class GatewayTimeoutError(BraintreeError):
+    """
+    Raised when a gateway response timeout occurs.
+    """
+    pass
diff --git a/braintree/exceptions/not_found_error.py b/braintree/exceptions/not_found_error.py
index e738e2b..a79eec4 100644
--- a/braintree/exceptions/not_found_error.py
+++ b/braintree/exceptions/not_found_error.py
@@ -4,6 +4,6 @@ class NotFoundError(BraintreeError):
     """
     Raised when an object is not found in the gateway, such as a Transaction.find("bad_id").
 
-    See https://developers.braintreepayments.com/reference/general/exceptions/python#not-found-error
+    See https://developer.paypal.com/braintree/docs/reference/general/exceptions/python#not-found-error
     """
     pass
diff --git a/braintree/exceptions/request_timeout_error.py b/braintree/exceptions/request_timeout_error.py
new file mode 100644
index 0000000..51fabab
--- /dev/null
+++ b/braintree/exceptions/request_timeout_error.py
@@ -0,0 +1,7 @@
+from braintree.exceptions.braintree_error import BraintreeError
+
+class RequestTimeoutError(BraintreeError):
+    """
+    Raised when a client request timeout occurs.
+    """
+    pass
diff --git a/braintree/exceptions/server_error.py b/braintree/exceptions/server_error.py
index ebbee70..751dc6a 100644
--- a/braintree/exceptions/server_error.py
+++ b/braintree/exceptions/server_error.py
@@ -2,8 +2,8 @@ from braintree.exceptions.braintree_error import BraintreeError
 
 class ServerError(BraintreeError):
     """
-    Raised when the gateway raises an error.  Please contant support at support@getbraintree.com.
+    Raised when the gateway raises an error.  Please contact support at support@getbraintree.com.
 
-    See https://developers.braintreepayments.com/reference/general/exceptions/python#server-error
+    See https://developer.paypal.com/braintree/docs/reference/general/exceptions/python#server-error
     """
     pass
diff --git a/braintree/exceptions/service_unavailable_error.py b/braintree/exceptions/service_unavailable_error.py
new file mode 100644
index 0000000..8043fd8
--- /dev/null
+++ b/braintree/exceptions/service_unavailable_error.py
@@ -0,0 +1,7 @@
+from braintree.exceptions.braintree_error import BraintreeError
+
+class ServiceUnavailableError(BraintreeError):
+    """
+    Raised when the gateway is unavailable.
+    """
+    pass
diff --git a/braintree/exceptions/upgrade_required_error.py b/braintree/exceptions/upgrade_required_error.py
index 8f40c5f..42f9f9f 100644
--- a/braintree/exceptions/upgrade_required_error.py
+++ b/braintree/exceptions/upgrade_required_error.py
@@ -4,6 +4,6 @@ class UpgradeRequiredError(BraintreeError):
     """
     Raised for unsupported client library versions.
 
-    See https://developers.braintreepayments.com/reference/general/exceptions/python#upgrade-required-error
+    See https://developer.paypal.com/braintree/docs/reference/general/exceptions/python#upgrade-required-error
     """
     pass
diff --git a/braintree/exchange_rate_quote.py b/braintree/exchange_rate_quote.py
new file mode 100644
index 0000000..ba1565f
--- /dev/null
+++ b/braintree/exchange_rate_quote.py
@@ -0,0 +1,6 @@
+from braintree.attribute_getter import AttributeGetter
+from braintree.montary_amount import MontaryAmount
+
+class ExchangeRateQuote(AttributeGetter):
+    def __init__(self,attributes):
+        AttributeGetter.__init__(self,attributes)
\ No newline at end of file
diff --git a/braintree/exchange_rate_quote_gateway.py b/braintree/exchange_rate_quote_gateway.py
new file mode 100644
index 0000000..6ad6cec
--- /dev/null
+++ b/braintree/exchange_rate_quote_gateway.py
@@ -0,0 +1,38 @@
+from braintree.exchange_rate_quote_payload import ExchangeRateQuotePayload
+from braintree.error_result import ErrorResult
+from braintree.successful_result import SuccessfulResult
+
+class ExchangeRateQuoteGateway(object):
+    def __init__(self, gateway, graphql_client = None):
+        self.gateway = gateway
+        self.config = gateway.config
+        self.graphql_client = None if graphql_client is None else graphql_client
+      
+    def generate(self, request):
+        definition = """
+          mutation ($exchangeRateQuoteRequest: GenerateExchangeRateQuoteInput!) {
+            generateExchangeRateQuote(input: $exchangeRateQuoteRequest) {
+              quotes {
+                id
+                baseAmount {value, currencyCode}
+                quoteAmount {value, currencyCode}
+                exchangeRate
+                tradeRate
+                expiresAt
+                refreshesAt
+              }
+            }
+          }"""
+
+        param = request.to_graphql_variables()
+        graphql_client = self.graphql_client if self.graphql_client is not None else self.gateway.graphql_client
+        response = graphql_client.query(definition, param)
+
+        if "data" in response and "generateExchangeRateQuote" in response["data"]:
+            result = response["data"]["generateExchangeRateQuote"]
+            self.exchange_rate_quote_payload = ExchangeRateQuotePayload(result)
+            return SuccessfulResult({"exchange_rate_quote_payload": self.exchange_rate_quote_payload})
+        elif "errors" in response:
+            error_codes = response["errors"][0]
+            error_codes["errors"] = dict()
+            return ErrorResult(self.gateway, error_codes)
\ No newline at end of file
diff --git a/braintree/exchange_rate_quote_input.py b/braintree/exchange_rate_quote_input.py
new file mode 100644
index 0000000..197d9c8
--- /dev/null
+++ b/braintree/exchange_rate_quote_input.py
@@ -0,0 +1,17 @@
+from braintree.attribute_getter import AttributeGetter
+
+class ExchangeRateQuoteInput(AttributeGetter):
+    def __init__(self,parent,attributes):
+        self.parent = parent
+        AttributeGetter.__init__(self,attributes)
+
+    def done(self):
+        return self.parent
+
+    def to_graphql_variables(self):
+        variables = dict()
+        variables["baseCurrency"] = self.base_currency if getattr(self,"base_currency",None) is not None else None
+        variables["quoteCurrency"] = self.quote_currency if getattr(self,"quote_currency",None) is not None else None
+        variables["baseAmount"] = self.base_amount if getattr(self,"base_amount",None) is not None else None
+        variables["markup"] = self.markup if getattr(self,"markup",None) is not None else None
+        return variables
\ No newline at end of file
diff --git a/braintree/exchange_rate_quote_payload.py b/braintree/exchange_rate_quote_payload.py
new file mode 100644
index 0000000..2107859
--- /dev/null
+++ b/braintree/exchange_rate_quote_payload.py
@@ -0,0 +1,29 @@
+from braintree.exchange_rate_quote import ExchangeRateQuote
+from braintree.montary_amount import MontaryAmount
+
+class ExchangeRateQuotePayload(object):
+    def __init__(self, data):
+        quote_objs = data.get("quotes")
+        if(quote_objs is not None):
+            self.quotes = list()
+            for quote_obj in quote_objs:
+                base_amount_obj = quote_obj.get("baseAmount")
+                quote_amount_obj = quote_obj.get("quoteAmount")
+                base_attrs = {"value":base_amount_obj.get("value"),
+                              "currency_code":base_amount_obj.get("currencyCode")}
+                base_amount = MontaryAmount(base_attrs)
+                quote_attrs = {"value":quote_amount_obj.get("value"),
+                               "currency_code":quote_amount_obj.get("currencyCode")}
+                quote_amount = MontaryAmount(quote_attrs)
+                attributes = {"id":quote_obj.get("id"),
+                              "exchange_rate":quote_obj.get("exchangeRate"),
+                              "trade_rate":quote_obj.get("tradeRate"),
+                              "expires_at":quote_obj.get("expiresAt"),
+                              "refreshes_at":quote_obj.get("refreshesAt"),
+                              "base_amount":base_amount,
+                              "quote_amount":quote_amount}
+                quote = ExchangeRateQuote(attributes)
+                self.quotes.append(quote)
+
+    def get_quotes(self):
+        return self.quotes
\ No newline at end of file
diff --git a/braintree/exchange_rate_quote_request.py b/braintree/exchange_rate_quote_request.py
new file mode 100644
index 0000000..d0e1061
--- /dev/null
+++ b/braintree/exchange_rate_quote_request.py
@@ -0,0 +1,21 @@
+from braintree.exchange_rate_quote_input import ExchangeRateQuoteInput
+
+class ExchangeRateQuoteRequest(object):
+    def __init__(self):
+        self.quotes = list()
+
+    def add_exchange_rate_quote_input(self,attributes):
+        new_input = ExchangeRateQuoteInput(self,attributes)
+        self.quotes.append(new_input)
+        return new_input
+
+    def to_graphql_variables(self):
+        variables = dict()
+        input = dict()
+        
+        quote_list = list()
+        for quote in self.quotes:
+            quote_list.append(quote.to_graphql_variables())
+        input["quotes"] = quote_list
+        variables["exchangeRateQuoteRequest"] = input
+        return variables
\ No newline at end of file
diff --git a/braintree/ideal_payment.py b/braintree/ideal_payment.py
deleted file mode 100644
index 627dcf7..0000000
--- a/braintree/ideal_payment.py
+++ /dev/null
@@ -1,28 +0,0 @@
-import braintree
-from braintree.resource import Resource
-from braintree.configuration import Configuration
-from braintree.iban_bank_account import IbanBankAccount
-
-# NEXT_MAJOR_VERSION Remove this class as legacy Ideal has been removed/disabled in the Braintree Gateway
-# DEPRECATED If you're looking to accept iDEAL as a payment method contact accounts@braintreepayments.com for a solution.
-class IdealPayment(Resource):
-
-    @staticmethod
-    def find(ideal_payment_id):
-        return Configuration.gateway().ideal_payment.find(ideal_payment_id)
-
-    @staticmethod
-    def sale(ideal_payment_id, transactionRequest):
-        request = transactionRequest.copy()
-        request["payment_method_nonce"] = ideal_payment_id
-        if not "options" in request:
-            request["options"] = {}
-        request["options"]["submit_for_settlement"] = True
-        return Configuration.gateway().transaction.sale(request)
-
-    def __init__(self, gateway, attributes):
-        Resource.__init__(self, gateway, attributes)
-        if attributes.get('iban_bank_account') is not None:
-            self.iban_bank_account = IbanBankAccount(gateway, self.iban_bank_account)
-        else:
-            self.iban_bank_account = None
diff --git a/braintree/ideal_payment_gateway.py b/braintree/ideal_payment_gateway.py
deleted file mode 100644
index 46df0cb..0000000
--- a/braintree/ideal_payment_gateway.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import braintree
-from braintree.ideal_payment import IdealPayment
-from braintree.exceptions.not_found_error import NotFoundError
-
-# NEXT_MAJOR_VERSION Remove this class as legacy Ideal has been removed/disabled in the Braintree Gateway
-# DEPRECATED If you're looking to accept iDEAL as a payment method contact accounts@braintreepayments.com for a solution.
-class IdealPaymentGateway(object):
-    def __init__(self, gateway):
-        self.gateway = gateway
-        self.config = gateway.config
-
-    def find(self, ideal_payment_id):
-        try:
-            if ideal_payment_id is None or ideal_payment_id.strip() == "":
-                raise NotFoundError()
-
-            response = self.config.http().get(self.config.base_merchant_path() + "/ideal_payments/" + ideal_payment_id)
-            if "ideal_payment" in response:
-                return IdealPayment(self.gateway, response["ideal_payment"])
-        except NotFoundError:
-            raise NotFoundError("iDEAL payment with token" + repr(ideal_payment_id) + " not found")
diff --git a/braintree/liability_shift.py b/braintree/liability_shift.py
new file mode 100644
index 0000000..8e5526c
--- /dev/null
+++ b/braintree/liability_shift.py
@@ -0,0 +1,4 @@
+from braintree.attribute_getter import AttributeGetter
+
+class LiabilityShift(AttributeGetter):
+    pass
diff --git a/braintree/local_payment_expired.py b/braintree/local_payment_expired.py
new file mode 100644
index 0000000..9638559
--- /dev/null
+++ b/braintree/local_payment_expired.py
@@ -0,0 +1,9 @@
+from braintree.resource import Resource
+
+class LocalPaymentExpired(Resource):
+    """
+    A class representing Braintree LocalPaymentExpired webhook.
+    """
+    def __init__(self, gateway, attributes):
+        Resource.__init__(self, gateway, attributes)
+
diff --git a/braintree/local_payment_funded.py b/braintree/local_payment_funded.py
new file mode 100644
index 0000000..b59d6a4
--- /dev/null
+++ b/braintree/local_payment_funded.py
@@ -0,0 +1,11 @@
+from braintree.resource import Resource
+from braintree.transaction import Transaction
+
+class LocalPaymentFunded(Resource):
+    """
+    A class representing Braintree LocalPaymentFunded webhook.
+    """
+    def __init__(self, gateway, attributes):
+        Resource.__init__(self, gateway, attributes)
+
+        self.transaction = Transaction(gateway, attributes.pop("transaction"))
diff --git a/braintree/local_payment_reversed.py b/braintree/local_payment_reversed.py
new file mode 100644
index 0000000..636aa38
--- /dev/null
+++ b/braintree/local_payment_reversed.py
@@ -0,0 +1,9 @@
+from braintree.resource import Resource
+
+class LocalPaymentReversed(Resource):
+    """
+    A class representing Braintree LocalPaymentReversed webhook.
+    """
+    def __init__(self, gateway, attributes):
+        Resource.__init__(self, gateway, attributes)
+
diff --git a/braintree/masterpass_card.py b/braintree/masterpass_card.py
index 5350cf6..34198c2 100644
--- a/braintree/masterpass_card.py
+++ b/braintree/masterpass_card.py
@@ -1,12 +1,14 @@
 import braintree
 from braintree.address import Address
 from braintree.resource import Resource
+from warnings import warn
 
 class MasterpassCard(Resource):
     """
-    A class representing Masterpass card
+    A class representing Masterpass card. Deprecated
     """
     def __init__(self, gateway, attributes):
+        warn("MasterpassCard is deprecated")
         Resource.__init__(self, gateway, attributes)
 
         if "billing_address" in attributes:
diff --git a/braintree/merchant_account/merchant_account.py b/braintree/merchant_account/merchant_account.py
index df8178f..13f81cf 100644
--- a/braintree/merchant_account/merchant_account.py
+++ b/braintree/merchant_account/merchant_account.py
@@ -2,6 +2,7 @@ from braintree.configuration import Configuration
 from braintree.resource import Resource
 from braintree.merchant_account import BusinessDetails, FundingDetails, IndividualDetails
 
+
 class MerchantAccount(Resource):
     class Status(object):
         Active = "active"
@@ -37,7 +38,9 @@ class MerchantAccount(Resource):
         return super(MerchantAccount, self).__repr__(detail_list)
 
     @staticmethod
-    def create(params={}):
+    def create(params=None):
+        if params is None:
+            params = {}
         return Configuration.gateway().merchant_account.create(params)
 
     @staticmethod
diff --git a/braintree/merchant_account_gateway.py b/braintree/merchant_account_gateway.py
index f0454a8..6e52db9 100644
--- a/braintree/merchant_account_gateway.py
+++ b/braintree/merchant_account_gateway.py
@@ -7,16 +7,21 @@ from braintree.resource_collection import ResourceCollection
 from braintree.successful_result import SuccessfulResult
 from braintree.exceptions.not_found_error import NotFoundError
 
+
 class MerchantAccountGateway(object):
     def __init__(self, gateway):
         self.gateway = gateway
         self.config = gateway.config
 
-    def create(self, params={}):
-        Resource.verify_keys(params, MerchantAccountGateway._detect_signature(params))
+    def create(self, params=None):
+        if params is None:
+            params = {}
+        Resource.verify_keys(params, MerchantAccountGateway._create_signature())
         return self._post("/merchant_accounts/create_via_api", {"merchant_account": params})
 
-    def update(self, merchant_account_id, params={}):
+    def update(self, merchant_account_id, params=None):
+        if params is None:
+            params = {}
         Resource.verify_keys(params, MerchantAccountGateway._update_signature())
         return self._put("/merchant_accounts/%s/update_via_api" % merchant_account_id, {"merchant_account": params})
 
@@ -29,7 +34,9 @@ class MerchantAccountGateway(object):
         except NotFoundError:
             raise NotFoundError("merchant account with id " + repr(merchant_account_id) + " not found")
 
-    def create_for_currency(self, params={}):
+    def create_for_currency(self, params=None):
+        if params is None:
+            params = {}
         return self._post("/merchant_accounts/create_for_currency", {"merchant_account": params})
 
     def all(self):
@@ -42,7 +49,9 @@ class MerchantAccountGateway(object):
         merchant_accounts = [MerchantAccount(self.gateway, merchant_account) for merchant_account in ResourceCollection._extract_as_array(body, "merchant_account")]
         return PaginatedResult(body["total_items"], body["page_size"], merchant_accounts)
 
-    def _post(self, url, params={}):
+    def _post(self, url, params=None):
+        if params is None:
+            params = {}
         response = self.config.http().post(self.config.base_merchant_path() + url, params)
 
         if "response" in response:
@@ -53,47 +62,15 @@ class MerchantAccountGateway(object):
         elif "api_error_response" in response:
             return ErrorResult(self.gateway, response["api_error_response"])
 
-    def _put(self, url, params={}):
+    def _put(self, url, params=None):
+        if params is None:
+            params = {}
         response = self.config.http().put(self.config.base_merchant_path() + url, params)
         if "merchant_account" in response:
             return SuccessfulResult({"merchant_account": MerchantAccount(self.gateway, response["merchant_account"])})
         elif "api_error_response" in response:
             return ErrorResult(self.gateway, response["api_error_response"])
 
-    @staticmethod
-    def _detect_signature(attributes):
-        if 'applicant_details' in attributes:
-            # Warn deprecated
-            return MerchantAccountGateway._create_deprecated_signature()
-        else:
-            return MerchantAccountGateway._create_signature()
-
-    @staticmethod
-    def _create_deprecated_signature():
-        return [
-            {'applicant_details': [
-                'company_name',
-                'first_name',
-                'last_name',
-                'email',
-                'phone',
-                'date_of_birth',
-                'ssn',
-                'tax_id',
-                'routing_number',
-                'account_number',
-                {'address': [
-                    'street_address',
-                    'postal_code',
-                    'locality',
-                    'region']}
-                ]
-            },
-            'tos_accepted',
-            'master_merchant_account_id',
-            'id'
-        ]
-
     @staticmethod
     def _create_signature():
         return [
diff --git a/braintree/merchant_gateway.py b/braintree/merchant_gateway.py
index 581ad58..66d0b15 100644
--- a/braintree/merchant_gateway.py
+++ b/braintree/merchant_gateway.py
@@ -6,6 +6,7 @@ from braintree.exceptions.not_found_error import NotFoundError
 from braintree.merchant import Merchant
 from braintree.oauth_credentials import OAuthCredentials
 
+
 class MerchantGateway(object):
     def __init__(self, gateway):
         self.gateway = gateway
@@ -14,7 +15,9 @@ class MerchantGateway(object):
     def create(self, params):
         return self.__create_merchant(params)
 
-    def __create_merchant(self, params={}):
+    def __create_merchant(self, params=None):
+        if params is None:
+            params = {}
         response = self.config.http().post("/merchants/create_via_api", {
             "merchant": params
         })
diff --git a/braintree/montary_amount.py b/braintree/montary_amount.py
new file mode 100644
index 0000000..aecbcff
--- /dev/null
+++ b/braintree/montary_amount.py
@@ -0,0 +1,8 @@
+from decimal import Decimal
+from braintree.attribute_getter import AttributeGetter
+
+class MontaryAmount(AttributeGetter):
+    def __init__(self,attributes):
+        AttributeGetter.__init__(self,attributes)
+        if getattr(self, "value", None) is not None:
+            self.value = Decimal(self.value)
\ No newline at end of file
diff --git a/braintree/oauth_gateway.py b/braintree/oauth_gateway.py
index b926825..5b6b25c 100644
--- a/braintree/oauth_gateway.py
+++ b/braintree/oauth_gateway.py
@@ -5,11 +5,8 @@ from braintree.exceptions.not_found_error import NotFoundError
 from braintree.oauth_credentials import OAuthCredentials
 
 import sys
-if sys.version_info[0] == 2:
-    from urllib import quote_plus
-else:
-    from urllib.parse import quote_plus
-    from functools import reduce
+from urllib.parse import quote_plus
+from functools import reduce
 
 class OAuthGateway(object):
     def __init__(self, gateway):
diff --git a/braintree/payment_instrument_type.py b/braintree/payment_instrument_type.py
index c9ce632..3ca33e3 100644
--- a/braintree/payment_instrument_type.py
+++ b/braintree/payment_instrument_type.py
@@ -1,16 +1,18 @@
 
 class PaymentInstrumentType():
+    # NEXT_MAJOR_VERSION remove amex express checkout
+    # NEXT_MAJOR_VERSION remove masterpass
+    AmexExpressCheckoutCard = "amex_express_checkout_card"
+    AndroidPayCard = "android_pay_card"
+    ApplePayCard = "apple_pay_card"
+    CreditCard = "credit_card"
+    EuropeBankAccount = "europe_bank_account"
+    LocalPayment = "local_payment"
+    MasterpassCard = "masterpass_card"
     PayPalAccount = "paypal_account"
     PayPalHere = "paypal_here"
-    EuropeBankAccount = "europe_bank_account"
-    CreditCard = "credit_card"
-    CoinbaseAccount = "coinbase_account"
-    ApplePayCard = "apple_pay_card"
-    AndroidPayCard = "android_pay_card"
-    AmexExpressCheckoutCard = "amex_express_checkout_card"
-    VenmoAccount = "venmo_account"
+    SamsungPayCard = "samsung_pay_card"
+    SepaDirectDebitAccount = "sepa_debit_account"
     UsBankAccount = "us_bank_account"
+    VenmoAccount = "venmo_account"
     VisaCheckoutCard = "visa_checkout_card"
-    MasterpassCard = "masterpass_card"
-    SamsungPayCard = "samsung_pay_card"
-    LocalPayment = "local_payment"
diff --git a/braintree/payment_method.py b/braintree/payment_method.py
index 02920ff..d1cb02a 100644
--- a/braintree/payment_method.py
+++ b/braintree/payment_method.py
@@ -3,9 +3,12 @@ from braintree.address import Address
 from braintree.resource import Resource
 from braintree.configuration import Configuration
 
+
 class PaymentMethod(Resource):
     @staticmethod
-    def create(params={}):
+    def create(params=None):
+        if params is None:
+            params = {}
         return Configuration.gateway().payment_method.create(params)
 
     @staticmethod
@@ -17,7 +20,9 @@ class PaymentMethod(Resource):
         return Configuration.gateway().payment_method.update(payment_method_token, params)
 
     @staticmethod
-    def delete(payment_method_token, options={}):
+    def delete(payment_method_token, options=None):
+        if options is None:
+            options = {}
         return Configuration.gateway().payment_method.delete(payment_method_token, options)
 
     @staticmethod
@@ -29,11 +34,12 @@ class PaymentMethod(Resource):
         options = [
             "fail_on_duplicate_payment_method",
             "make_default",
+            "skip_advanced_fraud_checking",
             "us_bank_account_verification_method",
+            "verification_account_type",
+            "verification_amount",
             "verification_merchant_account_id",
             "verify_card",
-            "verification_amount",
-            "verification_account_type",
             {
                 "adyen": [
                     "overwrite_brand",
@@ -52,55 +58,74 @@ class PaymentMethod(Resource):
             },
         ]
 
+        three_d_secure_pass_thru = [
+            "cavv",
+            "ds_transaction_id",
+            "eci_flag",
+            "three_d_secure_version",
+            "xid"
+        ]
+
         signature = [
             "billing_address_id",
             "cardholder_name",
             "customer_id",
             "cvv",
             "device_data",
-            "device_session_id",
             "expiration_date",
             "expiration_month",
             "expiration_year",
             "number",
             "payment_method_nonce",
             "paypal_refresh_token",
-            "paypal_vault_without_upgrade",
             "token",
+            "device_session_id", # NEXT_MAJOR_VERSION remove device_session_id
             {
                 "billing_address": Address.create_signature()
             },
             {
                 "options": options
+            },
+            {
+                "three_d_secure_pass_thru": three_d_secure_pass_thru
             }
+
         ]
         return signature
 
     @staticmethod
     def update_signature():
+        three_d_secure_pass_thru = [
+            "cavv",
+            "ds_transaction_id",
+            "eci_flag",
+            "three_d_secure_version",
+            "xid"
+        ]
+
         signature = [
             "billing_address_id",
             "cardholder_name",
             "cvv",
-            "device_session_id",
+            "device_data",
             "expiration_date",
             "expiration_month",
             "expiration_year",
             "number",
+            "payment_method_nonce",
             "token",
             "venmo_sdk_payment_method_code",
-            "device_data",
-            "fraud_merchant_id",
-            "payment_method_nonce",
+            "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",
-                    "verify_card",
+                    "venmo_sdk_session",
+                    "verification_account_type",
                     "verification_amount",
                     "verification_merchant_account_id",
-                    "verification_account_type",
-                    "venmo_sdk_session",
+                    "verify_card",
                     {
                         "adyen": [
                             "overwrite_brand",
@@ -111,6 +136,9 @@ class PaymentMethod(Resource):
             },
             {
                 "billing_address": Address.update_signature() + [{"options": ["update_existing"]}]
+            },
+            {
+                "three_d_secure_pass_thru": three_d_secure_pass_thru
             }
         ]
         return signature
diff --git a/braintree/payment_method_customer_data_updated_metadata.py b/braintree/payment_method_customer_data_updated_metadata.py
new file mode 100644
index 0000000..e1e3a02
--- /dev/null
+++ b/braintree/payment_method_customer_data_updated_metadata.py
@@ -0,0 +1,14 @@
+from braintree.resource import Resource
+from braintree.payment_method_parser import parse_payment_method
+from braintree.enriched_customer_data import EnrichedCustomerData
+
+class PaymentMethodCustomerDataUpdatedMetadata(Resource):
+    """
+    A class representing Braintree PaymentMethodCustomerDataUpdatedMetadata webhook.
+    """
+    def __init__(self, gateway, attributes):
+        Resource.__init__(self, gateway, attributes)
+        self.payment_method = parse_payment_method(gateway, attributes["payment_method"])
+        if attributes["enriched_customer_data"]:
+            self.enriched_customer_data = EnrichedCustomerData(gateway, attributes["enriched_customer_data"])
+
diff --git a/braintree/payment_method_gateway.py b/braintree/payment_method_gateway.py
index 954cd57..5cd475c 100644
--- a/braintree/payment_method_gateway.py
+++ b/braintree/payment_method_gateway.py
@@ -4,12 +4,14 @@ from braintree.credit_card import CreditCard
 from braintree.payment_method import PaymentMethod
 from braintree.paypal_account import PayPalAccount
 from braintree.europe_bank_account import EuropeBankAccount
-from braintree.coinbase_account import CoinbaseAccount
 from braintree.android_pay_card import AndroidPayCard
+# NEXT_MAJOR_VERSION remove amex express checkout
 from braintree.amex_express_checkout_card import AmexExpressCheckoutCard
+from braintree.sepa_direct_debit_account import SepaDirectDebitAccount
 from braintree.venmo_account import VenmoAccount
 from braintree.us_bank_account import UsBankAccount
 from braintree.visa_checkout_card import VisaCheckoutCard
+# NEXT_MAJOR_VERSION remove masterpass
 from braintree.masterpass_card import MasterpassCard
 from braintree.samsung_pay_card import SamsungPayCard
 from braintree.unknown_payment_method import UnknownPaymentMethod
@@ -23,18 +25,19 @@ from braintree.resource_collection import ResourceCollection
 from braintree.successful_result import SuccessfulResult
 
 import sys
-if sys.version_info[0] == 2:
-    from urllib import urlencode
-else:
-    from urllib.parse import urlencode
+from urllib.parse import urlencode
+
 
 class PaymentMethodGateway(object):
     def __init__(self, gateway):
         self.gateway = gateway
         self.config = gateway.config
 
-    def create(self, params={}):
+    def create(self, params=None):
+        if params is None:
+            params = {}
         Resource.verify_keys(params, PaymentMethod.create_signature())
+        self.__check_for_deprecated_attributes(params);
         return self._post("/payment_methods", {"payment_method": params})
 
     def find(self, payment_method_token):
@@ -49,6 +52,7 @@ class PaymentMethodGateway(object):
 
     def update(self, payment_method_token, params):
         Resource.verify_keys(params, PaymentMethod.update_signature())
+        self.__check_for_deprecated_attributes(params);
         try:
             if payment_method_token is None or payment_method_token.strip() == "":
                 raise NotFoundError()
@@ -60,7 +64,9 @@ class PaymentMethodGateway(object):
         except NotFoundError:
             raise NotFoundError("payment method with token " + repr(payment_method_token) + " not found")
 
-    def delete(self, payment_method_token, options={}):
+    def delete(self, payment_method_token, options=None):
+        if options is None:
+            options = {}
         Resource.verify_keys(options, PaymentMethod.delete_signature())
         query_param = ""
         if options:
@@ -114,13 +120,15 @@ class PaymentMethodGateway(object):
         except NotFoundError:
             raise NotFoundError("payment method with payment_method_token " + repr(payment_method_token) + " not found")
 
-    def _post(self, url, params={}, result_key="payment_method"):
+    def _post(self, url, params=None, result_key="payment_method"):
+        if params is None:
+            params = {}
         response = self.config.http().post(self.config.base_merchant_path() + url, params)
         if "api_error_response" in response:
             return ErrorResult(self.gateway, response["api_error_response"])
-        elif result_key is "revoke" and response.get("success", False):
+        elif result_key == "revoke" and response.get("success", False):
             return SuccessfulResult()
-        elif result_key is "payment_method_nonce":
+        elif result_key == "payment_method_nonce":
             payment_method_nonce = self._parse_payment_method_nonce(response)
             return SuccessfulResult({result_key: payment_method_nonce})
         else:
@@ -128,7 +136,9 @@ class PaymentMethodGateway(object):
             return SuccessfulResult({result_key: payment_method})
         return response
 
-    def _put(self, url, params={}):
+    def _put(self, url, params=None):
+        if params is None:
+            params = {}
         response = self.config.http().put(self.config.base_merchant_path() + url, params)
         if "api_error_response" in response:
             return ErrorResult(self.gateway, response["api_error_response"])
@@ -140,3 +150,9 @@ class PaymentMethodGateway(object):
         if "payment_method_nonce" in response:
             return PaymentMethodNonce(self.gateway, response["payment_method_nonce"])
         raise ValueError("payment_method_nonce not present in response")
+
+    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)
diff --git a/braintree/payment_method_nonce.py b/braintree/payment_method_nonce.py
index 59c5088..9402999 100644
--- a/braintree/payment_method_nonce.py
+++ b/braintree/payment_method_nonce.py
@@ -6,8 +6,8 @@ from braintree.bin_data import BinData
 
 class PaymentMethodNonce(Resource):
     @staticmethod
-    def create(payment_method_token):
-        return Configuration.gateway().payment_method_nonce.create(payment_method_token)
+    def create(payment_method_token, params = {}):
+        return Configuration.gateway().payment_method_nonce.create(payment_method_token, params)
 
     @staticmethod
     def find(payment_method_nonce):
@@ -21,6 +21,11 @@ class PaymentMethodNonce(Resource):
         else:
             self.three_d_secure_info = None
 
+        if "authentication_insight" in attributes and not attributes["authentication_insight"] is None:
+            self.authentication_insight = attributes["authentication_insight"]
+        else:
+            self.authentication_insight = None
+
         if "bin_data" in attributes and not attributes["bin_data"] is None:
             self.bin_data = BinData(attributes["bin_data"])
         else:
diff --git a/braintree/payment_method_nonce_gateway.py b/braintree/payment_method_nonce_gateway.py
index 323409b..be0e0fe 100644
--- a/braintree/payment_method_nonce_gateway.py
+++ b/braintree/payment_method_nonce_gateway.py
@@ -12,9 +12,11 @@ class PaymentMethodNonceGateway(object):
         self.gateway = gateway
         self.config = gateway.config
 
-    def create(self, payment_method_token):
+    def create(self, payment_method_token, params = {"payment_method_nonce": {}}):
         try:
-            response = self.config.http().post(self.config.base_merchant_path() + "/payment_methods/" + payment_method_token + "/nonces")
+            schema = [{"payment_method_nonce": ["merchant_account_id", "authentication_insight", {"authentication_insight_options": ["amount", "recurring_customer_consent", "recurring_max_amount"]}]}]
+            Resource.verify_keys(params, schema)
+            response = self.config.http().post(self.config.base_merchant_path() + "/payment_methods/" + payment_method_token + "/nonces", params)
             if "api_error_response" in response:
                 return ErrorResult(self.gateway, response["api_error_response"])
             else:
diff --git a/braintree/payment_method_parser.py b/braintree/payment_method_parser.py
index b7192a8..732285d 100644
--- a/braintree/payment_method_parser.py
+++ b/braintree/payment_method_parser.py
@@ -4,13 +4,13 @@ from braintree.credit_card import CreditCard
 from braintree.payment_method import PaymentMethod
 from braintree.paypal_account import PayPalAccount
 from braintree.europe_bank_account import EuropeBankAccount
-from braintree.coinbase_account import CoinbaseAccount
 from braintree.android_pay_card import AndroidPayCard
 from braintree.amex_express_checkout_card import AmexExpressCheckoutCard
 from braintree.venmo_account import VenmoAccount
 from braintree.us_bank_account import UsBankAccount
 from braintree.visa_checkout_card import VisaCheckoutCard
 from braintree.masterpass_card import MasterpassCard
+from braintree.sepa_direct_debit_account import SepaDirectDebitAccount
 from braintree.samsung_pay_card import SamsungPayCard
 from braintree.unknown_payment_method import UnknownPaymentMethod
 
@@ -25,16 +25,18 @@ def parse_payment_method(gateway, attributes):
         return ApplePayCard(gateway, attributes["apple_pay_card"])
     elif "android_pay_card" in attributes:
         return AndroidPayCard(gateway, attributes["android_pay_card"])
+    # NEXT_MAJOR_VERSION remove amex express checkout
     elif "amex_express_checkout_card" in attributes:
         return AmexExpressCheckoutCard(gateway, attributes["amex_express_checkout_card"])
-    elif "coinbase_account" in attributes:
-        return CoinbaseAccount(gateway, attributes["coinbase_account"])
+    elif "sepa_debit_account" in attributes:
+        return SepaDirectDebitAccount(gateway, attributes["sepa_debit_account"])
     elif "venmo_account" in attributes:
         return VenmoAccount(gateway, attributes["venmo_account"])
     elif "us_bank_account" in attributes:
         return UsBankAccount(gateway, attributes["us_bank_account"])
     elif "visa_checkout_card" in attributes:
         return VisaCheckoutCard(gateway, attributes["visa_checkout_card"])
+    # NEXT_MAJOR_VERSION remove masterpass
     elif "masterpass_card" in attributes:
         return MasterpassCard(gateway, attributes["masterpass_card"])
     elif "samsung_pay_card" in attributes:
diff --git a/braintree/paypal_account.py b/braintree/paypal_account.py
index 864fdde..bb08234 100644
--- a/braintree/paypal_account.py
+++ b/braintree/paypal_account.py
@@ -2,6 +2,7 @@ import braintree
 from braintree.resource import Resource
 from braintree.configuration import Configuration
 
+
 class PayPalAccount(Resource):
     @staticmethod
     def find(paypal_account_token):
@@ -12,7 +13,9 @@ class PayPalAccount(Resource):
         return Configuration.gateway().paypal_account.delete(paypal_account_token)
 
     @staticmethod
-    def update(paypal_account_token, params={}):
+    def update(paypal_account_token, params=None):
+        if params is None:
+            params = {}
         return Configuration.gateway().paypal_account.update(paypal_account_token, params)
 
     @staticmethod
diff --git a/braintree/paypal_account_gateway.py b/braintree/paypal_account_gateway.py
index c4e8a57..cd748af 100644
--- a/braintree/paypal_account_gateway.py
+++ b/braintree/paypal_account_gateway.py
@@ -5,6 +5,7 @@ from braintree.exceptions.not_found_error import NotFoundError
 from braintree.resource import Resource
 from braintree.successful_result import SuccessfulResult
 
+
 class PayPalAccountGateway(object):
     def __init__(self, gateway):
         self.gateway = gateway
@@ -25,7 +26,9 @@ class PayPalAccountGateway(object):
         self.config.http().delete(self.config.base_merchant_path() + "/payment_methods/paypal_account/" + paypal_account_token)
         return SuccessfulResult()
 
-    def update(self, paypal_account_token, params={}):
+    def update(self, paypal_account_token, params=None):
+        if params is None:
+            params = {}
         Resource.verify_keys(params, PayPalAccount.signature())
         response = self.config.http().put(self.config.base_merchant_path() + "/payment_methods/paypal_account/" + paypal_account_token, {"paypal_account": params})
         if "paypal_account" in response:
diff --git a/braintree/plan.py b/braintree/plan.py
index 01a59e3..5366f69 100644
--- a/braintree/plan.py
+++ b/braintree/plan.py
@@ -5,6 +5,8 @@ from braintree.configuration import Configuration
 from braintree.discount import Discount
 from braintree.resource_collection import ResourceCollection
 from braintree.resource import Resource
+from braintree.successful_result import SuccessfulResult
+from braintree.error_result import ErrorResult
 
 class Plan(Resource):
 
@@ -19,3 +21,72 @@ class Plan(Resource):
     def all():
         return Configuration.gateway().plan.all()
 
+    @staticmethod
+    def create(params=None):
+        if params is None:
+            params = {}
+        return Configuration.gateway().plan.create(params)
+
+    @staticmethod
+    def find(subscription_id):
+        return Configuration.gateway().plan.find(subscription_id)
+
+    @staticmethod
+    def update(subscription_id, params=None):
+        if params is None:
+            params = {}
+        return Configuration.gateway().plan.update(subscription_id, params)
+
+    @staticmethod
+    def create_signature():
+        return [
+            "billing_day_of_month",
+            "billing_frequency",
+            "currency_iso_code",
+            "description",
+            "id",
+            "merchant_id",
+            "name",
+            "number_of_billing_cycles",
+            "price",
+            "trial_duration",
+            "trial_duration_unit",
+            "trial_period"
+        ] + Plan._add_on_discount_signature()
+
+    @staticmethod
+    def update_signature():
+        return [
+            "billing_day_of_month",
+            "billing_frequency",
+            "currency_iso_code",
+            "description",
+            "id",
+            "merchant_id",
+            "name",
+            "number_of_billing_cycles",
+            "price",
+            "trial_duration",
+            "trial_duration_unit",
+            "trial_period"
+        ] + Plan._add_on_discount_signature()
+
+    @staticmethod
+    def _add_on_discount_signature():
+        return [
+            {
+                "add_ons": [
+                    {"add": ["amount", "inherited_from_id", "never_expires", "number_of_billing_cycles", "quantity"]},
+                    {"update": ["amount", "existing_id", "never_expires", "number_of_billing_cycles", "quantity"]},
+                    {"remove": ["_any_key_"]}
+                ]
+            },
+            {
+                "discounts": [
+                    {"add": ["amount", "inherited_from_id", "never_expires", "number_of_billing_cycles", "quantity"]},
+                    {"update": ["amount", "existing_id", "never_expires", "number_of_billing_cycles", "quantity"]},
+                    {"remove": ["_any_key_"]}
+                ]
+            }
+        ]
+
diff --git a/braintree/plan_gateway.py b/braintree/plan_gateway.py
index c6f1198..2cbf432 100644
--- a/braintree/plan_gateway.py
+++ b/braintree/plan_gateway.py
@@ -15,3 +15,33 @@ class PlanGateway(object):
     def all(self):
         response = self.config.http().get(self.config.base_merchant_path() + "/plans/")
         return [Plan(self.gateway, item) for item in ResourceCollection._extract_as_array(response, "plans")]
+
+    def create(self, params=None):
+        if params is None:
+            params = {}
+        Resource.verify_keys(params, Plan.create_signature())
+        response = self.config.http().post(self.config.base_merchant_path() + "/plans", {"plan": params})
+        if "plan" in response:
+            return SuccessfulResult({"plan": Plan(self.gateway, response["plan"])})
+        elif "api_error_response" in response:
+            return ErrorResult(self.gateway, response["api_error_response"])
+
+    def find(self, plan_id):
+        try:
+            if plan_id is None or plan_id.strip() == "":
+                raise NotFoundError()
+            response = self.config.http().get(self.config.base_merchant_path() + "/plans/" + plan_id)
+            return Plan(self.gateway, response["plan"])
+        except NotFoundError:
+            raise NotFoundError("Plan with id " + repr(plan_id) + " not found")
+
+    def update(self, plan_id, params=None):
+        if params is None:
+            params = {}
+        Resource.verify_keys(params, Plan.update_signature())
+        response = self.config.http().put(self.config.base_merchant_path() + "/plans/" + plan_id, {"plan": params})
+        if "plan" in response:
+            return SuccessfulResult({"plan": Plan(self.gateway, response["plan"])})
+        elif "api_error_response" in response:
+            return ErrorResult(self.gateway, response["api_error_response"])
+
diff --git a/braintree/resource.py b/braintree/resource.py
index 41d176f..23c90fb 100644
--- a/braintree/resource.py
+++ b/braintree/resource.py
@@ -3,8 +3,8 @@ import string
 import sys
 from braintree.attribute_getter import AttributeGetter
 
-text_type = unicode if sys.version_info[0] == 2 else str
-raw_type = str if sys.version_info[0] == 2 else bytes
+text_type = str
+raw_type = bytes
 
 class Resource(AttributeGetter):
     @staticmethod
@@ -51,10 +51,14 @@ class Resource(AttributeGetter):
 
     @staticmethod
     def __remove_wildcard_keys(allowed_keys, invalid_keys):
-        wildcard_keys = [re.sub("(?<=[^\\\\])_", "\\_", re.escape(key)).replace("\\[\\_\\_any\\_key\\_\\_\\]", "\\[[\w-]+\\]") for key in allowed_keys if re.search("\\[__any_key__\\]", key)]
+        wildcard_keys = [
+            re.sub(r"(?<=[^\\])_", "\\_", re.escape(key)).replace(r"\[\_\_any\_key\_\_\]", r"\[[\w-]+\]")
+            for key in allowed_keys
+            if re.search(r"\[__any_key__\]", key)
+        ]
         new_keys = []
         for key in invalid_keys:
-            if len([match for match in wildcard_keys if re.match("\A" + match + "\Z", key)]) == 0:
+            if len([match for match in wildcard_keys if re.match(r"\A" + match + r"\Z", key)]) == 0:
                 new_keys.append(key)
         return new_keys
 
diff --git a/braintree/risk_data.py b/braintree/risk_data.py
index 0882740..fa2b8b6 100644
--- a/braintree/risk_data.py
+++ b/braintree/risk_data.py
@@ -1,4 +1,8 @@
 from braintree.attribute_getter import AttributeGetter
+from braintree.liability_shift import LiabilityShift
 
 class RiskData(AttributeGetter):
-    pass
+    def __init__(self, attributes):
+        AttributeGetter.__init__(self, attributes)
+        if "liability_shift" in attributes:
+            self.liability_shift = LiabilityShift(attributes["liability_shift"])
diff --git a/braintree/search.py b/braintree/search.py
index a8e290a..fc8a4cb 100644
--- a/braintree/search.py
+++ b/braintree/search.py
@@ -1,3 +1,5 @@
+import warnings
+
 class Search:
     """
     Collection of classes used to build search queries.
@@ -73,6 +75,8 @@ class Search:
     class MultipleValueNodeBuilder(object):
         """Builds a query to check membership in a sequence."""
         def __init__(self, name, whitelist = []):
+            if "chargeback_protection_level" == name:
+                warnings.warn("Use protection_level parameter instead", DeprecationWarning)
             self.name = name
             self.whitelist = whitelist
 
diff --git a/braintree/sepa_direct_debit_account.py b/braintree/sepa_direct_debit_account.py
new file mode 100644
index 0000000..d4f9548
--- /dev/null
+++ b/braintree/sepa_direct_debit_account.py
@@ -0,0 +1,18 @@
+import braintree
+from braintree.resource import Resource
+from braintree.configuration import Configuration
+
+
+class SepaDirectDebitAccount(Resource):
+    @staticmethod
+    def find(sepa_direct_debit_account_token):
+        return Configuration.gateway().sepa_direct_debit_account.find(sepa_direct_debit_account_token)
+
+    @staticmethod
+    def delete(sepa_direct_debit_account_token):
+        return Configuration.gateway().sepa_direct_debit_account.delete(sepa_direct_debit_account_token)
+
+    def __init__(self, gateway, attributes):
+        Resource.__init__(self, gateway, attributes)
+        if "subscriptions" in attributes:
+            self.subscriptions = [braintree.subscription.Subscription(gateway, subscription) for subscription in self.subscriptions]
diff --git a/braintree/sepa_direct_debit_account_gateway.py b/braintree/sepa_direct_debit_account_gateway.py
new file mode 100644
index 0000000..3c3d2d9
--- /dev/null
+++ b/braintree/sepa_direct_debit_account_gateway.py
@@ -0,0 +1,27 @@
+import braintree
+from braintree.sepa_direct_debit_account import SepaDirectDebitAccount
+from braintree.error_result import ErrorResult
+from braintree.exceptions.not_found_error import NotFoundError
+from braintree.resource import Resource
+from braintree.successful_result import SuccessfulResult
+
+
+class SepaDirectDebitAccountGateway(object):
+    def __init__(self, gateway):
+        self.gateway = gateway
+        self.config = gateway.config
+
+    def find(self, sepa_direct_debit_account_token):
+        try:
+            if sepa_direct_debit_account_token is None or sepa_direct_debit_account_token.strip() == "":
+                raise NotFoundError()
+
+            response = self.config.http().get(self.config.base_merchant_path() + "/payment_methods/sepa_debit_account/" + sepa_direct_debit_account_token)
+            if "sepa_debit_account" in response:
+                return SepaDirectDebitAccount(self.gateway, response["sepa_debit_account"])
+        except NotFoundError:
+            raise NotFoundError("sepa direct debit account with token " + repr(sepa_direct_debit_account_token) + " not found")
+
+    def delete(self, sepa_direct_debit_account_token):
+        self.config.http().delete(self.config.base_merchant_path() + "/payment_methods/sepa_debit_account/" + sepa_direct_debit_account_token)
+        return SuccessfulResult()
diff --git a/braintree/subscription.py b/braintree/subscription.py
index a9b7d4d..4ba61d7 100644
--- a/braintree/subscription.py
+++ b/braintree/subscription.py
@@ -14,6 +14,7 @@ from braintree.transaction import Transaction
 from braintree.resource import Resource
 from braintree.configuration import Configuration
 
+
 class Subscription(Resource):
     """
     A class representing a Subscription.
@@ -31,10 +32,11 @@ class Subscription(Resource):
             "trial_period": True
         })
 
-    For more information on Subscriptions, see https://developers.braintreepayments.com/reference/request/subscription/create/python
+    For more information on Subscriptions, see https://developer.paypal.com/braintree/docs/reference/request/subscription/create/python
 
     """
 
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
     class TrialDurationUnit(object):
         """
         Constants representing trial duration units.  Available types are:
@@ -46,15 +48,16 @@ class Subscription(Resource):
         Day = "day"
         Month = "month"
 
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
     class Source(object):
         Api          = "api"
         ControlPanel = "control_panel"
         Recurring    = "recurring"
-        Unrecognized = "unrecognized"
 
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
     class Status(object):
         """
-        Constants representing subscription statusues.  Available statuses are:
+        Constants representing subscription statuses.  Available statuses are:
 
         * braintree.Subscription.Status.Active
         * braintree.Subscription.Status.Canceled
@@ -70,7 +73,7 @@ class Subscription(Resource):
         Pending = "Pending"
 
     @staticmethod
-    def create(params={}):
+    def create(params=None):
         """
         Create a Subscription
 
@@ -82,7 +85,8 @@ class Subscription(Resource):
             })
 
         """
-
+        if params is None:
+            params = {}
         return Configuration.gateway().subscription.create(params)
 
     @staticmethod
@@ -102,14 +106,14 @@ class Subscription(Resource):
             "trial_duration_unit",
             "trial_period",
             {
-                "descriptor": [ "name", "phone", "url" ]
+                "descriptor": ["name", "phone", "url"]
             },
             {
                 "options": [
                     "do_not_inherit_add_ons_or_discounts",
                     "start_immediately",
-                    { 
-                        "paypal": [ "description" ] 
+                    {
+                        "paypal": ["description"]
                     }
                 ]
             }
@@ -127,17 +131,12 @@ class Subscription(Resource):
 
         return Configuration.gateway().subscription.find(subscription_id)
 
-    @staticmethod
-    def retryCharge(subscription_id, amount=None):
-        warnings.warn("Please use Subscription.retry_charge instead", DeprecationWarning)
-        return Subscription.retry_charge(subscription_id, amount)
-
     @staticmethod
     def retry_charge(subscription_id, amount=None, submit_for_settlement=False):
         return Configuration.gateway().subscription.retry_charge(subscription_id, amount, submit_for_settlement)
 
     @staticmethod
-    def update(subscription_id, params={}):
+    def update(subscription_id, params=None):
         """
         Update an existing subscription
 
@@ -149,7 +148,8 @@ class Subscription(Resource):
             })
 
         """
-
+        if params is None:
+            params = {}
         return Configuration.gateway().subscription.update(subscription_id, params)
 
     @staticmethod
@@ -177,7 +177,7 @@ class Subscription(Resource):
         - status
 
         For text fields, you can search using the following operators: ==, !=, starts_with, ends_with
-        and contains. For mutiple value fields, you can search using the in_list operator. An example::
+        and contains. For multiple value fields, you can search using the in_list operator. An example::
 
             braintree.Subscription.search([
                 braintree.SubscriptionSearch.plan_id.starts_with("abc"),
@@ -203,9 +203,9 @@ class Subscription(Resource):
                 "descriptor": [ "name", "phone", "url" ]
             },
             {
-                "options": [ 
-                    "prorate_charges", 
-                    "replace_all_add_ons_and_discounts", 
+                "options": [
+                    "prorate_charges",
+                    "replace_all_add_ons_and_discounts",
                     "revert_subscription_on_proration_failure",
                     {
                         "paypal": [ "description" ]
@@ -232,9 +232,6 @@ class Subscription(Resource):
         ]
 
     def __init__(self, gateway, attributes):
-        if "next_bill_amount" in attributes:
-            self._next_bill_amount = Decimal(attributes["next_bill_amount"])
-            del(attributes["next_bill_amount"])
         Resource.__init__(self, gateway, attributes)
         if "price" in attributes:
             self.price = Decimal(self.price)
@@ -247,15 +244,10 @@ class Subscription(Resource):
         if "descriptor" in attributes:
             self.descriptor = Descriptor(gateway, attributes.pop("descriptor"))
         if "description" in attributes:
-            self.description = attributes["description"] 
+            self.description = attributes["description"]
         if "discounts" in attributes:
             self.discounts = [Discount(gateway, discount) for discount in self.discounts]
         if "status_history" in attributes:
             self.status_history = [SubscriptionStatusEvent(gateway, status_event) for status_event in self.status_history]
         if "transactions" in attributes:
             self.transactions = [Transaction(gateway, transaction) for transaction in self.transactions]
-
-    @property
-    def next_bill_amount(self):
-        warnings.warn("Please use Subscription.next_billing_period_amount instead", DeprecationWarning)
-        return self._next_bill_amount
diff --git a/braintree/subscription_gateway.py b/braintree/subscription_gateway.py
index e40c1f5..34a33a3 100644
--- a/braintree/subscription_gateway.py
+++ b/braintree/subscription_gateway.py
@@ -8,6 +8,7 @@ from braintree.resource_collection import ResourceCollection
 from braintree.successful_result import SuccessfulResult
 from braintree.transaction import Transaction
 
+
 class SubscriptionGateway(object):
     def __init__(self, gateway):
         self.gateway = gateway
@@ -20,7 +21,9 @@ class SubscriptionGateway(object):
         elif "api_error_response" in response:
             return ErrorResult(self.gateway, response["api_error_response"])
 
-    def create(self, params={}):
+    def create(self, params=None):
+        if params is None:
+            params = {}
         Resource.verify_keys(params, Subscription.create_signature())
         response = self.config.http().post(self.config.base_merchant_path() + "/subscriptions", {"subscription": params})
         if "subscription" in response:
@@ -56,7 +59,9 @@ class SubscriptionGateway(object):
         response = self.config.http().post(self.config.base_merchant_path() + "/subscriptions/advanced_search_ids", {"search": self.__criteria(query)})
         return ResourceCollection(query, response, self.__fetch)
 
-    def update(self, subscription_id, params={}):
+    def update(self, subscription_id, params=None):
+        if params is None:
+            params = {}
         Resource.verify_keys(params, Subscription.update_signature())
         response = self.config.http().put(self.config.base_merchant_path() + "/subscriptions/" + subscription_id, {"subscription": params})
         if "subscription" in response:
diff --git a/braintree/test/authentication_ids.py b/braintree/test/authentication_ids.py
new file mode 100644
index 0000000..593845e
--- /dev/null
+++ b/braintree/test/authentication_ids.py
@@ -0,0 +1,16 @@
+class AuthenticationIds(object):
+    ThreeDSecureVisaFullAuthentication = "fake-three-d-secure-visa-full-authentication-id"
+    ThreeDSecureVisaLookupTimeout = "fake-three-d-secure-visa-lookup-timeout-id"
+    ThreeDSecureVisaFailedSignature = "fake-three-d-secure-visa-failed-signature-id"
+    ThreeDSecureVisaFailedAuthentication = "fake-three-d-secure-visa-failed-authentication-id"
+    ThreeDSecureVisaAttemptsNonParticipating = "fake-three-d-secure-visa-attempts-non-participating-id"
+    ThreeDSecureVisaNoteEnrolled = "fake-three-d-secure-visa-not-enrolled-id"
+    ThreeDSecureVisaUnavailable = "fake-three-d-secure-visa-unavailable-id"
+    ThreeDSecureVisaMPILookupError = "fake-three-d-secure-visa-mpi-lookup-error-id"
+    ThreeDSecureVisaMPIAuthenticateError = "fake-three-d-secure-visa-mpi-authenticate-error-id"
+    ThreeDSecureVisaAuthenticationUnavailable = "fake-three-d-secure-visa-authentication-unavailable-id"
+    ThreeDSecureVisaBypassedAuthentication = "fake-three-d-secure-visa-bypassed-authentication-id"
+    ThreeDSecureTwoVisaSuccessfulFrictionlessAuthentication = "fake-three-d-secure-two-visa-successful-frictionless-authentication-id"
+    ThreeDSecureTwoVisaSuccessfulStepUpAuthentication = "fake-three-d-secure-two-visa-successful-step-up-authentication-id"
+    ThreeDSecureTwoVisaErrorOnLookup = "fake-three-d-secure-two-visa-error-on-lookup-id"
+    ThreeDSecureTwoVisaTimeoutOnLookup = "fake-three-d-secure-two-visa-timeout-on-lookup-id"
diff --git a/braintree/test/credit_card_numbers.py b/braintree/test/credit_card_numbers.py
index 1e73a88..07f852d 100644
--- a/braintree/test/credit_card_numbers.py
+++ b/braintree/test/credit_card_numbers.py
@@ -25,6 +25,7 @@ class CreditCardNumbers(object):
 
     Hiper = "6370950000000005"
     Hipercard = "6062820524845321"
+    Amex = "378734493671000"
 
     class FailsSandboxVerification(object):
         AmEx       = "378734493671000"
diff --git a/braintree/test/nonces.py b/braintree/test/nonces.py
index 5990ed5..e38916e 100644
--- a/braintree/test/nonces.py
+++ b/braintree/test/nonces.py
@@ -2,6 +2,7 @@ class Nonces(object):
     Transactable = "fake-valid-nonce"
     Consumed = "fake-consumed-nonce"
     PayPalOneTimePayment = "fake-paypal-one-time-nonce"
+    # NEXT_MAJOR_VERSION - no longer supported in the Gateway, remove this constant
     PayPalFuturePayment = "fake-paypal-future-nonce"
     PayPalBillingAgreement = "fake-paypal-billing-agreement-nonce"
     ApplePayVisa = "fake-apple-pay-visa-nonce"
@@ -9,16 +10,31 @@ class Nonces(object):
     ApplePayAmEx = "fake-apple-pay-amex-nonce"
     AbstractTransactable = "fake-abstract-transactable-nonce"
     Europe = "fake-europe-bank-account-nonce"
-    Coinbase = "fake-coinbase-nonce"
+    # NEXT_MAJOR_VERSION - rename AndroidPay to GooglePay
     AndroidPayCard = "fake-android-pay-nonce"
     AndroidPayCardDiscover = "fake-android-pay-discover-nonce"
     AndroidPayCardVisa = "fake-android-pay-visa-nonce"
     AndroidPayCardMasterCard = "fake-android-pay-mastercard-nonce"
     AndroidPayCardAmEx = "fake-android-pay-amex-nonce"
+    # NEXT_MAJOR_VERSION remove amex express checkout
     AmexExpressCheckoutCard = "fake-amex-express-checkout-nonce"
     VenmoAccount = "fake-venmo-account-nonce"
     VenmoAccountTokenIssuanceError = "fake-token-issuance-error-venmo-account-nonce"
     ThreeDSecureVisaFullAuthentication = "fake-three-d-secure-visa-full-authentication-nonce"
+    ThreeDSecureVisaLookupTimeout = "fake-three-d-secure-visa-lookup-timeout-nonce"
+    ThreeDSecureVisaFailedSignature = "fake-three-d-secure-visa-failed-signature-nonce"
+    ThreeDSecureVisaFailedAuthentication = "fake-three-d-secure-visa-failed-authentication-nonce"
+    ThreeDSecureVisaAttemptsNonParticipating = "fake-three-d-secure-visa-attempts-non-participating-nonce"
+    ThreeDSecureVisaNoteEnrolled = "fake-three-d-secure-visa-not-enrolled-nonce"
+    ThreeDSecureVisaUnavailable = "fake-three-d-secure-visa-unavailable-nonce"
+    ThreeDSecureVisaMPILookupError = "fake-three-d-secure-visa-mpi-lookup-error-nonce"
+    ThreeDSecureVisaMPIAuthenticateError = "fake-three-d-secure-visa-mpi-authenticate-error-nonce"
+    ThreeDSecureVisaAuthenticationUnavailable = "fake-three-d-secure-visa-authentication-unavailable-nonce"
+    ThreeDSecureVisaBypassedAuthentication = "fake-three-d-secure-visa-bypassed-authentication-nonce"
+    ThreeDSecureTwoVisaSuccessfulFrictionlessAuthentication = "fake-three-d-secure-two-visa-successful-frictionless-authentication-nonce"
+    ThreeDSecureTwoVisaSuccessfulStepUpAuthentication = "fake-three-d-secure-two-visa-successful-step-up-authentication-nonce"
+    ThreeDSecureTwoVisaErrorOnLookup = "fake-three-d-secure-two-visa-error-on-lookup-nonce"
+    ThreeDSecureTwoVisaTimeoutOnLookup = "fake-three-d-secure-two-visa-timeout-on-lookup-nonce"
     TransactableVisa = "fake-valid-visa-nonce"
     TransactableAmEx = "fake-valid-amex-nonce"
     TransactableMasterCard = "fake-valid-mastercard-nonce"
@@ -47,6 +63,8 @@ class Nonces(object):
     PayPalFuturePaymentRefreshToken = "fake-paypal-future-refresh-token-nonce"
     SEPA = "fake-sepa-bank-account-nonce"
     GatewayRejectedFraud = "fake-gateway-rejected-fraud-nonce"
+    GatewayRejectedRiskThreshold = "fake-gateway-rejected-risk-thresholds-nonce"
+    # NEXT_MAJOR_VERSION remove masterpass
     MasterpassAmEx = "fake-masterpass-amex-nonce"
     MasterpassDiscover = "fake-masterpass-discover-nonce"
     MasterpassMasterCard = "fake-masterpass-mastercard-nonce"
@@ -59,3 +77,4 @@ class Nonces(object):
     SamsungPayDiscover = "tokensam_fake_american_express"
     SamsungPayMasterCard = "tokensam_fake_mastercard"
     SamsungPayVisa = "tokensam_fake_visa"
+    SepaDirectDebit = "fake-sepa-direct-debit-nonce"
diff --git a/braintree/testing_gateway.py b/braintree/testing_gateway.py
index 7bc8722..9aacf54 100644
--- a/braintree/testing_gateway.py
+++ b/braintree/testing_gateway.py
@@ -38,7 +38,7 @@ class TestingGateway(object):
         response = self.config.http().post(self.config.base_merchant_path() + "/three_d_secure/create_verification/" + merchant_account_id, {
             "three_d_secure_verification": params
         })
-        return response["three_d_secure_verification"]["three_d_secure_token"]
+        return response["three_d_secure_verification"]["three_d_secure_authentication_id"]
 
     def __create_result(self, response):
         if "transaction" in response:
diff --git a/braintree/transaction.py b/braintree/transaction.py
index 4e1a2f9..eab179a 100644
--- a/braintree/transaction.py
+++ b/braintree/transaction.py
@@ -2,45 +2,43 @@ import braintree
 import warnings
 from decimal import Decimal
 from braintree.add_on import AddOn
+from braintree.address import Address
+from braintree.amex_express_checkout_card import AmexExpressCheckoutCard
+from braintree.android_pay_card import AndroidPayCard
 from braintree.apple_pay_card import ApplePayCard
 from braintree.authorization_adjustment import AuthorizationAdjustment
-from braintree.coinbase_account import CoinbaseAccount
-from braintree.android_pay_card import AndroidPayCard
-from braintree.amex_express_checkout_card import AmexExpressCheckoutCard
-from braintree.venmo_account import VenmoAccount
-from braintree.disbursement_detail import DisbursementDetail
-from braintree.dispute import Dispute
-from braintree.discount import Discount
-from braintree.successful_result import SuccessfulResult
-from braintree.status_event import StatusEvent
-from braintree.error_result import ErrorResult
-from braintree.resource import Resource
-from braintree.address import Address
 from braintree.configuration import Configuration
 from braintree.credit_card import CreditCard
 from braintree.customer import Customer
+from braintree.descriptor import Descriptor
+from braintree.disbursement_detail import DisbursementDetail
+from braintree.discount import Discount
+from braintree.dispute import Dispute
+from braintree.error_result import ErrorResult
+from braintree.europe_bank_account import EuropeBankAccount
+from braintree.exceptions.not_found_error import NotFoundError
+from braintree.facilitated_details import FacilitatedDetails
+from braintree.facilitator_details import FacilitatorDetails
+from braintree.liability_shift import LiabilityShift
+from braintree.local_payment import LocalPayment
+from braintree.masterpass_card import MasterpassCard
+from braintree.payment_instrument_type import PaymentInstrumentType
 from braintree.paypal_account import PayPalAccount
 from braintree.paypal_here import PayPalHere
-from braintree.europe_bank_account import EuropeBankAccount
-from braintree.subscription_details import SubscriptionDetails
+from braintree.resource import Resource
 from braintree.resource_collection import ResourceCollection
-from braintree.transparent_redirect import TransparentRedirect
-from braintree.exceptions.not_found_error import NotFoundError
-from braintree.descriptor import Descriptor
 from braintree.risk_data import RiskData
+from braintree.samsung_pay_card import SamsungPayCard
+from braintree.sepa_direct_debit_account import SepaDirectDebitAccount
+from braintree.status_event import StatusEvent
+from braintree.subscription_details import SubscriptionDetails
+from braintree.successful_result import SuccessfulResult
 from braintree.three_d_secure_info import ThreeDSecureInfo
 from braintree.transaction_line_item import TransactionLineItem
 from braintree.us_bank_account import UsBankAccount
-# NEXT_MAJOR_VERSION Remove this class as legacy Ideal has been removed/disabled in the Braintree Gateway
-# DEPRECATED If you're looking to accept iDEAL as a payment method contact accounts@braintreepayments.com for a solution.
-from braintree.ideal_payment import IdealPayment
-from braintree.local_payment import LocalPayment
+from braintree.venmo_account import VenmoAccount
 from braintree.visa_checkout_card import VisaCheckoutCard
-from braintree.masterpass_card import MasterpassCard
-from braintree.facilitated_details import FacilitatedDetails
-from braintree.facilitator_details import FacilitatorDetails
-from braintree.payment_instrument_type import PaymentInstrumentType
-from braintree.samsung_pay_card import SamsungPayCard
+
 
 class Transaction(Resource):
     """
@@ -93,15 +91,17 @@ class Transaction(Resource):
         print(result.transaction.amount)
         print(result.transaction.order_id)
 
-    For more information on Transactions, see https://developers.braintreepayments.com/reference/request/transaction/sale/python
+    For more information on Transactions, see https://developer.paypal.com/braintree/docs/reference/request/transaction/sale/python
 
     """
 
     def __repr__(self):
       detail_list = [
         "id",
+        "graphql_id",
         "additional_processor_response",
         "amount",
+        "acquirer_reference_number",
         "authorization_adjustments",
         "authorization_expires_at",
         "avs_error_response_code",
@@ -117,6 +117,8 @@ class Transaction(Resource):
         "disputes",
         "escrow_status",
         "gateway_rejection_reason",
+        "installments",
+        "liability_shift",
         "master_merchant_account_id",
         "merchant_account_id",
         "network_response_code",
@@ -126,6 +128,7 @@ class Transaction(Resource):
         "payment_instrument_type",
         "payment_method_token",
         "plan_id",
+        "processed_with_network_token",
         "processor_authorization_code",
         "processor_response_code",
         "processor_response_text",
@@ -135,6 +138,8 @@ class Transaction(Resource):
         "recurring",
         "refund_id",
         "refunded_transaction_id",
+        "retried",
+        "retrieval_reference_number",
         "service_fee_amount",
         "settlement_batch_id",
         "shipping_amount",
@@ -152,6 +157,7 @@ class Transaction(Resource):
 
       return super(Transaction, self).__repr__(detail_list)
 
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
     class CreatedUsing(object):
         """
         Constants representing how the transaction was created.  Available types are:
@@ -162,8 +168,8 @@ class Transaction(Resource):
 
         FullInformation = "full_information"
         Token           = "token"
-        Unrecognized    = "unrecognized"
 
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
     class GatewayRejectionReason(object):
         """
         Constants representing gateway rejection reasons. Available types are:
@@ -172,25 +178,34 @@ class Transaction(Resource):
         * braintree.Transaction.GatewayRejectionReason.AvsAndCvv
         * braintree.Transaction.GatewayRejectionReason.Cvv
         * braintree.Transaction.GatewayRejectionReason.Duplicate
+        * braintree.Transaction.GatewayRejectionReason.ExcessiveRetry
         * braintree.Transaction.GatewayRejectionReason.Fraud
+        * braintree.Transaction.GatewayRejectionReason.RiskThreshold
         * braintree.Transaction.GatewayRejectionReason.ThreeDSecure
+        * braintree.Transaction.GatewayRejectionReason.TokenIssuance
         """
         ApplicationIncomplete = "application_incomplete"
         Avs                   = "avs"
         AvsAndCvv             = "avs_and_cvv"
         Cvv                   = "cvv"
         Duplicate             = "duplicate"
+        ExcessiveRetry        = "excessive_retry"
         Fraud                 = "fraud"
+        RiskThreshold         = "risk_threshold"
         ThreeDSecure          = "three_d_secure"
         TokenIssuance         = "token_issuance"
-        Unrecognized          = "unrecognized"
 
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
+    class ReasonCode(object):
+        ANY_REASON_CODE = 'any_reason_code'
+
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
     class Source(object):
         Api          = "api"
         ControlPanel = "control_panel"
         Recurring    = "recurring"
-        Unrecognized = "unrecognized"
 
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
     class EscrowStatus(object):
         """
         Constants representing transaction escrow statuses. Available statuses are:
@@ -207,8 +222,8 @@ class Transaction(Resource):
         ReleasePending = "release_pending"
         Released       = "released"
         Refunded       = "refunded"
-        Unrecognized   = "unrecognized"
 
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
     class Status(object):
         """
         Constants representing transaction statuses. Available statuses are:
@@ -241,9 +256,8 @@ class Transaction(Resource):
         Settling               = "settling"
         SubmittedForSettlement = "submitted_for_settlement"
         Voided                 = "voided"
-        # NEXT_MAJOR_VERSION this is never used and should be removed
-        Unrecognized           = "unrecognized"
 
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
     class Type(object):
         """
         Constants representing transaction types. Available types are:
@@ -255,11 +269,13 @@ class Transaction(Resource):
         Credit = "credit"
         Sale = "sale"
 
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
     class IndustryType(object):
         Lodging = "lodging"
         TravelAndCruise = "travel_cruise"
         TravelAndFlight = "travel_flight"
 
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
     class AdditionalCharge(object):
         Restaurant = "restaurant"
         GiftShop = "gift_shop"
@@ -268,6 +284,18 @@ class Transaction(Resource):
         Laundry = "laundry"
         Other = "other"
 
+    @staticmethod
+    def adjust_authorization(transaction_id, amount):
+        """
+        adjust authorization for an existing transaction.
+
+        It expects a `transaction_id` and `amount`, which is the new total authorization amount
+
+        result = braintree.Transaction.adjust_authorization("my_transaction_id", "amount")
+
+        """
+        return Configuration.gateway().transaction.adjust_authorization(transaction_id, amount)
+
     @staticmethod
     def clone_transaction(transaction_id, params):
         return Configuration.gateway().transaction.clone_transaction(transaction_id, params)
@@ -286,19 +314,7 @@ class Transaction(Resource):
         return Configuration.gateway().transaction.cancel_release(transaction_id)
 
     @staticmethod
-    def confirm_transparent_redirect(query_string):
-        """
-        Confirms a transparent redirect request. It expects the query string from the
-        redirect request. The query string should _not_ include the leading "?" character. ::
-
-            result = braintree.Transaction.confirm_transparent_redirect_request("foo=bar&id=12345")
-        """
-
-        warnings.warn("Please use TransparentRedirect.confirm instead", DeprecationWarning)
-        return Configuration.gateway().transaction.confirm_transparent_redirect(query_string)
-
-    @staticmethod
-    def credit(params={}):
+    def credit(params=None):
         """
         Creates a transaction of type Credit.
 
@@ -324,7 +340,8 @@ class Transaction(Resource):
             })
 
         """
-
+        if params is None:
+            params = {}
         params["type"] = Transaction.Type.Credit
         return Transaction.create(params)
 
@@ -339,14 +356,6 @@ class Transaction(Resource):
         """
         return Configuration.gateway().transaction.find(transaction_id)
 
-    @staticmethod
-    def line_items(transaction_id):
-        """
-        Find a transaction's line items, given a transaction_id. This does not return
-        a result object. This will raise a :class:`NotFoundError <braintree.exceptions.not_found_error.NotFoundError>` if the provided transaction_id is not found. ::
-        """
-        return Configuration.gateway().transaction_line_item.find_all(transaction_id)
-
     @staticmethod
     def hold_in_escrow(transaction_id):
         """
@@ -374,7 +383,7 @@ class Transaction(Resource):
 
 
     @staticmethod
-    def sale(params={}):
+    def sale(params=None):
         """
         Creates a transaction of type Sale. Amount is required. Also, a credit card,
         customer_id or payment_method_token is required. ::
@@ -397,7 +406,10 @@ class Transaction(Resource):
                 "customer_id": "my_customer_id"
             })
         """
-
+        if params is None:
+            params = {}
+        if "recurring" in params.keys():
+            warnings.warn("Use transaction_source parameter instead", DeprecationWarning)
         params["type"] = Transaction.Type.Sale
         return Transaction.create(params)
 
@@ -419,7 +431,7 @@ class Transaction(Resource):
         return Configuration.gateway().transaction.release_from_escrow(transaction_id)
 
     @staticmethod
-    def submit_for_settlement(transaction_id, amount=None, params={}):
+    def submit_for_settlement(transaction_id, amount=None, params=None):
         """
         Submits an authorized transaction for settlement.
 
@@ -428,13 +440,14 @@ class Transaction(Resource):
             result = braintree.Transaction.submit_for_settlement("my_transaction_id")
 
         """
-
+        if params is None:
+            params = {}
         return Configuration.gateway().transaction.submit_for_settlement(transaction_id, amount, params)
 
     @staticmethod
-    def update_details(transaction_id, params={}):
+    def update_details(transaction_id, params=None):
         """
-        Updates exisiting details for transaction submtted_for_settlement.
+        Updates existing details for transaction submitted_for_settlement.
 
         Requires the transaction id::
 
@@ -449,32 +462,10 @@ class Transaction(Resource):
             )
 
         """
-
+        if params is None:
+            params = {}
         return Configuration.gateway().transaction.update_details(transaction_id, params)
 
-    @staticmethod
-    def tr_data_for_credit(tr_data, redirect_url):
-        """
-        Builds tr_data for a Transaction of type Credit
-        """
-        return Configuration.gateway().transaction.tr_data_for_credit(tr_data, redirect_url)
-
-    @staticmethod
-    def tr_data_for_sale(tr_data, redirect_url):
-        """
-        Builds tr_data for a Transaction of type Sale
-        """
-        return Configuration.gateway().transaction.tr_data_for_sale(tr_data, redirect_url)
-
-    @staticmethod
-    def transparent_redirect_create_url():
-        """
-        Returns the url to be used for creating Transactions through transparent redirect.
-        """
-
-        warnings.warn("Please use TransparentRedirect.url instead", DeprecationWarning)
-        return Configuration.gateway().transaction.transparent_redirect_create_url()
-
     @staticmethod
     def void(transaction_id):
         """
@@ -524,20 +515,23 @@ class Transaction(Resource):
     @staticmethod
     def create_signature():
         return [
-            "amount", "customer_id", "device_session_id", "fraud_merchant_id", "merchant_account_id", "order_id", "channel",
+            "amount", "customer_id", "merchant_account_id", "order_id", "channel",
             "payment_method_token", "purchase_order_number", "recurring", "transaction_source", "shipping_address_id",
-            "device_data", "billing_address_id", "payment_method_nonce", "tax_amount",
+            "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_token", "type", "venmo_sdk_payment_method_code", "service_fee_amount",
+            "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",
+            "device_session_id", "fraud_merchant_id", # NEXT_MAJOR_VERSION remove device_session_id and fraud_merchant_id
             {
                 "risk_data": [
-                    "customer_browser", "customer_ip"
+                    "customer_browser", "customer_device_id", "customer_ip", "customer_location_zip", "customer_tenure"
                 ]
             },
             {
                 "credit_card": [
-                    "token", "cardholder_name", "cvv", "expiration_date", "expiration_month", "expiration_year", "number"
+                    "token", "cardholder_name", "cvv", "expiration_date", "expiration_month", "expiration_year", "number",
+                    {"payment_reader_card_details": ["encrypted_card_data", "key_serial_number"]}
                 ]
             },
             {
@@ -549,14 +543,14 @@ class Transaction(Resource):
                 "billing": [
                     "first_name", "last_name", "company", "country_code_alpha2", "country_code_alpha3",
                     "country_code_numeric", "country_name", "extended_address", "locality",
-                    "postal_code", "region", "street_address"
+                    "phone_number", "postal_code", "region", "street_address"
                 ]
             },
             {
                 "shipping": [
                     "first_name", "last_name", "company", "country_code_alpha2", "country_code_alpha3",
                     "country_code_numeric", "country_name", "extended_address", "locality",
-                    "postal_code", "region", "street_address"
+                    "phone_number", "postal_code", "region", "shipping_method", "street_address"
                 ]
             },
             {
@@ -655,11 +649,36 @@ class Transaction(Resource):
                     "quantity", "name", "description", "kind", "unit_amount", "unit_tax_amount", "total_amount", "discount_amount", "tax_amount", "unit_of_measure", "product_code", "commodity_code", "url",
                 ]
             },
+            {"apple_pay_card": ["number", "cardholder_name", "cryptogram", "expiration_month", "expiration_year", "eci_indicator"]},
+            # NEXT_MAJOR_VERSION use google_pay_card in public API (map to android_pay_card internally)
+            {"android_pay_card": ["number", "cryptogram", "expiration_month", "expiration_year", "eci_indicator", "source_card_type", "source_card_last_four", "google_transaction_id"]},
+            {"installments": {"count"}},
         ]
 
     @staticmethod
     def submit_for_settlement_signature():
-        return ["order_id", {"descriptor": ["name", "phone", "url"]}]
+        return [
+                "order_id",
+                {"descriptor": ["name", "phone", "url"]},
+                "purchase_order_number",
+                "tax_amount",
+                "tax_exempt",
+                "discount_amount",
+                "shipping_amount",
+                "ships_from_postal_code",
+                {"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",
+                    ]
+                },
+                {"shipping":
+                    [
+                        "first_name", "last_name", "company", "country_code_alpha2", "country_code_alpha3",
+                        "country_code_numeric", "country_name", "extended_address", "locality",
+                        "postal_code", "region", "street_address",
+                    ]
+                },
+            ]
 
     @staticmethod
     def update_details_signature():
@@ -667,10 +686,10 @@ class Transaction(Resource):
 
     @staticmethod
     def refund_signature():
-        return ["amount", "order_id"]
+        return ["amount", "order_id", "merchant_account_id"]
 
     @staticmethod
-    def submit_for_partial_settlement(transaction_id, amount, params={}):
+    def submit_for_partial_settlement(transaction_id, amount, params=None):
         """
         Creates a partial settlement transaction for an authorized transaction
 
@@ -679,24 +698,19 @@ class Transaction(Resource):
             result = braintree.Transaction.submit_for_partial_settlement("my_transaction_id", "20.00")
 
         """
-
+        if params is None:
+            params = {}
         return Configuration.gateway().transaction.submit_for_partial_settlement(transaction_id, amount, params)
 
     def __init__(self, gateway, attributes):
-        if "refund_id" in attributes:
-            self._refund_id = attributes["refund_id"]
-            del(attributes["refund_id"])
-        else:
-            self._refund_id = None
-
         Resource.__init__(self, gateway, attributes)
 
         self.amount = Decimal(self.amount)
-        if "tax_amount" in attributes and self.tax_amount:
+        if "tax_amount" in attributes and getattr(self, "tax_amount", None):
             self.tax_amount = Decimal(self.tax_amount)
-        if "discount_amount" in attributes and self.discount_amount:
+        if "discount_amount" in attributes and getattr(self, "discount_amount", None):
             self.discount_amount = Decimal(self.discount_amount)
-        if "shipping_amount" in attributes and self.shipping_amount:
+        if "shipping_amount" in attributes and getattr(self, "shipping_amount", None):
             self.shipping_amount = Decimal(self.shipping_amount)
         if "billing" in attributes:
             self.billing_details = Address(gateway, attributes.pop("billing"))
@@ -708,30 +722,33 @@ class Transaction(Resource):
             self.paypal_here_details = PayPalHere(gateway, attributes.pop("paypal_here"))
         if "local_payment" in attributes:
             self.local_payment_details = LocalPayment(gateway, attributes.pop("local_payment"))
+        if "sepa_debit_account_detail" in attributes:
+            self.sepa_direct_debit_account_details = SepaDirectDebitAccount(gateway, attributes.pop("sepa_debit_account_detail"))
         if "europe_bank_account" in attributes:
             self.europe_bank_account_details = EuropeBankAccount(gateway, attributes.pop("europe_bank_account"))
         if "us_bank_account" in attributes:
             self.us_bank_account = UsBankAccount(gateway, attributes.pop("us_bank_account"))
-        # NEXT_MAJOR_VERSION Remove this class as legacy Ideal has been removed/disabled in the Braintree Gateway
-        # DEPRECATED If you're looking to accept iDEAL as a payment method contact accounts@braintreepayments.com for a solution.
-        if "ideal_payment" in attributes:
-            self.ideal_payment_details = IdealPayment(gateway, attributes.pop("ideal_payment"))
         if "apple_pay" in attributes:
             self.apple_pay_details = ApplePayCard(gateway, attributes.pop("apple_pay"))
-        if "coinbase_account" in attributes:
-            self.coinbase_details = CoinbaseAccount(gateway, attributes.pop("coinbase_account"))
+        # NEXT_MAJOR_VERSION rename to google_pay_card_details
         if "android_pay_card" in attributes:
             self.android_pay_card_details = AndroidPayCard(gateway, attributes.pop("android_pay_card"))
+        # NEXT_MAJOR_VERSION remove amex express checkout
         if "amex_express_checkout_card" in attributes:
             self.amex_express_checkout_card_details = AmexExpressCheckoutCard(gateway, attributes.pop("amex_express_checkout_card"))
         if "venmo_account" in attributes:
             self.venmo_account_details = VenmoAccount(gateway, attributes.pop("venmo_account"))
         if "visa_checkout_card" in attributes:
             self.visa_checkout_card_details = VisaCheckoutCard(gateway, attributes.pop("visa_checkout_card"))
+        # NEXt_MAJOR_VERSION remove masterpass
         if "masterpass_card" in attributes:
             self.masterpass_card_details = MasterpassCard(gateway, attributes.pop("masterpass_card"))
         if "samsung_pay_card" in attributes:
             self.samsung_pay_card_details = SamsungPayCard(gateway, attributes.pop("samsung_pay_card"))
+        if "sca_exemption_requested" in attributes:
+            self.sca_exemption_requested = attributes.pop("sca_exemption_requested")
+        else:
+            self.sca_exemption_requested = None
         if "customer" in attributes:
             self.customer_details = Customer(gateway, attributes.pop("customer"))
         if "shipping" in attributes:
@@ -754,7 +771,6 @@ class Transaction(Resource):
             self.authorization_adjustments = [AuthorizationAdjustment(authorization_adjustment) for authorization_adjustment in self.authorization_adjustments]
         if "payment_instrument_type" in attributes:
             self.payment_instrument_type = attributes["payment_instrument_type"]
-
         if "risk_data" in attributes:
             self.risk_data = RiskData(attributes["risk_data"])
         else:
@@ -770,11 +786,6 @@ class Transaction(Resource):
         if "network_transaction_id" in attributes:
             self.network_transaction_id = attributes["network_transaction_id"]
 
-    @property
-    def refund_id(self):
-        warnings.warn("Please use Transaction.refund_ids instead", DeprecationWarning)
-        return self._refund_id
-
     @property
     def vault_billing_address(self):
         """
@@ -803,7 +814,7 @@ class Transaction(Resource):
 
     @property
     def is_disbursed(self):
-       return self.disbursement_details.is_valid
+        return self.disbursement_details.is_valid
 
     @property
     def line_items(self):
diff --git a/braintree/transaction_details.py b/braintree/transaction_details.py
index 3002e34..2cdc96a 100644
--- a/braintree/transaction_details.py
+++ b/braintree/transaction_details.py
@@ -5,5 +5,5 @@ class TransactionDetails(AttributeGetter):
     def __init__(self, attributes):
         AttributeGetter.__init__(self, attributes)
 
-        if self.amount is not None:
+        if getattr(self, "amount", None) is not None:
             self.amount = Decimal(self.amount)
diff --git a/braintree/transaction_gateway.py b/braintree/transaction_gateway.py
index ca7e4ce..258a7fa 100644
--- a/braintree/transaction_gateway.py
+++ b/braintree/transaction_gateway.py
@@ -1,18 +1,27 @@
 import braintree
+import warnings
 from braintree.error_result import ErrorResult
 from braintree.resource import Resource
 from braintree.resource_collection import ResourceCollection
 from braintree.successful_result import SuccessfulResult
 from braintree.transaction import Transaction
-from braintree.transparent_redirect import TransparentRedirect
 from braintree.exceptions.not_found_error import NotFoundError
-from braintree.exceptions.down_for_maintenance_error import DownForMaintenanceError
+from braintree.exceptions.request_timeout_error import RequestTimeoutError
+
 
 class TransactionGateway(object):
     def __init__(self, gateway):
         self.gateway = gateway
         self.config = gateway.config
 
+    def adjust_authorization(self, transaction_id, amount):
+        transaction_params = {"amount": amount}
+        response = self.config.http().put(self.config.base_merchant_path() + "/transactions/" + transaction_id + "/adjust_authorization", {"transaction": transaction_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"])
+
     def clone_transaction(self, transaction_id, params):
         Resource.verify_keys(params, Transaction.clone_signature())
         return self._post("/transactions/" + transaction_id + "/clone", {"transaction-clone": params})
@@ -24,14 +33,17 @@ class TransactionGateway(object):
         elif "api_error_response" in response:
             return ErrorResult(self.gateway, response["api_error_response"])
 
-    def confirm_transparent_redirect(self, query_string):
-        id = self.gateway.transparent_redirect._parse_and_validate_query_string(query_string)["id"][0]
-        return self._post("/transactions/all/confirm_transparent_redirect_request", {"id": id})
-
     def create(self, params):
         Resource.verify_keys(params, Transaction.create_signature())
+        self.__check_for_deprecated_attributes(params)
         return self._post("/transactions", {"transaction": params})
 
+    def credit(self, params):
+        if params is None:
+            params = {}
+        params["type"] = Transaction.Type.Credit
+        return self.create(params)
+
     def find(self, transaction_id):
         try:
             if transaction_id is None or transaction_id.strip() == "":
@@ -74,6 +86,8 @@ class TransactionGateway(object):
             return ErrorResult(self.gateway, response["api_error_response"])
 
     def sale(self, params):
+        if "recurring" in params.keys():
+            warnings.warn("Use transaction_source parameter instead", DeprecationWarning)
         params.update({"type": "sale"})
         return self.create(params)
 
@@ -85,7 +99,7 @@ class TransactionGateway(object):
         if "search_results" in response:
             return ResourceCollection(query, response, self.__fetch)
         else:
-            raise DownForMaintenanceError("search timeout")
+            raise RequestTimeoutError("search timeout")
 
     def release_from_escrow(self, transaction_id):
         response = self.config.http().put(self.config.base_merchant_path() + "/transactions/" + transaction_id + "/release_from_escrow", {})
@@ -94,7 +108,9 @@ class TransactionGateway(object):
         elif "api_error_response" in response:
             return ErrorResult(self.gateway, response["api_error_response"])
 
-    def submit_for_settlement(self, transaction_id, amount=None, params={}):
+    def submit_for_settlement(self, transaction_id, amount=None, params=None):
+        if params is None:
+            params = {}
         Resource.verify_keys(params, Transaction.submit_for_settlement_signature())
         transaction_params = {"amount": amount}
         transaction_params.update(params)
@@ -105,7 +121,9 @@ class TransactionGateway(object):
         elif "api_error_response" in response:
             return ErrorResult(self.gateway, response["api_error_response"])
 
-    def update_details(self, transaction_id, params={}):
+    def update_details(self, transaction_id, params=None):
+        if params is None:
+            params = {}
         Resource.verify_keys(params, Transaction.update_details_signature())
         response = self.config.http().put(self.config.base_merchant_path() + "/transactions/" + transaction_id + "/update_details",
                 {"transaction": params})
@@ -114,7 +132,9 @@ class TransactionGateway(object):
         elif "api_error_response" in response:
             return ErrorResult(self.gateway, response["api_error_response"])
 
-    def submit_for_partial_settlement(self, transaction_id, amount, params={}):
+    def submit_for_partial_settlement(self, transaction_id, amount, params=None):
+        if params is None:
+            params = {}
         Resource.verify_keys(params, Transaction.submit_for_settlement_signature())
         transaction_params = {"amount": amount}
         transaction_params.update(params)
@@ -125,25 +145,6 @@ class TransactionGateway(object):
         elif "api_error_response" in response:
             return ErrorResult(self.gateway, response["api_error_response"])
 
-    def tr_data_for_credit(self, tr_data, redirect_url):
-        if "transaction" not in tr_data:
-            tr_data["transaction"] = {}
-        tr_data["transaction"]["type"] = Transaction.Type.Credit
-        Resource.verify_keys(tr_data, [{"transaction": Transaction.create_signature()}])
-        tr_data["kind"] = TransparentRedirect.Kind.CreateTransaction
-        return self.gateway.transparent_redirect.tr_data(tr_data, redirect_url)
-
-    def tr_data_for_sale(self, tr_data, redirect_url):
-        if "transaction" not in tr_data:
-            tr_data["transaction"] = {}
-        tr_data["transaction"]["type"] = Transaction.Type.Sale
-        Resource.verify_keys(tr_data, [{"transaction": Transaction.create_signature()}])
-        tr_data["kind"] = TransparentRedirect.Kind.CreateTransaction
-        return self.gateway.transparent_redirect.tr_data(tr_data, redirect_url)
-
-    def transparent_redirect_create_url(self):
-        return self.config.base_url() + self.config.base_merchant_path() + "/transactions/all/create_via_transparent_redirect_request"
-
     def void(self, transaction_id):
         response = self.config.http().put(self.config.base_merchant_path() + "/transactions/" + transaction_id + "/void")
         if "transaction" in response:
@@ -158,7 +159,7 @@ class TransactionGateway(object):
         if "credit_card_transactions" in response:
             return [Transaction(self.gateway, item) for item in ResourceCollection._extract_as_array(response["credit_card_transactions"], "transaction")]
         else:
-            raise DownForMaintenanceError("search timeout")
+            raise RequestTimeoutError("search timeout")
 
     def __criteria(self, query):
         criteria = {}
@@ -169,10 +170,17 @@ class TransactionGateway(object):
                 criteria[term.name] = term.to_param()
         return criteria
 
-    def _post(self, url, params={}):
+    def _post(self, url, params=None):
+        if params is None:
+            params = {}
         response = self.config.http().post(self.config.base_merchant_path() + url, 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"])
 
+    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)
diff --git a/braintree/transaction_line_item.py b/braintree/transaction_line_item.py
index ce747a6..3e58ce9 100644
--- a/braintree/transaction_line_item.py
+++ b/braintree/transaction_line_item.py
@@ -8,6 +8,7 @@ from braintree.configuration import Configuration
 class TransactionLineItem(AttributeGetter):
     pass
 
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
     class Kind(object):
         """
         Constants representing transaction line item kinds. Available kinds are:
diff --git a/braintree/transaction_line_item_gateway.py b/braintree/transaction_line_item_gateway.py
index 0c234f2..670ada4 100644
--- a/braintree/transaction_line_item_gateway.py
+++ b/braintree/transaction_line_item_gateway.py
@@ -4,7 +4,7 @@ from braintree.resource import Resource
 from braintree.resource_collection import ResourceCollection
 from braintree.transaction_line_item import TransactionLineItem
 from braintree.exceptions.not_found_error import NotFoundError
-from braintree.exceptions.down_for_maintenance_error import DownForMaintenanceError
+from braintree.exceptions.request_timeout_error import RequestTimeoutError
 
 class TransactionLineItemGateway(object):
     def __init__(self, gateway):
@@ -19,6 +19,6 @@ class TransactionLineItemGateway(object):
             if "line_items" in response:
                 return [TransactionLineItem(item) for item in ResourceCollection._extract_as_array(response, "line_items")]
             else:
-                raise DownForMaintenanceError()
+                raise RequestTimeoutError()
         except NotFoundError:
             raise NotFoundError("transaction line items with id " + repr(transaction_id) + " not found")
diff --git a/braintree/transaction_review.py b/braintree/transaction_review.py
new file mode 100644
index 0000000..8207a74
--- /dev/null
+++ b/braintree/transaction_review.py
@@ -0,0 +1,9 @@
+from braintree.resource import Resource
+
+class TransactionReview(Resource):
+    """
+    A class representing a Transaction Review.
+    """
+
+    def __init__(self, attributes):
+        Resource.__init__(self, None, attributes)
diff --git a/braintree/transaction_search.py b/braintree/transaction_search.py
index 50d6998..649044d 100644
--- a/braintree/transaction_search.py
+++ b/braintree/transaction_search.py
@@ -42,7 +42,9 @@ class TransactionSearch:
     paypal_payer_email           = Search.TextNodeBuilder("paypal_payer_email")
     paypal_payment_id            = Search.TextNodeBuilder("paypal_payment_id")
     paypal_authorization_id      = Search.TextNodeBuilder("paypal_authorization_id")
+    sepa_debit_paypal_v2_order_id = Search.TextNodeBuilder("sepa_debit_paypal_v2_order_id")
     credit_card_unique_identifier = Search.TextNodeBuilder("credit_card_unique_identifier")
+    store_id                     = Search.TextNodeBuilder("store_id")
 
     credit_card_expiration_date  = Search.EqualityNodeBuilder("credit_card_expiration_date")
     credit_card_number           = Search.PartialMatchNodeBuilder("credit_card_number")
@@ -51,6 +53,7 @@ class TransactionSearch:
     ids                          = Search.MultipleValueNodeBuilder("ids")
     merchant_account_id          = Search.MultipleValueNodeBuilder("merchant_account_id")
     payment_instrument_type      = Search.MultipleValueNodeBuilder("payment_instrument_type")
+    store_ids                    = Search.MultipleValueNodeBuilder("store_ids")
 
     created_using = Search.MultipleValueNodeBuilder(
         "created_using",
@@ -93,3 +96,5 @@ class TransactionSearch:
     settled_at = Search.RangeNodeBuilder("settled_at")
     submitted_for_settlement_at = Search.RangeNodeBuilder("submitted_for_settlement_at")
     voided_at = Search.RangeNodeBuilder("voided_at")
+    ach_return_responses_created_at = Search.RangeNodeBuilder("ach_return_responses_created_at")
+    reason_code = Search.MultipleValueNodeBuilder('reason_code')
diff --git a/braintree/transparent_redirect.py b/braintree/transparent_redirect.py
deleted file mode 100644
index bdcbad6..0000000
--- a/braintree/transparent_redirect.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import braintree
-from braintree.configuration import Configuration
-
-class TransparentRedirect:
-    """
-    A class used for Transparent Redirect operations
-    """
-
-    class Kind(object):
-        CreateCustomer = "create_customer"
-        UpdateCustomer = "update_customer"
-        CreatePaymentMethod = "create_payment_method"
-        UpdatePaymentMethod = "update_payment_method"
-        CreateTransaction = "create_transaction"
-
-    @staticmethod
-    def confirm(query_string):
-        """
-        Confirms a transparent redirect request. It expects the query string from the
-        redirect request. The query string should _not_ include the leading "?" character. ::
-
-            result = braintree.TransparentRedirect.confirm("foo=bar&id=12345")
-        """
-        return Configuration.gateway().transparent_redirect.confirm(query_string)
-
-
-    @staticmethod
-    def tr_data(data, redirect_url):
-        return Configuration.gateway().transparent_redirect.tr_data(data, redirect_url)
-
-    @staticmethod
-    def url():
-        """
-        Returns the url for POSTing Transparent Redirect HTML forms
-        """
-        return Configuration.gateway().transparent_redirect.url()
-
diff --git a/braintree/transparent_redirect_gateway.py b/braintree/transparent_redirect_gateway.py
deleted file mode 100644
index ec6dde7..0000000
--- a/braintree/transparent_redirect_gateway.py
+++ /dev/null
@@ -1,79 +0,0 @@
-import cgi
-from datetime import datetime
-import braintree
-from braintree.util.crypto import Crypto
-from braintree.error_result import ErrorResult
-from braintree.exceptions.forged_query_string_error import ForgedQueryStringError
-from braintree.util.http import Http
-from braintree.signature_service import SignatureService
-from braintree.successful_result import SuccessfulResult
-from braintree.transparent_redirect import TransparentRedirect
-
-class TransparentRedirectGateway(object):
-    def __init__(self, gateway):
-        self.gateway = gateway
-        self.config = gateway.config
-
-    def confirm(self, query_string):
-        """
-        Confirms a transparent redirect request. It expects the query string from the
-        redirect request. The query string should _not_ include the leading "?" character. ::
-
-            result = braintree.TransparentRedirect.confirm("foo=bar&id=12345")
-        """
-        parsed_query_string = self._parse_and_validate_query_string(query_string)
-        confirmation_gateway = {
-            TransparentRedirect.Kind.CreateCustomer: "customer",
-            TransparentRedirect.Kind.UpdateCustomer: "customer",
-            TransparentRedirect.Kind.CreatePaymentMethod: "credit_card",
-            TransparentRedirect.Kind.UpdatePaymentMethod: "credit_card",
-            TransparentRedirect.Kind.CreateTransaction: "transaction"
-        }[parsed_query_string["kind"][0]]
-
-        return getattr(self.gateway, confirmation_gateway)._post("/transparent_redirect_requests/" + parsed_query_string["id"][0] + "/confirm")
-
-    def tr_data(self, data, redirect_url):
-        data = self.__flatten_dictionary(data)
-        date_string = datetime.utcnow().strftime("%Y%m%d%H%M%S")
-        data["time"] = date_string
-        data["redirect_url"] = redirect_url
-        data["public_key"] = self.config.public_key
-        data["api_version"] = self.config.api_version()
-
-        return SignatureService(self.config.private_key).sign(data)
-
-    def url(self):
-        """
-        Returns the url for POSTing Transparent Redirect HTML forms
-        """
-        return self.config.base_url() + self.config.base_merchant_path() + "/transparent_redirect_requests"
-
-    def _parse_and_validate_query_string(self, query_string):
-        query_params = cgi.parse_qs(query_string)
-        http_status = int(query_params["http_status"][0])
-        message = query_params.get("bt_message")
-        if message is not None:
-            message = message[0]
-
-        if Http.is_error_status(http_status):
-            Http.raise_exception_from_status(http_status, message)
-
-        if not self._is_valid_tr_query_string(query_string):
-            raise ForgedQueryStringError
-
-        return query_params
-
-    def _is_valid_tr_query_string(self, query_string):
-        content, hash = query_string.split("&hash=")
-        return hash == Crypto.sha1_hmac_hash(self.config.private_key, content)
-
-    def __flatten_dictionary(self, params, parent=None):
-        data = {}
-        for key, val in params.items():
-            full_key = parent + "[" + key + "]" if parent else key
-            if isinstance(val, dict):
-                data.update(self.__flatten_dictionary(val, full_key))
-            else:
-                data[full_key] = val
-        return data
-
diff --git a/braintree/us_bank_account_verification.py b/braintree/us_bank_account_verification.py
index 1a147f1..a727dc9 100644
--- a/braintree/us_bank_account_verification.py
+++ b/braintree/us_bank_account_verification.py
@@ -4,6 +4,7 @@ import braintree.us_bank_account
 
 class UsBankAccountVerification(AttributeGetter):
 
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
     class Status(object):
         """
         Constants representing transaction statuses. Available statuses are:
@@ -23,6 +24,7 @@ class UsBankAccountVerification(AttributeGetter):
         Verified               = "verified"
         Pending                = "pending"
 
+    # NEXT_MAJOR_VERSION this can be an enum! they were added as of python 3.4 and we support 3.5+
     class VerificationMethod(object):
         """
         Constants representing transaction statuses. Available statuses are:
diff --git a/braintree/us_bank_account_verification_search.py b/braintree/us_bank_account_verification_search.py
index 22d6e4f..c97dd98 100644
--- a/braintree/us_bank_account_verification_search.py
+++ b/braintree/us_bank_account_verification_search.py
@@ -29,5 +29,5 @@ class UsBankAccountVerificationSearch:
     # Equality fields
     account_type = Search.EqualityNodeBuilder("account_type")
 
-    # Ends-with fieds
+    # Ends-with fields
     account_number = Search.EndsWithNodeBuilder("account_number")
diff --git a/braintree/util/constants.py b/braintree/util/constants.py
index 0a3e845..d495c7d 100644
--- a/braintree/util/constants.py
+++ b/braintree/util/constants.py
@@ -1,3 +1,4 @@
+#NEXT_MAJOR_VERSION we can change all Constants to enums, not sure if we'll still need this
 class Constants(object):
     @staticmethod
     def get_all_constant_values_from_class(klass):
diff --git a/braintree/util/crypto.py b/braintree/util/crypto.py
index 24f741d..fa27525 100644
--- a/braintree/util/crypto.py
+++ b/braintree/util/crypto.py
@@ -2,10 +2,7 @@ import hashlib
 import hmac
 import sys
 
-if sys.version_info[0] == 2:
-    text_type = unicode
-else:
-    text_type = str
+text_type = str
 
 class Crypto:
     @staticmethod
diff --git a/braintree/util/generator.py b/braintree/util/generator.py
index fe08a29..895c00c 100644
--- a/braintree/util/generator.py
+++ b/braintree/util/generator.py
@@ -2,14 +2,9 @@ import datetime
 import sys
 from decimal import Decimal
 
-if sys.version_info[0] == 2:
-    integer_types = int, long
-    text_type = unicode
-    binary_type = str
-else:
-    integer_types = int,
-    text_type = str
-    binary_type = bytes
+integer_types = int
+text_type = str
+binary_type = bytes
 
 class Generator(object):
     def __init__(self, dict):
diff --git a/braintree/util/graphql_client.py b/braintree/util/graphql_client.py
index 6b2795a..b5bd615 100644
--- a/braintree/util/graphql_client.py
+++ b/braintree/util/graphql_client.py
@@ -2,7 +2,7 @@ import json
 
 from braintree.exceptions.authentication_error import AuthenticationError
 from braintree.exceptions.authorization_error import AuthorizationError
-from braintree.exceptions.down_for_maintenance_error import DownForMaintenanceError
+from braintree.exceptions.service_unavailable_error import ServiceUnavailableError
 from braintree.exceptions.not_found_error import NotFoundError
 from braintree.exceptions.server_error import ServerError
 from braintree.exceptions.too_many_requests_error import TooManyRequestsError
@@ -35,7 +35,7 @@ class GraphQLClient(Http):
                 elif error_type == "INTERNAL":
                     raise ServerError
                 elif error_type == "SERVICE_AVAILABILITY":
-                    raise DownForMaintenanceError
+                    raise ServiceUnavailableError
                 else:
                     raise UnexpectedError("Unexpected Response: " + error["message"])
 
diff --git a/braintree/util/http.py b/braintree/util/http.py
index eb2924d..131b36d 100644
--- a/braintree/util/http.py
+++ b/braintree/util/http.py
@@ -1,9 +1,6 @@
 import sys
 import requests
-if sys.version_info[0] == 2:
-    from base64 import encodestring as encodebytes
-else:
-    from base64 import encodebytes
+from base64 import encodebytes
 import json
 import braintree
 from braintree import version
@@ -11,17 +8,19 @@ from braintree.environment import Environment
 from braintree.util.xml_util import XmlUtil
 from braintree.exceptions.authentication_error import AuthenticationError
 from braintree.exceptions.authorization_error import AuthorizationError
-from braintree.exceptions.down_for_maintenance_error import DownForMaintenanceError
-from braintree.exceptions.not_found_error import NotFoundError
-from braintree.exceptions.server_error import ServerError
-from braintree.exceptions.too_many_requests_error import TooManyRequestsError
-from braintree.exceptions.upgrade_required_error import UpgradeRequiredError
-from braintree.exceptions.unexpected_error import UnexpectedError
+from braintree.exceptions.gateway_timeout_error import GatewayTimeoutError
 from braintree.exceptions.http.connection_error import ConnectionError
 from braintree.exceptions.http.invalid_response_error import InvalidResponseError
-from braintree.exceptions.http.timeout_error import TimeoutError
 from braintree.exceptions.http.timeout_error import ConnectTimeoutError
 from braintree.exceptions.http.timeout_error import ReadTimeoutError
+from braintree.exceptions.http.timeout_error import TimeoutError
+from braintree.exceptions.not_found_error import NotFoundError
+from braintree.exceptions.request_timeout_error import RequestTimeoutError
+from braintree.exceptions.server_error import ServerError
+from braintree.exceptions.service_unavailable_error import ServiceUnavailableError
+from braintree.exceptions.too_many_requests_error import TooManyRequestsError
+from braintree.exceptions.unexpected_error import UnexpectedError
+from braintree.exceptions.upgrade_required_error import UpgradeRequiredError
 
 class Http(object):
     class ContentType(object):
@@ -31,7 +30,7 @@ class Http(object):
 
     @staticmethod
     def is_error_status(status):
-        return status not in [200, 201, 422]
+        return status not in [200, 201, 204, 422]
 
     @staticmethod
     def raise_exception_from_status(status, message=None):
@@ -41,6 +40,8 @@ class Http(object):
             raise AuthorizationError(message)
         elif status == 404:
             raise NotFoundError()
+        elif status == 408:
+            raise RequestTimeoutError()
         elif status == 426:
             raise UpgradeRequiredError()
         elif status == 429:
@@ -48,7 +49,9 @@ class Http(object):
         elif status == 500:
             raise ServerError()
         elif status == 503:
-            raise DownForMaintenanceError()
+            raise ServiceUnavailableError()
+        elif status == 504:
+            raise GatewayTimeoutError()
         else:
             raise UnexpectedError("Unexpected HTTP_RESPONSE " + str(status))
 
@@ -75,8 +78,7 @@ class Http(object):
         http_strategy = self.config.http_strategy()
         headers = self.__headers(content_type, header_overrides)
         request_body = self.__request_body(content_type, params, files)
-
-        full_path = path if path.startswith(self.config.base_url()) or path.startswith(self.config.graphql_base_url()) else (self.config.base_url() + path)
+        full_path = self.__full_path(path)
 
         try:
             status, response_body = http_strategy.http_do(http_verb, full_path, headers, request_body)
@@ -100,6 +102,7 @@ class Http(object):
     def http_do(self, http_verb, path, headers, request_body):
         data = request_body
         files = None
+        full_path = self.__full_path(path)
 
         if type(request_body) is tuple:
             data = request_body[0]
@@ -110,14 +113,22 @@ class Http(object):
         else:
           verify = self.environment.ssl_certificate
 
-        response = self.__request_function(http_verb)(
-            path if path.startswith(self.config.base_url()) or path.startswith(self.config.graphql_base_url()) else (self.config.base_url() + path),
-            headers=headers,
-            data=data,
-            files=files,
-            verify=verify,
-            timeout=self.config.timeout
-        )
+        with requests.Session() as session:
+            request = requests.Request(
+                method=http_verb,
+                url=full_path,
+                headers=headers,
+                data=data,
+                files=files)
+            prepared_request = request.prepare()
+            prepared_request.url = full_path
+            # there's a bug in requests module that requires we manually update proxy settings,
+            # see https://github.com/psf/requests/issues/5677
+            session.proxies.update(requests.utils.getproxies())
+
+            response = session.send(prepared_request,
+                verify=verify,
+                timeout=self.config.timeout)
 
         return [response.status_code, response.text]
 
@@ -135,16 +146,6 @@ class Http(object):
         else:
             raise UnexpectedError(exception)
 
-    def __request_function(self, method):
-        if method == "GET":
-            return requests.get
-        elif method == "POST":
-            return requests.post
-        elif method == "PUT":
-            return requests.put
-        elif method == "DELETE":
-            return requests.delete
-
     def __authorization_header(self):
         if self.config.has_client_credentials():
             return b"Basic " + encodebytes(
@@ -185,3 +186,7 @@ class Http(object):
             return params
         else:
             return (params, files)
+
+    def __full_path(self, path):
+        return path if path.startswith(self.config.base_url()) or path.startswith(self.config.graphql_base_url()) else (self.config.base_url() + path)
+
diff --git a/braintree/util/parser.py b/braintree/util/parser.py
index 2dd7df2..1bbd0fb 100644
--- a/braintree/util/parser.py
+++ b/braintree/util/parser.py
@@ -4,16 +4,13 @@ from braintree.util.datetime_parser import parse_datetime
 import re
 import sys
 
-if sys.version_info[0] == 2:
-    binary_type = str
-else:
-    binary_type = bytes
+binary_type = bytes
 
 class Parser(object):
     def __init__(self, xml):
         if isinstance(xml, binary_type):
             xml = xml.decode('utf-8')
-        self.doc = minidom.parseString("><".join(re.split(">\s+<", xml)).strip())
+        self.doc = minidom.parseString("><".join(re.split(r">\s+<", xml)).strip())
 
     def parse(self):
         return {self.__underscored(self.doc.documentElement.tagName): self.__parse_node(self.doc.documentElement)}
diff --git a/braintree/validation_error_collection.py b/braintree/validation_error_collection.py
index 6b4f266..5235dc2 100644
--- a/braintree/validation_error_collection.py
+++ b/braintree/validation_error_collection.py
@@ -1,14 +1,17 @@
 from braintree.validation_error import ValidationError
 
+
 class ValidationErrorCollection(object):
     """
     A class representing a collection of validation errors.
 
-    For more information on ValidationErrors, see https://developers.braintreepayments.com/reference/general/validation-errors/overview/python
+    For more information on ValidationErrors, see https://developer.paypal.com/braintree/docs/reference/general/validation-errors/overview/python
 
     """
 
-    def __init__(self, data={"errors": []}):
+    def __init__(self, data=None):
+        if data is None:
+            data = {"errors": []}
         self.data = data
 
     @property
@@ -87,7 +90,7 @@ class ValidationErrorCollection(object):
     def __nested_errors(self):
         nested_errors = {}
         for key in self.data:
-            if key == "errors": continue
+            if key == "errors":
+                continue
             nested_errors[key] = ValidationErrorCollection(self.data[key])
         return nested_errors
-
diff --git a/braintree/venmo_profile_data.py b/braintree/venmo_profile_data.py
new file mode 100644
index 0000000..64ccd90
--- /dev/null
+++ b/braintree/venmo_profile_data.py
@@ -0,0 +1,9 @@
+from braintree.resource import Resource
+
+class VenmoProfileData(Resource):
+    """
+    A class representing Braintree VenmoProfileData object.
+    """
+    def __init__(self, gateway, attributes):
+        Resource.__init__(self, gateway, attributes)
+
diff --git a/braintree/version.py b/braintree/version.py
index a681e20..15dd307 100644
--- a/braintree/version.py
+++ b/braintree/version.py
@@ -1 +1 @@
-Version = "3.57.1"
+Version = "4.19.0"
diff --git a/braintree/webhook_notification.py b/braintree/webhook_notification.py
index ed909d7..7797683 100644
--- a/braintree/webhook_notification.py
+++ b/braintree/webhook_notification.py
@@ -1,34 +1,56 @@
-from braintree.resource import Resource
+from braintree.account_updater_daily_report import AccountUpdaterDailyReport
 from braintree.configuration import Configuration
-from braintree.subscription import Subscription
-from braintree.merchant_account import MerchantAccount
-from braintree.transaction import Transaction
-from braintree.partner_merchant import PartnerMerchant
-from braintree.oauth_access_revocation import OAuthAccessRevocation
+from braintree.connected_merchant_paypal_status_changed import ConnectedMerchantPayPalStatusChanged
+from braintree.connected_merchant_status_transitioned import ConnectedMerchantStatusTransitioned
 from braintree.disbursement import Disbursement
 from braintree.dispute import Dispute
-from braintree.account_updater_daily_report import AccountUpdaterDailyReport
 from braintree.error_result import ErrorResult
-from braintree.validation_error_collection import ValidationErrorCollection
-from braintree.connected_merchant_paypal_status_changed import ConnectedMerchantPayPalStatusChanged
-from braintree.connected_merchant_status_transitioned import ConnectedMerchantStatusTransitioned
-# NEXT_MAJOR_VERSION Remove this class as legacy Ideal has been removed/disabled in the Braintree Gateway
-# DEPRECATED If you're looking to accept iDEAL as a payment method contact accounts@braintreepayments.com for a solution.
-from braintree.ideal_payment import IdealPayment
 from braintree.granted_payment_instrument_update import GrantedPaymentInstrumentUpdate
-from braintree.revoked_payment_method_metadata import RevokedPaymentMethodMetadata
 from braintree.local_payment_completed import LocalPaymentCompleted
+from braintree.local_payment_expired import LocalPaymentExpired
+from braintree.local_payment_funded import LocalPaymentFunded
+from braintree.local_payment_reversed import LocalPaymentReversed
+from braintree.merchant_account import MerchantAccount
+from braintree.oauth_access_revocation import OAuthAccessRevocation
+from braintree.partner_merchant import PartnerMerchant
+from braintree.payment_method_customer_data_updated_metadata import PaymentMethodCustomerDataUpdatedMetadata
+from braintree.resource import Resource
+from braintree.revoked_payment_method_metadata import RevokedPaymentMethodMetadata
+from braintree.subscription import Subscription
+from braintree.transaction import Transaction
+from braintree.transaction_review import TransactionReview
+from braintree.validation_error_collection import ValidationErrorCollection
 
 class WebhookNotification(Resource):
     class Kind(object):
         AccountUpdaterDailyReport = "account_updater_daily_report"
         Check = "check"
-        ConnectedMerchantStatusTransitioned = "connected_merchant_status_transitioned"
         ConnectedMerchantPayPalStatusChanged = "connected_merchant_paypal_status_changed"
+        ConnectedMerchantStatusTransitioned = "connected_merchant_status_transitioned"
+        Disbursement = "disbursement"
+        DisbursementException = "disbursement_exception"
+        DisputeAccepted = "dispute_accepted"
+        DisputeAutoAccepted = "dispute_auto_accepted"
+        DisputeDisputed = "dispute_disputed"
+        DisputeExpired = "dispute_expired"
+        DisputeLost = "dispute_lost"
+        DisputeOpened = "dispute_opened"
+        DisputeWon = "dispute_won"
+        GrantedPaymentMethodRevoked = "granted_payment_method_revoked"
+        GrantorUpdatedGrantedPaymentMethod = "grantor_updated_granted_payment_method"
+        LocalPaymentCompleted = "local_payment_completed"
+        LocalPaymentExpired = "local_payment_expired"
+        LocalPaymentFunded = "local_payment_funded"
+        LocalPaymentReversed = "local_payment_reversed"
+        OAuthAccessRevoked = "oauth_access_revoked"
         PartnerMerchantConnected = "partner_merchant_connected"
-        PartnerMerchantDisconnected = "partner_merchant_disconnected"
         PartnerMerchantDeclined = "partner_merchant_declined"
-        OAuthAccessRevoked = "oauth_access_revoked"
+        PartnerMerchantDisconnected = "partner_merchant_disconnected"
+        PaymentMethodCustomerDataUpdated = "payment_method_customer_data_updated"
+        PaymentMethodRevokedByCustomer = "payment_method_revoked_by_customer"
+        RecipientUpdatedGrantedPaymentMethod = "recipient_updated_granted_payment_method"
+        SubMerchantAccountApproved = "sub_merchant_account_approved"
+        SubMerchantAccountDeclined = "sub_merchant_account_declined"
         SubscriptionCanceled = "subscription_canceled"
         SubscriptionChargedSuccessfully = "subscription_charged_successfully"
         SubscriptionChargedUnsuccessfully = "subscription_charged_unsuccessfully"
@@ -36,28 +58,10 @@ class WebhookNotification(Resource):
         SubscriptionTrialEnded = "subscription_trial_ended"
         SubscriptionWentActive = "subscription_went_active"
         SubscriptionWentPastDue = "subscription_went_past_due"
-        SubMerchantAccountApproved = "sub_merchant_account_approved"
-        SubMerchantAccountDeclined = "sub_merchant_account_declined"
         TransactionDisbursed = "transaction_disbursed"
+        TransactionReviewed = "transaction_reviewed"
         TransactionSettled = "transaction_settled"
         TransactionSettlementDeclined = "transaction_settlement_declined"
-        DisbursementException = "disbursement_exception"
-        Disbursement = "disbursement"
-        DisputeOpened = "dispute_opened"
-        DisputeLost = "dispute_lost"
-        DisputeWon = "dispute_won"
-        # NEXT_MAJOR_VERSION Remove this class as legacy Ideal has been removed/disabled in the Braintree Gateway
-        # DEPRECATED If you're looking to accept iDEAL as a payment method contact accounts@braintreepayments.com for a solution.
-        IdealPaymentComplete = "ideal_payment_complete"
-        IdealPaymentFailed = "ideal_payment_failed"
-        # NEXT_MAJOR_VERSION remove GrantedPaymentInstrumentUpdate. Kind is not sent by Braintree Gateway.
-        # Kind will either be GrantorUpdatedGrantedPaymentMethod or RecipientUpdatedGrantedPaymentMethod.
-        GrantedPaymentInstrumentUpdate = "granted_payment_instrument_update"
-        GrantorUpdatedGrantedPaymentMethod = "grantor_updated_granted_payment_method"
-        RecipientUpdatedGrantedPaymentMethod = "recipient_updated_granted_payment_method"
-        GrantedPaymentMethodRevoked = "granted_payment_method_revoked"
-        PaymentMethodRevokedByCustomer = "payment_method_revoked_by_customer"
-        LocalPaymentCompleted = "local_payment_completed"
 
     @staticmethod
     def parse(signature, payload):
@@ -84,6 +88,8 @@ class WebhookNotification(Resource):
             self.merchant_account = MerchantAccount(gateway, node_wrapper['merchant_account'])
         elif "transaction" in node_wrapper:
             self.transaction = Transaction(gateway, node_wrapper['transaction'])
+        elif "transaction_review" in node_wrapper:
+            self.transaction_review = TransactionReview(node_wrapper['transaction_review'])
         elif "connected_merchant_status_transitioned" in node_wrapper:
             self.connected_merchant_status_transitioned = ConnectedMerchantStatusTransitioned(gateway, node_wrapper['connected_merchant_status_transitioned'])
         elif "connected_merchant_paypal_status_changed" in node_wrapper:
@@ -98,16 +104,20 @@ class WebhookNotification(Resource):
             self.dispute = Dispute(node_wrapper['dispute'])
         elif "account_updater_daily_report" in node_wrapper:
             self.account_updater_daily_report = AccountUpdaterDailyReport(gateway, node_wrapper['account_updater_daily_report'])
-        # NEXT_MAJOR_VERSION Remove this class as legacy Ideal has been removed/disabled in the Braintree Gateway
-        # DEPRECATED If you're looking to accept iDEAL as a payment method contact accounts@braintreepayments.com for a solution.
-        elif "ideal_payment" in node_wrapper:
-            self.ideal_payment = IdealPayment(gateway, node_wrapper['ideal_payment'])
         elif "granted_payment_instrument_update" in node_wrapper:
             self.granted_payment_instrument_update = GrantedPaymentInstrumentUpdate(gateway, node_wrapper["granted_payment_instrument_update"])
         elif attributes["kind"] in [WebhookNotification.Kind.GrantedPaymentMethodRevoked, WebhookNotification.Kind.PaymentMethodRevokedByCustomer]:
             self.revoked_payment_method_metadata = RevokedPaymentMethodMetadata(gateway, node_wrapper)
-        elif "local_payment" in node_wrapper:
+        elif "local_payment" in node_wrapper and attributes["kind"] == WebhookNotification.Kind.LocalPaymentCompleted:
             self.local_payment_completed = LocalPaymentCompleted(gateway, node_wrapper["local_payment"])
+        elif "local_payment_expired" in node_wrapper and attributes["kind"] == WebhookNotification.Kind.LocalPaymentExpired:
+            self.local_payment_expired = LocalPaymentExpired(gateway, node_wrapper["local_payment_expired"])
+        elif "local_payment_funded" in node_wrapper and attributes["kind"] == WebhookNotification.Kind.LocalPaymentFunded:
+            self.local_payment_funded = LocalPaymentFunded(gateway, node_wrapper["local_payment_funded"])
+        elif "local_payment_reversed" in node_wrapper and attributes["kind"] == WebhookNotification.Kind.LocalPaymentReversed:
+            self.local_payment_reversed = LocalPaymentReversed(gateway, node_wrapper["local_payment_reversed"])
+        elif "payment_method_customer_data_updated_metadata" in node_wrapper and attributes["kind"] == WebhookNotification.Kind.PaymentMethodCustomerDataUpdated:
+            self.payment_method_customer_data_updated_metadata = PaymentMethodCustomerDataUpdatedMetadata(gateway, node_wrapper["payment_method_customer_data_updated_metadata"])
 
         if "errors" in node_wrapper:
             self.errors = ValidationErrorCollection(node_wrapper['errors'])
diff --git a/braintree/webhook_notification_gateway.py b/braintree/webhook_notification_gateway.py
index 5e2ca99..20fb297 100644
--- a/braintree/webhook_notification_gateway.py
+++ b/braintree/webhook_notification_gateway.py
@@ -1,9 +1,6 @@
 import re
 import sys
-if sys.version_info[0] == 2:
-    from base64 import decodestring as decodebytes
-else:
-    from base64 import decodebytes
+from base64 import decodebytes
 import sys
 from braintree.exceptions.invalid_signature_error import InvalidSignatureError
 from braintree.exceptions.invalid_challenge_error import InvalidChallengeError
@@ -11,10 +8,7 @@ from braintree.util.crypto import Crypto
 from braintree.util.xml_util import XmlUtil
 from braintree.webhook_notification import WebhookNotification
 
-if sys.version_info[0] == 2:
-    text_type = unicode
-else:
-    text_type = str
+text_type = str
 
 class WebhookNotificationGateway(object):
     def __init__(self, gateway):
diff --git a/braintree/webhook_testing_gateway.py b/braintree/webhook_testing_gateway.py
index f715a4a..e0ec4ed 100644
--- a/braintree/webhook_testing_gateway.py
+++ b/braintree/webhook_testing_gateway.py
@@ -1,10 +1,7 @@
 from braintree.util.crypto import Crypto
 from braintree.webhook_notification import WebhookNotification
 import sys
-if sys.version_info[0] == 2:
-    from base64 import encodestring as encodebytes
-else:
-    from base64 import encodebytes
+from base64 import encodebytes
 from datetime import datetime
 
 class WebhookTestingGateway(object):
@@ -48,6 +45,8 @@ class WebhookTestingGateway(object):
             return self.__merchant_account_declined_sample_xml(id)
         elif kind == WebhookNotification.Kind.TransactionDisbursed:
             return self.__transaction_disbursed_sample_xml(id)
+        elif kind == WebhookNotification.Kind.TransactionReviewed:
+            return self.__transaction_reviewed_sample_xml(id)
         elif kind == WebhookNotification.Kind.TransactionSettled:
             return self.__transaction_settled_sample_xml(id)
         elif kind == WebhookNotification.Kind.TransactionSettlementDeclined:
@@ -70,31 +69,38 @@ class WebhookTestingGateway(object):
             return self.__dispute_lost_sample_xml(id)
         elif kind == WebhookNotification.Kind.DisputeWon:
             return self.__dispute_won_sample_xml(id)
+        elif kind == WebhookNotification.Kind.DisputeAccepted:
+            return self.__dispute_accepted_sample_xml(id)
+        elif kind == WebhookNotification.Kind.DisputeAutoAccepted:
+            return self.__dispute_auto_accepted_sample_xml(id)
+        elif kind == WebhookNotification.Kind.DisputeDisputed:
+            return self.__dispute_disputed_sample_xml(id)
+        elif kind == WebhookNotification.Kind.DisputeExpired:
+            return self.__dispute_expired_sample_xml(id)
         elif kind == WebhookNotification.Kind.SubscriptionChargedSuccessfully:
             return self.__subscription_charged_successfully_sample_xml(id)
         elif kind == WebhookNotification.Kind.SubscriptionChargedUnsuccessfully:
             return self.__subscription_charged_unsuccessfully_sample_xml(id)
         elif kind == WebhookNotification.Kind.AccountUpdaterDailyReport:
             return self.__account_updater_daily_report_sample_xml()
-        # NEXT_MAJOR_VERSION Remove this class as legacy Ideal has been removed/disabled in the Braintree Gateway
-        # DEPRECATED If you're looking to accept iDEAL as a payment method contact accounts@braintreepayments.com for a solution.
-        elif kind == WebhookNotification.Kind.IdealPaymentComplete:
-            return self.__ideal_payment_complete_sample_xml(id)
-        # NEXT_MAJOR_VERSION Remove this class as legacy Ideal has been removed/disabled in the Braintree Gateway
-        # DEPRECATED If you're looking to accept iDEAL as a payment method contact accounts@braintreepayments.com for a solution.
-        elif kind == WebhookNotification.Kind.IdealPaymentFailed:
-            return self.__ideal_payment_failed_sample_xml(id)
-        # NEXT_MAJOR_VERSION remove GrantedPaymentInstrumentUpdate
-        elif kind == WebhookNotification.Kind.GrantedPaymentInstrumentUpdate:
-            return self.__granted_payment_instrument_update()
         elif kind == WebhookNotification.Kind.GrantorUpdatedGrantedPaymentMethod:
             return self.__granted_payment_instrument_update()
         elif kind == WebhookNotification.Kind.RecipientUpdatedGrantedPaymentMethod:
             return self.__granted_payment_instrument_update()
         elif kind == WebhookNotification.Kind.PaymentMethodRevokedByCustomer:
             return self.__payment_method_revoked_by_customer(id)
+        elif kind == WebhookNotification.Kind.GrantedPaymentMethodRevoked:
+            return self.__granted_payment_method_revoked(id)
         elif kind == WebhookNotification.Kind.LocalPaymentCompleted:
             return self.__local_payment_completed()
+        elif kind == WebhookNotification.Kind.LocalPaymentExpired:
+            return self.__local_payment_expired()
+        elif kind == WebhookNotification.Kind.LocalPaymentFunded:
+            return self.__local_payment_funded()
+        elif kind == WebhookNotification.Kind.LocalPaymentReversed:
+            return self.__local_payment_reversed()
+        elif kind == WebhookNotification.Kind.PaymentMethodCustomerDataUpdated:
+            return self.__payment_method_customer_data_updated_sample_xml(id)
         else:
             return self.__subscription_sample_xml(id)
 
@@ -119,6 +125,17 @@ class WebhookTestingGateway(object):
             </transaction>
         """ % id
 
+    def __transaction_reviewed_sample_xml(self, id):
+        return """
+            <transaction-review>
+              <transaction-id>%s</transaction-id>
+              <decision>a smart decision</decision>
+              <reviewer-email>hey@girl.com</reviewer-email>
+              <reviewer-note>I reviewed this</reviewer-note>
+              <reviewed-time type="datetime">2021-04-20T06:09:00Z</reviewed-time>
+            </transaction-review>
+        """ % id
+
     def __transaction_settled_sample_xml(self, id):
         return """
             <transaction>
@@ -223,6 +240,30 @@ class WebhookTestingGateway(object):
         else:
             return self.__new_dispute_won_sample_xml(id)
 
+    def __dispute_accepted_sample_xml(self, id):
+        if id == "legacy_dispute_id":
+            return self.__old_dispute_accepted_sample_xml(id)
+        else:
+            return self.__new_dispute_accepted_sample_xml(id)
+
+    def __dispute_auto_accepted_sample_xml(self, id):
+        if id == "legacy_dispute_id":
+            return self.__old_dispute_auto_accepted_sample_xml(id)
+        else:
+            return self.__new_dispute_auto_accepted_sample_xml(id)
+
+    def __dispute_disputed_sample_xml(self, id):
+        if id == "legacy_dispute_id":
+            return self.__old_dispute_disputed_sample_xml(id)
+        else:
+            return self.__new_dispute_disputed_sample_xml(id)
+
+    def __dispute_expired_sample_xml(self, id):
+        if id == "legacy_dispute_id":
+            return self.__old_dispute_expired_sample_xml(id)
+        else:
+            return self.__new_dispute_expired_sample_xml(id)
+
     def __old_dispute_opened_sample_xml(self, id):
         return """
             <dispute>
@@ -281,6 +322,82 @@ class WebhookTestingGateway(object):
             </dispute>
         """ % (id, id)
 
+    def __old_dispute_accepted_sample_xml(self, id):
+        return """
+            <dispute>
+              <amount>250.00</amount>
+              <currency-iso-code>USD</currency-iso-code>
+              <received-date type="date">2014-03-01</received-date>
+              <reply-by-date type="date">2014-03-21</reply-by-date>
+              <kind>chargeback</kind>
+              <status>accepted</status>
+              <reason>fraud</reason>
+              <id>%s</id>
+              <transaction>
+                <id>%s</id>
+                <amount>250.00</amount>
+              </transaction>
+              <date-opened type="date">2014-03-28</date-opened>
+            </dispute>
+        """ % (id, id)
+
+    def __old_dispute_auto_accepted_sample_xml(self, id):
+        return """
+            <dispute>
+              <amount>250.00</amount>
+              <currency-iso-code>USD</currency-iso-code>
+              <received-date type="date">2014-03-01</received-date>
+              <reply-by-date type="date">2014-03-21</reply-by-date>
+              <kind>chargeback</kind>
+              <status>auto_accepted</status>
+              <reason>fraud</reason>
+              <id>%s</id>
+              <transaction>
+                <id>%s</id>
+                <amount>250.00</amount>
+              </transaction>
+              <date-opened type="date">2014-03-28</date-opened>
+            </dispute>
+        """ % (id, id)
+
+    def __old_dispute_disputed_sample_xml(self, id):
+        return """
+            <dispute>
+              <amount>250.00</amount>
+              <currency-iso-code>USD</currency-iso-code>
+              <received-date type="date">2014-03-01</received-date>
+              <reply-by-date type="date">2014-03-21</reply-by-date>
+              <kind>chargeback</kind>
+              <status>disputed</status>
+              <reason>fraud</reason>
+              <id>%s</id>
+              <transaction>
+                <id>%s</id>
+                <amount>250.00</amount>
+              </transaction>
+              <date-opened type="date">2014-03-28</date-opened>
+            </dispute>
+        """ % (id, id)
+
+    def __old_dispute_expired_sample_xml(self, id):
+        return """
+            <dispute>
+              <amount>250.00</amount>
+              <currency-iso-code>USD</currency-iso-code>
+              <received-date type="date">2014-03-01</received-date>
+              <reply-by-date type="date">2014-03-21</reply-by-date>
+              <kind>chargeback</kind>
+              <status>expired</status>
+              <reason>fraud</reason>
+              <id>%s</id>
+              <transaction>
+                <id>%s</id>
+                <amount>250.00</amount>
+              </transaction>
+              <date-opened type="date">2014-03-28</date-opened>
+            </dispute>
+        """ % (id, id)
+
     def __new_dispute_opened_sample_xml(self, id):
         return """
         <dispute>
@@ -443,6 +560,186 @@ class WebhookTestingGateway(object):
         </dispute>
         """ % (id, id)
 
+    def __new_dispute_accepted_sample_xml(self, id):
+        return """
+        <dispute>
+          <id>%s</id>
+          <amount>100.00</amount>
+          <amount-disputed>100.00</amount-disputed>
+          <amount-won>95.00</amount-won>
+          <case-number>CASE-12345</case-number>
+          <created-at type="datetime">2017-06-16T20:44:41Z</created-at>
+          <currency-iso-code>USD</currency-iso-code>
+          <forwarded-comments nil="true"/>
+          <kind>chargeback</kind>
+          <merchant-account-id>ytnlulaloidoqwvzxjrdqputg</merchant-account-id>
+          <reason>fraud</reason>
+          <reason-code nil="true"/>
+          <reason-description nil="true"/>
+          <received-date type="date">2016-02-15</received-date>
+          <reference-number>REF-9876</reference-number>
+          <reply-by-date type="date">2016-02-22</reply-by-date>
+          <status>accepted</status>
+          <updated-at type="datetime">2017-06-16T20:44:41Z</updated-at>
+          <original-dispute-id>9qde5qgp</original-dispute-id>
+          <status-history type="array">
+            <status-history>
+              <status>open</status>
+              <timestamp type="datetime">2017-06-15T20:44:41Z</timestamp>
+            </status-history>
+            <status-history>
+              <status>accepted</status>
+              <timestamp type="datetime">2017-06-16T20:44:41Z</timestamp>
+            </status-history>
+          </status-history>
+          <evidence type="array"/>
+          <transaction>
+            <id>%s</id>
+            <amount>100.00</amount>
+            <created-at>2017-06-21T20:44:41Z</created-at>
+            <order-id nil="true"/>
+            <purchase-order-number nil="true"/>
+            <payment-instrument-subtype>Visa</payment-instrument-subtype>
+          </transaction>
+          <date-opened type=\"date\">2014-03-28</date-opened>
+        </dispute>
+        """ % (id, id)
+
+    def __new_dispute_auto_accepted_sample_xml(self, id):
+        return """
+        <dispute>
+          <id>%s</id>
+          <amount>100.00</amount>
+          <amount-disputed>100.00</amount-disputed>
+          <amount-won>95.00</amount-won>
+          <case-number>CASE-12345</case-number>
+          <created-at type="datetime">2017-06-16T20:44:41Z</created-at>
+          <currency-iso-code>USD</currency-iso-code>
+          <forwarded-comments nil="true"/>
+          <kind>chargeback</kind>
+          <merchant-account-id>ytnlulaloidoqwvzxjrdqputg</merchant-account-id>
+          <reason>fraud</reason>
+          <reason-code nil="true"/>
+          <reason-description nil="true"/>
+          <received-date type="date">2016-02-15</received-date>
+          <reference-number>REF-9876</reference-number>
+          <reply-by-date type="date">2016-02-22</reply-by-date>
+          <status>auto_accepted</status>
+          <updated-at type="datetime">2017-06-16T20:44:41Z</updated-at>
+          <original-dispute-id>9qde5qgp</original-dispute-id>
+          <status-history type="array">
+            <status-history>
+              <status>open</status>
+              <timestamp type="datetime">2017-06-15T20:44:41Z</timestamp>
+            </status-history>
+            <status-history>
+              <status>auto_accepted</status>
+              <timestamp type="datetime">2017-06-16T20:44:41Z</timestamp>
+            </status-history>
+          </status-history>
+          <evidence type="array"/>
+          <transaction>
+            <id>%s</id>
+            <amount>100.00</amount>
+            <created-at>2017-06-21T20:44:41Z</created-at>
+            <order-id nil="true"/>
+            <purchase-order-number nil="true"/>
+            <payment-instrument-subtype>Visa</payment-instrument-subtype>
+          </transaction>
+          <date-opened type=\"date\">2014-03-28</date-opened>
+        </dispute>
+        """ % (id, id)
+
+    def __new_dispute_disputed_sample_xml(self, id):
+        return """
+        <dispute>
+          <id>%s</id>
+          <amount>100.00</amount>
+          <amount-disputed>100.00</amount-disputed>
+          <amount-won>95.00</amount-won>
+          <case-number>CASE-12345</case-number>
+          <created-at type="datetime">2017-06-16T20:44:41Z</created-at>
+          <currency-iso-code>USD</currency-iso-code>
+          <forwarded-comments nil="true"/>
+          <kind>chargeback</kind>
+          <merchant-account-id>ytnlulaloidoqwvzxjrdqputg</merchant-account-id>
+          <reason>fraud</reason>
+          <reason-code nil="true"/>
+          <reason-description nil="true"/>
+          <received-date type="date">2016-02-15</received-date>
+          <reference-number>REF-9876</reference-number>
+          <reply-by-date type="date">2016-02-22</reply-by-date>
+          <status>disputed</status>
+          <updated-at type="datetime">2017-06-16T20:44:41Z</updated-at>
+          <original-dispute-id>9qde5qgp</original-dispute-id>
+          <status-history type="array">
+            <status-history>
+              <status>open</status>
+              <timestamp type="datetime">2017-06-15T20:44:41Z</timestamp>
+            </status-history>
+            <status-history>
+              <status>disputed</status>
+              <timestamp type="datetime">2017-06-16T20:44:41Z</timestamp>
+            </status-history>
+          </status-history>
+          <evidence type="array"/>
+          <transaction>
+            <id>%s</id>
+            <amount>100.00</amount>
+            <created-at>2017-06-21T20:44:41Z</created-at>
+            <order-id nil="true"/>
+            <purchase-order-number nil="true"/>
+            <payment-instrument-subtype>Visa</payment-instrument-subtype>
+          </transaction>
+          <date-opened type=\"date\">2014-03-28</date-opened>
+        </dispute>
+        """ % (id, id)
+
+    def __new_dispute_expired_sample_xml(self, id):
+        return """
+        <dispute>
+          <id>%s</id>
+          <amount>100.00</amount>
+          <amount-disputed>100.00</amount-disputed>
+          <amount-won>95.00</amount-won>
+          <case-number>CASE-12345</case-number>
+          <created-at type="datetime">2017-06-16T20:44:41Z</created-at>
+          <currency-iso-code>USD</currency-iso-code>
+          <forwarded-comments nil="true"/>
+          <kind>chargeback</kind>
+          <merchant-account-id>ytnlulaloidoqwvzxjrdqputg</merchant-account-id>
+          <reason>fraud</reason>
+          <reason-code nil="true"/>
+          <reason-description nil="true"/>
+          <received-date type="date">2016-02-15</received-date>
+          <reference-number>REF-9876</reference-number>
+          <reply-by-date type="date">2016-02-22</reply-by-date>
+          <status>expired</status>
+          <updated-at type="datetime">2017-06-16T20:44:41Z</updated-at>
+          <original-dispute-id>9qde5qgp</original-dispute-id>
+          <status-history type="array">
+            <status-history>
+              <status>open</status>
+              <timestamp type="datetime">2017-06-15T20:44:41Z</timestamp>
+            </status-history>
+            <status-history>
+              <status>expired</status>
+              <timestamp type="datetime">2017-06-25T20:44:41Z</timestamp>
+            </status-history>
+          </status-history>
+          <evidence type="array"/>
+          <transaction>
+            <id>%s</id>
+            <amount>100.00</amount>
+            <created-at>2017-06-21T20:44:41Z</created-at>
+            <order-id nil="true"/>
+            <purchase-order-number nil="true"/>
+            <payment-instrument-subtype>Visa</payment-instrument-subtype>
+          </transaction>
+          <date-opened type=\"date\">2014-03-28</date-opened>
+        </dispute>
+        """ % (id, id)
+
     def __subscription_sample_xml(self, id):
         return """
             <subscription>
@@ -585,40 +882,6 @@ class WebhookTestingGateway(object):
             </account-updater-daily-report>
             """
 
-    # NEXT_MAJOR_VERSION Remove this class as legacy Ideal has been removed/disabled in the Braintree Gateway
-    # DEPRECATED If you're looking to accept iDEAL as a payment method contact accounts@braintreepayments.com for a solution.
-    def __ideal_payment_complete_sample_xml(self, id):
-        return """
-            <ideal-payment>
-                <id>%s</id>
-                <status>COMPLETE</status>
-                <issuer>ABCISSUER</issuer>
-                <order-id>ORDERABC</order-id>
-                <currency>EUR</currency>
-                <amount>10.00</amount>
-                <created-at>2016-11-29T23:27:34.547Z</created-at>
-                <approval-url>https://example.com</approval-url>
-                <ideal-transaction-id>1234567890</ideal-transaction-id>
-            </ideal-payment>
-            """ % id
-
-    # NEXT_MAJOR_VERSION Remove this class as legacy Ideal has been removed/disabled in the Braintree Gateway
-    # DEPRECATED If you're looking to accept iDEAL as a payment method contact accounts@braintreepayments.com for a solution.
-    def __ideal_payment_failed_sample_xml(self, id):
-        return """
-            <ideal-payment>
-                <id>%s</id>
-                <status>FAILED</status>
-                <issuer>ABCISSUER</issuer>
-                <order-id>ORDERABC</order-id>
-                <currency>EUR</currency>
-                <amount>10.00</amount>
-                <created-at>2016-11-29T23:27:34.547Z</created-at>
-                <approval-url>https://example.com</approval-url>
-                <ideal-transaction-id>1234567890</ideal-transaction-id>
-            </ideal-payment>
-            """ % id
-
     def __granted_payment_instrument_update(self):
         return """
             <granted-payment-instrument-update>
@@ -637,6 +900,9 @@ class WebhookTestingGateway(object):
             </granted-payment-instrument-update>
             """
 
+    def __granted_payment_method_revoked(self, id):
+        return self.__venmo_account_xml(id)
+
     def __payment_method_revoked_by_customer(self, id):
         return """
             <paypal-account>
@@ -672,3 +938,70 @@ class WebhookTestingGateway(object):
                 </transaction>
             </local-payment>
             """
+
+    def __local_payment_expired(self):
+        return """
+            <local-payment-expired>
+                <payment-id>a-payment-id</payment-id>
+                <payment-context-id>a-context-payment-id</payment-context-id>
+            </local-payment-expired>
+            """
+
+    def __local_payment_funded(self):
+        return """
+            <local-payment-funded>
+                <payment-id>a-payment-id</payment-id>
+                <payment-context-id>a-context-payment-id</payment-context-id>
+                <transaction>
+                    <id>1</id>
+                    <status>settled</status>
+                    <amount>10.00</amount>
+                    <order-id>order1234</order-id>
+                </transaction>
+            </local-payment-funded>
+            """
+
+    def __local_payment_reversed(self):
+        return """
+            <local-payment-reversed>
+                <payment-id>a-payment-id</payment-id>
+            </local-payment-reversed>
+            """
+
+    def __payment_method_customer_data_updated_sample_xml(self, id):
+        return """
+            <payment-method-customer-data-updated-metadata>
+                <token>TOKEN-12345</token>
+                <payment-method>%s</payment-method>
+                <datetime-updated type='dateTime'>2022-01-01T21:28:37Z</datetime-updated>
+                <enriched-customer-data>
+                    <fields-updated type='array'>
+                        <item>username</item>
+                    </fields-updated>
+                    <profile-data>
+                        <username>venmo_username</username>
+                        <first-name>John</first-name>
+                        <last-name>Doe</last-name>
+                        <phone-number>1231231234</phone-number>
+                        <email>john.doe@paypal.com</email>
+                    </profile-data>
+                </enriched-customer-data>
+            </payment-method-customer-data-updated-metadata>
+            """ % self.__venmo_account_xml(id)
+
+    def __venmo_account_xml(self, id):
+        return """
+            <venmo-account>
+                <created-at type="datetime">2018-10-11T21:28:37Z</created-at>
+                <updated-at type="datetime">2018-10-11T21:28:37Z</updated-at>
+                <default type="boolean">true</default>
+                <image-url>https://assets.braintreegateway.com/payment_method_logo/venmo.png?environment=test</image-url>
+                <token>%s</token>
+                <source-description>Venmo Account: venmojoe</source-description>
+                <username>venmojoe</username>
+                <venmo-user-id>456</venmo-user-id>
+                <subscriptions type="array"/>
+                <customer-id>venmo_customer_id</customer-id>
+                <global-id>cGF5bWVudG1ldGhvZF92ZW5tb2FjY291bnQ</global-id>
+            </venmo-account>
+            """ % id
diff --git a/ci.sh b/ci.sh
index efe6fb5..0c794d9 100755
--- a/ci.sh
+++ b/ci.sh
@@ -5,7 +5,7 @@ if [[ "$1" == "http" ]]; then
 fi
 
 if [[ "$1" == "python3" ]]; then
-  /usr/local/lib/python3.3/bin/nosetests-3.3
+  /usr/local/lib/python3.3/bin/python3 -m unittest discover
 else
   env rake $python_tests --trace
 fi
diff --git a/debian/changelog b/debian/changelog
index 21e4f45..d108305 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,11 @@
+python-braintree (4.19.0-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+  * New upstream release.
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Sat, 20 May 2023 07:04:12 -0000
+
 python-braintree (3.57.1-1) unstable; urgency=medium
 
   * New upstream release.
diff --git a/dev_requirements.txt b/dev_requirements.txt
new file mode 100644
index 0000000..75ad19f
--- /dev/null
+++ b/dev_requirements.txt
@@ -0,0 +1,2 @@
+twine>=1.9,<2.0
+-r requirements.txt
diff --git a/docs/Makefile b/docs/Makefile
deleted file mode 100644
index 9df406c..0000000
--- a/docs/Makefile
+++ /dev/null
@@ -1,89 +0,0 @@
-# Makefile for Sphinx documentation
-#
-
-# You can set these variables from the command line.
-SPHINXOPTS    =
-SPHINXBUILD   = sphinx-build
-PAPER         =
-BUILDDIR      = _build
-
-# Internal variables.
-PAPEROPT_a4     = -D latex_paper_size=a4
-PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-
-.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
-
-help:
-	@echo "Please use \`make <target>' where <target> is one of"
-	@echo "  html      to make standalone HTML files"
-	@echo "  dirhtml   to make HTML files named index.html in directories"
-	@echo "  pickle    to make pickle files"
-	@echo "  json      to make JSON files"
-	@echo "  htmlhelp  to make HTML files and a HTML help project"
-	@echo "  qthelp    to make HTML files and a qthelp project"
-	@echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
-	@echo "  changes   to make an overview of all changed/added/deprecated items"
-	@echo "  linkcheck to check all external links for integrity"
-	@echo "  doctest   to run all doctests embedded in the documentation (if enabled)"
-
-clean:
-	-rm -rf $(BUILDDIR)/*
-
-html:
-	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
-	@echo
-	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
-
-dirhtml:
-	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
-	@echo
-	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
-
-pickle:
-	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
-	@echo
-	@echo "Build finished; now you can process the pickle files."
-
-json:
-	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
-	@echo
-	@echo "Build finished; now you can process the JSON files."
-
-htmlhelp:
-	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
-	@echo
-	@echo "Build finished; now you can run HTML Help Workshop with the" \
-	      ".hhp project file in $(BUILDDIR)/htmlhelp."
-
-qthelp:
-	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
-	@echo
-	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
-	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
-	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Braintree.qhcp"
-	@echo "To view the help file:"
-	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Braintree.qhc"
-
-latex:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo
-	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
-	@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
-	      "run these through (pdf)latex."
-
-changes:
-	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
-	@echo
-	@echo "The overview file is in $(BUILDDIR)/changes."
-
-linkcheck:
-	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
-	@echo
-	@echo "Link check complete; look for any errors in the above output " \
-	      "or in $(BUILDDIR)/linkcheck/output.txt."
-
-doctest:
-	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
-	@echo "Testing of doctests in the sources finished, look at the " \
-	      "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/docs/_static/.git_empty_dir b/docs/_static/.git_empty_dir
deleted file mode 100644
index e69de29..0000000
diff --git a/docs/address.rst b/docs/address.rst
deleted file mode 100644
index 2b71100..0000000
--- a/docs/address.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Address
-=====================================
-
-.. automodule:: braintree.address
-   :members:
diff --git a/docs/conf.py b/docs/conf.py
deleted file mode 100644
index 4eb9134..0000000
--- a/docs/conf.py
+++ /dev/null
@@ -1,197 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Braintree documentation build configuration file, created by
-# sphinx-quickstart on Mon Mar 29 14:46:55 2010.
-#
-# This file is execfile()d with the current directory set to its containing dir.
-#
-# Note that not all possible configuration values are present in this
-# autogenerated file.
-#
-# All configuration values have a default; values that are commented out
-# serve to show the default.
-
-import sys, os
-
-# If extensions (or modules to document with autodoc) are in another directory,
-# add these directories to sys.path here. If the directory is relative to the
-# documentation root, use os.path.abspath to make it absolute, like shown here.
-#sys.path.append(os.path.abspath('.'))
-sys.path.insert(0, os.path.dirname(__file__) + '/../')
-import braintree
-
-# -- General configuration -----------------------------------------------------
-
-# Add any Sphinx extension module names here, as strings. They can be extensions
-# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc']
-
-# Add any paths that contain templates here, relative to this directory.
-#templates_path = ['_templates']
-templates_path = []
-
-# The suffix of source filenames.
-source_suffix = '.rst'
-
-# The encoding of source files.
-#source_encoding = 'utf-8'
-
-# The master toctree document.
-master_doc = 'index'
-
-# General information about the project.
-project = u'Braintree'
-copyright = u'2012, Braintree'
-
-# The version info for the project you're documenting, acts as replacement for
-# |version| and |release|, also used in various other places throughout the
-# built documents.
-#
-# The short X.Y version.
-version = braintree.version.Version
-# The full version, including alpha/beta/rc tags.
-release = braintree.version.Version
-
-# The language for content autogenerated by Sphinx. Refer to documentation
-# for a list of supported languages.
-#language = None
-
-# There are two options for replacing |today|: either, you set today to some
-# non-false value, then it is used:
-#today = ''
-# Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
-
-# List of documents that shouldn't be included in the build.
-#unused_docs = []
-
-# List of directories, relative to source directory, that shouldn't be searched
-# for source files.
-exclude_trees = ['_build']
-
-# The reST default role (used for this markup: `text`) to use for all documents.
-#default_role = None
-
-# If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
-
-# If true, the current module name will be prepended to all description
-# unit titles (such as .. function::).
-#add_module_names = True
-
-# If true, sectionauthor and moduleauthor directives will be shown in the
-# output. They are ignored by default.
-#show_authors = False
-
-# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
-
-# A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
-
-
-# -- Options for HTML output ---------------------------------------------------
-
-# The theme to use for HTML and HTML Help pages.  Major themes that come with
-# Sphinx are currently 'default' and 'sphinxdoc'.
-html_theme = 'default'
-
-# Theme options are theme-specific and customize the look and feel of a theme
-# further.  For a list of options available for each theme, see the
-# documentation.
-#html_theme_options = {}
-
-# Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
-
-# The name for this set of Sphinx documents.  If None, it defaults to
-# "<project> v<release> documentation".
-#html_title = None
-
-# A shorter title for the navigation bar.  Default is the same as html_title.
-#html_short_title = None
-
-# The name of an image file (relative to this directory) to place at the top
-# of the sidebar.
-#html_logo = None
-
-# The name of an image file (within the static path) to use as favicon of the
-# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
-# pixels large.
-#html_favicon = None
-
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
-
-# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
-# using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
-
-# If true, SmartyPants will be used to convert quotes and dashes to
-# typographically correct entities.
-#html_use_smartypants = True
-
-# Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
-
-# Additional templates that should be rendered to pages, maps page names to
-# template names.
-#html_additional_pages = {}
-
-# If false, no module index is generated.
-#html_use_modindex = True
-
-# If false, no index is generated.
-#html_use_index = True
-
-# If true, the index is split into individual pages for each letter.
-#html_split_index = False
-
-# If true, links to the reST sources are added to the pages.
-#html_show_sourcelink = True
-
-# If true, an OpenSearch description file will be output, and all pages will
-# contain a <link> tag referring to it.  The value of this option must be the
-# base URL from which the finished HTML is served.
-#html_use_opensearch = ''
-
-# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = ''
-
-# Output file base name for HTML help builder.
-htmlhelp_basename = 'Braintreedoc'
-
-
-# -- Options for LaTeX output --------------------------------------------------
-
-# The paper size ('letter' or 'a4').
-#latex_paper_size = 'letter'
-
-# The font size ('10pt', '11pt' or '12pt').
-#latex_font_size = '10pt'
-
-# Grouping the document tree into LaTeX files. List of tuples
-# (source start file, target name, title, author, documentclass [howto/manual]).
-latex_documents = [
-  ('index', 'Braintree.tex', u'Braintree Documentation',
-   u'Braintree', 'manual'),
-]
-
-# The name of an image file (relative to this directory) to place at the top of
-# the title page.
-#latex_logo = None
-
-# For "manual" documents, if this is true, then toplevel headings are parts,
-# not chapters.
-#latex_use_parts = False
-
-# Additional stuff for the LaTeX preamble.
-#latex_preamble = ''
-
-# Documents to append as an appendix to all manuals.
-#latex_appendices = []
-
-# If false, no module index is generated.
-#latex_use_modindex = True
diff --git a/docs/configuration.rst b/docs/configuration.rst
deleted file mode 100644
index ea43f45..0000000
--- a/docs/configuration.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Configuration
-=====================================
-
-.. automodule:: braintree.configuration
-   :members:
diff --git a/docs/credit_card.rst b/docs/credit_card.rst
deleted file mode 100644
index 8f40ddf..0000000
--- a/docs/credit_card.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Credit Card
-=====================================
-
-.. automodule:: braintree.credit_card
-   :members:
diff --git a/docs/credit_card_verification.rst b/docs/credit_card_verification.rst
deleted file mode 100644
index 2036335..0000000
--- a/docs/credit_card_verification.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Credit Card Verification
-=====================================
-
-.. automodule:: braintree.credit_card_verification
-   :members:
diff --git a/docs/customer.rst b/docs/customer.rst
deleted file mode 100644
index daae0b8..0000000
--- a/docs/customer.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Customer
-=====================================
-
-.. automodule:: braintree.customer
-   :members:
diff --git a/docs/environment.rst b/docs/environment.rst
deleted file mode 100644
index 9e34b47..0000000
--- a/docs/environment.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Environment
-=====================================
-
-.. automodule:: braintree.environment
-   :members:
diff --git a/docs/error_codes.rst b/docs/error_codes.rst
deleted file mode 100644
index f813889..0000000
--- a/docs/error_codes.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Error Codes
-=====================================
-
-.. automodule:: braintree.error_codes
-   :members:
diff --git a/docs/error_result.rst b/docs/error_result.rst
deleted file mode 100644
index 335ee35..0000000
--- a/docs/error_result.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Error Result
-=====================================
-
-.. automodule:: braintree.error_result
-   :members:
diff --git a/docs/exceptions/authentication_error.rst b/docs/exceptions/authentication_error.rst
deleted file mode 100644
index 16a74d0..0000000
--- a/docs/exceptions/authentication_error.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Authentication Error
-=====================================
-
-.. automodule:: braintree.exceptions.authentication_error
-   :members:
diff --git a/docs/exceptions/authorization_error.rst b/docs/exceptions/authorization_error.rst
deleted file mode 100644
index b34525e..0000000
--- a/docs/exceptions/authorization_error.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Authorization Error
-=====================================
-
-.. automodule:: braintree.exceptions.authorization_error
-   :members:
diff --git a/docs/exceptions/down_for_maintenance_error.rst b/docs/exceptions/down_for_maintenance_error.rst
deleted file mode 100644
index c73eb23..0000000
--- a/docs/exceptions/down_for_maintenance_error.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Down For Maintenance Error
-=====================================
-
-.. automodule:: braintree.exceptions.down_for_maintenance_error
-   :members:
diff --git a/docs/exceptions/forged_query_string_error.rst b/docs/exceptions/forged_query_string_error.rst
deleted file mode 100644
index 4d223c7..0000000
--- a/docs/exceptions/forged_query_string_error.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Forged Query String Error
-=====================================
-
-.. automodule:: braintree.exceptions.forged_query_string_error
-   :members:
diff --git a/docs/exceptions/not_found_error.rst b/docs/exceptions/not_found_error.rst
deleted file mode 100644
index 7f21f9b..0000000
--- a/docs/exceptions/not_found_error.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Not Found Error
-=====================================
-
-.. automodule:: braintree.exceptions.not_found_error
-   :members:
diff --git a/docs/exceptions/server_error.rst b/docs/exceptions/server_error.rst
deleted file mode 100644
index 93b2845..0000000
--- a/docs/exceptions/server_error.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Server Error
-=====================================
-
-.. automodule:: braintree.exceptions.server_error
-   :members:
diff --git a/docs/exceptions/unexpected_error.rst b/docs/exceptions/unexpected_error.rst
deleted file mode 100644
index 99a3013..0000000
--- a/docs/exceptions/unexpected_error.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Unexpected Error
-=====================================
-
-.. automodule:: braintree.exceptions.unexpected_error
-   :members:
diff --git a/docs/exceptions/upgrade_required_error.rst b/docs/exceptions/upgrade_required_error.rst
deleted file mode 100644
index c815c99..0000000
--- a/docs/exceptions/upgrade_required_error.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Upgrade Required Error
-=====================================
-
-.. automodule:: braintree.exceptions.upgrade_required_error
-   :members:
diff --git a/docs/index.rst b/docs/index.rst
deleted file mode 100644
index d2f25e6..0000000
--- a/docs/index.rst
+++ /dev/null
@@ -1,79 +0,0 @@
-.. Braintree documentation master file, created by
-   sphinx-quickstart on Mon Mar 29 14:46:55 2010.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-Braintree Python Client Library
-=====================================
-
-The Braintree library provides integration access to the Braintree Gateway.
-
-Quick Start
------------
-
-See: https://developers.braintreepayments.com/start/hello-server/python
-
-Braintree Objects
------------------
-
-.. toctree::
-   :maxdepth: 2
-
-   address
-   credit_card
-   credit_card_verification
-   customer
-   subscription
-   transaction
-
-Utility Objects
----------------
-
-.. toctree::
-   :maxdepth: 2
-
-   resource_collection
-   transparent_redirect
-   successful_result
-
-Errors
-------
-
-.. toctree::
-   :maxdepth: 2
-
-   error_codes
-   error_result
-   validation_error
-   validation_error_collection
-
-Configuration
--------------
-
-.. toctree::
-   :maxdepth: 2
-
-   configuration
-   environment
-
-Exceptions
-----------
-
-.. toctree::
-   :maxdepth: 2
-
-   exceptions/authentication_error
-   exceptions/authorization_error
-   exceptions/down_for_maintenance_error
-   exceptions/forged_query_string_error
-   exceptions/not_found_error
-   exceptions/server_error
-   exceptions/unexpected_error
-   exceptions/upgrade_required_error
-
-Indices
--------
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..93120e6
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1 @@
+docutils<0.18
diff --git a/docs/resource_collection.rst b/docs/resource_collection.rst
deleted file mode 100644
index adf927b..0000000
--- a/docs/resource_collection.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Resource Collection
-=====================================
-
-.. automodule:: braintree.resource_collection
-   :members:
diff --git a/docs/subscription.rst b/docs/subscription.rst
deleted file mode 100644
index a4a9998..0000000
--- a/docs/subscription.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Subscription
-=====================================
-
-.. automodule:: braintree.subscription
-   :members:
diff --git a/docs/successful_result.rst b/docs/successful_result.rst
deleted file mode 100644
index 96caf4b..0000000
--- a/docs/successful_result.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Successful Result
-=====================================
-
-.. automodule:: braintree.successful_result
-   :members:
diff --git a/docs/transaction.rst b/docs/transaction.rst
deleted file mode 100644
index 5c9a0ea..0000000
--- a/docs/transaction.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Transaction
-=====================================
-
-.. automodule:: braintree.transaction
-   :members:
diff --git a/docs/transparent_redirect.rst b/docs/transparent_redirect.rst
deleted file mode 100644
index 8131362..0000000
--- a/docs/transparent_redirect.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Transparent Redirect
-=====================================
-
-.. automodule:: braintree.transparent_redirect
-   :members:
diff --git a/docs/validation_error.rst b/docs/validation_error.rst
deleted file mode 100644
index 5326079..0000000
--- a/docs/validation_error.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Validation Error
-=====================================
-
-.. automodule:: braintree.validation_error
-   :members:
diff --git a/docs/validation_error_collection.rst b/docs/validation_error_collection.rst
deleted file mode 100644
index 04cbbbe..0000000
--- a/docs/validation_error_collection.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Validation Error Collection
-=====================================
-
-.. automodule:: braintree.validation_error_collection
-   :members:
diff --git a/requirements.txt b/requirements.txt
index 8910361..2293380 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1 @@
 requests>=0.11.1,<3.0
-mock>=2.0,<3.0
-nose>=1.3.7,<2.0
-twine>=1.9,<2.0
diff --git a/setup.py b/setup.py
index e9179d1..b1074c4 100644
--- a/setup.py
+++ b/setup.py
@@ -7,17 +7,17 @@ long_description = """
         The Braintree Python SDK provides integration access to the Braintree Gateway.
 
         1. https://github.com/braintree/braintree_python - README and Samples
-        2. https://developers.braintreepayments.com/python/sdk/server/overview - API Reference
+        2. https://developer.paypal.com/braintree/docs/reference/overview - API Reference
       """
 
 setup(
     name="braintree",
-    version="3.57.1",
+    version="4.19.0",
     description="Braintree Python Library",
     long_description=long_description,
     author="Braintree",
     author_email="support@braintreepayments.com",
-    url="https://developers.braintreepayments.com/python/sdk/server/overview",
+    url="https://developer.paypal.com/braintree/docs/reference/overview",
     packages=["braintree", "braintree.dispute_details", "braintree.exceptions", "braintree.exceptions.http", "braintree.merchant_account", "braintree.util", "braintree.test"],
     package_data={"braintree": ["ssl/*"]},
     install_requires=["requests>=0.11.1,<3.0"],
@@ -25,10 +25,9 @@ setup(
     license="MIT",
     classifiers=[
         "License :: OSI Approved :: MIT License",
-        "Programming Language :: Python :: 2.6",
-        "Programming Language :: Python :: 2.7",
-        "Programming Language :: Python :: 3.3",
-        "Programming Language :: Python :: 3.4",
-        "Programming Language :: Python :: 3.5"
+        "Programming Language :: Python :: 3.5",
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8"
     ]
 )
diff --git a/tests/integration/test_address.py b/tests/integration/test_address.py
index 1862cd8..8dd2a37 100644
--- a/tests/integration/test_address.py
+++ b/tests/integration/test_address.py
@@ -13,6 +13,7 @@ class TestAddress(unittest.TestCase):
             "locality": "Chicago",
             "region": "Illinois",
             "postal_code": "60622",
+            "phone_number": "8675309",
             "country_name": "United States of America",
             "country_code_alpha2": "US",
             "country_code_alpha3": "USA",
@@ -30,6 +31,7 @@ class TestAddress(unittest.TestCase):
         self.assertEqual("Chicago", address.locality)
         self.assertEqual("Illinois", address.region)
         self.assertEqual("60622", address.postal_code)
+        self.assertEqual("8675309", address.phone_number)
         self.assertEqual("US", address.country_code_alpha2)
         self.assertEqual("USA", address.country_code_alpha3)
         self.assertEqual("840", address.country_code_numeric)
@@ -84,10 +86,10 @@ class TestAddress(unittest.TestCase):
 
         self.assertTrue(result.is_success)
 
-    @raises(NotFoundError)
     def test_delete_with_valid_customer_id_and_non_existing_address(self):
-        customer = Customer.create().customer
-        Address.delete(customer.id, "notreal")
+        with self.assertRaises(NotFoundError):
+            customer = Customer.create().customer
+            Address.delete(customer.id, "notreal")
 
     def test_find_with_valid_customer_id_and_address_id(self):
         customer = Customer.create().customer
@@ -96,10 +98,9 @@ class TestAddress(unittest.TestCase):
 
         self.assertEqual(address.street_address, found_address.street_address)
 
-    @raises_with_regexp(NotFoundError,
-        "address for customer 'notreal' with id 'badaddress' not found")
     def test_find_with_invalid_customer_id_and_address_id(self):
-        Address.find("notreal", "badaddress")
+        with self.assertRaisesRegex(NotFoundError, "address for customer 'notreal' with id 'badaddress' not found"):
+            Address.find("notreal", "badaddress")
 
     def test_update_with_valid_values(self):
         customer = Customer.create().customer
@@ -161,7 +162,7 @@ class TestAddress(unittest.TestCase):
         self.assertEqual(1, len(country_name_errors))
         self.assertEqual(ErrorCodes.Address.CountryNameIsNotAccepted, country_name_errors[0].code)
 
-    @raises(NotFoundError)
     def test_update_raises_not_found_error_if_given_bad_address(self):
-        customer = Customer.create().customer
-        Address.update(customer.id, "notfound", {"street_address": "123 Main St."})
+        with self.assertRaises(NotFoundError):
+            customer = Customer.create().customer
+            Address.update(customer.id, "notfound", {"street_address": "123 Main St."})
diff --git a/tests/integration/test_apple_pay.py b/tests/integration/test_apple_pay.py
new file mode 100644
index 0000000..7f97897
--- /dev/null
+++ b/tests/integration/test_apple_pay.py
@@ -0,0 +1,49 @@
+from tests.test_helper import *
+
+class TestApplePay(unittest.TestCase):
+    @staticmethod
+    def get_gateway():
+        config = Configuration("development", "integration_merchant_id",
+                               public_key="integration_public_key",
+                               private_key="integration_private_key")
+        return BraintreeGateway(config)
+
+    def test_register_domain_registers_an_apple_pay_domain(self):
+        result = self.get_gateway().apple_pay.register_domain("www.example.com")
+
+        self.assertTrue(result.is_success)
+
+    def test_register_domain_gets_a_validation_error_when_attempting_to_register_no_domains(self):
+        result = self.get_gateway().apple_pay.register_domain("")
+
+        self.assertFalse(result.is_success)
+        self.assertEqual(result.errors.for_object("apple_pay")[0].message, "Domain name is required.")
+
+    def test_delete_customer_with_path_traversal(self):
+        try:
+            customer = Customer.create({"first_name":"Waldo"}).customer
+            self.get_gateway().apple_pay.unregister_domain("../../../customers/{}".format(customer.id))
+        except NotFoundError:
+            pass
+
+        found_customer = Customer.find(customer.id)
+        self.assertNotEqual(None, found_customer)
+        self.assertEqual("Waldo", found_customer.first_name)
+
+
+    def test_unregister_domain_unregisters_an_apple_pay_domain(self):
+        result = self.get_gateway().apple_pay.unregister_domain("example.org")
+        self.assertTrue(result.is_success)
+
+    def test_unregister_domain_unregisters_an_apple_pay_domain_with_schem_in_url(self):
+        result = self.get_gateway().apple_pay.unregister_domain("http://example.org")
+        self.assertTrue(result.is_success)
+
+    def test_unregister_domain_escapes_the_unregistered_domain_query_parameter(self):
+        result = self.get_gateway().apple_pay.unregister_domain("ex&mple.org")
+        self.assertTrue(result.is_success)
+
+    def test_registered_domains_returns_stubbed_registered_domains(self):
+        result = self.get_gateway().apple_pay.registered_domains()
+        self.assertEqual(len(result), 1)
+        self.assertEqual(result[0], "www.example.com")
diff --git a/tests/integration/test_braintree_gateway.py b/tests/integration/test_braintree_gateway.py
index 82673b2..ef76fb7 100644
--- a/tests/integration/test_braintree_gateway.py
+++ b/tests/integration/test_braintree_gateway.py
@@ -1,10 +1,10 @@
-from unittest import TestCase
+from tests.test_helper import *
 
 from braintree.braintree_gateway import BraintreeGateway
 from braintree.configuration import Configuration
 from braintree.environment import Environment
 
-class TestBraintreeGateway(TestCase):
+class TestBraintreeGateway(unittest.TestCase):
 
     @staticmethod
     def get_gateway():
@@ -13,6 +13,7 @@ class TestBraintreeGateway(TestCase):
                                private_key="integration_private_key")
         return BraintreeGateway(config)
 
+    @unittest.skip("until we have a more stable ci env")
     def test_can_make_tokenize_credit_card_via_graphql(self):
         definition = """
           mutation ExampleServerSideSingleUseToken($input: TokenizeCreditCardInput!) {
diff --git a/tests/integration/test_client_token.py b/tests/integration/test_client_token.py
index 9e5f94a..53146ea 100644
--- a/tests/integration/test_client_token.py
+++ b/tests/integration/test_client_token.py
@@ -160,8 +160,8 @@ class TestClientToken(unittest.TestCase):
 
         self.assertEqual(expected_merchant_account_id, merchant_account_id)
 
-    @raises_with_regexp(Exception, "'Invalid keys: merchant_id'")
     def test_required_data_cannot_be_overridden(self):
-        TestHelper.generate_decoded_client_token({
-            "merchant_id": "1234"
-        })
+        with self.assertRaisesRegex(Exception, "'Invalid keys: merchant_id'"):
+            TestHelper.generate_decoded_client_token({
+                "merchant_id": "1234"
+            })
diff --git a/tests/integration/test_coinbase.py b/tests/integration/test_coinbase.py
deleted file mode 100644
index 7d214a3..0000000
--- a/tests/integration/test_coinbase.py
+++ /dev/null
@@ -1,29 +0,0 @@
-from tests.test_helper import *
-
-from braintree.test.nonces import Nonces
-from braintree.exceptions.not_found_error import NotFoundError
-from braintree.error_codes import ErrorCodes
-
-class TestCoinbase(unittest.TestCase):
-
-    def test_customer(self):
-        result = Customer.create({"payment_method_nonce": Nonces.Coinbase})
-
-        self.assertFalse(result.is_success)
-        self.assertEquals(ErrorCodes.PaymentMethod.PaymentMethodNoLongerSupported, result.errors.for_object("coinbase_account").on("base")[0].code)
-
-    def test_vault(self):
-        result = Customer.create()
-        result = PaymentMethod.create({
-            "customer_id": result.customer.id,
-            "payment_method_nonce": Nonces.Coinbase
-        })
-
-        self.assertFalse(result.is_success)
-        self.assertEquals(ErrorCodes.PaymentMethod.PaymentMethodNoLongerSupported, result.errors.for_object("coinbase_account").on("base")[0].code)
-
-    def test_transaction(self):
-        result = Transaction.sale({"payment_method_nonce": Nonces.Coinbase, "amount": "1.00"})
-
-        self.assertFalse(result.is_success)
-        self.assertEquals(ErrorCodes.PaymentMethod.PaymentMethodNoLongerSupported, result.errors.for_object("transaction").on("base")[0].code)
diff --git a/tests/integration/test_credit_card.py b/tests/integration/test_credit_card.py
index d0613ac..6017d82 100644
--- a/tests/integration/test_credit_card.py
+++ b/tests/integration/test_credit_card.py
@@ -1,7 +1,7 @@
+import datetime
 from tests.test_helper import *
 from braintree.test.credit_card_defaults import CreditCardDefaults
 from braintree.test.credit_card_numbers import CreditCardNumbers
-import braintree.test.venmo_sdk as venmo_sdk
 
 class TestCreditCard(unittest.TestCase):
     def test_create_with_three_d_secure_nonce(self):
@@ -28,6 +28,44 @@ class TestCreditCard(unittest.TestCase):
         self.assertEqual("05", three_d_secure_info.eci_flag)
         self.assertEqual("1.0.2", three_d_secure_info.three_d_secure_version)
 
+    def test_create_with_three_d_secure_pass_thru(self):
+        customer_id = Customer.create().customer.id
+        result = CreditCard.create({
+            "customer_id": customer_id,
+            "number": "4111111111111111",
+            "expiration_date": "05/2009",
+            "three_d_secure_pass_thru": {
+                "three_d_secure_version": "1.1.1",
+                "eci_flag": "05",
+                "cavv": "some-cavv",
+                "xid": "some-xid"
+            },
+            "options": {
+                "verify_card": "true",
+            }
+        })
+
+        self.assertTrue(result.is_success)
+
+    def test_create_with_three_d_secure_pass_thru_without_eci_flag(self):
+        customer_id = Customer.create().customer.id
+        result = CreditCard.create({
+            "customer_id": customer_id,
+            "number": "4111111111111111",
+            "expiration_date": "05/2009",
+            "three_d_secure_pass_thru": {
+                "three_d_secure_version": "1.1.1",
+                "cavv": "some-cavv",
+                "xid": "some-xid"
+            },
+            "options": {
+                "verify_card": "true",
+            }
+        })
+
+        self.assertFalse(result.is_success)
+        self.assertEqual("EciFlag is required.", result.message)
+
     def test_create_adds_credit_card_to_existing_customer(self):
         customer = Customer.create().customer
         result = CreditCard.create({
@@ -48,7 +86,6 @@ class TestCreditCard(unittest.TestCase):
         self.assertEqual("05/2014", credit_card.expiration_date)
         self.assertEqual("John Doe", credit_card.cardholder_name)
         self.assertNotEqual(re.search(r"\A\w{32}\Z", credit_card.unique_number_identifier), None)
-        self.assertFalse(credit_card.venmo_sdk)
         self.assertNotEqual(re.search("png", credit_card.image_url), None)
 
     def test_create_and_make_default(self):
@@ -93,19 +130,37 @@ class TestCreditCard(unittest.TestCase):
         self.assertEqual("05/2014", credit_card.expiration_date)
 
     def test_create_with_security_params(self):
-        customer = Customer.create().customer
-        result = CreditCard.create({
-            "customer_id": customer.id,
-            "number": "4111111111111111",
-            "expiration_month": "05",
-            "expiration_year": "2014",
-            "cvv": "100",
-            "cardholder_name": "John Doe",
-            "device_session_id": "abc123",
-            "fraud_merchant_id": "456"
-        })
+            customer = Customer.create().customer
+            result = CreditCard.create({
+                "customer_id": customer.id,
+                "number": "4111111111111111",
+                "expiration_month": "05",
+                "expiration_year": "2014",
+                "cvv": "100",
+                "cardholder_name": "John Doe",
+                "device_data": "abc123"
+                })
 
-        self.assertTrue(result.is_success)
+            self.assertTrue(result.is_success)
+
+    def test_create_with_deprecated_security_params_sends_warning(self):
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            customer = Customer.create().customer
+            result = CreditCard.create({
+                "customer_id": customer.id,
+                "number": "4111111111111111",
+                "expiration_month": "05",
+                "expiration_year": "2014",
+                "cvv": "100",
+                "cardholder_name": "John Doe",
+                "device_session_id": "abc123",
+                "fraud_merchant_id": "456"
+                })
+
+            self.assertTrue(result.is_success)
+            assert len(w) > 0
+            assert issubclass(w[-1].category, DeprecationWarning)
 
     def test_create_can_specify_the_desired_token(self):
         token = str(random.randint(1, 1000000))
@@ -179,44 +234,74 @@ class TestCreditCard(unittest.TestCase):
         self.assertTrue(result.is_success)
         self.assertEqual(None, result.credit_card.billing_address)
 
-
     def test_unsuccessful_create_with_card_verification_returns_risk_data(self):
-        with AdvancedFraudIntegrationMerchant():
+        with FraudProtectionEnterpriseIntegrationMerchant():
             customer = Customer.create().customer
             result = CreditCard.create({
                 "customer_id": customer.id,
                 "number": "4000111111111115",
                 "expiration_date": "05/2014",
                 "options": {"verify_card": True},
-                "device_session_id": "abc123"
+                "device_data": "abc123"
             })
 
             self.assertFalse(result.is_success)
             verification = result.credit_card_verification
             self.assertIsInstance(verification.risk_data, RiskData)
             self.assertTrue(hasattr(verification.risk_data, 'id'))
-            self.assertEqual("Approve", verification.risk_data.decision)
-            self.assertTrue(hasattr(verification.risk_data, 'device_data_captured'))
+            self.assertTrue(hasattr(verification.risk_data, 'decision'))
+            self.assertTrue(hasattr(verification.risk_data, 'decision_reasons'))
             self.assertTrue(hasattr(verification.risk_data, 'fraud_service_provider'))
+            self.assertTrue(hasattr(verification.risk_data, 'transaction_risk_score'))
 
     def test_successful_create_with_card_verification_returns_risk_data(self):
-        with AdvancedFraudIntegrationMerchant():
+        with FraudProtectionEnterpriseIntegrationMerchant():
             customer = Customer.create().customer
             result = CreditCard.create({
                 "customer_id": customer.id,
                 "number": "4111111111111111",
                 "expiration_date": "05/2014",
                 "options": {"verify_card": True},
-                "device_session_id": "abc123"
+                "device_data": "abc123"
             })
 
             self.assertTrue(result.is_success)
             verification = result.credit_card.verification
             self.assertIsInstance(verification.risk_data, RiskData)
-            self.assertTrue(hasattr(verification.risk_data, 'id'))
-            self.assertEqual("Approve", verification.risk_data.decision)
-            self.assertTrue(hasattr(verification.risk_data, 'device_data_captured'))
-            self.assertTrue(hasattr(verification.risk_data, 'fraud_service_provider'))
+
+    def test_create_includes_risk_data_when_skip_advanced_fraud_checking_is_false(self):
+        with FraudProtectionEnterpriseIntegrationMerchant():
+            customer = Customer.create().customer
+            result = CreditCard.create({
+                "customer_id": customer.id,
+                "number": "4111111111111111",
+                "expiration_date": "05/2014",
+                "options": {
+                    "verify_card": True,
+                    "skip_advanced_fraud_checking": False
+                    },
+                })
+
+            self.assertTrue(result.is_success)
+            verification = result.credit_card.verification
+            self.assertIsInstance(verification.risk_data, RiskData)
+
+    def test_create_does_not_include_risk_data_when_skip_advanced_fraud_checking_is_true(self):
+        with FraudProtectionEnterpriseIntegrationMerchant():
+            customer = Customer.create().customer
+            result = CreditCard.create({
+                "customer_id": customer.id,
+                "number": "4111111111111111",
+                "expiration_date": "05/2014",
+                "options": {
+                    "verify_card": True,
+                    "skip_advanced_fraud_checking": True
+                    },
+                })
+
+            self.assertTrue(result.is_success)
+            verification = result.credit_card.verification
+            self.assertIsNone(verification.risk_data)
 
     def test_create_with_card_verification(self):
         customer = Customer.create().customer
@@ -421,31 +506,6 @@ class TestCreditCard(unittest.TestCase):
         self.assertEqual(1, len(country_name_errors))
         self.assertEqual(ErrorCodes.Address.CountryNameIsNotAccepted, country_name_errors[0].code)
 
-    def test_create_with_venmo_sdk_payment_method_code(self):
-        customer = Customer.create().customer
-        result = CreditCard.create({
-            "customer_id": customer.id,
-            "venmo_sdk_payment_method_code": venmo_sdk.VisaPaymentMethodCode
-        })
-
-        self.assertTrue(result.is_success)
-        self.assertEqual("411111", result.credit_card.bin)
-        self.assertFalse(result.credit_card.venmo_sdk)
-
-    def test_create_with_invalid_venmo_sdk_payment_method_code(self):
-        customer = Customer.create().customer
-        result = CreditCard.create({
-            "customer_id": customer.id,
-            "venmo_sdk_payment_method_code": venmo_sdk.InvalidPaymentMethodCode
-        })
-
-        self.assertFalse(result.is_success)
-        self.assertEqual("Invalid VenmoSDK payment method code", result.message)
-
-        venmo_sdk_payment_method_code_errors = result.errors.for_object("credit_card").on("venmo_sdk_payment_method_code")
-        self.assertEqual(1, len(venmo_sdk_payment_method_code_errors))
-        self.assertEqual(ErrorCodes.CreditCard.InvalidVenmoSDKPaymentMethodCode, venmo_sdk_payment_method_code_errors[0].code)
-
     def test_create_with_payment_method_nonce(self):
         config = Configuration.instantiate()
         authorization_fingerprint = json.loads(TestHelper.generate_decoded_client_token())["authorizationFingerprint"]
@@ -473,37 +533,72 @@ class TestCreditCard(unittest.TestCase):
         self.assertTrue(result.is_success)
         self.assertEqual("411111", result.credit_card.bin)
 
-    def test_create_with_venmo_sdk_session(self):
+    def test_delete_customer_with_path_traversal(self):
+        try:
+            customer = Customer.create({"first_name":"Waldo"}).customer
+            CreditCard.delete("../../customers/{}".format(customer.id))
+        except NotFoundError:
+            pass
+
+        found_customer = Customer.find(customer.id)
+        self.assertNotEqual(None, found_customer)
+        self.assertEqual("Waldo", found_customer.first_name)
+
+    def test_update_with_valid_options(self):
         customer = Customer.create().customer
-        result = CreditCard.create({
+        credit_card = CreditCard.create({
             "customer_id": customer.id,
             "number": "4111111111111111",
             "expiration_date": "05/2014",
             "cvv": "100",
-            "cardholder_name": "John Doe",
-            "options": {
-                "venmo_sdk_session": venmo_sdk.Session
-            }
+            "cardholder_name": "John Doe"
+        }).credit_card
+
+        result = CreditCard.update(credit_card.token, {
+            "number": "5105105105105100",
+            "expiration_date": "06/2010",
+            "cvv": "123",
+            "cardholder_name": "Jane Jones"
         })
+
         self.assertTrue(result.is_success)
-        self.assertFalse(result.credit_card.venmo_sdk)
+        credit_card = result.credit_card
+        self.assertTrue(re.search(r"\A\w{4,}\Z", credit_card.token) is not None)
+        self.assertEqual("510510", credit_card.bin)
+        self.assertEqual("5100", credit_card.last_4)
+        self.assertEqual("06", credit_card.expiration_month)
+        self.assertEqual("2010", credit_card.expiration_year)
+        self.assertEqual("06/2010", credit_card.expiration_date)
+        self.assertEqual("Jane Jones", credit_card.cardholder_name)
 
-    def test_create_with_invalid_venmo_sdk_session(self):
+    def test_update_with_three_d_secure_pass_thru(self):
         customer = Customer.create().customer
-        result = CreditCard.create({
+        credit_card = CreditCard.create({
             "customer_id": customer.id,
             "number": "4111111111111111",
             "expiration_date": "05/2014",
             "cvv": "100",
-            "cardholder_name": "John Doe",
+            "cardholder_name": "John Doe"
+        }).credit_card
+
+        result = CreditCard.update(credit_card.token, {
+            "number": "4111111111111111",
+            "expiration_date": "05/2009",
+            "cvv": "123",
+            "three_d_secure_pass_thru": {
+                "three_d_secure_version": "1.1.1",
+                "eci_flag": "05",
+                "cavv": "some-cavv",
+                "xid": "some-xid"
+            },
             "options": {
-                "venmo_sdk_session": venmo_sdk.InvalidSession
+                "verify_card": "true",
             }
         })
+
         self.assertTrue(result.is_success)
-        self.assertFalse(result.credit_card.venmo_sdk)
 
-    def test_update_with_valid_options(self):
+    def test_update_with_three_d_secure_pass_thru_without_eci_flag(self):
         customer = Customer.create().customer
         credit_card = CreditCard.create({
             "customer_id": customer.id,
@@ -514,21 +609,21 @@ class TestCreditCard(unittest.TestCase):
         }).credit_card
 
         result = CreditCard.update(credit_card.token, {
-            "number": "5105105105105100",
-            "expiration_date": "06/2010",
+            "number": "4111111111111111",
+            "expiration_date": "05/2009",
             "cvv": "123",
-            "cardholder_name": "Jane Jones"
+            "three_d_secure_pass_thru": {
+                "three_d_secure_version": "1.1.1",
+                "cavv": "some-cavv",
+                "xid": "some-xid"
+            },
+            "options": {
+                "verify_card": "true",
+            }
         })
 
-        self.assertTrue(result.is_success)
-        credit_card = result.credit_card
-        self.assertTrue(re.search(r"\A\w{4,}\Z", credit_card.token) is not None)
-        self.assertEqual("510510", credit_card.bin)
-        self.assertEqual("5100", credit_card.last_4)
-        self.assertEqual("06", credit_card.expiration_month)
-        self.assertEqual("2010", credit_card.expiration_year)
-        self.assertEqual("06/2010", credit_card.expiration_date)
-        self.assertEqual("Jane Jones", credit_card.cardholder_name)
+        self.assertFalse(result.is_success)
+        self.assertEqual("EciFlag is required.", result.message)
 
     def test_update_billing_address_creates_new_by_default(self):
         customer = Customer.create().customer
@@ -657,6 +752,52 @@ class TestCreditCard(unittest.TestCase):
         self.assertFalse(result.is_success)
         self.assertEqual(CreditCardVerification.Status.ProcessorDeclined, result.credit_card_verification.status)
 
+    def test_update_includes_risk_data_when_skip_advanced_fraud_checking_is_false(self):
+        with FraudProtectionEnterpriseIntegrationMerchant():
+            customer = Customer.create().customer
+            credit_card = CreditCard.create({
+                "customer_id": customer.id,
+                "number": "4111111111111111",
+                "expiration_date": "05/2014",
+                "cvv": "100",
+                "cardholder_name": "John Doe"
+            }).credit_card
+
+            result = CreditCard.update(credit_card.token, {
+                "expiration_date": "06/2020",
+                "options": {
+                    "verify_card": True,
+                    "skip_advanced_fraud_checking": False
+                    }
+            })
+
+            self.assertTrue(result.is_success)
+            verification = result.credit_card.verification
+            self.assertIsInstance(verification.risk_data, RiskData)
+
+    def test_update_does_not_include_risk_data_when_skip_advanced_fraud_checking_is_true(self):
+        with FraudProtectionEnterpriseIntegrationMerchant():
+            customer = Customer.create().customer
+            credit_card = CreditCard.create({
+                "customer_id": customer.id,
+                "number": "4111111111111111",
+                "expiration_date": "05/2014",
+                "cvv": "100",
+                "cardholder_name": "John Doe"
+            }).credit_card
+
+            result = CreditCard.update(credit_card.token, {
+                "expiration_date": "06/2020",
+                "options": {
+                    "verify_card": True,
+                    "skip_advanced_fraud_checking": True
+                    }
+            })
+
+            self.assertTrue(result.is_success)
+            verification = result.credit_card.verification
+            self.assertIsNone(verification.risk_data)
+
     def test_update_billing_address(self):
         customer = Customer.create().customer
         credit_card = CreditCard.create({
@@ -740,21 +881,21 @@ class TestCreditCard(unittest.TestCase):
         result = CreditCard.delete(credit_card.token)
         self.assertTrue(result.is_success)
 
-    @raises(NotFoundError)
     def test_delete_raises_error_when_deleting_twice(self):
-        customer = Customer.create().customer
-        credit_card = CreditCard.create({
-            "customer_id": customer.id,
-            "number": "4111111111111111",
-            "expiration_date": "05/2014"
-        }).credit_card
+        with self.assertRaises(NotFoundError):
+            customer = Customer.create().customer
+            credit_card = CreditCard.create({
+                "customer_id": customer.id,
+                "number": "4111111111111111",
+                "expiration_date": "05/2014"
+            }).credit_card
 
-        CreditCard.delete(credit_card.token)
-        CreditCard.delete(credit_card.token)
+            CreditCard.delete(credit_card.token)
+            CreditCard.delete(credit_card.token)
 
-    @raises(NotFoundError)
     def test_delete_with_invalid_token(self):
-        CreditCard.delete("notreal")
+        with self.assertRaises(NotFoundError):
+            CreditCard.delete("notreal")
 
     def test_find_with_valid_token(self):
         customer = Customer.create().customer
@@ -797,9 +938,9 @@ class TestCreditCard(unittest.TestCase):
         self.assertEqual(Decimal("1.00"), subscription.price)
         self.assertEqual(credit_card.token, subscription.payment_method_token)
 
-    @raises_with_regexp(NotFoundError, "payment method with token 'bad_token' not found")
     def test_find_with_invalid_token(self):
-        CreditCard.find("bad_token")
+        with self.assertRaisesRegex(NotFoundError, "payment method with token 'bad_token' not found"):
+            CreditCard.find("bad_token")
 
     def test_from_nonce_with_unlocked_nonce(self):
         config = Configuration.instantiate()
@@ -830,310 +971,69 @@ class TestCreditCard(unittest.TestCase):
         self.assertEqual(1, len(customer.credit_cards))
         self.assertEqual(customer.credit_cards[0].token, card.token)
 
-    @raises_with_regexp(NotFoundError, "payment method with nonce .* or not found")
     def test_from_nonce_with_unlocked_nonce_pointing_to_shared_card(self):
-        config = Configuration.instantiate()
+        with self.assertRaisesRegex(NotFoundError, "payment method with nonce .* or not found"):
+            config = Configuration.instantiate()
+
+            client_token = TestHelper.generate_decoded_client_token()
+            authorization_fingerprint = json.loads(client_token)["authorizationFingerprint"]
+            http = ClientApiHttp(config, {
+                "authorization_fingerprint": authorization_fingerprint,
+                "shared_customer_identifier": "fake_identifier",
+                "shared_customer_identifier_type": "testing"
+            })
 
-        client_token = TestHelper.generate_decoded_client_token()
-        authorization_fingerprint = json.loads(client_token)["authorizationFingerprint"]
-        http = ClientApiHttp(config, {
-            "authorization_fingerprint": authorization_fingerprint,
-            "shared_customer_identifier": "fake_identifier",
-            "shared_customer_identifier_type": "testing"
-        })
+            status_code, response = http.add_card({
+                "credit_card": {
+                    "number": "4111111111111111",
+                    "expiration_month": "11",
+                    "expiration_year": "2099",
+                },
+                "share": True
+            })
+            self.assertEqual(201, status_code)
+            nonce = json.loads(response)["creditCards"][0]["nonce"]
 
-        status_code, response = http.add_card({
-            "credit_card": {
-                "number": "4111111111111111",
-                "expiration_month": "11",
-                "expiration_year": "2099",
-            },
-            "share": True
-        })
-        self.assertEqual(201, status_code)
-        nonce = json.loads(response)["creditCards"][0]["nonce"]
+            CreditCard.from_nonce(nonce)
 
-        CreditCard.from_nonce(nonce)
 
-    @raises_with_regexp(NotFoundError, ".* consumed .*")
     def test_from_nonce_with_consumed_nonce(self):
-        config = Configuration.instantiate()
-        customer = Customer.create().customer
-
-        client_token = TestHelper.generate_decoded_client_token({
-            "customer_id": customer.id,
-        })
-        authorization_fingerprint = json.loads(client_token)["authorizationFingerprint"]
-        http = ClientApiHttp(config, {
-            "authorization_fingerprint": authorization_fingerprint,
-            "shared_customer_identifier": "fake_identifier",
-            "shared_customer_identifier_type": "testing"
-        })
-
-        status_code, response = http.add_card({
-            "credit_card": {
-                "number": "4111111111111111",
-                "expiration_month": "11",
-                "expiration_year": "2099",
-            }
-        })
-        self.assertEqual(201, status_code)
-        nonce = json.loads(response)["creditCards"][0]["nonce"]
-
-        CreditCard.from_nonce(nonce)
-        CreditCard.from_nonce(nonce)
-
-    def test_create_from_transparent_redirect(self):
-        customer = Customer.create().customer
-        tr_data = {
-            "credit_card": {
-                "customer_id": customer.id
-            }
-        }
-        post_params = {
-            "tr_data": CreditCard.tr_data_for_create(tr_data, "http://example.com/path?foo=bar"),
-            "credit_card[cardholder_name]": "Card Holder",
-            "credit_card[number]": "4111111111111111",
-            "credit_card[expiration_date]": "05/2012",
-            "credit_card[billing_address][country_code_alpha2]": "MX",
-            "credit_card[billing_address][country_code_alpha3]": "MEX",
-            "credit_card[billing_address][country_code_numeric]": "484",
-            "credit_card[billing_address][country_name]": "Mexico",
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params, CreditCard.transparent_redirect_create_url())
-        result = CreditCard.confirm_transparent_redirect(query_string)
-        self.assertTrue(result.is_success)
-        credit_card = result.credit_card
-        self.assertEqual("411111", credit_card.bin)
-        self.assertEqual("1111", credit_card.last_4)
-        self.assertEqual("05", credit_card.expiration_month)
-        self.assertEqual("2012", credit_card.expiration_year)
-        self.assertEqual(customer.id, credit_card.customer_id)
-        self.assertEqual("MX", credit_card.billing_address.country_code_alpha2)
-        self.assertEqual("MEX", credit_card.billing_address.country_code_alpha3)
-        self.assertEqual("484", credit_card.billing_address.country_code_numeric)
-        self.assertEqual("Mexico", credit_card.billing_address.country_name)
-
-
-    def test_create_from_transparent_redirect_and_make_default(self):
-        customer = Customer.create().customer
-        card1 = CreditCard.create({
-            "customer_id": customer.id,
-            "number": "4111111111111111",
-            "expiration_date": "05/2014",
-            "cvv": "100",
-            "cardholder_name": "John Doe"
-        }).credit_card
-        self.assertTrue(card1.default)
+        with self.assertRaisesRegex(NotFoundError, ".* consumed .*"):
+            config = Configuration.instantiate()
+            customer = Customer.create().customer
 
-        tr_data = {
-            "credit_card": {
+            client_token = TestHelper.generate_decoded_client_token({
                 "customer_id": customer.id,
-                "options": {
-                    "make_default": True
-                }
-            }
-        }
-        post_params = {
-            "tr_data": CreditCard.tr_data_for_create(tr_data, "http://example.com/path?foo=bar"),
-            "credit_card[cardholder_name]": "Card Holder",
-            "credit_card[number]": "4111111111111111",
-            "credit_card[expiration_date]": "05/2012",
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params, CreditCard.transparent_redirect_create_url())
-        card2 = CreditCard.confirm_transparent_redirect(query_string).credit_card
-
-        self.assertFalse(CreditCard.find(card1.token).default)
-        self.assertTrue(card2.default)
-
-    def test_create_from_transparent_redirect_with_error_result(self):
-        customer = Customer.create().customer
-        tr_data = {
-            "credit_card": {
-                "customer_id": customer.id
-            }
-        }
-
-        post_params = {
-            "tr_data": CreditCard.tr_data_for_create(tr_data, "http://example.com/path"),
-            "credit_card[cardholder_name]": "Card Holder",
-            "credit_card[number]": "eleventy",
-            "credit_card[expiration_date]": "y2k"
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params, CreditCard.transparent_redirect_create_url())
-        result = CreditCard.confirm_transparent_redirect(query_string)
-        self.assertFalse(result.is_success)
-
-        credit_card_number_errors = result.errors.for_object("credit_card").on("number")
-        self.assertEqual(1, len(credit_card_number_errors))
-        self.assertEqual(ErrorCodes.CreditCard.NumberHasInvalidLength, credit_card_number_errors[0].code)
-
-        expiration_date_errors = result.errors.for_object("credit_card").on("expiration_date")
-        self.assertEqual(1, len(expiration_date_errors))
-        self.assertEqual(ErrorCodes.CreditCard.ExpirationDateIsInvalid, expiration_date_errors[0].code)
-
-    def test_update_from_transparent_redirect_with_successful_result(self):
-        old_token = str(random.randint(1, 1000000))
-        new_token = str(random.randint(1, 1000000))
-        credit_card = Customer.create({
-            "credit_card": {
-                "cardholder_name": "Old Cardholder Name",
-                "number": "4111111111111111",
-                "expiration_date": "05/2012",
-                "token": old_token
-            }
-        }).customer.credit_cards[0]
-
-        tr_data = {
-            "payment_method_token": old_token,
-            "credit_card": {
-                "token": new_token
-            }
-        }
-
-        post_params = {
-            "tr_data": CreditCard.tr_data_for_update(tr_data, "http://example.com/path"),
-            "credit_card[cardholder_name]": "New Cardholder Name",
-            "credit_card[expiration_date]": "05/2014"
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params, CreditCard.transparent_redirect_update_url())
-        result = CreditCard.confirm_transparent_redirect(query_string)
-        self.assertTrue(result.is_success)
-        credit_card = result.credit_card
-        self.assertEqual(new_token, credit_card.token)
-        self.assertEqual("411111", credit_card.bin)
-        self.assertEqual("1111", credit_card.last_4)
-        self.assertEqual("05", credit_card.expiration_month)
-        self.assertEqual("2014", credit_card.expiration_year)
-
-    def test_update_from_transparent_redirect_and_make_default(self):
-        customer = Customer.create({
-            "credit_card": {
-                "number": "4111111111111111",
-                "expiration_date": "05/2012"
-            }
-        }).customer
-        card1 = customer.credit_cards[0]
-
-        card2 = CreditCard.create({
-            "customer_id": customer.id,
-            "number": "4111111111111111",
-            "expiration_date": "05/2014",
-        }).credit_card
-
-        self.assertTrue(card1.default)
-        self.assertFalse(card2.default)
-
-        tr_data = {
-            "payment_method_token": card2.token,
-            "credit_card": {
-                "options": {
-                    "make_default": True
-                }
-            }
-        }
-
-        post_params = {
-            "tr_data": CreditCard.tr_data_for_update(tr_data, "http://example.com/path"),
-            "credit_card[cardholder_name]": "New Cardholder Name",
-            "credit_card[expiration_date]": "05/2014"
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params, CreditCard.transparent_redirect_update_url())
-        CreditCard.confirm_transparent_redirect(query_string)
-
-        self.assertFalse(CreditCard.find(card1.token).default)
-        self.assertTrue(CreditCard.find(card2.token).default)
-
-    def test_update_from_transparent_redirect_and_update_existing_billing_address(self):
-        customer = Customer.create({
-            "credit_card": {
-                "number": "4111111111111111",
-                "expiration_date": "05/2012",
-                "billing_address": {
-                    "street_address": "123 Old St",
-                    "locality": "Chicago",
-                    "region": "Illinois",
-                    "postal_code": "60621"
-                }
-            }
-        }).customer
-        card = customer.credit_cards[0]
+            })
+            authorization_fingerprint = json.loads(client_token)["authorizationFingerprint"]
+            http = ClientApiHttp(config, {
+                "authorization_fingerprint": authorization_fingerprint,
+                "shared_customer_identifier": "fake_identifier",
+                "shared_customer_identifier_type": "testing"
+            })
 
-        tr_data = {
-            "payment_method_token": card.token,
-            "credit_card": {
-                "billing_address": {
-                    "street_address": "123 New St",
-                    "locality": "Columbus",
-                    "region": "Ohio",
-                    "postal_code": "43215",
-                    "options": {
-                        "update_existing": True
-                    }
+            status_code, response = http.add_card({
+                "credit_card": {
+                    "number": "4111111111111111",
+                    "expiration_month": "11",
+                    "expiration_year": "2099",
                 }
-            }
-        }
-
-        post_params = {
-            "tr_data": CreditCard.tr_data_for_update(tr_data, "http://example.com/path")
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params, CreditCard.transparent_redirect_update_url())
-
-        CreditCard.confirm_transparent_redirect(query_string)
-
-        self.assertEqual(1, len(Customer.find(customer.id).addresses))
-        updated_card = CreditCard.find(card.token)
-        self.assertEqual("123 New St", updated_card.billing_address.street_address)
-        self.assertEqual("Columbus", updated_card.billing_address.locality)
-        self.assertEqual("Ohio", updated_card.billing_address.region)
-        self.assertEqual("43215", updated_card.billing_address.postal_code)
-
-    def test_update_from_transparent_redirect_with_error_result(self):
-        old_token = str(random.randint(1, 1000000))
-        Customer.create({
-            "credit_card": {
-                "cardholder_name": "Old Cardholder Name",
-                "number": "4111111111111111",
-                "expiration_date": "05/2012",
-                "token": old_token
-            }
-        })
-
-        tr_data = {
-            "payment_method_token": old_token,
-            "credit_card": {
-                "token": "bad token"
-            }
-        }
-
-        post_params = {
-            "tr_data": CreditCard.tr_data_for_update(tr_data, "http://example.com/path"),
-            "credit_card[cardholder_name]": "New Cardholder Name",
-            "credit_card[expiration_date]": "05/2014"
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params, CreditCard.transparent_redirect_update_url())
-        result = CreditCard.confirm_transparent_redirect(query_string)
-        self.assertFalse(result.is_success)
+            })
+            self.assertEqual(201, status_code)
+            nonce = json.loads(response)["creditCards"][0]["nonce"]
 
-        credit_card_token_errors = result.errors.for_object("credit_card").on("token")
-        self.assertEqual(1, len(credit_card_token_errors))
-        self.assertEqual(ErrorCodes.CreditCard.TokenInvalid, credit_card_token_errors[0].code)
+            CreditCard.from_nonce(nonce)
+            CreditCard.from_nonce(nonce)
 
     def test_expired_can_iterate_over_all_items(self):
+        year = datetime.now().year - 3
         customer_id = Customer.all().first.id
 
         for _ in range(110 - CreditCard.expired().maximum_size):
             CreditCard.create({
                 "customer_id": customer_id,
                 "number": "4111111111111111",
-                "expiration_date": "01/2015"
+                "expiration_date": "01/" + str(year)
             })
 
         collection = CreditCard.expired()
@@ -1390,3 +1290,22 @@ class TestCreditCard(unittest.TestCase):
         self.assertEqual(updated_result.is_success, True)
         self.assertEqual("debit", updated_result.credit_card.verifications[0]["credit_card"]["account_type"])
         self.assertEqual("credit", updated_result.credit_card.verifications[1]["credit_card"]["account_type"])
+
+    def test_network_tokenized_credit_card(self):
+        credit_card = CreditCard.find("network_tokenized_credit_card")
+
+        self.assertEqual(credit_card.is_network_tokenized, True)
+
+    def test_non_network_tokenized_credit_card(self):
+        customer = Customer.create().customer
+        card = CreditCard.create({
+            "customer_id": customer.id,
+            "number": "4111111111111111",
+            "expiration_date": "05/2014",
+            "cvv": "100",
+            "cardholder_name": "John Doe"
+        }).credit_card
+
+        credit_card = CreditCard.find(card.token)
+
+        self.assertEqual(credit_card.is_network_tokenized, False)
diff --git a/tests/integration/test_credit_card_verification.py b/tests/integration/test_credit_card_verification.py
index ad3d0b5..5c07f9f 100644
--- a/tests/integration/test_credit_card_verification.py
+++ b/tests/integration/test_credit_card_verification.py
@@ -125,6 +125,7 @@ class TestCreditCardVerfication(unittest.TestCase):
         created_verification = customer.credit_card_verification
         found_verification = CreditCardVerification.find(created_verification.id)
         self.assertEqual(created_verification, found_verification)
+        self.assertNotEqual(None, found_verification.graphql_id)
 
     def test_verification_not_found(self):
         self.assertRaises(NotFoundError, CreditCardVerification.find,
@@ -168,3 +169,85 @@ class TestCreditCardVerfication(unittest.TestCase):
         self.assertEqual("XX", verification.network_response_code)
         self.assertEqual("sample network response text", verification.network_response_text)
 
+    def test_create_success_network_transaction_id(self):
+        result = CreditCardVerification.create({
+            "credit_card": {
+                "number": CreditCardNumbers.Visa,
+                "cardholder_name": "John Smith",
+                "expiration_date": "05/2012"
+            },
+        })
+
+        self.assertTrue(result.is_success)
+        verification = result.verification
+        self.assertRegex(verification.network_transaction_id, r'\d{15}')
+
+    def test_verification_with_three_d_secure_authentication_id_with_nonce(self):
+        config = Configuration(
+                environment=Environment.Development,
+                merchant_id="integration_merchant_id",
+                public_key="integration_public_key",
+                private_key="integration_private_key"
+                )
+        gateway = BraintreeGateway(config)
+        credit_card = {
+                "credit_card": {
+                    "number": CreditCardNumbers.Visa,
+                    "expiration_month": "05",
+                    "expiration_year": "2029",
+                    }
+                }
+        nonce = TestHelper.generate_three_d_secure_nonce(gateway, credit_card)
+        found_nonce = PaymentMethodNonce.find(nonce)
+        three_d_secure_info = found_nonce.three_d_secure_info
+
+        result = CreditCardVerification.create({
+            "credit_card": {
+                "number": CreditCardNumbers.Visa,
+                "cardholder_name": "John Smith",
+                },
+            "options": {"merchant_account_id": TestHelper.three_d_secure_merchant_account_id},
+            "payment_method_nonce": nonce,
+            "three_d_secure_authentication_id": three_d_secure_info.three_d_secure_authentication_id
+            })
+
+        self.assertTrue(result.is_success)
+        verification = result.verification
+        self.assertEqual("1000", verification.processor_response_code)
+        self.assertEqual(ProcessorResponseTypes.Approved, verification.processor_response_type)
+
+    def test_verification_with_three_d_secure_pass_thru(self):
+        result = CreditCardVerification.create({
+            "credit_card": {
+                "number": CreditCardNumbers.Visa,
+                "cardholder_name": "John Smith",
+                "expiration_date": "05/2029"},
+            "options": {"merchant_account_id": TestHelper.three_d_secure_merchant_account_id},
+            "three_d_secure_pass_thru": {
+                "eci_flag": "02",
+                "cavv": "some_cavv",
+                "xid": "some_xid",
+                "three_d_secure_version": "1.0.2",
+                "authentication_response": "Y",
+                "directory_response": "Y",
+                "cavv_algorithm": "2",
+                "ds_transaction_id": "some_ds_id"}})
+
+        self.assertTrue(result.is_success)
+        verification = result.verification
+        self.assertEqual("1000", verification.processor_response_code)
+        self.assertEqual(ProcessorResponseTypes.Approved, verification.processor_response_type)
+
+    def test_verification_with_intended_transaction_source(self):
+        result = CreditCardVerification.create({
+            "credit_card": {
+                "number": CreditCardNumbers.Visa,
+                "cardholder_name": "John Smith",
+                "expiration_date": "05/2029"},
+            "intended_transaction_source": "installment"
+            })
+
+        self.assertTrue(result.is_success)
+        verification = result.verification
+        self.assertEqual("1000", verification.processor_response_code)
+        self.assertEqual(ProcessorResponseTypes.Approved, verification.processor_response_type)
diff --git a/tests/integration/test_credit_card_verification_search.py b/tests/integration/test_credit_card_verification_search.py
index b787262..a9df27e 100644
--- a/tests/integration/test_credit_card_verification_search.py
+++ b/tests/integration/test_credit_card_verification_search.py
@@ -29,6 +29,29 @@ class TestVerificationSearch(unittest.TestCase):
         self.assertEqual(1, found_verifications.maximum_size)
         self.assertEqual(verification_id, found_verifications.first.id)
 
+    def test_search_on_payment_method_token(self):
+        customer_id = "%s" % random.randint(1, 10000)
+        payment_method_token = customer_id + "token"
+
+        result = Customer.create({
+            "id": customer_id,
+            "credit_card": {
+                "token": payment_method_token,
+                "expiration_date": "10/2018",
+                "number": CreditCardNumbers.Visa,
+                "options": {
+                    "verify_card": True
+                }
+            }
+        })
+
+        found_verifications = CreditCardVerification.search(
+            CreditCardVerificationSearch.payment_method_token == payment_method_token
+        )
+
+        self.assertEqual(1, found_verifications.maximum_size)
+        self.assertEqual(payment_method_token, found_verifications.first.credit_card["token"])
+
     def test_all_text_fields(self):
         email = "mark.a@example.com"
         cardholder_name = "Tom %s" % random.randint(1, 10000)
diff --git a/tests/integration/test_customer.py b/tests/integration/test_customer.py
index 5d838eb..7e2ce81 100644
--- a/tests/integration/test_customer.py
+++ b/tests/integration/test_customer.py
@@ -1,6 +1,5 @@
 # -*- coding: latin-1 -*-
 from tests.test_helper import *
-import braintree.test.venmo_sdk as venmo_sdk
 from braintree.test.nonces import Nonces
 
 class TestCustomer(unittest.TestCase):
@@ -58,7 +57,16 @@ class TestCustomer(unittest.TestCase):
         self.assertEqual("www.email.com", customer.website)
         self.assertNotEqual(None, customer.id)
 
-    def test_create_with_device_session_id_and_fraud_merchant_id(self):
+    def test_create_with_tax_identifiers(self):
+        result = Customer.create({
+            "tax_identifiers": [
+                {"country_code": "US", "identifier": "123456789"},
+                {"country_code": "GB", "identifier": "987654321"}]
+            })
+
+        self.assertTrue(result.is_success)
+
+    def test_create_with_device_data(self):
         result = Customer.create({
             "first_name": "Joe",
             "last_name": "Brown",
@@ -67,17 +75,40 @@ class TestCustomer(unittest.TestCase):
             "phone": "312.555.1234",
             "fax": "614.555.5678",
             "website": "www.email.com",
+            "device_data": "abc123",
             "credit_card": {
                 "number": "4111111111111111",
                 "expiration_date": "05/2010",
-                "cvv": "100",
-                "device_session_id": "abc123",
-                "fraud_merchant_id": "456"
-            }
-        })
+                "cvv": "100"
+                }
+            })
 
         self.assertTrue(result.is_success)
 
+    def test_create_with_device_session_id_and_fraud_merchant_id_sends_deprecation_warning(self):
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            result = Customer.create({
+                "first_name": "Joe",
+                "last_name": "Brown",
+                "company": "Fake Company",
+                "email": "joe@email.com",
+                "phone": "312.555.1234",
+                "fax": "614.555.5678",
+                "website": "www.email.com",
+                "device_session_id": "abc123",
+                "fraud_merchant_id": "456",
+                "credit_card": {
+                    "number": "4111111111111111",
+                    "expiration_date": "05/2010",
+                    "cvv": "100"
+                    }
+                })
+
+            self.assertTrue(result.is_success)
+            assert len(w) > 0
+            assert issubclass(w[-1].category, DeprecationWarning)
+
     def test_create_with_risk_data_security_parameters(self):
         result = Customer.create({
             "first_name": "Joe",
@@ -97,6 +128,39 @@ class TestCustomer(unittest.TestCase):
 
         self.assertTrue(result.is_success)
 
+    def test_create_includes_risk_data_when_skip_advanced_fraud_checking_is_false(self):
+        with FraudProtectionEnterpriseIntegrationMerchant():
+            result = Customer.create({
+                "credit_card": {
+                    "number": "4111111111111111",
+                    "expiration_date": "05/2014",
+                    "options": {
+                        "verify_card": True,
+                        "skip_advanced_fraud_checking": False
+                        },
+                    },
+                })
+
+            self.assertTrue(result.is_success)
+            verification = result.customer.credit_cards[0].verification
+            self.assertIsInstance(verification.risk_data, RiskData)
+
+    def test_create_does_not_include_risk_data_when_skip_advanced_fraud_checking_is_true(self):
+        with FraudProtectionEnterpriseIntegrationMerchant():
+            result = Customer.create({
+                "credit_card": {
+                    "number": "4111111111111111",
+                    "expiration_date": "05/2014",
+                    "options": {
+                        "verify_card": True,
+                        "skip_advanced_fraud_checking": True
+                        },
+                    },
+                })
+
+            self.assertTrue(result.is_success)
+            verification = result.customer.credit_cards[0].verification
+            self.assertIsNone(verification.risk_data)
 
     def test_create_and_update_with_verification_account_type(self):
         result_with_account_type_credit = Customer.create({
@@ -217,6 +281,19 @@ class TestCustomer(unittest.TestCase):
         customer = result.customer
         self.assertEqual(1, len(customer.apple_pay_cards))
         self.assertIsInstance(customer.apple_pay_cards[0], ApplePayCard)
+        self.assertNotEqual(customer.apple_pay_cards[0].bin, None)
+        self.assertNotEqual(customer.apple_pay_cards[0].token, None)
+        self.assertNotEqual(customer.apple_pay_cards[0].prepaid, None)
+        self.assertNotEqual(customer.apple_pay_cards[0].healthcare, None)
+        self.assertNotEqual(customer.apple_pay_cards[0].debit, None)
+        self.assertNotEqual(customer.apple_pay_cards[0].durbin_regulated, None)
+        self.assertNotEqual(customer.apple_pay_cards[0].commercial, None)
+        self.assertNotEqual(customer.apple_pay_cards[0].payroll, None)
+        self.assertNotEqual(customer.apple_pay_cards[0].issuing_bank, None)
+        self.assertNotEqual(customer.apple_pay_cards[0].country_of_issuance, None)
+        self.assertNotEqual(customer.apple_pay_cards[0].product_id, None)
+        self.assertNotEqual(customer.apple_pay_cards[0].last_4, None)
+        self.assertNotEqual(customer.apple_pay_cards[0].card_type, None)
 
     def test_create_with_three_d_secure_nonce(self):
         result = Customer.create({
@@ -242,6 +319,42 @@ class TestCustomer(unittest.TestCase):
         self.assertEqual("05", three_d_secure_info.eci_flag)
         self.assertEqual("1.0.2", three_d_secure_info.three_d_secure_version)
 
+    def test_create_with_three_d_secure_pass_thru(self):
+        result = Customer.create({
+            "payment_method_nonce": Nonces.Transactable,
+            "credit_card": {
+                "three_d_secure_pass_thru": {
+                    "three_d_secure_version": "1.1.1",
+                    "eci_flag": "05",
+                    "cavv": "some-cavv",
+                    "xid": "some-xid"
+                    },
+                "options": {
+                    "verify_card": True,
+                    },
+                },
+            })
+
+        self.assertTrue(result.is_success)
+
+    def test_create_with_three_d_secure_pass_thru_without_eci_flag(self):
+        result = Customer.create({
+            "payment_method_nonce": Nonces.Transactable,
+            "credit_card": {
+                "three_d_secure_pass_thru": {
+                    "three_d_secure_version": "1.1.1",
+                    "cavv": "some-cavv",
+                    "xid": "some-xid"
+                    },
+                "options": {
+                    "verify_card": True,
+                    },
+            },
+        })
+
+        self.assertFalse(result.is_success)
+        self.assertEqual("EciFlag is required.", result.message)
+
     def test_create_with_android_pay_proxy_card_nonce(self):
         result = Customer.create({"payment_method_nonce": Nonces.AndroidPayCardDiscover})
         self.assertTrue(result.is_success)
@@ -257,6 +370,19 @@ class TestCustomer(unittest.TestCase):
         customer = result.customer
         self.assertEqual(1, len(customer.android_pay_cards))
         self.assertIsInstance(customer.android_pay_cards[0], AndroidPayCard)
+        self.assertNotEqual(customer.android_pay_cards[0].bin, None)
+        self.assertNotEqual(customer.android_pay_cards[0].token, None)
+        self.assertNotEqual(customer.android_pay_cards[0].prepaid, None)
+        self.assertNotEqual(customer.android_pay_cards[0].healthcare, None)
+        self.assertNotEqual(customer.android_pay_cards[0].debit, None)
+        self.assertNotEqual(customer.android_pay_cards[0].durbin_regulated, None)
+        self.assertNotEqual(customer.android_pay_cards[0].commercial, None)
+        self.assertNotEqual(customer.android_pay_cards[0].payroll, None)
+        self.assertNotEqual(customer.android_pay_cards[0].issuing_bank, None)
+        self.assertNotEqual(customer.android_pay_cards[0].country_of_issuance, None)
+        self.assertNotEqual(customer.android_pay_cards[0].product_id, None)
+        self.assertNotEqual(customer.android_pay_cards[0].last_4, None)
+        self.assertNotEqual(customer.android_pay_cards[0].card_type, None)
 
     def test_create_with_amex_express_checkout_card_nonce(self):
         result = Customer.create({"payment_method_nonce": Nonces.AmexExpressCheckoutCard})
@@ -289,8 +415,8 @@ class TestCustomer(unittest.TestCase):
         self.assertEqual(1, len(customer.us_bank_accounts))
         self.assertIsInstance(customer.us_bank_accounts[0], UsBankAccount)
 
-    def test_create_with_paypal_future_payments_nonce(self):
-        result = Customer.create({"payment_method_nonce": Nonces.PayPalFuturePayment})
+    def test_create_with_paypal_billing_agreements_nonce(self):
+        result = Customer.create({"payment_method_nonce": Nonces.PayPalBillingAgreement})
         self.assertTrue(result.is_success)
 
         customer = result.customer
@@ -415,6 +541,64 @@ class TestCustomer(unittest.TestCase):
         self.assertEqual("1111", credit_card.last_4)
         self.assertEqual("05/2010", credit_card.expiration_date)
 
+    def test_create_for_raw_apple_pay(self):
+        result = Customer.create({
+            "first_name": "Rickey",
+            "last_name": "Crabapple",
+            "apple_pay_card": {
+                "number": "4111111111111111",
+                "expiration_month": "05",
+                "expiration_year": "2014",
+                "eci_indicator": "5",
+                "cryptogram": "01010101010101010101",
+                "cardholder_name": "John Doe",
+                "billing_address": {
+                    "postal_code": 83704
+                }
+            }
+        })
+
+        customer = result.customer
+        self.assertEqual("Rickey", customer.first_name)
+        self.assertEqual("Crabapple", customer.last_name)
+
+        self.assertTrue(result.is_success)
+        apple_pay_card = result.customer.apple_pay_cards[0]
+        self.assertEqual("411111", apple_pay_card.bin)
+        self.assertEqual("1111", apple_pay_card.last_4)
+        self.assertEqual("2014", apple_pay_card.expiration_year)
+        self.assertEqual("05", apple_pay_card.expiration_month)
+        self.assertEqual(apple_pay_card.billing_address["postal_code"], "83704")
+
+    def test_create_for_raw_apple_pay_with_invalid_params(self):
+        result = Customer.create({
+            "first_name": "Rickey",
+            "last_name": "Crabapple",
+            "apple_pay_card": {
+                "number": "4111111111111111",
+                "expiration_year": "2014",
+                "expiration_month": "01",
+                "eci_indicator": "5",
+                "cryptogram": "01010101010101010101",
+                "cardholder_name": "John Doe",
+                "billing_address": {
+                    "street_address": "head 100 yds south once you hear the beehive",
+                    "postal_code": '$$$$',
+                    "country_code_alpha2": "UX",
+                }
+            }
+        })
+#
+        errors = result.errors.for_object("apple_pay").on("billing_address")
+        self.assertFalse(result.is_success)
+
+        postal_errors = result.errors.for_object("apple_pay").for_object("billing_address").on("postal_code")
+        country_errors = result.errors.for_object("apple_pay").for_object("billing_address").on("country_code_alpha2")
+        self.assertEqual(1, len(postal_errors))
+        self.assertEqual(1, len(country_errors))
+        self.assertEqual(ErrorCodes.Address.CountryCodeAlpha2IsNotAccepted, country_errors[0].code)
+        self.assertEqual(ErrorCodes.Address.PostalCodeInvalidCharacters, postal_errors[0].code)
+
     def test_create_customer_and_verify_payment_method(self):
         result = Customer.create({
             "first_name": "Mike",
@@ -555,34 +739,6 @@ class TestCustomer(unittest.TestCase):
         self.assertEqual(1, len(custom_fields_errors))
         self.assertEqual(ErrorCodes.Customer.CustomFieldIsInvalid, custom_fields_errors[0].code)
 
-    def test_create_with_venmo_sdk_session(self):
-        result = Customer.create({
-            "first_name": "Jack",
-            "last_name": "Kennedy",
-            "credit_card": {
-                "number": "4111111111111111",
-                "expiration_date": "05/2010",
-                "options": {
-                    "venmo_sdk_session": venmo_sdk.Session
-                }
-            }
-        })
-
-        self.assertTrue(result.is_success)
-        self.assertFalse(result.customer.credit_cards[0].venmo_sdk)
-
-    def test_create_with_venmo_sdk_payment_method_code(self):
-        result = Customer.create({
-            "first_name": "Jack",
-            "last_name": "Kennedy",
-            "credit_card": {
-                "venmo_sdk_payment_method_code": venmo_sdk.generate_test_payment_method_code("4111111111111111")
-            }
-        })
-
-        self.assertTrue(result.is_success)
-        self.assertEqual("411111", result.customer.credit_cards[0].bin)
-
     def test_create_with_payment_method_nonce(self):
         config = Configuration.instantiate()
         authorization_fingerprint = json.loads(TestHelper.generate_decoded_client_token())["authorizationFingerprint"]
@@ -616,11 +772,31 @@ class TestCustomer(unittest.TestCase):
 
         self.assertTrue(result.is_success)
 
-    @raises(NotFoundError)
     def test_delete_with_invalid_customer(self):
-        customer = Customer.create().customer
-        Customer.delete(customer.id)
-        Customer.delete(customer.id)
+        with self.assertRaises(NotFoundError):
+            customer = Customer.create().customer
+            Customer.delete(customer.id)
+            Customer.delete(customer.id)
+
+    def test_delete_payment_method_with_path_traversal(self):
+        try:
+            customer = Customer.create().customer
+            credit_card = CreditCard.create({
+                "customer_id": customer.id,
+                "number": "4111111111111111",
+                "expiration_date": "05/2009",
+                "cvv": "100",
+                "cardholder_name": "John Doe"
+            }).credit_card
+            Customer.delete("../payment_methods/any/{}".format(credit_card.token))
+        except NotFoundError:
+            pass
+
+        payment_method = PaymentMethod.find(credit_card.token)
+        self.assertNotEqual(None, payment_method)
+        self.assertEqual(credit_card.token, payment_method.token)
+        self.assertEqual(credit_card.customer_id, payment_method.customer_id)
+        self.assertEqual("John Doe", payment_method.cardholder_name)
 
     def test_find_with_valid_customer(self):
         customer = Customer.create({
@@ -632,6 +808,7 @@ class TestCustomer(unittest.TestCase):
         self.assertEqual(customer.id, found_customer.id)
         self.assertEqual(customer.first_name, found_customer.first_name)
         self.assertEqual(customer.last_name, found_customer.last_name)
+        self.assertNotEqual(None, customer.graphql_id)
 
     def test_find_customer_with_us_bank_account(self):
         customer = Customer.create({
@@ -650,9 +827,9 @@ class TestCustomer(unittest.TestCase):
         self.assertEqual(1, len(found_customer.us_bank_accounts))
         self.assertIsInstance(found_customer.us_bank_accounts[0], UsBankAccount)
 
-    @raises_with_regexp(NotFoundError, "customer with id 'badid' not found")
     def test_find_with_invalid_customer(self):
-        Customer.find("badid")
+        with self.assertRaisesRegex(NotFoundError, "customer with id 'badid' not found"):
+            Customer.find("badid")
 
     def test_find_customer_with_all_filterable_associations_filtered_out(self):
         customer = Customer.create({
@@ -915,13 +1092,49 @@ class TestCustomer(unittest.TestCase):
         customer = Customer.create().customer
 
         result = Customer.update(customer.id, {
-            "payment_method_nonce": Nonces.PayPalFuturePayment
+            "payment_method_nonce": Nonces.PayPalBillingAgreement
         })
         self.assertTrue(result.is_success)
 
         customer = result.customer
         self.assertNotEqual(None, customer.paypal_accounts[0])
 
+    def test_update_with_invalid_three_d_secure_pass_thru_params(self):
+        customer = Customer.create().customer
+        result = Customer.update(customer.id, {
+            "payment_method_nonce": Nonces.Transactable,
+            "credit_card": {
+                "three_d_secure_pass_thru": {
+                    "eci_flag": "05",
+                    "cavv": "some-cavv",
+                    "xid": "some-xid"
+                    },
+                "options": {
+                    "verify_card": True,
+                    },
+                },
+            })
+        self.assertFalse(result.is_success)
+        self.assertEqual("ThreeDSecureVersion is required.", result.message)
+
+    def test_update_with_valid_three_d_secure_pass_thru_params(self):
+        customer = Customer.create().customer
+        result = Customer.update(customer.id, {
+            "payment_method_nonce": Nonces.Transactable,
+            "credit_card": {
+                "three_d_secure_pass_thru": {
+                    "eci_flag": "05",
+                    "cavv": "some-cavv",
+                    "three_d_secure_version": "1.2.0",
+                    "xid": "some-xid"
+                    },
+                "options": {
+                    "verify_card": True,
+                    },
+                },
+            })
+        self.assertTrue(result.is_success)
+
     def test_update_with_paypal_one_time_nonce_fails(self):
         customer = Customer.create().customer
         result = Customer.update(customer.id, {
@@ -1003,158 +1216,115 @@ class TestCustomer(unittest.TestCase):
 
         self.assertTrue(result.is_success)
 
-    def test_create_from_transparent_redirect_with_successful_result(self):
-        tr_data = {
-            "customer": {
-                "first_name": "John",
-                "last_name": "Doe",
-                "company": "Doe Co",
-            }
-        }
-        post_params = {
-            "tr_data": Customer.tr_data_for_create(tr_data, "http://example.com/path"),
-            "customer[email]": "john@doe.com",
-            "customer[phone]": "312.555.2323",
-            "customer[fax]": "614.555.5656",
-            "customer[website]": "www.johndoe.com",
-            "customer[credit_card][number]": "4111111111111111",
-            "customer[credit_card][expiration_date]": "05/2012",
-            "customer[credit_card][billing_address][country_code_alpha2]": "MX",
-            "customer[credit_card][billing_address][country_code_alpha3]": "MEX",
-            "customer[credit_card][billing_address][country_code_numeric]": "484",
-            "customer[credit_card][billing_address][country_name]": "Mexico",
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params, Customer.transparent_redirect_create_url())
-        result = Customer.confirm_transparent_redirect(query_string)
-        self.assertTrue(result.is_success)
-        customer = result.customer
-        self.assertEqual("John", customer.first_name)
-        self.assertEqual("Doe", customer.last_name)
-        self.assertEqual("Doe Co", customer.company)
-        self.assertEqual("john@doe.com", customer.email)
-        self.assertEqual("312.555.2323", customer.phone)
-        self.assertEqual("614.555.5656", customer.fax)
-        self.assertEqual("www.johndoe.com", customer.website)
-        self.assertEqual("05/2012", customer.credit_cards[0].expiration_date)
-        self.assertEqual("MX", customer.credit_cards[0].billing_address.country_code_alpha2)
-        self.assertEqual("MEX", customer.credit_cards[0].billing_address.country_code_alpha3)
-        self.assertEqual("484", customer.credit_cards[0].billing_address.country_code_numeric)
-        self.assertEqual("Mexico", customer.credit_cards[0].billing_address.country_name)
-
-    def test_create_from_transparent_redirect_with_error_result(self):
-        tr_data = {
-            "customer": {
-                "company": "Doe Co",
-            }
-        }
-        post_params = {
-            "tr_data": Customer.tr_data_for_create(tr_data, "http://example.com/path"),
-            "customer[email]": "john#doe.com",
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params, Customer.transparent_redirect_create_url())
-        result = Customer.confirm_transparent_redirect(query_string)
-        self.assertFalse(result.is_success)
-
-        email_errors = result.errors.for_object("customer").on("email")
-        self.assertEqual(1, len(email_errors))
-        self.assertEqual(ErrorCodes.Customer.EmailIsInvalid, email_errors[0].code)
-
-    def test_update_from_transparent_redirect_with_successful_result(self):
-        customer = Customer.create({
-            "first_name": "Jane",
-        }).customer
+    def test_update_includes_risk_data_when_skip_advanced_fraud_checking_is_false(self):
+        with FraudProtectionEnterpriseIntegrationMerchant():
+            customer = Customer.create().customer
 
-        tr_data = {
-            "customer_id": customer.id,
-            "customer": {
-                "first_name": "John",
-            }
-        }
-        post_params = {
-            "tr_data": Customer.tr_data_for_update(tr_data, "http://example.com/path"),
-            "customer[email]": "john@doe.com",
-        }
+            result = Customer.update(customer.id, {
+                "credit_card": {
+                    "number": "4111111111111111",
+                    "expiration_date": "05/2014",
+                    "cvv": "100",
+                    "cardholder_name": "John Doe",
+                    "options": {
+                        "verify_card": True,
+                        "skip_advanced_fraud_checking": False
+                        }
+                    }
+                })
 
-        query_string = TestHelper.simulate_tr_form_post(post_params, Customer.transparent_redirect_update_url())
-        result = Customer.confirm_transparent_redirect(query_string)
-        self.assertTrue(result.is_success)
-        customer = result.customer
-        self.assertEqual("John", customer.first_name)
-        self.assertEqual("john@doe.com", customer.email)
+            self.assertTrue(result.is_success)
+            verification = result.customer.credit_cards[0].verification
+            self.assertIsInstance(verification.risk_data, RiskData)
 
-    def test_update_with_nested_values_via_transparent_redirect(self):
-        customer = Customer.create({
-            "first_name": "Joe",
-            "last_name": "Brown",
-            "credit_card": {
-                "number": "4111111111111111",
-                "expiration_date": "10/10",
-                "billing_address": {
-                    "postal_code": "11111"
-                }
-            }
-        }).customer
-        credit_card = customer.credit_cards[0]
-        address = credit_card.billing_address
+    def test_update_does_not_include_risk_data_when_skip_advanced_fraud_checking_is_true(self):
+        with FraudProtectionEnterpriseIntegrationMerchant():
+            customer = Customer.create().customer
 
-        tr_data = {
-            "customer_id": customer.id,
-            "customer": {
-                "first_name": "Joe",
-                "last_name": "Brown",
+            result = Customer.update(customer.id, {
                 "credit_card": {
-                    "expiration_date": "12/12",
+                    "number": "4111111111111111",
+                    "expiration_date": "05/2014",
+                    "cvv": "100",
+                    "cardholder_name": "John Doe",
+                    "options": {
+                        "verify_card": True,
+                        "skip_advanced_fraud_checking": True
+                        }
+                    }
+                })
+
+            self.assertTrue(result.is_success)
+            verification = result.customer.credit_cards[0].verification
+            self.assertIsNone(verification.risk_data)
+
+    def test_update_works_for_raw_apple_pay(self):
+        with FraudProtectionEnterpriseIntegrationMerchant():
+            customer = Customer.create().customer
+            secure_token = TestHelper.random_token_block(None)
+
+            result = Customer.update(customer.id, {
+                "apple_pay_card": {
+                    "number": "4111111111111111",
+                    "expiration_month": "05",
+                    "expiration_year": "2014",
+                    "eci_indicator": "0",
+                    "cryptogram": "01010101010101010101",
+                    "cardholder_name": "John Doe",
+                    "token": secure_token,
                     "options": {
-                        "update_existing_token": credit_card.token
+                        "make_default": True
                     },
                     "billing_address": {
-                        "postal_code": "44444",
-                        "options": {
-                            "update_existing": True
-                        }
+                        "street_address": "123 Abc Way",
+                        "locality": "Chicago",
+                        "region": "Illinois",
+                        "postal_code": "60622",
+                        "phone_number": "312.555.1234",
+                        "country_code_alpha2": "US",
+                        "country_code_alpha3": "USA",
+                        "country_code_numeric": "840",
+                        "country_name": "United States of America"
                     }
-                }
-            }
-        }
-        post_params = {
-            "tr_data": Customer.tr_data_for_update(tr_data, "http://example.com/path"),
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params, Customer.transparent_redirect_update_url())
-        updated_customer = Customer.confirm_transparent_redirect(query_string).customer
-        updated_credit_card = CreditCard.find(credit_card.token)
-        updated_address = Address.find(customer.id, address.id)
-
-        self.assertEqual("Joe", updated_customer.first_name)
-        self.assertEqual("Brown", updated_customer.last_name)
-        self.assertEqual("12/2012", updated_credit_card.expiration_date)
-        self.assertEqual("44444", updated_address.postal_code)
+                },
+            })
 
-    def test_update_from_transparent_redirect_with_error_result(self):
+            self.assertTrue(result.is_success)
+            self.assertEqual(secure_token, result.customer.payment_methods[0].token)
+
+            self.assertNotEqual(0, len(result.customer.apple_pay_cards))
+            apple_pay_card = result.customer.apple_pay_cards[0]
+            self.assertTrue(apple_pay_card.default)
+            self.assertEqual(apple_pay_card.expiration_month, "05")
+            self.assertEqual(apple_pay_card.expiration_year, "2014")
+            self.assertEqual(apple_pay_card.cardholder_name, "John Doe")
+            self.assertEqual(apple_pay_card.bin, "411111")
+
+            self.assertEqual(apple_pay_card.billing_address["street_address"], "123 Abc Way")
+            self.assertEqual(apple_pay_card.billing_address["locality"], "Chicago")
+            self.assertEqual(apple_pay_card.billing_address["region"], "Illinois")
+            self.assertEqual(apple_pay_card.billing_address["postal_code"], "60622")
+            self.assertEqual(apple_pay_card.billing_address["phone_number"], "312.555.1234")
+            self.assertEqual(apple_pay_card.billing_address["country_code_alpha2"], "US")
+            self.assertEqual(apple_pay_card.billing_address["country_code_alpha3"], "USA")
+            self.assertEqual(apple_pay_card.billing_address["country_code_numeric"], "840")
+            self.assertEqual(apple_pay_card.billing_address["country_name"], "United States of America")
+
+    def test_update_with_tax_identifiers(self):
         customer = Customer.create({
-            "first_name": "Jane",
-        }).customer
+            "tax_identifiers": [
+                {"country_code": "US", "identifier": "123456789"},
+                {"country_code": "GB", "identifier": "987654321"}]
+            }).customer
 
-        tr_data = {
-            "customer_id": customer.id,
-            "customer": {
-                "first_name": "John",
-            }
-        }
-        post_params = {
-            "tr_data": Customer.tr_data_for_update(tr_data, "http://example.com/path"),
-            "customer[email]": "john#doe.com",
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params, Customer.transparent_redirect_update_url())
-        result = Customer.confirm_transparent_redirect(query_string)
-        self.assertFalse(result.is_success)
+        result = Customer.update(customer.id, {
+            "tax_identifiers": [{
+                "country_code": "GB",
+                "identifier": "567891234"
+                }]
+            })
 
-        customer_email_errors = result.errors.for_object("customer").on("email")
-        self.assertEqual(1, len(customer_email_errors))
-        self.assertEqual(ErrorCodes.Customer.EmailIsInvalid, customer_email_errors[0].code)
+        self.assertTrue(result.is_success)
 
     def test_customer_payment_methods(self):
         customer = Customer("gateway", {
diff --git a/tests/integration/test_dispute_search.py b/tests/integration/test_dispute_search.py
index 63d13f4..b31e11d 100644
--- a/tests/integration/test_dispute_search.py
+++ b/tests/integration/test_dispute_search.py
@@ -71,7 +71,45 @@ class TestDisputeSearch(unittest.TestCase):
         ])
 
         disputes = [dispute for dispute in collection.disputes.items]
-        self.assertEquals(2, len(disputes))
+        self.assertGreaterEqual(len(disputes), 2)
+
+    def test_advanced_search_returns_disputes_by_chargeback_protection_level(self):
+            collection = Dispute.search([
+                DisputeSearch.chargeback_protection_level.in_list([
+                    braintree.Dispute.ChargebackProtectionLevel.Effortless,
+                ])
+            ])
+
+            disputes = [dispute for dispute in collection.disputes.items]
+            self.assertEqual(len(disputes) > 0, True)
+
+            for dispute in disputes:
+                # NEXT_MAJOR_VERSION Remove this assertion when chargeback_protection_level is removed from the SDK
+                self.assertEqual(dispute.chargeback_protection_level, braintree.Dispute.ChargebackProtectionLevel.Effortless)
+                self.assertEqual(dispute.protection_level, braintree.Dispute.ProtectionLevel.EffortlessCBP)
+
+    def test_advanced_search_returns_disputes_by_pre_dispute_program(self):
+            collection = Dispute.search([
+                DisputeSearch.pre_dispute_program.in_list([
+                    braintree.Dispute.PreDisputeProgram.VisaRdr,
+                ])
+            ])
+
+            disputes = [dispute for dispute in collection.disputes.items]
+            self.assertEqual(len(disputes), 1)
+            self.assertEqual(disputes[0].pre_dispute_program, braintree.Dispute.PreDisputeProgram.VisaRdr)
+
+    def test_advanced_search_returns_disputes_with_no_pre_dispute_program(self):
+            collection = Dispute.search([
+                DisputeSearch.pre_dispute_program == braintree.Dispute.PreDisputeProgram.NONE
+            ])
+
+            disputes = [dispute for dispute in collection.disputes.items]
+            pre_dispute_programs = set([dispute.pre_dispute_program for dispute in disputes])
+
+            self.assertGreater(len(disputes), 1)
+            self.assertEqual(len(pre_dispute_programs), 1)
+            self.assertIn(braintree.Dispute.PreDisputeProgram.NONE, pre_dispute_programs)
 
     def test_advanced_search_returns_disputes_by_date_range(self):
         collection = Dispute.search([
@@ -79,7 +117,7 @@ class TestDisputeSearch(unittest.TestCase):
         ])
 
         disputes = [dispute for dispute in collection.disputes.items]
-        self.assertEquals(1, len(disputes))
+        self.assertGreaterEqual(len(disputes), 1)
 
         self.assertEquals(disputes[0].received_date, date(2014, 3, 4))
 
diff --git a/tests/integration/test_disputes.py b/tests/integration/test_disputes.py
index 8f11a6b..7c331d6 100644
--- a/tests/integration/test_disputes.py
+++ b/tests/integration/test_disputes.py
@@ -45,9 +45,9 @@ class TestDisputes(unittest.TestCase):
         self.assertEqual(result.errors.for_object("dispute")[0].code, ErrorCodes.Dispute.CanOnlyAcceptOpenDispute)
         self.assertEqual(result.errors.for_object("dispute")[0].message, "Disputes can only be accepted when they are in an Open state")
 
-    @raises_with_regexp(NotFoundError, "dispute with id 'invalid-id' not found")
     def test_accept_raises_error_when_dispute_not_found(self):
-        dispute = Dispute.accept("invalid-id")
+        with self.assertRaisesRegex(NotFoundError, "dispute with id 'invalid-id' not found"):
+            dispute = Dispute.accept("invalid-id")
 
     def test_add_file_evidence_adds_evidence(self):
         dispute = self.create_sample_dispute()
@@ -70,9 +70,9 @@ class TestDisputes(unittest.TestCase):
         self.assertTrue(result.is_success)
         self.assertEqual(result.evidence.category, "GENERAL")
 
-    @raises_with_regexp(NotFoundError, "dispute with id 'unknown_dispute_id' not found")
     def test_add_file_evidence_raises_error_when_dispute_not_found(self):
-        dispute = Dispute.add_file_evidence("unknown_dispute_id", "text evidence")
+        with self.assertRaisesRegex(NotFoundError, "dispute with id 'unknown_dispute_id' not found"):
+            dispute = Dispute.add_file_evidence("unknown_dispute_id", "text evidence")
 
     def test_add_file_evidence_raises_error_when_dispute_not_open(self):
         dispute = self.create_sample_dispute()
@@ -121,44 +121,20 @@ class TestDisputes(unittest.TestCase):
         self.assertIsNone(evidence.category)
         self.assertIsNone(evidence.sequence_number)
 
-    def test_add_text_evidence_adds_tag_and_sequence_number_text_evidence(self):
+    def test_add_text_evidence_adds_category_and_sequence_number_text_evidence(self):
         dispute = self.create_sample_dispute()
 
-        result = Dispute.add_text_evidence(dispute.id, { "content": "PROOF_OF_FULFILLMENT", "tag": "EVIDENCE_TYPE" })
-        result_carrier_name = Dispute.add_text_evidence(dispute.id, { "content": "UPS", "tag": "CARRIER_NAME", "sequence_number": "0" })
-        result_tracking_number = Dispute.add_text_evidence(dispute.id, { "content": "UPS-1243", "tag": "TRACKING_NUMBER", "sequence_number": "0" })
+        result = Dispute.add_text_evidence(dispute.id, { "content": "PROOF_OF_FULFILLMENT", "category": "DEVICE_ID", "sequence_number": "0" })
 
         self.assertTrue(result.is_success)
         evidence = result.evidence
         self.assertEqual(evidence.comment, "PROOF_OF_FULFILLMENT")
-        self.assertEqual(evidence.tag, "EVIDENCE_TYPE")
-        self.assertIsNone(evidence.sequence_number)
-
-        self.assertTrue(result_carrier_name.is_success)
-        evidence = result_carrier_name.evidence
-        self.assertEqual(evidence.comment, "UPS")
-        self.assertEqual(evidence.tag, "CARRIER_NAME")
-        self.assertEqual(evidence.sequence_number, 0)
-
-        self.assertTrue(result_tracking_number.is_success)
-        evidence = result_tracking_number.evidence
-        self.assertEqual(evidence.comment, "UPS-1243")
-        self.assertEqual(evidence.tag, "TRACKING_NUMBER")
-        self.assertEqual(evidence.sequence_number, 0)
-
-    def test_add_text_evidence_adds_category_text_evidence(self):
-        dispute = self.create_sample_dispute()
-
-        result = Dispute.add_text_evidence(dispute.id, { "content": "device id" , "category": "DEVICE_ID" })
-
-        self.assertTrue(result.is_success)
-        evidence = result.evidence
-        self.assertEqual(evidence.comment, "device id")
         self.assertEqual(evidence.category, "DEVICE_ID")
+        self.assertEqual(evidence.sequence_number, 0)
 
-    @raises_with_regexp(NotFoundError, "Dispute with ID 'unknown_dispute_id' not found")
     def test_add_text_evidence_raises_error_when_dispute_not_found(self):
-        dispute = Dispute.add_text_evidence("unknown_dispute_id", "text evidence")
+        with self.assertRaisesRegex(NotFoundError, "Dispute with ID 'unknown_dispute_id' not found"):
+            dispute = Dispute.add_text_evidence("unknown_dispute_id", "text evidence")
 
     def test_add_text_evidence_raises_error_when_dispute_not_open(self):
         dispute = self.create_sample_dispute()
@@ -209,6 +185,39 @@ class TestDisputes(unittest.TestCase):
 
         self.assertTrue(result.is_success)
 
+    def test_add_text_evidence_adds_category_and_shipping_tracking_carrier_name(self):
+        dispute = self.create_sample_dispute()
+
+        result = Dispute.add_text_evidence(dispute.id, { "content": "UPS", "category": "CARRIER_NAME", "sequence_number": "0" })
+
+        self.assertTrue(result.is_success)
+        evidence = result.evidence
+        self.assertEqual(evidence.comment, "UPS")
+        self.assertEqual(evidence.category, "CARRIER_NAME")
+        self.assertEqual(evidence.sequence_number, 0)
+
+    def test_add_text_evidence_adds_category_and_shipping_tracking_tracking_number(self):
+        dispute = self.create_sample_dispute()
+
+        result = Dispute.add_text_evidence(dispute.id, { "content": "3", "category": "TRACKING_NUMBER", "sequence_number": "0" })
+
+        self.assertTrue(result.is_success)
+        evidence = result.evidence
+        self.assertEqual(evidence.comment, "3")
+        self.assertEqual(evidence.category, "TRACKING_NUMBER")
+        self.assertEqual(evidence.sequence_number, 0)
+
+    def test_add_text_evidence_adds_category_and_shipping_tracking_tracking_url(self):
+        dispute = self.create_sample_dispute()
+
+        result = Dispute.add_text_evidence(dispute.id, { "content": "https://example.com/tracking-number/abc12345", "category": "TRACKING_URL", "sequence_number": "1" })
+
+        self.assertTrue(result.is_success)
+        evidence = result.evidence
+        self.assertEqual(evidence.comment, "https://example.com/tracking-number/abc12345")
+        self.assertEqual(evidence.category, "TRACKING_URL")
+        self.assertEqual(evidence.sequence_number, 1)
+
     def test_finalize_changes_dispute_status_to_disputed(self):
         dispute = self.create_sample_dispute()
 
@@ -254,9 +263,9 @@ class TestDisputes(unittest.TestCase):
         error_codes = [error.code for error in result.errors.for_object("dispute")]
         self.assertIn(ErrorCodes.Dispute.NonDisputedPriorTransactionEvidenceMissingDate, error_codes)
 
-    @raises_with_regexp(NotFoundError, "dispute with id 'invalid-id' not found")
     def test_finalize_raises_error_when_dispute_not_found(self):
-        dispute = Dispute.finalize("invalid-id")
+        with self.assertRaisesRegex(NotFoundError, "dispute with id 'invalid-id' not found"):
+            dispute = Dispute.finalize("invalid-id")
 
     def test_find_returns_dispute_with_given_id(self):
         dispute = Dispute.find("open_dispute")
@@ -266,10 +275,25 @@ class TestDisputes(unittest.TestCase):
         self.assertEqual(dispute.id, "open_dispute")
         self.assertEqual(dispute.status, Dispute.Status.Open)
         self.assertEqual(dispute.transaction.id, "open_disputed_transaction")
+        self.assertEqual(None, dispute.transaction.installment_count)
+        self.assertNotEqual(None, dispute.graphql_id)
 
-    @raises_with_regexp(NotFoundError, "dispute with id 'invalid-id' not found")
     def test_find_raises_error_when_dispute_not_found(self):
-        dispute = Dispute.find("invalid-id")
+        with self.assertRaisesRegex(NotFoundError, "dispute with id 'invalid-id' not found"):
+            dispute = Dispute.find("invalid-id")
+
+    def test_delete_customer_with_path_traversal(self):
+        try:
+            customer = Customer.create({"first_name":"Waldo"}).customer
+            dispute = self.create_sample_dispute()
+            Dispute.remove_evidence(dispute.id, "../../../customers/{}".format(customer.id))
+        except NotFoundError:
+            pass
+
+        found_customer = Customer.find(customer.id)
+        self.assertNotEqual(None, found_customer)
+        self.assertEqual("Waldo", found_customer.first_name)
+
 
     def test_remove_evidence_removes_evidence_from_the_dispute(self):
         dispute = self.create_sample_dispute()
@@ -278,9 +302,9 @@ class TestDisputes(unittest.TestCase):
 
         self.assertTrue(result.is_success)
 
-    @raises_with_regexp(NotFoundError, "evidence with id 'unknown_evidence_id' for dispute with id 'unknown_dispute_id' not found")
     def test_remove_evidence_raises_error_when_dispute_or_evidence_not_found(self):
-        Dispute.remove_evidence("unknown_dispute_id", "unknown_evidence_id")
+        with self.assertRaisesRegex(NotFoundError, "evidence with id 'unknown_evidence_id' for dispute with id 'unknown_dispute_id' not found"):
+            Dispute.remove_evidence("unknown_dispute_id", "unknown_evidence_id")
 
     def test_remove_evidence_errors_when_dispute_not_open(self):
         dispute = self.create_sample_dispute()
diff --git a/tests/integration/test_document_upload.py b/tests/integration/test_document_upload.py
index 773a62b..22c1be8 100644
--- a/tests/integration/test_document_upload.py
+++ b/tests/integration/test_document_upload.py
@@ -1,5 +1,4 @@
 import os
-from nose.exc import SkipTest
 from tests.test_helper import *
 from braintree.test.nonces import Nonces
 
@@ -70,7 +69,24 @@ class TestDocumentUpload(unittest.TestCase):
         finally:
             os.remove(file_path)
 
-    def test_create_returns_error_with_malformed_file(self):
+    def test_create_returns_error_when_file_is_empty(self):
+        file_path = os.path.join(os.path.dirname(__file__), "..", "fixtures/empty_file.png")
+        try:
+            f = open(file_path, 'w')
+            f.close()
+
+            empty_file = open(file_path, 'rb')
+
+            result = DocumentUpload.create({
+                "kind": braintree.DocumentUpload.Kind.EvidenceDocument,
+                "file": empty_file
+            })
+
+            self.assertEqual(result.errors.for_object("document_upload")[0].code, ErrorCodes.DocumentUpload.FileIsEmpty)
+        finally:
+            os.remove(file_path)
+
+    def test_create_returns_error_with_too_long_file(self):
         file_path = os.path.join(os.path.dirname(__file__), "..", "fixtures/too_long.pdf")
         too_long_pdf = open(file_path, "rb")
 
@@ -81,23 +97,23 @@ class TestDocumentUpload(unittest.TestCase):
 
         self.assertEqual(result.errors.for_object("document_upload")[0].code, ErrorCodes.DocumentUpload.FileIsTooLong)
 
-    @raises_with_regexp(KeyError, "'Invalid keys: invalid_key'")
     def test_create_returns_invalid_keys_errors_with_invalid_signature(self):
-        result = DocumentUpload.create({
-            "kind": braintree.DocumentUpload.Kind.EvidenceDocument,
-            "invalid_key": "do not add"
-        })
+        with self.assertRaisesRegex(KeyError, "'Invalid keys: invalid_key'"):
+            result = DocumentUpload.create({
+                "kind": braintree.DocumentUpload.Kind.EvidenceDocument,
+                "invalid_key": "do not add"
+            })
 
-    @raises_with_regexp(ValueError, "file must be a file handle")
     def test_create_throws_error_when_not_valid_file(self):
-        result = DocumentUpload.create({
-            "kind": braintree.DocumentUpload.Kind.EvidenceDocument,
-            "file": "not_a_file"
-        })
+        with self.assertRaisesRegex(ValueError, "file must be a file handle"):
+            result = DocumentUpload.create({
+                "kind": braintree.DocumentUpload.Kind.EvidenceDocument,
+                "file": "not_a_file"
+            })
 
-    @raises_with_regexp(ValueError, "file must be a file handle")
     def test_create_throws_error_when_none_file(self):
-        result = DocumentUpload.create({
-            "kind": braintree.DocumentUpload.Kind.EvidenceDocument,
-            "file": None
-        })
+        with self.assertRaisesRegex(ValueError, "file must be a file handle"):
+            result = DocumentUpload.create({
+                "kind": braintree.DocumentUpload.Kind.EvidenceDocument,
+                "file": None
+            })
diff --git a/tests/integration/test_exchange_rate_quote.py b/tests/integration/test_exchange_rate_quote.py
new file mode 100644
index 0000000..d816a83
--- /dev/null
+++ b/tests/integration/test_exchange_rate_quote.py
@@ -0,0 +1,108 @@
+from tests.test_helper import *
+from braintree.exchange_rate_quote_request import ExchangeRateQuoteRequest
+
+class TestExchangeRateQuote(unittest.TestCase):
+    @staticmethod
+    def get_gateway():
+        config = Configuration("development", "integration_merchant_id",
+                               public_key="integration_public_key",
+                               private_key="integration_private_key")
+        return BraintreeGateway(config)
+
+    def test_exchange_rate_quote_with_full_graphql(self):
+        attribute1 = {"base_currency":"USD",
+                      "quote_currency":"EUR",
+                      "base_amount":"12.19",
+                      "markup":"12.14"}
+
+        attribute2 = {"base_currency":"EUR",
+                      "quote_currency":"CAD",
+                      "base_amount":"15.16",
+                      "markup":"2.64"}
+
+        request = ExchangeRateQuoteRequest().add_exchange_rate_quote_input(
+            attribute1).done().add_exchange_rate_quote_input(attribute2).done()
+
+        result = self.get_gateway().exchange_rate_quote.generate(request)
+        self.assertTrue(result.is_success)
+        quotes = result.exchange_rate_quote_payload.get_quotes()
+        self.assertEqual(2, len(quotes))
+
+        quote1 = quotes[0]
+        self.assertEqual("12.19", str(quote1.base_amount.value))
+        self.assertEqual("USD", quote1.base_amount.currency_code)
+        self.assertEqual("12.16", str(quote1.quote_amount.value))
+        self.assertEqual("EUR", quote1.quote_amount.currency_code)
+        self.assertEqual("0.997316360864", quote1.exchange_rate)
+        self.assertEqual("0.01", quote1.trade_rate)
+        self.assertEqual("2021-06-16T02:00:00.000000Z", quote1.expires_at)
+        self.assertEqual("2021-06-16T00:00:00.000000Z", quote1.refreshes_at)
+        self.assertEqual("ZXhjaGFuZ2VyYXRlcXVvdGVfMDEyM0FCQw", quote1.id)
+
+        quote2 = quotes[1]
+        self.assertEqual("15.16", str(quote2.base_amount.value))
+        self.assertEqual("EUR", quote2.base_amount.currency_code)
+        self.assertEqual("23.30", str(quote2.quote_amount.value))
+        self.assertEqual("CAD", quote2.quote_amount.currency_code)
+        self.assertEqual("1.536744692129366", quote2.exchange_rate)
+        self.assertIsNone(quote2.trade_rate)
+        self.assertEqual("2021-06-16T02:00:00.000000Z", quote2.expires_at)
+        self.assertEqual("2021-06-16T00:00:00.000000Z", quote2.refreshes_at)
+        self.assertEqual("ZXhjaGFuZ2VyYXRlcXVvdGVfQUJDMDEyMw", quote2.id)
+
+    def test_exchange_rate_quote_with_graphqul_quote_currency_validation_error(self):
+        attribute1 = {"base_currency":"USD",
+                      "base_amount":"12.19",
+                      "markup":"12.14"}
+
+        attribute2 = {"base_currency":"EUR",
+                      "quote_currency":"CAD",
+                      "base_amount":"15.16",
+                      "markup":"2.64"}
+        request = ExchangeRateQuoteRequest().add_exchange_rate_quote_input(
+            attribute1).done().add_exchange_rate_quote_input(attribute2).done()
+
+        result = self.get_gateway().exchange_rate_quote.generate(request)
+        self.assertFalse(result.is_success)
+        self.assertTrue("'quoteCurrency'" in result.message)
+
+    def test_exchange_rate_quote_with_graphql_base_currency_validation_error(self):
+        attribute1 = {"base_currency":"USD",
+                      "quote_currency":"EUR",
+                      "base_amount":"12.19",
+                      "markup":"12.14"}
+
+        attribute2 = {"quote_currency":"CAD",
+                      "base_amount":"15.16",
+                      "markup":"2.64"}
+
+        request = ExchangeRateQuoteRequest().add_exchange_rate_quote_input(
+            attribute1).done().add_exchange_rate_quote_input(attribute2).done()
+
+        result = self.get_gateway().exchange_rate_quote.generate(request)
+        self.assertFalse(result.is_success)
+        self.assertTrue("'baseCurrency'" in result.message)
+
+    def test_exchange_rate_quote_with_graphql_without_base_amount(self):
+        attribute1 = {"base_currency":"USD",
+                      "quote_currency":"EUR"}
+
+        attribute2 = {"base_currency":"EUR",
+                      "quote_currency":"CAD"}
+
+        request = ExchangeRateQuoteRequest().add_exchange_rate_quote_input(
+            attribute1).done().add_exchange_rate_quote_input(attribute2).done()
+                
+        result = self.get_gateway().exchange_rate_quote.generate(request)
+        self.assertTrue(result.is_success)
+
+    def test_exchange_rate_quote_with_graphql_without_base_and_quote_currency(self):
+        attribute1 = {"base_amount":"12.19",
+                      "markup":"12.14"}
+
+        request = ExchangeRateQuoteRequest().add_exchange_rate_quote_input(
+            attribute1).done()
+
+        result = self.get_gateway().exchange_rate_quote.generate(request)
+        self.assertFalse(result.is_success)
+        self.assertTrue("'baseCurrency'" in result.message)
\ No newline at end of file
diff --git a/tests/integration/test_graphql_client.py b/tests/integration/test_graphql_client.py
index 8e4d61f..af1e17a 100644
--- a/tests/integration/test_graphql_client.py
+++ b/tests/integration/test_graphql_client.py
@@ -29,7 +29,6 @@ class TestGraphQLClient(TestCase):
         definition = '''
           mutation CreateClientToken($input: CreateClientTokenInput!) {
             createClientToken(input: $input) {
-            clientMutationId
             clientToken
             }
           }
@@ -37,7 +36,6 @@ class TestGraphQLClient(TestCase):
 
         variables = {
             "input": {
-                "clientMutationId": "abc123",
                 "clientToken": {
                     "merchantAccountId": "ABC123"
                 }
diff --git a/tests/integration/test_http.py b/tests/integration/test_http.py
index b87218a..ff9e7bf 100644
--- a/tests/integration/test_http.py
+++ b/tests/integration/test_http.py
@@ -15,15 +15,15 @@ class TestHttp(unittest.TestCase):
         config = Configuration(environment, "merchant_id", public_key="public_key", private_key="private_key")
         return config.http()
 
-    @raises(AuthenticationError)
     def test_successful_connection_sandbox(self):
-        http = self.get_http(Environment.Sandbox)
-        http.get("/")
+        with self.assertRaises(AuthenticationError):
+            http = self.get_http(Environment.Sandbox)
+            http.get("/")
 
-    @raises(AuthenticationError)
     def test_successful_connection_production(self):
-        http = self.get_http(Environment.Production)
-        http.get("/")
+        with self.assertRaises(AuthenticationError):
+            http = self.get_http(Environment.Production)
+            http.get("/")
 
     def test_wrapping_http_exceptions(self):
         config = Configuration(
@@ -92,3 +92,26 @@ class TestHttp(unittest.TestCase):
             correct_exception = False
 
         self.assertTrue(correct_exception)
+
+    def test_sessions_include_proxy_environments(self):
+        proxies = {'https': 'http://i-clearly-dont-work', 'http': 'https://i-clearly-dont-work'}
+        os.environ['HTTP_PROXY'] = proxies['http']
+        os.environ['HTTPS_PROXY'] = proxies['https']
+        self.assertEqual(requests.utils.getproxies(), proxies)
+
+        config = Configuration(
+            Environment.Development,
+            "integration_merchant_id",
+            public_key="integration_public_key",
+            private_key="integration_private_key",
+            wrap_http_exceptions=True,
+        )
+        gateway = braintree.braintree_gateway.BraintreeGateway(config)
+
+        try:
+            gateway.plan.all()
+            os.environ.clear()
+            assert False, "The proxy is invalid this request should not be successful."
+        except Exception as e:
+            os.environ.clear()
+            assert 'Cannot connect to proxy' in str(e)
diff --git a/tests/integration/test_merchant_account.py b/tests/integration/test_merchant_account.py
index 9ad96ae..2f707c6 100644
--- a/tests/integration/test_merchant_account.py
+++ b/tests/integration/test_merchant_account.py
@@ -1,35 +1,12 @@
 from tests.test_helper import *
 
 class TestMerchantAccount(unittest.TestCase):
-    DEPRECATED_APPLICATION_PARAMS = {
-        "applicant_details": {
-            "company_name": "Garbage Garage",
-            "first_name": "Joe",
-            "last_name": "Bloggs",
-            "email": "joe@bloggs.com",
-            "phone": "555-555-5555",
-            "address": {
-                "street_address": "123 Credibility St.",
-                "postal_code": "60606",
-                "locality": "Chicago",
-                "region": "IL",
-            },
-            "date_of_birth": "10/9/1980",
-            "ssn": "123-00-1234",
-            "tax_id": "123456789",
-            "routing_number": "122100024",
-            "account_number": "43759348798"
-        },
-        "tos_accepted": True,
-        "master_merchant_account_id": "sandbox_master_merchant_account"
-    }
-
     VALID_APPLICATION_PARAMS = {
         "individual": {
             "first_name": "Joe",
             "last_name": "Bloggs",
             "email": "joe@bloggs.com",
-            "phone": "555-555-5555",
+            "phone": "555-123-1234",
             "address": {
                 "street_address": "123 Credibility St.",
                 "postal_code": "60606",
@@ -42,7 +19,7 @@ class TestMerchantAccount(unittest.TestCase):
         "business": {
             "dba_name": "Garbage Garage",
             "legal_name": "Junk Jymnasium",
-            "tax_id": "123456789",
+            "tax_id": "423456789",
             "address": {
                 "street_address": "123 Reputation St.",
                 "postal_code": "40222",
@@ -60,13 +37,6 @@ class TestMerchantAccount(unittest.TestCase):
         "master_merchant_account_id": "sandbox_master_merchant_account"
     }
 
-    def test_create_accepts_deprecated_parameters(self):
-        result = MerchantAccount.create(self.DEPRECATED_APPLICATION_PARAMS)
-
-        self.assertTrue(result.is_success)
-        self.assertEqual(MerchantAccount.Status.Pending, result.merchant_account.status)
-        self.assertEqual("sandbox_master_merchant_account", result.merchant_account.master_merchant_account.id)
-
     def test_create_application_with_valid_params_and_no_id(self):
         result = MerchantAccount.create(self.VALID_APPLICATION_PARAMS)
 
@@ -95,15 +65,12 @@ class TestMerchantAccount(unittest.TestCase):
 
     def test_create_requires_all_fields(self):
         result = MerchantAccount.create(
-            {"master_merchant_account_id": "sandbox_master_merchant_account",
-             "applicant_details": {},
-            "tos_accepted": True}
+            {"master_merchant_account_id": "sandbox_master_merchant_account"}
         )
         self.assertFalse(result.is_success)
 
-        first_name_errors = result.errors.for_object("merchant_account").for_object("applicant_details").on("first_name")
-        self.assertEqual(1, len(first_name_errors))
-        self.assertEqual(ErrorCodes.MerchantAccount.ApplicantDetails.FirstNameIsRequired, first_name_errors[0].code)
+        tos_errors = result.errors.for_object("merchant_account").on("tos_accepted")
+        self.assertEqual(ErrorCodes.MerchantAccount.TosAcceptedIsRequired , tos_errors[0].code)
 
     def test_create_funding_destination_accepts_a_bank(self):
         params = self.VALID_APPLICATION_PARAMS.copy()
@@ -144,7 +111,7 @@ class TestMerchantAccount(unittest.TestCase):
             "business": {
                 "dba_name": "James's Bloggs",
                 "legal_name": "James's Junkyard",
-                "tax_id": "987654321",
+                "tax_id": "987651324",
                 "address": {
                     "street_address": "456 Fake St",
                     "postal_code": "48104",
@@ -178,7 +145,7 @@ class TestMerchantAccount(unittest.TestCase):
         self.assertEqual(result.merchant_account.individual_details.address_details.postal_code, "60622")
         self.assertEqual(result.merchant_account.business_details.dba_name, "James's Bloggs")
         self.assertEqual(result.merchant_account.business_details.legal_name, "James's Junkyard")
-        self.assertEqual(result.merchant_account.business_details.tax_id, "987654321")
+        self.assertEqual(result.merchant_account.business_details.tax_id, "987651324")
         self.assertEqual(result.merchant_account.business_details.address_details.street_address, "456 Fake St")
         self.assertEqual(result.merchant_account.business_details.address_details.postal_code, "48104")
         self.assertEqual(result.merchant_account.business_details.address_details.locality, "Ann Arbor")
@@ -428,9 +395,9 @@ class TestMerchantAccount(unittest.TestCase):
         self.assertEqual(merchant_account.status, MerchantAccount.Status.Active)
         self.assertTrue(merchant_account.default)
 
-    @raises(NotFoundError)
     def test_find_404(self):
-        MerchantAccount.find("not_a_real_id")
+        with self.assertRaises(NotFoundError):
+            MerchantAccount.find("not_a_real_id")
 
     def test_merchant_account_create_for_currency(self):
         self.gateway = BraintreeGateway(
diff --git a/tests/integration/test_oauth.py b/tests/integration/test_oauth.py
index c3e6939..2d88a52 100644
--- a/tests/integration/test_oauth.py
+++ b/tests/integration/test_oauth.py
@@ -1,10 +1,7 @@
 from tests.test_helper import *
 from braintree.test.nonces import Nonces
 import sys
-if sys.version_info[0] == 2:
-    import urlparse
-else:
-    import urllib.parse as urlparse
+import urllib.parse as urlparse
 
 class TestOAuthGateway(unittest.TestCase):
     def setUp(self):
diff --git a/tests/integration/test_payment_method.py b/tests/integration/test_payment_method.py
index d23a8eb..f921017 100644
--- a/tests/integration/test_payment_method.py
+++ b/tests/integration/test_payment_method.py
@@ -29,17 +29,53 @@ class TestPaymentMethod(unittest.TestCase):
         self.assertEqual("05", three_d_secure_info.eci_flag)
         self.assertEqual("1.0.2", three_d_secure_info.three_d_secure_version)
 
-    def test_create_with_paypal_future_payments_nonce(self):
+    def test_create_with_three_d_secure_pass_thru(self):
         customer_id = Customer.create().customer.id
         result = PaymentMethod.create({
             "customer_id": customer_id,
-            "payment_method_nonce": Nonces.PayPalFuturePayment
+            "payment_method_nonce": Nonces.Transactable,
+            "three_d_secure_pass_thru": {
+                "three_d_secure_version": "1.1.0",
+                "eci_flag": "05",
+                "cavv": "some-cavv",
+                "xid": "some-xid"
+            },
+            "options": {
+                "verify_card": "true",
+            }
+        })
+
+        self.assertTrue(result.is_success)
+
+    def test_create_with_three_d_secure_pass_thru_without_eci_flag(self):
+        customer_id = Customer.create().customer.id
+        result = PaymentMethod.create({
+            "customer_id": customer_id,
+            "payment_method_nonce": Nonces.Transactable,
+            "three_d_secure_pass_thru": {
+                "three_d_secure_version": "1.1.0",
+                "cavv": "some-cavv",
+                "xid": "some-xid"
+            },
+            "options": {
+                "verify_card": "true",
+            }
+        })
+
+        self.assertFalse(result.is_success)
+        self.assertEqual("EciFlag is required.", result.message)
+
+    def test_create_with_paypal_billing_agreements_nonce(self):
+        customer_id = Customer.create().customer.id
+        result = PaymentMethod.create({
+            "customer_id": customer_id,
+            "payment_method_nonce": Nonces.PayPalBillingAgreement
         })
 
         self.assertTrue(result.is_success)
         created_account = result.payment_method
         self.assertEqual(PayPalAccount, created_account.__class__)
-        self.assertEqual("jane.doe@example.com", created_account.email)
+        self.assertEqual("jane.doe@paypal.com", created_account.email)
         self.assertNotEqual(created_account.image_url, None)
 
         found_account = PaymentMethod.find(result.payment_method.token)
@@ -118,25 +154,6 @@ class TestPaymentMethod(unittest.TestCase):
         self.assertEqual(created_account.billing_agreement_id, found_account.billing_agreement_id)
         self.assertEqual(created_account.payer_id, found_account.payer_id)
 
-    def test_create_with_paypal_refresh_token_without_upgrade(self):
-        customer_id = Customer.create().customer.id
-        result = PaymentMethod.create({
-            "customer_id": customer_id,
-            "paypal_refresh_token": "PAYPAL_REFRESH_TOKEN",
-            "paypal_vault_without_upgrade": True,
-        })
-
-        self.assertTrue(result.is_success)
-        created_account = result.payment_method
-        self.assertEqual(PayPalAccount, created_account.__class__)
-        self.assertEqual(created_account.billing_agreement_id, None)
-
-        found_account = PaymentMethod.find(result.payment_method.token)
-        self.assertNotEqual(None, found_account)
-        self.assertEqual(created_account.token, found_account.token)
-        self.assertEqual(created_account.customer_id, found_account.customer_id)
-        self.assertEqual(created_account.billing_agreement_id, found_account.billing_agreement_id)
-
     def test_create_returns_validation_failures(self):
         http = ClientApiHttp.create()
         status_code, nonce = http.get_paypal_nonce({
@@ -239,6 +256,18 @@ class TestPaymentMethod(unittest.TestCase):
         self.assertIsInstance(apple_pay_card, ApplePayCard)
         self.assertNotEqual(apple_pay_card.bin, None)
         self.assertNotEqual(apple_pay_card.token, None)
+        self.assertNotEqual(apple_pay_card.prepaid, None)
+        self.assertNotEqual(apple_pay_card.healthcare, None)
+        self.assertNotEqual(apple_pay_card.debit, None)
+        self.assertNotEqual(apple_pay_card.durbin_regulated, None)
+        self.assertNotEqual(apple_pay_card.commercial, None)
+        self.assertNotEqual(apple_pay_card.payroll, None)
+        self.assertNotEqual(apple_pay_card.issuing_bank, None)
+        self.assertNotEqual(apple_pay_card.country_of_issuance, None)
+        self.assertNotEqual(apple_pay_card.product_id, None)
+        self.assertNotEqual(apple_pay_card.last_4, None)
+        self.assertNotEqual(apple_pay_card.card_type, None)
+
         self.assertEqual(apple_pay_card.customer_id, customer_id)
         self.assertEqual(ApplePayCard.CardType.MasterCard, apple_pay_card.card_type)
         self.assertEqual("MasterCard 0017", apple_pay_card.payment_instrument_name)
@@ -275,6 +304,7 @@ class TestPaymentMethod(unittest.TestCase):
         self.assertIsInstance(android_pay_card.updated_at, datetime)
         self.assertEqual("601111", android_pay_card.bin)
         self.assertEqual("google_transaction_id", android_pay_card.google_transaction_id)
+        self.assertFalse(android_pay_card.is_network_tokenized)
 
     def test_create_with_fake_android_pay_network_token_nonce(self):
         customer_id = Customer.create().customer.id
@@ -303,6 +333,20 @@ class TestPaymentMethod(unittest.TestCase):
         self.assertIsInstance(android_pay_card.updated_at, datetime)
         self.assertEqual("555555", android_pay_card.bin)
         self.assertEqual("google_transaction_id", android_pay_card.google_transaction_id)
+        self.assertTrue(android_pay_card.is_network_tokenized)
+        self.assertNotEqual(android_pay_card.bin, None)
+        self.assertNotEqual(android_pay_card.token, None)
+        self.assertNotEqual(android_pay_card.prepaid, None)
+        self.assertNotEqual(android_pay_card.healthcare, None)
+        self.assertNotEqual(android_pay_card.debit, None)
+        self.assertNotEqual(android_pay_card.durbin_regulated, None)
+        self.assertNotEqual(android_pay_card.commercial, None)
+        self.assertNotEqual(android_pay_card.payroll, None)
+        self.assertNotEqual(android_pay_card.issuing_bank, None)
+        self.assertNotEqual(android_pay_card.country_of_issuance, None)
+        self.assertNotEqual(android_pay_card.product_id, None)
+        self.assertNotEqual(android_pay_card.last_4, None)
+        self.assertNotEqual(android_pay_card.card_type, None)
 
     def test_create_with_fake_amex_express_checkout_card_nonce(self):
         customer_id = Customer.create().customer.id
@@ -340,7 +384,7 @@ class TestPaymentMethod(unittest.TestCase):
         self.assertTrue(venmo_account.default)
         self.assertIsNotNone(venmo_account.token)
         self.assertEqual("venmojoe", venmo_account.username)
-        self.assertEqual("Venmo-Joe-1", venmo_account.venmo_user_id)
+        self.assertEqual("1234567891234567891", venmo_account.venmo_user_id)
         self.assertEqual("Venmo Account: venmojoe", venmo_account.source_description)
         self.assertRegexpMatches(venmo_account.image_url, r"\.png")
         self.assertEqual(customer_id, venmo_account.customer_id)
@@ -420,6 +464,68 @@ class TestPaymentMethod(unittest.TestCase):
         self.assertTrue(result.credit_card_verification.processor_response_text == "Do Not Honor")
         self.assertTrue(result.credit_card_verification.merchant_account_id == TestHelper.non_default_merchant_account_id)
 
+    def test_create_includes_risk_data_when_skip_advanced_fraud_checking_is_false(self):
+        with FraudProtectionEnterpriseIntegrationMerchant():
+            config = Configuration.instantiate()
+            customer_id = Customer.create().customer.id
+            client_token = json.loads(TestHelper.generate_decoded_client_token())
+            authorization_fingerprint = client_token["authorizationFingerprint"]
+            http = ClientApiHttp(config, {
+                "authorization_fingerprint": authorization_fingerprint,
+                "shared_customer_identifier": "fake_identifier",
+                "shared_customer_identifier_type": "testing",
+                })
+            status_code, nonce = http.get_credit_card_nonce({
+                "number": "4111111111111111",
+                "expirationMonth": "11",
+                "expirationYear": "2099",
+                })
+            self.assertTrue(status_code == 201)
+
+            result = PaymentMethod.create({
+                "customer_id": customer_id,
+                "payment_method_nonce": nonce,
+                "options": {
+                    "verify_card": True,
+                    "skip_advanced_fraud_checking": False
+                    },
+                })
+
+            self.assertTrue(result.is_success)
+            verification = result.payment_method.verification
+            self.assertIsInstance(verification.risk_data, RiskData)
+
+    def test_create_does_not_include_risk_data_when_skip_advanced_fraud_checking_is_true(self):
+        with FraudProtectionEnterpriseIntegrationMerchant():
+            config = Configuration.instantiate()
+            customer_id = Customer.create().customer.id
+            client_token = json.loads(TestHelper.generate_decoded_client_token())
+            authorization_fingerprint = client_token["authorizationFingerprint"]
+            http = ClientApiHttp(config, {
+                "authorization_fingerprint": authorization_fingerprint,
+                "shared_customer_identifier": "fake_identifier",
+                "shared_customer_identifier_type": "testing",
+                })
+            status_code, nonce = http.get_credit_card_nonce({
+                "number": "4111111111111111",
+                "expirationMonth": "11",
+                "expirationYear": "2099",
+                })
+            self.assertTrue(status_code == 201)
+
+            result = PaymentMethod.create({
+                "customer_id": customer_id,
+                "payment_method_nonce": nonce,
+                "options": {
+                    "verify_card": True,
+                    "skip_advanced_fraud_checking": True
+                    },
+                })
+
+            self.assertTrue(result.is_success)
+            verification = result.payment_method.verification
+            self.assertIsNone(verification.risk_data)
+
     def test_create_respects_fail_one_duplicate_payment_method_when_included_outside_of_the_nonce(self):
         customer_id = Customer.create().customer.id
         credit_card_result = CreditCard.create({
@@ -798,7 +904,7 @@ class TestPaymentMethod(unittest.TestCase):
         found_account = PaymentMethod.find(result.payment_method.token)
         self.assertNotEqual(None, found_account)
         self.assertEqual(PayPalAccount, found_account.__class__)
-        self.assertEqual("jane.doe@example.com", found_account.email)
+        self.assertTrue(found_account.email)
 
     def test_find_returns_a_credit_card(self):
         customer = Customer.create().customer
@@ -831,6 +937,17 @@ class TestPaymentMethod(unittest.TestCase):
         self.assertEqual(AndroidPayCard, found_android_pay_card.__class__)
         self.assertEqual(found_android_pay_card.token, android_pay_card.token)
 
+    def test_delete_customer_with_path_traversal(self):
+        try:
+            customer = Customer.create({"first_name":"Waldo"}).customer
+            PaymentMethod.delete("../../customers/{}".format(customer.id))
+        except NotFoundError:
+            pass
+
+        found_customer = Customer.find(customer.id)
+        self.assertNotEqual(None, found_customer)
+        self.assertEqual("Waldo", found_customer.first_name)
+
     def test_delete_deletes_a_credit_card(self):
         customer = Customer.create().customer
         result = CreditCard.create({
@@ -880,6 +997,60 @@ class TestPaymentMethod(unittest.TestCase):
         self.assertTrue(updated_credit_card.last_4 == CreditCardNumbers.MasterCard[-4:])
         self.assertTrue(updated_credit_card.expiration_date == "06/2013")
 
+    def test_update_with_three_d_secure_pass_thru(self):
+        customer_id = Customer.create().customer.id
+        credit_card_result = CreditCard.create({
+            "cardholder_name": "Original Holder",
+            "customer_id": customer_id,
+            "cvv": "123",
+            "number": CreditCardNumbers.Visa,
+            "expiration_date": "05/2012"
+        })
+        update_result = PaymentMethod.update(credit_card_result.credit_card.token, {
+            "cardholder_name": "New Holder",
+            "cvv": "456",
+            "number": CreditCardNumbers.MasterCard,
+            "expiration_date": "06/2013",
+            "three_d_secure_pass_thru": {
+                "three_d_secure_version": "1.1.0",
+                "eci_flag": "05",
+                "cavv": "some-cavv",
+                "xid": "some-xid"
+            },
+            "options": {
+                "verify_card": "true",
+            }
+        })
+
+        self.assertTrue(update_result.is_success)
+
+    def test_create_with_three_d_secure_pass_thru_without_eci_flag(self):
+        customer_id = Customer.create().customer.id
+        credit_card_result = CreditCard.create({
+            "cardholder_name": "Original Holder",
+            "customer_id": customer_id,
+            "cvv": "123",
+            "number": CreditCardNumbers.Visa,
+            "expiration_date": "05/2012"
+        })
+        update_result = PaymentMethod.update(credit_card_result.credit_card.token, {
+            "cardholder_name": "New Holder",
+            "cvv": "456",
+            "number": CreditCardNumbers.MasterCard,
+            "expiration_date": "06/2013",
+            "three_d_secure_pass_thru": {
+                "three_d_secure_version": "1.1.0",
+                "cavv": "some-cavv",
+                "xid": "some-xid"
+            },
+            "options": {
+                "verify_card": "true",
+            }
+        })
+
+        self.assertFalse(update_result.is_success)
+        self.assertEqual("EciFlag is required.", update_result.message)
+
     def test_update_credit_cards_with_account_type_credit(self):
         customer = Customer.create().customer
         result = CreditCard.create({
@@ -1109,6 +1280,52 @@ class TestPaymentMethod(unittest.TestCase):
         self.assertTrue(update_result.credit_card_verification.status == CreditCardVerification.Status.ProcessorDeclined)
         self.assertTrue(update_result.credit_card_verification.gateway_rejection_reason is None)
 
+    def test_update_includes_risk_data_when_skip_advanced_fraud_checking_is_false(self):
+        with FraudProtectionEnterpriseIntegrationMerchant():
+            customer_id = Customer.create().customer.id
+            credit_card_result = CreditCard.create({
+                "cardholder_name": "Card Holder",
+                "customer_id": customer_id,
+                "cvv": "123",
+                "number": "4111111111111111",
+                "expiration_date": "05/2020"
+            })
+
+            update_result = PaymentMethod.update(credit_card_result.credit_card.token, {
+                "expiration_date": "10/2020",
+                "options": {
+                    "verify_card": True,
+                    "skip_advanced_fraud_checking": False
+                    },
+                })
+
+            self.assertTrue(update_result.is_success)
+            verification = update_result.payment_method.verification
+            self.assertIsInstance(verification.risk_data, RiskData)
+
+    def test_update_does_not_include_risk_data_when_skip_advanced_fraud_checking_is_true(self):
+        with FraudProtectionEnterpriseIntegrationMerchant():
+            customer_id = Customer.create().customer.id
+            credit_card_result = CreditCard.create({
+                "cardholder_name": "Card Holder",
+                "customer_id": customer_id,
+                "cvv": "123",
+                "number": "4111111111111111",
+                "expiration_date": "05/2020"
+            })
+
+            update_result = PaymentMethod.update(credit_card_result.credit_card.token, {
+                "expiration_date": "10/2020",
+                "options": {
+                    "verify_card": True,
+                    "skip_advanced_fraud_checking": True
+                    },
+                })
+
+            self.assertTrue(update_result.is_success)
+            verification = update_result.payment_method.verification
+            self.assertIsNone(verification.risk_data)
+
     def test_update_can_update_the_billing_address(self):
         customer_id = Customer.create().customer.id
         credit_card_result = CreditCard.create({
@@ -1341,46 +1558,26 @@ class TestPaymentMethod(unittest.TestCase):
         granting_gateway, _ = TestHelper.create_payment_method_grant_fixtures()
         self.assertRaises(NotFoundError, granting_gateway.payment_method.revoke, "non-existant-token")
 
-class CreditCardForwardingTest(unittest.TestCase):
-    def setUp(self):
-        braintree.Configuration.configure(
-            braintree.Environment.Development,
-            "forward_payment_method_merchant_id",
-            "forward_payment_method_public_key",
-            "forward_payment_method_private_key"
-        )
-
-    def tearDown(self):
-        braintree.Configuration.configure(
-            braintree.Environment.Development,
-            "integration_merchant_id",
-            "integration_public_key",
-            "integration_private_key"
-        )
+    def test_vault_sepa_direct_debit_payment_method_with_fake_nonce(self):
+        customer_id = Customer.create().customer.id
 
-    def test_forward_raises_exception(self):
-        customer = Customer.create().customer
-        credit_card_result = CreditCard.create({
-            "customer_id": customer.id,
-            "number": "4111111111111111",
-            "expiration_date": "05/2025"
+        result = PaymentMethod.create({
+            "payment_method_nonce": Nonces.SepaDirectDebit,
+            "customer_id": customer_id,
         })
-        self.assertTrue(credit_card_result.is_success)
-        source_merchant_card = credit_card_result.credit_card
 
-        self.assertRaises(NotFoundError, CreditCard.forward, source_merchant_card.token, "integration_merchant_id")
+        self.assertTrue(result.is_success)
 
-    def test_forward_invalid_token_raises_exception(self):
-        self.assertRaises(NotFoundError, CreditCard.forward, "invalid", "integration_merchant_id")
+    def test_delete_sepa_direct_debit_payment_method(self):
+        customer_id = Customer.create().customer.id
 
-    def test_forward_invalid_receiving_merchant_raises_exception(self):
-        customer = Customer.create().customer
-        credit_card_result = CreditCard.create({
-            "customer_id": customer.id,
-            "number": "4111111111111111",
-            "expiration_date": "05/2025"
+        result = PaymentMethod.create({
+            "payment_method_nonce": Nonces.SepaDirectDebit,
+            "customer_id": customer_id,
         })
-        self.assertTrue(credit_card_result.is_success)
-        source_merchant_card = credit_card_result.credit_card
 
-        self.assertRaises(NotFoundError, CreditCard.forward, source_merchant_card.token, "invalid_merchant_id")
+        self.assertTrue(result.is_success)
+
+        delete_result = PaymentMethod.delete(result.payment_method.token)
+
+        self.assertRaises(NotFoundError, PaymentMethod.find, result.payment_method.token)
diff --git a/tests/integration/test_payment_method_nonce.py b/tests/integration/test_payment_method_nonce.py
index 1f93199..d4be51e 100644
--- a/tests/integration/test_payment_method_nonce.py
+++ b/tests/integration/test_payment_method_nonce.py
@@ -1,6 +1,13 @@
 from tests.test_helper import *
+from braintree.test.nonces import Nonces
 
 class TestPaymentMethodNonce(unittest.TestCase):
+    indian_payment_token = "india_visa_credit"
+    european_payment_token = "european_visa_credit"
+    indian_merchant_token = "india_three_d_secure_merchant_account"
+    european_merchant_token = "european_three_d_secure_merchant_account"
+    amount_threshold_for_rbi = 2000
+
     def test_create_nonce_from_payment_method(self):
         customer_id = Customer.create().customer.id
         credit_card_result = CreditCard.create({
@@ -15,6 +22,55 @@ class TestPaymentMethodNonce(unittest.TestCase):
         self.assertNotEqual(None, result.payment_method_nonce)
         self.assertNotEqual(None, result.payment_method_nonce.nonce)
 
+    def test_create_nonce_from_payment_method_with_invalid_params(self):
+        nonce_request = {
+            "merchant_account_id": self.indian_merchant_token,
+            "authentication_insight": True,
+            "invalid_keys": "foo"
+        }
+        params = {"payment_method_nonce": nonce_request}
+
+        with self.assertRaises(KeyError):
+            PaymentMethodNonce.create(self.indian_payment_token, params)
+
+    def test_create_nonce_with_auth_insight_regulation_environment_unavailable(self):
+        customer_id = Customer.create().customer.id
+        credit_card_result = CreditCard.create({
+            "customer_id": customer_id,
+            "number": "4111111111111111",
+            "expiration_date": "05/2014",
+        })
+        auth_insight_result = self._request_authentication_insights(self.indian_merchant_token, credit_card_result.credit_card.token)
+        self.assertEqual("unavailable", auth_insight_result["regulation_environment"])
+
+    def test_create_nonce_with_auth_insight_regulation_environment_unregulated(self):
+        auth_insight_result = self._request_authentication_insights(self.european_merchant_token, self.indian_payment_token)
+        self.assertEqual("unregulated", auth_insight_result["regulation_environment"])
+
+    def test_create_nonce_with_auth_insight_regulation_environment_psd2(self):
+        auth_insight_result = self._request_authentication_insights(self.european_merchant_token, self.european_payment_token)
+        self.assertEqual("psd2", auth_insight_result["regulation_environment"])
+
+    def test_create_nonce_with_auth_insight_regulation_environment_rbi(self):
+        auth_insight_result = self._request_authentication_insights(self.indian_merchant_token, self.indian_payment_token, self.amount_threshold_for_rbi)
+        self.assertEqual("rbi", auth_insight_result["regulation_environment"])
+
+    def test_create_nonce_with_auth_insight_sca_indicator_unavailable(self):
+        auth_insight_result = self._request_authentication_insights(self.indian_merchant_token, self.indian_payment_token)
+        self.assertEqual("unavailable", auth_insight_result["sca_indicator"])
+
+    def test_create_nonce_with_auth_insight_sca_indicator_sca_required(self):
+        auth_insight_result = self._request_authentication_insights(self.indian_merchant_token, self.indian_payment_token, self.amount_threshold_for_rbi + 1)
+        self.assertEqual("sca_required", auth_insight_result["sca_indicator"])
+
+    def test_create_nonce_with_auth_insight_sca_indicator_sca_optional(self):
+        auth_insight_result = self._request_authentication_insights(self.indian_merchant_token, self.indian_payment_token, self.amount_threshold_for_rbi, False, None)
+        self.assertEqual("sca_optional", auth_insight_result["sca_indicator"])
+
+    def test_create_nonce_with_auth_insight_sca_indicator_sca_required_with_recurring_customer_consent_and_max_amount(self):
+        auth_insight_result = self._request_authentication_insights(self.indian_merchant_token, self.indian_payment_token, self.amount_threshold_for_rbi, True, 1000)
+        self.assertEqual("sca_required", auth_insight_result["sca_indicator"])
+
     def test_create_raises_not_found_when_404(self):
         self.assertRaises(NotFoundError, PaymentMethodNonce.create, "not-a-token")
 
@@ -40,15 +96,7 @@ class TestPaymentMethodNonce(unittest.TestCase):
         )
         gateway = BraintreeGateway(config)
 
-        credit_card = {
-            "credit_card": {
-                "number": "4111111111111111",
-                "expiration_month": "12",
-                "expiration_year": "2020"
-            }
-        }
-
-        nonce = TestHelper.generate_three_d_secure_nonce(gateway, credit_card)
+        nonce = "fake-three-d-secure-visa-full-authentication-nonce"
         found_nonce = PaymentMethodNonce.find(nonce)
         three_d_secure_info = found_nonce.three_d_secure_info
 
@@ -58,10 +106,11 @@ class TestPaymentMethodNonce(unittest.TestCase):
         self.assertEqual("authenticate_successful", three_d_secure_info.status)
         self.assertEqual(True, three_d_secure_info.liability_shifted)
         self.assertEqual(True, three_d_secure_info.liability_shift_possible)
-        self.assertEqual("test_cavv", three_d_secure_info.cavv)
-        self.assertEqual("test_xid", three_d_secure_info.xid)
-        self.assertEqual("test_eci", three_d_secure_info.eci_flag)
+        self.assertEqual("cavv_value", three_d_secure_info.cavv)
+        self.assertEqual("xid_value", three_d_secure_info.xid)
+        self.assertEqual("05", three_d_secure_info.eci_flag)
         self.assertEqual("1.0.2", three_d_secure_info.three_d_secure_version)
+        self.assertIsNotNone(three_d_secure_info.three_d_secure_authentication_id)
 
     def test_find_nonce_shows_paypal_details(self):
         found_nonce = PaymentMethodNonce.find("fake-google-pay-paypal-nonce")
@@ -76,7 +125,15 @@ class TestPaymentMethodNonce(unittest.TestCase):
 
         self.assertEquals("99", found_nonce.details["last_two"])
         self.assertEquals("venmojoe", found_nonce.details["username"])
-        self.assertEquals("Venmo-Joe-1", found_nonce.details["venmo_user_id"])
+        self.assertEquals("1234567891234567891", found_nonce.details["venmo_user_id"])
+
+    def test_find_nonce_shows_sepa_direct_debit_details(self):
+        found_nonce = PaymentMethodNonce.find(Nonces.SepaDirectDebit)
+
+        self.assertEquals("1234", found_nonce.details["last_4"])
+        self.assertEquals("RECURRENT", found_nonce.details["mandate_type"])
+        self.assertEquals("a-fake-bank-reference-token", found_nonce.details["bank_reference_token"])
+        self.assertEquals("a-fake-mp-customer-id", found_nonce.details["merchant_or_partner_customer_id"])
 
     def test_exposes_null_3ds_info_if_none_exists(self):
         http = ClientApiHttp.create()
@@ -150,3 +207,16 @@ class TestPaymentMethodNonce(unittest.TestCase):
         self.assertEqual(CreditCard.Payroll.Unknown, bin_data.payroll)
         self.assertEqual(CreditCard.Prepaid.Unknown, bin_data.prepaid)
         self.assertEqual(CreditCard.ProductId.Unknown, bin_data.product_id)
+
+    def _request_authentication_insights(self, merchant_account_id, payment_method_token, amount = None, recurring_customer_consent = None, recurring_max_amount = None):
+        nonce_request = {
+            "merchant_account_id": merchant_account_id,
+            "authentication_insight": True,
+            "authentication_insight_options": {
+                "amount": amount,
+                "recurring_customer_consent": recurring_customer_consent,
+                "recurring_max_amount": recurring_max_amount,
+             }
+        }
+        result = PaymentMethodNonce.create(payment_method_token, {"payment_method_nonce": nonce_request})
+        return result.payment_method_nonce.authentication_insight
diff --git a/tests/integration/test_paypal_account.py b/tests/integration/test_paypal_account.py
index 00e647c..2a2090f 100644
--- a/tests/integration/test_paypal_account.py
+++ b/tests/integration/test_paypal_account.py
@@ -7,7 +7,7 @@ class TestPayPalAccount(unittest.TestCase):
         customer_id = Customer.create().customer.id
         result = PaymentMethod.create({
             "customer_id": customer_id,
-            "payment_method_nonce": Nonces.PayPalFuturePayment
+            "payment_method_nonce": Nonces.PayPalBillingAgreement
         })
         self.assertTrue(result.is_success)
 
@@ -74,10 +74,22 @@ class TestPayPalAccount(unittest.TestCase):
         paypal_account = PayPalAccount.find(result.payment_method.token)
         self.assertNotEqual(None, paypal_account.billing_agreement_id)
 
+    def test_delete_customer_with_path_traversal(self):
+        try:
+            customer = Customer.create({"first_name":"Waldo"}).customer
+            PayPalAccount.delete("../../{}".format(customer.id))
+        except NotFoundError:
+            pass
+
+        found_customer = Customer.find(customer.id)
+        self.assertNotEqual(None, found_customer)
+        self.assertEqual("Waldo", found_customer.first_name)
+
+
     def test_delete_deletes_paypal_account(self):
         result = PaymentMethod.create({
             "customer_id": Customer.create().customer.id,
-            "payment_method_nonce": Nonces.PayPalFuturePayment
+            "payment_method_nonce": Nonces.PayPalBillingAgreement
         })
         self.assertTrue(result.is_success)
         paypal_account_token = result.payment_method.token
diff --git a/tests/integration/test_plan.py b/tests/integration/test_plan.py
index 04c0fc1..389db5a 100644
--- a/tests/integration/test_plan.py
+++ b/tests/integration/test_plan.py
@@ -1,5 +1,5 @@
-from tests.test_helper import *
 
+from tests.test_helper import *
 class TestPlan(unittest.TestCase):
 
     def test_all_returns_empty_list(self):
@@ -77,3 +77,72 @@ class TestPlan(unittest.TestCase):
 
         self.assertEqual(1, len(actual_plan.discounts))
         self.assertEqual(discount_attributes["name"], actual_plan.discounts[0].name)
+
+    def test_create_returns_successful_result_if_valid(self):
+        attributes = {
+            "billing_day_of_month": 12,
+            "billing_frequency": 1,
+            "currency_iso_code": "USD",
+            "description": "description on create",
+            "name": "my new plan name",
+            "number_of_billing_cycles": 1,
+            "price": "9.99",
+            "trial_period": False
+        }
+
+        result = Plan.create(attributes)
+        self.assertTrue(result.is_success)
+        plan = result.plan
+        self.assertEqual(12, attributes["billing_day_of_month"])
+        self.assertEqual(1, attributes["billing_frequency"])
+        self.assertEqual("USD", attributes["currency_iso_code"])
+        self.assertEqual("description on create", attributes["description"])
+        self.assertEqual("my new plan name", attributes["name"])
+        self.assertEqual(1, attributes["number_of_billing_cycles"])
+        self.assertEqual("9.99", attributes["price"])
+
+    def test_find_with_valid_id(self):
+        plan_attributes = {
+            "billing_day_of_month": 12,
+            "billing_frequency": 1,
+            "currency_iso_code": "USD",
+            "description": "description on create",
+            "name": "my new plan name",
+            "number_of_billing_cycles": 1,
+            "price": "9.99",
+            "trial_period": False
+        }
+
+        created_plan = Plan.create(plan_attributes).plan
+        found_plan = Plan.find(created_plan.id)
+        self.assertEqual(created_plan.name, found_plan.name)
+        self.assertEqual(created_plan.id, found_plan.id)
+        self.assertEqual(created_plan.price, found_plan.price)
+        self.assertEqual(created_plan.billing_day_of_month, found_plan.billing_day_of_month)
+
+    def test_find_with_invalid_token(self):
+        with self.assertRaisesRegex(NotFoundError, "Plan with id 'bad_token' not found"):
+            Plan.find("bad_token")
+
+    def test_update_returns_successful_result_if_valid(self):
+        plan_attributes = {
+            "billing_day_of_month": 12,
+            "billing_frequency": 1,
+            "currency_iso_code": "USD",
+            "description": "description on create",
+            "name": "my new plan name",
+            "number_of_billing_cycles": 1,
+            "price": "9.99",
+            "trial_period": False
+        }
+
+        created_plan = Plan.create(plan_attributes).plan
+        result = Plan.update(created_plan.id, {
+            "name": "updated name",
+            "price": Decimal("99.88")
+        })
+        self.assertTrue(result.is_success)
+        updated_plan = result.plan
+        self.assertEqual("updated name", updated_plan.name)
+        self.assertEqual("99.88", updated_plan.price)
+
diff --git a/tests/integration/test_sepa_direct_debit_account.py b/tests/integration/test_sepa_direct_debit_account.py
new file mode 100644
index 0000000..a39d190
--- /dev/null
+++ b/tests/integration/test_sepa_direct_debit_account.py
@@ -0,0 +1,61 @@
+from tests.test_helper import *
+import time
+from braintree.test.nonces import Nonces
+
+class TestSepaDirectDebitAccount(unittest.TestCase):
+    def test_find_returns_sepa_direct_debit_account(self):
+        result = PaymentMethod.create({
+            "customer_id": Customer.create().customer.id,
+            "payment_method_nonce": Nonces.SepaDirectDebit
+        })
+        self.assertTrue(result.is_success)
+
+        found_account = SepaDirectDebitAccount.find(result.payment_method.token)
+        self.assertEqual(found_account.bank_reference_token, "a-fake-bank-reference-token")
+        self.assertEqual(found_account.mandate_type, "RECURRENT")
+        self.assertEqual(found_account.last_4, "1234")
+        self.assertEqual(found_account.merchant_or_partner_customer_id, "a-fake-mp-customer-id")
+        self.assertEqual(found_account.token, result.payment_method.token)
+        self.assertTrue(found_account.global_id)
+
+    def test_find_returns_subscriptions_associated_with_a_sepa_direct_debit_account(self):
+        result = PaymentMethod.create({
+            "customer_id": Customer.create().customer.id,
+            "payment_method_nonce": Nonces.SepaDirectDebit
+        })
+        self.assertTrue(result.is_success)
+
+        token = result.payment_method.token
+
+        subscription1 = Subscription.create({
+            "payment_method_token": token,
+            "plan_id": TestHelper.trialless_plan["id"]
+        }).subscription
+
+        subscription2 = Subscription.create({
+            "payment_method_token": token,
+            "plan_id": TestHelper.trialless_plan["id"]
+        }).subscription
+
+        sepa_direct_debit_account = SepaDirectDebitAccount.find(result.payment_method.token)
+        self.assertTrue(subscription1.id in [s.id for s in sepa_direct_debit_account.subscriptions])
+        self.assertTrue(subscription2.id in [s.id for s in sepa_direct_debit_account.subscriptions])
+
+    def test_find_raises_on_not_found_token(self):
+        self.assertRaises(NotFoundError, SepaDirectDebitAccount.find, "non-existant-token")
+
+    def test_delete_sepa_direct_debit_account(self):
+        result = PaymentMethod.create({
+            "customer_id": Customer.create().customer.id,
+            "payment_method_nonce": Nonces.SepaDirectDebit
+        })
+        self.assertTrue(result.is_success)
+        sepa_direct_debit_account_token = result.payment_method.token
+
+        delete_result = SepaDirectDebitAccount.delete(sepa_direct_debit_account_token)
+        self.assertTrue(delete_result.is_success)
+
+        self.assertRaises(NotFoundError, SepaDirectDebitAccount.find, sepa_direct_debit_account_token)
+
+    def test_delete_raises_on_not_found(self):
+        self.assertRaises(NotFoundError, SepaDirectDebitAccount.delete, "non-existant-token")
diff --git a/tests/integration/test_subscription.py b/tests/integration/test_subscription.py
index 0277df4..184ea16 100644
--- a/tests/integration/test_subscription.py
+++ b/tests/integration/test_subscription.py
@@ -31,7 +31,6 @@ class TestSubscription(unittest.TestCase):
         subscription = result.subscription
         self.assertNotEqual(None, re.search(r"\A\w{6}\Z", subscription.id))
         self.assertEqual(Decimal("12.34"), subscription.price)
-        self.assertEqual(Decimal("12.34"), subscription.next_bill_amount)
         self.assertEqual(Decimal("12.34"), subscription.next_billing_period_amount)
         self.assertEqual(Subscription.Status.Active, subscription.status)
         self.assertEqual("integration_trialless_plan", subscription.plan_id)
@@ -564,9 +563,9 @@ class TestSubscription(unittest.TestCase):
         found_subscription = Subscription.find(subscription.id)
         self.assertEqual(subscription.id, found_subscription.id)
 
-    @raises_with_regexp(NotFoundError, "subscription with id 'bad_token' not found")
     def test_find_with_invalid_token(self):
-        Subscription.find("bad_token")
+        with self.assertRaisesRegex(NotFoundError, "subscription with id 'bad_token' not found"):
+            Subscription.find("bad_token")
 
     def test_update_creates_a_prorated_transaction_when_merchant_is_set_to_prorate(self):
         result = Subscription.update(self.updateable_subscription.id, {
@@ -744,11 +743,11 @@ class TestSubscription(unittest.TestCase):
         self.assertEqual(1, len(id_errors))
         self.assertEqual("81906", id_errors[0].code)
 
-    @raises(NotFoundError)
     def test_update_raises_error_when_subscription_not_found(self):
-        Subscription.update("notfound", {
-            "id": "newid",
-        })
+        with self.assertRaises(NotFoundError):
+            Subscription.update("notfound", {
+                "id": "newid",
+            })
 
     def test_update_allows_overriding_of_inherited_add_ons_and_discounts(self):
         subscription = Subscription.create({
@@ -1035,9 +1034,9 @@ class TestSubscription(unittest.TestCase):
         self.assertTrue(len(status_errors), 1)
         self.assertEqual("81905", status_errors[0].code)
 
-    @raises(NotFoundError)
     def test_cancel_raises_not_found_error_with_bad_subscription(self):
-        Subscription.cancel("notreal")
+        with self.assertRaises(NotFoundError):
+            Subscription.cancel("notreal")
 
     def test_search_with_argument_list_rather_than_literal_list(self):
         trial_subscription = Subscription.create({
@@ -1309,23 +1308,6 @@ class TestSubscription(unittest.TestCase):
         self.assertTrue(TestHelper.includes(collection, subscription_found))
         self.assertFalse(TestHelper.includes(collection, subscription_not_found))
 
-    def test_retryCharge_without_amount__deprecated(self):
-        subscription = Subscription.create({
-            "payment_method_token": self.credit_card.token,
-            "plan_id": TestHelper.trialless_plan["id"],
-        }).subscription
-        TestHelper.make_past_due(subscription)
-
-        result = Subscription.retryCharge(subscription.id)
-
-        self.assertTrue(result.is_success)
-        transaction = result.transaction
-
-        self.assertEqual(subscription.price, transaction.amount)
-        self.assertNotEqual(None, transaction.processor_authorization_code)
-        self.assertEqual(Transaction.Type.Sale, transaction.type)
-        self.assertEqual(Transaction.Status.Authorized, transaction.status)
-
     def test_retry_charge_without_amount(self):
         subscription = Subscription.create({
             "payment_method_token": self.credit_card.token,
@@ -1343,23 +1325,6 @@ class TestSubscription(unittest.TestCase):
         self.assertEqual(Transaction.Type.Sale, transaction.type)
         self.assertEqual(Transaction.Status.Authorized, transaction.status)
 
-    def test_retryCharge_with_amount__deprecated(self):
-        subscription = Subscription.create({
-            "payment_method_token": self.credit_card.token,
-            "plan_id": TestHelper.trialless_plan["id"],
-        }).subscription
-        TestHelper.make_past_due(subscription)
-
-        result = Subscription.retryCharge(subscription.id, Decimal(TransactionAmounts.Authorize))
-
-        self.assertTrue(result.is_success)
-        transaction = result.transaction
-
-        self.assertEqual(Decimal(TransactionAmounts.Authorize), transaction.amount)
-        self.assertNotEqual(None, transaction.processor_authorization_code)
-        self.assertEqual(Transaction.Type.Sale, transaction.type)
-        self.assertEqual(Transaction.Status.Authorized, transaction.status)
-
     def test_retry_charge_with_amount(self):
         subscription = Subscription.create({
             "payment_method_token": self.credit_card.token,
@@ -1447,7 +1412,7 @@ class TestSubscription(unittest.TestCase):
 
     def test_create_fails_with_paypal_future_payment_method_nonce(self):
         result = Subscription.create({
-            "payment_method_nonce": Nonces.PayPalFuturePayment,
+            "payment_method_nonce": Nonces.PayPalBillingAgreement,
             "plan_id": TestHelper.trialless_plan["id"]
         })
 
diff --git a/tests/integration/test_testing_gateway.py b/tests/integration/test_testing_gateway.py
index 95e04b3..df0090b 100644
--- a/tests/integration/test_testing_gateway.py
+++ b/tests/integration/test_testing_gateway.py
@@ -8,30 +8,30 @@ class TestTestingGateway(unittest.TestCase):
         braintree_gateway = BraintreeGateway(config)
         self.gateway = TestingGateway(braintree_gateway)
 
-    @raises(TestOperationPerformedInProductionError)
     def test_error_is_raised_in_production_for_settle_transaction(self):
-        self.gateway.settle_transaction("")
+        with self.assertRaises(TestOperationPerformedInProductionError):
+            self.gateway.settle_transaction("")
 
-    @raises(TestOperationPerformedInProductionError)
     def test_error_is_raised_in_production_for_make_past_due(self):
-        self.gateway.make_past_due("")
+        with self.assertRaises(TestOperationPerformedInProductionError):
+            self.gateway.make_past_due("")
 
-    @raises(TestOperationPerformedInProductionError)
     def test_error_is_raised_in_production_for_escrow_transaction(self):
-        self.gateway.escrow_transaction("")
+        with self.assertRaises(TestOperationPerformedInProductionError):
+            self.gateway.escrow_transaction("")
 
-    @raises(TestOperationPerformedInProductionError)
     def test_error_is_raised_in_production_for_settlement_confirm_transaction(self):
-        self.gateway.settlement_confirm_transaction("")
+        with self.assertRaises(TestOperationPerformedInProductionError):
+            self.gateway.settlement_confirm_transaction("")
 
-    @raises(TestOperationPerformedInProductionError)
     def test_error_is_raised_in_production_for_settlement_decline_transaction(self):
-        self.gateway.settlement_decline_transaction("")
+        with self.assertRaises(TestOperationPerformedInProductionError):
+            self.gateway.settlement_decline_transaction("")
 
-    @raises(TestOperationPerformedInProductionError)
     def test_error_is_raised_in_production_for_create_3ds_verification(self):
-        self.gateway.create_3ds_verification("", "")
+        with self.assertRaises(TestOperationPerformedInProductionError):
+            self.gateway.create_3ds_verification("", "")
 
-    @raises(TestOperationPerformedInProductionError)
     def test_error_is_raised_in_production(self):
-        self.gateway.settle_transaction("")
+        with self.assertRaises(TestOperationPerformedInProductionError):
+            self.gateway.settle_transaction("")
diff --git a/tests/integration/test_transaction.py b/tests/integration/test_transaction.py
index 9be33f7..c668615 100644
--- a/tests/integration/test_transaction.py
+++ b/tests/integration/test_transaction.py
@@ -5,8 +5,6 @@ from braintree.test.nonces import Nonces
 from braintree.dispute import Dispute
 from braintree.payment_instrument_type import PaymentInstrumentType
 
-import braintree.test.venmo_sdk as venmo_sdk
-
 class TestTransaction(unittest.TestCase):
 
     def test_sale(self):
@@ -24,22 +22,41 @@ class TestTransaction(unittest.TestCase):
         self.assertEqual(ProcessorResponseTypes.Approved, transaction.processor_response_type)
 
     def test_sale_returns_risk_data(self):
-        with AdvancedFraudIntegrationMerchant():
+        with FraudProtectionEnterpriseIntegrationMerchant():
             result = Transaction.sale({
                 "amount": TransactionAmounts.Authorize,
                 "credit_card": {
                     "number": "4111111111111111",
                     "expiration_date": "05/2009"
                 },
-                "device_session_id": "abc123",
+                "device_data": "abc123",
             })
 
             self.assertTrue(result.is_success)
             transaction = result.transaction
             self.assertIsInstance(transaction.risk_data, RiskData)
             self.assertNotEqual(transaction.risk_data.id, None)
-            self.assertEqual(transaction.risk_data.decision, "Approve")
-            self.assertTrue(hasattr(transaction.risk_data, 'device_data_captured'))
+            self.assertTrue(hasattr(transaction.risk_data, 'decision'))
+            self.assertTrue(hasattr(transaction.risk_data, 'decision_reasons'))
+            self.assertTrue(hasattr(transaction.risk_data, 'transaction_risk_score'))
+
+    # def test_sale_returns_chargeback_protection_risk_data(self):
+    #     with EffortlessChargebackProtectionMerchant():
+    #         result = Transaction.sale({
+    #             "amount": TransactionAmounts.Authorize,
+    #             "credit_card": {
+    #                 "number": "4111111111111111",
+    #                 "expiration_date": "05/2009"
+    #             },
+    #             "device_data": "abc123",
+    #         })
+    #
+    #         self.assertTrue(result.is_success)
+    #         transaction = result.transaction
+    #         risk_data = result.transaction.risk_data
+    #         self.assertIsInstance(risk_data, RiskData)
+    #         self.assertNotEqual(risk_data.id, None)
+    #         self.assertTrue(hasattr(risk_data, 'liability_shift'))
 
     def test_sale_receives_network_transaction_id_visa(self):
         result = Transaction.sale({
@@ -122,6 +139,20 @@ class TestTransaction(unittest.TestCase):
         self.assertFalse(result.is_success)
         self.assertEqual(result.transaction, None)
 
+    def test_sale_accepts_external_vault_status_amex(self):
+        result = Transaction.sale({
+            "amount": TransactionAmounts.Authorize,
+            "credit_card": {
+                "number": CreditCardNumbers.Amex,
+                "expiration_date": "05/2009"
+            },
+            "external_vault": {
+                "status": "will_vault",
+            }
+        })
+
+        self.assertTrue(result.is_success)
+        self.assertNotEqual(result.transaction.network_transaction_id, "")
 
     def test_sale_accepts_blank_external_vault_previous_network_transaction_id_non_visa(self):
         result = Transaction.sale({
@@ -238,25 +269,6 @@ class TestTransaction(unittest.TestCase):
 
         self.assertTrue(result.is_success)
 
-    def test_sale_with_external_vault_validation_error_invalid_card_type(self):
-        result = Transaction.sale({
-            "amount": TransactionAmounts.Authorize,
-            "credit_card": {
-                "number": CreditCardNumbers.Elo,
-                "expiration_date": "05/2009"
-            },
-            "external_vault": {
-                "status": "vaulted",
-                "previous_network_transaction_id": "123456789012345"
-            }
-        })
-
-        self.assertFalse(result.is_success)
-        self.assertEqual(
-            ErrorCodes.Transaction.ExternalVault.CardTypeIsInvalid,
-            result.errors.for_object("transaction").for_object("external_vault").on("previous_network_transaction_id")[0].code
-        )
-
     def test_sale_returns_a_successful_result_with_type_of_sale(self):
         result = Transaction.sale({
             "amount": TransactionAmounts.Authorize,
@@ -275,6 +287,7 @@ class TestTransaction(unittest.TestCase):
         self.assertEqual("1111", transaction.credit_card_details.last_4)
         self.assertEqual("05/2009", transaction.credit_card_details.expiration_date)
         self.assertEqual(None, transaction.voice_referral_number)
+        self.assertEqual(None, transaction.acquirer_reference_number)
 
     def test_sale_allows_amount_as_a_decimal(self):
         result = Transaction.sale({
@@ -314,7 +327,9 @@ class TestTransaction(unittest.TestCase):
         result = Transaction.sale({
             "amount": "100.00",
             "order_id": "123",
+            "product_sku": "productsku01",
             "channel": "MyShoppingCartProvider",
+            "exchange_rate_quote_id": "dummyExchangeRateQuoteId-Brainree-Python",
             "credit_card": {
                 "cardholder_name": "The Cardholder",
                 "number": "5105105105105100",
@@ -338,6 +353,7 @@ class TestTransaction(unittest.TestCase):
                 "extended_address": "Suite 403",
                 "locality": "Chicago",
                 "region": "IL",
+                "phone_number": "122-555-1237",
                 "postal_code": "60622",
                 "country_name": "United States of America",
                 "country_code_alpha2": "US",
@@ -352,11 +368,13 @@ class TestTransaction(unittest.TestCase):
                 "extended_address": "Apt 2F",
                 "locality": "Bartlett",
                 "region": "IL",
+                "phone_number": "122-555-1236",
                 "postal_code": "60103",
                 "country_name": "Mexico",
                 "country_code_alpha2": "MX",
                 "country_code_alpha3": "MEX",
-                "country_code_numeric": "484"
+                "country_code_numeric": "484",
+                "shipping_method": Address.ShippingMethod.Electronic
             }
         })
 
@@ -413,6 +431,104 @@ class TestTransaction(unittest.TestCase):
         self.assertEqual("484", transaction.shipping_details.country_code_numeric)
         self.assertEqual(None, transaction.additional_processor_response)
 
+    def test_sale_with_exchange_rate_quote_id(self):
+        result = Transaction.sale({
+            "amount": TransactionAmounts.Authorize,
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
+            },
+            "exchange_rate_quote_id": "dummyExchangeRateQuoteId-Brainree-Python",
+        })
+
+        self.assertTrue(result.is_success)
+        transaction = result.transaction
+        self.assertEqual("1000", transaction.processor_response_code)
+        self.assertEqual(ProcessorResponseTypes.Approved, transaction.processor_response_type)
+
+    def test_sale_with_invalid_exchange_rate_quote_id(self):
+        result = Transaction.sale({
+            "amount": TransactionAmounts.Authorize,
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
+            },
+            "exchange_rate_quote_id": "a" * 4010,
+        })
+
+        self.assertFalse(result.is_success)
+
+        exchange_rate_quote_id_errors = result.errors.for_object("transaction").on("exchange_rate_quote_id")
+        self.assertEqual(ErrorCodes.Transaction.ExchangeRateQuoteIdIsTooLong, exchange_rate_quote_id_errors[0].code)
+
+    def test_sale_with_invalid_product_sku(self):
+        result = Transaction.sale({
+            "amount": Decimal(TransactionAmounts.Authorize),
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
+            },
+            "product_sku": "product$ku!",
+        })
+
+        self.assertFalse(result.is_success)
+
+        product_sku_errors = result.errors.for_object("transaction").on("product_sku")
+        self.assertEqual(1, len(product_sku_errors))
+        self.assertEqual(ErrorCodes.Transaction.ProductSkuIsInvalid, product_sku_errors[0].code)
+
+    def test_sale_to_create_error_tax_amount_is_required_for_aib_swedish(self):
+        result = Transaction.sale({
+            "amount": Decimal(TransactionAmounts.Authorize),
+            "merchant_account_id": TestHelper.aib_swe_ma_merchant_account_id,
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2030"
+            }
+        })
+
+        self.assertFalse(result.is_success)
+
+        tax_amount_errors = result.errors.for_object("transaction").on("tax_amount")
+        self.assertEqual(1, len(tax_amount_errors))
+        self.assertEqual(ErrorCodes.Transaction.TaxAmountIsRequiredForAibSwedish, tax_amount_errors[0].code)
+
+    def test_sale_with_invalid_address(self):
+        customer = Customer.create({
+            "first_name": "Pingu",
+            "last_name": "Penguin",
+        }).customer
+
+        result = Transaction.sale({
+            "amount": Decimal(TransactionAmounts.Authorize),
+            "customer_id": customer.id,
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
+            },
+            "billing": {
+                "phone_number": "123-234-3456=098765"
+            },
+            "shipping": {
+                "phone_number": "123-234-3456=098765",
+                "shipping_method": "urgent"
+            },
+        })
+
+        self.assertFalse(result.is_success)
+
+        billing_phone_number_errors = result.errors.for_object("transaction").for_object("billing").on("phone_number")
+        self.assertEqual(1, len(billing_phone_number_errors))
+        self.assertEqual(ErrorCodes.Transaction.BillingPhoneNumberIsInvalid, billing_phone_number_errors[0].code)
+
+        shipping_phone_number_errors = result.errors.for_object("transaction").for_object("shipping").on("phone_number")
+        self.assertEqual(1, len(shipping_phone_number_errors))
+        self.assertEqual(ErrorCodes.Transaction.ShippingPhoneNumberIsInvalid, shipping_phone_number_errors[0].code)
+
+        shipping_method_errors = result.errors.for_object("transaction").for_object("shipping").on("shipping_method")
+        self.assertEqual(1, len(shipping_method_errors))
+        self.assertEqual(ErrorCodes.Transaction.ShippingMethodIsInvalid, shipping_method_errors[0].code)
+
     def test_sale_with_vault_customer_and_credit_card_data(self):
         customer = Customer.create({
             "first_name": "Pingu",
@@ -560,12 +676,37 @@ class TestTransaction(unittest.TestCase):
             },
             "risk_data": {
                 "customer_browser": "IE7",
-                "customer_ip": "192.168.0.1"
+                "customer_device_id": "customer_device_id_012",
+                "customer_ip": "192.168.0.1",
+                "customer_location_zip": "91244",
+                "customer_tenure": 20
             }
         })
 
         self.assertTrue(result.is_success)
 
+    def test_sale_with_invalid_risk_data_security_parameters(self):
+        result = Transaction.sale({
+            "amount": TransactionAmounts.Authorize,
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
+            },
+            "risk_data": {
+                "customer_browser": "IE7",
+                "customer_device_id": "customer_device_id_012",
+                "customer_ip": "192.168.0.1",
+                "customer_location_zip": "912$4",
+                "customer_tenure": "20"
+            }
+        })
+
+        self.assertFalse(result.is_success)
+
+        customer_location_zip_errors = result.errors.for_object("transaction").for_object("risk_data").on("customer_location_zip")
+        self.assertEqual(1, len(customer_location_zip_errors))
+        self.assertEqual(ErrorCodes.RiskData.CustomerLocationZipInvalidCharacters, customer_location_zip_errors[0].code)
+
     def test_sale_with_billing_address_id(self):
         result = Customer.create({
             "credit_card": {
@@ -598,19 +739,22 @@ class TestTransaction(unittest.TestCase):
         self.assertEqual("123 Fake St.", transaction.billing_details.street_address)
         self.assertEqual(address.id, transaction.billing_details.id)
 
-    def test_sale_with_device_session_id_and_fraud_merchant_id(self):
-        result = Transaction.sale({
-            "amount": TransactionAmounts.Authorize,
-            "credit_card": {
-                "number": "4111111111111111",
-                "expiration_date": "05/2010"
-            },
-            "device_session_id": "abc123",
-            "fraud_merchant_id": "456"
-        })
-
-        self.assertTrue(result.is_success)
+    def test_sale_with_device_session_id_and_fraud_merchant_id_sends_deprecation_warning(self):
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            result = Transaction.sale({
+                "amount": TransactionAmounts.Authorize,
+                "credit_card": {
+                    "number": "4111111111111111",
+                    "expiration_date": "05/2010"
+                    },
+                "device_session_id": "abc123",
+                "fraud_merchant_id": "456"
+                })
 
+            self.assertTrue(result.is_success)
+            assert len(w) > 0
+            assert issubclass(w[-1].category, DeprecationWarning)
 
     def test_sale_with_level_2(self):
         result = Transaction.sale({
@@ -694,6 +838,34 @@ class TestTransaction(unittest.TestCase):
         self.assertEqual(Decimal("2.00"), transaction.shipping_amount)
         self.assertEqual("12345", transaction.ships_from_postal_code)
 
+    def test_sca_exemption_successful_result(self):
+        result = Transaction.sale({
+            "amount": TransactionAmounts.Authorize,
+            "credit_card": {
+                "number": "4023490000000008",
+                "expiration_date": "05/2009"
+            },
+            "sca_exemption": "low_value"
+        })
+
+        self.assertTrue(result.is_success)
+        self.assertEqual(result.transaction.sca_exemption_requested, "low_value")
+
+    def test_invalid_sca_exemption_failure_result(self):
+        result = Transaction.sale({
+            "amount": TransactionAmounts.Authorize,
+            "credit_card": {
+                "number": "4023490000000008",
+                "expiration_date": "05/2009"
+            },
+            "sca_exemption": "invalid"
+        })
+
+        self.assertFalse(result.is_success)
+        errors = result.errors.for_object("transaction").on("sca_exemption")
+        self.assertEqual(1, len(errors))
+        self.assertEqual(ErrorCodes.Transaction.ScaExemptionInvalid, errors[0].code)
+
     def test_create_with_discount_amount_invalid(self):
         result = Transaction.sale({
             "amount": Decimal("100"),
@@ -844,39 +1016,6 @@ class TestTransaction(unittest.TestCase):
         self.assertEqual(ProcessorResponseTypes.HardDeclined, transaction.processor_response_type)
         self.assertEqual("2015 : Transaction Not Allowed", transaction.additional_processor_response)
 
-    def test_sale_with_gateway_rejected_with_incomplete_application(self):
-        gateway = BraintreeGateway(
-            client_id="client_id$development$integration_client_id",
-            client_secret="client_secret$development$integration_client_secret",
-            environment=Environment.Development
-        )
-
-        result = gateway.merchant.create({
-            "email": "name@email.com",
-            "country_code_alpha3": "USA",
-            "payment_methods": ["credit_card", "paypal"]
-        })
-
-        gateway = BraintreeGateway(
-            access_token=result.credentials.access_token,
-            environment=Environment.Development
-        )
-
-        result = gateway.transaction.sale({
-            "amount": "4000.00",
-            "billing": {
-                "street_address": "200 Fake Street"
-            },
-            "credit_card": {
-                "number": "4111111111111111",
-                "expiration_date": "05/2009"
-            }
-        })
-
-        self.assertFalse(result.is_success)
-        transaction = result.transaction
-        self.assertEqual(Transaction.GatewayRejectionReason.ApplicationIncomplete, transaction.gateway_rejection_reason)
-
     def test_sale_with_gateway_rejected_with_avs(self):
         old_merchant_id = Configuration.merchant_id
         old_public_key = Configuration.public_key
@@ -963,18 +1102,63 @@ class TestTransaction(unittest.TestCase):
             Configuration.public_key = old_public_key
             Configuration.private_key = old_private_key
 
+    @unittest.skip("pending")
+    def test_sale_with_gateway_rejected_with_excessive_retry(self):
+        with DuplicateCheckingMerchant():
+            excessive_retry = False
+            counter = 0
+            while not (excessive_retry or counter > 25):
+                result = Transaction.sale({
+                    "amount": TransactionAmounts.Decline,
+                    "credit_card": {
+                        "number": CreditCardNumbers.Visa,
+                        "expiration_date": "05/2017",
+                        "cvv": "333"
+                    }
+                })
+                excessive_retry = (result.transaction.status == braintree.Transaction.Status.GatewayRejected)
+                counter += 1
+
+            self.assertFalse(result.is_success)
+            self.assertEqual(Transaction.GatewayRejectionReason.ExcessiveRetry, result.transaction.gateway_rejection_reason)
+
     def test_sale_with_gateway_rejected_with_fraud(self):
-        result = Transaction.sale({
-            "amount": TransactionAmounts.Authorize,
-            "credit_card": {
-                "number": "4000111111111511",
-                "expiration_date": "05/2017",
-                "cvv": "333"
-            }
-        })
+        with AdvancedFraudKountIntegrationMerchant():
+            result = Transaction.sale({
+                "amount": TransactionAmounts.Authorize,
+                "credit_card": {
+                    "number": "4000111111111511",
+                    "expiration_date": "05/2017",
+                    "cvv": "333"
+                }
+            })
 
-        self.assertFalse(result.is_success)
-        self.assertEqual(Transaction.GatewayRejectionReason.Fraud, result.transaction.gateway_rejection_reason)
+            self.assertFalse(result.is_success)
+            self.assertEqual(Transaction.GatewayRejectionReason.Fraud, result.transaction.gateway_rejection_reason)
+
+    def test_sale_with_gateway_rejected_with_risk_threshold(self):
+        with AdvancedFraudKountIntegrationMerchant():
+            result = Transaction.sale({
+                "amount": TransactionAmounts.Authorize,
+                "credit_card": {
+                    "number": "4111130000000003",
+                    "expiration_date": "05/2017",
+                    "cvv": "333"
+                }
+            })
+
+            self.assertFalse(result.is_success)
+            self.assertEqual(Transaction.GatewayRejectionReason.RiskThreshold, result.transaction.gateway_rejection_reason)
+
+    def test_sale_with_gateway_rejected_with_risk_threshold_nonce(self):
+        with AdvancedFraudKountIntegrationMerchant():
+            result = Transaction.sale({
+                "amount": TransactionAmounts.Authorize,
+                "payment_method_nonce": Nonces.GatewayRejectedRiskThreshold
+            })
+
+            self.assertFalse(result.is_success)
+            self.assertEqual(Transaction.GatewayRejectionReason.RiskThreshold, result.transaction.gateway_rejection_reason)
 
     def test_sale_with_gateway_rejected_token_issuance(self):
         result = Transaction.sale({
@@ -1150,31 +1334,6 @@ class TestTransaction(unittest.TestCase):
             result.errors.for_object("transaction").on("base")[0].code
         )
 
-    def test_sale_with_venmo_sdk_session(self):
-        result = Transaction.sale({
-            "amount": "10.00",
-            "credit_card": {
-                "number": "4111111111111111",
-                "expiration_date": "05/2009"
-            },
-            "options": {
-                "venmo_sdk_session": venmo_sdk.Session
-            }
-        })
-
-        self.assertTrue(result.is_success)
-        self.assertFalse(result.transaction.credit_card_details.venmo_sdk)
-
-    def test_sale_with_venmo_sdk_payment_method_code(self):
-        result = Transaction.sale({
-            "amount": "10.00",
-            "venmo_sdk_payment_method_code": venmo_sdk.VisaPaymentMethodCode
-        })
-
-        self.assertTrue(result.is_success)
-        transaction = result.transaction
-        self.assertEqual("411111", transaction.credit_card_details.bin)
-
     def test_sale_with_payment_method_nonce(self):
         config = Configuration.instantiate()
         parsed_client_token = TestHelper.generate_decoded_client_token()
@@ -1235,6 +1394,7 @@ class TestTransaction(unittest.TestCase):
         self.assertEqual(CreditCard.CardType.Discover, android_pay_card_details.card_type)
         self.assertTrue(int(android_pay_card_details.expiration_month) > 0)
         self.assertTrue(int(android_pay_card_details.expiration_year) > 0)
+        self.assertFalse(android_pay_card_details.is_network_tokenized)
 
     def test_sale_with_fake_android_pay_network_token_nonce(self):
         result = Transaction.sale({
@@ -1250,6 +1410,7 @@ class TestTransaction(unittest.TestCase):
         self.assertEqual(CreditCard.CardType.MasterCard, android_pay_card_details.card_type)
         self.assertTrue(int(android_pay_card_details.expiration_month) > 0)
         self.assertTrue(int(android_pay_card_details.expiration_year) > 0)
+        self.assertTrue(android_pay_card_details.is_network_tokenized)
 
     def test_sale_with_fake_amex_express_checkout_card_nonce(self):
         result = Transaction.sale({
@@ -1281,7 +1442,7 @@ class TestTransaction(unittest.TestCase):
         venmo_account_details = result.transaction.venmo_account_details
         self.assertIsNotNone(venmo_account_details)
         self.assertEqual(venmo_account_details.username, "venmojoe")
-        self.assertEqual(venmo_account_details.venmo_user_id, "Venmo-Joe-1")
+        self.assertEqual(venmo_account_details.venmo_user_id, "1234567891234567891")
 
     def test_sale_with_fake_venmo_account_nonce_and_profile_id(self):
         result = Transaction.sale({
@@ -1298,7 +1459,7 @@ class TestTransaction(unittest.TestCase):
         self.assertTrue(result.is_success)
 
     def test_sale_with_advanced_fraud_checking_skipped(self):
-        with AdvancedFraudIntegrationMerchant():
+        with AdvancedFraudKountIntegrationMerchant():
             result = Transaction.sale({
                 "amount": TransactionAmounts.Authorize,
                 "credit_card": {
@@ -2489,6 +2650,25 @@ class TestTransaction(unittest.TestCase):
         transaction = result.transaction
         self.assertEqual(True, transaction.recurring)
 
+    def test_create_recurring_flag_sends_deprecation_warning(self):
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            result = Transaction.sale({
+                "amount": "100",
+                "credit_card": {
+                    "number": "4111111111111111",
+                    "expiration_date": "05/2009"
+                },
+                "recurring": True
+            })
+
+            self.assertTrue(result.is_success)
+            transaction = result.transaction
+            self.assertEqual(True, transaction.recurring)
+            assert len(w) > 0
+            assert issubclass(w[-1].category, DeprecationWarning)
+            assert "Use transaction_source parameter instead" in str(w[-1].message)
+
     def test_create_can_set_transaction_source_flag_recurring_first(self):
         result = Transaction.sale({
             "amount": "100",
@@ -2517,6 +2697,30 @@ class TestTransaction(unittest.TestCase):
         transaction = result.transaction
         self.assertEqual(True, transaction.recurring)
 
+    def test_create_can_set_transaction_source_installment_first(self):
+        result = Transaction.sale({
+            "amount": "100",
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
+            },
+            "transaction_source": "installment_first"
+        })
+
+        self.assertTrue(result.is_success)
+
+    def test_create_can_set_transaction_source_flag_installment(self):
+        result = Transaction.sale({
+            "amount": "100",
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
+            },
+            "transaction_source": "installment"
+        })
+
+        self.assertTrue(result.is_success)
+
     def test_create_can_set_transaction_source_flag_merchant(self):
         result = Transaction.sale({
             "amount": "100",
@@ -2988,10 +3192,11 @@ class TestTransaction(unittest.TestCase):
         TestHelper.settle_transaction(transaction.id)
         found_transaction = Transaction.find(transaction.id)
         self.assertEqual(transaction.id, found_transaction.id)
+        self.assertNotEqual(None, transaction.graphql_id)
 
-    @raises_with_regexp(NotFoundError, "transaction with id 'notreal' not found")
     def test_find_for_bad_transaction_raises_not_found_error(self):
-        Transaction.find("notreal")
+        with self.assertRaisesRegex(NotFoundError, "transaction with id 'notreal' not found"):
+            Transaction.find("notreal")
 
     def test_void_with_successful_result(self):
         transaction = Transaction.sale({
@@ -3071,121 +3276,12 @@ class TestTransaction(unittest.TestCase):
             result.errors.for_object("transaction").for_object("billing").on("country_name")[0].code
         )
 
-    def test_sale_from_transparent_redirect_with_successful_result(self):
-        tr_data = {
-            "transaction": {
-                "amount": TransactionAmounts.Authorize,
-            }
-        }
-        post_params = {
-            "tr_data": Transaction.tr_data_for_sale(tr_data, "http://example.com/path"),
-            "transaction[credit_card][number]": "4111111111111111",
-            "transaction[credit_card][expiration_date]": "05/2010",
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params, Transaction.transparent_redirect_create_url())
-        result = Transaction.confirm_transparent_redirect(query_string)
-        self.assertTrue(result.is_success)
-
-        transaction = result.transaction
-        self.assertEqual(Decimal(TransactionAmounts.Authorize), transaction.amount)
-        self.assertEqual(Transaction.Type.Sale, transaction.type)
-        self.assertEqual("411111", transaction.credit_card_details.bin)
-        self.assertEqual("1111", transaction.credit_card_details.last_4)
-        self.assertEqual("05/2010", transaction.credit_card_details.expiration_date)
-
-    def test_sale_from_transparent_redirect_with_error_result(self):
-        tr_data = {
-            "transaction": {
-                "amount": TransactionAmounts.Authorize,
-            }
-        }
-        post_params = {
-            "tr_data": Transaction.tr_data_for_sale(tr_data, "http://example.com/path"),
-            "transaction[credit_card][number]": "booya",
-            "transaction[credit_card][expiration_date]": "05/2010",
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params, Transaction.transparent_redirect_create_url())
-        result = Transaction.confirm_transparent_redirect(query_string)
-        self.assertFalse(result.is_success)
-        self.assertTrue(len(result.errors.for_object("transaction").for_object("credit_card").on("number")) > 0)
-
-    def test_sale_from_transparent_redirect_with_403_and_message(self):
-        tr_data = {
-            "transaction": {
-                "amount": TransactionAmounts.Authorize
-            }
-        }
-        post_params = {
-            "tr_data": Transaction.tr_data_for_sale(tr_data, "http://example.com/path"),
-            "transaction[credit_card][number]": "booya",
-            "transaction[credit_card][expiration_date]": "05/2010",
-            "transaction[bad]": "value"
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params, Transaction.transparent_redirect_create_url())
-        try:
-            Transaction.confirm_transparent_redirect(query_string)
-            self.fail()
-        except AuthorizationError as e:
-            self.assertEqual("Invalid params: transaction[bad]", str(e))
-
-    def test_credit_from_transparent_redirect_with_successful_result(self):
-        tr_data = {
-            "transaction": {
-                "amount": TransactionAmounts.Authorize,
-            }
-        }
-        post_params = {
-            "tr_data": Transaction.tr_data_for_credit(tr_data, "http://example.com/path"),
-            "transaction[credit_card][number]": "4111111111111111",
-            "transaction[credit_card][expiration_date]": "05/2010",
-            "transaction[billing][country_code_alpha2]": "US",
-            "transaction[billing][country_code_alpha3]": "USA",
-            "transaction[billing][country_code_numeric]": "840",
-            "transaction[billing][country_name]": "United States of America"
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params, Transaction.transparent_redirect_create_url())
-        result = Transaction.confirm_transparent_redirect(query_string)
-        self.assertTrue(result.is_success)
-
-        transaction = result.transaction
-        self.assertEqual(Decimal(TransactionAmounts.Authorize), transaction.amount)
-        self.assertEqual(Transaction.Type.Credit, transaction.type)
-        self.assertEqual("411111", transaction.credit_card_details.bin)
-        self.assertEqual("1111", transaction.credit_card_details.last_4)
-        self.assertEqual("05/2010", transaction.credit_card_details.expiration_date)
-
-        self.assertEqual("US", transaction.billing_details.country_code_alpha2)
-        self.assertEqual("USA", transaction.billing_details.country_code_alpha3)
-        self.assertEqual("840", transaction.billing_details.country_code_numeric)
-        self.assertEqual("United States of America", transaction.billing_details.country_name)
-
-    def test_credit_from_transparent_redirect_with_error_result(self):
-        tr_data = {
-            "transaction": {
-                "amount": TransactionAmounts.Authorize,
-            }
-        }
-        post_params = {
-            "tr_data": Transaction.tr_data_for_credit(tr_data, "http://example.com/path"),
-            "transaction[credit_card][number]": "booya",
-            "transaction[credit_card][expiration_date]": "05/2010",
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params, Transaction.transparent_redirect_create_url())
-        result = Transaction.confirm_transparent_redirect(query_string)
-        self.assertFalse(result.is_success)
-        self.assertTrue(len(result.errors.for_object("transaction").for_object("credit_card").on("number")) > 0)
-
-    def test_submit_for_settlement_without_amount(self):
-        transaction = Transaction.sale({
-            "amount": TransactionAmounts.Authorize,
-            "credit_card": {
-                "number": "4111111111111111",
-                "expiration_date": "05/2009"
+    def test_submit_for_settlement_without_amount(self):
+        transaction = Transaction.sale({
+            "amount": TransactionAmounts.Authorize,
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
             }
         }).transaction
 
@@ -3248,7 +3344,77 @@ class TestTransaction(unittest.TestCase):
         self.assertEqual("3334445555", submitted_transaction.descriptor.phone)
         self.assertEqual("ebay.com", submitted_transaction.descriptor.url)
 
-    @raises_with_regexp(KeyError, "'Invalid keys: invalid_param'")
+    def test_submit_for_settlement_with_level_2_data(self):
+        transaction = Transaction.sale({
+            "amount": TransactionAmounts.Authorize,
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
+            }
+        }).transaction
+
+        params = {"purchase_order_number": "123456", "tax_amount": "2.00", "tax_exempt": False}
+
+        submitted_transaction = Transaction.submit_for_settlement(transaction.id, Decimal("900"), params).transaction
+
+        self.assertEqual(Transaction.Status.SubmittedForSettlement, submitted_transaction.status)
+
+    def test_submit_for_settlement_with_level_3_data(self):
+        transaction = Transaction.sale({
+            "amount": TransactionAmounts.Authorize,
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
+            }
+        }).transaction
+
+        params = {
+                "discount_amount": "12.33",
+                "shipping_amount": "5.00",
+                "ships_from_postal_code": "90210",
+                "line_items": [{
+                    "quantity": "1.0232",
+                    "name": "Name #1",
+                    "kind": TransactionLineItem.Kind.Debit,
+                    "unit_amount": "45.1232",
+                    "total_amount": "45.15",
+                    }]
+                }
+
+        submitted_transaction = Transaction.submit_for_settlement(transaction.id, Decimal("900"), params).transaction
+
+        self.assertEqual(Transaction.Status.SubmittedForSettlement, submitted_transaction.status)
+
+    def test_submit_for_settlement_with_shipping_data(self):
+        transaction = Transaction.sale({
+            "amount": TransactionAmounts.Authorize,
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
+            }
+        }).transaction
+
+        params = {
+                "discount_amount": "12.33",
+                "shipping_amount": "5.00",
+                "ships_from_postal_code": "90210",
+                "shipping": {
+                    "first_name": "Andrew",
+                    "last_name": "Mason",
+                    "company": "Braintree",
+                    "street_address": "456 W Main St",
+                    "extended_address": "Apt 2F",
+                    "locality": "Bartlett",
+                    "region": "IL",
+                    "postal_code": "60103",
+                    "country_name": "United States of America",
+                    }
+                }
+
+        submitted_transaction = Transaction.submit_for_settlement(transaction.id, Decimal("900"), params).transaction
+
+        self.assertEqual(Transaction.Status.SubmittedForSettlement, submitted_transaction.status)
+
     def test_submit_for_settlement_with_invalid_params(self):
         transaction = Transaction.sale({
             "amount": TransactionAmounts.Authorize,
@@ -3267,11 +3433,13 @@ class TestTransaction(unittest.TestCase):
             "invalid_param": "foo",
         }
 
-        Transaction.submit_for_settlement(transaction.id, Decimal("900"), params)
+        with self.assertRaisesRegex(KeyError, "'Invalid keys: invalid_param'"):
+            Transaction.submit_for_settlement(transaction.id, Decimal("900"), params)
 
     def test_submit_for_settlement_with_validation_error(self):
         transaction = Transaction.sale({
             "amount": TransactionAmounts.Authorize,
+            "merchant_account_id": TestHelper.card_processor_brl_merchant_account_id,
             "credit_card": {
                 "number": "4111111111111111",
                 "expiration_date": "05/2009"
@@ -3336,7 +3504,6 @@ class TestTransaction(unittest.TestCase):
         self.assertEqual("3334445555", result.transaction.descriptor.phone)
         self.assertEqual("ebay.com", result.transaction.descriptor.url)
 
-    @raises_with_regexp(KeyError, "'Invalid keys: invalid_key'")
     def test_update_details_with_invalid_params(self):
         transaction = Transaction.sale({
             "amount": "10.00",
@@ -3360,7 +3527,8 @@ class TestTransaction(unittest.TestCase):
             }
         }
 
-        Transaction.update_details(transaction.id, params)
+        with self.assertRaisesRegex(KeyError, "'Invalid keys: invalid_key'"):
+            Transaction.update_details(transaction.id, params)
 
     def test_update_details_with_invalid_order_id(self):
         transaction = Transaction.sale({
@@ -3571,7 +3739,8 @@ class TestTransaction(unittest.TestCase):
         transaction = self.__create_transaction_to_refund()
         options = {
             "amount": Decimal("1.00"),
-            "order_id": "abcd"
+            "order_id": "abcd",
+            "merchant_account_id": TestHelper.non_default_merchant_account_id
         }
         result = Transaction.refund(transaction.id, options)
 
@@ -3584,6 +3753,10 @@ class TestTransaction(unittest.TestCase):
             Decimal("1.00"),
             result.transaction.amount
         )
+        self.assertEqual(
+            TestHelper.non_default_merchant_account_id,
+            result.transaction.merchant_account_id
+        )
 
     def test_refund_returns_an_error_if_unsettled(self):
         transaction = Transaction.sale({
@@ -3605,6 +3778,48 @@ class TestTransaction(unittest.TestCase):
             result.errors.for_object("transaction").on("base")[0].code
         )
 
+    def test_refund_returns_an_error_if_soft_declined(self):
+        transaction = Transaction.sale({
+            "amount": Decimal("9000.00"),
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
+            },
+            "options": {
+                "submit_for_settlement": True
+            }
+        }).transaction
+        TestHelper.settle_transaction(transaction.id)
+
+        result = Transaction.refund(transaction.id, Decimal("2046.00"))
+        refund = result.transaction
+
+        self.assertFalse(result.is_success)
+        self.assertEqual(Transaction.Status.ProcessorDeclined, refund.status)
+        self.assertEqual(ProcessorResponseTypes.SoftDeclined, refund.processor_response_type)
+        self.assertEqual("2046 : Declined", refund.additional_processor_response)
+
+    def test_refund_returns_an_error_if_hard_declined(self):
+        transaction = Transaction.sale({
+            "amount": Decimal("9000.00"),
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
+            },
+            "options": {
+                "submit_for_settlement": True
+            }
+        }).transaction
+        TestHelper.settle_transaction(transaction.id)
+
+        result = Transaction.refund(transaction.id, Decimal("2009.00"))
+        refund = result.transaction
+
+        self.assertFalse(result.is_success)
+        self.assertEqual(Transaction.Status.ProcessorDeclined, refund.status)
+        self.assertEqual(ProcessorResponseTypes.HardDeclined, refund.processor_response_type)
+        self.assertEqual("2009 : No Such Issuer", refund.additional_processor_response)
+
     @staticmethod
     def __create_transaction_to_refund():
         transaction = Transaction.sale({
@@ -3651,6 +3866,30 @@ class TestTransaction(unittest.TestCase):
         TestHelper.escrow_transaction(transaction.id)
         return transaction
 
+    @staticmethod
+    def __first_data_transaction_params():
+        merchant_dict = {
+                "merchant_account_id": TestHelper.fake_first_data_merchant_account_id,
+                "amount": "75.50",
+                "credit_card": {
+                    "number": "5105105105105100",
+                    "expiration_date": "05/2012"
+                    },
+                }
+        return merchant_dict
+
+    @staticmethod
+    def __first_data_visa_transaction_params():
+        merchant_dict = {
+                "merchant_account_id": TestHelper.fake_first_data_merchant_account_id,
+                 "amount": "75.50",
+                  "credit_card": {
+                       "number": CreditCardNumbers.Visa,
+                       "expiration_date": "05/2012"
+                       }
+                  }
+        return merchant_dict
+
     def test_snapshot_plan_id_add_ons_and_discounts_from_subscription(self):
         credit_card = Customer.create({
             "first_name": "Mike",
@@ -4167,6 +4406,172 @@ class TestTransaction(unittest.TestCase):
             result.errors.for_object("transaction").on("three_d_secure_token")[0].code
         )
 
+    def test_sale_with_three_d_secure_authentication_id_with_vaulted_token(self):
+        customer_id = Customer.create().customer.id
+        credit_card_result = CreditCard.create({
+            "customer_id": customer_id,
+            "number": "4111111111111111",
+            "expiration_date": "12/2020",
+        })
+        config = Configuration(
+            environment=Environment.Development,
+            merchant_id="integration_merchant_id",
+            public_key="integration_public_key",
+            private_key="integration_private_key"
+        )
+        gateway = BraintreeGateway(config)
+        payment_method_token = credit_card_result.credit_card.token
+
+        three_d_secure_authentication_id = TestHelper.create_3ds_verification(TestHelper.three_d_secure_merchant_account_id, {
+            "number": "4111111111111111",
+            "expiration_month": "12",
+            "expiration_year": "2020",
+        })
+
+        result = Transaction.sale({
+            "merchant_account_id": TestHelper.three_d_secure_merchant_account_id,
+            "amount": TransactionAmounts.Authorize,
+            "payment_method_token": payment_method_token,
+            "three_d_secure_authentication_id": three_d_secure_authentication_id
+        })
+
+        self.assertTrue(result.is_success)
+
+
+    def test_sale_with_three_d_secure_authentication_id_with_nonce(self):
+        config = Configuration(
+            environment=Environment.Development,
+            merchant_id="integration_merchant_id",
+            public_key="integration_public_key",
+            private_key="integration_private_key"
+        )
+        gateway = BraintreeGateway(config)
+        credit_card = {
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_month": "12",
+                "expiration_year": "2020"
+            }
+        }
+        nonce = TestHelper.generate_three_d_secure_nonce(gateway, credit_card)
+
+        found_nonce = PaymentMethodNonce.find(nonce)
+        three_d_secure_info = found_nonce.three_d_secure_info
+
+        result = Transaction.sale({
+            "merchant_account_id": TestHelper.three_d_secure_merchant_account_id,
+            "amount": TransactionAmounts.Authorize,
+            "payment_method_nonce": nonce,
+            "three_d_secure_authentication_id": three_d_secure_info.three_d_secure_authentication_id
+        })
+
+        self.assertTrue(result.is_success)
+
+    def test_sale_returns_error_with_none_three_d_secure_authentication_id(self):
+        result = Transaction.sale({
+            "merchant_account_id": TestHelper.three_d_secure_merchant_account_id,
+            "amount": TransactionAmounts.Authorize,
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
+            },
+            "three_d_secure_authentication_id": None
+        })
+
+        self.assertFalse(result.is_success)
+        self.assertEqual(
+            ErrorCodes.Transaction.ThreeDSecureAuthenticationIdIsInvalid,
+            result.errors.for_object("transaction").on("three_d_secure_authentication_id")[0].code
+        )
+
+    def test_sale_returns_error_with_mismatched_payment_data_with_three_d_secure_authentication_id(self):
+        three_d_secure_authentication_id = TestHelper.create_3ds_verification(TestHelper.three_d_secure_merchant_account_id, {
+            "number": "4111111111111111",
+            "expiration_month": "05",
+            "expiration_year": "2009",
+        })
+
+        result = Transaction.sale({
+            "merchant_account_id": TestHelper.three_d_secure_merchant_account_id,
+            "amount": TransactionAmounts.Authorize,
+            "credit_card": {
+                "number": "5105105105105100",
+                "expiration_date": "05/2009"
+            },
+            "three_d_secure_authentication_id": three_d_secure_authentication_id
+        })
+
+        self.assertFalse(result.is_success)
+        self.assertEqual(
+            ErrorCodes.Transaction.ThreeDSecureTransactionPaymentMethodDoesntMatchThreeDSecureAuthenticationPaymentMethod,
+            result.errors.for_object("transaction").on("three_d_secure_authentication_id")[0].code
+        )
+
+    def test_sale_returns_error_with_mismatched_3ds_data_with_three_d_secure_authentication_id(self):
+        three_d_secure_authentication_id = TestHelper.create_3ds_verification(TestHelper.three_d_secure_merchant_account_id, {
+            "number": "4111111111111111",
+            "expiration_month": "12",
+            "expiration_year": "2020",
+        })
+
+        config = Configuration(
+            environment=Environment.Development,
+            merchant_id="integration_merchant_id",
+            public_key="integration_public_key",
+            private_key="integration_private_key"
+        )
+        gateway = BraintreeGateway(config)
+        credit_card = {
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_month": "12",
+                "expiration_year": "2020"
+            }
+        }
+        nonce = TestHelper.generate_three_d_secure_nonce(gateway, credit_card)
+        result = Transaction.sale({
+            "merchant_account_id": TestHelper.three_d_secure_merchant_account_id,
+            "amount": TransactionAmounts.Authorize,
+            "credit_card": {
+                "number": "5105105105105100",
+                "expiration_date": "05/2009"
+            },
+            "payment_method_nonce": nonce,
+            "three_d_secure_authentication_id": three_d_secure_authentication_id
+        })
+
+        self.assertFalse(result.is_success)
+        self.assertEqual(
+            ErrorCodes.Transaction.ThreeDSecureAuthenticationIdDoesntMatchNonceThreeDSecureAuthentication,
+            result.errors.for_object("transaction").on("three_d_secure_authentication_id")[0].code
+        )
+
+    def test_transaction_with_both_three_d_secure_authentication_id_and_three_d_secure_pass_thru(self):
+        three_d_secure_authentication_id = TestHelper.create_3ds_verification(TestHelper.three_d_secure_merchant_account_id, {
+            "number": "4111111111111111",
+            "expiration_month": "05",
+            "expiration_year": "2009",
+        })
+        result = Transaction.sale({
+            "merchant_account_id": TestHelper.three_d_secure_merchant_account_id,
+            "amount": TransactionAmounts.Authorize,
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
+            },
+            "three_d_secure_authentication_id": three_d_secure_authentication_id,
+            "three_d_secure_pass_thru": {
+                "eci_flag": "02",
+                "cavv": "some-cavv",
+                "xid": "some-xid"
+            }
+        })
+        self.assertFalse(result.is_success)
+        self.assertEqual(
+            ErrorCodes.Transaction.ThreeDSecureAuthenticationIdWithThreeDSecurePassThruIsInvalid,
+            result.errors.for_object("transaction").on("three_d_secure_authentication_id")[0].code
+        )
+
     def test_transaction_with_three_d_secure_pass_thru(self):
         result = Transaction.sale({
             "merchant_account_id": TestHelper.three_d_secure_merchant_account_id,
@@ -4276,7 +4681,7 @@ class TestTransaction(unittest.TestCase):
             "amount": TransactionAmounts.Authorize,
             "credit_card": {
                 "number": "4111111111111111",
-                "expiration_date": "10/2020",
+                "expiration_date": ExpirationHelper.ADYEN.value,
                 "cvv": "737"
             },
             "three_d_secure_pass_thru": {
@@ -4300,9 +4705,9 @@ class TestTransaction(unittest.TestCase):
             "amount": TransactionAmounts.Authorize,
             "credit_card": {
                 "number": "4111111111111111",
-                "expiration_date": "10/2020",
+                "expiration_date": ExpirationHelper.ADYEN.value,
                 "cvv": "737"
-            },
+                },
             "three_d_secure_pass_thru": {
                 "eci_flag": "02",
                 "cavv": "some-cavv",
@@ -4327,7 +4732,7 @@ class TestTransaction(unittest.TestCase):
             "amount": TransactionAmounts.Authorize,
             "credit_card": {
                 "number": "4111111111111111",
-                "expiration_date": "10/2020",
+                "expiration_date": ExpirationHelper.ADYEN.value,
                 "cvv": "737"
             },
             "three_d_secure_pass_thru": {
@@ -4354,7 +4759,7 @@ class TestTransaction(unittest.TestCase):
             "amount": TransactionAmounts.Authorize,
             "credit_card": {
                 "number": "4111111111111111",
-                "expiration_date": "10/2020",
+                "expiration_date": ExpirationHelper.ADYEN.value,
                 "cvv": "737"
             },
             "three_d_secure_pass_thru": {
@@ -4375,6 +4780,7 @@ class TestTransaction(unittest.TestCase):
             result.errors.for_object("transaction").for_object("three_d_secure_pass_thru").on("cavv_algorithm")[0].code
         )
 
+    @unittest.skip("until we have a more stable ci env")
     def test_sale_with_amex_rewards_succeeds(self):
         result = Transaction.sale({
             "merchant_account_id": TestHelper.fake_amex_direct_merchant_account_id,
@@ -4399,6 +4805,7 @@ class TestTransaction(unittest.TestCase):
         self.assertEqual(Transaction.Type.Sale, transaction.type)
         self.assertEqual(Transaction.Status.SubmittedForSettlement, transaction.status)
 
+    @unittest.skip("until we have a more stable ci env")
     def test_sale_with_amex_rewards_succeeds_even_if_card_is_ineligible(self):
         result = Transaction.sale({
             "merchant_account_id": TestHelper.fake_amex_direct_merchant_account_id,
@@ -4423,6 +4830,7 @@ class TestTransaction(unittest.TestCase):
         self.assertEqual(Transaction.Type.Sale, transaction.type)
         self.assertEqual(Transaction.Status.SubmittedForSettlement, transaction.status)
 
+    @unittest.skip("until we have a more stable ci env")
     def test_sale_with_amex_rewards_succeeds_even_if_card_balance_is_insufficient(self):
         result = Transaction.sale({
             "merchant_account_id": TestHelper.fake_amex_direct_merchant_account_id,
@@ -4447,6 +4855,7 @@ class TestTransaction(unittest.TestCase):
         self.assertEqual(Transaction.Type.Sale, transaction.type)
         self.assertEqual(Transaction.Status.SubmittedForSettlement, transaction.status)
 
+    @unittest.skip("until we have a more stable ci env")
     def test_submit_for_settlement_with_amex_rewards_succeeds(self):
         result = Transaction.sale({
             "merchant_account_id": TestHelper.fake_amex_direct_merchant_account_id,
@@ -4473,6 +4882,7 @@ class TestTransaction(unittest.TestCase):
         submitted_transaction = Transaction.submit_for_settlement(transaction.id).transaction
         self.assertEqual(Transaction.Status.SubmittedForSettlement, submitted_transaction.status)
 
+    @unittest.skip("until we have a more stable ci env")
     def test_submit_for_settlement_with_amex_rewards_succeeds_even_if_card_is_ineligible(self):
         result = Transaction.sale({
             "merchant_account_id": TestHelper.fake_amex_direct_merchant_account_id,
@@ -4499,6 +4909,7 @@ class TestTransaction(unittest.TestCase):
         submitted_transaction = Transaction.submit_for_settlement(transaction.id).transaction
         self.assertEqual(Transaction.Status.SubmittedForSettlement, submitted_transaction.status)
 
+    @unittest.skip("until we have a more stable ci env")
     def test_submit_for_settlement_with_amex_rewards_succeeds_even_if_card_balance_is_insufficient(self):
         result = Transaction.sale({
             "merchant_account_id": TestHelper.fake_amex_direct_merchant_account_id,
@@ -4525,6 +4936,10 @@ class TestTransaction(unittest.TestCase):
         submitted_transaction = Transaction.submit_for_settlement(transaction.id).transaction
         self.assertEqual(Transaction.Status.SubmittedForSettlement, submitted_transaction.status)
 
+    def test_find_exposes_acquirer_reference_numbers(self):
+        transaction = Transaction.find("transactionwithacquirerreferencenumber")
+        self.assertEqual("123456789 091019", transaction.acquirer_reference_number)
+
     def test_find_exposes_authorization_adjustments(self):
         transaction = Transaction.find("authadjustmenttransaction")
         authorization_adjustment = transaction.authorization_adjustments[0]
@@ -4586,7 +5001,7 @@ class TestTransaction(unittest.TestCase):
         self.assertEqual("xidvalue", three_d_secure_info.xid)
         self.assertEqual("dstxnid", three_d_secure_info.ds_transaction_id)
         self.assertEqual("07", three_d_secure_info.eci_flag)
-        self.assertEqual("1.0.2", three_d_secure_info.three_d_secure_version)
+        self.assertIsNotNone(three_d_secure_info.three_d_secure_version)
 
     def test_find_exposes_none_for_null_three_d_secure_info(self):
         transaction = Transaction.find("settledtransaction")
@@ -4859,7 +5274,7 @@ class TestTransaction(unittest.TestCase):
     def test_creating_paypal_transaction_with_future_payment_nonce(self):
         result = Transaction.sale({
             "amount": TransactionAmounts.Authorize,
-            "payment_method_nonce": Nonces.PayPalFuturePayment
+            "payment_method_nonce": Nonces.PayPalBillingAgreement
         })
 
         self.assertTrue(result.is_success)
@@ -4870,6 +5285,21 @@ class TestTransaction(unittest.TestCase):
         self.assertNotEqual(None, re.search(r'AUTH-\w+', transaction.paypal_details.authorization_id))
         self.assertNotEqual(None, transaction.paypal_details.debug_id)
 
+    def test_creating_paypal_transaction_with_billing_agreement_nonce(self):
+        result = Transaction.sale({
+            "amount": TransactionAmounts.Authorize,
+            "payment_method_nonce": Nonces.PayPalBillingAgreement
+        })
+
+        self.assertTrue(result.is_success)
+        transaction = result.transaction
+
+        self.assertEqual(transaction.paypal_details.payer_email, "payer@example.com")
+        self.assertNotEqual(None, re.search(r'PAY-\w+', transaction.paypal_details.payment_id))
+        self.assertNotEqual(None, re.search(r'AUTH-\w+', transaction.paypal_details.authorization_id))
+        self.assertNotEqual(None, transaction.paypal_details.debug_id)
+        self.assertNotEqual(None, transaction.paypal_details.billing_agreement_id)
+
     def test_validation_failure_on_invalid_paypal_nonce(self):
         http = ClientApiHttp.create()
 
@@ -4899,12 +5329,50 @@ class TestTransaction(unittest.TestCase):
         error_code = result.errors.for_object("transaction").on("payment_method_nonce")[0].code
         self.assertEqual(error_code, ErrorCodes.Transaction.PaymentMethodNonceUnknown)
 
+    def test_creating_sepa_direct_debit_transaction_with_vaulted_fake_nonce(self):
+        customer_id = Customer.create().customer.id
+
+        result = PaymentMethod.create({
+            "customer_id": customer_id,
+            "payment_method_nonce": Nonces.SepaDirectDebit,
+        })
+
+        self.assertTrue(result.is_success)
+
+        transaction_result = Transaction.sale({
+            "amount": TransactionAmounts.Authorize,
+            "payment_method_token": result.payment_method.token,
+            "options": {
+                "submit_for_settlement": True,
+            }
+        })
+
+        self.assertTrue(transaction_result.is_success)
+        transaction = transaction_result.transaction
+        sdd_details = transaction.sepa_direct_debit_account_details
+
+        self.assertEqual(sdd_details.bank_reference_token, "a-fake-bank-reference-token")
+        self.assertEqual(sdd_details.mandate_type, "RECURRENT")
+        self.assertEqual(sdd_details.last_4, "1234")
+        self.assertEqual(sdd_details.merchant_or_partner_customer_id, "a-fake-mp-customer-id")
+        self.assertEqual(sdd_details.transaction_fee_amount, "0.01")
+        self.assertEqual(sdd_details.transaction_fee_currency_iso_code, "USD")
+        self.assertEqual(sdd_details.token, result.payment_method.token)
+        self.assertTrue(sdd_details.capture_id)
+        self.assertTrue(sdd_details.global_id)
+        self.assertIsNone(sdd_details.refund_id)
+        self.assertIsNone(sdd_details.debug_id)
+        self.assertIsNone(sdd_details.paypal_v2_order_id)
+        self.assertIsNone(sdd_details.refund_from_transaction_fee_amount)
+        self.assertIsNone(sdd_details.refund_from_transaction_fee_currency_iso_code)
+        self.assertIsNone(sdd_details.settlement_type)
+
     def test_creating_paypal_transaction_with_vaulted_token(self):
         customer_id = Customer.create().customer.id
 
         result = PaymentMethod.create({
             "customer_id": customer_id,
-            "payment_method_nonce": Nonces.PayPalFuturePayment
+            "payment_method_nonce": Nonces.PayPalBillingAgreement
         })
 
         self.assertTrue(result.is_success)
@@ -5184,7 +5652,6 @@ class TestTransaction(unittest.TestCase):
         self.assertEqual("3334445555", submitted_transaction.descriptor.phone)
         self.assertEqual("ebay.com", submitted_transaction.descriptor.url)
 
-    @raises_with_regexp(KeyError, "'Invalid keys: invalid_param'")
     def test_submit_for_partial_settlement_with_invalid_params(self):
         transaction = Transaction.sale({
             "amount": TransactionAmounts.Authorize,
@@ -5203,7 +5670,8 @@ class TestTransaction(unittest.TestCase):
             "invalid_param": "foo",
         }
 
-        Transaction.submit_for_partial_settlement(transaction.id, Decimal("900"), params)
+        with self.assertRaisesRegex(KeyError, "'Invalid keys: invalid_param'"):
+            Transaction.submit_for_partial_settlement(transaction.id, Decimal("900"), params)
 
     def test_facilitated_transaction(self):
         granting_gateway, credit_card = TestHelper.create_payment_method_grant_fixtures()
@@ -5234,121 +5702,13 @@ class TestTransaction(unittest.TestCase):
 
         self.assertTrue(result.transaction.billing["postal_code"], "95131")
 
-    def test_shared_vault_transaction_with_nonce(self):
-        config = Configuration(
-            merchant_id="integration_merchant_public_id",
-            public_key="oauth_app_partner_user_public_key",
-            private_key="oauth_app_partner_user_private_key",
-            environment=Environment.Development
-        )
-
-        gateway = BraintreeGateway(config)
-        customer = gateway.customer.create({"first_name": "Bob"}).customer
-        address = gateway.address.create({
-            "customer_id": customer.id,
-            "first_name": "Joe",
-        }).address
-
-        credit_card = gateway.credit_card.create(
-            params={
-                "customer_id": customer.id,
-                "number": "4111111111111111",
-                "expiration_date": "05/2009",
-            }
-        ).credit_card
-
-        shared_nonce = gateway.payment_method_nonce.create(
-            credit_card.token
-        ).payment_method_nonce.nonce
-
-        oauth_app_gateway = BraintreeGateway(
-            client_id="client_id$development$integration_client_id",
-            client_secret="client_secret$development$integration_client_secret",
-            environment=Environment.Development
-        )
-        code = TestHelper.create_grant(oauth_app_gateway, {
-            "merchant_public_id": "integration_merchant_id",
-            "scope": "grant_payment_method,shared_vault_transactions"
-        })
-        access_token = oauth_app_gateway.oauth.create_token_from_code({
-            "code": code
-        }).credentials.access_token
-
-        recipient_gateway = BraintreeGateway(access_token=access_token)
-
-        result = recipient_gateway.transaction.sale({
-            "shared_payment_method_nonce": shared_nonce,
-            "shared_customer_id": customer.id,
-            "shared_shipping_address_id": address.id,
-            "shared_billing_address_id": address.id,
-            "amount": "100"
-        })
-
-        self.assertTrue(result.is_success)
-        self.assertEqual(result.transaction.shipping_details.first_name, address.first_name)
-        self.assertEqual(result.transaction.billing_details.first_name, address.first_name)
-        self.assertEqual(result.transaction.customer_details.first_name, customer.first_name)
-
-    def test_shared_vault_transaction_with_token(self):
-        config = Configuration(
-            merchant_id="integration_merchant_public_id",
-            public_key="oauth_app_partner_user_public_key",
-            private_key="oauth_app_partner_user_private_key",
-            environment=Environment.Development
-        )
-
-        gateway = BraintreeGateway(config)
-        customer = gateway.customer.create({"first_name": "Bob"}).customer
-        address = gateway.address.create({
-            "customer_id": customer.id,
-            "first_name": "Joe",
-        }).address
-
-        credit_card = gateway.credit_card.create(
-            params={
-                "customer_id": customer.id,
-                "number": "4111111111111111",
-                "expiration_date": "05/2009",
-            }
-        ).credit_card
-
-        oauth_app_gateway = BraintreeGateway(
-            client_id="client_id$development$integration_client_id",
-            client_secret="client_secret$development$integration_client_secret",
-            environment=Environment.Development
-        )
-        code = TestHelper.create_grant(oauth_app_gateway, {
-            "merchant_public_id": "integration_merchant_id",
-            "scope": "grant_payment_method,shared_vault_transactions"
-        })
-        access_token = oauth_app_gateway.oauth.create_token_from_code({
-            "code": code
-        }).credentials.access_token
-
-        recipient_gateway = BraintreeGateway(
-            access_token=access_token,
-        )
-
-        result = recipient_gateway.transaction.sale({
-            "shared_payment_method_token": credit_card.token,
-            "shared_customer_id": customer.id,
-            "shared_shipping_address_id": address.id,
-            "shared_billing_address_id": address.id,
-            "amount": "100"
-        })
-
-        self.assertTrue(result.is_success)
-        self.assertEqual(result.transaction.shipping_details.first_name, address.first_name)
-        self.assertEqual(result.transaction.billing_details.first_name, address.first_name)
-        self.assertEqual(result.transaction.customer_details.first_name, customer.first_name)
-
     def test_sale_elo_card(self):
         result = Transaction.sale({
             "amount": TransactionAmounts.Authorize,
             "merchant_account_id": TestHelper.adyen_merchant_account_id,
             "credit_card": {
                 "number": CreditCardNumbers.Elo,
-                "expiration_date": "10/2020",
+                "expiration_date": ExpirationHelper.ADYEN.value,
                 "cvv": "737",
             }
         })
@@ -5535,3 +5895,257 @@ class TestTransaction(unittest.TestCase):
         self.assertEqual(ProcessorResponseTypes.Approved, transaction.processor_response_type)
         self.assertEqual("XX", transaction.network_response_code)
         self.assertEqual("sample network response text", transaction.network_response_text)
+
+    def test_retrieval_reference_number(self):
+        result = Transaction.sale({
+            "amount": TransactionAmounts.Authorize,
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
+            },
+        })
+
+        transaction = result.transaction
+        self.assertIsNotNone(transaction.retrieval_reference_number)
+
+    def test_network_tokenized_credit_card_transaction(self):
+        result = Transaction.sale({
+            "amount": TransactionAmounts.Authorize,
+            "payment_method_token": "network_tokenized_credit_card",
+            })
+
+        self.assertTrue(result.is_success)
+        transaction = result.transaction
+        self.assertEqual("1000", transaction.processor_response_code)
+        self.assertEqual(ProcessorResponseTypes.Approved, transaction.processor_response_type)
+        self.assertEqual(True, transaction.processed_with_network_token)
+
+    def test_non_network_tokenized_credit_card_transaction(self):
+        result = Transaction.sale({
+            "amount": TransactionAmounts.Authorize,
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
+            },
+        })
+
+        self.assertTrue(result.is_success)
+        transaction = result.transaction
+        self.assertEqual("1000", transaction.processor_response_code)
+        self.assertEqual(ProcessorResponseTypes.Approved, transaction.processor_response_type)
+        self.assertEqual(False, transaction.processed_with_network_token)
+
+    def test_installment_count_transaction(self):
+        result = Transaction.sale({
+            "amount": TransactionAmounts.Authorize,
+            "merchant_account_id": TestHelper.card_processor_brl_merchant_account_id,
+            "credit_card": {
+                "number": CreditCardNumbers.Visa,
+                "expiration_date": "05/2009"
+            },
+            "installments": {
+                "count": 4,
+            },
+        })
+
+        transaction = result.transaction
+        self.assertTrue(result.is_success)
+        self.assertEqual("1000", transaction.processor_response_code)
+        self.assertEqual(ProcessorResponseTypes.Approved, transaction.processor_response_type)
+        self.assertEqual(4, transaction.installment_count)
+
+
+    def test_installment_transaction(self):
+        result = Transaction.sale({
+            "amount": TransactionAmounts.Authorize,
+            "merchant_account_id": TestHelper.card_processor_brl_merchant_account_id,
+            "credit_card": {
+                "number": CreditCardNumbers.Visa,
+                "expiration_date": "05/2009"
+            },
+            "installments": {
+                "count": 4,
+            },
+            "options": {
+                "submit_for_settlement": True
+            },
+        })
+
+        transaction = result.transaction
+        self.assertTrue(result.is_success)
+        self.assertEqual("1000", transaction.processor_response_code)
+        self.assertEqual(ProcessorResponseTypes.Approved, transaction.processor_response_type)
+        self.assertEquals(4, transaction.installment_count)
+        self.assertEquals(4, len(transaction.installments))
+        for i, t in enumerate(transaction.installments) :
+            self.assertEquals('250.00', t['amount'])
+            self.assertEquals('% s_INST_% s'%(transaction.id,i+1), t['id'])
+
+        result = Transaction.refund(transaction.id,"20.00")
+        self.assertTrue(result.is_success)
+
+        refund = result.transaction
+
+        for t in refund.refunded_installments :
+            self.assertEquals('-5.00', t['adjustments'][0]['amount'])
+            self.assertEquals("REFUND",t['adjustments'][0]['kind'])
+
+    def test_manual_key_entry_transactions_with_valid_card_details(self):
+        result = Transaction.sale({
+            "amount": "10.00",
+            "credit_card": {
+                "payment_reader_card_details": {
+                    "encrypted_card_data": "8F34DFB312DC79C24FD5320622F3E11682D79E6B0C0FD881",
+                    "key_serial_number": "FFFFFF02000572A00005",
+                    }
+                },
+            })
+
+        self.assertTrue(result.is_success)
+
+    def test_manual_key_entry_transactions_with_invalid_card_details(self):
+        result = Transaction.sale({
+            "amount": "10.00",
+            "credit_card": {
+                "payment_reader_card_details": {
+                    "encrypted_card_data": "invalid",
+                    "key_serial_number": "invalid",
+                    }
+                },
+            })
+
+        self.assertFalse(result.is_success)
+        error_code = result.errors.for_object("transaction")[0].code
+        self.assertEqual(ErrorCodes.Transaction.PaymentInstrumentNotSupportedByMerchantAccount, error_code)
+
+    def test_adjust_authorization_for_successful_adjustment(self):
+        initial_transaction_sale = Transaction.sale(self.__first_data_transaction_params())
+        self.assertTrue(initial_transaction_sale.is_success)
+        adjusted_authorization_result = Transaction.adjust_authorization(initial_transaction_sale.transaction.id, Decimal("85.50"))
+
+        self.assertTrue(adjusted_authorization_result.is_success)
+        self.assertEqual(adjusted_authorization_result.transaction.amount, Decimal("85.50"))
+
+    def test_adjust_authorization_processor_not_supports_multi_auth_adjustment(self):
+        initial_transaction_sale =  Transaction.sale({
+            "merchant_account_id": TestHelper.default_merchant_account_id,
+            "amount": "75.50",
+            "credit_card": {
+                "number": CreditCardNumbers.Visa,
+                "expiration_date": "06/2009"
+                },
+            })
+        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.ProcessorDoesNotSupportAuthAdjustment, error_code)
+
+    def test_adjust_authorization_amount_submitted_is_zero(self):
+        initial_transaction_sale =  Transaction.sale(self.__first_data_transaction_params())
+        self.assertTrue(initial_transaction_sale.is_success)
+        adjusted_authorization_result = Transaction.adjust_authorization(initial_transaction_sale.transaction.id, "0.0")
+
+        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("authorization_adjustment").on("amount")[0].code
+        self.assertEqual(ErrorCodes.Transaction.AdjustmentAmountMustBeGreaterThanZero, error_code)
+
+    def test_adjust_authorization_when_amount_submitted_same_as_authorized(self):
+        initial_transaction_sale =  Transaction.sale(self.__first_data_transaction_params())
+        self.assertTrue(initial_transaction_sale.is_success)
+        adjusted_authorization_result = Transaction.adjust_authorization(initial_transaction_sale.transaction.id, "75.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("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 }
+        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.TransactionIsNotEligibleForAdjustment, error_code)
+
+    def test_adjust_authorization_when_processor_does_not_support_incremental_auth(self):
+        initial_transaction_sale =  Transaction.sale(self.__first_data_visa_transaction_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.ProcessorDoesNotSupportIncrementalAuth, error_code)
+
+    def test_adjust_authorization_when_processor_does_not_support_reversal(self):
+        initial_transaction_sale =  Transaction.sale(self.__first_data_visa_transaction_params())
+        self.assertTrue(initial_transaction_sale.is_success)
+        adjusted_authorization_result = Transaction.adjust_authorization(initial_transaction_sale.transaction.id, "65.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.ProcessorDoesNotSupportPartialAuthReversal, error_code)
+
+    def test_retried_transaction(self):
+        result = Transaction.sale({
+            "merchant_account_id": TestHelper.default_merchant_account_id,
+            "amount": TransactionAmounts.Decline,
+            "payment_method_token": "network_tokenized_credit_card",
+            })
+        self.assertFalse(result.is_success)
+        transaction = result.transaction
+        self.assertTrue(transaction.retried)
+
+    def test_non_retried_transaction(self):
+        result = Transaction.sale({
+            "merchant_account_id": TestHelper.default_merchant_account_id,
+            "amount": TransactionAmounts.Authorize,
+            "credit_card": {
+                "number": CreditCardNumbers.Visa,
+                "expiration_date": "06/2009"
+                },
+            })
+        self.assertTrue(result.is_success)
+        transaction = result.transaction
+        self.assertFalse(hasattr(transaction, 'retried'))
+
+    def test_ineligible_retry_transaction(self):
+        result = Transaction.sale({
+            "merchant_account_id": TestHelper.non_default_merchant_account_id,
+            "amount": TransactionAmounts.Authorize,
+            "credit_card": {
+                "number": CreditCardNumbers.Visa,
+                "expiration_date": "06/2009"
+                },
+            })
+        self.assertTrue(result.is_success)
+        transaction = result.transaction
+        self.assertFalse(hasattr(transaction, 'retried'))
diff --git a/tests/integration/test_transaction_gateway.py b/tests/integration/test_transaction_gateway.py
new file mode 100644
index 0000000..fa1e888
--- /dev/null
+++ b/tests/integration/test_transaction_gateway.py
@@ -0,0 +1,246 @@
+from tests.test_helper import *
+from braintree.configuration import Configuration
+
+class TestTransactionGateway(unittest.TestCase):
+
+    def setUp(self):
+        config = Configuration(
+            environment=Environment.Development,
+            merchant_id="integration_merchant_id",
+            public_key="integration_public_key",
+            private_key="integration_private_key"
+        )
+        self.gateway = BraintreeGateway(config)
+
+    def test_credit_with_a_successful_result(self):
+        result = self.gateway.transaction.credit({
+            "amount": Decimal(TransactionAmounts.Authorize),
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
+            }
+        })
+
+        self.assertTrue(result.is_success)
+        transaction = result.transaction
+        self.assertNotEqual(None, re.search(r"\A\w{6,}\Z", transaction.id))
+        self.assertEqual(Transaction.Type.Credit, transaction.type)
+        self.assertEqual(Decimal(TransactionAmounts.Authorize), transaction.amount)
+        cc_details = transaction.credit_card_details
+        self.assertEqual("411111", cc_details.bin)
+        self.assertEqual("1111", cc_details.last_4)
+        self.assertEqual("05/2009", cc_details.expiration_date)
+
+    def test_shared_vault_transaction_with_nonce(self):
+        config = Configuration(
+            merchant_id="integration_merchant_public_id",
+            public_key="oauth_app_partner_user_public_key",
+            private_key="oauth_app_partner_user_private_key",
+            environment=Environment.Development
+        )
+
+        gateway = BraintreeGateway(config)
+        customer = gateway.customer.create({"first_name": "Bob"}).customer
+        address = gateway.address.create({
+            "customer_id": customer.id,
+            "first_name": "Joe",
+        }).address
+
+        credit_card = gateway.credit_card.create(
+            params={
+                "customer_id": customer.id,
+                "number": "4111111111111111",
+                "expiration_date": "05/2009",
+            }
+        ).credit_card
+
+        shared_nonce = gateway.payment_method_nonce.create(
+            credit_card.token
+        ).payment_method_nonce.nonce
+
+        oauth_app_gateway = BraintreeGateway(
+            client_id="client_id$development$integration_client_id",
+            client_secret="client_secret$development$integration_client_secret",
+            environment=Environment.Development
+        )
+        code = TestHelper.create_grant(oauth_app_gateway, {
+            "merchant_public_id": "integration_merchant_id",
+            "scope": "grant_payment_method,shared_vault_transactions"
+        })
+        access_token = oauth_app_gateway.oauth.create_token_from_code({
+            "code": code
+        }).credentials.access_token
+
+        recipient_gateway = BraintreeGateway(access_token=access_token)
+
+        result = recipient_gateway.transaction.sale({
+            "shared_payment_method_nonce": shared_nonce,
+            "shared_customer_id": customer.id,
+            "shared_shipping_address_id": address.id,
+            "shared_billing_address_id": address.id,
+            "amount": "100"
+        })
+
+        self.assertTrue(result.is_success)
+        self.assertEqual(result.transaction.shipping_details.first_name, address.first_name)
+        self.assertEqual(result.transaction.billing_details.first_name, address.first_name)
+        self.assertEqual(result.transaction.customer_details.first_name, customer.first_name)
+
+    def test_shared_vault_transaction_with_token(self):
+        config = Configuration(
+            merchant_id="integration_merchant_public_id",
+            public_key="oauth_app_partner_user_public_key",
+            private_key="oauth_app_partner_user_private_key",
+            environment=Environment.Development
+        )
+
+        gateway = BraintreeGateway(config)
+        customer = gateway.customer.create({"first_name": "Bob"}).customer
+        address = gateway.address.create({
+            "customer_id": customer.id,
+            "first_name": "Joe",
+        }).address
+
+        credit_card = gateway.credit_card.create(
+            params={
+                "customer_id": customer.id,
+                "number": "4111111111111111",
+                "expiration_date": "05/2009",
+            }
+        ).credit_card
+
+        oauth_app_gateway = BraintreeGateway(
+            client_id="client_id$development$integration_client_id",
+            client_secret="client_secret$development$integration_client_secret",
+            environment=Environment.Development
+        )
+        code = TestHelper.create_grant(oauth_app_gateway, {
+            "merchant_public_id": "integration_merchant_id",
+            "scope": "grant_payment_method,shared_vault_transactions"
+        })
+        access_token = oauth_app_gateway.oauth.create_token_from_code({
+            "code": code
+        }).credentials.access_token
+
+        recipient_gateway = BraintreeGateway(
+            access_token=access_token,
+        )
+
+        result = recipient_gateway.transaction.sale({
+            "shared_payment_method_token": credit_card.token,
+            "shared_customer_id": customer.id,
+            "shared_shipping_address_id": address.id,
+            "shared_billing_address_id": address.id,
+            "amount": "100"
+        })
+
+        self.assertTrue(result.is_success)
+        self.assertEqual(result.transaction.shipping_details.first_name, address.first_name)
+        self.assertEqual(result.transaction.billing_details.first_name, address.first_name)
+        self.assertEqual(result.transaction.customer_details.first_name, customer.first_name)
+
+    def test_sale_with_gateway_rejected_with_incomplete_application(self):
+        gateway = BraintreeGateway(
+            client_id="client_id$development$integration_client_id",
+            client_secret="client_secret$development$integration_client_secret",
+            environment=Environment.Development
+        )
+
+        result = gateway.merchant.create({
+            "email": "name@email.com",
+            "country_code_alpha3": "USA",
+            "payment_methods": ["credit_card", "paypal"]
+        })
+
+        gateway = BraintreeGateway(
+            access_token=result.credentials.access_token,
+            environment=Environment.Development
+        )
+
+        result = gateway.transaction.sale({
+            "amount": "4000.00",
+            "billing": {
+                "street_address": "200 Fake Street"
+            },
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
+            }
+        })
+
+        self.assertFalse(result.is_success)
+        transaction = result.transaction
+        self.assertEqual(Transaction.GatewayRejectionReason.ApplicationIncomplete, transaction.gateway_rejection_reason)
+
+    def test_sale_with_apple_pay_params(self):
+        result = self.gateway.transaction.sale({
+            "amount": Decimal(TransactionAmounts.Authorize),
+            "apple_pay_card": {
+                "cardholder_name": "Evelyn Boyd Granville",
+                "cryptogram": "AAAAAAAA/COBt84dnIEcwAA3gAAGhgEDoLABAAhAgAABAAAALnNCLw==",
+                "eci_indicator": "07",
+                "expiration_month": "10",
+                "expiration_year": "14",
+                "number": "370295001292109"
+            }
+        })
+
+        self.assertTrue(result.is_success)
+        self.assertEqual(Transaction.Status.Authorized, result.transaction.status)
+
+    def test_sale_with_google_pay_params(self):
+        result = self.gateway.transaction.sale({
+            "amount": Decimal(TransactionAmounts.Authorize),
+            "android_pay_card": {
+                "cryptogram": "AAAAAAAA/COBt84dnIEcwAA3gAAGhgEDoLABAAhAgAABAAAALnNCLw==",
+                "eci_indicator": "07",
+                "expiration_month": "10",
+                "expiration_year": "14",
+                "google_transaction_id": "12345",
+                "number": "4012888888881881",
+                "source_card_last_four": "1881",
+                "source_card_type": "Visa"
+            }
+        })
+
+        self.assertTrue(result.is_success)
+        self.assertEqual(Transaction.Status.Authorized, result.transaction.status)
+        self.assertEqual("android_pay_card", result.transaction.payment_instrument_type)
+        self.assertEqual("10", result.transaction.android_pay_card_details.expiration_month)
+        self.assertEqual("14", result.transaction.android_pay_card_details.expiration_year)
+        self.assertEqual("12345", result.transaction.android_pay_card_details.google_transaction_id)
+        self.assertEqual("1881", result.transaction.android_pay_card_details.source_card_last_4)
+        self.assertEqual("Visa", result.transaction.android_pay_card_details.source_card_type)
+
+    def test_create_can_set_recurring_flag(self):
+        result = self.gateway.transaction.sale({
+            "amount": "100",
+            "credit_card": {
+                "number": "4111111111111111",
+                "expiration_date": "05/2009"
+                },
+            "recurring": True
+            })
+
+        self.assertTrue(result.is_success)
+        transaction = result.transaction
+        self.assertEqual(True, transaction.recurring)
+
+    def test_create_recurring_flag_sends_deprecation_warning(self):
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            result = self.gateway.transaction.sale({
+                "amount": "100",
+                "credit_card": {
+                    "number": "4111111111111111",
+                    "expiration_date": "05/2009"
+                },
+                "recurring": True
+            })
+
+            self.assertTrue(result.is_success)
+            transaction = result.transaction
+            self.assertEqual(True, transaction.recurring)
+            assert len(w) > 0
+            assert issubclass(w[-1].category, DeprecationWarning)
+            assert "Use transaction_source parameter instead" in str(w[-1].message)
diff --git a/tests/integration/test_transaction_line_item_gateway.py b/tests/integration/test_transaction_line_item_gateway.py
index 5a98393..b011d1e 100644
--- a/tests/integration/test_transaction_line_item_gateway.py
+++ b/tests/integration/test_transaction_line_item_gateway.py
@@ -2,7 +2,7 @@ from tests.test_helper import *
 
 class TestTransactionLineItemGateway(unittest.TestCase):
 
-    @raises(NotFoundError)
     def test_transaction_line_item_gateway_find_all_raises_when_transaction_not_found(self):
-        transaction_id = "willnotbefound"
-        TransactionLineItem.find_all(transaction_id)
+        with self.assertRaises(NotFoundError):
+            transaction_id = "willnotbefound"
+            TransactionLineItem.find_all(transaction_id)
diff --git a/tests/integration/test_transaction_search.py b/tests/integration/test_transaction_search.py
index d4bf414..0a078f9 100644
--- a/tests/integration/test_transaction_search.py
+++ b/tests/integration/test_transaction_search.py
@@ -1,3 +1,4 @@
+from unittest.mock import patch
 from tests.test_helper import *
 
 class TestTransactionSearch(unittest.TestCase):
@@ -273,6 +274,40 @@ class TestTransactionSearch(unittest.TestCase):
         self.assertEqual(transaction.payment_instrument_type, PaymentInstrumentType.LocalPayment)
         self.assertEqual(transaction.id, collection.first.id)
 
+    def test_advanced_search_with_payment_instrument_type_is_sepa_debit_account(self):
+        transaction = Transaction.sale({
+            "amount": TransactionAmounts.Authorize,
+            "options": {
+                "submit_for_settlement": True,
+            },
+            "payment_method_nonce": Nonces.SepaDirectDebit
+        }).transaction
+
+        collection = Transaction.search(
+            TransactionSearch.id == transaction.id,
+            TransactionSearch.payment_instrument_type == "SEPADebitAccount"
+        )
+
+        self.assertEqual(transaction.payment_instrument_type, PaymentInstrumentType.SepaDirectDebitAccount)
+        self.assertEqual(transaction.id, collection.first.id)
+
+    def test_advanced_search_with_sepa_debit_paypal_v2_order_id(self):
+        transaction = Transaction.sale({
+            "amount": TransactionAmounts.Authorize,
+            "options": {
+                "submit_for_settlement": True,
+            },
+            "payment_method_nonce": Nonces.SepaDirectDebit
+        }).transaction
+
+        collection = Transaction.search(
+            TransactionSearch.id == transaction.id,
+            TransactionSearch.sepa_debit_paypal_v2_order_id == transaction.sepa_direct_debit_account_details.paypal_v2_order_id
+        )
+
+        self.assertEqual(transaction.payment_instrument_type, PaymentInstrumentType.SepaDirectDebitAccount)
+        self.assertEqual(transaction.id, collection.first.id)
+
     def test_advanced_search_with_payment_instrument_type_is_apple_pay(self):
         transaction = Transaction.sale({
             "amount": TransactionAmounts.Authorize,
@@ -419,9 +454,9 @@ class TestTransactionSearch(unittest.TestCase):
 
         self.assertEqual(0, collection.maximum_size)
 
-    @raises_with_regexp(AttributeError, "Invalid argument\(s\) for created_using: noSuchCreatedUsing")
     def test_advanced_search_multiple_value_node_allowed_values_created_using(self):
-        Transaction.search([TransactionSearch.created_using == "noSuchCreatedUsing"])
+        with self.assertRaisesRegex(AttributeError, "Invalid argument\(s\) for created_using: noSuchCreatedUsing"):
+            Transaction.search([TransactionSearch.created_using == "noSuchCreatedUsing"])
 
     def test_advanced_search_multiple_value_node_credit_card_customer_location(self):
         transaction = Transaction.sale({
@@ -455,12 +490,12 @@ class TestTransactionSearch(unittest.TestCase):
 
         self.assertEqual(0, collection.maximum_size)
 
-    @raises_with_regexp(AttributeError,
-            "Invalid argument\(s\) for credit_card_customer_location: noSuchCreditCardCustomerLocation")
     def test_advanced_search_multiple_value_node_allowed_values_credit_card_customer_location(self):
-        Transaction.search([
-            TransactionSearch.credit_card_customer_location == "noSuchCreditCardCustomerLocation"
-        ])
+        with self.assertRaisesRegex(AttributeError,
+            "Invalid argument\(s\) for credit_card_customer_location: noSuchCreditCardCustomerLocation"):
+            Transaction.search([
+                TransactionSearch.credit_card_customer_location == "noSuchCreditCardCustomerLocation"
+            ])
 
     def test_advanced_search_multiple_value_node_merchant_account_id(self):
         transaction = Transaction.sale({
@@ -586,12 +621,12 @@ class TestTransactionSearch(unittest.TestCase):
         self.assertEqual(transaction.id, collection.first.id)
         self.assertEqual(transaction.credit_card_details.card_type, collection.first.credit_card_details.card_type)
 
-    @raises_with_regexp(AttributeError,
-            "Invalid argument\(s\) for credit_card_card_type: noSuchCreditCardCardType")
     def test_advanced_search_multiple_value_node_allowed_values_credit_card_card_type(self):
-        Transaction.search([
-            TransactionSearch.credit_card_card_type == "noSuchCreditCardCardType"
-        ])
+        with self.assertRaisesRegex(AttributeError,
+                "Invalid argument\(s\) for credit_card_card_type: noSuchCreditCardCardType"):
+            Transaction.search([
+                TransactionSearch.credit_card_card_type == "noSuchCreditCardCardType"
+            ])
 
     def test_advanced_search_multiple_value_node_status(self):
         transaction = Transaction.sale({
@@ -639,9 +674,18 @@ class TestTransactionSearch(unittest.TestCase):
         ])
         print(collection)
 
-    @raises_with_regexp(AttributeError, "Invalid argument\(s\) for status: noSuchStatus")
+    def test_advanced_search_settlement_confirmed_transaction(self):
+        transaction_id = "settlement_confirmed_txn"
+
+        collection = Transaction.search([
+            TransactionSearch.id == transaction_id
+        ])
+
+        self.assertEqual(1, collection.maximum_size)
+
     def test_advanced_search_multiple_value_node_allowed_values_status(self):
-        Transaction.search([TransactionSearch.status == "noSuchStatus"])
+        with self.assertRaisesRegex(AttributeError, "Invalid argument\(s\) for status: noSuchStatus"):
+            Transaction.search([TransactionSearch.status == "noSuchStatus"])
 
     def test_advanced_search_multiple_value_node_source(self):
         transaction = Transaction.sale({
@@ -707,11 +751,11 @@ class TestTransactionSearch(unittest.TestCase):
 
         self.assertEqual(0, collection.maximum_size)
 
-    @raises_with_regexp(AttributeError, "Invalid argument\(s\) for type: noSuchType")
     def test_advanced_search_multiple_value_node_allowed_values_type(self):
-        Transaction.search([
-            TransactionSearch.type == "noSuchType"
-        ])
+        with self.assertRaisesRegex(AttributeError, "Invalid argument\(s\) for type: noSuchType"):
+            Transaction.search([
+                TransactionSearch.type == "noSuchType"
+            ])
 
     def test_advanced_search_multiple_value_node_type_with_refund(self):
         name = "Anabel Atkins%s" % random.randint(1, 100000)
@@ -1004,7 +1048,6 @@ class TestTransactionSearch(unittest.TestCase):
 
         collection = Transaction.search([
             TransactionSearch.id == transaction_id,
-            TransactionSearch.disbursement_date <= disbursement_time
         ])
 
         self.assertEqual(1, collection.maximum_size)
@@ -1012,7 +1055,6 @@ class TestTransactionSearch(unittest.TestCase):
 
         collection = Transaction.search([
             TransactionSearch.id == transaction_id,
-            TransactionSearch.disbursement_date <= future
         ])
 
         self.assertEqual(1, collection.maximum_size)
@@ -1574,8 +1616,80 @@ class TestTransactionSearch(unittest.TestCase):
         self.assertEqual(1, collection.maximum_size)
         self.assertEqual(transaction.id, collection.first.id)
 
-    @raises(DownForMaintenanceError)
+    def test_advanced_search_can_search_on_store_id_1(self):
+        transaction_id = "contact_visa_transaction"
+
+        collection = Transaction.search([
+            TransactionSearch.id == transaction_id,
+            TransactionSearch.store_id == "store-id"
+        ])
+
+        self.assertEqual(1, collection.maximum_size)
+
+        collection = Transaction.search([
+            TransactionSearch.id == transaction_id,
+            TransactionSearch.store_id == "invalid-store-id"
+        ])
+
+        self.assertEqual(0, collection.maximum_size)
+
+    def test_advanced_search_can_search_on_store_ids(self):
+        transaction_id = "contact_visa_transaction"
+
+        collection = Transaction.search([
+            TransactionSearch.id == transaction_id,
+            TransactionSearch.store_ids.in_list(["store-id"])
+        ])
+
+        self.assertEqual(1, collection.maximum_size)
+
+        collection = Transaction.search([
+            TransactionSearch.id == transaction_id,
+            TransactionSearch.store_ids.in_list(["invalid-store-id"])
+        ])
+
+        self.assertEqual(0, collection.maximum_size)
+
     def test_search_handles_a_search_timeout(self):
-        Transaction.search([
-            TransactionSearch.amount.between("-1100", "1600")
+        with self.assertRaises(RequestTimeoutError):
+            Transaction.search([
+                TransactionSearch.amount.between("-1100", "1600")
+            ])
+
+    def test_search_returns_records_from_valid_daterange(self):
+        yesterday = datetime.now() - timedelta(days=1)
+        tomorrow = datetime.now() + timedelta(days=1)
+
+        collection = Transaction.search([
+            TransactionSearch.ach_return_responses_created_at.between(yesterday, tomorrow)
+        ])
+        self.assertEqual(2, collection.maximum_size)
+
+    def test_search_returns_records_from_invalid_daterange(self):
+        day_after_tomorrow = datetime.now() + timedelta(days=1)
+        tomorrow = datetime.now() + timedelta(days=2)
+
+        collection = Transaction.search([
+            TransactionSearch.ach_return_responses_created_at.between(tomorrow, day_after_tomorrow)
+        ])
+        self.assertEqual(0, collection.maximum_size)
+
+    def test_search_returns_records_for_one_reasoncode(self):
+        collection = Transaction.search([
+            TransactionSearch.reason_code.in_list(['R01'])
         ])
+        self.assertEqual(1, collection.maximum_size)
+
+    def test_search_returns_records_for_multiple_reasoncode(self):
+        collection = Transaction.search([
+            TransactionSearch.reason_code.in_list(['R01', 'R02'])
+        ])
+        self.assertEqual(2, collection.maximum_size)
+
+    def test_search_returns_multiple_records_for_any_reason_code(self):
+        collection = Transaction.search([
+            TransactionSearch.reason_code == Transaction.ReasonCode.ANY_REASON_CODE
+        ])
+        self.assertEqual(2, collection.maximum_size)
+
+
diff --git a/tests/integration/test_transaction_with_us_bank_account.py b/tests/integration/test_transaction_with_us_bank_account.py
index 571b66c..a73b80b 100644
--- a/tests/integration/test_transaction_with_us_bank_account.py
+++ b/tests/integration/test_transaction_with_us_bank_account.py
@@ -77,6 +77,7 @@ class TestTransactionWithUsBankAccount(unittest.TestCase):
         error_code = result.errors.for_object("transaction").on("payment_method_nonce")[0].code
         self.assertEqual(error_code, ErrorCodes.Transaction.PaymentMethodNonceUnknown)
 
+    @unittest.skip("until we have a more stable ci env")
     def test_verification_create_with_plaid_nonce(self):
         result = Transaction.sale({
             "amount": TransactionAmounts.Authorize,
diff --git a/tests/integration/test_transparent_redirect.py b/tests/integration/test_transparent_redirect.py
deleted file mode 100644
index e336d07..0000000
--- a/tests/integration/test_transparent_redirect.py
+++ /dev/null
@@ -1,186 +0,0 @@
-from tests.test_helper import *
-
-class TestTransparentRedirect(unittest.TestCase):
-    @raises(DownForMaintenanceError)
-    def test_parse_and_validate_query_string_checks_http_status_before_hash(self):
-        customer = Customer.create().customer
-        tr_data = {
-            "credit_card": {
-                "customer_id": customer.id
-            }
-        }
-        post_params = {
-            "tr_data": CreditCard.tr_data_for_create(tr_data, "http://example.com/path?foo=bar"),
-            "credit_card[cardholder_name]": "Card Holder",
-            "credit_card[number]": "4111111111111111",
-            "credit_card[expiration_date]": "05/2012",
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params, Configuration.instantiate().base_merchant_path() + "/test/maintenance")
-        CreditCard.confirm_transparent_redirect(query_string)
-
-    @raises(AuthenticationError)
-    def test_parse_and_validate_query_string_raises_authentication_error_with_bad_credentials(self):
-        customer = Customer.create().customer
-        tr_data = {
-            "credit_card": {
-                "customer_id": customer.id
-            }
-        }
-
-        old_private_key = Configuration.private_key
-        try:
-            Configuration.private_key = "bad"
-
-            post_params = {
-                "tr_data": CreditCard.tr_data_for_create(tr_data, "http://example.com/path?foo=bar"),
-                "credit_card[cardholder_name]": "Card Holder",
-                "credit_card[number]": "4111111111111111",
-                "credit_card[expiration_date]": "05/2012",
-            }
-            query_string = TestHelper.simulate_tr_form_post(post_params, CreditCard.transparent_redirect_create_url())
-            CreditCard.confirm_transparent_redirect(query_string)
-        finally:
-            Configuration.private_key = old_private_key
-
-    def test_transaction_sale_from_transparent_redirect_with_successful_result(self):
-        tr_data = {
-            "transaction": {
-                "amount": TransactionAmounts.Authorize,
-            }
-        }
-        post_params = {
-            "tr_data": Transaction.tr_data_for_sale(tr_data, "http://example.com/path"),
-            "transaction[credit_card][number]": "4111111111111111",
-            "transaction[credit_card][expiration_date]": "05/2010",
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params)
-        result = TransparentRedirect.confirm(query_string)
-        self.assertTrue(result.is_success)
-
-        transaction = result.transaction
-        self.assertEqual(Decimal(TransactionAmounts.Authorize), transaction.amount)
-        self.assertEqual(Transaction.Type.Sale, transaction.type)
-        self.assertEqual("411111", transaction.credit_card_details.bin)
-        self.assertEqual("1111", transaction.credit_card_details.last_4)
-        self.assertEqual("05/2010", transaction.credit_card_details.expiration_date)
-
-    def test_transaction_credit_from_transparent_redirect_with_successful_result(self):
-        tr_data = {
-            "transaction": {
-                "amount": TransactionAmounts.Authorize,
-            }
-        }
-        post_params = {
-            "tr_data": Transaction.tr_data_for_credit(tr_data, "http://example.com/path"),
-            "transaction[credit_card][number]": "4111111111111111",
-            "transaction[credit_card][expiration_date]": "05/2010",
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params)
-        result = TransparentRedirect.confirm(query_string)
-        self.assertTrue(result.is_success)
-
-        transaction = result.transaction
-        self.assertEqual(Decimal(TransactionAmounts.Authorize), transaction.amount)
-        self.assertEqual(Transaction.Type.Credit, transaction.type)
-        self.assertEqual("411111", transaction.credit_card_details.bin)
-        self.assertEqual("1111", transaction.credit_card_details.last_4)
-        self.assertEqual("05/2010", transaction.credit_card_details.expiration_date)
-
-    def test_customer_create_from_transparent_redirect(self):
-        tr_data = {
-            "customer": {
-                "first_name": "John",
-                "last_name": "Doe",
-                "company": "Doe Co",
-            }
-        }
-        post_params = {
-            "tr_data": Customer.tr_data_for_create(tr_data, "http://example.com/path"),
-            "customer[email]": "john@doe.com",
-            "customer[phone]": "312.555.2323",
-            "customer[fax]": "614.555.5656",
-            "customer[website]": "www.johndoe.com"
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params)
-        result = TransparentRedirect.confirm(query_string)
-        self.assertTrue(result.is_success)
-        customer = result.customer
-        self.assertEqual("John", customer.first_name)
-        self.assertEqual("Doe", customer.last_name)
-        self.assertEqual("Doe Co", customer.company)
-        self.assertEqual("john@doe.com", customer.email)
-        self.assertEqual("312.555.2323", customer.phone)
-        self.assertEqual("614.555.5656", customer.fax)
-        self.assertEqual("www.johndoe.com", customer.website)
-
-    def test_customer_update_from_transparent_redirect(self):
-        customer = Customer.create({"first_name": "Sarah", "last_name": "Humphrey"}).customer
-
-        tr_data = {
-            "customer_id": customer.id,
-            "customer": {
-                "first_name": "Stan",
-            }
-        }
-        post_params = {
-            "tr_data": Customer.tr_data_for_update(tr_data, "http://example.com/path"),
-            "customer[last_name]": "Humphrey",
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params)
-        result = TransparentRedirect.confirm(query_string)
-        self.assertTrue(result.is_success)
-
-        customer = Customer.find(customer.id)
-        self.assertEqual("Stan", customer.first_name)
-        self.assertEqual("Humphrey", customer.last_name)
-
-    def test_payment_method_create_from_transparent_redirect(self):
-        customer = Customer.create({"first_name": "Sarah", "last_name": "Humphrey"}).customer
-        tr_data = {
-            "credit_card": {
-                "customer_id": customer.id,
-                "number": "4111111111111111",
-            }
-        }
-        post_params = {
-            "tr_data": CreditCard.tr_data_for_create(tr_data, "http://example.com/path"),
-            "credit_card[expiration_month]": "01",
-            "credit_card[expiration_year]": "10"
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params)
-        result = TransparentRedirect.confirm(query_string)
-        self.assertTrue(result.is_success)
-        credit_card = result.credit_card
-        self.assertEqual("411111", credit_card.bin)
-        self.assertEqual("1111", credit_card.last_4)
-        self.assertEqual("01/2010", credit_card.expiration_date)
-
-    def test_payment_method_update_from_transparent_redirect(self):
-        customer = Customer.create({"first_name": "Sarah", "last_name": "Humphrey"}).customer
-        credit_card = CreditCard.create({
-            "customer_id": customer.id,
-            "number": "4111111111111111",
-            "expiration_date": "10/10"
-        }).credit_card
-
-        tr_data = {
-            "payment_method_token": credit_card.token,
-            "credit_card": {
-                "expiration_date": "12/12"
-            }
-        }
-        post_params = {
-            "tr_data": CreditCard.tr_data_for_update(tr_data, "http://example.com/path"),
-        }
-
-        query_string = TestHelper.simulate_tr_form_post(post_params)
-        TransparentRedirect.confirm(query_string)
-        credit_card = CreditCard.find(credit_card.token)
-
-        self.assertEqual("12/2012", credit_card.expiration_date)
diff --git a/tests/test_helper.py b/tests/test_helper.py
index 05a4b99..4e1dee2 100644
--- a/tests/test_helper.py
+++ b/tests/test_helper.py
@@ -1,31 +1,21 @@
+from base64 import b64decode, encodebytes
+from contextlib import contextmanager
+from datetime import date, datetime, timedelta
+from decimal import Decimal
+from enum import Enum
+from http.client import HTTPConnection
+from subprocess import Popen, PIPE
+from urllib.parse import urlencode, quote_plus
 import json
 import os
-import re
 import random
+import re
+import requests
+import subprocess
 import sys
+import time
 import unittest
 import warnings
-import subprocess
-import time
-
-if sys.version_info[0] == 2:
-    from urllib import urlencode, quote_plus
-    from httplib import HTTPConnection
-    from base64 import encodestring as encodebytes
-else:
-    from urllib.parse import urlencode, quote_plus
-    from http.client import HTTPConnection
-    from base64 import encodebytes
-import requests
-
-from base64 import b64decode
-from contextlib import contextmanager
-from datetime import date, datetime, timedelta
-from decimal import Decimal
-from subprocess import Popen, PIPE
-
-from nose.tools import make_decorator
-from nose.tools import raises
 
 from braintree import *
 from braintree.exceptions import *
@@ -34,28 +24,6 @@ from braintree.test.nonces import Nonces
 from braintree.testing_gateway import *
 from braintree.util import *
 
-def raises_with_regexp(expected_exception_class, regexp_to_match):
-    def decorate(func):
-        name = func.__name__
-        def generated_function(*args, **kwargs):
-            exception_string = None
-            try:
-                func(*args, **kwargs)
-            except expected_exception_class as e:
-                exception_string = str(e)
-            except:
-                raise
-
-            if exception_string is None:
-                message = "%s() did not raise %s" % (name, expected_exception_class.__name__)
-                raise AssertionError(message)
-            elif re.match(regexp_to_match, exception_string) is None:
-                message = "%s() exception message (%s) did not match (%s)" % \
-                    (name, exception_string, regexp_to_match)
-                raise AssertionError(message)
-        return make_decorator(func)(generated_function)
-    return decorate
-
 def reset_braintree_configuration():
     Configuration.configure(
         Environment.Development,
@@ -65,7 +33,7 @@ def reset_braintree_configuration():
     )
 reset_braintree_configuration()
 
-class AdvancedFraudIntegrationMerchant:
+class AdvancedFraudKountIntegrationMerchant:
     def __enter__(self):
         Configuration.configure(
             Environment.Development,
@@ -77,6 +45,42 @@ class AdvancedFraudIntegrationMerchant:
     def __exit__(self, type, value, trace):
         reset_braintree_configuration()
 
+class FraudProtectionEnterpriseIntegrationMerchant:
+    def __enter__(self):
+        Configuration.configure(
+            Environment.Development,
+            "fraud_protection_enterprise_integration_merchant_id",
+            "fraud_protection_enterprise_integration_public_key",
+            "fraud_protection_enterprise_integration_private_key"
+        )
+
+    def __exit__(self, type, value, trace):
+        reset_braintree_configuration()
+
+class EffortlessChargebackProtectionMerchant:
+    def __enter__(self):
+        Configuration.configure(
+            Environment.Development,
+            "fraud_protection_effortless_chargeback_protection_merchant_id",
+            "effortless_chargeback_protection_public_key",
+            "effortless_chargeback_protection_private_key"
+        )
+
+    def __exit__(self, type, value, trace):
+        reset_braintree_configuration()
+
+class DuplicateCheckingMerchant:
+    def __enter__(self):
+        Configuration.configure(
+            Environment.Development,
+            "dup_checking_integration_merchant_id",
+            "dup_checking_integration_public_key",
+            "dup_checking_integration_private_key"
+        )
+
+    def __exit__(self, type, value, trace):
+        reset_braintree_configuration()
+
 def showwarning(*_):
     pass
 warnings.showwarning = showwarning
@@ -88,10 +92,13 @@ class TestHelper(object):
     three_d_secure_merchant_account_id = "three_d_secure_merchant_account"
     fake_amex_direct_merchant_account_id = "fake_amex_direct_usd"
     fake_venmo_account_merchant_account_id = "fake_first_data_venmo_account"
+    fake_first_data_merchant_account_id = "fake_first_data_merchant_account"
     us_bank_merchant_account_id = "us_bank_merchant_account"
     another_us_bank_merchant_account_id = "another_us_bank_merchant_account"
     adyen_merchant_account_id = "adyen_ma"
     hiper_brl_merchant_account_id = "hiper_brl"
+    card_processor_brl_merchant_account_id = "card_processor_brl"
+    aib_swe_ma_merchant_account_id = "aib_swe_ma"
 
     add_on_discount_plan = {
          "description": "Plan for integration tests -- with add-ons and discounts",
@@ -126,8 +133,8 @@ class TestHelper(object):
     }
 
     valid_token_characters = list("bcdfghjkmnpqrstvwxyz23456789")
-    text_type = unicode if sys.version_info[0] == 2 else str
-    raw_type = str if sys.version_info[0] == 2 else bytes
+    text_type = str
+    raw_type = bytes
 
     @staticmethod
     def make_past_due(subscription, number_of_days_past_due=1):
@@ -153,16 +160,6 @@ class TestHelper(object):
     def settlement_pending_transaction(transaction_id):
         return Configuration.gateway().testing.settlement_pending_transaction(transaction_id)
 
-    @staticmethod
-    def simulate_tr_form_post(post_params, url=TransparentRedirect.url()):
-        form_data = urlencode(post_params)
-        conn = HTTPConnection(Configuration.environment.server_and_port)
-        conn.request("POST", url, form_data, TestHelper.__headers())
-        response = conn.getresponse()
-        query_string = response.getheader("location").split("?", 1)[1]
-        conn.close()
-        return query_string
-
     @staticmethod
     def create_3ds_verification(merchant_account_id, params):
         return Configuration.gateway().testing.create_3ds_verification(merchant_account_id, params)
@@ -252,61 +249,84 @@ class TestHelper(object):
 
     @staticmethod
     def generate_valid_us_bank_account_nonce(routing_number="021000021", account_number="567891234"):
-        client_token = json.loads(TestHelper.generate_decoded_client_token())
-        headers = {
-            "Content-Type": "application/json",
-            "Braintree-Version": "2016-10-07",
-            "Authorization": "Bearer " + client_token["braintree_api"]["access_token"]
-        }
-        payload = {
-            "type": "us_bank_account",
-            "billing_address": {
-                "street_address": "123 Ave",
-                "region": "CA",
-                "locality": "San Francisco",
-                "postal_code": "94112"
-            },
-            "account_type": "checking",
-            "ownership_type": "personal",
-            "routing_number": routing_number,
-            "account_number": account_number,
-            "first_name": "Dan",
-            "last_name": "Schulman",
-            "ach_mandate": {
-                "text": "cl mandate text"
+        query = '''
+          mutation TokenizeUsBankAccount($input: TokenizeUsBankAccountInput!) {
+            tokenizeUsBankAccount(input: $input) {
+              paymentMethod {
+                id
+              }
             }
+          }
+        '''
+
+        variables = {
+            "input": {
+                "usBankAccount": {
+                    "accountNumber": account_number,
+                    "routingNumber": routing_number,
+                    "accountType": "CHECKING",
+                    "individualOwner": {
+                        "firstName": "Dan",
+                        "lastName": "Schulman"
+                     },
+                    "achMandate": "cl mandate text",
+                    "billingAddress": {
+                        "streetAddress": "123 Ave",
+                        "state": "CA",
+                        "city": "San Francisco",
+                        "zipCode": "94112"
+                     }
+                }
+            }
+        }
+
+        graphql_request = {
+            "query": query,
+            "variables": variables
         }
-        resp = requests.post(client_token["braintree_api"]["url"] + "/tokens", headers=headers, data=json.dumps(payload) )
-        respJson = json.loads(resp.text)
-        return respJson["data"]["id"]
+
+        response = TestHelper.__send_graphql_request(graphql_request)
+        return response["data"]["tokenizeUsBankAccount"]["paymentMethod"]["id"]
 
     @staticmethod
     def generate_plaid_us_bank_account_nonce():
-        client_token = json.loads(TestHelper.generate_decoded_client_token())
-        headers = {
-            "Content-Type": "application/json",
-            "Braintree-Version": "2016-10-07",
-            "Authorization": "Bearer " + client_token["braintree_api"]["access_token"]
-        }
-        payload = {
-            "type": "plaid_public_token",
-            "public_token": "good",
-            "account_id": "plaid_account_id",
-            "ownership_type": "business",
-            "business_name": "PayPal, Inc.",
-            "billing_address": {
-                "street_address": "123 Ave",
-                "region": "CA",
-                "locality": "San Francisco",
-                "postal_code": "94112"
-            },
-            "ach_mandate": {
-                "text": "cl mandate text"
+        query = '''
+          mutation TokenizeUsBankLogin($input: TokenizeUsBankLoginInput!) {
+            tokenizeUsBankLogin(input: $input) {
+              paymentMethod {
+                id
+              }
             }
+          }
+        '''
+
+        variables = {
+            "input": {
+                "usBankLogin": {
+                    "publicToken": "good",
+                    "accountId": "plaid_account_id",
+                    "accountType": "CHECKING",
+                    "businessOwner": {
+                        "businessName": "PayPal, Inc."
+                     },
+                    "achMandate": "cl mandate text",
+                    "billingAddress": {
+                        "streetAddress": "123 Ave",
+                        "state": "CA",
+                        "city": "San Francisco",
+                        "zipCode": "94112"
+                     }
+                }
+            }
+        }
+
+        graphql_request = {
+            "query": query,
+            "variables": variables
         }
-        resp = requests.post(client_token["braintree_api"]["url"] + "/tokens", headers=headers, data=json.dumps(payload) )
-        respJson = json.loads(resp.text)
-        return respJson["data"]["id"]
+
+        response = TestHelper.__send_graphql_request(graphql_request)
+        return response["data"]["tokenizeUsBankLogin"]["paymentMethod"]["id"]
 
     @staticmethod
     def generate_invalid_us_bank_account_nonce():
@@ -316,33 +336,6 @@ class TestHelper(object):
         token += "_xxx"
         return token
 
-    @staticmethod
-    def generate_valid_ideal_payment_id(amount=TransactionAmounts.Authorize):
-        client_token = json.loads(TestHelper.generate_decoded_client_token({
-            "merchant_account_id": "ideal_merchant_account"
-        }))
-        client = ClientApiHttp(Configuration.instantiate(), {
-            "authorization_fingerprint": client_token["authorizationFingerprint"]
-        })
-        _, configuration = client.get_configuration()
-        route_id = json.loads(configuration)["ideal"]["routeId"]
-        headers = {
-            "Content-Type": "application/json",
-            "Braintree-Version": "2015-11-01",
-            "Authorization": "Bearer " + client_token["braintree_api"]["access_token"]
-        }
-        payload = {
-            "issuer": "RABONL2U",
-            "order_id": "ABC123",
-            "amount": amount,
-            "currency": "EUR",
-            "route_id": route_id,
-            "redirect_url": "https://braintree-api.com",
-        }
-        resp = requests.post(client_token["braintree_api"]["url"] + "/ideal-payments", headers=headers, data=json.dumps(payload) )
-        respJson = json.loads(resp.text)
-        return respJson["data"]["id"]
-
     @staticmethod
     def generate_three_d_secure_nonce(gateway, params):
         url = gateway.config.base_merchant_path() + "/three_d_secure/create_nonce/" + TestHelper.three_d_secure_merchant_account_id
@@ -436,6 +429,17 @@ class TestHelper(object):
         signature = "%s|%s" % (gateway.config.public_key, hmac_payload)
         return {'bt_signature': signature, 'bt_payload': payload}
 
+    @staticmethod
+    def __send_graphql_request(graphql_request):
+        client_token = json.loads(TestHelper.generate_decoded_client_token())
+        headers = {
+            "Content-Type": "application/json",
+            "Braintree-Version": "2016-10-07",
+            "Authorization": "Bearer " + client_token["braintree_api"]["access_token"]
+        }
+        resp = requests.post(client_token["braintree_api"]["url"] + "/graphql", headers=headers, data=json.dumps(graphql_request))
+        return json.loads(resp.text)
+
 class ClientApiHttp(Http):
     def __init__(self, config, options):
         self.config = config
@@ -515,7 +519,6 @@ class ClientApiHttp(Http):
 
         return [status_code, nonce]
 
-
     def get_credit_card_nonce(self, credit_card_params):
         url = "/merchants/%s/client_api/v1/payment_methods/credit_cards" % self.config.merchant_id
         params = {"credit_card": credit_card_params}
@@ -533,6 +536,9 @@ class ClientApiHttp(Http):
     def __headers(self):
         return {
             "Content-type": "application/json",
-            "User-Agent": "Braintree Python " + version.Version,
+            "User-Agent": "Braintree Python " + version.Version, #pylint: disable=E0602
             "X-ApiVersion": Configuration.api_version()
         }
+
+class ExpirationHelper(Enum):
+    ADYEN = "03/2030"
diff --git a/tests/unit/test_address.py b/tests/unit/test_address.py
index ebcd6a3..93623cc 100644
--- a/tests/unit/test_address.py
+++ b/tests/unit/test_address.py
@@ -1,30 +1,58 @@
 from tests.test_helper import *
 
 class TestAddress(unittest.TestCase):
-    @raises_with_regexp(KeyError, "'Invalid keys: bad_key'")
     def test_create_raise_exception_with_bad_keys(self):
-        Address.create({"customer_id": "12345", "bad_key": "value"})
+        with self.assertRaisesRegex(KeyError, "'Invalid keys: bad_key'"):
+            Address.create({"customer_id": "12345", "bad_key": "value"})
 
-    @raises_with_regexp(KeyError, "'customer_id must be provided'")
     def test_create_raises_error_if_no_customer_id_given(self):
-        Address.create({"country_name": "United States of America"})
+        with self.assertRaisesRegex(KeyError, "'customer_id must be provided'"):
+            Address.create({"country_name": "United States of America"})
 
-    @raises_with_regexp(KeyError, "'customer_id contains invalid characters'")
     def test_create_raises_key_error_if_given_invalid_customer_id(self):
-        Address.create({"customer_id": "!@#$%"})
+        with self.assertRaisesRegex(KeyError, "'customer_id contains invalid characters'"):
+            Address.create({"customer_id": "!@#$%"})
 
-    @raises_with_regexp(KeyError, "'Invalid keys: bad_key'")
     def test_update_raise_exception_with_bad_keys(self):
-        Address.update("customer_id", "address_id", {"bad_key": "value"})
+        with self.assertRaisesRegex(KeyError, "'Invalid keys: bad_key'"):
+            Address.update("customer_id", "address_id", {"bad_key": "value"})
+
+    def test_update_raises_key_error_if_given_invalid_customer_id(self):
+        with self.assertRaisesRegex(KeyError, "'customer_id contains invalid characters'"):
+            Address.update("!@#$%", "foo")
+
+    def test_update_raises_key_error_if_given_invalid_address_id(self):
+        with self.assertRaisesRegex(KeyError, "'address_id contains invalid characters'"):
+            Address.update("foo", "!@#$%")
+
+    def test_delete_raises_key_error_if_given_invalid_customer_id(self):
+        with self.assertRaisesRegex(KeyError, "'customer_id contains invalid characters'"):
+            Address.delete("!@#$%", "foo")
+
+    def test_delete_raises_key_error_if_given_invalid_address_id(self):
+        with self.assertRaisesRegex(KeyError, "'address_id contains invalid characters'"):
+            Address.delete("foo", "!@#$%")
 
-    @raises(NotFoundError)
     def test_finding_address_with_empty_customer_id_raises_not_found_exception(self):
-        Address.find(" ", "address_id")
+        with self.assertRaises(NotFoundError):
+            Address.find(" ", "address_id")
 
-    @raises(NotFoundError)
     def test_finding_address_with_none_customer_id_raises_not_found_exception(self):
-        Address.find(None, "address_id")
+        with self.assertRaises(NotFoundError):
+            Address.find(None, "address_id")
 
-    @raises(NotFoundError)
     def test_finding_address_with_empty_address_id_raises_not_found_exception(self):
-        Address.find("customer_id", " ")
+        with self.assertRaises(NotFoundError):
+            Address.find("customer_id", " ")
+
+    def test_finding_address_with_none_address_id_raises_not_found_exception(self):
+        with self.assertRaises(NotFoundError):
+            Address.find("customer_id", None)
+
+    def test_find_raises_key_error_if_given_invalid_customer_id(self):
+        with self.assertRaisesRegex(KeyError, "'customer_id contains invalid characters'"):
+            Address.find("!@#$%", "foo")
+
+    def test_find_raises_key_error_if_given_invalid_address_id(self):
+        with self.assertRaisesRegex(KeyError, "'address_id contains invalid characters'"):
+            Address.find("foo", "!@#$%")
diff --git a/tests/unit/test_apple_pay_gateway.py b/tests/unit/test_apple_pay_gateway.py
new file mode 100644
index 0000000..a89b8bd
--- /dev/null
+++ b/tests/unit/test_apple_pay_gateway.py
@@ -0,0 +1,27 @@
+from tests.test_helper import *
+from braintree.apple_pay_gateway import ApplePayGateway
+from unittest.mock import MagicMock
+
+class TestApplePayGateway(unittest.TestCase):
+    @staticmethod
+    def setup_apple_pay_gateway_and_mock_http():
+        braintree_gateway = BraintreeGateway(Configuration.instantiate())
+        apple_pay_gateway = ApplePayGateway(braintree_gateway)
+        http_mock = MagicMock(name='config.http')
+        braintree_gateway.config.http = http_mock
+        return apple_pay_gateway, http_mock
+
+    def test_registered_domains(self):
+        apple_pay_gateway, http_mock = self.setup_apple_pay_gateway_and_mock_http()
+        apple_pay_gateway.registered_domains()
+        self.assertTrue("get('/merchants/integration_merchant_id/processing/apple_pay/registered_domains')" in str(http_mock.mock_calls))
+
+    def test_register_domain(self):
+        apple_pay_gateway, http_mock = self.setup_apple_pay_gateway_and_mock_http()
+        apple_pay_gateway.register_domain('test.example.com')
+        self.assertTrue("post('/merchants/integration_merchant_id/processing/apple_pay/validate_domains', {'url': 'test.example.com'})" in str(http_mock.mock_calls))
+
+    def test_unregister_domain(self):
+        apple_pay_gateway, http_mock = self.setup_apple_pay_gateway_and_mock_http()
+        apple_pay_gateway.unregister_domain('test.example.com')
+        self.assertTrue("delete('/merchants/integration_merchant_id/processing/apple_pay/unregister_domain?url=test.example.com')" in str(http_mock.mock_calls))
diff --git a/tests/unit/test_credit_card.py b/tests/unit/test_credit_card.py
index a6ff4f5..0340957 100644
--- a/tests/unit/test_credit_card.py
+++ b/tests/unit/test_credit_card.py
@@ -2,62 +2,50 @@ from tests.test_helper import *
 import datetime
 
 class TestCreditCard(unittest.TestCase):
-    @raises_with_regexp(KeyError, "'Invalid keys: bad_key'")
     def test_create_raises_exception_with_bad_keys(self):
-        CreditCard.create({"bad_key": "value"})
+        with self.assertRaisesRegex(KeyError, "'Invalid keys: bad_key'"):
+            CreditCard.create({"bad_key": "value"})
 
-    @raises_with_regexp(KeyError, "'Invalid keys: bad_key'")
     def test_update_raises_exception_with_bad_keys(self):
-        CreditCard.update("token", {"bad_key": "value"})
-
-    @raises_with_regexp(KeyError, "'Invalid keys: bad_key'")
-    def test_tr_data_for_create_raises_error_with_bad_keys(self):
-        CreditCard.tr_data_for_create({"bad_key": "value"}, "http://example.com")
-
-    @raises_with_regexp(KeyError, "'Invalid keys: bad_key'")
-    def test_tr_data_for_update_raises_error_with_bad_keys(self):
-        CreditCard.tr_data_for_update({"bad_key": "value"}, "http://example.com")
-
-    def test_transparent_redirect_create_url(self):
-        port = os.getenv("GATEWAY_PORT") or "3000"
-        self.assertEqual(
-            "http://localhost:" + port + "/merchants/integration_merchant_id/payment_methods/all/create_via_transparent_redirect_request",
-            CreditCard.transparent_redirect_create_url()
-        )
-
-    def test_transparent_redirect_update_url(self):
-        port = os.getenv("GATEWAY_PORT") or "3000"
-        self.assertEqual(
-            "http://localhost:" + port + "/merchants/integration_merchant_id/payment_methods/all/update_via_transparent_redirect_request",
-            CreditCard.transparent_redirect_update_url()
-        )
-
-    @raises(DownForMaintenanceError)
-    def test_confirm_transaprant_redirect_raises_error_given_503_status_in_query_string(self):
-        CreditCard.confirm_transparent_redirect(
-            "http_status=503&id=6kdj469tw7yck32j&hash=1b3d29199a282e63074a7823b76bccacdf732da6"
-        )
+        with self.assertRaisesRegex(KeyError, "'Invalid keys: bad_key'"):
+            CreditCard.update("token", {"bad_key": "value"})
 
     def test_create_signature(self):
         expected = ["billing_address_id", "cardholder_name", "cvv", "expiration_date", "expiration_month",
-            "expiration_year", "device_session_id", "fraud_merchant_id", "number", "token", "venmo_sdk_payment_method_code",
+            "expiration_year", "number", "token", "venmo_sdk_payment_method_code",
             "device_data", "payment_method_nonce",
+            "device_session_id", "fraud_merchant_id",
             {
                 "billing_address": [
                     "company", "country_code_alpha2", "country_code_alpha3", "country_code_numeric", "country_name",
                     "extended_address", "first_name", "last_name", "locality", "postal_code", "region", "street_address"
                 ]
             },
-            {"options": ["make_default", "verification_merchant_account_id", "verify_card", "verification_amount", "verification_account_type", "venmo_sdk_session", "fail_on_duplicate_payment_method", {"adyen":["overwrite_brand", "selected_brand"]}
+            {"options": [
+                "fail_on_duplicate_payment_method",
+                "make_default",
+                "skip_advanced_fraud_checking",
+                "venmo_sdk_session",
+                "verification_account_type",
+                "verification_amount",
+                "verification_merchant_account_id",
+                "verify_card",
+                {"adyen":["overwrite_brand", "selected_brand"]}
             ]},
+            {
+                "three_d_secure_pass_thru": [
+                    "cavv", "ds_transaction_id", "eci_flag", "three_d_secure_version", "xid"
+                ]
+            },
             "customer_id"
         ]
         self.assertEqual(expected, CreditCard.create_signature())
 
     def test_update_signature(self):
         expected = ["billing_address_id", "cardholder_name", "cvv", "expiration_date", "expiration_month",
-            "expiration_year", "device_session_id", "fraud_merchant_id", "number", "token", "venmo_sdk_payment_method_code",
+            "expiration_year", "number", "token", "venmo_sdk_payment_method_code",
             "device_data", "payment_method_nonce",
+            "device_session_id", "fraud_merchant_id",
             {
                 "billing_address": [
                     "company", "country_code_alpha2", "country_code_alpha3", "country_code_numeric", "country_name",
@@ -65,26 +53,40 @@ class TestCreditCard(unittest.TestCase):
                     {"options": ["update_existing"]}
                 ]
             },
-            {"options": ["make_default", "verification_merchant_account_id", "verify_card", "verification_amount", "verification_account_type", "venmo_sdk_session", "fail_on_duplicate_payment_method", {"adyen":["overwrite_brand", "selected_brand"]}
-            ]}
+            {"options": [
+                "fail_on_duplicate_payment_method",
+                "make_default",
+                "skip_advanced_fraud_checking",
+                "venmo_sdk_session",
+                "verification_account_type",
+                "verification_amount",
+                "verification_merchant_account_id",
+                "verify_card",
+                {"adyen":["overwrite_brand", "selected_brand"]}
+            ]},
+            {
+                "three_d_secure_pass_thru": [
+                    "cavv", "ds_transaction_id", "eci_flag", "three_d_secure_version", "xid"
+                ]
+            },
         ]
         self.assertEqual(expected, CreditCard.update_signature())
 
-    @raises(NotFoundError)
     def test_finding_empty_id_raises_not_found_exception(self):
-        CreditCard.find(" ")
+        with self.assertRaises(NotFoundError):
+            CreditCard.find(" ")
 
-    @raises(NotFoundError)
     def test_finding_none_raises_not_found_exception(self):
-        CreditCard.find(None)
+        with self.assertRaises(NotFoundError):
+            CreditCard.find(None)
 
-    @raises(NotFoundError)
     def test_from_nonce_empty_id_raises_not_found_exception(self):
-        CreditCard.from_nonce(" ")
+        with self.assertRaises(NotFoundError):
+            CreditCard.from_nonce(" ")
 
-    @raises(NotFoundError)
     def test_from_nonce_none_raises_not_found_exception(self):
-        CreditCard.from_nonce(None)
+        with self.assertRaises(NotFoundError):
+            CreditCard.from_nonce(None)
 
     def test_multiple_verifications_sort(self):
         verification1 = {"created_at": datetime.datetime(2014, 11, 18, 23, 20, 20), "id": 123, "amount": "0.00"}
diff --git a/tests/unit/test_credit_card_verification.py b/tests/unit/test_credit_card_verification.py
index c0f90ec..d3c1425 100644
--- a/tests/unit/test_credit_card_verification.py
+++ b/tests/unit/test_credit_card_verification.py
@@ -2,9 +2,42 @@ from tests.test_helper import *
 
 class TestCreditCardVerification(unittest.TestCase):
 
-    @raises_with_regexp(KeyError, "'Invalid keys: bad_key'")
     def test_create_raises_exception_with_bad_keys(self):
-        CreditCardVerification.create({"bad_key": "value", "credit_card": {"number": "value"}})
+        with self.assertRaisesRegex(KeyError, "'Invalid keys: bad_key'"):
+            CreditCardVerification.create({"bad_key": "value", "credit_card": {"number": "value"}})
+
+    def test_create_signature(self):
+        billing_address_params = [
+                "company", "country_code_alpha2", "country_code_alpha3", "country_code_numeric",
+                "country_name", "extended_address", "first_name", "last_name", "locality",
+                "postal_code", "region", "street_address"
+                ]
+        credit_card_params = [
+                "number", "cvv", "cardholder_name", "cvv", "expiration_date", "expiration_month",
+                "expiration_year", {"billing_address": billing_address_params}
+                ]
+        options_params = [
+                "account_type", "amount", "merchant_account_id"
+                ]
+        three_d_secure_pass_thru_params = [
+                "eci_flag",
+                "cavv",
+                "xid",
+                "authentication_response",
+                "directory_response",
+                "cavv_algorithm",
+                "ds_transaction_id",
+                "three_d_secure_version"
+                ]
+        expected = [
+                {"credit_card": credit_card_params},
+                "intended_transaction_source",
+                {"options": options_params},
+                "payment_method_nonce",
+                "three_d_secure_authentication_id",
+                {"three_d_secure_pass_thru": three_d_secure_pass_thru_params}]
+
+        self.assertEqual(expected, CreditCardVerification.create_signature())
 
     def test_constructor_with_amount(self):
         attributes = {
@@ -49,10 +82,10 @@ class TestCreditCardVerification(unittest.TestCase):
         self.assertEqual(verification.network_response_code, None)
         self.assertEqual(verification.network_response_text, None)
 
-    @raises(NotFoundError)
     def test_finding_empty_id_raises_not_found_exception(self):
-        CreditCardVerification.find(" ")
+        with self.assertRaises(NotFoundError):
+            CreditCardVerification.find(" ")
 
-    @raises(NotFoundError)
     def test_finding_none_raises_not_found_exception(self):
-        CreditCardVerification.find(None)
+        with self.assertRaises(NotFoundError):
+            CreditCardVerification.find(None)
diff --git a/tests/unit/test_customer.py b/tests/unit/test_customer.py
index b4f2d5c..c83e43d 100644
--- a/tests/unit/test_customer.py
+++ b/tests/unit/test_customer.py
@@ -1,46 +1,45 @@
 from tests.test_helper import *
 
 class TestCustomer(unittest.TestCase):
-    @raises_with_regexp(KeyError, "'Invalid keys: bad_key'")
     def test_create_raise_exception_with_bad_keys(self):
-        Customer.create({"bad_key": "value"})
+        with self.assertRaisesRegex(KeyError, "'Invalid keys: bad_key'"):
+            Customer.create({"bad_key": "value"})
 
-    @raises_with_regexp(KeyError, "'Invalid keys: credit_card\[bad_key\]'")
     def test_create_raise_exception_with_bad_nested_keys(self):
-        Customer.create({"credit_card": {"bad_key": "value"}})
+        with self.assertRaisesRegex(KeyError, "'Invalid keys: credit_card\[bad_key\]'"):
+            Customer.create({"credit_card": {"bad_key": "value"}})
 
-    @raises_with_regexp(KeyError, "'Invalid keys: bad_key'")
     def test_update_raise_exception_with_bad_keys(self):
-        Customer.update("id", {"bad_key": "value"})
+        with self.assertRaisesRegex(KeyError, "'Invalid keys: bad_key'"):
+            Customer.update("id", {"bad_key": "value"})
 
-    @raises_with_regexp(KeyError, "'Invalid keys: credit_card\[bad_key\]'")
     def test_update_raise_exception_with_bad_nested_keys(self):
-        Customer.update("id", {"credit_card": {"bad_key": "value"}})
+        with self.assertRaisesRegex(KeyError, "'Invalid keys: credit_card\[bad_key\]'"):
+            Customer.update("id", {"credit_card": {"bad_key": "value"}})
 
-    @raises_with_regexp(KeyError, "'Invalid keys: bad_key'")
-    def test_tr_data_for_create_raises_error_with_bad_keys(self):
-        Customer.tr_data_for_create({"bad_key": "value"}, "http://example.com")
-
-    @raises_with_regexp(KeyError, "'Invalid keys: bad_key'")
-    def test_tr_data_for_update_raises_error_with_bad_keys(self):
-        Customer.tr_data_for_update({"bad_key": "value"}, "http://example.com")
-
-    @raises(NotFoundError)
     def test_finding_empty_id_raises_not_found_exception(self):
-        Customer.find(" ")
+        with self.assertRaises(NotFoundError):
+            Customer.find(" ")
 
-    @raises(NotFoundError)
     def test_finding_none_raises_not_found_exception(self):
-        Customer.find(None)
+        with self.assertRaises(NotFoundError):
+            Customer.find(None)
 
     def test_initialize_sets_paypal_accounts(self):
         customer = Customer("gateway", {
             "paypal_accounts": [
                 {"token": "token1"},
                 {"token": "token2"}
+            ],
+            "sepa_debit_accounts": [
+                {"token": "sdd1"},
+                {"token": "sdd2"}
             ]
         })
 
         self.assertEqual(2, len(customer.paypal_accounts))
         self.assertEqual("token1", customer.paypal_accounts[0].token)
         self.assertEqual("token2", customer.paypal_accounts[1].token)
+        self.assertEqual(2, len(customer.sepa_direct_debit_accounts))
+        self.assertEqual("sdd1", customer.sepa_direct_debit_accounts[0].token)
+        self.assertEqual("sdd2", customer.sepa_direct_debit_accounts[1].token)
diff --git a/tests/unit/test_dispute.py b/tests/unit/test_dispute.py
index 2332127..2646747 100644
--- a/tests/unit/test_dispute.py
+++ b/tests/unit/test_dispute.py
@@ -26,6 +26,7 @@ class TestDispute(unittest.TestCase):
         "amount_disputed": "100.00",
         "amount_won": "0.00",
         "case_number": "CB123456",
+        "chargeback_protection_level": "effortless",
         "created_at": datetime(2013, 4, 10, 10, 50, 39),
         "currency_iso_code": "USD",
         "date_opened": date(2013, 4, 1),
@@ -35,6 +36,7 @@ class TestDispute(unittest.TestCase):
         "kind": "chargeback",
         "merchant_account_id": "abc123",
         "original_dispute_id": "original_dispute_id",
+        "pre_dispute_program": "none",
         "reason": "fraud",
         "reason_code": "83",
         "reason_description": "Reason code 83 description",
@@ -66,10 +68,16 @@ class TestDispute(unittest.TestCase):
             "id": "transaction_id",
             "amount": "100.00",
             "created_at": datetime(2013, 3, 19, 10, 50, 39),
+            "installment_count": None,
             "order_id": None,
             "purchase_order_number": "po",
             "payment_instrument_subtype": "Visa",
         },
+        "paypal_messages": [{
+            "message": "message",
+            "sender": "seller",
+            "sent_at": datetime(2013, 4, 10, 10, 50, 39),
+        }],
     }
 
     def test_legacy_constructor(self):
@@ -109,11 +117,15 @@ class TestDispute(unittest.TestCase):
         self.assertEqual(dispute.amount_disputed, 100.0)
         self.assertEqual(dispute.amount_won, 0.00)
         self.assertEqual(dispute.case_number, "CB123456")
+        # NEXT_MAJOR_VERSION Remove this assertion when chargeback_protection_level is removed from the SDK
+        self.assertEqual(dispute.chargeback_protection_level, "effortless")
+        self.assertEqual(dispute.protection_level, braintree.Dispute.ProtectionLevel.EffortlessCBP)
         self.assertEqual(dispute.created_at, datetime(2013, 4, 10, 10, 50, 39))
         self.assertEqual(dispute.forwarded_comments, "Forwarded comments")
         self.assertEqual(dispute.processor_comments, "Forwarded comments")
         self.assertEqual(dispute.merchant_account_id, "abc123")
         self.assertEqual(dispute.original_dispute_id, "original_dispute_id")
+        self.assertEqual(dispute.pre_dispute_program, Dispute.PreDisputeProgram.NONE)
         self.assertEqual(dispute.reason_code, "83")
         self.assertEqual(dispute.reason_description, "Reason code 83 description")
         self.assertEqual(dispute.reference_number, "123456")
@@ -128,11 +140,58 @@ class TestDispute(unittest.TestCase):
         self.assertEqual(dispute.evidence[1].id, "evidence2")
         self.assertEqual(dispute.evidence[1].sent_to_processor_at, "2009-04-11")
         self.assertIsNone(dispute.evidence[1].url)
+        self.assertEqual(dispute.paypal_messages[0].message, "message")
+        self.assertEqual(dispute.paypal_messages[0].sender, "seller")
+        self.assertEqual(dispute.paypal_messages[0].sent_at, datetime(2013, 4, 10, 10, 50, 39))
         self.assertEqual(dispute.status_history[0].disbursement_date, "2013-04-11")
         self.assertEqual(dispute.status_history[0].effective_date, "2013-04-10")
         self.assertEqual(dispute.status_history[0].status, "open")
         self.assertEqual(dispute.status_history[0].timestamp, datetime(2013, 4, 10, 10, 50, 39))
 
+    def test_constructor_populates_standard_cbp_level(self):
+        attributes = dict(self.attributes)
+        del attributes["amount"]
+        attributes["chargeback_protection_level"] = "standard"
+
+        dispute = Dispute(attributes)
+
+        # NEXT_MAJOR_VERSION Remove this assertion when chargeback_protection_level is removed from the SDK
+        self.assertEqual(dispute.chargeback_protection_level, braintree.Dispute.ChargebackProtectionLevel.Standard)
+        self.assertEqual(dispute.protection_level, braintree.Dispute.ProtectionLevel.StandardCBP)
+
+    def test_constructor_populates_none_cbp_level(self):
+        attributes = dict(self.attributes)
+        del attributes["amount"]
+        attributes["chargeback_protection_level"] = None
+
+        dispute = Dispute(attributes)
+
+        # NEXT_MAJOR_VERSION Remove this assertion when chargeback_protection_level is removed from the SDK
+        self.assertEqual(dispute.chargeback_protection_level, None)
+        self.assertEqual(dispute.protection_level, braintree.Dispute.ProtectionLevel.NoProtection)
+
+    def test_constructor_populates_empty_cbp_level(self):
+        attributes = dict(self.attributes)
+        del attributes["amount"]
+        attributes["chargeback_protection_level"] = ""
+
+        dispute = Dispute(attributes)
+
+        # NEXT_MAJOR_VERSION Remove this assertion when chargeback_protection_level is removed from the SDK
+        self.assertEqual(dispute.chargeback_protection_level, "")
+        self.assertEqual(dispute.protection_level, braintree.Dispute.ProtectionLevel.NoProtection)
+
+    def test_constructor_populates_not_protected_cbp_level(self):
+        attributes = dict(self.attributes)
+        del attributes["amount"]
+        attributes["chargeback_protection_level"] = "not_protected"
+
+        dispute = Dispute(attributes)
+
+        # NEXT_MAJOR_VERSION Remove this assertion when chargeback_protection_level is removed from the SDK
+        self.assertEqual(dispute.chargeback_protection_level, braintree.Dispute.ChargebackProtectionLevel.NotProtected)
+        self.assertEqual(dispute.protection_level, braintree.Dispute.ProtectionLevel.NoProtection)
+
     def test_constructor_handles_none_fields(self):
         attributes = dict(self.attributes)
         attributes.update({
@@ -140,16 +199,19 @@ class TestDispute(unittest.TestCase):
             "date_opened": None,
             "date_won": None,
             "evidence": None,
+            "paypal_messages": None,
             "reply_by_date": None,
             "status_history": None
         })
 
         dispute = Dispute(attributes)
 
-        self.assertIsNone(dispute.reply_by_date)
         self.assertIsNone(dispute.amount)
         self.assertIsNone(dispute.date_opened)
         self.assertIsNone(dispute.date_won)
+        self.assertIsNone(dispute.evidence)
+        self.assertIsNone(dispute.paypal_messages)
+        self.assertIsNone(dispute.reply_by_date)
         self.assertIsNone(dispute.status_history)
 
     def test_constructor_populates_transaction(self):
@@ -158,94 +220,95 @@ class TestDispute(unittest.TestCase):
         self.assertEqual(dispute.transaction.id, "transaction_id")
         self.assertEqual(dispute.transaction.amount, Decimal("100.00"))
         self.assertEqual(dispute.transaction.created_at, datetime(2013, 3, 19, 10, 50, 39))
+        self.assertIsNone(dispute.transaction.installment_count)
         self.assertIsNone(dispute.transaction.order_id)
         self.assertEqual(dispute.transaction.purchase_order_number, "po")
         self.assertEqual(dispute.transaction.payment_instrument_subtype, "Visa")
 
-    @raises_with_regexp(NotFoundError, "dispute with id None not found")
     def test_accept_none_raises_not_found_exception(self):
-        Dispute.accept(None)
+        with self.assertRaisesRegex(NotFoundError, "dispute with id None not found"):
+            Dispute.accept(None)
 
-    @raises_with_regexp(NotFoundError, "dispute with id ' ' not found")
     def test_accept_empty_id_raises_not_found_exception(self):
-        Dispute.accept(" ")
+        with self.assertRaisesRegex(NotFoundError, "dispute with id ' ' not found"):
+            Dispute.accept(" ")
 
-    @raises_with_regexp(NotFoundError, "dispute_id cannot be blank")
     def test_add_text_evidence_empty_id_raises_not_found_exception(self):
-        Dispute.add_text_evidence(" ", "evidence")
+        with self.assertRaisesRegex(NotFoundError, "dispute_id cannot be blank"):
+            Dispute.add_text_evidence(" ", "evidence")
 
-    @raises_with_regexp(NotFoundError, "dispute_id cannot be blank")
     def test_add_text_evidence_none_id_raises_not_found_exception(self):
-        Dispute.add_text_evidence(None, "evidence")
+        with self.assertRaisesRegex(NotFoundError, "dispute_id cannot be blank"):
+            Dispute.add_text_evidence(None, "evidence")
 
-    @raises_with_regexp(ValueError, "content cannot be blank")
     def test_add_text_evidence_empty_evidence_raises_value_exception(self):
-        Dispute.add_text_evidence("dispute_id", " ")
+        with self.assertRaisesRegex(ValueError, "content cannot be blank"):
+            Dispute.add_text_evidence("dispute_id", " ")
 
-    @raises_with_regexp(ValueError, "sequence_number must be an integer")
     def test_add_text_evidence_sequence_number_not_number_evidence_raises_value_exception(self):
-        Dispute.add_text_evidence("dispute_id", { "content": "content", "sequence_number": "a" })
+        with self.assertRaisesRegex(ValueError, "sequence_number must be an integer"):
+            Dispute.add_text_evidence("dispute_id", { "content": "content", "sequence_number": "a" })
 
-    @raises_with_regexp(ValueError, "sequence_number must be an integer")
-    def test_add_text_evidence_sequence_number_not_number_evidence_raises_value_exception(self):
-        Dispute.add_text_evidence("dispute_id", { "content": "content", "sequence_number": "1abc" })
+    def test_add_text_evidence_sequence_number_number_and_letter_evidence_raises_value_exception(self):
+        with self.assertRaisesRegex(ValueError, "sequence_number must be an integer"):
+            Dispute.add_text_evidence("dispute_id", { "content": "content", "sequence_number": "1abc" })
 
-    @raises_with_regexp(ValueError, "category must be a string")
     def test_add_text_evidence_category_is_number_evidence_raises_value_exception(self):
-        Dispute.add_text_evidence("dispute_id", { "content": "content", "category": 5 })
+        with self.assertRaisesRegex(ValueError, "category must be a string"):
+            Dispute.add_text_evidence("dispute_id", { "content": "content", "category": 5 })
 
-    @raises_with_regexp(NotFoundError, "dispute with id ' ' not found")
     def test_add_file_evidence_empty_id_raises_not_found_exception(self):
-        Dispute.add_file_evidence(" ", 1)
+        with self.assertRaisesRegex(NotFoundError, "dispute with id ' ' not found"):
+            Dispute.add_file_evidence(" ", 1)
 
-    @raises_with_regexp(NotFoundError, "dispute with id None not found")
     def test_add_file_evidence_none_id_raises_not_found_exception(self):
-        Dispute.add_file_evidence(None, 1)
+        with self.assertRaisesRegex(NotFoundError, "dispute with id None not found"):
+            Dispute.add_file_evidence(None, 1)
 
-    @raises_with_regexp(ValueError, "document_id cannot be blank")
     def test_add_file_evidence_empty_evidence_raises_value_exception(self):
-        Dispute.add_file_evidence("dispute_id", " ")
+        with self.assertRaisesRegex(ValueError, "document_id cannot be blank"):
+            Dispute.add_file_evidence("dispute_id", " ")
 
-    @raises_with_regexp(ValueError, "document_id cannot be blank")
     def test_add_file_evidence_none_evidence_raises_value_exception(self):
-        Dispute.add_file_evidence("dispute_id", None)
+        with self.assertRaisesRegex(ValueError, "document_id cannot be blank"):
+            Dispute.add_file_evidence("dispute_id", None)
 
-    @raises_with_regexp(ValueError, "category must be a string")
     def test_add_file_evidence_categorized_document_id_must_be_a_string(self):
-        Dispute.add_file_evidence("dispute_id", { "document_id": "213", "category": 5 })
+        with self.assertRaisesRegex(ValueError, "category must be a string"):
+            Dispute.add_file_evidence("dispute_id", { "document_id": "213", "category": 5 })
 
-    @raises_with_regexp(ValueError, "document_id cannot be blank")
     def test_add_file_evidence_empty_categorized_evidence_raises_value_exception(self):
-        Dispute.add_file_evidence("dispute_id", { "category": "DEVICE_ID" })
+        with self.assertRaisesRegex(ValueError, "document_id cannot be blank"):
+            Dispute.add_file_evidence("dispute_id", { "category": "DEVICE_ID" })
 
-    @raises_with_regexp(NotFoundError, "dispute with id None not found")
     def test_finalize_none_raises_not_found_exception(self):
-        Dispute.finalize(None)
+        with self.assertRaisesRegex(NotFoundError, "dispute with id None not found"):
+            Dispute.finalize(None)
 
-    @raises_with_regexp(NotFoundError, "dispute with id ' ' not found")
     def test_finalize_empty_id_raises_not_found_exception(self):
-        Dispute.finalize(" ")
+        with self.assertRaisesRegex(NotFoundError, "dispute with id ' ' not found"):
+            Dispute.finalize(" ")
 
-    @raises_with_regexp(NotFoundError, "dispute with id None not found")
     def test_finding_none_raises_not_found_exception(self):
-        Dispute.find(None)
+        with self.assertRaisesRegex(NotFoundError, "dispute with id None not found"):
+            Dispute.find(None)
 
-    @raises_with_regexp(NotFoundError, "dispute with id ' ' not found")
     def test_finding_empty_id_raises_not_found_exception(self):
-        Dispute.find(" ")
+        with self.assertRaisesRegex(NotFoundError, "dispute with id ' ' not found"):
+            Dispute.find(" ")
 
-    @raises_with_regexp(NotFoundError, "evidence with id 'evidence' for dispute with id ' ' not found")
     def test_remove_evidence_empty_dispute_id_raises_not_found_exception(self):
-        Dispute.remove_evidence(" ", "evidence")
+        with self.assertRaisesRegex(NotFoundError, "evidence with id 'evidence' for dispute with id ' ' not found"):
+            Dispute.remove_evidence(" ", "evidence")
 
-    @raises_with_regexp(NotFoundError, "evidence with id 'evidence' for dispute with id None not found")
     def test_remove_evidence_none_dispute_id_raises_not_found_exception(self):
-        Dispute.remove_evidence(None, "evidence")
+        with self.assertRaisesRegex(NotFoundError, "evidence with id 'evidence' for dispute with id None not found"):
+            Dispute.remove_evidence(None, "evidence")
 
-    @raises_with_regexp(NotFoundError, "evidence with id None for dispute with id 'dispute_id' not found")
     def test_remove_evidence_evidence_none_id_raises_not_found_exception(self):
-        Dispute.remove_evidence("dispute_id", None)
+        with self.assertRaisesRegex(NotFoundError, "evidence with id None for dispute with id 'dispute_id' not found"):
+            Dispute.remove_evidence("dispute_id", None)
 
-    @raises_with_regexp(NotFoundError, "evidence with id ' ' for dispute with id 'dispute_id' not found")
     def test_remove_evidence_empty_evidence_id_raises_value_exception(self):
-        Dispute.remove_evidence("dispute_id", " ")
+        with self.assertRaisesRegex(NotFoundError, "evidence with id ' ' for dispute with id 'dispute_id' not found"):
+            Dispute.remove_evidence("dispute_id", " ")
diff --git a/tests/unit/test_document_upload.py b/tests/unit/test_document_upload.py
index 8ebecd8..dc06c8a 100644
--- a/tests/unit/test_document_upload.py
+++ b/tests/unit/test_document_upload.py
@@ -1,6 +1,6 @@
 from tests.test_helper import *
 
 class TestDocumentUpload(unittest.TestCase):
-    @raises_with_regexp(KeyError, "'Invalid keys: bad_key'")
     def test_create_raises_exception_with_bad_keys(self):
-        DocumentUpload.create({"bad_key": "value"})
+        with self.assertRaisesRegex(KeyError, "'Invalid keys: bad_key'"):
+            DocumentUpload.create({"bad_key": "value"})
diff --git a/tests/unit/test_exchange_rate_quote_gateway.py b/tests/unit/test_exchange_rate_quote_gateway.py
new file mode 100644
index 0000000..f7c4627
--- /dev/null
+++ b/tests/unit/test_exchange_rate_quote_gateway.py
@@ -0,0 +1,95 @@
+from braintree.exchange_rate_quote_gateway import ExchangeRateQuoteGateway
+from braintree.exchange_rate_quote_request import ExchangeRateQuoteRequest
+from tests.test_helper import *
+from unittest.mock import Mock
+
+class TestExchangeRateQuoteGateway(unittest.TestCase):
+    @staticmethod
+    def get_gateway():
+        config = Configuration("development", "integration_merchant_id",
+                               public_key="integration_public_key",
+                               private_key="integration_private_key")
+        return BraintreeGateway(config)
+
+    def test_generate_success(self):
+        attribute1 = {"base_currency":"USD",
+                      "quote_currency":"EUR",
+                      "base_amount":"12.19",
+                      "markup":"1.89"}
+
+        request = ExchangeRateQuoteRequest().add_exchange_rate_quote_input(attribute1).done()
+
+        raw_response = """
+                    {
+                      "data": {
+                      "generateExchangeRateQuote": {
+                        "quotes": [
+                        {
+                          "id": "ZXhjaGFuZ2VyYXRlcXVvdGVfMDEyM0FCQw",
+                          "baseAmount": {
+                              "value": "12.19",
+                              "currencyCode": "USD"
+                          },
+                          "quoteAmount": {
+                            "value": "12.16",
+                            "currencyCode": "EUR"
+                          },
+                          "exchangeRate": "0.997316360864",
+                            "expiresAt": "2021-06-16T02:00:00.000000Z",
+                            "refreshesAt": "2021-06-16T00:00:00.000000Z"
+                        }
+                        ]
+                      }
+                    },
+                      "extensions": {
+                      "requestId": "5ef2e69a-fb0e-4d71-82a3-ea59722ac64d"
+                      }
+                    }
+                """
+        response = json.loads(raw_response)
+        self.graphql_client = self.get_gateway().graphql_client
+        self.graphql_client.query = Mock(return_value=response)
+        exchange_rate_quote_gateway = ExchangeRateQuoteGateway(self.get_gateway(),self.graphql_client)
+        result = exchange_rate_quote_gateway.generate(request)
+        quotes = result.exchange_rate_quote_payload.get_quotes()
+        self.assertIsNotNone(quotes)
+        self.assertEqual(1,len(quotes))
+
+        quote1 = quotes[0]
+        self.assertEqual("12.19", str(quote1.base_amount.value))
+        self.assertEqual("USD", quote1.base_amount.currency_code)
+        self.assertEqual("12.16", str(quote1.quote_amount.value))
+        self.assertEqual("EUR", quote1.quote_amount.currency_code)
+        self.assertEqual("0.997316360864", quote1.exchange_rate)
+        self.assertEqual("2021-06-16T02:00:00.000000Z", quote1.expires_at)
+        self.assertEqual("2021-06-16T00:00:00.000000Z", quote1.refreshes_at)
+        self.assertEqual("ZXhjaGFuZ2VyYXRlcXVvdGVfMDEyM0FCQw", quote1.id)
+
+    def test_generate_error(self):
+        attribute1 = {"base_currency":"USD"}
+        request = ExchangeRateQuoteRequest().add_exchange_rate_quote_input(attribute1).done()
+
+        raw_response = """
+                {
+                  "errors": [
+                    {
+                      "message": "Field 'quoteCurrency' of variable 'exchangeRateQuoteRequest' has coerced Null value for NonNull type 'CurrencyCodeAlpha!'",
+                      "locations": [
+                        {
+                          "line": 1,
+                          "column": 11
+                        }
+                      ]
+                    }
+                  ],
+                  "extensions": {
+                    "requestId": "96c023c9-0192-4008-8f28-25a7f8714bab"
+                  }
+                }
+                """
+        response = json.loads(raw_response)
+        self.graphql_client = self.get_gateway().graphql_client
+        self.graphql_client.query = Mock(return_value=response)
+        exchange_rate_quote_gateway = ExchangeRateQuoteGateway(self.get_gateway(),self.graphql_client)
+        result = exchange_rate_quote_gateway.generate(request)
+        self.assertTrue("'quoteCurrency'" in result.message)
\ No newline at end of file
diff --git a/tests/unit/test_exchange_rate_quote_input.py b/tests/unit/test_exchange_rate_quote_input.py
new file mode 100644
index 0000000..291d999
--- /dev/null
+++ b/tests/unit/test_exchange_rate_quote_input.py
@@ -0,0 +1,36 @@
+from braintree.exchange_rate_quote_request import ExchangeRateQuoteInput
+from tests.test_helper import *
+
+class TestExchangeRateQuoteInput(unittest.TestCase):
+    def test_to_graphql_variables(self):
+        attributes = {"base_currency":"USD",
+                      "quote_currency":"EUR",
+                      "base_amount":"10.15",
+                      "markup":"5.00"}
+        input = ExchangeRateQuoteInput(None,attributes)
+
+        map = input.to_graphql_variables()
+        self.assertEqual(map.get("baseCurrency"), "USD")
+        self.assertEqual(map.get("quoteCurrency"), "EUR")
+        self.assertEqual(map.get("baseAmount"), "10.15")
+        self.assertEqual(map.get("markup"), "5.00")
+
+    def test_to_graphql_variables_without_markup_and_base_amount(self):
+        attributes = {"base_currency":"USD",
+                      "quote_currency":"CAD"}
+        input = ExchangeRateQuoteInput(None,attributes)
+
+        map = input.to_graphql_variables()
+        self.assertEqual(map.get("baseCurrency"), "USD")
+        self.assertEqual(map.get("quoteCurrency"), "CAD")
+        self.assertIsNone(map.get("baseAmount"))
+        self.assertIsNone(map.get("markup"))
+
+    def test_to_graphql_variables_with_all_empty_fields(self):
+        input = ExchangeRateQuoteInput(None, None)
+
+        map = input.to_graphql_variables()
+        self.assertIsNone(map.get("baseCurrency"))
+        self.assertIsNone(map.get("quoteCurrency"))
+        self.assertIsNone(map.get("baseAmount"))
+        self.assertIsNone(map.get("markup"))
\ No newline at end of file
diff --git a/tests/unit/test_exchange_rate_quote_request.py b/tests/unit/test_exchange_rate_quote_request.py
new file mode 100644
index 0000000..cf3831c
--- /dev/null
+++ b/tests/unit/test_exchange_rate_quote_request.py
@@ -0,0 +1,66 @@
+from braintree.exchange_rate_quote_request import ExchangeRateQuoteRequest
+from tests.test_helper import *
+
+class TestExchangeRateQuoteRequest(unittest.TestCase):
+    def test_to_graphql_variables(self):
+        attribute1 = {"base_currency":"USD",
+                      "quote_currency":"EUR",
+                      "base_amount":"5.00",
+                      "markup":"3.00"}
+
+        attribute2 = {"base_currency":"EUR",
+                      "quote_currency":"CAD",
+                      "base_amount":"15.00",
+                      "markup":"2.64"}
+
+        request = ExchangeRateQuoteRequest().add_exchange_rate_quote_input(
+            attribute1).done().add_exchange_rate_quote_input(attribute2).done()
+
+        request_map = request.to_graphql_variables().get("exchangeRateQuoteRequest")
+        self.assertIsNotNone(request_map)
+        
+        quotes = request_map.get("quotes")
+        self.assertIsNotNone(quotes)
+        self.assertEqual(2,len(quotes))
+
+        quote1 = quotes[0]
+        self.assertEqual("USD", quote1.get("baseCurrency"))
+        self.assertEqual("EUR", quote1.get("quoteCurrency"))
+        self.assertEqual("5.00", quote1.get("baseAmount"))
+        self.assertEqual("3.00", quote1.get("markup"))
+
+        quote2 = quotes[1]
+        self.assertEqual("EUR", quote2.get("baseCurrency"))
+        self.assertEqual("CAD", quote2.get("quoteCurrency"))
+        self.assertEqual("15.00", quote2.get("baseAmount"))
+        self.assertEqual("2.64", quote2.get("markup"))
+
+    def test_to_graphql_variables_with_missing_fields(self):
+        attribute1 = {"base_currency":"USD",
+                      "quote_currency":"EUR",
+                      "base_amount":"5.00"}
+
+        attribute2 = {"base_currency":"EUR",
+                      "quote_currency":"CAD"}
+
+        request = ExchangeRateQuoteRequest().add_exchange_rate_quote_input(attribute1
+                ).done().add_exchange_rate_quote_input(attribute2).done()
+
+        request_map = request.to_graphql_variables().get("exchangeRateQuoteRequest")
+        self.assertIsNotNone(request_map)
+        
+        quotes = request_map.get("quotes")
+        self.assertIsNotNone(quotes)
+        self.assertEqual(2,len(quotes))
+
+        quote1 = quotes[0]
+        self.assertEqual("USD", quote1.get("baseCurrency"))
+        self.assertEqual("EUR", quote1.get("quoteCurrency"))
+        self.assertEqual("5.00", quote1.get("baseAmount"))
+        self.assertIsNone(quote1.get("markup"))
+
+        quote2 = quotes[1]
+        self.assertEqual("EUR", quote2.get("baseCurrency"))
+        self.assertEqual("CAD", quote2.get("quoteCurrency"))
+        self.assertIsNone(quote2.get("baseAmount"))
+        self.assertIsNone(quote2.get("markup"))
\ No newline at end of file
diff --git a/tests/unit/test_exports.py b/tests/unit/test_exports.py
index 8f1ce47..7791bb7 100644
--- a/tests/unit/test_exports.py
+++ b/tests/unit/test_exports.py
@@ -31,9 +31,6 @@ class TestExports(unittest.TestCase):
         self.assertNotEqual(braintree.ErrorResult, None)
         self.assertNotEqual(braintree.Errors, None)
         self.assertNotEqual(braintree.EuropeBankAccount, None)
-        # NEXT_MAJOR_VERSION Remove this class as legacy Ideal has been removed/disabled in the Braintree Gateway
-        # DEPRECATED If you're looking to accept iDEAL as a payment method contact accounts@braintreepayments.com for a solution.
-        self.assertNotEqual(braintree.IdealPayment, None)
         self.assertNotEqual(braintree.Merchant, None)
         self.assertNotEqual(braintree.MerchantAccount, None)
         self.assertNotEqual(braintree.MerchantAccountGateway, None)
@@ -62,8 +59,6 @@ class TestExports(unittest.TestCase):
         self.assertNotEqual(braintree.TransactionDetails, None)
         self.assertNotEqual(braintree.TransactionGateway, None)
         self.assertNotEqual(braintree.TransactionSearch, None)
-        self.assertNotEqual(braintree.TransparentRedirect, None)
-        self.assertNotEqual(braintree.TransparentRedirectGateway, None)
         self.assertNotEqual(braintree.UnknownPaymentMethod, None)
         self.assertNotEqual(braintree.UsBankAccount, None)
         self.assertNotEqual(braintree.ValidationErrorCollection, None)
diff --git a/tests/unit/test_graphql_client.py b/tests/unit/test_graphql_client.py
index 7df00d9..e1062e9 100644
--- a/tests/unit/test_graphql_client.py
+++ b/tests/unit/test_graphql_client.py
@@ -1,7 +1,20 @@
 from tests.test_helper import *
 
 class TestGraphQLClient(unittest.TestCase):
-    @raises(UpgradeRequiredError)
+    def test_raise_exception_from_status_service_unavailable(self):
+        response = {
+            "errors": [
+                {
+                    "message": "error message",
+                    "extensions": {
+                        "errorClass": "SERVICE_AVAILABILITY"
+                    }
+                }
+            ]
+        }
+        with self.assertRaises(ServiceUnavailableError):
+            GraphQLClient.raise_exception_for_graphql_error(response)
+
     def test_raise_exception_from_status_for_upgrade_required(self):
         response = {
             "errors": [
@@ -13,9 +26,9 @@ class TestGraphQLClient(unittest.TestCase):
                 }
             ]
         }
-        GraphQLClient.raise_exception_for_graphql_error(response)
+        with self.assertRaises(UpgradeRequiredError):
+            GraphQLClient.raise_exception_for_graphql_error(response)
 
-    @raises(TooManyRequestsError)
     def test_raise_exception_from_too_many_requests(self):
         response = {
             "errors": [
@@ -27,7 +40,8 @@ class TestGraphQLClient(unittest.TestCase):
                 }
             ]
         }
-        GraphQLClient.raise_exception_for_graphql_error(response)
+        with self.assertRaises(TooManyRequestsError):
+            GraphQLClient.raise_exception_for_graphql_error(response)
 
     def test_does_not_raise_exception_from_validation_error(self):
         response = {
@@ -42,7 +56,6 @@ class TestGraphQLClient(unittest.TestCase):
         }
         GraphQLClient.raise_exception_for_graphql_error(response)
 
-    @raises(ServerError)
     def test_raise_exception_from_validation_error_and_legitimate_error(self):
         response = {
             "errors": [
@@ -60,4 +73,5 @@ class TestGraphQLClient(unittest.TestCase):
                 }
             ]
         }
-        GraphQLClient.raise_exception_for_graphql_error(response)
+        with self.assertRaises(ServerError):
+            GraphQLClient.raise_exception_for_graphql_error(response)
diff --git a/tests/unit/test_http.py b/tests/unit/test_http.py
index 661d9b0..600086a 100644
--- a/tests/unit/test_http.py
+++ b/tests/unit/test_http.py
@@ -3,15 +3,28 @@ import traceback
 from tests.test_helper import *
 from braintree.exceptions.http.timeout_error import *
 from braintree.attribute_getter import AttributeGetter
+from unittest.mock import patch
 
 class TestHttp(unittest.TestCase):
-    @raises(UpgradeRequiredError)
+    def test_raise_exception_from_request_timeout(self):
+        with self.assertRaises(RequestTimeoutError):
+            Http.raise_exception_from_status(408)
+
     def test_raise_exception_from_status_for_upgrade_required(self):
-        Http.raise_exception_from_status(426)
+        with self.assertRaises(UpgradeRequiredError):
+            Http.raise_exception_from_status(426)
 
-    @raises(TooManyRequestsError)
     def test_raise_exception_from_too_many_requests(self):
-        Http.raise_exception_from_status(429)
+        with self.assertRaises(TooManyRequestsError):
+            Http.raise_exception_from_status(429)
+
+    def test_raise_exception_from_service_unavailable(self):
+        with self.assertRaises(ServiceUnavailableError):
+            Http.raise_exception_from_status(503)
+
+    def test_raise_exception_from_gateway_timeout(self):
+        with self.assertRaises(GatewayTimeoutError):
+            Http.raise_exception_from_status(504)
 
     def test_header_includes_gzip_accept_encoding(self):
         config = AttributeGetter({
@@ -110,18 +123,50 @@ class TestHttp(unittest.TestCase):
 
         return Http(config, "fake_environment")
 
-    @raises(ReadTimeoutError)
     def test_raise_read_timeout_error(self):
         def test_http_do_strategy(http_verb, path, headers, request_body):
             return (200, "")
 
-        http = self.setup_http_strategy(test_http_do_strategy)
-        http.handle_exception(requests.exceptions.ReadTimeout())
+        with self.assertRaises(ReadTimeoutError):
+            http = self.setup_http_strategy(test_http_do_strategy)
+            http.handle_exception(requests.exceptions.ReadTimeout())
 
-    @raises(ConnectTimeoutError)
-    def test_raise_read_timeout_error(self):
+    def test_raise_connect_timeout_error(self):
         def test_http_do_strategy(http_verb, path, headers, request_body):
             return (200, "")
 
-        http = self.setup_http_strategy(test_http_do_strategy)
-        http.handle_exception(requests.exceptions.ConnectTimeout())
+        with self.assertRaises(ConnectTimeoutError):
+            http = self.setup_http_strategy(test_http_do_strategy)
+            http.handle_exception(requests.exceptions.ConnectTimeout())
+
+    def test_request_urls_retain_dots(self):
+        with patch('requests.Session.send') as send:
+            send.return_value.status_code = 200
+            config = Configuration(
+                Environment.Development,
+                "integration_merchant_id",
+                public_key="integration_public_key",
+                private_key="integration_private_key",
+                wrap_http_exceptions=True
+            )
+            http = config.http()
+            http.get("/../../customers/")
+
+            prepared_request = send.call_args[0][0]
+            request_url = prepared_request.url
+            self.assertTrue(request_url.endswith("/../../customers/"))
+
+    def test_sessions_close_after_request(self):
+        with patch('requests.Session.send') as send, patch('requests.Session.close') as close:
+            send.return_value.status_code = 200
+            config = Configuration(
+                Environment.Development,
+                "integration_merchant_id",
+                public_key="integration_public_key",
+                private_key="integration_private_key",
+                wrap_http_exceptions=True
+            )
+            http = config.http()
+            http.get("/../../customers/")
+
+            self.assertTrue(close.called)
diff --git a/tests/unit/test_liability_shift.py b/tests/unit/test_liability_shift.py
new file mode 100644
index 0000000..debf91f
--- /dev/null
+++ b/tests/unit/test_liability_shift.py
@@ -0,0 +1,13 @@
+from tests.test_helper import *
+from braintree import *
+
+class TestLiabilityShift(unittest.TestCase):
+    def test_initialization_of_attributes(self):
+        liability_shift = LiabilityShift(
+                {
+                  "responsible_party": "paypal",
+                  "conditions": ["unauthorized"],
+                }
+        )
+        self.assertEqual("paypal", liability_shift.responsible_party)
+        self.assertEqual(["unauthorized"], liability_shift.conditions)
diff --git a/tests/unit/test_payment_method_gateway.py b/tests/unit/test_payment_method_gateway.py
index 85cd75f..1e328fd 100644
--- a/tests/unit/test_payment_method_gateway.py
+++ b/tests/unit/test_payment_method_gateway.py
@@ -1,9 +1,6 @@
 from tests.test_helper import *
 from braintree.payment_method_gateway import PaymentMethodGateway
-if sys.version_info[0] == 2:
-    from mock import MagicMock
-else:
-    from unittest.mock import MagicMock
+from unittest.mock import MagicMock
 
 class TestPaymentMethodGateway(unittest.TestCase):
     def test_create_signature(self):
@@ -15,26 +12,26 @@ class TestPaymentMethodGateway(unittest.TestCase):
             "customer_id",
             "cvv",
             "device_data",
-            "device_session_id",
             "expiration_date",
             "expiration_month",
             "expiration_year",
             "number",
             "payment_method_nonce",
             "paypal_refresh_token",
-            "paypal_vault_without_upgrade",
             "token",
+            "device_session_id",
             {
                 "billing_address": Address.create_signature()},
             {
                 "options": [
                     "fail_on_duplicate_payment_method",
                     "make_default",
+                    "skip_advanced_fraud_checking",
                     "us_bank_account_verification_method",
+                    "verification_account_type",
+                    "verification_amount",
                     "verification_merchant_account_id",
                     "verify_card",
-                    "verification_amount",
-                    "verification_account_type",
                     {
                         "adyen":[
                             "overwrite_brand",
@@ -60,15 +57,25 @@ class TestPaymentMethodGateway(unittest.TestCase):
                                     "first_name",
                                     "last_name",
                                     "locality",
+                                    "phone_number",
                                     "postal_code",
                                     "region",
-                                    "street_address"
+                                    "street_address",
                                 ]
                             },
                         ]
                     },
                 ]
-            }
+            },
+            {
+                "three_d_secure_pass_thru": [
+                    "cavv",
+                    "ds_transaction_id",
+                    "eci_flag",
+                    "three_d_secure_version",
+                    "xid"
+                    ]
+            },
         ]
 
         self.assertEqual(expected_signature, actual_signature)
@@ -80,25 +87,26 @@ class TestPaymentMethodGateway(unittest.TestCase):
             "billing_address_id",
             "cardholder_name",
             "cvv",
-            "device_session_id",
+            "device_data",
             "expiration_date",
             "expiration_month",
             "expiration_year",
             "number",
+            "payment_method_nonce",
             "token",
             "venmo_sdk_payment_method_code",
-            "device_data",
+            "device_session_id",
             "fraud_merchant_id",
-            "payment_method_nonce",
             {
                 "options": [
                     "make_default",
+                    "skip_advanced_fraud_checking",
                     "us_bank_account_verification_method",
-                    "verify_card",
+                    "venmo_sdk_session",
+                    "verification_account_type",
                     "verification_amount",
                     "verification_merchant_account_id",
-                    "verification_account_type",
-                    "venmo_sdk_session",
+                    "verify_card",
                     {
                         "adyen":[
                             "overwrite_brand",
@@ -109,7 +117,16 @@ class TestPaymentMethodGateway(unittest.TestCase):
             },
             {
                 "billing_address" : Address.update_signature() + [{"options": ["update_existing"]}]
-            }
+            },
+            {
+                "three_d_secure_pass_thru": [
+                    "cavv",
+                    "ds_transaction_id",
+                    "eci_flag",
+                    "three_d_secure_version",
+                    "xid"
+                    ]
+            },
         ]
 
         self.assertEqual(expected_signature, actual_signature)
diff --git a/tests/unit/test_payment_method_nonce.py b/tests/unit/test_payment_method_nonce.py
index 873bc58..f5e8f38 100644
--- a/tests/unit/test_payment_method_nonce.py
+++ b/tests/unit/test_payment_method_nonce.py
@@ -1,10 +1,10 @@
 from tests.test_helper import *
 
 class TestPaymentMethodNonce(unittest.TestCase):
-    @raises(NotFoundError)
     def test_finding_empty_id_raises_not_found_exception(self):
-        PaymentMethodNonce.find(" ")
+        with self.assertRaises(NotFoundError):
+            PaymentMethodNonce.find(" ")
 
-    @raises(NotFoundError)
     def test_finding_None_id_raises_not_found_exception(self):
-        PaymentMethodNonce.find(None)
+        with self.assertRaises(NotFoundError):
+            PaymentMethodNonce.find(None)
diff --git a/tests/unit/test_payment_method_parser.py b/tests/unit/test_payment_method_parser.py
index 69399d7..e03e61f 100644
--- a/tests/unit/test_payment_method_parser.py
+++ b/tests/unit/test_payment_method_parser.py
@@ -1,9 +1,6 @@
 from tests.test_helper import *
 from braintree.payment_method_parser import parse_payment_method
-if sys.version_info[0] == 2:
-    from mock import MagicMock
-else:
-    from unittest.mock import MagicMock
+from unittest.mock import MagicMock
 
 class TestPaymentMethodParser(unittest.TestCase):
     def test_parse_response_returns_a_credit_card(self):
@@ -24,6 +21,15 @@ class TestPaymentMethodParser(unittest.TestCase):
         self.assertEqual("1234", paypal_account.token)
         self.assertFalse(paypal_account.default)
 
+    def test_parse_response_returns_a_sepa_direct_debit_account(self):
+        sdd_account = parse_payment_method(BraintreeGateway(None), {
+            "sepa_debit_account": {"token": "1234", "default": False}
+        })
+
+        self.assertEqual(SepaDirectDebitAccount, sdd_account.__class__)
+        self.assertEqual("1234", sdd_account.token)
+        self.assertFalse(sdd_account.default)
+
     def test_parse_response_returns_an_unknown_payment_method(self):
         unknown_payment_method = parse_payment_method(BraintreeGateway(None), {
             "new_fancy_payment_method": {
diff --git a/tests/unit/test_resource.py b/tests/unit/test_resource.py
index 0a85af1..0943035 100644
--- a/tests/unit/test_resource.py
+++ b/tests/unit/test_resource.py
@@ -16,7 +16,6 @@ class TestResource(unittest.TestCase):
         }
         Resource.verify_keys(params, signature)
 
-    @raises(KeyError)
     def test_verify_keys_escapes_brackets_in_signature(self):
         signature = [
             {"customer": [{"custom_fields": ["__any_key__"]}]}
@@ -24,7 +23,8 @@ class TestResource(unittest.TestCase):
         params = {
             "customer_id": "value",
         }
-        Resource.verify_keys(params, signature)
+        with self.assertRaises(KeyError):
+            Resource.verify_keys(params, signature)
 
     def test_verify_keys_works_with_array_param(self):
         signature = [
@@ -37,7 +37,6 @@ class TestResource(unittest.TestCase):
         }
         Resource.verify_keys(params, signature)
 
-    @raises(KeyError)
     def test_verify_keys_raises_on_bad_array_param(self):
         signature = [
             {"customer": ["one", "two"]}
@@ -47,7 +46,8 @@ class TestResource(unittest.TestCase):
                 "invalid": "foo"
             }
         }
-        Resource.verify_keys(params, signature)
+        with self.assertRaises(KeyError):
+            Resource.verify_keys(params, signature)
 
     def test_verify_keys_works_with_arrays(self):
         signature = [
@@ -65,7 +65,6 @@ class TestResource(unittest.TestCase):
         }
         Resource.verify_keys(params, signature)
 
-    @raises(KeyError)
     def test_verify_keys_raises_with_invalid_param_in_arrays(self):
         signature = [
             {"add_ons": [{"update": ["existing_id", "quantity"]}]}
@@ -80,7 +79,8 @@ class TestResource(unittest.TestCase):
                 ]
             }
         }
-        Resource.verify_keys(params, signature)
+        with self.assertRaises(KeyError):
+            Resource.verify_keys(params, signature)
 
     def test_verify_keys_allows_text(self):
         text_string = u"text_string"
diff --git a/tests/unit/test_resource_collection.py b/tests/unit/test_resource_collection.py
index e90c241..a63fa33 100644
--- a/tests/unit/test_resource_collection.py
+++ b/tests/unit/test_resource_collection.py
@@ -45,7 +45,7 @@ class TestResourceCollection(unittest.TestCase):
         collection = ResourceCollection("some_query", empty_collection_data, TestResourceCollection.TestResource.fetch)
         self.assertEqual(collection.ids, [])
 
-    @raises_with_regexp(UnexpectedError, "Unprocessable entity due to an invalid request")
     def test_no_search_results(self):
         bad_collection_data = {}
-        ResourceCollection("some_query", bad_collection_data, TestResourceCollection.TestResource.fetch)
+        with self.assertRaisesRegex(UnexpectedError, "Unprocessable entity due to an invalid request"):
+            ResourceCollection("some_query", bad_collection_data, TestResourceCollection.TestResource.fetch)
diff --git a/tests/unit/test_risk_data.py b/tests/unit/test_risk_data.py
index 0fa32ed..b9e8183 100644
--- a/tests/unit/test_risk_data.py
+++ b/tests/unit/test_risk_data.py
@@ -3,8 +3,26 @@ from braintree import *
 
 class TestRiskData(unittest.TestCase):
     def test_initialization_of_attributes(self):
-        risk_data = RiskData({"id": "123", "decision": "Unknown", "device_data_captured": True, "fraud_service_provider": "some_fraud_provider"})
+        risk_data = RiskData(
+                {
+                    "id": "123",
+                    "decision": "Unknown",
+                    "device_data_captured": True,
+                    "fraud_service_provider":
+                    "some_fraud_provider",
+                    "transaction_risk_score": "42",
+                    "decision_reasons": ["reason"],
+                    "liability_shift": {
+                          "responsible_party": "paypal",
+                          "conditions": ["unauthorized"],
+                        }
+                    }
+                )
         self.assertEqual("123", risk_data.id)
         self.assertEqual("Unknown", risk_data.decision)
         self.assertEqual(True, risk_data.device_data_captured)
         self.assertEqual("some_fraud_provider", risk_data.fraud_service_provider)
+        self.assertEqual("42", risk_data.transaction_risk_score)
+        self.assertEqual(["reason"], risk_data.decision_reasons)
+        self.assertEqual("paypal", risk_data.liability_shift.responsible_party)
+        self.assertEqual(["unauthorized"], risk_data.liability_shift.conditions)
diff --git a/tests/unit/test_search.py b/tests/unit/test_search.py
index 8ec5c2a..81a2d44 100644
--- a/tests/unit/test_search.py
+++ b/tests/unit/test_search.py
@@ -37,10 +37,10 @@ class TestSearch(unittest.TestCase):
         node = Search.MultipleValueNodeBuilder("name", ["okay"])
         self.assertEqual(["okay"], (node == "okay").to_param())
 
-    @raises(AttributeError)
     def test_multiple_value_node_with_value_not_in_whitelist(self):
-        node = Search.MultipleValueNodeBuilder("name", ["okay", "also okay"])
-        node == "not okay"
+        with self.assertRaises(AttributeError):
+            node = Search.MultipleValueNodeBuilder("name", ["okay", "also okay"])
+            node == "not okay"
 
     def test_multiple_value_or_text_node_is(self):
         node = Search.MultipleValueOrTextNodeBuilder("name")
@@ -74,10 +74,10 @@ class TestSearch(unittest.TestCase):
         node = Search.MultipleValueOrTextNodeBuilder("name", ["okay"])
         self.assertEqual(["okay"], node.in_list("okay").to_param())
 
-    @raises(AttributeError)
     def test_multiple_value_or_text_node_with_value_not_in_whitelist(self):
-        node = Search.MultipleValueOrTextNodeBuilder("name", ["okay"])
-        node.in_list("not okay").to_param()
+        with self.assertRaises(AttributeError):
+            node = Search.MultipleValueOrTextNodeBuilder("name", ["okay"])
+            node.in_list("not okay").to_param()
 
     def test_range_node_min_ge(self):
         node = Search.RangeNodeBuilder("name")
diff --git a/tests/unit/test_sepa_direct_debit_account.py b/tests/unit/test_sepa_direct_debit_account.py
new file mode 100644
index 0000000..24daafb
--- /dev/null
+++ b/tests/unit/test_sepa_direct_debit_account.py
@@ -0,0 +1,33 @@
+from tests.test_helper import *
+from datetime import date
+
+class TestSepaDirectDebitAccount(unittest.TestCase):
+    def test_constructor(self):
+        attributes = {
+            "bank_reference_token": "a-reference-token",
+            "created_at": date(2013, 4, 10),
+            "customer_global_id": "a-customer-global-id",
+            "global_id": "a-global-id",
+            "image_url": "a-image-url",
+            "last_4": "4321",
+            "mandate_type": "ONE_OFF",
+            "merchant_or_partner_customer_id": "a-mp-customer-id",
+            "subscriptions": [{"price": "10.00"}],
+            "updated_at": date(2013, 4, 10),
+            "view_mandate_url": "a-view-mandate-url",
+        }
+
+        sepa_direct_debit_account = SepaDirectDebitAccount({}, attributes)
+        self.assertEqual(sepa_direct_debit_account.bank_reference_token, "a-reference-token")
+        self.assertEqual(sepa_direct_debit_account.created_at, date(2013, 4, 10))
+        self.assertEqual(sepa_direct_debit_account.customer_global_id, "a-customer-global-id")
+        self.assertEqual(sepa_direct_debit_account.global_id, "a-global-id")
+        self.assertEqual(sepa_direct_debit_account.image_url, "a-image-url")
+        self.assertEqual(sepa_direct_debit_account.last_4, "4321")
+        self.assertEqual(sepa_direct_debit_account.mandate_type, "ONE_OFF")
+        self.assertEqual(sepa_direct_debit_account.merchant_or_partner_customer_id, "a-mp-customer-id")
+        subscription = sepa_direct_debit_account.subscriptions[0]
+        self.assertEqual(type(subscription), Subscription)
+        self.assertEqual(subscription.price, Decimal("10.00"))
+        self.assertEqual(sepa_direct_debit_account.updated_at, date(2013, 4, 10))
+        self.assertEqual(sepa_direct_debit_account.view_mandate_url, "a-view-mandate-url")
diff --git a/tests/unit/test_subscription.py b/tests/unit/test_subscription.py
index 0c14fb5..92b75ec 100644
--- a/tests/unit/test_subscription.py
+++ b/tests/unit/test_subscription.py
@@ -1,18 +1,18 @@
 from tests.test_helper import *
 
 class TestSubscription(unittest.TestCase):
-    @raises_with_regexp(KeyError, "'Invalid keys: bad_key'")
     def test_create_raises_exception_with_bad_keys(self):
-        Subscription.create({"bad_key": "value"})
+        with self.assertRaisesRegex(KeyError, "'Invalid keys: bad_key'"):
+            Subscription.create({"bad_key": "value"})
 
-    @raises_with_regexp(KeyError, "'Invalid keys: bad_key'")
     def test_update_raises_exception_with_bad_keys(self):
-        Subscription.update("id", {"bad_key": "value"})
+        with self.assertRaisesRegex(KeyError, "'Invalid keys: bad_key'"):
+            Subscription.update("id", {"bad_key": "value"})
 
-    @raises(NotFoundError)
     def test_finding_empty_id_raises_not_found_exception(self):
-        Subscription.find(" ")
+        with self.assertRaises(NotFoundError):
+            Subscription.find(" ")
 
-    @raises(NotFoundError)
     def test_finding_None_id_raises_not_found_exception(self):
-        Subscription.find(None)
+        with self.assertRaises(NotFoundError):
+            Subscription.find(None)
diff --git a/tests/unit/test_subscription_search.py b/tests/unit/test_subscription_search.py
index 31ec078..669c62e 100644
--- a/tests/unit/test_subscription_search.py
+++ b/tests/unit/test_subscription_search.py
@@ -36,14 +36,14 @@ class TestSubscriptionSearch(unittest.TestCase):
             Subscription.Status.PastDue
         )
 
-    @raises(AttributeError)
     def test_status_not_in_whitelist(self):
-        SubscriptionSearch.status.in_list(
-            Subscription.Status.Active,
-            Subscription.Status.Canceled,
-            Subscription.Status.Expired,
-            "not a status"
-        )
+        with self.assertRaises(AttributeError):
+            SubscriptionSearch.status.in_list(
+                Subscription.Status.Active,
+                Subscription.Status.Canceled,
+                Subscription.Status.Expired,
+                "not a status"
+            )
 
     def test_ids_is_a_multiple_value_node(self):
         self.assertEqual(Search.MultipleValueNodeBuilder, type(SubscriptionSearch.ids))
diff --git a/tests/unit/test_transaction.py b/tests/unit/test_transaction.py
index dfd7dba..9981625 100644
--- a/tests/unit/test_transaction.py
+++ b/tests/unit/test_transaction.py
@@ -3,35 +3,28 @@ from braintree.test.credit_card_numbers import CreditCardNumbers
 from datetime import datetime
 from datetime import date
 from braintree.authorization_adjustment import AuthorizationAdjustment
-if sys.version_info[0] == 2:
-    from mock import MagicMock
-else:
-    from unittest.mock import MagicMock
+from unittest.mock import MagicMock
 
 class TestTransaction(unittest.TestCase):
-    @raises_with_regexp(KeyError, "'Invalid keys: bad_key'")
     def test_clone_transaction_raises_exception_with_bad_keys(self):
-        Transaction.clone_transaction("an id", {"bad_key": "value"})
+        with self.assertRaisesRegex(KeyError, "'Invalid keys: bad_key'"):
+            Transaction.clone_transaction("an id", {"bad_key": "value"})
 
-    @raises_with_regexp(KeyError, "'Invalid keys: bad_key'")
     def test_sale_raises_exception_with_bad_keys(self):
-        Transaction.sale({"bad_key": "value"})
+        with self.assertRaisesRegex(KeyError, "'Invalid keys: bad_key'"):
+            Transaction.sale({"bad_key": "value"})
 
-    @raises_with_regexp(KeyError, "'Invalid keys: credit_card\[bad_key\]'")
     def test_sale_raises_exception_with_nested_bad_keys(self):
-        Transaction.sale({"credit_card": {"bad_key": "value"}})
+        with self.assertRaisesRegex(KeyError, "'Invalid keys: credit_card\[bad_key\]'"):
+            Transaction.sale({"credit_card": {"bad_key": "value"}})
 
-    @raises_with_regexp(KeyError, "'Invalid keys: bad_key'")
-    def test_tr_data_for_sale_raises_error_with_bad_keys(self):
-        Transaction.tr_data_for_sale({"bad_key": "value"}, "http://example.com")
-
-    @raises(NotFoundError)
     def test_finding_empty_id_raises_not_found_exception(self):
-        Transaction.find(" ")
+        with self.assertRaises(NotFoundError):
+            Transaction.find(" ")
 
-    @raises(NotFoundError)
     def test_finding_none_raises_not_found_exception(self):
-        Transaction.find(None)
+        with self.assertRaises(NotFoundError):
+            Transaction.find(None)
 
     def test_constructor_includes_disbursement_information(self):
         attributes = {
@@ -62,6 +55,16 @@ class TestTransaction(unittest.TestCase):
         self.assertEqual(transaction.disbursement_details.funds_held, False)
         self.assertEqual(transaction.is_disbursed, True)
 
+    def test_constructor_includes_sepa_direct_debit_return_code(self):
+        attributes = {
+            'amount': '27.00',
+            'sepa_direct_debit_return_code': 'AM04'
+        }
+
+        transaction = Transaction(None, attributes)
+
+        self.assertEqual(transaction.sepa_direct_debit_return_code, 'AM04')
+
     def test_transaction_handles_nil_risk_data(self):
         attributes = {
             'amount': '27.00',
@@ -159,26 +162,6 @@ class TestTransaction(unittest.TestCase):
         transaction_gateway._post = MagicMock(name='config.http.post')
         return transaction_gateway
 
-    def test_ideal_payment_details(self):
-        attributes = {
-            'amount': '27.00',
-            'tax_amount': '1.00',
-            'ideal_payment': {
-                'ideal_payment_id': 'idealpayment_abc_123',
-                'masked_iban': '12************7890',
-                'bic': 'RABONL2U',
-                'image_url': 'http://www.example.com/ideal.png',
-            },
-        }
-
-        transaction = Transaction(None, attributes)
-
-        self.assertEqual(transaction.ideal_payment_details.ideal_payment_id, 'idealpayment_abc_123')
-        self.assertEqual(transaction.ideal_payment_details.masked_iban, '12************7890')
-        self.assertEqual(transaction.ideal_payment_details.bic, 'RABONL2U')
-        self.assertEqual(transaction.ideal_payment_details.image_url, 'http://www.example.com/ideal.png')
-
-
     def test_constructor_doesnt_includes_auth_adjustments(self):
         attributes = {
             'amount': '27.00',
@@ -219,25 +202,37 @@ class TestTransaction(unittest.TestCase):
         self.assertEqual(transaction_adjustment.processor_response_code, "1000")
         self.assertEqual(transaction_adjustment.processor_response_text, "Approved")
 
-    def test_constructor_includes_network_transaction_id(self):
+    def test_constructor_includes_network_transaction_id_and_response_code_and_response_text(self):
         attributes = {
             'amount': '27.00',
             'tax_amount': '1.00',
-            'network_transaction_id': '123456789012345'
+            'network_transaction_id': '123456789012345',
+            'network_response_code': '00',
+            'network_response_text': 'Successful approval/completion or V.I.P. PIN verification is successful'
         }
 
         transaction = Transaction(None, attributes)
         self.assertEqual(transaction.network_transaction_id, "123456789012345")
+        self.assertEqual(transaction.network_response_code, "00")
+        self.assertEqual(transaction.network_response_text, "Successful approval/completion or V.I.P. PIN verification is successful")
 
-    def test_constructor_includes_network_transaction_id(self):
+    def test_constructor_includes_installment_count(self):
         attributes = {
             'amount': '27.00',
             'tax_amount': '1.00',
-            'network_response_code': '00',
-            'network_response_text': 'Successful approval/completion or V.I.P. PIN verification is successful'
+            'installments': {
+                'count': 4
+            }
         }
 
         transaction = Transaction(None, attributes)
-        self.assertEqual(transaction.network_response_code, "00")
-        self.assertEqual(transaction.network_response_text, "Successful approval/completion or V.I.P. PIN verification is successful")
+        self.assertEqual(transaction.installments["count"], 4)
+
+    def test_gateway_rejection_reason_for_excessive_retry(self):
+        attributes = {
+            'amount': '27.00',
+            'gateway_rejection_reason': 'excessive_retry'
+        }
 
+        transaction = Transaction(None, attributes)
+        self.assertEqual(transaction.gateway_rejection_reason, braintree.Transaction.GatewayRejectionReason.ExcessiveRetry)
diff --git a/tests/unit/test_transparent_redirect.py b/tests/unit/test_transparent_redirect.py
deleted file mode 100644
index ba84fc9..0000000
--- a/tests/unit/test_transparent_redirect.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from tests.test_helper import *
-
-class TestTransparentRedirect(unittest.TestCase):
-    def test_tr_data(self):
-        data = TransparentRedirect.tr_data({"key": "val"}, "http://example.com/path?foo=bar")
-        self.__assert_valid_tr_data(data)
-
-    def __assert_valid_tr_data(self, data):
-        test_hash, content = data.split("|", 1)
-        self.assertEqual(test_hash, Crypto.sha1_hmac_hash(Configuration.private_key, content))
-
-    @raises(ForgedQueryStringError)
-    def test_parse_and_validate_query_string_raises_for_invalid_hash(self):
-        Configuration.gateway().transparent_redirect._parse_and_validate_query_string(
-            "http_status=200&id=7kdj469tw7yck32j&hash=99c9ff20cd7910a1c1e793ff9e3b2d15586dc6b9"
-        )
-
-    @raises(AuthenticationError)
-    def test_parse_and_validate_query_string_raises_for_http_status_401(self):
-        Configuration.gateway().transparent_redirect._parse_and_validate_query_string(
-            "http_status=401&id=6kdj469tw7yck32j&hash=5a26e3cde5ebedb0ec1ba8d35724360334fbf419"
-        )
-
-    @raises(AuthorizationError)
-    def test_parse_and_validate_query_string_raises_for_http_status_403(self):
-        Configuration.gateway().transparent_redirect._parse_and_validate_query_string(
-            "http_status=403&id=6kdj469tw7yck32j&hash=126d5130b71a4907e460fad23876ed70dd41dcd2"
-        )
-
-    @raises(NotFoundError)
-    def test_parse_and_validate_query_string_raises_for_http_status_404(self):
-        Configuration.gateway().transparent_redirect._parse_and_validate_query_string(
-            "http_status=404&id=6kdj469tw7yck32j&hash=0d3724a45cf1cda5524aa68f1f28899d34d2ff3a"
-        )
-
-    @raises(ServerError)
-    def test_parse_and_validate_query_string_raises_for_http_status_500(self):
-        Configuration.gateway().transparent_redirect._parse_and_validate_query_string(
-            "http_status=500&id=6kdj469tw7yck32j&hash=a839a44ca69d59a3d6f639c294794989676632dc"
-        )
-
-    @raises(DownForMaintenanceError)
-    def test_parse_and_validate_query_string_raises_for_http_status_503(self):
-        Configuration.gateway().transparent_redirect._parse_and_validate_query_string(
-            "http_status=503&id=6kdj469tw7yck32j&hash=1b3d29199a282e63074a7823b76bccacdf732da6"
-        )
-
-    @raises(UnexpectedError)
-    def test_parse_and_validate_query_string_raises_for_unexpected_http_status(self):
-        Configuration.gateway().transparent_redirect._parse_and_validate_query_string(
-            "http_status=600&id=6kdj469tw7yck32j&hash=740633356f93384167d887de0c1d9745e3de8fb6"
-        )
-
-    def test_api_version(self):
-        data = TransparentRedirect.tr_data({"key": "val"}, "http://example.com/path?foo=bar")
-        self.assertTrue("api_version=5" in data)
diff --git a/tests/unit/test_us_bank_account_verification.py b/tests/unit/test_us_bank_account_verification.py
index f51bdc7..b85be98 100644
--- a/tests/unit/test_us_bank_account_verification.py
+++ b/tests/unit/test_us_bank_account_verification.py
@@ -4,13 +4,13 @@ from tests.test_helper import *
 from braintree.us_bank_account_verification import UsBankAccountVerification
 
 class TestUsBankAccountVerification(unittest.TestCase):
-    @raises(NotFoundError)
     def test_finding_empty_id_raises_not_found_exception(self):
-        UsBankAccountVerification.find(" ")
+        with self.assertRaises(NotFoundError):
+            UsBankAccountVerification.find(" ")
 
-    @raises(NotFoundError)
     def test_finding_none_raises_not_found_exception(self):
-        UsBankAccountVerification.find(None)
+        with self.assertRaises(NotFoundError):
+            UsBankAccountVerification.find(None)
 
     def test_attributes(self):
         attributes = {
diff --git a/tests/unit/test_webhooks.py b/tests/unit/test_webhooks.py
index 943bec7..97750b2 100644
--- a/tests/unit/test_webhooks.py
+++ b/tests/unit/test_webhooks.py
@@ -6,6 +6,20 @@ from braintree.paypal_account import PayPalAccount
 from braintree.venmo_account import VenmoAccount
 
 class TestWebhooks(unittest.TestCase):
+    def test_granted_payment_method_revoked(self):
+        webhook_testing_gateway = WebhookTestingGateway(BraintreeGateway(Configuration.instantiate()))
+
+        sample_notification = webhook_testing_gateway.sample_notification(WebhookNotification.Kind.GrantedPaymentMethodRevoked, 'granted_payment_method_revoked_id')
+
+        notification = WebhookNotification.parse(sample_notification['bt_signature'], sample_notification['bt_payload'])
+
+        metadata = notification.revoked_payment_method_metadata
+
+        self.assertEqual(WebhookNotification.Kind.GrantedPaymentMethodRevoked, notification.kind)
+        self.assertEqual("venmo_customer_id", metadata.customer_id)
+        self.assertEqual("granted_payment_method_revoked_id", metadata.token)
+        self.assertEqual(type(metadata.revoked_payment_method), VenmoAccount)
+
     def test_sample_notification_builds_a_parsable_notification(self):
         sample_notification = WebhookTesting.sample_notification(
             WebhookNotification.Kind.SubscriptionWentPastDue,
@@ -30,14 +44,13 @@ class TestWebhooks(unittest.TestCase):
 
         self.assertEqual('my_source_merchant_id', notification.source_merchant_id)
 
-    @raises(InvalidSignatureError)
     def test_completely_invalid_signature(self):
         sample_notification = WebhookTesting.sample_notification(
             WebhookNotification.Kind.SubscriptionWentPastDue,
             "my_id"
         )
-
-        WebhookNotification.parse("bad_stuff", sample_notification['bt_payload'])
+        with self.assertRaises(InvalidSignatureError):
+            WebhookNotification.parse("bad_stuff", sample_notification['bt_payload'])
 
     def test_parse_raises_when_public_key_is_wrong(self):
         sample_notification = WebhookTesting.sample_notification(
@@ -180,6 +193,21 @@ class TestWebhooks(unittest.TestCase):
         self.assertEqual(100, notification.transaction.amount)
         self.assertEqual(datetime(2013, 7, 9, 18, 23, 29), notification.transaction.disbursement_details.disbursement_date)
 
+    def test_builds_notification_for_reviewed_transactions(self):
+        sample_notification = WebhookTesting.sample_notification(
+            WebhookNotification.Kind.TransactionReviewed,
+            "my_id"
+        )
+
+        notification = WebhookNotification.parse(sample_notification['bt_signature'], sample_notification['bt_payload'])
+
+        self.assertEqual(WebhookNotification.Kind.TransactionReviewed, notification.kind)
+        self.assertEqual("my_id", notification.transaction_review.transaction_id)
+        self.assertEqual("a smart decision", notification.transaction_review.decision)
+        self.assertEqual("hey@girl.com", notification.transaction_review.reviewer_email)
+        self.assertEqual("I reviewed this", notification.transaction_review.reviewer_note)
+        self.assertEqual(datetime(2021, 4, 20, 6, 9, 0), notification.transaction_review.reviewed_time)
+
     def test_builds_notification_for_settled_transactions(self):
         sample_notification = WebhookTesting.sample_notification(
             WebhookNotification.Kind.TransactionSettled,
@@ -287,6 +315,58 @@ class TestWebhooks(unittest.TestCase):
         self.assertEqual(notification.dispute.date_opened, date(2014, 3, 28))
         self.assertEqual(notification.dispute.date_won, date(2014, 9, 1))
 
+    def test_builds_notification_for_old_dispute_accepted(self):
+        sample_notification = WebhookTesting.sample_notification(
+            WebhookNotification.Kind.DisputeAccepted,
+            "legacy_dispute_id"
+        )
+
+        notification = WebhookNotification.parse(sample_notification['bt_signature'], sample_notification['bt_payload'])
+
+        self.assertEqual(WebhookNotification.Kind.DisputeAccepted, notification.kind)
+        self.assertEqual("legacy_dispute_id", notification.dispute.id)
+        self.assertEqual(Dispute.Status.Accepted, notification.dispute.status)
+        self.assertEqual(Dispute.Kind.Chargeback, notification.dispute.kind)
+
+    def test_builds_notification_for_old_dispute_auto_accepted(self):
+        sample_notification = WebhookTesting.sample_notification(
+            WebhookNotification.Kind.DisputeAutoAccepted,
+            "legacy_dispute_id"
+        )
+
+        notification = WebhookNotification.parse(sample_notification['bt_signature'], sample_notification['bt_payload'])
+
+        self.assertEqual(WebhookNotification.Kind.DisputeAutoAccepted, notification.kind)
+        self.assertEqual("legacy_dispute_id", notification.dispute.id)
+        self.assertEqual(Dispute.Status.AutoAccepted, notification.dispute.status)
+        self.assertEqual(Dispute.Kind.Chargeback, notification.dispute.kind)
+
+    def test_builds_notification_for_old_dispute_disputed(self):
+        sample_notification = WebhookTesting.sample_notification(
+            WebhookNotification.Kind.DisputeDisputed,
+            "legacy_dispute_id"
+        )
+
+        notification = WebhookNotification.parse(sample_notification['bt_signature'], sample_notification['bt_payload'])
+
+        self.assertEqual(WebhookNotification.Kind.DisputeDisputed, notification.kind)
+        self.assertEqual("legacy_dispute_id", notification.dispute.id)
+        self.assertEqual(Dispute.Status.Disputed, notification.dispute.status)
+        self.assertEqual(Dispute.Kind.Chargeback, notification.dispute.kind)
+
+    def test_builds_notification_for_old_dispute_expired(self):
+        sample_notification = WebhookTesting.sample_notification(
+            WebhookNotification.Kind.DisputeExpired,
+            "legacy_dispute_id"
+        )
+
+        notification = WebhookNotification.parse(sample_notification['bt_signature'], sample_notification['bt_payload'])
+
+        self.assertEqual(WebhookNotification.Kind.DisputeExpired, notification.kind)
+        self.assertEqual("legacy_dispute_id", notification.dispute.id)
+        self.assertEqual(Dispute.Status.Expired, notification.dispute.status)
+        self.assertEqual(Dispute.Kind.Chargeback, notification.dispute.kind)
+
     def test_builds_notification_for_new_dispute_opened(self):
         sample_notification = WebhookTesting.sample_notification(
             WebhookNotification.Kind.DisputeOpened,
@@ -330,6 +410,58 @@ class TestWebhooks(unittest.TestCase):
         self.assertEqual(notification.dispute.date_opened, date(2014, 3, 28))
         self.assertEqual(notification.dispute.date_won, date(2014, 9, 1))
 
+    def test_builds_notification_for_new_dispute_accepted(self):
+        sample_notification = WebhookTesting.sample_notification(
+            WebhookNotification.Kind.DisputeAccepted,
+            "my_id"
+        )
+
+        notification = WebhookNotification.parse(sample_notification['bt_signature'], sample_notification['bt_payload'])
+
+        self.assertEqual(WebhookNotification.Kind.DisputeAccepted, notification.kind)
+        self.assertEqual("my_id", notification.dispute.id)
+        self.assertEqual(Dispute.Status.Accepted, notification.dispute.status)
+        self.assertEqual(Dispute.Kind.Chargeback, notification.dispute.kind)
+
+    def test_builds_notification_for_new_dispute_auto_accepted(self):
+        sample_notification = WebhookTesting.sample_notification(
+            WebhookNotification.Kind.DisputeAutoAccepted,
+            "my_id"
+        )
+
+        notification = WebhookNotification.parse(sample_notification['bt_signature'], sample_notification['bt_payload'])
+
+        self.assertEqual(WebhookNotification.Kind.DisputeAutoAccepted, notification.kind)
+        self.assertEqual("my_id", notification.dispute.id)
+        self.assertEqual(Dispute.Status.AutoAccepted, notification.dispute.status)
+        self.assertEqual(Dispute.Kind.Chargeback, notification.dispute.kind)
+
+    def test_builds_notification_for_new_dispute_disputed(self):
+        sample_notification = WebhookTesting.sample_notification(
+            WebhookNotification.Kind.DisputeDisputed,
+            "my_id"
+        )
+
+        notification = WebhookNotification.parse(sample_notification['bt_signature'], sample_notification['bt_payload'])
+
+        self.assertEqual(WebhookNotification.Kind.DisputeDisputed, notification.kind)
+        self.assertEqual("my_id", notification.dispute.id)
+        self.assertEqual(Dispute.Status.Disputed, notification.dispute.status)
+        self.assertEqual(Dispute.Kind.Chargeback, notification.dispute.kind)
+
+    def test_builds_notification_for_new_dispute_expired(self):
+        sample_notification = WebhookTesting.sample_notification(
+            WebhookNotification.Kind.DisputeExpired,
+            "my_id"
+        )
+
+        notification = WebhookNotification.parse(sample_notification['bt_signature'], sample_notification['bt_payload'])
+
+        self.assertEqual(WebhookNotification.Kind.DisputeExpired, notification.kind)
+        self.assertEqual("my_id", notification.dispute.id)
+        self.assertEqual(Dispute.Status.Expired, notification.dispute.status)
+        self.assertEqual(Dispute.Kind.Chargeback, notification.dispute.kind)
+
     def test_builds_notification_for_partner_merchant_connected(self):
         sample_notification = WebhookTesting.sample_notification(
             WebhookNotification.Kind.PartnerMerchantConnected,
@@ -643,3 +775,68 @@ class TestWebhooks(unittest.TestCase):
         self.assertEqual("ee257d98-de40-47e8-96b3-a6954ea7a9a4", local_payment_completed.payment_method_nonce)
         self.assertTrue(isinstance(local_payment_completed.transaction, Transaction))
 
+    def test_local_payment_expired_webhook(self):
+        sample_notification = WebhookTesting.sample_notification(
+            WebhookNotification.Kind.LocalPaymentExpired,
+            "my_id"
+        )
+
+        notification = WebhookNotification.parse(sample_notification["bt_signature"], sample_notification["bt_payload"])
+        local_payment_expired = notification.local_payment_expired
+
+        self.assertEqual(WebhookNotification.Kind.LocalPaymentExpired, notification.kind)
+        self.assertEqual("a-payment-id", local_payment_expired.payment_id)
+        self.assertEqual("a-context-payment-id", local_payment_expired.payment_context_id)
+
+    def test_local_payment_funded_webhook(self):
+        sample_notification = WebhookTesting.sample_notification(
+            WebhookNotification.Kind.LocalPaymentFunded,
+            "my_id"
+        )
+
+        notification = WebhookNotification.parse(sample_notification["bt_signature"], sample_notification["bt_payload"])
+        local_payment_funded = notification.local_payment_funded
+
+        self.assertEqual(WebhookNotification.Kind.LocalPaymentFunded, notification.kind)
+        self.assertEqual("a-payment-id", local_payment_funded.payment_id)
+        self.assertEqual("a-context-payment-id", local_payment_funded.payment_context_id)
+        self.assertTrue(isinstance(local_payment_funded.transaction, Transaction))
+        self.assertEqual("1", local_payment_funded.transaction.id)
+        self.assertEqual("settled", local_payment_funded.transaction.status)
+        self.assertEqual("order1234", local_payment_funded.transaction.order_id)
+
+    def test_local_payment_reversed_webhook(self):
+        sample_notification = WebhookTesting.sample_notification(
+            WebhookNotification.Kind.LocalPaymentReversed,
+            "my_id"
+        )
+
+        notification = WebhookNotification.parse(sample_notification["bt_signature"], sample_notification["bt_payload"])
+        local_payment_reversed = notification.local_payment_reversed
+
+        self.assertEqual(WebhookNotification.Kind.LocalPaymentReversed, notification.kind)
+        self.assertEqual("a-payment-id", local_payment_reversed.payment_id)
+
+    def test_payment_method_customer_data_updated_webhook(self):
+        sample_notification = WebhookTesting.sample_notification(
+            WebhookNotification.Kind.PaymentMethodCustomerDataUpdated,
+            "my_id"
+        )
+
+        notification = WebhookNotification.parse(sample_notification["bt_signature"], sample_notification["bt_payload"])
+        payment_method_customer_data_updated = notification.payment_method_customer_data_updated_metadata
+
+        self.assertEqual(WebhookNotification.Kind.PaymentMethodCustomerDataUpdated, notification.kind)
+
+        self.assertEqual(payment_method_customer_data_updated.token, "TOKEN-12345")
+        self.assertEqual(payment_method_customer_data_updated.datetime_updated, "2022-01-01T21:28:37Z")
+
+        enriched_customer_data = payment_method_customer_data_updated.enriched_customer_data
+        self.assertEqual(enriched_customer_data.fields_updated, ["username"])
+
+        profile_data = enriched_customer_data.profile_data
+        self.assertEqual(profile_data.first_name, "John")
+        self.assertEqual(profile_data.last_name, "Doe")
+        self.assertEqual(profile_data.username, "venmo_username")
+        self.assertEqual(profile_data.phone_number, "1231231234")
+        self.assertEqual(profile_data.email, "john.doe@paypal.com")

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree-4.19.0.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree-4.19.0.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree-4.19.0.egg-info/not-zip-safe
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree-4.19.0.egg-info/requires.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree-4.19.0.egg-info/top_level.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/apple_pay_gateway.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/apple_pay_options.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/dispute_details/paypal_message.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/enriched_customer_data.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/exceptions/gateway_timeout_error.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/exceptions/request_timeout_error.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/exceptions/service_unavailable_error.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/exchange_rate_quote.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/exchange_rate_quote_gateway.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/exchange_rate_quote_input.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/exchange_rate_quote_payload.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/exchange_rate_quote_request.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/liability_shift.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/local_payment_expired.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/local_payment_funded.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/local_payment_reversed.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/montary_amount.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/payment_method_customer_data_updated_metadata.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/sepa_direct_debit_account.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/sepa_direct_debit_account_gateway.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/test/authentication_ids.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/transaction_review.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/venmo_profile_data.py

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree-3.57.1.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree-3.57.1.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree-3.57.1.egg-info/not-zip-safe
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree-3.57.1.egg-info/requires.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree-3.57.1.egg-info/top_level.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/coinbase_account.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/exceptions/down_for_maintenance_error.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/exceptions/forged_query_string_error.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/ideal_payment.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/ideal_payment_gateway.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/transparent_redirect.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/braintree/transparent_redirect_gateway.py

No differences were encountered in the control files

More details

Full run details