ashmont 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,104 @@
1
+ require "ashmont/errors"
2
+ require "active_support/core_ext"
3
+ require "braintree"
4
+
5
+ module Ashmont
6
+ class Subscription
7
+ attr_reader :token, :errors
8
+
9
+ def initialize(token = nil, cached_attributes = {})
10
+ @token = token
11
+ @cached_attributes = cached_attributes
12
+ @errors = {}
13
+ end
14
+
15
+ delegate :transactions, :to => :remote_subscription, :allow_nil => true
16
+
17
+ def status
18
+ @cached_attributes[:status] || remote_status
19
+ end
20
+
21
+ def save(attributes)
22
+ attributes_for_merchant = add_merchant_to_attributes(attributes)
23
+ if token
24
+ update(attributes_for_merchant)
25
+ else
26
+ create(attributes_for_merchant)
27
+ end
28
+ end
29
+
30
+ def retry_charge
31
+ transaction = Braintree::Subscription.retry_charge(token).transaction
32
+ result = Braintree::Transaction.submit_for_settlement(transaction.id)
33
+ reload
34
+ if result.success?
35
+ true
36
+ else
37
+ @errors = Ashmont::Errors.new(transaction, result.errors)
38
+ false
39
+ end
40
+ end
41
+
42
+ def most_recent_transaction
43
+ transactions.sort_by(&:created_at).last
44
+ end
45
+
46
+ def next_billing_date
47
+ merchant_account_time_zone.parse(remote_subscription.next_billing_date) if remote_subscription
48
+ end
49
+
50
+ def reload
51
+ @remote_subscription = nil
52
+ @cached_attributes = {}
53
+ self
54
+ end
55
+
56
+ def past_due?
57
+ status == Braintree::Subscription::Status::PastDue
58
+ end
59
+
60
+ private
61
+
62
+ def remote_status
63
+ remote_subscription.status if remote_subscription
64
+ end
65
+
66
+ def add_merchant_to_attributes(attributes)
67
+ if Ashmont.merchant_account_id
68
+ attributes.merge(:merchant_account_id => Ashmont.merchant_account_id)
69
+ else
70
+ attributes
71
+ end
72
+ end
73
+
74
+ def create(attributes)
75
+ result = Braintree::Subscription.create(attributes)
76
+ if result.success?
77
+ @token = result.subscription.id
78
+ @remote_subscription = result.subscription
79
+ true
80
+ else
81
+ false
82
+ end
83
+ end
84
+
85
+ def update(attributes)
86
+ @remote_subscription = nil
87
+ Braintree::Subscription.update(token, attributes)
88
+ end
89
+
90
+ def remote_subscription
91
+ @remote_subscription ||= find_remote_subscription
92
+ end
93
+
94
+ def find_remote_subscription
95
+ if token
96
+ Braintree::Subscription.find(token)
97
+ end
98
+ end
99
+
100
+ def merchant_account_time_zone
101
+ ActiveSupport::TimeZone[Ashmont.merchant_account_time_zone]
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,3 @@
1
+ module Ashmont
2
+ VERSION = "0.0.12"
3
+ end
data/lib/ashmont.rb ADDED
@@ -0,0 +1,13 @@
1
+ require "ashmont/version"
2
+ require "ashmont/subscription"
3
+ require "ashmont/customer"
4
+ require "ashmont/subscribed_customer"
5
+
6
+ module Ashmont
7
+ class << self
8
+ attr_accessor :merchant_account_time_zone
9
+ attr_accessor :merchant_account_id
10
+ end
11
+
12
+ self.merchant_account_time_zone = 'Eastern Time (US & Canada)'
13
+ end
@@ -0,0 +1,286 @@
1
+ require 'spec_helper'
2
+ require 'ashmont/customer'
3
+
4
+ describe Ashmont::Customer do
5
+ it "returns its first credit card" do
6
+ token = "xyz"
7
+ remote_customer = stub("customer", :credit_cards => ["first", "second"])
8
+ Braintree::Customer.stubs(:find => remote_customer)
9
+
10
+ result = Ashmont::Customer.new(token).credit_card
11
+
12
+ Braintree::Customer.should have_received(:find).with(token)
13
+ result.should == "first"
14
+ end
15
+
16
+ it "returns nothing without a remote customer" do
17
+ Ashmont::Customer.new.credit_card.should be_nil
18
+ end
19
+
20
+ it "returns all credit cards" do
21
+ token = "xyz"
22
+ remote_customer = stub("customer", :credit_cards => ["first", "second"])
23
+ Braintree::Customer.stubs(:find => remote_customer)
24
+
25
+ result = Ashmont::Customer.new(token).credit_cards
26
+
27
+ Braintree::Customer.should have_received(:find).with(token)
28
+ result.should == ["first", "second"]
29
+ end
30
+
31
+ it "returns an empty array without a remote customer" do
32
+ Ashmont::Customer.new.credit_cards.should == []
33
+ end
34
+
35
+
36
+ it "returns the remote email" do
37
+ remote_customer = stub("customer", :email => "admin@example.com")
38
+ Braintree::Customer.stubs(:find => remote_customer)
39
+
40
+ Ashmont::Customer.new("abc").billing_email.should == "admin@example.com"
41
+ end
42
+
43
+ it "doesn't have an email without a remote customer" do
44
+ Ashmont::Customer.new.billing_email.should be_nil
45
+ end
46
+
47
+ it "creates a valid remote customer" do
48
+ token = "xyz"
49
+ remote_customer = stub("customer", :credit_cards => [], :id => token)
50
+ create_result = stub("result", :success? => true, :customer => remote_customer)
51
+ attributes = { "email" => "ben@example.com" }
52
+ Braintree::Customer.stubs(:create => create_result)
53
+
54
+ customer = Ashmont::Customer.new
55
+ customer.save(attributes).should be_true
56
+
57
+ Braintree::Customer.should have_received(:create).with(:email => "ben@example.com", :credit_card => {})
58
+ customer.token.should == token
59
+ customer.errors.should be_empty
60
+ end
61
+
62
+ it "creates a remote customer with a credit card" do
63
+ token = "xyz"
64
+ remote_customer = stub("customer", :credit_cards => [], :id => token)
65
+ create_result = stub("result", :success? => true, :customer => remote_customer)
66
+ attributes = { "email" => "ben@example.com" }
67
+ Braintree::Customer.stubs(:create => create_result)
68
+
69
+ customer = Ashmont::Customer.new
70
+ customer.save(
71
+ :email => "jrobot@example.com",
72
+ :cardholder_name => "Jim Robot",
73
+ :number => "4111111111111115",
74
+ :cvv => "123",
75
+ :expiration_month => 5,
76
+ :expiration_year => 2013,
77
+ :street_address => "1 E Main St",
78
+ :extended_address => "Suite 3",
79
+ :locality => "Chicago",
80
+ :region => "Illinois",
81
+ :postal_code => "60622",
82
+ :country_name => "United States of America"
83
+ )
84
+
85
+ Braintree::Customer.should have_received(:create).with(
86
+ :email => "jrobot@example.com",
87
+ :credit_card => {
88
+ :cardholder_name => "Jim Robot",
89
+ :number => "4111111111111115",
90
+ :cvv => "123",
91
+ :expiration_month => 5,
92
+ :expiration_year => 2013,
93
+ :billing_address => {
94
+ :street_address => "1 E Main St",
95
+ :extended_address => "Suite 3",
96
+ :locality => "Chicago",
97
+ :region => "Illinois",
98
+ :postal_code => "60622",
99
+ :country_name => "United States of America"
100
+ }
101
+ }
102
+ )
103
+ end
104
+
105
+ it "returns errors while creating an invalid remote customer" do
106
+ error_messages = "error messages"
107
+ errors = "errors"
108
+ verification = "failure"
109
+ create_result = stub("result",
110
+ :success? => false,
111
+ :errors => error_messages,
112
+ :credit_card_verification => verification)
113
+ Braintree::Customer.stubs(:create => create_result)
114
+ Ashmont::Errors.stubs(:new => errors)
115
+
116
+ customer = Ashmont::Customer.new
117
+ customer.save("email" => "ben.franklin@example.com").should be_false
118
+
119
+ Ashmont::Errors.should have_received(:new).with(verification, error_messages)
120
+ customer.errors.should == errors
121
+ end
122
+
123
+ it "updates a remote customer with valid changes" do
124
+ token = "xyz"
125
+ updates = { "email" => 'somebody@example.com' }
126
+ customer = stub("customer", :id => token, :email => "somebody@example.com")
127
+ update_result = stub('result', :success? => true, :customer => customer)
128
+ Braintree::Customer.stubs(:update => update_result)
129
+
130
+ customer = Ashmont::Customer.new(token)
131
+ customer.save(updates).should be_true
132
+
133
+ Braintree::Customer.should have_received(:update).with(token, :email => "somebody@example.com", :credit_card => {})
134
+ customer.billing_email.should == "somebody@example.com"
135
+ end
136
+
137
+ it "updates a remote customer with a credit card" do
138
+ token = "xyz"
139
+ payment_method_token = "abc"
140
+ credit_card = stub("credit_card", :token => payment_method_token)
141
+ remote_customer = stub("customer", :credit_cards => [credit_card], :id => token)
142
+ update_result = stub("result", :success? => true, :customer => remote_customer)
143
+ Braintree::Customer.stubs(:update => update_result)
144
+ Braintree::Customer.stubs(:find => remote_customer)
145
+
146
+ customer = Ashmont::Customer.new(token)
147
+ customer.save(
148
+ :email => "jrobot@example.com",
149
+ :cardholder_name => "Jim Robot",
150
+ :number => "4111111111111115",
151
+ :cvv => "123",
152
+ :expiration_month => 5,
153
+ :expiration_year => 2013,
154
+ :street_address => "1 E Main St",
155
+ :extended_address => "Suite 3",
156
+ :locality => "Chicago",
157
+ :region => "Illinois",
158
+ :postal_code => "60622",
159
+ :country_name => "United States of America"
160
+ )
161
+
162
+ Braintree::Customer.should have_received(:update).with(
163
+ token,
164
+ :email => "jrobot@example.com",
165
+ :credit_card => {
166
+ :cardholder_name => "Jim Robot",
167
+ :number => "4111111111111115",
168
+ :cvv => "123",
169
+ :expiration_month => 5,
170
+ :expiration_year => 2013,
171
+ :billing_address => {
172
+ :street_address => "1 E Main St",
173
+ :extended_address => "Suite 3",
174
+ :locality => "Chicago",
175
+ :region => "Illinois",
176
+ :postal_code => "60622",
177
+ :country_name => "United States of America"
178
+ },
179
+ :options => {
180
+ :update_existing_token => payment_method_token
181
+ }
182
+ }
183
+ )
184
+ end
185
+
186
+ it "returns errors while updating an invalid customer" do
187
+ error_messages = "error messages"
188
+ errors = "errors"
189
+ verification = "failure"
190
+ update_result = stub("result",
191
+ :success? => false,
192
+ :errors => error_messages,
193
+ :credit_card_verification => verification)
194
+ Braintree::Customer.stubs(:update => update_result)
195
+ Ashmont::Errors.stubs(:new => errors)
196
+
197
+ customer = Ashmont::Customer.new("xyz")
198
+ customer.save("email" => "ben.franklin@example.com").should be_false
199
+
200
+ Ashmont::Errors.should have_received(:new).with(verification, error_messages)
201
+ customer.errors.should == errors
202
+ end
203
+
204
+ it "delete a remote customer" do
205
+ token = "xyz"
206
+ Braintree::Customer.stubs(:delete)
207
+
208
+ customer = Ashmont::Customer.new(token)
209
+ customer.delete
210
+
211
+ Braintree::Customer.should have_received(:delete).with(token)
212
+ end
213
+
214
+ it "has billing info with a credit card" do
215
+ token = "abc"
216
+ credit_card = stub("credit_card", :token => token)
217
+ remote_customer = stub("customer", :credit_cards => [credit_card])
218
+ Braintree::Customer.stubs(:find => remote_customer)
219
+
220
+ customer = Ashmont::Customer.new("xyz")
221
+ customer.should have_billing_info
222
+ customer.payment_method_token.should == token
223
+ end
224
+
225
+ it "doesn't have billing info without a credit card" do
226
+ remote_customer = stub("customer", :credit_cards => [])
227
+ Braintree::Customer.stubs(:find => remote_customer)
228
+
229
+ customer = Ashmont::Customer.new("xyz")
230
+ customer.should_not have_billing_info
231
+ customer.payment_method_token.should be_nil
232
+ end
233
+
234
+ %w(last_4 cardholder_name expiration_month expiration_year).each do |credit_card_attribute|
235
+ it "delegates ##{credit_card_attribute} to its credit card" do
236
+ credit_card = stub("credit_card", credit_card_attribute => "expected")
237
+ remote_customer = stub("customer", :credit_cards => [credit_card])
238
+ Braintree::Customer.stubs(:find => remote_customer)
239
+
240
+ Ashmont::Customer.new("xyz").send(credit_card_attribute).should == "expected"
241
+ end
242
+ end
243
+
244
+ %w(street_address extended_address locality region postal_code country_name).each do |billing_address_attribute|
245
+ it "delegates ##{billing_address_attribute} to its billing address" do
246
+ billing_address = stub("billing_address", billing_address_attribute => "expected")
247
+ credit_card = stub("credit_card", :billing_address => billing_address)
248
+ remote_customer = stub("customer", :credit_cards => [credit_card])
249
+ Braintree::Customer.stubs(:find => remote_customer)
250
+
251
+ Ashmont::Customer.new("xyz").send(billing_address_attribute).should == "expected"
252
+ end
253
+ end
254
+
255
+ it "confirms a transparent redirect query string" do
256
+ token = "xyz"
257
+ query_string = "abcmagic"
258
+ remote_customer = stub("customer", :credit_cards => [], :id => token)
259
+ confirm_result = stub("result", :success? => true, :customer => remote_customer)
260
+ Braintree::TransparentRedirect.stubs(:confirm => confirm_result)
261
+
262
+ customer = Ashmont::Customer.new
263
+ customer.confirm(query_string).should be_true
264
+
265
+ Braintree::TransparentRedirect.should have_received(:confirm).with(query_string)
266
+ customer.token.should == token
267
+ end
268
+
269
+ it "adds errors for an invalid transparent redirect query string" do
270
+ error_messages = "error messages"
271
+ errors = "errors"
272
+ verification = "failure"
273
+ confirm_result = stub("result",
274
+ :success? => false,
275
+ :errors => error_messages,
276
+ :credit_card_verification => verification)
277
+ Braintree::TransparentRedirect.stubs(:confirm => confirm_result)
278
+ Ashmont::Errors.stubs(:new => errors)
279
+
280
+ customer = Ashmont::Customer.new
281
+ customer.confirm("abc").should be_false
282
+
283
+ Ashmont::Errors.should have_received(:new).with(verification, error_messages)
284
+ customer.errors.should == errors
285
+ end
286
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ashmont::Errors do
4
+ it "adds an error for a declined number" do
5
+ errors_for(:status => "processor_declined", :processor_response_text => "failure").
6
+ should include("number was denied by the payment processor with the message: failure")
7
+ end
8
+
9
+ it "adds an error for a mismatched cvv" do
10
+ errors_for(:status => "gateway_rejected").should include("cvv did not match")
11
+ end
12
+
13
+ it "adds a generic card number error" do
14
+ errors_for(:messages => { :number => "Credit card number is unsupported" }).
15
+ should include("number is unsupported")
16
+ end
17
+
18
+ it "adds a generic cvv error" do
19
+ errors_for(:messages => { :CVV => "CVV is unsupported" }).
20
+ should include("cvv is unsupported")
21
+ end
22
+
23
+ it "adds a generic expiration_month error" do
24
+ errors_for(:messages => { :expiration_month => "Expiration month is unsupported" }).
25
+ should include("expiration_month is unsupported")
26
+ end
27
+
28
+ it "adds a generic expiration_year error" do
29
+ errors_for(:messages => { :expiration_year => "Expiration year is unsupported" }).
30
+ should include("expiration_year is unsupported")
31
+ end
32
+
33
+ it "handles error results without a status" do
34
+ result = {}
35
+ errors = [ stub("error", :attribute => "foo", :message => "bar") ]
36
+ expect { Ashmont::Errors.new(result, errors).to_hash }.to_not raise_error(NoMethodError)
37
+ end
38
+
39
+ def errors_for(options = {})
40
+ result = stub("result",
41
+ :status => options[:status] || "rejected",
42
+ :processor_response_text => options[:processor_response_text] || "error")
43
+ errors = (options[:messages] || {}).map do |attribute, message|
44
+ stub("error", :attribute => attribute.to_s, :message => message)
45
+ end
46
+
47
+ Ashmont::Errors.new(result, errors).to_hash.inject([]) do |result, (attribute, message)|
48
+ result << [attribute, message].join(" ")
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,123 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ashmont::SubscribedCustomer do
4
+ it "delegates Customer methods to the customer" do
5
+ customer = stub("customer")
6
+ subscribed_customer = build_subscribed_customer(:customer => customer)
7
+ customer_methods = Ashmont::Customer.instance_methods(false)
8
+ merged_methods = [:errors, :save]
9
+ delegated_methods = customer_methods - merged_methods
10
+ delegated_methods.each do |method|
11
+ customer.stubs(method => "expected")
12
+ result = subscribed_customer.send(method, "argument")
13
+ customer.should have_received(method).with("argument")
14
+ result.should == "expected"
15
+ end
16
+ end
17
+
18
+ it "delegates some Subscription methods to the subscription" do
19
+ subscription = stub("subscription")
20
+ subscribed_customer = build_subscribed_customer(:subscription => subscription)
21
+ %w(reload status next_billing_date transactions most_recent_transaction retry_charge past_due?).each do |method|
22
+ subscription.stubs(method => "expected")
23
+ result = subscribed_customer.send(method, "argument")
24
+ subscription.should have_received(method).with("argument")
25
+ result.should == "expected"
26
+ end
27
+ end
28
+
29
+ it "delegates #subscription_token to its subscription's token" do
30
+ subscription = stub("subscription", :token => "expected")
31
+ subscribed_customer = build_subscribed_customer(:subscription => subscription)
32
+ subscribed_customer.subscription_token.should == "expected"
33
+ end
34
+
35
+ it "#reloads the subscription" do
36
+ subscription = stub("subscription", :reload => "expected")
37
+ subscribed_customer = build_subscribed_customer(:subscription => subscription)
38
+
39
+ result = subscribed_customer.reload
40
+
41
+ subscription.should have_received(:reload)
42
+ result.should == "expected"
43
+ end
44
+
45
+ it "can #save the customer" do
46
+ attributes = { :email => "somebody@example.com" }
47
+ customer = stub("customer", :save => true, :token => nil)
48
+ subscribed_customer = build_subscribed_customer(:customer => customer)
49
+
50
+ result = subscribed_customer.save(attributes)
51
+
52
+ customer.should have_received(:save).with(attributes)
53
+ result.should be_true
54
+ end
55
+
56
+ it "saves a new customer without attributes" do
57
+ customer = stub("customer", :token => nil, :save => true)
58
+ subscribed_customer = build_subscribed_customer(:customer => customer)
59
+
60
+ subscribed_customer.save({})
61
+
62
+ customer.should have_received(:save).with({})
63
+ end
64
+
65
+ it "doesn't #save the customer without any customer attributes" do
66
+ attributes = { :price => 49 }
67
+ customer = stub("customer", :save => true, :token => "xyz")
68
+ subscribed_customer = build_subscribed_customer(:customer => customer)
69
+
70
+ subscribed_customer.save(attributes)
71
+
72
+ customer.should have_received(:save).never
73
+ end
74
+
75
+ it "can #save the subscription" do
76
+ attributes = { :plan_id => 41, :price => 15 }
77
+ subscription = stub("subscription", :save => "expected", :past_due? => false)
78
+ payment_method_token = "xyz"
79
+ customer = stub("customer", :payment_method_token => payment_method_token, :token => "xyz")
80
+ subscribed_customer = build_subscribed_customer(:subscription => subscription, :customer => customer)
81
+
82
+ result = subscribed_customer.save(attributes)
83
+
84
+ subscription.should have_received(:save).with(:plan_id => 41, :price => "15", :payment_method_token => payment_method_token)
85
+ result.should == "expected"
86
+ end
87
+
88
+ it "retries the subscription when past due" do
89
+ subscription = stub("subscription", :past_due? => true, :retry_charge => true)
90
+
91
+ subscribed_customer = build_subscribed_customer(:subscription => subscription)
92
+ subscribed_customer.save({})
93
+
94
+ subscription.should have_received(:retry_charge)
95
+ end
96
+
97
+ it "merges #errors from the customer and subscription" do
98
+ subscription = stub("subscription", :errors => stub_errors("one" => "first"))
99
+ customer = stub("customer", :errors => stub_errors("two" => "second"))
100
+ subscribed_customer = build_subscribed_customer(:subscription => subscription, :customer => customer)
101
+
102
+ subscribed_customer.errors.to_hash.should == { "one" => "first", "two" => "second" }
103
+ end
104
+
105
+ def build_subscribed_customer(options = {})
106
+ Ashmont::SubscribedCustomer.new(
107
+ options[:customer] || build_customer,
108
+ options[:subscription] || build_subscription
109
+ )
110
+ end
111
+
112
+ def build_customer
113
+ Ashmont::Customer.new("xyz")
114
+ end
115
+
116
+ def build_subscription
117
+ Ashmont::Subscription.new("abc", :status => "Active")
118
+ end
119
+
120
+ def stub_errors(messages)
121
+ stub("errors", :to_hash => messages)
122
+ end
123
+ end