ashmont 0.0.12

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