Permalink
Browse files

2.2.0

  • Loading branch information...
1 parent d103526 commit 2cfda43d4542a72813089686ce54541390a01a56 @braintreeps braintreeps committed May 17, 2010
View
@@ -1,3 +1,9 @@
+== 2.2.0
+
+* Prevent race condition when pulling back collection results -- search results represent the state of the data at the time the query was run
+* Rename ResourceCollection's approximate_size to maximum_size because items that no longer match the query will not be returned in the result set
+* Correctly handle HTTP error 426 (Upgrade Required) -- the error code is returned when your client library version is no longer compatible with the gateway
+
== 2.1.0
* Added transaction advanced search
View
@@ -1,7 +1,8 @@
-require 'bigdecimal'
+require "bigdecimal"
require "cgi"
require "date"
require "digest/sha1"
+require "enumerator"
require "logger"
require "net/http"
require "net/https"
@@ -66,25 +66,17 @@ def self.credit!(token, transaction_attributes)
# Returns a ResourceCollection of expired credit cards.
def self.expired(options = {})
- page_number = options[:page] || 1
- response = Http.get("/payment_methods/all/expired?page=#{page_number}")
- attributes = response[:payment_methods]
- attributes[:items] = Util.extract_attribute_as_array(attributes, :credit_card).map do |payment_method_attributes|
- new payment_method_attributes
- end
- ResourceCollection.new(attributes) { |page_number| CreditCard.expired(:page => page_number) }
+ response = Http.post("/payment_methods/all/expired_ids")
+ ResourceCollection.new(response) { |ids| _fetch_expired(ids) }
end
# Returns a ResourceCollection of credit cards expiring between +start_date+ and +end_date+ inclusive.
# Only the month and year of the start and end dates are used.
def self.expiring_between(start_date, end_date, options = {})
- page_number = options[:page] || 1
- response = Http.get("/payment_methods/all/expiring?page=#{page_number}&start=#{start_date.strftime('%m%Y')}&end=#{end_date.strftime('%m%Y')}")
- attributes = response[:payment_methods]
- attributes[:items] = Util.extract_attribute_as_array(attributes, :credit_card).map do |payment_method_attributes|
- new payment_method_attributes
- end
- ResourceCollection.new(attributes) { |page_number| CreditCard.expiring_between(start_date, end_date, :page => page_number) }
+ formatted_start_date = start_date.strftime('%m%Y')
+ formatted_end_date = end_date.strftime('%m%Y')
+ response = Http.post("/payment_methods/all/expiring_ids?start=#{formatted_start_date}&end=#{formatted_end_date}")
+ ResourceCollection.new(response) { |ids| _fetch_expiring_between(formatted_start_date, formatted_end_date, ids) }
end
# Finds the credit card with the given +token+. Raises a NotFoundError if it cannot be found.
@@ -124,6 +116,21 @@ def self.update_credit_card_url
"#{Braintree::Configuration.base_merchant_url}/payment_methods/all/update_via_transparent_redirect_request"
end
+ def self._fetch_expired(ids)
+ response = Http.post("/payment_methods/all/expired", :search => {:ids => ids})
+ attributes = response[:payment_methods]
+ Util.extract_attribute_as_array(attributes, :credit_card).map { |attrs| _new(attrs) }
+ end
+
+ def self._fetch_expiring_between(formatted_start_date, formatted_end_date, ids)
+ response = Http.post(
+ "/payment_methods/all/expiring?start=#{formatted_start_date}&end=#{formatted_end_date}",
+ :search => {:ids => ids}
+ )
+ attributes = response[:payment_methods]
+ Util.extract_attribute_as_array(attributes, :credit_card).map { |attrs| _new(attrs) }
+ end
+
def initialize(attributes) # :nodoc:
_init attributes
@subscriptions = (@subscriptions || []).map { |subscription_hash| Subscription._new(subscription_hash) }
View
@@ -8,25 +8,21 @@ class Customer
attr_reader :addresses, :company, :created_at, :credit_cards, :email, :fax, :first_name, :id, :last_name,
:phone, :updated_at, :website, :custom_fields
- # Returns a ResourceCollection of all customers stored in the vault. Due to race conditions, this method
- # may not reliably return all customers stored in the vault.
+ # Returns a ResourceCollection of all customers stored in the vault.
#
- # page = Braintree::Customer.all
- # loop do
- # page.each do |customer|
- # puts "Customer #{customer.id} email is #{customer.email}"
- # end
- # break if page.last_page?
- # page = page.next_page
+ # customers = Braintree::Customer.all
+ # customers.each do |customer|
+ # puts "Customer #{customer.id} email is #{customer.email}"
# end
- def self.all(options = {})
- page_number = options[:page] || 1
- response = Http.get("/customers?page=#{page_number}")
+ def self.all
+ response = Http.post "/customers/advanced_search_ids"
+ ResourceCollection.new(response) { |ids| _fetch_customers(ids) }
+ end
+
+ def self._fetch_customers(ids)
+ response = Http.post "/customers/advanced_search", {:search => {:ids => ids}}
attributes = response[:customers]
- attributes[:items] = Util.extract_attribute_as_array(attributes, :customer).map do |customer_attributes|
- new customer_attributes
- end
- ResourceCollection.new(attributes) { |page_number| Customer.all(:page => page_number) }
+ Util.extract_attribute_as_array(attributes, :customer).map { |attrs| _new(attrs) }
end
# Creates a customer using the given +attributes+. If <tt>:id</tt> is not passed,
@@ -100,13 +96,16 @@ def self.sale!(customer_id, transaction_attributes)
# Returns a ResourceCollection of transactions for the customer with the given +customer_id+.
def self.transactions(customer_id, options = {})
- page_number = options[:page] || 1
- response = Http.get "/customers/#{customer_id}/transactions?page=#{page_number}"
+ response = Http.post "/customers/#{customer_id}/transaction_ids"
+ ResourceCollection.new(response) { |ids| _fetch_transactions(customer_id, ids) }
+ end
+
+ def self._fetch_transactions(customer_id, ids)
+ response = Http.post "/customers/#{customer_id}/transactions", :search => {:ids => ids}
attributes = response[:credit_card_transactions]
- attributes[:items] = Util.extract_attribute_as_array(attributes, :transaction).map do |transaction_attributes|
+ Util.extract_attribute_as_array(attributes, :transaction).map do |transaction_attributes|
Transaction._new transaction_attributes
end
- ResourceCollection.new(attributes) { |page_number| Customer.transactions(customer_id, :page => page_number) }
end
def self.update(customer_id, attributes)
@@ -38,6 +38,9 @@ class SSLCertificateError < BraintreeError; end
# This shouldn't happen.
class UnexpectedError < BraintreeError; end
+ # Raised when a client library that has been End of Life'd is being used.
+ class UpgradeRequiredError < BraintreeError; end
+
# Raised from bang methods when validations fail.
class ValidationsFailed < BraintreeError
attr_reader :error_result
@@ -1,55 +1,35 @@
module Braintree
class ResourceCollection
- include BaseModule
include Enumerable
- def initialize(attributes, &block) # :nodoc:
- set_instance_variables_from_hash attributes
+ def initialize(response, &block) # :nodoc:
+ @ids = Util.extract_attribute_as_array(response[:search_results], :ids)
+ @page_size = response[:search_results][:page_size]
@paging_block = block
end
- # Yields each item on the current page.
+ # Yields each item
def each(&block)
- @items.each(&block)
-
- _next_page.each(&block) unless _last_page?
+ @ids.each_slice(@page_size) do |page_of_ids|
+ resources = @paging_block.call(page_of_ids)
+ resources.each(&block)
+ end
end
def empty?
- @items.empty?
+ @ids.empty?
end
- # Returns the first item from the current page.
+ # Returns the first item in the collection or nil if the collection is empty
def first
- @items.first
- end
-
- # Returns true if the page is the last page. False otherwise.
- def _last_page?
- @current_page_number == _total_pages
- end
-
- # Retrieves the next page of records.
- def _next_page
- if _last_page?
- return nil
- end
- @paging_block.call(@current_page_number + 1)
+ @paging_block.call([@ids.first]).first
end
- # The size of a resource collection is only approximate due to race conditions when pulling back results. This method
- # should be avoided.
- def _approximate_size
- @total_items
- end
-
- # Returns the total number of pages.
- def _total_pages
- total = @total_items / @page_size
- if @total_items % @page_size != 0
- total += 1
- end
- total
+ # Only the maximum size of a resource collection can be determined since the data on the server can change while
+ # fetching blocks of results for iteration. For example, customers can be deleted while iterating, so the number
+ # of results iterated over may be less than the maximum_size. In general, this method should be avoided.
+ def maximum_size
+ @ids.size
end
end
end
@@ -97,14 +97,19 @@ def self.retry_charge(subscription_id, amount=nil)
# s.days_past_due.is "30"
# s.status.in [Subscription::Status::PastDue]
# end
- def self.search(page=1, &block)
+ def self.search(&block)
search = SubscriptionSearch.new
- block.call(search)
+ block.call(search) if block
- response = Http.post "/subscriptions/advanced_search?page=#{page}", {:search => search.to_hash}
+ response = Http.post "/subscriptions/advanced_search_ids", {:search => search.to_hash}
+ ResourceCollection.new(response) { |ids| _fetch_subscriptions(search, ids) }
+ end
+
+ def self._fetch_subscriptions(search, ids)
+ search.ids.in ids
+ response = Http.post "/subscriptions/advanced_search", {:search => search.to_hash}
attributes = response[:subscriptions]
- attributes[:items] = Util.extract_attribute_as_array(attributes, :subscription).map { |attrs| _new(attrs) }
- ResourceCollection.new(attributes) { |page_number| Subscription.search(page_number, &block) }
+ Util.extract_attribute_as_array(attributes, :subscription).map { |attrs| _new(attrs) }
end
def self.update(subscription_id, attributes)
@@ -1,5 +1,6 @@
module Braintree
class SubscriptionSearch < AdvancedSearch
+ multiple_value_field :ids
search_fields :plan_id, :days_past_due
multiple_value_field :status, :allows => [
Subscription::Status::Active,
@@ -223,14 +223,13 @@ def self.sale!(attributes)
# Returns a ResourceCollection of transactions matching the search query.
# If <tt>query</tt> is a string, the search will be a basic search.
# If <tt>query</tt> is a hash, the search will be an advanced search.
- def self.search(query = nil, page=1, &block)
- if block_given?
- _advanced_search page, &block
- elsif query.is_a?(String)
- _basic_search query, page
- else
- raise ArgumentError, "expected search to be a string or a block"
- end
+ # See: http://www.braintreepaymentsolutions.com/gateway/transaction-api#searching
+ def self.search(&block)
+ search = TransactionSearch.new
+ block.call(search) if block
+
+ response = Http.post "/transactions/advanced_search_ids", {:search => search.to_hash}
+ ResourceCollection.new(response) { |ids| _fetch_transactions(search, ids) }
end
# Submits transaction with +transaction_id+ for settlement.
@@ -395,28 +394,10 @@ def self._do_create(url, params) # :nodoc:
end
end
- def self._advanced_search(page, &block) # :nodoc:
- search = TransactionSearch.new
- block.call(search)
-
- response = Http.post "/transactions/advanced_search?page=#{page}", {:search => search.to_hash}
- attributes = response[:credit_card_transactions]
- attributes[:items] = Util.extract_attribute_as_array(attributes, :transaction).map { |attrs| _new(attrs) }
-
- ResourceCollection.new(attributes) { |page_number| Transaction.search(nil, page_number, &block) }
- end
-
def self._attributes # :nodoc:
[:amount, :created_at, :credit_card_details, :customer_details, :id, :status, :type, :updated_at]
end
- def self._basic_search(query, page) # :nodoc:
- response = Http.get "/transactions/all/search?q=#{Util.url_encode(query)}&page=#{Util.url_encode(page)}"
- attributes = response[:credit_card_transactions]
- attributes[:items] = Util.extract_attribute_as_array(attributes, :transaction).map { |attrs| _new(attrs) }
- ResourceCollection.new(attributes) { |page_number| Transaction.search(query, page_number) }
- end
-
def self._create_signature # :nodoc:
[
:amount, :customer_id, :merchant_account_id, :order_id, :payment_method_token, :type,
@@ -429,6 +410,13 @@ def self._create_signature # :nodoc:
]
end
+ def self._fetch_transactions(search, ids)
+ search.ids.in ids
+ response = Http.post "/transactions/advanced_search", {:search => search.to_hash}
+ attributes = response[:credit_card_transactions]
+ Util.extract_attribute_as_array(attributes, :transaction).map { |attrs| _new(attrs) }
+ end
+
def _init(attributes) # :nodoc:
set_instance_variables_from_hash(attributes)
@amount = Util.to_big_decimal(amount)
@@ -47,6 +47,7 @@ class TransactionSearch < AdvancedSearch
CreditCard::CustomerLocation::International,
CreditCard::CustomerLocation::US
]
+ multiple_value_field :ids
multiple_value_field :merchant_account_id
multiple_value_field :status, :allows => Transaction::Status::All
multiple_value_field :source, :allows => [
@@ -45,7 +45,7 @@ def self.parse_and_validate_query_string(query_string) # :nodoc:
if params[:http_status] == nil
raise UnexpectedError, "expected query string to have an http_status param"
elsif params[:http_status] != '200'
- Util.raise_exception_for_status_code(params[:http_status])
+ Util.raise_exception_for_status_code(params[:http_status], params[:bt_message])
end
if _hash(query_string_without_hash) == params[:hash]
View
@@ -41,14 +41,16 @@ def self.symbolize_keys(hash)
end
end
- def self.raise_exception_for_status_code(status_code)
+ def self.raise_exception_for_status_code(status_code, message=nil)
case status_code.to_i
when 401
raise AuthenticationError
when 403
- raise AuthorizationError
+ raise AuthorizationError, message
when 404
raise NotFoundError
+ when 426
+ raise UpgradeRequiredError, "Please upgrade your client library."
when 500
raise ServerError
when 503
View
@@ -1,7 +1,7 @@
module Braintree
module Version
Major = 2
- Minor = 1
+ Minor = 2
Tiny = 0
String = "#{Major}.#{Minor}.#{Tiny}"
@@ -23,7 +23,7 @@ def self._typecast_xml_value(value)
when 'Hash'
if value['type'] == 'array'
child_key, entries = value.detect { |k,v| k != 'type' } # child_key is throwaway
- if entries.nil? || (c = value[CONTENT_ROOT] && c.blank?)
+ if entries.nil? || ((c = value[CONTENT_ROOT]) && c.strip.empty?)
[]
else
case entries.class.to_s # something weird with classes not matching here. maybe singleton methods breaking is_a?
Oops, something went wrong.

0 comments on commit 2cfda43

Please sign in to comment.