Skip to content

Commit c130afa

Browse files
committed
2.3.0
1 parent e730534 commit c130afa

25 files changed

+616
-47
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
## 2.3.0
2+
3+
* Added unified TransparentRedirect url and confirm methods and deprecated old methods
4+
* Added methods to CreditCard to allow searching on expiring and expired credit cards
5+
* Allow credit card verification against a specified merchant account
6+
* Added all method on Customer to retrieve all customers
7+
* Added ability to update a customer, credit card, and billing address in one request
8+
* Allow updating the payment method token on a subscription
9+
* Added methods to navigate between a Transaction and its refund (in both directions)
10+
111
## 2.2.1
212

313
* Use isinstance instead of type to cater to inheritance (thanks danielgtaylor)

Rakefile

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,18 @@ load File.dirname(__FILE__) + "/cruise.rake"
22

33
task :default => :test
44

5-
task :test do
6-
sh "nosetests"
5+
task :test => ["test:unit", "test:integration"]
6+
7+
namespace :test do
8+
desc "run unit tests"
9+
task :unit do
10+
sh "nosetests tests/unit"
11+
end
12+
13+
desc "run integration tests"
14+
task :integration do
15+
sh "nosetests tests/integration"
16+
end
717
end
818

919
task :clean do

braintree/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from resource_collection import ResourceCollection
1010
from search import Search
1111
from subscription import Subscription
12+
from status_event import StatusEvent
1213
from transaction import Transaction
1314
from transaction_search import TransactionSearch
1415
from subscription_search import SubscriptionSearch

braintree/credit_card.py

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import braintree
2+
import warnings
3+
from braintree.util.deprecation_util import DeprecationUtil
24
from braintree.util.http import Http
35
from braintree.successful_result import SuccessfulResult
46
from braintree.error_result import ErrorResult
57
from braintree.resource import Resource
68
from braintree.address import Address
79
from braintree.exceptions.not_found_error import NotFoundError
810
from braintree.configuration import Configuration
11+
from braintree.ids_search import IdsSearch
12+
from braintree.resource_collection import ResourceCollection
913
from braintree.transparent_redirect import TransparentRedirect
1014

1115
class CreditCard(Resource):
@@ -96,8 +100,9 @@ def confirm_transparent_redirect(query_string):
96100
result = braintree.CreditCard.confirm_transparent_redirect_request("foo=bar&id=12345")
97101
"""
98102

99-
id = TransparentRedirect.parse_and_validate_query_string(query_string)
100-
return CreditCard.__post("/payment_methods/all/confirm_transparent_redirect_request", {"id": id})
103+
warnings.warn("Please use TransparentRedirect.confirm instead", DeprecationWarning)
104+
id = TransparentRedirect.parse_and_validate_query_string(query_string)["id"][0]
105+
return CreditCard._post("/payment_methods/all/confirm_transparent_redirect_request", {"id": id})
101106

102107
@staticmethod
103108
def create(params={}):
@@ -111,7 +116,7 @@ def create(params={}):
111116
"""
112117

113118
Resource.verify_keys(params, CreditCard.create_signature())
114-
return CreditCard.__post("/payment_methods", {"credit_card": params})
119+
return CreditCard._post("/payment_methods", {"credit_card": params})
115120

116121
@staticmethod
117122
def update(credit_card_token, params={}):
@@ -141,6 +146,35 @@ def delete(credit_card_token):
141146
Http().delete("/payment_methods/" + credit_card_token)
142147
return SuccessfulResult()
143148

