Skip to content

Commit 2354b0d

Browse files
committed
2.7.0
1 parent 2a999b6 commit 2354b0d

16 files changed

+380
-14
lines changed

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
*.pyc
2-
/tags
2+
/dist
33
/docs/_build
4+
/tags
5+
MANIFEST

CHANGELOG.md

+9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## 2.7.0
2+
3+
* Added Customer search
4+
* Added dynamic descriptors to Subscriptions and Transactions
5+
* Added level 2 fields to Transactions:
6+
* tax_amount
7+
* tax_exempt
8+
* purchase_order_number
9+
110
## 2.6.1
211

312
* Added billing_address_id to allowed parameters for credit cards create and update

braintree/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
from credit_card_gateway import CreditCardGateway
88
from credit_card_verification import CreditCardVerification
99
from customer import Customer
10+
from customer_search import CustomerSearch
1011
from customer_gateway import CustomerGateway
1112
from discount import Discount
13+
from descriptor import Descriptor
1214
from error_codes import ErrorCodes
1315
from error_result import ErrorResult
1416
from errors import Errors

braintree/customer.py

+4
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ def find(customer_id):
110110

111111
return Configuration.gateway().customer.find(customer_id)
112112

113+
@staticmethod
114+
def search(*query):
115+
return Configuration.gateway().customer.search(*query)
116+
113117
@staticmethod
114118
def tr_data_for_create(tr_data, redirect_url):
115119
""" Builds tr_data for creating a Customer. """

braintree/customer_gateway.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def __init__(self, gateway):
1515

1616
def all(self):
1717
response = self.config.http().post("/customers/advanced_search_ids")
18-
return ResourceCollection(None, response, self.__fetch)
18+
return ResourceCollection({}, response, self.__fetch)
1919

2020
def confirm_transparent_redirect(self, query_string):
2121
id = self.gateway.transparent_redirect._parse_and_validate_query_string(query_string)["id"][0]
@@ -36,6 +36,13 @@ def find(self, customer_id):
3636
except NotFoundError:
3737
raise NotFoundError("customer with id " + customer_id + " not found")
3838

39+
def search(self, *query):
40+
if isinstance(query[0], list):
41+
query = query[0]
42+
43+
response = self.config.http().post("/customers/advanced_search_ids", {"search": self.__criteria(query)})
44+
return ResourceCollection(query, response, self.__fetch)
45+
3946
def tr_data_for_create(self, tr_data, redirect_url):
4047
Resource.verify_keys(tr_data, [{"customer": Customer.create_signature()}])
4148
tr_data["kind"] = TransparentRedirect.Kind.CreateCustomer
@@ -60,9 +67,18 @@ def update(self, customer_id, params={}):
6067
elif "api_error_response" in response:
6168
return ErrorResult(self.gateway, response["api_error_response"])
6269

63-
def __fetch(self, query, ids):
70+
def __criteria(self, query):
6471
criteria = {}
65-
criteria["ids"] = IdsSearch.ids.in_list(ids).to_param()
72+
for term in query:
73+
if criteria.get(term.name):
74+
criteria[term.name] = dict(criteria[term.name].items() + term.to_param().items())
75+
else:
76+
criteria[term.name] = term.to_param()
77+
return criteria
78+
79+
def __fetch(self, query, ids):
80+
criteria = self.__criteria(query)
81+
criteria["ids"] = braintree.customer_search.CustomerSearch.ids.in_list(ids).to_param()
6682
response = self.config.http().post("/customers/advanced_search", {"search": criteria})
6783
return [Customer(self.gateway, item) for item in ResourceCollection._extract_as_array(response["customers"], "customer")]
6884

