braintree 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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