149+
@staticmethod
150+
def expired():
151+
""" Return a collection of expired credit cards. """
152+
response = Http().post("/payment_methods/all/expired_ids")
153+
return ResourceCollection(None, response, CreditCard.__fetch_expired)
154+
155+
@staticmethod
156+
def expiring_between(start_date, end_date):
157+
""" Return a collection of credit cards expiring between the given dates. """
158+
formatted_start_date = start_date.strftime("%m%Y")
159+
formatted_end_date = end_date.strftime("%m%Y")
160+
query = "start=%s&end=%s" % (formatted_start_date, formatted_end_date)
161+
response = Http().post("/payment_methods/all/expiring_ids?" + query)
162+
return ResourceCollection(query, response, CreditCard.__fetch_existing_between)
163+
164+
@staticmethod
165+
def __fetch_expired(query, ids):
166+
criteria = {}
167+
criteria["ids"] = IdsSearch.ids.in_list(ids).to_param()
168+
response = Http().post("/payment_methods/all/expired", {"search": criteria})
169+
return [CreditCard(item) for item in ResourceCollection._extract_as_array(response["payment_methods"], "credit_card")]
170+
171+
@staticmethod
172+
def __fetch_existing_between(query, ids):
173+
criteria = {}
174+
criteria["ids"] = IdsSearch.ids.in_list(ids).to_param()
175+
response = Http().post("/payment_methods/all/expiring?" + query, {"search": criteria})
176+
return [CreditCard(item) for item in ResourceCollection._extract_as_array(response["payment_methods"], "credit_card")]
177+
144178
@staticmethod
145179
def find(credit_card_token):
146180
"""
@@ -168,16 +202,21 @@ def update_signature():
168202
@staticmethod
169203
def signature(type):
170204
billing_address_params = ["company", "country_name", "extended_address", "first_name", "last_name", "locality", "postal_code", "region", "street_address"]
205+
options = ["make_default", "verification_merchant_account_id", "verify_card"]
206+
171207
signature = [
172208
"cardholder_name", "cvv", "expiration_date", "expiration_month", "expiration_year", "number", "token",
173209
{"billing_address": billing_address_params},
174-
{"options": ["make_default", "verify_card"]}
210+
{"options": options}
175211
]
176212

177213
if type == "create":
178214
signature.append("customer_id")
179215
elif type == "update":
180216
billing_address_params.append({"options": ["update_existing"]})
217+
elif type == "update_via_customer":
218+
options.append("update_existing_token")
219+
billing_address_params.append({"options": ["update_existing"]})
181220
else:
182221
raise AttributeError
183222

@@ -188,6 +227,7 @@ def transparent_redirect_create_url():
188227
"""
189228
Returns the url to use for creating CreditCards through transparent redirect.
190229
"""
230+
warnings.warn("Please use TransparentRedirect.url instead", DeprecationWarning)
191231
return Configuration.base_merchant_url() + "/payment_methods/all/create_via_transparent_redirect_request"
192232

193233
@staticmethod
@@ -197,25 +237,28 @@ def tr_data_for_create(tr_data, redirect_url):
197237
"""
198238

199239
Resource.verify_keys(tr_data, [{"credit_card": CreditCard.create_signature()}])
240+
tr_data["kind"] = TransparentRedirect.Kind.CreatePaymentMethod
200241
return TransparentRedirect.tr_data(tr_data, redirect_url)
201242

202243
@staticmethod
203244
def tr_data_for_update(tr_data, redirect_url):
204245
"""
205246
Builds tr_data for CreditCard updating.
206247
"""
207-
Resource.verify_keys(tr_data, ["payment_method_token", {"credit_card": CreditCard.create_signature()}])
248+
Resource.verify_keys(tr_data, ["payment_method_token", {"credit_card": CreditCard.update_signature()}])
249+
tr_data["kind"] = TransparentRedirect.Kind.UpdatePaymentMethod
208250
return TransparentRedirect.tr_data(tr_data, redirect_url)
209251

210252
@staticmethod
211253
def transparent_redirect_update_url():
212254
"""
213255
Returns the url to be used for updating CreditCards through transparent redirect.
214256
"""
257+
warnings.warn("Please use TransparentRedirect.url instead", DeprecationWarning)
215258
return Configuration.base_merchant_url() + "/payment_methods/all/update_via_transparent_redirect_request"
216259

217260
@staticmethod
218-
def __post(url, params):
261+
def _post(url, params={}):
219262
response = Http().post(url, params)
220263
if "credit_card" in response:
221264
return SuccessfulResult({"credit_card": CreditCard(response["credit_card"])})
@@ -224,6 +267,7 @@ def __post(url, params):
224267

225268
def __init__(self, attributes):
226269
Resource.__init__(self, attributes)
270+
self.is_expired = self.expired
227271
if "billing_address" in attributes:
228272
self.billing_address = Address(self.billing_address)
229273
if "subscriptions" in attributes:

braintree/customer.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import warnings
12
from braintree.util.http import Http
23
from braintree.successful_result import SuccessfulResult
34
from braintree.error_result import ErrorResult
45
from braintree.resource import Resource
56
from braintree.credit_card import CreditCard
67
from braintree.address import Address
78
from braintree.configuration import Configuration
9+
from braintree.ids_search import IdsSearch
810
from braintree.exceptions.not_found_error import NotFoundError
11+
from braintree.resource_collection import ResourceCollection
912
from braintree.transparent_redirect import TransparentRedirect
1013

