braintree 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. data/LICENSE +22 -0
  2. data/README.rdoc +62 -0
  3. data/lib/braintree.rb +66 -0
  4. data/lib/braintree/address.rb +122 -0
  5. data/lib/braintree/base_module.rb +29 -0
  6. data/lib/braintree/configuration.rb +99 -0
  7. data/lib/braintree/credit_card.rb +231 -0
  8. data/lib/braintree/credit_card_verification.rb +31 -0
  9. data/lib/braintree/customer.rb +231 -0
  10. data/lib/braintree/digest.rb +20 -0
  11. data/lib/braintree/error_codes.rb +95 -0
  12. data/lib/braintree/error_result.rb +39 -0
  13. data/lib/braintree/errors.rb +29 -0
  14. data/lib/braintree/http.rb +105 -0
  15. data/lib/braintree/paged_collection.rb +55 -0
  16. data/lib/braintree/ssl_expiration_check.rb +28 -0
  17. data/lib/braintree/successful_result.rb +38 -0
  18. data/lib/braintree/test/credit_card_numbers.rb +50 -0
  19. data/lib/braintree/test/transaction_amounts.rb +10 -0
  20. data/lib/braintree/transaction.rb +360 -0
  21. data/lib/braintree/transaction/address_details.rb +15 -0
  22. data/lib/braintree/transaction/credit_card_details.rb +22 -0
  23. data/lib/braintree/transaction/customer_details.rb +13 -0
  24. data/lib/braintree/transparent_redirect.rb +110 -0
  25. data/lib/braintree/util.rb +94 -0
  26. data/lib/braintree/validation_error.rb +15 -0
  27. data/lib/braintree/validation_error_collection.rb +80 -0
  28. data/lib/braintree/version.rb +9 -0
  29. data/lib/braintree/xml.rb +12 -0
  30. data/lib/braintree/xml/generator.rb +80 -0
  31. data/lib/braintree/xml/libxml.rb +69 -0
  32. data/lib/braintree/xml/parser.rb +93 -0
  33. data/lib/ssl/securetrust_ca.crt +44 -0
  34. data/lib/ssl/valicert_ca.crt +18 -0
  35. data/spec/integration/braintree/address_spec.rb +352 -0
  36. data/spec/integration/braintree/credit_card_spec.rb +676 -0
  37. data/spec/integration/braintree/customer_spec.rb +664 -0
  38. data/spec/integration/braintree/http_spec.rb +201 -0
  39. data/spec/integration/braintree/test/transaction_amounts_spec.rb +29 -0
  40. data/spec/integration/braintree/transaction_spec.rb +900 -0
  41. data/spec/integration/spec_helper.rb +38 -0
  42. data/spec/script/httpsd.rb +27 -0
  43. data/spec/spec_helper.rb +41 -0
  44. data/spec/unit/braintree/address_spec.rb +86 -0
  45. data/spec/unit/braintree/configuration_spec.rb +190 -0
  46. data/spec/unit/braintree/credit_card_spec.rb +137 -0
  47. data/spec/unit/braintree/credit_card_verification_spec.rb +17 -0
  48. data/spec/unit/braintree/customer_spec.rb +103 -0
  49. data/spec/unit/braintree/digest_spec.rb +28 -0
  50. data/spec/unit/braintree/error_result_spec.rb +42 -0
  51. data/spec/unit/braintree/errors_spec.rb +81 -0
  52. data/spec/unit/braintree/http_spec.rb +42 -0
  53. data/spec/unit/braintree/paged_collection_spec.rb +128 -0
  54. data/spec/unit/braintree/ssl_expiration_check_spec.rb +92 -0
  55. data/spec/unit/braintree/successful_result_spec.rb +27 -0
  56. data/spec/unit/braintree/transaction/credit_card_details_spec.rb +22 -0
  57. data/spec/unit/braintree/transaction_spec.rb +136 -0
  58. data/spec/unit/braintree/transparent_redirect_spec.rb +154 -0
  59. data/spec/unit/braintree/util_spec.rb +142 -0
  60. data/spec/unit/braintree/validation_error_collection_spec.rb +128 -0
  61. data/spec/unit/braintree/validation_error_spec.rb +19 -0
  62. data/spec/unit/braintree/xml/libxml_spec.rb +51 -0
  63. data/spec/unit/braintree/xml_spec.rb +122 -0
  64. data/spec/unit/spec_helper.rb +1 -0
  65. metadata +118 -0
