braintree-rails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/Gemfile +10 -0
  2. data/Gemfile.lock +34 -0
  3. data/README.md +2 -0
  4. data/Rakefile +4 -0
  5. data/braintree-rails.gemspec +18 -0
  6. data/lib/braintree-rails.rb +17 -0
  7. data/lib/braintree_rails/address.rb +75 -0
  8. data/lib/braintree_rails/addresses.rb +24 -0
  9. data/lib/braintree_rails/credit_card.rb +67 -0
  10. data/lib/braintree_rails/credit_cards.rb +24 -0
  11. data/lib/braintree_rails/customer.rb +37 -0
  12. data/lib/braintree_rails/exceptions.rb +2 -0
  13. data/lib/braintree_rails/model.rb +192 -0
  14. data/lib/env.rb +3 -0
  15. data/lib/tasks/test.rake +18 -0
  16. data/lib/test_env.rb +4 -0
  17. data/log/braintree_test.log +225696 -0
  18. data/test/config/braintree_auth.yml +4 -0
  19. data/test/config/braintree_auth.yml.example +4 -0
  20. data/test/fixtures/address.xml +19 -0
  21. data/test/fixtures/credit_card.xml +36 -0
  22. data/test/fixtures/credit_card_validation_error.xml +26 -0
  23. data/test/fixtures/customer.xml +90 -0
  24. data/test/integration/braintree_rails/address_integration_test.rb +72 -0
  25. data/test/integration/braintree_rails/credit_card_integration_test.rb +147 -0
  26. data/test/integration/braintree_rails/customer_integration_test.rb +41 -0
  27. data/test/integration/integration_test_helper.rb +13 -0
  28. data/test/test_helper.rb +28 -0
  29. data/test/unit/braintree_rails/address_test.rb +87 -0
  30. data/test/unit/braintree_rails/addresses_test.rb +38 -0
  31. data/test/unit/braintree_rails/credit_card_test.rb +240 -0
  32. data/test/unit/braintree_rails/credit_cards_test.rb +38 -0
  33. data/test/unit/braintree_rails/customer_test.rb +171 -0
  34. data/test/unit/unit_test_helper.rb +3 -0
  35. metadata +176 -0
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'http://rubygems.org'
2
+ gem 'braintree', '~> 2.14.0'
3
+ gem 'activemodel', '~> 3.0'
4
+ gem 'activesupport', '~> 3.0'
5
+
6
+ group :test do
7
+ gem 'minitest'
8
+ gem 'webmock'
9
+ gem 'turn'
10
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,34 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activemodel (3.2.8)
5
+ activesupport (= 3.2.8)
6
+ builder (~> 3.0.0)
7
+ activesupport (3.2.8)
8
+ i18n (~> 0.6)
9
+ multi_json (~> 1.0)
10
+ addressable (2.3.2)
11
+ ansi (1.4.3)
12
+ braintree (2.14.0)
13
+ builder (>= 2.0.0)
14
+ builder (3.0.3)
15
+ crack (0.3.1)
16
+ i18n (0.6.1)
17
+ minitest (3.4.0)
18
+ multi_json (1.3.6)
19
+ turn (0.9.6)
20
+ ansi
21
+ webmock (1.8.10)
22
+ addressable (>= 2.2.7)
23
+ crack (>= 0.1.7)
24
+
25
+ PLATFORMS
26
+ ruby
27
+
28
+ DEPENDENCIES
29
+ activemodel (~> 3.0)
30
+ activesupport (~> 3.0)
31
+ braintree (~> 2.14.0)
32
+ minitest
33
+ turn
34
+ webmock
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ braintree-rails
2
+ ===============
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'lib/env'))
2
+ Dir.glob(File.join(ROOT_PATH, 'lib/tasks/*.rake')).each { |file| import file }
3
+
4
+ task :default => ['test:all']
@@ -0,0 +1,18 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = 'braintree-rails'
3
+ spec.version = '0.0.1'
4
+ spec.summary = 'Provides ActiveModel compatible wrappers for Braintree models.'
5
+ spec.description = 'Provides ActiveModel compatible wrappers for Braintree models and more.'
6
+ spec.author = 'Lin Yang'
7
+ spec.email = 'github@linyang.me'
8
+ spec.license = 'MIT'
9
+ spec.files = Dir['**/*']
10
+ spec.test_files = Dir['test/**']
11
+ spec.homepage = 'https://github.com/lyang/braintree-rails'
12
+ spec.add_runtime_dependency 'braintree', '~> 2.14.0'
13
+ spec.add_runtime_dependency 'activemodel', '~> 3.0'
14
+ spec.add_runtime_dependency 'activesupport', '~> 3.0'
15
+ spec.add_development_dependency 'minitest'
16
+ spec.add_development_dependency 'webmock'
17
+ spec.add_development_dependency 'turn'
18
+ end
@@ -0,0 +1,17 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'env'))
2
+ require 'active_model'
3
+ require 'active_support/core_ext/object/try'
4
+ require 'active_support/core_ext/hash/except'
5
+ require 'active_support/inflector'
6
+ require 'ostruct'
7
+ require 'delegate'
8
+ require 'braintree'
9
+ require 'braintree/exceptions'
10
+
11
+ require 'model'
12
+ require 'exceptions'
13
+ require 'address'
14
+ require 'addresses'
15
+ require 'credit_card'
16
+ require 'credit_cards'
17
+ require 'customer'
@@ -0,0 +1,75 @@
1
+ module BraintreeRails
2
+ class Address < SimpleDelegator
3
+ Attributes = [:id, :customer_id, :first_name, :last_name, :company, :street_address, :extended_address, :locality, :country_name, :country_code_alpha2, :country_code_alpha3, :country_code_numeric, :region, :postal_code]
4
+ include Model
5
+
6
+ validates :first_name, :last_name, :company, :street_address, :extended_address, :locality, :region, :length => {:maximum => 255}
7
+ validates :country_code_alpha2, :inclusion => { :in => Braintree::Address::CountryNames.map {|country| country[1]} }
8
+ validates :postal_code, :street_address, :presence => true
9
+ validates :postal_code, :format => { :with => /^[- a-z0-9]+$/i}
10
+
11
+ def initialize(address = {})
12
+ address = ensure_address(address)
13
+ assign_attributes(extract_values(address))
14
+ super
15
+ end
16
+
17
+ def country_name=(val)
18
+ self.country_code_alpha2= Braintree::Address::CountryNames.find{|country| country[0] == val}.try(:[], 1)
19
+ @country_name = val
20
+ end
21
+
22
+ def country_code_alpha3=(val)
23
+ self.country_code_alpha2= Braintree::Address::CountryNames.find{|country| country[2] == val}.try(:[], 1)
24
+ @country_code_alpha3 = val
25
+ end
26
+
27
+ def country_code_numeric=(val)
28
+ self.country_code_alpha2= Braintree::Address::CountryNames.find{|country| country[3] == val}.try(:[], 1)
29
+ @country_code_numeric = val
30
+ end
31
+
32
+ def destroy!
33
+ if persisted?
34
+ self.class.braintree_model_class.delete(customer_id, id)
35
+ end
36
+ @persisted = false
37
+ freeze
38
+ end
39
+
40
+ protected
41
+ def ensure_address(address)
42
+ case address
43
+ when Braintree::Address
44
+ @persisted = true
45
+ address
46
+ when Hash
47
+ @persisted = false
48
+ OpenStruct.new(address)
49
+ else
50
+ @persisted = address.respond_to?(:persisted?) ? address.persisted? : false
51
+ address
52
+ end
53
+ end
54
+
55
+ def update
56
+ with_update_braintree do
57
+ self.class.braintree_model_class.update(self.customer_id, self.id, self.attributes.except(:id, :customer_id))
58
+ end
59
+ end
60
+
61
+ def update!
62
+ with_update_braintree do
63
+ self.class.braintree_model_class.update!(self.customer_id, self.id, self.attributes.except(:id, :customer_id))
64
+ end
65
+ end
66
+
67
+ def attributes_to_exclude_from_update
68
+ [:id, :customer_id, :country_name, :country_code_alpha3, :country_code_numeric]
69
+ end
70
+
71
+ def attributes_to_exclude_from_create
72
+ [:country_name, :country_code_alpha3, :country_code_numeric]
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,24 @@
1
+ module BraintreeRails
2
+ class Addresses < SimpleDelegator
3
+ def initialize(customer, addresses)
4
+ @customer = customer
5
+ super(Array(addresses).map{|address| Address.new(address)})
6
+ end
7
+
8
+ def find(id = nil, &block)
9
+ id.nil? ? super(&block) : super() { |a| a.id == id }
10
+ end
11
+
12
+ def build(params)
13
+ Address.new(params.merge(:customer_id => @customer.id))
14
+ end
15
+
16
+ def create(params)
17
+ build(params).tap { |address| address.save }
18
+ end
19
+
20
+ def create!(params)
21
+ build(params).tap { |address| address.save! }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,67 @@
1
+ module BraintreeRails
2
+ class CreditCard < SimpleDelegator
3
+ Attributes = [:customer_id, :number, :token, :cvv, :cardholder_name, :expiration_date, :expiration_month, :expiration_year, :billing_address].freeze
4
+
5
+ include Model
6
+
7
+ validates :customer_id, :presence => true, :length => {:maximum => 36}, :if => :new_record?
8
+ validates :number, :presence => true, :numericality => { :only_integer => true }, :length => {:minimum => 12, :maximum => 19}, :if => :new_record?
9
+ validates :cvv, :presence => true, :numericality => { :only_integer => true, :greater_than_or_equal_to => 100, :less_than_or_equal_to => 9999 }
10
+ validates :cardholder_name, :length => {:maximum => 255}
11
+ validates :expiration_month, :presence => true, :numericality => { :only_integer => true, :greater_than_or_equal_to => 1, :less_than_or_equal_to => 12 }
12
+ validates :expiration_year, :presence => true, :numericality => { :only_integer => true, :greater_than_or_equal_to => 1976, :less_than_or_equal_to => 2200 }
13
+ validates_each :billing_address do |record, attribute, value|
14
+ record.errors.add(attribute, "is not valid. #{value.errors.full_messages.join("\n")}") unless value.valid?
15
+ end
16
+
17
+ def initialize(credit_card = {})
18
+ credit_card = ensure_credit_card(credit_card)
19
+ assign_attributes(extract_values(credit_card))
20
+ super
21
+ end
22
+
23
+ def id
24
+ token
25
+ end
26
+
27
+ def expiration_date=(date)
28
+ expiration_month, expiration_year = date.split('/')
29
+ self.expiration_month = expiration_month
30
+ self.expiration_year = expiration_year.gsub(/^(\d\d)$/, '20\1')
31
+ end
32
+
33
+ def expiration_date
34
+ expiration_month.present? ? "#{expiration_month}/#{expiration_year}" : nil
35
+ end
36
+
37
+ def billing_address=(val)
38
+ @billing_address = Address.new(val)
39
+ end
40
+
41
+ protected
42
+ def ensure_credit_card(credit_card)
43
+ case credit_card
44
+ when String
45
+ @persisted = true
46
+ Braintree::CreditCard.find(credit_card)
47
+ when Braintree::CreditCard
48
+ @persisted = true
49
+ credit_card
50
+ when Hash
51
+ @persisted = false
52
+ OpenStruct.new(credit_card.reverse_merge(:billing_address => {}))
53
+ else
54
+ @persisted = credit_card.respond_to?(:persisted?) ? credit_card.persisted? : false
55
+ credit_card
56
+ end
57
+ end
58
+
59
+ def attributes_to_exclude_from_update
60
+ [:token, :customer_id, :expiration_date]
61
+ end
62
+
63
+ def attributes_to_exclude_from_create
64
+ [:expiration_date]
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,24 @@
1
+ module BraintreeRails
2
+ class CreditCards < SimpleDelegator
3
+ def initialize(customer, credit_cards)
4
+ @customer = customer
5
+ super(Array(credit_cards).map{|card| CreditCard.new(card)})
6
+ end
7
+
8
+ def find(token = nil, &block)
9
+ token.nil? ? super(&block) : super() { |c| c.token == token }
10
+ end
11
+
12
+ def build(params)
13
+ CreditCard.new(params.merge(:customer_id => @customer.id))
14
+ end
15
+
16
+ def create(params)
17
+ build(params).tap { |credit_card| credit_card.save }
18
+ end
19
+
20
+ def create!(params)
21
+ build(params).tap { |credit_card| credit_card.save! }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,37 @@
1
+ module BraintreeRails
2
+ class Customer < SimpleDelegator
3
+ Attributes = [:id, :first_name, :last_name, :email, :company, :website, :phone, :fax].freeze
4
+ include Model
5
+
6
+ validates :id, :format => {:with => /^[-_a-z0-9]*$/i}, :length => {:maximum => 36}, :exclusion => {:in => %w(all new)}
7
+ validates :first_name, :last_name, :company, :website, :phone, :fax, :length => {:maximum => 255}
8
+
9
+ attr_reader :addresses, :credit_cards
10
+
11
+ def initialize(customer = {})
12
+ customer = ensure_customer(customer)
13
+ assign_attributes(extract_values(customer))
14
+ @addresses = Addresses.new(self, customer.addresses)
15
+ @credit_cards = CreditCards.new(self, customer.credit_cards)
16
+ super
17
+ end
18
+
19
+ protected
20
+ def ensure_customer(customer)
21
+ case customer
22
+ when String
23
+ @persisted = true
24
+ Braintree::Customer.find(customer)
25
+ when Braintree::Customer
26
+ @persisted = true
27
+ customer
28
+ when Hash
29
+ @persisted = false
30
+ OpenStruct.new(customer.reverse_merge(:addresses => [], :credit_cards => []))
31
+ else
32
+ @persisted = customer.respond_to?(:persisted?) ? customer.persisted? : false
33
+ customer
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,2 @@
1
+ class RecordInvalid < Braintree::BraintreeError; end
2
+ class RecordInvalid < Braintree::BraintreeError; end
@@ -0,0 +1,192 @@
1
+ module BraintreeRails
2
+ module Model
3
+ module ClassMethods
4
+ def self.extended(receiver)
5
+ receiver.class_eval do
6
+ extend ::ActiveModel::Naming
7
+ include ::ActiveModel::Validations
8
+ include ::ActiveModel::Serialization
9
+ attr_accessor(*self::Attributes)
10
+
11
+ class << receiver
12
+ alias :build :new
13
+
14
+ def create(params)
15
+ new(params).tap { |new_record| new_record.save }
16
+ end
17
+
18
+ def create!(params)
19
+ new(params).tap { |new_record| new_record.save! }
20
+ end
21
+
22
+ def braintree_model_class
23
+ "braintree/#{braintree_model_name}".camelize.constantize
24
+ end
25
+
26
+ def braintree_model_name
27
+ name.demodulize.underscore
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ module InstanceMethods
35
+ def persisted?
36
+ @persisted
37
+ end
38
+
39
+ def new_record?
40
+ !persisted?
41
+ end
42
+
43
+ def to_key
44
+ persisted? ? [id] : nil
45
+ end
46
+
47
+ def to_param
48
+ to_key.join("-")
49
+ end
50
+
51
+ def attributes
52
+ self.class::Attributes.inject({}) do |hash, attribute|
53
+ value = self.send(attribute)
54
+ hash[attribute] = value if value.present?
55
+ hash
56
+ end
57
+ end
58
+
59
+ def save
60
+ create_or_update
61
+ rescue RecordInvalid
62
+ false
63
+ end
64
+
65
+ def save!
66
+ create_or_update || raise(RecordNotSaved)
67
+ end
68
+
69
+ def update_attributes(attributes)
70
+ assign_attributes(attributes)
71
+ save
72
+ end
73
+
74
+ def update_attributes!(attributes)
75
+ assign_attributes(attributes)
76
+ save!
77
+ end
78
+
79
+ def destroy
80
+ destroy!
81
+ rescue Braintree::NotFoundError
82
+ @persisted = false
83
+ freeze
84
+ end
85
+ alias :delete :destroy
86
+
87
+ def destroy!
88
+ if persisted?
89
+ self.class.braintree_model_class.delete(id)
90
+ end
91
+ @persisted = false
92
+ freeze
93
+ end
94
+ alias :delete! :destroy!
95
+
96
+ def attributes_for_update
97
+ attributes.except(*attributes_to_exclude_from_update).tap do |hash|
98
+ hash.each_pair do |key, value|
99
+ hash[key] = value.attributes_for_update if value.respond_to?(:attributes_for_update)
100
+ end
101
+ end
102
+ end
103
+
104
+ def attributes_for_create
105
+ attributes.except(*attributes_to_exclude_from_create).tap do |hash|
106
+ hash.each_pair do |key, value|
107
+ hash[key] = value.attributes_for_create if value.respond_to?(:attributes_for_create)
108
+ end
109
+ end
110
+ end
111
+
112
+ private
113
+ def create_or_update
114
+ !!(new_record? ? create : update)
115
+ end
116
+
117
+ def create
118
+ with_update_braintree do
119
+ self.class.braintree_model_class.create(attributes_for_create)
120
+ end
121
+ end
122
+
123
+ def create!
124
+ with_update_braintree do
125
+ self.class.braintree_model_class.create!(attributes_for_create)
126
+ end
127
+ end
128
+
129
+ def update
130
+ with_update_braintree do
131
+ self.class.braintree_model_class.update(id, attributes_for_update)
132
+ end
133
+ end
134
+
135
+ def update!
136
+ with_update_braintree do
137
+ self.class.braintree_model_class.update!(id, attributes_for_update)
138
+ end
139
+ end
140
+
141
+ def attributes_to_exclude_from_update
142
+ [:id]
143
+ end
144
+
145
+ def attributes_to_exclude_from_create
146
+ []
147
+ end
148
+
149
+ def extract_values(obj)
150
+ self.class::Attributes.inject({}) do |hash, attr|
151
+ hash[attr] = obj.send(attr) if obj.respond_to?(attr)
152
+ hash
153
+ end
154
+ end
155
+
156
+ def assign_attributes(hash)
157
+ hash.each do |attribute, value|
158
+ send("#{attribute}=", value) if respond_to?("#{attribute}=")
159
+ end
160
+ end
161
+
162
+ def add_errors(validation_errors)
163
+ validation_errors.each do |error|
164
+ if respond_to?(error.attribute)
165
+ self.errors.add error.attribute, error.message
166
+ else
167
+ self.error.add :base, error.message
168
+ end
169
+ end
170
+ end
171
+
172
+ def with_update_braintree
173
+ raise RecordInvalid unless valid?
174
+ result = yield
175
+ if result.respond_to?(:success?) && !result.success?
176
+ add_errors(result.errors)
177
+ false
178
+ else
179
+ new_record = result.respond_to?(self.class.braintree_model_name) ? result.send(self.class.braintree_model_name) : result
180
+ assign_attributes(extract_values(new_record))
181
+ @persisted = true
182
+ self.__setobj__(new_record)
183
+ end
184
+ end
185
+ end
186
+
187
+ def self.included(receiver)
188
+ receiver.extend ClassMethods
189
+ receiver.send :include, InstanceMethods
190
+ end
191
+ end
192
+ end