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
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