@@ -0,0 +1,15 @@
1
+ module Braintree
2
+ class Transaction
3
+ class AddressDetails # :nodoc:
4
+ include BaseModule
5
+
6
+ attr_reader :first_name, :last_name, :company,
7
+ :street_address, :extended_address, :locality, :region,
8
+ :postal_code, :country_name
9
+
10
+ def initialize(attributes)
11
+ set_instance_variables_from_hash attributes unless attributes.nil?
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ module Braintree
2
+ class Transaction
3
+ class CreditCardDetails # :nodoc:
4
+ include BaseModule
5
+
6
+ attr_reader :bin, :card_type, :expiration_month,
7
+ :expiration_year, :issuer_location, :last_4, :token
8
+
9
+ def initialize(attributes)
10
+ set_instance_variables_from_hash attributes unless attributes.nil?
11
+ end
12
+
13
+ def expiration_date
14
+ "#{expiration_month}/#{expiration_year}"
15
+ end
16
+
17
+ def masked_number
18
+ "#{bin}******#{last_4}"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ module Braintree
2
+ class Transaction
3
+ class CustomerDetails # :nodoc:
4
+ include BaseModule
5
+
6
+ attr_reader :company, :email, :fax, :first_name, :id, :last_name, :phone, :website
7
+
8
+ def initialize(attributes)
9
+ set_instance_variables_from_hash attributes unless attributes.nil?
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,110 @@
1
+ module Braintree
2
+ # The TransparentRedirect module provides methods to build the tr_data param
3
+ # that must be submitted when using the transparent redirect API. For more information
4
+ # about transparent redirect, see (TODO).
5
+ #
6
+ # You must provide a redirect_url that the gateway will redirect the user to when the
7
+ # action is complete.
8
+ #
9
+ # tr_data = Braintree::TransparentRedirect.create_customer_data(
10
+ # :redirect_url => "http://example.com/redirect_back_to_merchant_site
11
+ # )
12
+ #
13
+ # In addition to the redirect_url, any data that needs to be protected from user tampering
14
+ # should be included in the tr_data. For example, to prevent the user from tampering with the transaction
15
+ # amount, include the amount in the tr_data.
16
+ #
17
+ # tr_data = Braintree::TransparentRedirect.transaction_data(
18
+ # :redirect_url => "http://example.com/complete_transaction",
19
+ # :transaction => {:amount => "100.00"}
20
+ # )
21
+ module TransparentRedirect
22
+ TransparentRedirectKeys = [:redirect_url] # :nodoc:
23
+ CreateCustomerSignature = TransparentRedirectKeys + [{:customer => Customer._create_signature}] # :nodoc:
24
+ UpdateCustomerSignature = TransparentRedirectKeys + [:customer_id, {:customer => Customer._update_signature}] # :nodoc:
25
+ TransactionSignature = TransparentRedirectKeys + [{:transaction => Transaction._create_signature}] # :nodoc:
26
+ CreateCreditCardSignature = TransparentRedirectKeys + [{:credit_card => CreditCard._create_signature}] # :nodoc:
27
+ UpdateCreditCardSignature = TransparentRedirectKeys + [:payment_method_token, {:credit_card => CreditCard._update_signature}] # :nodoc:
28
+
29
+ # Returns the tr_data string for creating a credit card.
30
+ def self.create_credit_card_data(params)
31
+ Util.verify_keys(CreateCreditCardSignature, params)
32
+ _data(params)
33
+ end
34
+
35
+ # Returns the tr_data string for creating a customer.
36
+ def self.create_customer_data(params)
37
+ Util.verify_keys(CreateCustomerSignature, params)
38
+ _data(params)
39
+ end
40
+
41
+ # Returns the tr_data string for creating a transaction.
42
+ def self.transaction_data(params)
43
+ Util.verify_keys(TransactionSignature, params)
44
+ transaction_type = params[:transaction] && params[:transaction][:type]
45
+ unless %w[sale credit].include?(transaction_type)
46
+ raise ArgumentError, "expected transaction[type] of sale or credit, was: #{transaction_type.inspect}"
47
+ end
48
+ _data(params)
49
+ end
50
+
51
+ # Returns the tr_data string for updating a credit card.
52
+ # The payment_method_token of the credit card to update is required.
53
+ #
54
+ # tr_data = Braintree::TransparentRedirect.update_credit_card_data(
55
+ # :redirect_url => "http://example.com/redirect_here",
56
+ # :payment_method_token => "token123"
57
+ # )
58
+ def self.update_credit_card_data(params)
59
+ Util.verify_keys(UpdateCreditCardSignature, params)
60
+ unless params[:payment_method_token]
61
+ raise ArgumentError, "expected params to contain :payment_method_token of payment method to update"
62
+ end
63
+ _data(params)
64
+ end
65
+
66
+ # Returns the tr_data string for updating a customer.
67
+ # The customer_id of the customer to update is required.
68
+ #
69
+ # tr_data = Braintree::TransparentRedirect.update_customer_data(
70
+ # :redirect_url => "http://example.com/redirect_here",
71
+ # :customer_id => "customer123"
72
+ # )
73
+ def self.update_customer_data(params)
74
+ Util.verify_keys(UpdateCustomerSignature, params)
75
+ unless params[:customer_id]
76
+ raise ArgumentError, "expected params to contain :customer_id of customer to update"
77
+ end
78
+ _data(params)
79
+ end
80
+
81
+ def self.parse_and_validate_query_string(query_string) # :nodoc:
82
+ params = Util.symbolize_keys(Util.parse_query_string(query_string))
83
+ query_string_without_hash = query_string[/(.*)&hash=.*/, 1]
84
+ if _hash(query_string_without_hash) == params[:hash]
85
+ if params[:http_status] == '200'
86
+ params
87
+ else
88
+ Util.raise_exception_for_status_code(params[:http_status])
89
+ end
90
+ else
91
+ raise ForgedQueryString
92
+ end
93
+ end
94
+
95
+ def self._data(params) # :nodoc:
96
+ raise ArgumentError, "expected params to contain :redirect_url" unless params[:redirect_url]
97
+ tr_data_segment = Util.hash_to_query_string(params.merge(
98
+ :api_version => Configuration::API_VERSION,
99
+ :time => Time.now.utc.strftime("%Y%m%d%H%M%S"),
100
+ :public_key => Configuration.public_key
101
+ ))
102
+ tr_data_hash = _hash(tr_data_segment)
103
+ "#{tr_data_hash}|#{tr_data_segment}"
104
+ end
105
+
106
+ def self._hash(string) # :nodoc:
107
+ ::Braintree::Digest.hexdigest(string)
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,94 @@
1
+ module Braintree
2
+ module Util # :nodoc:
3
+ def self.extract_attribute_as_array(hash, attribute)
4
+ value = hash.delete(attribute)
5
+ value.is_a?(Array) ? value : [value]
6
+ end
7
+
8
+ def self.hash_to_query_string(hash, namespace = nil)
9
+ hash.collect do |key, value|
10
+ full_key = namespace ? "#{namespace}[#{key}]" : key
11
+ if value.is_a?(Hash)
12
+ hash_to_query_string(value, full_key)
13
+ else
14
+ url_encode(full_key) + "=" + url_encode(value)
15
+ end
16
+ end.sort * '&'
17
+ end
18
+
19
+ def self.parse_query_string(qs)
20
+ qs.split('&').inject({}) do |result, couplet|
21
+ pair = couplet.split('=')
22
+ result[CGI.unescape(pair[0]).to_sym] = CGI.unescape(pair[1])
23
+ result
24
+ end
25
+ end
26
+
27
+ def self.url_encode(text)
28
+ CGI.escape text.to_s
29
+ end
30
+
31
+ def self.symbolize_keys(hash)
32
+ hash.each do |key, value|
33
+ hash.delete(key)
34
+ hash[key.to_sym] = value
35
+ if value.is_a?(Hash)
36
+ symbolize_keys(value)
37
+ elsif value.is_a?(Array) && value.all? { |v| v.is_a?(Hash) }
38
+ value.each { |v| symbolize_keys(v) }
39
+ end
40
+ end
41
+ hash
42
+ end
43
+
44
+ def self.raise_exception_for_status_code(status_code)
45
+ case status_code.to_i
46
+ when 401
47
+ raise AuthenticationError
48
+ when 403
49
+ raise AuthorizationError
50
+ when 404
51
+ raise NotFoundError
52
+ when 500
53
+ raise ServerError
54
+ when 503
55
+ raise DownForMaintenanceError
56
+ else
57
+ raise UnexpectedError, "Unexpected HTTP_RESPONSE #{status_code.to_i}"
58
+ end
59
+ end
60
+
61
+ def self.verify_keys(valid_keys, hash)
62
+ invalid_keys = _flatten_hash_keys(hash) - _flatten_valid_keys(valid_keys)
63
+ if invalid_keys.any?
64
+ sorted = invalid_keys.sort_by { |k| k.to_s }.join(", ")
65
+ raise ArgumentError, "invalid keys: #{sorted}"
66
+ end
67
+ end
68
+
69
+ def self._flatten_valid_keys(valid_keys, namespace = nil)
70
+ valid_keys.inject([]) do |result, key|
71
+ if key.is_a?(Hash)
72
+ full_key = key.keys[0]
73
+ full_key = (namespace ? "#{namespace}[#{full_key}]" : full_key)
74
+ result += _flatten_valid_keys(key.values[0], full_key)
75
+ else
76
+ result << (namespace ? "#{namespace}[#{key}]" : key.to_s)
77
+ end
78
+ result
79
+ end.sort
80
+ end
81
+
82
+ def self._flatten_hash_keys(hash, namespace = nil)
83
+ hash.inject([]) do |result, (key, value)|
84
+ full_key = (namespace ? "#{namespace}[#{key}]" : key.to_s)
85
+ if value.is_a?(Hash)
86
+ result += _flatten_hash_keys(value, full_key)
87
+ else
88
+ result << full_key
89
+ end
90
+ result
91
+ end.sort
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,15 @@
1
+ module Braintree
2
+ class ValidationError
3
+ include BaseModule
4
+
5
+ attr_reader :attribute, :code, :message
6
+
7
+ def initialize(attributes)
8
+ set_instance_variables_from_hash attributes
9
+ end
10
+
11
+ def inspect # :nodoc:
12
+ "#<#{self.class} (#{code}) #{message}>"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,80 @@
1
+ module Braintree
2
+ # A collection of validation errors.
3
+ #
4
+ # result = Braintree::Customer.create(
5
+ # :email => "invalid",
6
+ # :credit_card => {
7
+ # :number => "invalidnumber",
8
+ # :billing_address => {
9
+ # :country_name => "invalid"
10
+ # }
11
+ # }
12
+ # )
13
+ # result.success?
14
+ # #=> false
15
+ # result.errors.for(:customer).on(:email)
16
+ # #=> [#<Braintree::ValidationError (81604) Email is an invalid format.>]
17
+ # result.errors.for(:customer).for(:credit_card).on(:number)
18
+ # #=> [#<Braintree::ValidationError (81715) Credit card number is invalid.>]
19
+ # result.errors.for(:customer).for(:credit_card).for(:billing_address).on(:country_name)
20
+ # #=> [#<Braintree::ValidationError (91803) Country name is not an accepted country.>]
21
+ class ValidationErrorCollection
22
+ include Enumerable
23
+
24
+ def initialize(data) # :nodoc:
25
+ @errors = data[:errors].map { |hash| Braintree::ValidationError.new(hash) }
26
+ @nested = {}
27
+ data.keys.each do |key|
28
+ next if key == :errors
29
+ @nested[key] = ValidationErrorCollection.new(data[key])
30
+ end
31
+ end
32
+
33
+ # Accesses the error at the given index.
34
+ def [](index)
35
+ @errors[index]
36
+ end
37
+
38
+ def deep_size # :nodoc:
39
+ size + @nested.values.inject(0) { |count, error_collection| count + error_collection.deep_size }
40
+ end
41
+
42
+ # Iterates over errors at the current level. Nested errors will not be yielded.
43
+ def each(&block)
44
+ @errors.each(&block)
45
+ end
46
+
47
+ # Returns a ValidationErrorCollection of errors nested under the given nested_key.
48
+ # Returns nil if there are not any errors nested under the given key.
49
+ def for(nested_key)
50
+ @nested[nested_key]
51
+ end
52
+
53
+ def inspect # :nodoc:
54
+ "#<#{self.class} errors#{_inner_inspect}>"
55
+ end
56
+
57
+ # Returns an array of ValidationError objects on the given attribute.
58
+ def on(attribute)
59
+ @errors.select { |error| error.attribute == attribute.to_s }
60
+ end
61
+
62
+ # The number of errors at this level. This does not include nested errors.
63
+ def size
64
+ @errors.size
65
+ end
66
+
67
+ def _inner_inspect(scope = []) # :nodoc:
68
+ all = []
69
+ scope_string = scope.join("/")
70
+ if @errors.any?
71
+ all << "#{scope_string}:[" + @errors.map { |e| "(#{e.code}) #{e.message}" }.join(", ") + "]"
72
+ end
73
+ @nested.each do |key, values|
74
+ all << values._inner_inspect(scope + [key])
75
+ end
76
+ all.join(", ")
77
+ end
78
+ end
79
+ end
80
+
@@ -0,0 +1,9 @@
1
+ module Braintree
2
+ module Version
3
+ Major = 1
4
+ Minor = 0
5
+ Tiny = 0
6
+
7
+ String = "#{Major}.#{Minor}.#{Tiny}"
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ module Braintree
2
+ module Xml # :nodoc:
3
+ def self.hash_from_xml(xml)
4
+ Parser.hash_from_xml(xml)
5
+ end
6
+
7
+ def self.hash_to_xml(hash)
8
+ Generator.hash_to_xml(hash)
9
+ end
10
+ end
11
+ end
12
+
@@ -0,0 +1,80 @@
1
+ # Portions of this code were copied and modified from Ruby on Rails, released
2
+ # under the MIT license, copyright (c) 2005-2009 David Heinemeier Hansson
3
+ module Braintree
4
+ module Xml
5
+ module Generator # :nodoc:
6
+ XML_TYPE_NAMES = {
7
+ "Fixnum" => "integer",
8
+ "Bignum" => "integer",
9
+ "TrueClass" => "boolean",
10
+ "FalseClass" => "boolean",
11
+ "DateTime" => "datetime",
12
+ "Time" => "datetime",
13
+ }
14
+ XML_FORMATTING = {
15
+ "symbol" => Proc.new { |symbol| symbol.to_s },
16
+ "datetime" => Proc.new { |time| time.xmlschema },
17
+ }
18
+
19
+ def self.hash_to_xml(hash)
20
+ root, contents = hash.keys[0], hash.values[0]
21
+
22
+ if contents.is_a?(String)
23
+ builder = Builder::XmlMarkup.new
24
+ builder.__send__(root) { |b| b.text! contents }
25
+ else
26
+ _convert_to_xml contents, :root => root
27
+ end
28
+ end
29
+
30
+ def self._convert_to_xml(hash_to_convert, options = {})
31
+ raise ArgumentError, "need root" unless options[:root]
32
+ options[:indent] ||= 2
33
+ options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
34
+ options[:builder].instruct! unless options.delete(:skip_instruct)
35
+ root = options[:root].to_s.tr("_", "-")
36
+
37
+ options[:builder].__send__(:method_missing, root) do
38
+ hash_to_convert.each do |key, value|
39
+ case value
40
+ when ::Hash
41
+ _convert_to_xml(value, options.merge(:root => key, :skip_instruct => true))
42
+ when ::Array
43
+ _array_to_xml(value, options.merge(:root => key, :skip_instruct => true))
44
+ else
45
+ type_name = XML_TYPE_NAMES[value.class.name]
46
+
47
+ key = key.to_s.tr("_", "-")
48
+
49
+ attributes = ((value.nil? || type_name.nil?) ? {} : { :type => type_name })
50
+ if value.nil?
51
+ attributes[:nil] = true
52
+ end
53
+
54
+ options[:builder].tag!(key,
55
+ XML_FORMATTING[type_name] ? XML_FORMATTING[type_name].call(value) : value,
56
+ attributes
57
+ )
58
+ end
59
+ end
60
+ end
61
+
62
+ end
63
+
64
+ def self._array_to_xml(array, options = {})
65
+ raise "expected all elements to be hashes" unless array.all? { |e| e.is_a?(Hash) }
66
+ raise "expected options[:root]" unless options[:root]
67
+ raise "expected options[:builder]" unless options[:builder]
68
+ options[:indent] ||= 2
69
+ root = options.delete(:root).to_s.tr("_", "-")
70
+ if array.empty?
71
+ options[:builder].tag!(root, :type => "array")
72
+ else
73
+ options[:builder].tag!(root, :type => "array") do
74
+ array.each { |e| _convert_to_xml(e, options.merge(:root => "item", :skip_instruct => true)) }
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end