1114
class Customer(Resource):
@@ -55,6 +58,19 @@ class Customer(Resource):
5558
For more information on Customers, see http://www.braintreepaymentsolutions.com/gateway/customer-api
5659
"""
5760

61+
@staticmethod
62+
def all():
63+
""" Return a collection of all customers. """
64+
response = Http().post("/customers/advanced_search_ids")
65+
return ResourceCollection(None, response, Customer.__fetch)
66+
67+
@staticmethod
68+
def __fetch(query, ids):
69+
criteria = {}
70+
criteria["ids"] = IdsSearch.ids.in_list(ids).to_param()
71+
response = Http().post("/customers/advanced_search", {"search": criteria})
72+
return [CreditCard(item) for item in ResourceCollection._extract_as_array(response["customers"], "customer")]
73+
5874
@staticmethod
5975
def confirm_transparent_redirect(query_string):
6076
"""
@@ -64,8 +80,9 @@ def confirm_transparent_redirect(query_string):
6480
result = braintree.Customer.confirm_transparent_redirect_request("foo=bar&id=12345")
6581
"""
6682

67-
id = TransparentRedirect.parse_and_validate_query_string(query_string)
68-
return Customer.__post("/customers/all/confirm_transparent_redirect_request", {"id": id})
83+
warnings.warn("Please use TransparentRedirect.confirm instead", DeprecationWarning)
84+
id = TransparentRedirect.parse_and_validate_query_string(query_string)["id"][0]
85+
return Customer._post("/customers/all/confirm_transparent_redirect_request", {"id": id})
6986

7087
@staticmethod
7188
def create(params={}):
@@ -79,7 +96,7 @@ def create(params={}):
7996
"""
8097

8198
Resource.verify_keys(params, Customer.create_signature())
82-
return Customer.__post("/customers", {"customer": params})
99+
return Customer._post("/customers", {"customer": params})
83100

84101
@staticmethod
85102
def delete(customer_id):
@@ -113,25 +130,29 @@ def tr_data_for_create(tr_data, redirect_url):
113130
""" Builds tr_data for creating a Customer. """
114131

115132
Resource.verify_keys(tr_data, [{"customer": Customer.create_signature()}])
133+
tr_data["kind"] = TransparentRedirect.Kind.CreateCustomer
116134
return TransparentRedirect.tr_data(tr_data, redirect_url)
117135

118136
@staticmethod
119137
def tr_data_for_update(tr_data, redirect_url):
120138
""" Builds tr_data for updating a Customer. """
121139

122140
Resource.verify_keys(tr_data, [{"customer": Customer.update_signature()}])
141+
tr_data["kind"] = TransparentRedirect.Kind.UpdateCustomer
123142
return TransparentRedirect.tr_data(tr_data, redirect_url)
124143

125144
@staticmethod
126145
def transparent_redirect_create_url():
127146
""" Returns the url to use for creating Customers through transparent redirect. """
128147

148+
warnings.warn("Please use TransparentRedirect.url instead", DeprecationWarning)
129149
return Configuration.base_merchant_url() + "/customers/all/create_via_transparent_redirect_request"
130150

131151
@staticmethod
132152
def transparent_redirect_update_url():
133153
""" Returns the url to use for updating Customers through transparent redirect. """
134154

155+
warnings.warn("Please use TransparentRedirect.url instead", DeprecationWarning)
135156
return Configuration.base_merchant_url() + "/customers/all/update_via_transparent_redirect_request"
136157

137158
@staticmethod
@@ -163,11 +184,12 @@ def create_signature():
163184
def update_signature():
164185
return [
165186
"company", "email", "fax", "first_name", "id", "last_name", "phone", "website",
187+
{"credit_card": CreditCard.signature("update_via_customer")},
166188
{"custom_fields": ["__any_key__"]}
167189
]
168190

169191
@staticmethod
170-
def __post(url, params={}):
192+
def _post(url, params={}):
171193
response = Http().post(url, params)
172194
if "customer" in response:
173195
return SuccessfulResult({"customer": Customer(response["customer"])})