braintree/customer_search.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from braintree.search import Search
2+
3+
class CustomerSearch:
4+
address_extended_address = Search.TextNodeBuilder("address_extended_address")
5+
address_first_name = Search.TextNodeBuilder("address_first_name")
6+
address_last_name = Search.TextNodeBuilder("address_last_name")
7+
address_locality = Search.TextNodeBuilder("address_locality")
8+
address_postal_code = Search.TextNodeBuilder("address_postal_code")
9+
address_region = Search.TextNodeBuilder("address_region")
10+
address_street_address = Search.TextNodeBuilder("address_street_address")
11+
cardholder_name = Search.TextNodeBuilder("cardholder_name")
12+
company = Search.TextNodeBuilder("company")
13+
created_at = Search.RangeNodeBuilder("created_at")
14+
credit_card_expiration_date = Search.EqualityNodeBuilder("credit_card_expiration_date")
15+
credit_card_number = Search.TextNodeBuilder("credit_card_number")
16+
email = Search.TextNodeBuilder("email")
17+
fax = Search.TextNodeBuilder("fax")
18+
first_name = Search.TextNodeBuilder("first_name")
19+
id = Search.TextNodeBuilder("id")
20+
ids = Search.MultipleValueNodeBuilder("ids")
21+
last_name = Search.TextNodeBuilder("last_name")
22+
payment_method_token = Search.TextNodeBuilder("payment_method_token")
23+
phone = Search.TextNodeBuilder("phone")
24+
website = Search.TextNodeBuilder("website")

braintree/descriptor.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from braintree.resource import Resource
2+
3+
class Descriptor(Resource):
4+
def __init__(self, gateway, attributes):
5+
Resource.__init__(self, gateway, attributes)

braintree/error_codes.py

+13-5
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ class Customer(object):
8080
WebsiteIsInvalid = "81616"
8181
WebsiteIsTooLong = "81615"
8282

83+
class Descriptor(object):
84+
NameFormatIsInvalid = "92201"
85+
PhoneFormatIsInvalid = "92202"
86+
8387
class Subscription(object):
8488
BillingDayOfMonthCannotBeUpdated = "91918"
8589
BillingDayOfMonthIsInvalid = "91914"
@@ -103,6 +107,7 @@ class Subscription(object):
103107
PaymentMethodTokenCardTypeIsNotAccepted = "91902"
104108
PaymentMethodTokenIsInvalid = "91903"
105109
PaymentMethodTokenNotAssociatedWithCustomer = "91905"
110+
PlanBillingFrequencyCannotBeUpdated = "91922"
106111
PlanIdIsInvalid = "91904"
107112
PriceCannotBeBlank = "81903"
108113
PriceFormatIsInvalid = "81904"
@@ -134,8 +139,8 @@ class Modification(object):
134139

135140
class Transaction(object):
136141
AmountCannotBeNegative = "81501"
137-
AmountIsRequired = "81502"
138142
AmountIsInvalid = "81503"
143+
AmountIsRequired = "81502"
139144
AmountIsTooLarge = "81528"
140145
AmountMustBeGreaterThanZero = "81531"
141146
BillingAddressConflict = "91530"
@@ -144,16 +149,16 @@ class Transaction(object):
144149
CannotRefundUnlessSettled = "91506"
145150
CannotSubmitForSettlement = "91507"
146151
CreditCardIsRequired = "91508"
147-
CustomerDefaultPaymentMethodCardTypeIsNotAccepted = "81509"
148152
CustomFieldIsInvalid = "91526"
149153
CustomFieldIsTooLong = "81527"
150-
CustomerIdIsInvalid = "91510"
154+
CustomerDefaultPaymentMethodCardTypeIsNotAccepted = "81509"
151155
CustomerDoesNotHaveCreditCard = "91511"
156+
CustomerIdIsInvalid = "91510"
152157
HasAlreadyBeenRefunded = "91512"
153-
MerchantAccountNameIsInvalid = "91513" # Deprecated
154158
MerchantAccountIdIsInvalid = "91513"
155159
MerchantAccountIsSusped = "91514" # Deprecated
156160
MerchantAccountIsSuspended = "91514"
161+
MerchantAccountNameIsInvalid = "91513" # Deprecated
157162
OrderIdIsTooLong = "91501"
158163
PaymentMethodConflict = "91515"
159164
PaymentMethodDoesNotBelongToCustomer = "91516"
@@ -162,14 +167,17 @@ class Transaction(object):
162167
PaymentMethodTokenIsInvalid = "91518"
163168
ProcessorAuthorizationCodeCannotBeSet = "91519"
164169
ProcessorAuthorizationCodeIsInvalid = "81520"
170+
PurchaseOrderNumberIsTooLong = "91537"
165171
RefundAmountIsTooLarge = "91521"
166172
SettlementAmountIsTooLarge = "91522"
167173
SubscriptionDoesNotBelongToCustomer = "91529"
168174
SubscriptionIdIsInvalid = "91528"
169175
SubscriptionStatusMustBePastDue = "91531"
176+
TaxAmountCannotBeNegative = "81534"
177+
TaxAmountFormatIsInvalid = "81535"
178+
TaxAmountIsTooLarge = "81536"
170179
TypeIsInvalid = "91523"
171180
TypeIsRequired = "91524"
172181

