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
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2009 Braintree Payment Solutions
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,62 @@
1
+ = Braintree Ruby Client Library
2
+
3
+ The Braintree gem provides integration access to the Braintree Gateway.
4
+
5
+ == Dependencies
6
+
7
+ * builder
8
+ * libxml-ruby
9
+
10
+ == Quick Start Example
11
+
12
+ require "rubygems"
13
+ require "braintree"
14
+
15
+ Braintree::Configuration.environment = :sandbox
16
+ Braintree::Configuration.merchant_id = "the_merchant_id"
17
+ Braintree::Configuration.public_key = "a_public_key"
18
+ Braintree::Configuration.private_key = "a_private_key"
19
+
20
+ transaction = Braintree::Transaction.sale!(
21
+ :amount => "100.00",
22
+ :credit_card => {
23
+ :number => "5105105105105100",
24
+ :expiration_date => "05/12"
25
+ }
26
+ )
27
+ puts "Transaction ID: #{transaction.id}"
28
+ puts "Status: #{transaction.status}"
29
+
30
+ == Bang Methods
31
+
32
+ Most methods have a bang and a non-bang version (e.g. <tt>Braintree::Customer.create</tt> and <tt>Braintree::Customer.create!</tt>).
33
+ The non-bang version will either return a +SuccessfulResult+ or an +ErrorResult+. The bang version will either return
34
+ the created or updated resource, or it will raise a ValidationsFailed exception.
35
+
36
+ Example of using non-bang method:
37
+
38
+ result = Braintree::Customer.create!(:first_name => "Josh")
39
+ if result.success?
40
+ puts "Created customer #{result.customer.id}
41
+ else
42
+ puts "Validations failed"
43
+ result.errors.for(:customer).each do |error|
44
+ puts error.message
45
+ end
46
+ end
47
+
48
+ Example of using bang method:
49
+
50
+ begin
51
+ customer = Braintree::Customer.create!(:first_name => "Josh")
52
+ puts "Created customer #{customer.id}
53
+ rescue Braintree::ValidationsFailed
54
+ puts "Validations failed"
55
+ end
56
+
57
+ We recommend using the bang methods when you assume that the data is valid and do not expect validations to fail.
58
+ Otherwise, we recommend using the non-bang methods.
59
+
60
+ == License
61
+
62
+ See the LICENSE file.
@@ -0,0 +1,66 @@
1
+ require "cgi"
2
+ require "digest/sha1"
3
+ require "logger"
4
+ require "net/http"
5
+ require "net/https"
6
+ require "stringio"
7
+ require "time"
8
+ require "zlib"
9
+
10
+ require "builder"
11
+ require "libxml"
12
+ unless ::LibXML::XML.respond_to?(:default_keep_blanks=)
13
+ raise LoadError, "The version of libxml that you're using is not compatible with the Braintree gem."
14
+ end
15
+
16
+ module Braintree # :nodoc:
17
+ # Super class for all Braintree exceptions.
18
+ class BraintreeError < ::StandardError; end
19
+
20
+ # Raised when authentication fails. This may be caused by an incorrect <tt>Braintree::Configuration</tt>
21
+ class AuthenticationError < BraintreeError; end
22
+
23
+ # Raised when the API key being used is not authorized to perform the attempted action according
24
+ # to the roles assigned to the user who owns the API key.
25
+ class AuthorizationError < BraintreeError; end
26
+
27
+ # Raised when the Braintree gem is not completely configured. See <tt>Braintree::Configuration</tt>.
28
+ class ConfigurationError < BraintreeError
29
+ def initialize(setting, message) # :nodoc:
30
+ super "Braintree::Configuration.#{setting} #{message}"
31
+ end
32
+ end
33
+
34
+ # Raised when the gateway is down for maintenance.
35
+ class DownForMaintenanceError < BraintreeError; end
36
+
37
+ # Raised from methods that confirm transparent request requests
38
+ # when the given query string cannot be verified. This may indicate
39
+ # an attempted hack on the merchant's transparent redirect
40
+ # confirmation URL.
41
+ class ForgedQueryString < BraintreeError; end
42
+
43
+ # Raised when a record could not be found.
44
+ class NotFoundError < BraintreeError; end
45
+
46
+ # Raised when an unexpected server error occurs.
47
+ class ServerError < BraintreeError; end
48
+
49
+ # Raised when the SSL certificate fails verification.
50
+ class SSLCertificateError < BraintreeError; end
51
+
52
+ # Raised when an error occurs that the client library is not built to handle.
53
+ # This shouldn't happen.
54
+ class UnexpectedError < BraintreeError; end
55
+
56
+ # Raised from bang methods when validations fail.
57
+ class ValidationsFailed < BraintreeError; end
58
+ end
59
+
60
+ require "braintree/base_module"
61
+
62
+ Dir.glob("#{File.dirname(__FILE__)}/braintree/**/*.rb").sort.each do |file|
63
+ next if file =~ /base_module/
64
+ load file
65
+ end
66
+ Braintree::SSLExpirationCheck.check_dates
@@ -0,0 +1,122 @@
1
+ module Braintree
2
+ # An Address belongs to a Customer. It can be associated to a
3
+ # CreditCard as the billing address. It can also be used
4
+ # as the shipping address when creating a Transaction.
5
+ class Address
6
+ include BaseModule # :nodoc:
7
+
8
+ attr_reader :company, :country_name, :created_at, :customer_id, :extended_address, :first_name, :id,
9
+ :last_name, :locality, :postal_code, :region, :street_address, :updated_at
10
+
11
+ def self.create(attributes)
12
+ Util.verify_keys(_create_signature, attributes)
13
+ unless attributes[:customer_id]
14
+ raise ArgumentError, "Expected hash to contain a :customer_id"
15
+ end
16
+ unless attributes[:customer_id] =~ /\A[0-9A-Za-z_-]+\z/
17
+ raise ArgumentError, ":customer_id contains invalid characters"
18
+ end
19
+ response = Http.post "/customers/#{attributes.delete(:customer_id)}/addresses", :address => attributes
20
+ if response[:address]
21
+ SuccessfulResult.new(:address => new(response[:address]))
22
+ elsif response[:api_error_response]
23
+ ErrorResult.new(response[:api_error_response])
24
+ else
25
+ raise UnexpectedError, "expected :address or :api_error_response"
26
+ end
27
+ end
28
+
29
+ def self.create!(attributes)
30
+ return_object_or_raise(:address) { create(attributes) }
31
+ end
32
+
33
+ def self.delete(customer_or_customer_id, address_id)
34
+ customer_id = _determine_customer_id(customer_or_customer_id)
35
+ Http.delete("/customers/#{customer_id}/addresses/#{address_id}")
36
+ SuccessfulResult.new
37
+ end
38
+
39
+ # Finds the address with the given +address_id+ that is associated to the given +customer_or_customer_id+.
40
+ # If the address cannot be found, a NotFoundError will be raised.
41
+ def self.find(customer_or_customer_id, address_id)
42
+ customer_id = _determine_customer_id(customer_or_customer_id)
43
+ response = Http.get("/customers/#{customer_id}/addresses/#{address_id}")
44
+ new(response[:address])
45
+ rescue NotFoundError
46
+ raise NotFoundError, "address for customer #{customer_id.inspect} with id #{address_id.inspect} not found"
47
+ end
48
+
49
+ def self.update(customer_or_customer_id, address_id, attributes)
50
+ Util.verify_keys(_update_signature, attributes)
51
+ customer_id = _determine_customer_id(customer_or_customer_id)
52
+ response = Http.put "/customers/#{customer_id}/addresses/#{address_id}", :address => attributes
53
+ if response[:address]
54
+ SuccessfulResult.new(:address => new(response[:address]))
55
+ elsif response[:api_error_response]
56
+ ErrorResult.new(response[:api_error_response])
57
+ else
58
+ raise UnexpectedError, "expected :address or :api_error_response"
59
+ end
60
+ end
61
+
62
+ def self.update!(customer_or_customer_id, address_id, attributes)
63
+ return_object_or_raise(:address) { update(customer_or_customer_id, address_id, attributes) }
64
+ end
65
+
66
+ def initialize(attributes) # :nodoc:
67
+ set_instance_variables_from_hash(attributes)
68
+ end
69
+
70
+ def ==(other) # :nodoc:
71
+ return false unless other.is_a?(Address)
72
+ id == other.id && customer_id == other.customer_id
73
+ end
74
+
75
+ # Deletes the address.
76
+ def delete
77
+ Address.delete(customer_id, self.id)
78
+ end
79
+
80
+ def update(attributes)
81
+ Util.verify_keys(self.class._update_signature, attributes)
82
+ response = Http.put "/customers/#{customer_id}/addresses/#{id}", :address => attributes
83
+ if response[:address]
84
+ set_instance_variables_from_hash response[:address]
85
+ SuccessfulResult.new(:address => self)
86
+ elsif response[:api_error_response]
87
+ ErrorResult.new(response[:api_error_response])
88
+ else
89
+ raise UnexpectedError, "expected :address or :api_error_response"
90
+ end
91
+ end
92
+
93
+ def update!(attributes)
94
+ return_object_or_raise(:address) { update(attributes) }
95
+ end
96
+
97
+ class << self
98
+ protected :new
99
+ end
100
+
101
+ def self._create_signature # :nodoc:
102
+ [:company, :country_name, :customer_id, :extended_address, :first_name,
103
+ :last_name, :locality, :postal_code, :region, :street_address]
104
+ end
105
+
106
+ def self._determine_customer_id(customer_or_customer_id) # :nodoc:
107
+ customer_id = customer_or_customer_id.is_a?(Customer) ? customer_or_customer_id.id : customer_or_customer_id
108
+ unless customer_id =~ /\A[\w_-]+\z/
109
+ raise ArgumentError, "customer_id contains invalid characters"
110
+ end
111
+ customer_id
112
+ end
113
+
114
+ def self._new(*args) # :nodoc:
115
+ self.new *args
116
+ end
117
+
118
+ def self._update_signature # :nodoc:
119
+ _create_signature
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,29 @@
1
+ module Braintree
2
+ module BaseModule # :nodoc: all
3
+ module Methods
4
+ def return_object_or_raise(object_to_return)
5
+ result = yield
6
+ if result.success?
7
+ result.send object_to_return
8
+ else
9
+ raise ValidationsFailed
10
+ end
11
+ end
12
+
13
+ def set_instance_variables_from_hash(hash)
14
+ hash.each do |key, value|
15
+ instance_variable_set "@#{key}", value
16
+ end
17
+ end
18
+
19
+ def singleton_class
20
+ class << self; self; end
21
+ end
22
+ end
23
+
24
+ def self.included(klass)
25
+ klass.extend Methods
26
+ end
27
+ include Methods
28
+ end
29
+ end
@@ -0,0 +1,99 @@
1
+ module Braintree
2
+ # The following configuration attributes need to be set to use the gem:
3
+ # * merchant_id
4
+ # * public_key
5
+ # * private_key
6
+ # * environment
7
+ #
8
+ # By default, the logger will log to +STDOUT+. The log level is set to info.
9
+ # The logger can be set to any Logger object.
10
+ module Configuration
11
+ API_VERSION = "1" # :nodoc:
12
+
13
+ class << self
14
+ attr_accessor :logger
15
+ attr_writer :merchant_id, :public_key, :private_key
16
+ end
17
+
18
+ def self.expectant_reader(*attributes) # :nodoc:
19
+ attributes.each do |attribute|
20
+ (class << self; self; end).send(:define_method, attribute) do
21
+ attribute_value = instance_variable_get("@#{attribute}")
22
+ raise ConfigurationError.new(attribute.to_s, "needs to be set") unless attribute_value
23
+ attribute_value
24
+ end
25
+ end
26
+ end
27
+ expectant_reader :environment, :merchant_id, :public_key, :private_key
28
+
29
+ def self.base_merchant_url # :nodoc:
30
+ "#{protocol}://#{server}:#{port}#{base_merchant_path}"
31
+ end
32
+
33
+ def self.base_merchant_path # :nodoc:
34
+ "/merchants/#{Braintree::Configuration.merchant_id}"
35
+ end
36
+
37
+ def self.ca_file # :nodoc:
38
+ case environment
39
+ when :qa, :sandbox
40
+ File.expand_path(File.join(File.dirname(__FILE__), "..", "ssl", "valicert_ca.crt"))
41
+ when :production
42
+ File.expand_path(File.join(File.dirname(__FILE__), "..", "ssl", "securetrust_ca.crt"))
43
+ end
44
+ end
45
+
46
+ # Sets the Braintree environment to use. Valid values are <tt>:sandbox</tt> and <tt>:production</tt>
47
+ def self.environment=(env)
48
+ unless [:development, :qa, :sandbox, :production].include?(env)
49
+ raise ArgumentError, "#{env.inspect} is not a valid environment"
50
+ end
51
+ @environment = env
52
+ end
53
+
54
+ def self.logger # :nodoc:
55
+ @logger ||= _default_logger
56
+ end
57
+
58
+ def self.port # :nodoc:
59
+ case environment
60
+ when :development
61
+ 3000
62
+ when :production, :qa, :sandbox
63
+ 443
64
+ end
65
+ end
66
+
67
+ def self.protocol # :nodoc:
68
+ ssl? ? "https" : "http"
69
+ end
70
+
71
+ def self.server # :nodoc:
72
+ case environment
73
+ when :development
74
+ "localhost"
75
+ when :production
76
+ "www.braintreegateway.com"
77
+ when :qa
78
+ "qa.braintreegateway.com"
79
+ when :sandbox
80
+ "sandbox.braintreegateway.com"
81
+ end
82
+ end
83
+
84
+ def self.ssl? # :nodoc:
85
+ case environment
86
+ when :development
87
+ false
88
+ when :production, :qa, :sandbox
89
+ true
90
+ end
91
+ end
92
+
93
+ def self._default_logger # :nodoc:
94
+ logger = Logger.new(STDOUT)
95
+ logger.level = Logger::INFO
96
+ logger
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,231 @@
1
+ module Braintree
2
+ class CreditCard
3
+ include BaseModule # :nodoc:
4
+
5
+ attr_reader :billing_address, :bin, :card_type, :cardholder_name, :created_at, :customer_id, :expiration_month,
6
+ :expiration_year, :last_4, :token, :updated_at
7
+
8
+ def self.create(attributes)
9
+ if attributes.has_key?(:expiration_date) && (attributes.has_key?(:expiration_month) || attributes.has_key?(:expiration_year))
10
+ raise ArgumentError.new("create with both expiration_month and expiration_year or only expiration_date")
11
+ end
12
+ Util.verify_keys(_create_signature, attributes)
13
+ _do_create("/payment_methods", :credit_card => attributes)
14
+ end
15
+
16
+ def self.create!(attributes)
17
+ return_object_or_raise(:credit_card) { create(attributes) }
18
+ end
19
+
20
+ # The transparent redirect URL to use to create a credit card.
21
+ def self.create_credit_card_url
22
+ "#{Braintree::Configuration.base_merchant_url}/payment_methods/all/create_via_transparent_redirect_request"
23
+ end
24
+
25
+ def self.create_from_transparent_redirect(query_string)
26
+ params = TransparentRedirect.parse_and_validate_query_string query_string
27
+ _do_create("/payment_methods/all/confirm_transparent_redirect_request", :id => params[:id])
28
+ end
29
+
30
+ def self.credit(token, transaction_attributes)
31
+ Transaction.credit(transaction_attributes.merge(
32
+ :payment_method_token => token
33
+ ))
34
+ end
35
+
36
+ def self.credit!(token, transaction_attributes)
37
+ return_object_or_raise(:transaction) { credit(token, transaction_attributes) }
38
+ end
39
+
40
+ # Returns a PagedCollection of expired credit cards.
41
+ def self.expired(options = {})
42
+ page_number = options[:page] || 1
43
+ response = Http.get("/payment_methods/all/expired?page=#{page_number}")
44
+ attributes = response[:payment_methods]
45
+ attributes[:items] = Util.extract_attribute_as_array(attributes, :credit_card).map do |payment_method_attributes|
46
+ new payment_method_attributes
47
+ end
48
+ PagedCollection.new(attributes) { |page_number| CreditCard.expired(:page => page_number) }
49
+ end
50
+
51
+ # Returns a PagedCollection of credit cards expiring between +start_date+ and +end_date+ inclusive.
52
+ # Only the month and year of the start and end dates are used.
53
+ def self.expiring_between(start_date, end_date, options = {})
54
+ page_number = options[:page] || 1
55
+ response = Http.get("/payment_methods/all/expiring?page=#{page_number}&start=#{start_date.strftime('%m%Y')}&end=#{end_date.strftime('%m%Y')}")
56
+ attributes = response[:payment_methods]
57
+ attributes[:items] = Util.extract_attribute_as_array(attributes, :credit_card).map do |payment_method_attributes|
58
+ new payment_method_attributes
59
+ end
60
+ PagedCollection.new(attributes) { |page_number| CreditCard.expiring_between(start_date, end_date, :page => page_number) }
61
+ end
62
+
63
+ # Finds the credit card with the given +token+. Raises a NotFoundError if it cannot be found.
64
+ def self.find(token)
65
+ response = Http.get "/payment_methods/#{token}"
66
+ new(response[:credit_card])
67
+ rescue NotFoundError
68
+ raise NotFoundError, "payment method with token #{token.inspect} not found"
69
+ end
70
+
71
+ def self.sale(token, transaction_attributes)
72
+ Transaction.sale(transaction_attributes.merge(
73
+ :payment_method_token => token
74
+ ))
75
+ end
76
+
77
+ def self.sale!(token, transaction_attributes)
78
+ return_object_or_raise(:transaction) { sale(token, transaction_attributes) }
79
+ end
80
+
81
+ def self.update_from_transparent_redirect(query_string)
82
+ params = TransparentRedirect.parse_and_validate_query_string query_string
83
+ _do_update(:post, "/payment_methods/all/confirm_transparent_redirect_request", :id => params[:id])
84
+ end
85
+
86
+ # The transparent redirect URL to use to update a credit card.
87
+ def self.update_credit_card_url
88
+ "#{Braintree::Configuration.base_merchant_url}/payment_methods/all/update_via_transparent_redirect_request"
89
+ end
90
+
91
+ def initialize(attributes) # :nodoc:
92
+ _init attributes
93
+ end
94
+
95
+ # Creates a credit transaction for this credit card.
96
+ def credit(transaction_attributes)
97
+ Transaction.credit(transaction_attributes.merge(
98
+ :payment_method_token => self.token
99
+ ))
100
+ end
101
+
102
+ def credit!(transaction_attributes)
103
+ return_object_or_raise(:transaction) { credit(transaction_attributes) }
104
+ end
105
+
106
+ def delete
107
+ Http.delete("/payment_methods/#{token}")
108
+ end
109
+
110
+ # Returns true if this credit card is the customer's default.
111
+ def default?
112
+ @default
113
+ end
114
+
115
+ # Expiration date formatted as MM/YYYY
116
+ def expiration_date
117
+ "#{expiration_month}/#{expiration_year}"
118
+ end
119
+
120
+ # Returns true if the credit card is expired.
121
+ def expired?
122
+ if expiration_year.to_i == Time.now.year
123
+ return expiration_month.to_i < Time.now.month
124
+ end
125
+ expiration_year.to_i < Time.now.year
126
+ end
127
+
128
+ def inspect # :nodoc:
129
+ first = [:token]
130
+ order = first + (self.class._attributes - first)
131
+ nice_attributes = order.map do |attr|
132
+ "#{attr}: #{send(attr).inspect}"
133
+ end
134
+ "#<#{self.class} #{nice_attributes.join(', ')}>"
135
+ end
136
+
137
+ def masked_number
138
+ "#{bin}******#{last_4}"
139
+ end
140
+
141
+ # Creates a sale transaction for this credit card.
142
+ def sale(transaction_attributes)
143
+ CreditCard.sale(self.token, transaction_attributes)
144
+ end
145
+
146
+ def sale!(transaction_attributes)
147
+ return_object_or_raise(:transaction) { sale(transaction_attributes) }
148
+ end
149
+
150
+ def update(attributes)
151
+ Util.verify_keys(self.class._update_signature, attributes)
152
+ response = Http.put "/payment_methods/#{token}", :credit_card => attributes
153
+ if response[:credit_card]
154
+ _init response[:credit_card]
155
+ SuccessfulResult.new(:credit_card => self)
156
+ elsif response[:api_error_response]
157
+ ErrorResult.new(response[:api_error_response])
158
+ else
159
+ raise UnexpectedError, "expected :credit_card or :api_error_response"
160
+ end
161
+ end
162
+
163
+ def update!(attributes)
164
+ return_object_or_raise(:credit_card) { update(attributes) }
165
+ end
166
+
167
+ # Returns true if +other+ is a +CreditCard+ with the same token.
168
+ def ==(other)
169
+ return false unless other.is_a?(CreditCard)
170
+ token == other.token
171
+ end
172
+
173
+ class << self
174
+ protected :new
175
+ end
176
+
177
+ def self._attributes # :nodoc:
178
+ [
179
+ :billing_address, :bin, :card_type, :cardholder_name, :created_at, :customer_id, :expiration_month,
180
+ :expiration_year, :last_4, :token, :updated_at
181
+ ]
182
+ end
183
+
184
+ def self._create_signature # :nodoc:
185
+ [
186
+ :customer_id, :cardholder_name, :cvv, :number, :expiration_date, :token,
187
+ {:options => [:verify_card]},
188
+ {:billing_address => [:first_name, :last_name, :company, :country_name, :extended_address, :locality, :region, :postal_code, :street_address]}
189
+ ]
190
+ end
191
+
192
+ def self._new(*args) # :nodoc:
193
+ self.new *args
194
+ end
195
+
196
+ def self._do_create(url, params) # :nodoc:
197
+ response = Http.post url, params
198
+ if response[:credit_card]
199
+ SuccessfulResult.new(:credit_card => new(response[:credit_card]))
200
+ elsif response[:api_error_response]
201
+ ErrorResult.new(response[:api_error_response])
202
+ else
203
+ raise UnexpectedError, "expected :credit_card or :api_error_response"
204
+ end
205
+ end
206
+
207
+ def self._do_update(http_verb, url, params) # :nodoc:
208
+ response = Http.send http_verb, url, params
209
+ if response[:credit_card]
210
+ SuccessfulResult.new(:credit_card => new(response[:credit_card]))
211
+ elsif response[:api_error_response]
212
+ ErrorResult.new(response[:api_error_response])
213
+ else
214
+ raise UnexpectedError, "expected :credit_card or :api_error_response"
215
+ end
216
+ end
217
+
218
+ def self._update_signature # :nodoc:
219
+ [
220
+ :cardholder_name, :cvv, :number, :expiration_date, :token,
221
+ {:options => [:verify_card]},
222
+ {:billing_address => [:first_name, :last_name, :company, :country_name, :extended_address, :locality, :region, :postal_code, :street_address]}
223
+ ]
224
+ end
225
+
226
+ def _init(attributes) # :nodoc:
227
+ set_instance_variables_from_hash(attributes)
228
+ @billing_address = attributes[:billing_address] ? Address._new(attributes[:billing_address]) : nil
229
+ end
230
+ end
231
+ end