braintree-rails 0.0.1

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