173182
class Options(object):
174183
VaultIsDisabled = "91525"
175-

braintree/subscription.py

+9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import braintree
44
import warnings
55
from braintree.add_on import AddOn
6+
from braintree.descriptor import Descriptor
67
from braintree.discount import Discount
78
from braintree.exceptions.not_found_error import NotFoundError
89
from braintree.resource_collection import ResourceCollection
@@ -88,6 +89,9 @@ def create_signature():
8889
"trial_duration",
8990
"trial_duration_unit",
9091
"trial_period",
92+
{
93+
"descriptor": [ "name", "phone" ]
94+
},
9195
{
9296
"options": [
9397
"do_not_inherit_add_ons_or_discounts",
@@ -173,6 +177,9 @@ def update_signature():
173177
"payment_method_token",
174178
"plan_id",
175179
"price",
180+
{
181+
"descriptor": [ "name", "phone" ]
182+
},
176183
{
177184
"options": [ "prorate_charges", "replace_all_add_ons_and_discounts", "revert_subscription_on_proration_failure" ]
178185
}
@@ -205,6 +212,8 @@ def __init__(self, gateway, attributes):
205212
self.next_billing_period_amount = Decimal(self.next_billing_period_amount)
206213
if "add_ons" in attributes:
207214
self.add_ons = [AddOn(gateway, add_on) for add_on in self.add_ons]
215+
if "descriptor" in attributes:
216+
self.descriptor = Descriptor(gateway, attributes.pop("descriptor"))
208217
if "discounts" in attributes:
209218
self.discounts = [Discount(gateway, discount) for discount in self.discounts]
210219
if "transactions" in attributes:

braintree/transaction.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from braintree.resource_collection import ResourceCollection
1616
from braintree.transparent_redirect import TransparentRedirect
1717
from braintree.exceptions.not_found_error import NotFoundError
18+
from braintree.descriptor import Descriptor
1819

1920
class Transaction(Resource):
2021
"""
@@ -305,7 +306,7 @@ def create(params):
305306
@staticmethod
306307
def create_signature():
307308
return [
308-
"amount", "customer_id", "merchant_account_id", "order_id", "payment_method_token", "type",
309+
"amount", "customer_id", "merchant_account_id", "order_id", "payment_method_token", "purchase_order_number", "shipping_address_id", "tax_amount", "tax_exempt", "type",
309310
{
310311
"credit_card": [
311312
"token", "cardholder_name", "cvv", "expiration_date", "expiration_month", "expiration_year", "number"
@@ -336,7 +337,8 @@ def create_signature():
336337
"store_shipping_address_in_vault"
337338
]
338339
},
339-
{"custom_fields": ["__any_key__"]}
340+
{"custom_fields": ["__any_key__"]},
341+
{"descriptor": ["name", "phone"]}
340342
]
341343

342344
def __init__(self, gateway, attributes):
@@ -349,6 +351,8 @@ def __init__(self, gateway, attributes):
349351
Resource.__init__(self, gateway, attributes)
350352

351353
self.amount = Decimal(self.amount)
354+
if self.tax_amount:
355+
self.tax_amount = Decimal(self.tax_amount)
352356
if "billing" in attributes:
353357
self.billing_details = Address(gateway, attributes.pop("billing"))
354358
if "credit_card" in attributes:
@@ -363,6 +367,8 @@ def __init__(self, gateway, attributes):
363367
self.discounts = [Discount(gateway, discount) for discount in self.discounts]
364368
if "status_history" in attributes:
365369
self.status_history = [StatusEvent(gateway, status_event) for status_event in self.status_history]
370+
if "descriptor" in attributes:
371+
self.descriptor = Descriptor(gateway, attributes.pop("descriptor"))
366372

367373
@property
368374
def refund_id(self):

braintree/util/http.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def __http_do(self, http_verb, path, params=None):
5959
conn.request(
6060
http_verb,
6161
self.config.base_merchant_path() + path,
62-
params and XmlUtil.xml_from_dict(params),
62+
XmlUtil.xml_from_dict(params) if params else '',
6363
self.__headers()
6464
)
6565
response = conn.getresponse()

braintree/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Version = "2.6.1"
1+
Version = "2.7.0"
+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from tests.test_helper import *
2+
3+
class TestCustomerSearch(unittest.TestCase):
4+
def test_advanced_search_no_results(self):
5+
collection = Transaction.search([
6+
TransactionSearch.billing_first_name == "no_such_person"
7+
])
8+
self.assertEquals(0, collection.maximum_size)
9+
10+
def test_advanced_search_searches_all_text_fields(self):
11+
token = "creditcard%s" % randint(1, 100000)
12+
13+
customer = Customer.create({
14+
"first_name": "Timmy",
15+
"last_name": "O'Toole",
16+
"company": "O'Toole and Son(s)",
17+
"email": "timmy@example.com",
18+
"fax": "3145551234",
19+
"phone": "5551231234",
20+
"website": "http://example.com",
21+
"credit_card": {
22+
"cardholder_name": "Tim Toole",
23+
"number": "4111111111111111",
24+
"expiration_date": "05/2010",
25+
"token": token,
26+
"billing_address": {
27+
"first_name": "Thomas",
28+
"last_name": "Otool",
29+
"street_address": "1 E Main St",
30+
"extended_address": "Suite 3",
31+
"locality": "Chicago",
32+
"region": "Illinois",
33+
"postal_code": "60622",
34+
"country_name": "United States of America"
35+
}
36+
}
37+
}).customer
38+
39+
search_criteria = {
40+
"first_name": "Timmy",
41+
"last_name": "O'Toole",
42+
"company": "O'Toole and Son(s)",
43+
"email": "timmy@example.com",
44+
"phone": "5551231234",
45+
"fax": "3145551234",
46+
"website": "http://example.com",
47+
"address_first_name": "Thomas",
48+
"address_last_name": "Otool",
49+
"address_street_address": "1 E Main St",
50+
"address_postal_code": "60622",
51+
"address_extended_address": "Suite 3",
52+
"address_locality": "Chicago",
53+
"address_region": "Illinois",
54+
"payment_method_token": token,
55+
"cardholder_name": "Tim Toole",
56+
"credit_card_number": "4111111111111111",
57+
"credit_card_expiration_date": "05/2010"
58+
}
59+
60+
criteria = [getattr(CustomerSearch, search_field) == value for search_field, value in search_criteria.items()]
61+
criteria.append(CustomerSearch.id == customer.id)
62+
63+
collection = Customer.search(criteria)
64+
65+
self.assertEquals(1, collection.maximum_size)
66+
self.assertEquals(customer.id, collection.first.id)
67+
68+
for search_field, value in search_criteria.items():
69+
collection = Customer.search(
70+
CustomerSearch.id == customer.id,
71+
getattr(CustomerSearch, search_field) == value
72+
)
73+
74+
self.assertEquals(1, collection.maximum_size)
75+
self.assertEquals(customer.id, collection.first.id)
76+
77+
def test_advanced_search_range_node_created_at(self):
78+
customer = Customer.create().customer
79+
80+
past = customer.created_at - timedelta(minutes=10)
81+
future = customer.created_at + timedelta(minutes=10)
82+
83+
collection = Customer.search(
84+
CustomerSearch.id == customer.id,
85+
CustomerSearch.created_at.between(past, future)
86+
)
87+
88+
self.assertEquals(1, collection.maximum_size)
89+
self.assertEquals(customer.id, collection.first.id)
90+
91+
collection = Customer.search(
92+
CustomerSearch.id == customer.id,
93+
CustomerSearch.created_at <= future
94+
)
95+
96+
self.assertEquals(1, collection.maximum_size)
97+
self.assertEquals(customer.id, collection.first.id)
98+
99+
collection = Customer.search(
100+
CustomerSearch.id == customer.id,
101+
CustomerSearch.created_at >= past
102+
)
103+
104+
self.assertEquals(1, collection.maximum_size)
105+
self.assertEquals(customer.id, collection.first.id)

tests/integration/test_http.py

-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ def test_unsuccessful_connection_to_ssl_server_with_wrong_domain(self):
4747
http.get("/")
4848
self.assertTrue(False)
4949
except pycurl.error, e:
50-
print e
5150
error_code, error_msg = e
5251
self.assertEquals(pycurl.E_SSL_PEER_CERTIFICATE, error_code)
5352
self.assertTrue(re.search("SSL: certificate subject name", error_msg))

0 commit comments

Comments
 (0)