Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

2.12.0

  • Loading branch information...
commit 89001774e45763f4ae2d55b5d9d43e92d21bcb6d 1 parent 5884a6e
@braintreeps braintreeps authored
View
5 CHANGELOG.md
@@ -1,3 +1,8 @@
+## 2.12.0
+
+* Added ability to retrieve all Plans, AddOns, and Discounts
+* Added Transaction cloning
+
## 2.11.0
* Added SettlementBatchSummary
View
4 braintree/__init__.py
@@ -1,4 +1,5 @@
from add_on import AddOn
+from add_on_gateway import AddOnGateway
from address import Address
from address_gateway import AddressGateway
from braintree_gateway import BraintreeGateway
@@ -10,11 +11,14 @@
from customer_search import CustomerSearch
from customer_gateway import CustomerGateway
from discount import Discount
+from discount_gateway import DiscountGateway
from descriptor import Descriptor
from error_codes import ErrorCodes
from error_result import ErrorResult
from errors import Errors
from environment import Environment
+from plan import Plan
+from plan_gateway import PlanGateway
from resource_collection import ResourceCollection
from search import Search
from settlement_batch_summary import SettlementBatchSummary
View
6 braintree/add_on.py
@@ -1,4 +1,8 @@
+from braintree.configuration import Configuration
from braintree.modification import Modification
class AddOn(Modification):
- pass
+
+ @staticmethod
+ def all():
+ return Configuration.gateway().add_on.all()
View
13 braintree/add_on_gateway.py
@@ -0,0 +1,13 @@
+import braintree
+from braintree.add_on import AddOn
+from braintree.resource_collection import ResourceCollection
+
+class AddOnGateway(object):
+ def __init__(self, gateway):
+ self.gateway = gateway
+ self.config = gateway.config
+
+ def all(self):
+ response = self.config.http().get("/add_ons/")
+ add_ons = {"add_on": response["add_ons"]}
+ return [AddOn(self.gateway, item) for item in ResourceCollection._extract_as_array(add_ons, "add_on")]
View
6 braintree/braintree_gateway.py
@@ -1,6 +1,9 @@
+from braintree.add_on_gateway import AddOnGateway
from braintree.address_gateway import AddressGateway
from braintree.credit_card_gateway import CreditCardGateway
from braintree.customer_gateway import CustomerGateway
+from braintree.discount_gateway import DiscountGateway
+from braintree.plan_gateway import PlanGateway
from braintree.settlement_batch_summary_gateway import SettlementBatchSummaryGateway
from braintree.subscription_gateway import SubscriptionGateway
from braintree.transaction_gateway import TransactionGateway
@@ -9,9 +12,12 @@
class BraintreeGateway(object):
def __init__(self, config):
self.config = config
+ self.add_on = AddOnGateway(self)
self.address = AddressGateway(self)
self.credit_card = CreditCardGateway(self)
self.customer = CustomerGateway(self)
+ self.discount = DiscountGateway(self)
+ self.plan = PlanGateway(self)
self.settlement_batch_summary = SettlementBatchSummaryGateway(self)
self.subscription = SubscriptionGateway(self)
self.transaction = TransactionGateway(self)
View
7 braintree/discount.py
@@ -1,4 +1,9 @@
from braintree.modification import Modification
+from braintree.configuration import Configuration
+
class Discount(Modification):
- pass
+
+ @staticmethod
+ def all():
+ return Configuration.gateway().discount.all()
View
13 braintree/discount_gateway.py
@@ -0,0 +1,13 @@
+import braintree
+from braintree.discount import Discount
+from braintree.resource_collection import ResourceCollection
+
+class DiscountGateway(object):
+ def __init__(self, gateway):
+ self.gateway = gateway
+ self.config = gateway.config
+
+ def all(self):
+ response = self.config.http().get("/discounts/")
+ discounts = {"discount": response["discounts"]}
+ return [Discount(self.gateway, item) for item in ResourceCollection._extract_as_array(discounts, "discount")]
View
7 braintree/error_codes.py
@@ -154,6 +154,10 @@ class Transaction(object):
AmountMustBeGreaterThanZero = "81531"
BillingAddressConflict = "91530"
CannotBeVoided = "91504"
+ CannotCloneCredit = "91543"
+ CannotCloneTransactionWithVaultCreditCard = "91540"
+ CannotCloneUnsuccessfulTransaction = "91542"
+ CannotCloneVoiceAuthorizations = "91541"
CannotRefundCredit = "91505"
CannotRefundUnlessSettled = "91506"
CannotRefundWithSuspendedMerchantAccount = "91538"
@@ -177,6 +181,8 @@ class Transaction(object):
PaymentMethodTokenIsInvalid = "91518"
ProcessorAuthorizationCodeCannotBeSet = "91519"
ProcessorAuthorizationCodeIsInvalid = "81520"
+ ProcessorDoesNotSupportCredits = "91546"
+ ProcessorDoesNotSupportVoiceAuthorizations = "91545"
PurchaseOrderNumberIsTooLong = "91537"
RefundAmountIsTooLarge = "91521"
SettlementAmountIsTooLarge = "91522"
@@ -192,3 +198,4 @@ class Transaction(object):
class Options(object):
VaultIsDisabled = "91525"
+ SubmitForSettlementIsRequiredForCloning = "91544"
View
21 braintree/plan.py
@@ -0,0 +1,21 @@
+from braintree.util.http import Http
+import braintree
+from braintree.add_on import AddOn
+from braintree.configuration import Configuration
+from braintree.discount import Discount
+from braintree.resource_collection import ResourceCollection
+from braintree.resource import Resource
+
+class Plan(Resource):
+
+ def __init__(self, gateway, attributes):
+ Resource.__init__(self, gateway, attributes)
+ if "add_ons" in attributes:
+ self.add_ons = [AddOn(gateway, add_on) for add_on in self.add_ons]
+ if "discounts" in attributes:
+ self.discounts = [Discount(gateway, discount) for discount in self.discounts]
+
+ @staticmethod
+ def all():
+ return Configuration.gateway().plan.all()
+
View
18 braintree/plan_gateway.py
@@ -0,0 +1,18 @@
+import re
+import braintree
+from braintree.plan import Plan
+from braintree.error_result import ErrorResult
+from braintree.exceptions.not_found_error import NotFoundError
+from braintree.resource import Resource
+from braintree.resource_collection import ResourceCollection
+from braintree.successful_result import SuccessfulResult
+
+class PlanGateway(object):
+ def __init__(self, gateway):
+ self.gateway = gateway
+ self.config = gateway.config
+
+ def all(self):
+ response = self.config.http().get("/plans/")
+ plans = {"plan": response["plans"]}
+ return [Plan(self.gateway, item) for item in ResourceCollection._extract_as_array(plans, "plan")]
View
8 braintree/transaction.py
@@ -139,6 +139,10 @@ class Type(object):
Sale = "sale"
@staticmethod
+ def clone_transaction(transaction_id, params):
+ return Configuration.gateway().transaction.clone_transaction(transaction_id, params)
+
+ @staticmethod
def confirm_transparent_redirect(query_string):
"""
Confirms a transparent redirect request. It expects the query string from the
@@ -306,6 +310,10 @@ def create(params):
return Configuration.gateway().transaction.create(params)
@staticmethod
+ def clone_signature():
+ return ["amount", {"options": ["submit_for_settlement"]}]
+
+ @staticmethod
def create_signature():
return [
"amount", "customer_id", "merchant_account_id", "order_id", "payment_method_token", "purchase_order_number", "shipping_address_id", "tax_amount", "tax_exempt", "type",
View
4 braintree/transaction_gateway.py
@@ -12,6 +12,10 @@ def __init__(self, gateway):
self.gateway = gateway
self.config = gateway.config
+ def clone_transaction(self, transaction_id, params):
+ Resource.verify_keys(params, Transaction.clone_signature())
+ return self._post("/transactions/" + transaction_id + "/clone", {"transaction-clone": params})
+
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})
View
2  braintree/version.py
@@ -1 +1 @@
-Version = "2.11.0"
+Version = "2.12.0"
View
33 tests/integration/test_add_ons.py
@@ -0,0 +1,33 @@
+from tests.test_helper import *
+
+class TestAddOn(unittest.TestCase):
+
+ def test_all_returns_all_add_ons(self):
+ new_id = str(random.randint(1, 1000000))
+ attributes = {
+ "amount": "100.00",
+ "description": "some description",
+ "id": new_id,
+ "kind": "add_on",
+ "name": "python_add_on",
+ "never_expires": False,
+ "number_of_billing_cycles": 1
+ }
+
+ Configuration.instantiate().http().post("/modifications/create_modification_for_tests", {"modification": attributes})
+
+ add_ons = AddOn.all()
+
+ for add_on in add_ons:
+ if add_on.id == new_id:
+ actual_add_on = add_on
+
+ self.assertNotEquals(None, actual_add_on)
+
+ self.assertEquals(attributes["amount"], "100.00")
+ self.assertEquals(attributes["description"], "some description")
+ self.assertEquals(attributes["id"], new_id)
+ self.assertEquals(attributes["kind"], "add_on")
+ self.assertEquals(attributes["name"], "python_add_on")
+ self.assertEquals(attributes["never_expires"], False)
+ self.assertEquals(attributes["number_of_billing_cycles"], 1)
View
33 tests/integration/test_discounts.py
@@ -0,0 +1,33 @@
+from tests.test_helper import *
+
+class TestDiscounts(unittest.TestCase):
+
+ def test_all_returns_all_discounts(self):
+ new_id = str(random.randint(1, 1000000))
+ attributes = {
+ "amount": "100.00",
+ "description": "some description",
+ "id": new_id,
+ "kind": "discount",
+ "name": "python_discount",
+ "never_expires": False,
+ "number_of_billing_cycles": 1
+ }
+
+ Configuration.instantiate().http().post("/modifications/create_modification_for_tests", {"modification": attributes})
+
+ discounts = Discount.all()
+
+ for discount in discounts:
+ if discount.id == new_id:
+ actual_discount = discount
+
+ self.assertNotEquals(None, actual_discount)
+
+ self.assertEquals(attributes["amount"], "100.00")
+ self.assertEquals(attributes["description"], "some description")
+ self.assertEquals(attributes["id"], new_id)
+ self.assertEquals(attributes["kind"], "discount")
+ self.assertEquals(attributes["name"], "python_discount")
+ self.assertEquals(attributes["never_expires"], False)
+ self.assertEquals(attributes["number_of_billing_cycles"], 1)
View
2  tests/integration/test_http.py
@@ -38,6 +38,8 @@ def test_unsuccessful_connection_to_good_ssl_server_with_wrong_cert(self):
error_code, error_msg = e
self.assertEquals(pycurl.E_SSL_CACERT, error_code)
self.assertTrue(re.search('verif(y|ication) failed', error_msg))
+ except AuthenticationError:
+ self.fail("Expected to Receive an SSL error from pycurl, but received an Authentication Error instead, check your local openssl installation")
def test_unsuccessful_connection_to_ssl_server_with_wrong_domain(self):
try:
View
66 tests/integration/test_plan.py
@@ -0,0 +1,66 @@
+from tests.test_helper import *
+
+class TestPlan(unittest.TestCase):
+
+ def test_all_returns_all_the_plans(self):
+ plan_token = str(random.randint(1, 1000000))
+ attributes = {
+ "id": plan_token,
+ "billing_day_of_month": 1,
+ "billing_frequency": 1,
+ "currency_iso_code": "USD",
+ "description": "some description",
+ "name": "python test plan",
+ "number_of_billing_cycles": 1,
+ "price": "1.00",
+ "trial_duration": 3,
+ "trial_duration_unit": "day",
+ "trial_period": True,
+ }
+
+ Configuration.instantiate().http().post("/plans/create_plan_for_tests", {"plan": attributes})
+
+ add_on_attributes = {
+ "amount": "100.00",
+ "description": "some description",
+ "plan_id": plan_token,
+ "kind": "add_on",
+ "name": "python_add_on",
+ "never_expires": False,
+ "number_of_billing_cycles": 1
+ }
+
+ Configuration.instantiate().http().post("/modifications/create_modification_for_tests", {"modification": add_on_attributes})
+ discount_attributes = {
+ "amount": "100.00",
+ "description": "some description",
+ "plan_id": plan_token,
+ "kind": "discount",
+ "name": "python_discount",
+ "never_expires": False,
+ "number_of_billing_cycles": 1
+ }
+
+ Configuration.instantiate().http().post("/modifications/create_modification_for_tests", {"modification": discount_attributes})
+
+ plans = Plan.all()
+
+ for plan in plans:
+ if plan.id == plan_token:
+ actual_plan = plan
+
+ self.assertNotEquals(None, actual_plan)
+
+ self.assertEquals(attributes["billing_day_of_month"], 1)
+ self.assertEquals(attributes["billing_frequency"], 1)
+ self.assertEquals(attributes["currency_iso_code"], "USD")
+ self.assertEquals(attributes["description"], "some description")
+ self.assertEquals(attributes["name"], "python test plan")
+ self.assertEquals(attributes["number_of_billing_cycles"], 1)
+ self.assertEquals(attributes["price"], "1.00")
+ self.assertEquals(attributes["trial_duration"], 3)
+ self.assertEquals(attributes["trial_duration_unit"], "day")
+ self.assertEquals(attributes["trial_period"], True)
+
+ self.assertEquals(add_on_attributes["name"], actual_plan.add_ons[0].name)
+ self.assertEquals(discount_attributes["name"], actual_plan.discounts[0].name)
View
4 tests/integration/test_settlement_batch_summary.py
@@ -30,7 +30,7 @@ def test_generate_returns_transactions_settled_on_a_given_day(self):
transaction = result.transaction
TestHelper.settle_transaction(transaction.id)
- result = SettlementBatchSummary.generate(datetime.today().strftime("%Y-%m-%d"))
+ result = SettlementBatchSummary.generate(TestHelper.now_in_eastern())
self.assertTrue(result.is_success)
visa_records = [row for row in result.settlement_batch_summary.records if row['card_type'] == 'Visa'][0]
@@ -54,7 +54,7 @@ def test_generate_can_be_grouped_by_a_custom_field(self):
transaction = result.transaction
TestHelper.settle_transaction(transaction.id)
- result = SettlementBatchSummary.generate(datetime.today().strftime("%Y-%m-%d"), 'store_me')
+ result = SettlementBatchSummary.generate(TestHelper.now_in_eastern(), 'store_me')
self.assertTrue(result.is_success)
self.assertTrue('store_me' in result.settlement_batch_summary.records[0])
View
1  tests/integration/test_subscription.py
@@ -42,6 +42,7 @@ def test_create_returns_successful_result_if_valid(self):
self.assertEquals(date, type(subscription.billing_period_end_date))
self.assertEquals(date, type(subscription.paid_through_date))
+ self.assertEquals(1, subscription.current_billing_cycle)
self.assertEquals(0, subscription.failure_count)
self.assertEquals(self.credit_card.token, subscription.payment_method_token)
View
75 tests/integration/test_transaction.py
@@ -1276,3 +1276,78 @@ def test_descriptors_has_validation_errors_if_format_is_invalid(self):
ErrorCodes.Descriptor.PhoneFormatIsInvalid,
result.errors.for_object("transaction").for_object("descriptor").on("phone")[0].code
)
+
+ def test_clone_transaction(self):
+ result = Transaction.sale({
+ "amount": "100.00",
+ "order_id": "123",
+ "credit_card": {
+ "number": "5105105105105100",
+ "expiration_date": "05/2011",
+ },
+ "customer": {
+ "first_name": "Dan",
+ },
+ "billing": {
+ "first_name": "Carl",
+ },
+ "shipping": {
+ "first_name": "Andrew",
+ }
+ })
+
+ self.assertTrue(result.is_success)
+ transaction = result.transaction
+
+ clone_result = Transaction.clone_transaction(transaction.id, {"amount": "123.45", "options": {"submit_for_settlement": "false"}})
+ self.assertTrue(clone_result.is_success)
+ clone_transaction = clone_result.transaction
+
+ self.assertNotEquals(transaction.id, clone_transaction.id)
+
+ self.assertEquals(Transaction.Type.Sale, clone_transaction.type)
+ self.assertEquals(Transaction.Status.Authorized, clone_transaction.status)
+ self.assertEquals(Decimal("123.45"), clone_transaction.amount)
+ self.assertEquals("123", clone_transaction.order_id)
+ self.assertEquals("510510******5100", clone_transaction.credit_card_details.masked_number)
+ self.assertEquals("Dan", clone_transaction.customer_details.first_name)
+ self.assertEquals("Carl", clone_transaction.billing_details.first_name)
+ self.assertEquals("Andrew", clone_transaction.shipping_details.first_name)
+
+ def test_clone_transaction_submits_for_settlement(self):
+ result = Transaction.sale({
+ "amount": "100.00",
+ "credit_card": {
+ "number": "5105105105105100",
+ "expiration_date": "05/2011",
+ }
+ })
+ self.assertTrue(result.is_success)
+ transaction = result.transaction
+
+ clone_result = Transaction.clone_transaction(transaction.id, {"amount": "123.45", "options": {"submit_for_settlement": "true"}})
+ self.assertTrue(clone_result.is_success)
+ clone_transaction = clone_result.transaction
+
+ self.assertEquals(Transaction.Type.Sale, clone_transaction.type)
+ self.assertEquals(Transaction.Status.SubmittedForSettlement, clone_transaction.status)
+
+ def test_clone_transaction_with_validations(self):
+ result = Transaction.credit({
+ "amount": "100.00",
+ "credit_card": {
+ "number": "5105105105105100",
+ "expiration_date": "05/2011",
+ }
+ })
+
+ self.assertTrue(result.is_success)
+ transaction = result.transaction
+
+ clone_result = Transaction.clone_transaction(transaction.id, {"amount": "123.45"})
+ self.assertFalse(clone_result.is_success)
+
+ self.assertEquals(
+ ErrorCodes.Transaction.CannotCloneCredit,
+ clone_result.errors.for_object("transaction").on("base")[0].code
+ )
View
6 tests/test_helper.py
@@ -100,6 +100,12 @@ def includes_status(collection, status):
return False
@staticmethod
+ def now_in_eastern():
+ now = datetime.utcnow()
+ offset = timedelta(hours=5)
+ return (now - offset).strftime("%Y-%m-%d")
+
+ @staticmethod
def unique(list):
return set(list)
View
7 tests/unit/test_transaction.py
@@ -1,6 +1,13 @@
from tests.test_helper import *
class TestTransaction(unittest.TestCase):
+ def test_clone_transaction_raises_exception_with_bad_keys(self):
+ try:
+ Transaction.clone_transaction("an id", {"bad_key": "value"})
+ self.assertTrue(False)
+ except KeyError, e:
+ self.assertEquals("'Invalid keys: bad_key'", str(e))
+
def test_sale_raises_exception_with_bad_keys(self):
try:
Transaction.sale({"bad_key": "value"})
Please sign in to comment.
Something went wrong with that request. Please try again.