braintree/error_codes.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,33 @@ class CreditCard(object):
3030
BillingAddressIdIsInvalid = "91702"
3131
CardholderNameIsTooLong = "81723"
3232
CreditCardTypeIsNotAccepted = "81703"
33-
CustomerIdIsRequired = "91704"
33+
CreditCardTypeIsNotAcceptedBySubscriptionMerchantAccount = "81718"
3434
CustomerIdIsInvalid = "91705"
35-
CvvIsRequired = "81706"
35+
CustomerIdIsRequired = "91704"
3636
CvvIsInvalid = "81707"
37+
CvvIsRequired = "81706"
3738
ExpirationDateConflict = "91708"
38-
ExpirationDateIsRequired = "81709"
3939
ExpirationDateIsInvalid = "81710"
40+
ExpirationDateIsRequired = "81709"
4041
ExpirationDateYearIsInvalid = "81711"
4142
ExpirationMonthIsInvalid = "81712"
4243
ExpirationYearIsInvalid = "81713"
43-
NumberIsRequired = "81714"
44-
NumberIsInvalid = "81715"
4544
NumberHasInvalidLength = "81716"
45+
NumberIsInvalid = "81715"
46+
NumberIsRequired = "81714"
4647
NumberMustBeTestNumber = "81717"
4748
TokenInvalid = "91718"
4849
TokenIsInUse = "91719"
49-
TokenIsTooLong = "91720"
5050
TokenIsNotAllowed = "91721"
5151
TokenIsRequired = "91722"
52+
TokenIsTooLong = "91720"
53+
54+
class Options(object):
55+
UpdateExistingTokenIsInvalid = "91723"
56+
5257

5358
class Customer(object):
54-
CompanyisTooLong = "81601"
59+
CompanyIsTooLong = "81601"
5560
CustomFieldIsInvalid = "91602"
5661
CustomFieldIsTooLong = "81603"
5762
EmailIsInvalid = "81604"
@@ -72,6 +77,10 @@ class Subscription(object):
7277
CannotEditCanceledSubscription = "81901"
7378
IdIsInUse = "81902"
7479
MerchantAccountIdIsInvalid = "91901"
80+
PaymentMethodTokenCardTypeIsNotAccepted = "91902"
81+
PaymentMethodTokenIsInvalid = "91903"
82+
PaymentMethodTokenNotAssociatedWithCustomer = "91905"
83+
PlanIdIsInvalid = "91904"
7584
PriceCannotBeBlank = "81903"
7685
PriceFormatIsInvalid = "81904"
7786
StatusIsCanceled = "81905"
@@ -85,6 +94,7 @@ class Transaction(object):
8594
AmountIsRequired = "81502"
8695
AmountIsInvalid = "81503"
8796
AmountIsTooLarge = "81528"
97+
BillingAddressConflict = "91530"
8898
CannotBeVoided = "91504"
8999
CannotRefundCredit = "91505"
90100
CannotRefundUnlessSettled = "91506"

braintree/ids_search.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from braintree.search import Search
2+
3+
class IdsSearch:
4+
ids = Search.MultipleValueNodeBuilder("ids")

braintree/resource_collection.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ class ResourceCollection(object):
77
print transaction.id
88
"""
99

10-
def __init__(self, query, results, klass):
11-
self.__klass = klass
10+
def __init__(self, query, results, method):
1211
self.__page_size = results["search_results"]["page_size"]
1312
self.__ids = results["search_results"]["ids"]
1413
self.__query = query
14+
self.__method = method
1515

1616
@property
1717
def maximum_size(self):
@@ -24,13 +24,13 @@ def maximum_size(self):
2424
@property
2525
def first(self):
2626
""" Returns the first item in the results. """
27-
return self.__klass.fetch(self.__query, self.__ids[0:1])[0]
27+
return self.__method(self.__query, self.__ids[0:1])[0]
2828

2929
@property
3030
def items(self):
3131
""" Returns a generator allowing iteration over all of the results. """
3232
for batch in self.__batch_ids():
33-
for item in self.__klass.fetch(self.__query, batch):
33+
for item in self.__method(self.__query, batch):
3434
yield item
3535

3636
def __batch_ids(self):

braintree/status_event.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from decimal import Decimal
2+
from braintree.resource import Resource
3+
4+
class StatusEvent(Resource):
5+
def __init__(self, attributes):
6+
Resource.__init__(self, attributes)
7+
8+
self.amount = Decimal(self.amount)

0 commit comments

Comments
 (0)