saucy 0.2.17 → 0.2.18
Sign up to get free protection for your applications and to get access to all the features.
- data/app/controllers/billings_controller.rb +1 -1
- data/app/controllers/plans_controller.rb +1 -1
- data/lib/saucy.rb +1 -1
- data/lib/saucy/account.rb +1 -182
- data/lib/saucy/subscription.rb +213 -0
- data/spec/models/account_spec.rb +0 -287
- data/spec/models/subscription_spec.rb +390 -0
- metadata +30 -28
@@ -15,7 +15,7 @@ class BillingsController < ApplicationController
|
|
15
15
|
|
16
16
|
def update
|
17
17
|
@account = current_account
|
18
|
-
if @account.
|
18
|
+
if @account.save_customer_and_subscription!(params[:account])
|
19
19
|
redirect_to account_billing_path(@account), :notice => t('.update.notice', :default => "Billing information updated successfully")
|
20
20
|
else
|
21
21
|
render :edit
|
@@ -13,7 +13,7 @@ class PlansController < ApplicationController
|
|
13
13
|
def update
|
14
14
|
@plans = Plan.ordered
|
15
15
|
@account = current_account
|
16
|
-
if @account.
|
16
|
+
if @account.save_customer_and_subscription!(params[:account])
|
17
17
|
redirect_to edit_account_path(@account), :notice => t('.update.notice', :default => "Plan changed successfully")
|
18
18
|
else
|
19
19
|
render :edit
|
data/lib/saucy.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
require 'saucy/user'
|
2
2
|
require 'saucy/account'
|
3
|
+
require 'saucy/subscription'
|
3
4
|
require 'saucy/project'
|
4
5
|
require 'saucy/plan'
|
5
6
|
require 'saucy/account_authorization'
|
6
7
|
require 'saucy/configuration'
|
7
8
|
require 'saucy/engine'
|
8
9
|
require 'saucy/projects_controller'
|
9
|
-
|
data/lib/saucy/account.rb
CHANGED
@@ -3,17 +3,8 @@ module Saucy
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
included do
|
6
|
-
|
7
|
-
require "braintree"
|
6
|
+
include Saucy::Subscription
|
8
7
|
|
9
|
-
CUSTOMER_ATTRIBUTES = { :cardholder_name => :cardholder_name,
|
10
|
-
:billing_email => :email,
|
11
|
-
:card_number => :number,
|
12
|
-
:expiration_month => :expiration_month,
|
13
|
-
:expiration_year => :expiration_year,
|
14
|
-
:verification_code => :cvv }
|
15
|
-
|
16
|
-
extend ActiveSupport::Memoizable
|
17
8
|
has_many :memberships, :dependent => :destroy
|
18
9
|
has_many :users, :through => :memberships
|
19
10
|
has_many :projects, :dependent => :destroy
|
@@ -28,20 +19,11 @@ module Saucy
|
|
28
19
|
validates_uniqueness_of :name, :keyword
|
29
20
|
validates_presence_of :name, :keyword, :plan_id
|
30
21
|
|
31
|
-
attr_accessor *CUSTOMER_ATTRIBUTES.keys
|
32
|
-
|
33
22
|
attr_accessible :name, :keyword
|
34
23
|
|
35
24
|
validates_format_of :keyword,
|
36
25
|
:with => %r{^[a-z0-9]+$},
|
37
26
|
:message => "must be only lower case letters."
|
38
|
-
|
39
|
-
before_create :create_customer
|
40
|
-
before_create :create_subscription, :if => :billed?
|
41
|
-
after_destroy :destroy_customer
|
42
|
-
|
43
|
-
memoize :customer
|
44
|
-
memoize :subscription
|
45
27
|
end
|
46
28
|
|
47
29
|
module InstanceMethods
|
@@ -68,169 +50,6 @@ module Saucy
|
|
68
50
|
def memberships_by_name
|
69
51
|
memberships.by_name
|
70
52
|
end
|
71
|
-
|
72
|
-
def customer
|
73
|
-
Braintree::Customer.find(customer_token) if customer_token
|
74
|
-
end
|
75
|
-
|
76
|
-
def credit_card
|
77
|
-
customer.credit_cards[0] if customer && customer.credit_cards.any?
|
78
|
-
end
|
79
|
-
|
80
|
-
def subscription
|
81
|
-
Braintree::Subscription.find(subscription_token) if subscription_token
|
82
|
-
end
|
83
|
-
|
84
|
-
def save_braintree!(attributes)
|
85
|
-
successful = true
|
86
|
-
self.plan = ::Plan.find(attributes[:plan_id]) if changing_plan?(attributes)
|
87
|
-
if changing_braintree_attributes?(attributes)
|
88
|
-
successful = update_braintree_customer(attributes)
|
89
|
-
end
|
90
|
-
if successful && changing_plan?(attributes)
|
91
|
-
save_subscription
|
92
|
-
flush_cache :subscription
|
93
|
-
end
|
94
|
-
successful && save
|
95
|
-
end
|
96
|
-
|
97
|
-
def can_change_plan_to?(new_plan)
|
98
|
-
plan.limits.where(:value_type => :number).all? do |limit|
|
99
|
-
new_plan.limit(limit.name).value >= send(limit.name).count
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
def past_due?
|
104
|
-
subscription_status == Braintree::Subscription::Status::PastDue
|
105
|
-
end
|
106
|
-
|
107
|
-
private
|
108
|
-
|
109
|
-
def changing_plan?(attributes)
|
110
|
-
attributes[:plan_id].present?
|
111
|
-
end
|
112
|
-
|
113
|
-
def changing_braintree_attributes?(attributes)
|
114
|
-
CUSTOMER_ATTRIBUTES.keys.any? { |attribute| attributes[attribute].present? }
|
115
|
-
end
|
116
|
-
|
117
|
-
def set_braintree_attributes(attributes)
|
118
|
-
CUSTOMER_ATTRIBUTES.keys.each do |attribute|
|
119
|
-
send("#{attribute}=", attributes[attribute]) if attributes[attribute].present?
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
def update_braintree_customer(attributes)
|
124
|
-
set_braintree_attributes(attributes)
|
125
|
-
result = Braintree::Customer.update(customer_token, customer_attributes)
|
126
|
-
handle_customer_result(result)
|
127
|
-
result.success?
|
128
|
-
end
|
129
|
-
|
130
|
-
def save_subscription
|
131
|
-
if subscription
|
132
|
-
Braintree::Subscription.update(subscription_token, :plan_id => plan_id)
|
133
|
-
elsif plan.billed?
|
134
|
-
create_subscription
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
def customer_attributes
|
139
|
-
{
|
140
|
-
:email => billing_email,
|
141
|
-
:credit_card => credit_card_attributes
|
142
|
-
}
|
143
|
-
end
|
144
|
-
|
145
|
-
def credit_card_attributes
|
146
|
-
if plan.billed?
|
147
|
-
card_attributes = { :cardholder_name => cardholder_name,
|
148
|
-
:number => card_number,
|
149
|
-
:expiration_month => expiration_month,
|
150
|
-
:expiration_year => expiration_year,
|
151
|
-
:cvv => verification_code }
|
152
|
-
if credit_card
|
153
|
-
card_attributes.merge!(:options => credit_card_options)
|
154
|
-
end
|
155
|
-
card_attributes
|
156
|
-
else
|
157
|
-
{}
|
158
|
-
end
|
159
|
-
end
|
160
|
-
|
161
|
-
def credit_card_options
|
162
|
-
if customer && customer.credit_cards.any?
|
163
|
-
{ :update_existing_token => credit_card.token }
|
164
|
-
else
|
165
|
-
{}
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
def create_customer
|
170
|
-
result = Braintree::Customer.create(customer_attributes)
|
171
|
-
handle_customer_result(result)
|
172
|
-
end
|
173
|
-
|
174
|
-
def destroy_customer
|
175
|
-
Braintree::Customer.delete(customer_token)
|
176
|
-
end
|
177
|
-
|
178
|
-
def handle_customer_result(result)
|
179
|
-
if result.success?
|
180
|
-
self.customer_token = result.customer.id
|
181
|
-
flush_cache :customer
|
182
|
-
else
|
183
|
-
verification = result.credit_card_verification
|
184
|
-
if verification && verification.status == "processor_declined"
|
185
|
-
errors[:card_number] << "was denied by the payment processor with the message: #{verification.processor_response_text}"
|
186
|
-
elsif verification && verification.status == "gateway_rejected"
|
187
|
-
errors[:verification_code] << "did not match"
|
188
|
-
elsif result.errors.any?
|
189
|
-
result.errors.each do |error|
|
190
|
-
if error.attribute == "number"
|
191
|
-
errors[:card_number] << error.message.gsub("Credit card number ", "")
|
192
|
-
elsif error.attribute == "CVV"
|
193
|
-
errors[:verification_code] << error.message.gsub("CVV ", "")
|
194
|
-
elsif error.attribute == "expiration_month"
|
195
|
-
errors[:expiration_month] << error.message.gsub("Expiration month ", "")
|
196
|
-
elsif error.attribute == "expiration_year"
|
197
|
-
errors[:expiration_year] << error.message.gsub("Expiration year ", "")
|
198
|
-
end
|
199
|
-
end
|
200
|
-
end
|
201
|
-
end
|
202
|
-
result.success?
|
203
|
-
end
|
204
|
-
|
205
|
-
def create_subscription
|
206
|
-
result = Braintree::Subscription.create(
|
207
|
-
:payment_method_token => credit_card.token,
|
208
|
-
:plan_id => plan_id
|
209
|
-
)
|
210
|
-
if result.success?
|
211
|
-
self.subscription_token = result.subscription.id
|
212
|
-
self.next_billing_date = result.subscription.next_billing_date
|
213
|
-
self.subscription_status = result.subscription.status
|
214
|
-
else
|
215
|
-
false
|
216
|
-
end
|
217
|
-
end
|
218
|
-
end
|
219
|
-
|
220
|
-
module ClassMethods
|
221
|
-
def update_subscriptions!
|
222
|
-
recently_billed = where("next_billing_date <= '#{Time.now}'")
|
223
|
-
recently_billed.each do |account|
|
224
|
-
account.subscription_status = account.subscription.status
|
225
|
-
account.next_billing_date = account.subscription.next_billing_date
|
226
|
-
account.save!
|
227
|
-
if account.past_due?
|
228
|
-
BillingMailer.problem(account, account.subscription.transactions.last).deliver!
|
229
|
-
else
|
230
|
-
BillingMailer.receipt(account, account.subscription.transactions.last).deliver!
|
231
|
-
end
|
232
|
-
end
|
233
|
-
end
|
234
53
|
end
|
235
54
|
end
|
236
55
|
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
module Saucy
|
2
|
+
module Subscription
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
require "rubygems"
|
7
|
+
require "braintree"
|
8
|
+
|
9
|
+
extend ActiveSupport::Memoizable
|
10
|
+
|
11
|
+
CUSTOMER_ATTRIBUTES = { :cardholder_name => :cardholder_name,
|
12
|
+
:billing_email => :email,
|
13
|
+
:card_number => :number,
|
14
|
+
:expiration_month => :expiration_month,
|
15
|
+
:expiration_year => :expiration_year,
|
16
|
+
:verification_code => :cvv }
|
17
|
+
|
18
|
+
attr_accessor *CUSTOMER_ATTRIBUTES.keys
|
19
|
+
|
20
|
+
before_create :create_customer
|
21
|
+
before_create :create_subscription, :if => :billed?
|
22
|
+
after_destroy :destroy_customer
|
23
|
+
|
24
|
+
memoize :customer
|
25
|
+
memoize :subscription
|
26
|
+
end
|
27
|
+
|
28
|
+
module InstanceMethods
|
29
|
+
def customer
|
30
|
+
Braintree::Customer.find(customer_token) if customer_token
|
31
|
+
end
|
32
|
+
|
33
|
+
def credit_card
|
34
|
+
customer.credit_cards[0] if customer && customer.credit_cards.any?
|
35
|
+
end
|
36
|
+
|
37
|
+
def subscription
|
38
|
+
Braintree::Subscription.find(subscription_token) if subscription_token
|
39
|
+
end
|
40
|
+
|
41
|
+
def save_customer_and_subscription!(attributes)
|
42
|
+
successful = true
|
43
|
+
self.plan = ::Plan.find(attributes[:plan_id]) if changing_plan?(attributes)
|
44
|
+
if changing_customer_attributes?(attributes)
|
45
|
+
successful = update_customer(attributes)
|
46
|
+
end
|
47
|
+
if successful && past_due?
|
48
|
+
successful = retry_subscription_charge!
|
49
|
+
end
|
50
|
+
if successful && changing_plan?(attributes)
|
51
|
+
save_subscription
|
52
|
+
flush_cache :subscription
|
53
|
+
end
|
54
|
+
successful && save
|
55
|
+
end
|
56
|
+
|
57
|
+
def can_change_plan_to?(new_plan)
|
58
|
+
plan.limits.where(:value_type => :number).all? do |limit|
|
59
|
+
new_plan.limit(limit.name).value >= send(limit.name).count
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def past_due?
|
64
|
+
subscription_status == Braintree::Subscription::Status::PastDue
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def retry_subscription_charge!
|
70
|
+
authorized_transaction = Braintree::Subscription.retry_charge(subscription.id).transaction
|
71
|
+
result = Braintree::Transaction.submit_for_settlement(authorized_transaction.id)
|
72
|
+
handle_errors(authorized_transaction, result.errors) if !result.success?
|
73
|
+
update_subscription_cache!
|
74
|
+
result.success?
|
75
|
+
end
|
76
|
+
|
77
|
+
def update_subscription_cache!
|
78
|
+
flush_cache :subscription
|
79
|
+
update_attribute(:subscription_status, subscription.status)
|
80
|
+
update_attribute(:next_billing_date, subscription.next_billing_date)
|
81
|
+
end
|
82
|
+
|
83
|
+
def changing_plan?(attributes)
|
84
|
+
attributes[:plan_id].present?
|
85
|
+
end
|
86
|
+
|
87
|
+
def changing_customer_attributes?(attributes)
|
88
|
+
CUSTOMER_ATTRIBUTES.keys.any? { |attribute| attributes[attribute].present? }
|
89
|
+
end
|
90
|
+
|
91
|
+
def set_customer_attributes(attributes)
|
92
|
+
CUSTOMER_ATTRIBUTES.keys.each do |attribute|
|
93
|
+
send("#{attribute}=", attributes[attribute]) if attributes[attribute].present?
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def update_customer(attributes)
|
98
|
+
set_customer_attributes(attributes)
|
99
|
+
result = Braintree::Customer.update(customer_token, customer_attributes)
|
100
|
+
handle_customer_result(result)
|
101
|
+
result.success?
|
102
|
+
end
|
103
|
+
|
104
|
+
def save_subscription
|
105
|
+
if subscription
|
106
|
+
Braintree::Subscription.update(subscription_token, :plan_id => plan_id)
|
107
|
+
elsif plan.billed?
|
108
|
+
create_subscription
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def customer_attributes
|
113
|
+
{
|
114
|
+
:email => billing_email,
|
115
|
+
:credit_card => credit_card_attributes
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
def credit_card_attributes
|
120
|
+
if plan.billed?
|
121
|
+
card_attributes = { :cardholder_name => cardholder_name,
|
122
|
+
:number => card_number,
|
123
|
+
:expiration_month => expiration_month,
|
124
|
+
:expiration_year => expiration_year,
|
125
|
+
:cvv => verification_code }
|
126
|
+
if credit_card
|
127
|
+
card_attributes.merge!(:options => credit_card_options)
|
128
|
+
end
|
129
|
+
card_attributes
|
130
|
+
else
|
131
|
+
{}
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def credit_card_options
|
136
|
+
if customer && customer.credit_cards.any?
|
137
|
+
{ :update_existing_token => credit_card.token }
|
138
|
+
else
|
139
|
+
{}
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def create_customer
|
144
|
+
result = Braintree::Customer.create(customer_attributes)
|
145
|
+
handle_customer_result(result)
|
146
|
+
end
|
147
|
+
|
148
|
+
def destroy_customer
|
149
|
+
Braintree::Customer.delete(customer_token)
|
150
|
+
end
|
151
|
+
|
152
|
+
def handle_customer_result(result)
|
153
|
+
if result.success?
|
154
|
+
self.customer_token = result.customer.id
|
155
|
+
flush_cache :customer
|
156
|
+
else
|
157
|
+
handle_errors(result.credit_card_verification, result.errors)
|
158
|
+
end
|
159
|
+
result.success?
|
160
|
+
end
|
161
|
+
|
162
|
+
def handle_errors(result, remote_errors)
|
163
|
+
if result && result.status == "processor_declined"
|
164
|
+
errors[:card_number] << "was denied by the payment processor with the message: #{result.processor_response_text}"
|
165
|
+
elsif result && result.status == "gateway_rejected"
|
166
|
+
errors[:verification_code] << "did not match"
|
167
|
+
elsif remote_errors.any?
|
168
|
+
remote_errors.each do |error|
|
169
|
+
if error.attribute == "number"
|
170
|
+
errors[:card_number] << error.message.gsub("Credit card number ", "")
|
171
|
+
elsif error.attribute == "CVV"
|
172
|
+
errors[:verification_code] << error.message.gsub("CVV ", "")
|
173
|
+
elsif error.attribute == "expiration_month"
|
174
|
+
errors[:expiration_month] << error.message.gsub("Expiration month ", "")
|
175
|
+
elsif error.attribute == "expiration_year"
|
176
|
+
errors[:expiration_year] << error.message.gsub("Expiration year ", "")
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def create_subscription
|
183
|
+
result = Braintree::Subscription.create(
|
184
|
+
:payment_method_token => credit_card.token,
|
185
|
+
:plan_id => plan_id
|
186
|
+
)
|
187
|
+
if result.success?
|
188
|
+
self.subscription_token = result.subscription.id
|
189
|
+
self.next_billing_date = result.subscription.next_billing_date
|
190
|
+
self.subscription_status = result.subscription.status
|
191
|
+
else
|
192
|
+
false
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
module ClassMethods
|
198
|
+
def update_subscriptions!
|
199
|
+
recently_billed = where("next_billing_date <= '#{Time.now}'")
|
200
|
+
recently_billed.each do |account|
|
201
|
+
account.subscription_status = account.subscription.status
|
202
|
+
account.next_billing_date = account.subscription.next_billing_date
|
203
|
+
account.save!
|
204
|
+
if account.past_due?
|
205
|
+
BillingMailer.problem(account, account.subscription.transactions.last).deliver!
|
206
|
+
else
|
207
|
+
BillingMailer.receipt(account, account.subscription.transactions.last).deliver!
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
data/spec/models/account_spec.rb
CHANGED
@@ -65,291 +65,4 @@ describe Account do
|
|
65
65
|
|
66
66
|
result.should == expected
|
67
67
|
end
|
68
|
-
|
69
|
-
it "manifests braintree processor_declined errors as errors on number and doesn't save" do
|
70
|
-
FakeBraintree.failures["4111111111111112"] = { "message" => "Do Not Honor", "code" => "2000", "status" => "processor_declined" }
|
71
|
-
account = Factory.build(:account,
|
72
|
-
:cardholder_name => "Ralph Robot",
|
73
|
-
:billing_email => "ralph@example.com",
|
74
|
-
:card_number => "4111111111111112",
|
75
|
-
:expiration_month => 5,
|
76
|
-
:expiration_year => 2012,
|
77
|
-
:plan => Factory(:paid_plan))
|
78
|
-
account.save.should_not be
|
79
|
-
FakeBraintree.customers.should be_empty
|
80
|
-
account.persisted?.should_not be
|
81
|
-
account.errors[:card_number].any? { |e| e =~ /denied/ }.should be
|
82
|
-
end
|
83
|
-
|
84
|
-
it "manifests braintree gateway_rejected errors as errors on number and doesn't save" do
|
85
|
-
FakeBraintree.failures["4111111111111112"] = { "message" => "Gateway Rejected: cvv", "code" => "N", "status" => "gateway_rejected" }
|
86
|
-
account = Factory.build(:account,
|
87
|
-
:cardholder_name => "Ralph Robot",
|
88
|
-
:billing_email => "ralph@example.com",
|
89
|
-
:card_number => "4111111111111112",
|
90
|
-
:expiration_month => 5,
|
91
|
-
:expiration_year => 2012,
|
92
|
-
:verification_code => 200,
|
93
|
-
:plan => Factory(:paid_plan))
|
94
|
-
account.save.should_not be
|
95
|
-
FakeBraintree.customers.should be_empty
|
96
|
-
account.persisted?.should_not be
|
97
|
-
account.errors[:verification_code].any? { |e| e =~ /did not match/ }.should be
|
98
|
-
end
|
99
|
-
|
100
|
-
it "manifests braintree gateway_rejected errors as errors on number and doesn't save" do
|
101
|
-
FakeBraintree.failures["4111111111111111"] = { "message" => "Credit card number is invalid.", "errors" => { "customer" => { "errors" => [], "credit-card" => { "errors" => [{ "message" => "Credit card number is invalid.", "code" => 81715, "attribute" => :number }] }}}}
|
102
|
-
account = Factory.build(:account,
|
103
|
-
:cardholder_name => "Ralph Robot",
|
104
|
-
:billing_email => "ralph@example.com",
|
105
|
-
:card_number => "4111111111111111",
|
106
|
-
:expiration_month => 5,
|
107
|
-
:expiration_year => 2012,
|
108
|
-
:verification_code => 123,
|
109
|
-
:plan => Factory(:paid_plan))
|
110
|
-
account.save.should_not be
|
111
|
-
FakeBraintree.customers.should be_empty
|
112
|
-
account.persisted?.should_not be
|
113
|
-
account.errors[:card_number].any? { |e| e =~ /is invalid/ }.should be
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
describe Account, "with a paid plan" do
|
118
|
-
subject do
|
119
|
-
Factory(:account,
|
120
|
-
:cardholder_name => "Ralph Robot",
|
121
|
-
:billing_email => "ralph@example.com",
|
122
|
-
:card_number => "4111111111111111",
|
123
|
-
:verification_code => "123",
|
124
|
-
:expiration_month => 5,
|
125
|
-
:expiration_year => 2012,
|
126
|
-
:plan => Factory(:paid_plan))
|
127
|
-
end
|
128
|
-
|
129
|
-
it "has a customer_token" do
|
130
|
-
subject.customer_token.should_not be_nil
|
131
|
-
end
|
132
|
-
|
133
|
-
it "has a subscription_token" do
|
134
|
-
subject.subscription_token.should_not be_nil
|
135
|
-
end
|
136
|
-
|
137
|
-
it "has a customer" do
|
138
|
-
subject.customer.should_not be_nil
|
139
|
-
end
|
140
|
-
|
141
|
-
it "has a credit card" do
|
142
|
-
subject.credit_card.should_not be_nil
|
143
|
-
end
|
144
|
-
|
145
|
-
it "has a subscription" do
|
146
|
-
subject.subscription.should_not be_nil
|
147
|
-
end
|
148
|
-
|
149
|
-
it "has a next_billing_date" do
|
150
|
-
subject.next_billing_date.should_not be_nil
|
151
|
-
end
|
152
|
-
|
153
|
-
it "has an active subscription status" do
|
154
|
-
subject.subscription_status.should == Braintree::Subscription::Status::Active
|
155
|
-
end
|
156
|
-
|
157
|
-
it "is not past due" do
|
158
|
-
subject.past_due?.should_not be
|
159
|
-
end
|
160
|
-
|
161
|
-
it "creates a braintree customer, credit card, and subscription" do
|
162
|
-
FakeBraintree.customers[subject.customer_token].should_not be_nil
|
163
|
-
FakeBraintree.customers[subject.customer_token]["credit_cards"].first.should_not be_nil
|
164
|
-
FakeBraintree.subscriptions[subject.subscription_token].should_not be_nil
|
165
|
-
end
|
166
|
-
|
167
|
-
it "changes the subscription when the plan is changed" do
|
168
|
-
new_plan = Factory(:paid_plan, :name => "New Plan")
|
169
|
-
subject.save_braintree!(:plan_id => new_plan.id)
|
170
|
-
FakeBraintree.subscriptions[subject.subscription_token]["plan_id"].should == new_plan.id
|
171
|
-
end
|
172
|
-
|
173
|
-
it "updates the customer and credit card information when changed" do
|
174
|
-
subject.save_braintree!(:billing_email => "jrobot@example.com",
|
175
|
-
:cardholder_name => "Jim Robot",
|
176
|
-
:card_number => "4111111111111115",
|
177
|
-
:verification_code => "123",
|
178
|
-
:expiration_month => 5,
|
179
|
-
:expiration_year => 2013)
|
180
|
-
subject.customer.email.should == "jrobot@example.com"
|
181
|
-
subject.credit_card.cardholder_name.should == "Jim Robot"
|
182
|
-
end
|
183
|
-
|
184
|
-
it "deletes the customer when deleted" do
|
185
|
-
subject.destroy
|
186
|
-
FakeBraintree.customers[subject.customer_token].should be_nil
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
describe Account, "with a free plan" do
|
191
|
-
subject do
|
192
|
-
Factory(:account, :plan => Factory(:plan))
|
193
|
-
end
|
194
|
-
|
195
|
-
it "has a customer_token" do
|
196
|
-
subject.customer_token.should_not be_nil
|
197
|
-
end
|
198
|
-
|
199
|
-
it "has a customer" do
|
200
|
-
subject.customer.should_not be_nil
|
201
|
-
end
|
202
|
-
|
203
|
-
it "doesn't have a credit_card" do
|
204
|
-
subject.credit_card.should be_nil
|
205
|
-
end
|
206
|
-
|
207
|
-
it "doesn't have a subscription_token" do
|
208
|
-
subject.subscription_token.should be_nil
|
209
|
-
end
|
210
|
-
|
211
|
-
it "doesn't have a subscription" do
|
212
|
-
subject.subscription.should be_nil
|
213
|
-
end
|
214
|
-
|
215
|
-
it "creates a braintree customer" do
|
216
|
-
FakeBraintree.customers[subject.customer_token].should_not be_nil
|
217
|
-
end
|
218
|
-
|
219
|
-
it "doesn't create a credit card, and subscription" do
|
220
|
-
FakeBraintree.customers[subject.customer_token]["credit_cards"].should be_nil
|
221
|
-
FakeBraintree.subscriptions[subject.subscription_token].should be_nil
|
222
|
-
end
|
223
|
-
|
224
|
-
it "creates a credit card, and subscription when the plan is changed to a paid plan and the billing info is supplied" do
|
225
|
-
new_plan = Factory(:paid_plan, :name => "New Plan")
|
226
|
-
subject.save_braintree!(:plan_id => new_plan.id,
|
227
|
-
:cardholder_name => "Ralph Robot",
|
228
|
-
:billing_email => "ralph@example.com",
|
229
|
-
:card_number => "4111111111111111",
|
230
|
-
:verification_code => "123",
|
231
|
-
:expiration_month => 5,
|
232
|
-
:expiration_year => 2012)
|
233
|
-
|
234
|
-
FakeBraintree.customers[subject.customer_token]["credit_cards"].first.should_not be_nil
|
235
|
-
FakeBraintree.subscriptions[subject.subscription_token].should_not be_nil
|
236
|
-
FakeBraintree.subscriptions[subject.subscription_token]["plan_id"].should == new_plan.id
|
237
|
-
subject.credit_card.should_not be_nil
|
238
|
-
subject.subscription.should_not be_nil
|
239
|
-
end
|
240
|
-
|
241
|
-
it "doesn't create a credit card, and subscription when the plan is changed to a different free plan" do
|
242
|
-
new_plan = Factory(:plan, :name => "New Plan")
|
243
|
-
subject.save_braintree!(:plan_id => new_plan.id)
|
244
|
-
|
245
|
-
subject.credit_card.should be_nil
|
246
|
-
subject.subscription.should be_nil
|
247
|
-
end
|
248
|
-
end
|
249
|
-
|
250
|
-
describe Account, "with a plan and limits, and other plans" do
|
251
|
-
subject { Factory(:account) }
|
252
|
-
|
253
|
-
before do
|
254
|
-
Factory(:limit, :name => "users", :value => 1, :plan => subject.plan)
|
255
|
-
Factory(:limit, :name => "projects", :value => 1, :plan => subject.plan)
|
256
|
-
Factory(:limit, :name => "ssl", :value => 1, :value_type => :boolean, :plan => subject.plan)
|
257
|
-
@can_switch = Factory(:plan)
|
258
|
-
Factory(:limit, :name => "users", :value => 1, :plan => @can_switch)
|
259
|
-
Factory(:limit, :name => "projects", :value => 1, :plan => @can_switch)
|
260
|
-
Factory(:limit, :name => "ssl", :value => 0, :value_type => :boolean, :plan => @can_switch)
|
261
|
-
@cannot_switch = Factory(:plan)
|
262
|
-
Factory(:limit, :name => "users", :value => 0, :plan => @cannot_switch)
|
263
|
-
Factory(:limit, :name => "projects", :value => 0, :plan => @cannot_switch)
|
264
|
-
Factory(:limit, :name => "ssl", :value => 1, :value_type => :boolean, :plan => @cannot_switch)
|
265
|
-
|
266
|
-
Factory(:membership, :account => subject)
|
267
|
-
Factory(:project, :account => subject)
|
268
|
-
end
|
269
|
-
|
270
|
-
it "indicates whether the account can switch to another plan" do
|
271
|
-
subject.can_change_plan_to?(@can_switch).should be
|
272
|
-
subject.can_change_plan_to?(@cannot_switch).should_not be
|
273
|
-
end
|
274
|
-
end
|
275
|
-
|
276
|
-
describe Account, "with a paid subscription" do
|
277
|
-
subject do
|
278
|
-
Factory(:account,
|
279
|
-
:cardholder_name => "Ralph Robot",
|
280
|
-
:billing_email => "ralph@example.com",
|
281
|
-
:card_number => "4111111111111111",
|
282
|
-
:verification_code => "123",
|
283
|
-
:expiration_month => 5,
|
284
|
-
:expiration_year => 2012,
|
285
|
-
:plan => Factory(:paid_plan))
|
286
|
-
end
|
287
|
-
|
288
|
-
it "gets marked as past due and updates its next_billing_date when subscriptions are updated and it has been rejected by the gateway" do
|
289
|
-
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
290
|
-
subscription["status"] = Braintree::Subscription::Status::PastDue
|
291
|
-
subscription["next_billing_date"] = 2.months.from_now
|
292
|
-
|
293
|
-
Timecop.travel(subject.next_billing_date + 1.day) do
|
294
|
-
Account.update_subscriptions!
|
295
|
-
subject.reload.subscription_status.should == Braintree::Subscription::Status::PastDue
|
296
|
-
subject.next_billing_date.to_s.should == subscription["next_billing_date"].to_s
|
297
|
-
subject.past_due?.should be
|
298
|
-
end
|
299
|
-
end
|
300
|
-
|
301
|
-
it "gets marked as not past due and updates its next_billing_date when the subscription is active after its billing date" do
|
302
|
-
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
303
|
-
subscription["status"] = Braintree::Subscription::Status::Active
|
304
|
-
subscription["next_billing_date"] = 2.months.from_now
|
305
|
-
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Settled,
|
306
|
-
:subscription_id => subject.subscription_token }
|
307
|
-
subscription["transactions"] = [FakeBraintree.generated_transaction]
|
308
|
-
|
309
|
-
Timecop.travel(subject.next_billing_date + 1.day) do
|
310
|
-
Account.update_subscriptions!
|
311
|
-
subject.reload.subscription_status.should == Braintree::Subscription::Status::Active
|
312
|
-
subject.next_billing_date.to_s.should == subscription["next_billing_date"].to_s
|
313
|
-
end
|
314
|
-
end
|
315
|
-
|
316
|
-
it "receives a receipt email at it's billing email with transaction details" do
|
317
|
-
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
318
|
-
subscription["status"] = Braintree::Subscription::Status::Active
|
319
|
-
subscription["next_billing_date"] = 2.months.from_now
|
320
|
-
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Settled,
|
321
|
-
:subscription_id => subject.subscription_token }
|
322
|
-
subscription["transactions"] = [FakeBraintree.generated_transaction]
|
323
|
-
|
324
|
-
Timecop.travel(subject.next_billing_date + 1.day) do
|
325
|
-
ActionMailer::Base.deliveries.clear
|
326
|
-
|
327
|
-
Account.update_subscriptions!
|
328
|
-
|
329
|
-
ActionMailer::Base.deliveries.any? do |email|
|
330
|
-
email.to == [subject.billing_email] &&
|
331
|
-
email.subject =~ /receipt/i
|
332
|
-
end.should be
|
333
|
-
end
|
334
|
-
end
|
335
|
-
|
336
|
-
it "receives a receipt email at it's billing email with a notice that it failed when billing didn't work" do
|
337
|
-
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
338
|
-
subscription["status"] = Braintree::Subscription::Status::PastDue
|
339
|
-
subscription["next_billing_date"] = 2.months.from_now
|
340
|
-
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Failed,
|
341
|
-
:subscription_id => subject.subscription_token }
|
342
|
-
subscription["transactions"] = [FakeBraintree.generated_transaction]
|
343
|
-
|
344
|
-
Timecop.travel(subject.next_billing_date + 1.day) do
|
345
|
-
ActionMailer::Base.deliveries.clear
|
346
|
-
|
347
|
-
Account.update_subscriptions!
|
348
|
-
|
349
|
-
ActionMailer::Base.deliveries.any? do |email|
|
350
|
-
email.to == [subject.billing_email] &&
|
351
|
-
email.subject =~ /problem/i
|
352
|
-
end.should be
|
353
|
-
end
|
354
|
-
end
|
355
68
|
end
|
@@ -0,0 +1,390 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Account do
|
4
|
+
subject { Factory(:account) }
|
5
|
+
|
6
|
+
it "manifests braintree processor_declined errors as errors on number and doesn't save" do
|
7
|
+
FakeBraintree.failures["4111111111111112"] = { "message" => "Do Not Honor", "code" => "2000", "status" => "processor_declined" }
|
8
|
+
account = Factory.build(:account,
|
9
|
+
:cardholder_name => "Ralph Robot",
|
10
|
+
:billing_email => "ralph@example.com",
|
11
|
+
:card_number => "4111111111111112",
|
12
|
+
:expiration_month => 5,
|
13
|
+
:expiration_year => 2012,
|
14
|
+
:plan => Factory(:paid_plan))
|
15
|
+
account.save.should_not be
|
16
|
+
FakeBraintree.customers.should be_empty
|
17
|
+
account.persisted?.should_not be
|
18
|
+
account.errors[:card_number].any? { |e| e =~ /denied/ }.should be
|
19
|
+
end
|
20
|
+
|
21
|
+
it "manifests braintree gateway_rejected errors as errors on number and doesn't save" do
|
22
|
+
FakeBraintree.failures["4111111111111112"] = { "message" => "Gateway Rejected: cvv", "code" => "N", "status" => "gateway_rejected" }
|
23
|
+
account = Factory.build(:account,
|
24
|
+
:cardholder_name => "Ralph Robot",
|
25
|
+
:billing_email => "ralph@example.com",
|
26
|
+
:card_number => "4111111111111112",
|
27
|
+
:expiration_month => 5,
|
28
|
+
:expiration_year => 2012,
|
29
|
+
:verification_code => 200,
|
30
|
+
:plan => Factory(:paid_plan))
|
31
|
+
account.save.should_not be
|
32
|
+
FakeBraintree.customers.should be_empty
|
33
|
+
account.persisted?.should_not be
|
34
|
+
account.errors[:verification_code].any? { |e| e =~ /did not match/ }.should be
|
35
|
+
end
|
36
|
+
|
37
|
+
it "manifests braintree gateway_rejected errors as errors on number and doesn't save" do
|
38
|
+
FakeBraintree.failures["4111111111111111"] = { "message" => "Credit card number is invalid.", "errors" => { "customer" => { "errors" => [], "credit-card" => { "errors" => [{ "message" => "Credit card number is invalid.", "code" => 81715, "attribute" => :number }] }}}}
|
39
|
+
account = Factory.build(:account,
|
40
|
+
:cardholder_name => "Ralph Robot",
|
41
|
+
:billing_email => "ralph@example.com",
|
42
|
+
:card_number => "4111111111111111",
|
43
|
+
:expiration_month => 5,
|
44
|
+
:expiration_year => 2012,
|
45
|
+
:verification_code => 123,
|
46
|
+
:plan => Factory(:paid_plan))
|
47
|
+
account.save.should_not be
|
48
|
+
FakeBraintree.customers.should be_empty
|
49
|
+
account.persisted?.should_not be
|
50
|
+
account.errors[:card_number].any? { |e| e =~ /is invalid/ }.should be
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe Account, "with a paid plan" do
|
55
|
+
subject do
|
56
|
+
Factory(:account,
|
57
|
+
:cardholder_name => "Ralph Robot",
|
58
|
+
:billing_email => "ralph@example.com",
|
59
|
+
:card_number => "4111111111111111",
|
60
|
+
:verification_code => "123",
|
61
|
+
:expiration_month => 5,
|
62
|
+
:expiration_year => 2012,
|
63
|
+
:plan => Factory(:paid_plan))
|
64
|
+
end
|
65
|
+
|
66
|
+
it "has a customer_token" do
|
67
|
+
subject.customer_token.should_not be_nil
|
68
|
+
end
|
69
|
+
|
70
|
+
it "has a subscription_token" do
|
71
|
+
subject.subscription_token.should_not be_nil
|
72
|
+
end
|
73
|
+
|
74
|
+
it "has a customer" do
|
75
|
+
subject.customer.should_not be_nil
|
76
|
+
end
|
77
|
+
|
78
|
+
it "has a credit card" do
|
79
|
+
subject.credit_card.should_not be_nil
|
80
|
+
end
|
81
|
+
|
82
|
+
it "has a subscription" do
|
83
|
+
subject.subscription.should_not be_nil
|
84
|
+
end
|
85
|
+
|
86
|
+
it "has a next_billing_date" do
|
87
|
+
subject.next_billing_date.should_not be_nil
|
88
|
+
end
|
89
|
+
|
90
|
+
it "has an active subscription status" do
|
91
|
+
subject.subscription_status.should == Braintree::Subscription::Status::Active
|
92
|
+
end
|
93
|
+
|
94
|
+
it "is not past due" do
|
95
|
+
subject.past_due?.should_not be
|
96
|
+
end
|
97
|
+
|
98
|
+
it "creates a braintree customer, credit card, and subscription" do
|
99
|
+
FakeBraintree.customers[subject.customer_token].should_not be_nil
|
100
|
+
FakeBraintree.customers[subject.customer_token]["credit_cards"].first.should_not be_nil
|
101
|
+
FakeBraintree.subscriptions[subject.subscription_token].should_not be_nil
|
102
|
+
end
|
103
|
+
|
104
|
+
it "changes the subscription when the plan is changed" do
|
105
|
+
new_plan = Factory(:paid_plan, :name => "New Plan")
|
106
|
+
subject.save_customer_and_subscription!(:plan_id => new_plan.id)
|
107
|
+
FakeBraintree.subscriptions[subject.subscription_token]["plan_id"].should == new_plan.id
|
108
|
+
end
|
109
|
+
|
110
|
+
it "updates the customer and credit card information when changed" do
|
111
|
+
subject.save_customer_and_subscription!(:billing_email => "jrobot@example.com",
|
112
|
+
:cardholder_name => "Jim Robot",
|
113
|
+
:card_number => "4111111111111115",
|
114
|
+
:verification_code => "123",
|
115
|
+
:expiration_month => 5,
|
116
|
+
:expiration_year => 2013)
|
117
|
+
subject.customer.email.should == "jrobot@example.com"
|
118
|
+
subject.credit_card.cardholder_name.should == "Jim Robot"
|
119
|
+
end
|
120
|
+
|
121
|
+
it "deletes the customer when deleted" do
|
122
|
+
subject.destroy
|
123
|
+
FakeBraintree.customers[subject.customer_token].should be_nil
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe Account, "with a free plan" do
|
128
|
+
subject do
|
129
|
+
Factory(:account, :plan => Factory(:plan))
|
130
|
+
end
|
131
|
+
|
132
|
+
it "has a customer_token" do
|
133
|
+
subject.customer_token.should_not be_nil
|
134
|
+
end
|
135
|
+
|
136
|
+
it "has a customer" do
|
137
|
+
subject.customer.should_not be_nil
|
138
|
+
end
|
139
|
+
|
140
|
+
it "doesn't have a credit_card" do
|
141
|
+
subject.credit_card.should be_nil
|
142
|
+
end
|
143
|
+
|
144
|
+
it "doesn't have a subscription_token" do
|
145
|
+
subject.subscription_token.should be_nil
|
146
|
+
end
|
147
|
+
|
148
|
+
it "doesn't have a subscription" do
|
149
|
+
subject.subscription.should be_nil
|
150
|
+
end
|
151
|
+
|
152
|
+
it "creates a braintree customer" do
|
153
|
+
FakeBraintree.customers[subject.customer_token].should_not be_nil
|
154
|
+
end
|
155
|
+
|
156
|
+
it "doesn't create a credit card, and subscription" do
|
157
|
+
FakeBraintree.customers[subject.customer_token]["credit_cards"].should be_nil
|
158
|
+
FakeBraintree.subscriptions[subject.subscription_token].should be_nil
|
159
|
+
end
|
160
|
+
|
161
|
+
it "creates a credit card, and subscription when the plan is changed to a paid plan and the billing info is supplied" do
|
162
|
+
new_plan = Factory(:paid_plan, :name => "New Plan")
|
163
|
+
subject.save_customer_and_subscription!(:plan_id => new_plan.id,
|
164
|
+
:cardholder_name => "Ralph Robot",
|
165
|
+
:billing_email => "ralph@example.com",
|
166
|
+
:card_number => "4111111111111111",
|
167
|
+
:verification_code => "123",
|
168
|
+
:expiration_month => 5,
|
169
|
+
:expiration_year => 2012)
|
170
|
+
|
171
|
+
FakeBraintree.customers[subject.customer_token]["credit_cards"].first.should_not be_nil
|
172
|
+
FakeBraintree.subscriptions[subject.subscription_token].should_not be_nil
|
173
|
+
FakeBraintree.subscriptions[subject.subscription_token]["plan_id"].should == new_plan.id
|
174
|
+
subject.credit_card.should_not be_nil
|
175
|
+
subject.subscription.should_not be_nil
|
176
|
+
end
|
177
|
+
|
178
|
+
it "doesn't create a credit card, and subscription when the plan is changed to a different free plan" do
|
179
|
+
new_plan = Factory(:plan, :name => "New Plan")
|
180
|
+
subject.save_customer_and_subscription!(:plan_id => new_plan.id)
|
181
|
+
|
182
|
+
subject.credit_card.should be_nil
|
183
|
+
subject.subscription.should be_nil
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe Account, "with a plan and limits, and other plans" do
|
188
|
+
subject { Factory(:account) }
|
189
|
+
|
190
|
+
before do
|
191
|
+
Factory(:limit, :name => "users", :value => 1, :plan => subject.plan)
|
192
|
+
Factory(:limit, :name => "projects", :value => 1, :plan => subject.plan)
|
193
|
+
Factory(:limit, :name => "ssl", :value => 1, :value_type => :boolean, :plan => subject.plan)
|
194
|
+
@can_switch = Factory(:plan)
|
195
|
+
Factory(:limit, :name => "users", :value => 1, :plan => @can_switch)
|
196
|
+
Factory(:limit, :name => "projects", :value => 1, :plan => @can_switch)
|
197
|
+
Factory(:limit, :name => "ssl", :value => 0, :value_type => :boolean, :plan => @can_switch)
|
198
|
+
@cannot_switch = Factory(:plan)
|
199
|
+
Factory(:limit, :name => "users", :value => 0, :plan => @cannot_switch)
|
200
|
+
Factory(:limit, :name => "projects", :value => 0, :plan => @cannot_switch)
|
201
|
+
Factory(:limit, :name => "ssl", :value => 1, :value_type => :boolean, :plan => @cannot_switch)
|
202
|
+
|
203
|
+
Factory(:membership, :account => subject)
|
204
|
+
Factory(:project, :account => subject)
|
205
|
+
end
|
206
|
+
|
207
|
+
it "indicates whether the account can switch to another plan" do
|
208
|
+
subject.can_change_plan_to?(@can_switch).should be
|
209
|
+
subject.can_change_plan_to?(@cannot_switch).should_not be
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
describe Account, "with a paid subscription" do
|
214
|
+
subject do
|
215
|
+
Factory(:account,
|
216
|
+
:cardholder_name => "Ralph Robot",
|
217
|
+
:billing_email => "ralph@example.com",
|
218
|
+
:card_number => "4111111111111111",
|
219
|
+
:verification_code => "123",
|
220
|
+
:expiration_month => 5,
|
221
|
+
:expiration_year => 2012,
|
222
|
+
:plan => Factory(:paid_plan))
|
223
|
+
end
|
224
|
+
|
225
|
+
it "gets marked as past due and updates its next_billing_date when subscriptions are updated and it has been rejected by the gateway" do
|
226
|
+
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
227
|
+
subscription["status"] = Braintree::Subscription::Status::PastDue
|
228
|
+
subscription["next_billing_date"] = 2.months.from_now
|
229
|
+
|
230
|
+
Timecop.travel(subject.next_billing_date + 1.day) do
|
231
|
+
Account.update_subscriptions!
|
232
|
+
subject.reload.subscription_status.should == Braintree::Subscription::Status::PastDue
|
233
|
+
subject.next_billing_date.to_s.should == subscription["next_billing_date"].to_s
|
234
|
+
subject.past_due?.should be
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
it "gets marked as not past due and updates its next_billing_date when the subscription is active after its billing date" do
|
239
|
+
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
240
|
+
subscription["status"] = Braintree::Subscription::Status::Active
|
241
|
+
subscription["next_billing_date"] = 2.months.from_now
|
242
|
+
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Settled,
|
243
|
+
:subscription_id => subject.subscription_token }
|
244
|
+
subscription["transactions"] = [FakeBraintree.generated_transaction]
|
245
|
+
|
246
|
+
Timecop.travel(subject.next_billing_date + 1.day) do
|
247
|
+
Account.update_subscriptions!
|
248
|
+
subject.reload.subscription_status.should == Braintree::Subscription::Status::Active
|
249
|
+
subject.next_billing_date.to_s.should == subscription["next_billing_date"].to_s
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
it "receives a receipt email at it's billing email with transaction details" do
|
254
|
+
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
255
|
+
subscription["status"] = Braintree::Subscription::Status::Active
|
256
|
+
subscription["next_billing_date"] = 2.months.from_now
|
257
|
+
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Settled,
|
258
|
+
:subscription_id => subject.subscription_token }
|
259
|
+
subscription["transactions"] = [FakeBraintree.generated_transaction]
|
260
|
+
|
261
|
+
Timecop.travel(subject.next_billing_date + 1.day) do
|
262
|
+
ActionMailer::Base.deliveries.clear
|
263
|
+
|
264
|
+
Account.update_subscriptions!
|
265
|
+
|
266
|
+
ActionMailer::Base.deliveries.any? do |email|
|
267
|
+
email.to == [subject.billing_email] &&
|
268
|
+
email.subject =~ /receipt/i
|
269
|
+
end.should be
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
it "receives a receipt email at it's billing email with a notice that it failed when billing didn't work" do
|
274
|
+
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
275
|
+
subscription["status"] = Braintree::Subscription::Status::PastDue
|
276
|
+
subscription["next_billing_date"] = 2.months.from_now
|
277
|
+
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Failed,
|
278
|
+
:subscription_id => subject.subscription_token }
|
279
|
+
subscription["transactions"] = [FakeBraintree.generated_transaction]
|
280
|
+
|
281
|
+
Timecop.travel(subject.next_billing_date + 1.day) do
|
282
|
+
ActionMailer::Base.deliveries.clear
|
283
|
+
|
284
|
+
Account.update_subscriptions!
|
285
|
+
|
286
|
+
ActionMailer::Base.deliveries.any? do |email|
|
287
|
+
email.to == [subject.billing_email] &&
|
288
|
+
email.subject =~ /problem/i
|
289
|
+
end.should be
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
describe Account, "with a paid subscription that is past due" do
|
295
|
+
subject do
|
296
|
+
Factory(:account,
|
297
|
+
:cardholder_name => "Ralph Robot",
|
298
|
+
:billing_email => "ralph@example.com",
|
299
|
+
:card_number => "4111111111111111",
|
300
|
+
:verification_code => "123",
|
301
|
+
:expiration_month => 5,
|
302
|
+
:expiration_year => 2012,
|
303
|
+
:plan => Factory(:paid_plan))
|
304
|
+
end
|
305
|
+
|
306
|
+
before do
|
307
|
+
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
308
|
+
subscription["status"] = Braintree::Subscription::Status::PastDue
|
309
|
+
subscription["next_billing_date"] = 2.months.from_now
|
310
|
+
|
311
|
+
Timecop.travel(subject.next_billing_date + 1.day) do
|
312
|
+
Account.update_subscriptions!
|
313
|
+
end
|
314
|
+
subject.reload
|
315
|
+
end
|
316
|
+
|
317
|
+
it "retries the subscription charge and updates the subscription when the billing information is correctly updated" do
|
318
|
+
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
319
|
+
subscription["status"] = Braintree::Subscription::Status::Active
|
320
|
+
subscription["next_billing_date"] = 2.months.from_now
|
321
|
+
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Settled,
|
322
|
+
:subscription_id => subject.subscription_token }
|
323
|
+
transaction = FakeBraintree.generated_transaction
|
324
|
+
subscription["transactions"] = [transaction]
|
325
|
+
|
326
|
+
retry_transaction = stub(:id => "12345")
|
327
|
+
retry_authorization = stub(:transaction => retry_transaction)
|
328
|
+
|
329
|
+
Braintree::Subscription.expects(:retry_charge).with(subject.subscription_token).returns(retry_authorization)
|
330
|
+
Braintree::Transaction.expects(:submit_for_settlement).with(retry_transaction.id).returns(stub(:success? => true))
|
331
|
+
|
332
|
+
subject.save_customer_and_subscription!(:card_number => "4111111111111111",
|
333
|
+
:verification_code => "124",
|
334
|
+
:expiration_month => 6,
|
335
|
+
:expiration_year => 2012).should be
|
336
|
+
|
337
|
+
subject.reload.subscription_status.should == Braintree::Subscription::Status::Active
|
338
|
+
subject.next_billing_date.to_s.should == subscription["next_billing_date"].to_s
|
339
|
+
end
|
340
|
+
|
341
|
+
it "retries the subscription charge and updates the subscription when the payment processing fails" do
|
342
|
+
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
343
|
+
subscription["status"] = Braintree::Subscription::Status::PastDue
|
344
|
+
subscription["next_billing_date"] = 1.day.from_now
|
345
|
+
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Failed,
|
346
|
+
:subscription_id => subject.subscription_token }
|
347
|
+
transaction = FakeBraintree.generated_transaction
|
348
|
+
subscription["transactions"] = [transaction]
|
349
|
+
|
350
|
+
retry_transaction = stub(:id => "12345", :status => "processor_declined", "processor_response_text" => "no good")
|
351
|
+
retry_authorization = stub(:transaction => retry_transaction)
|
352
|
+
|
353
|
+
Braintree::Subscription.expects(:retry_charge).with(subject.subscription_token).returns(retry_authorization)
|
354
|
+
Braintree::Transaction.expects(:submit_for_settlement).with(retry_transaction.id).returns(stub(:success? => false, :errors => []))
|
355
|
+
|
356
|
+
subject.save_customer_and_subscription!(:card_number => "4111111111",
|
357
|
+
:verification_code => "124",
|
358
|
+
:expiration_month => 6,
|
359
|
+
:expiration_year => 2012).should_not be
|
360
|
+
|
361
|
+
subject.errors[:card_number].should include("was denied by the payment processor with the message: no good")
|
362
|
+
subject.reload.subscription_status.should == Braintree::Subscription::Status::PastDue
|
363
|
+
subject.next_billing_date.to_s.should == subscription["next_billing_date"].to_s
|
364
|
+
end
|
365
|
+
|
366
|
+
it "retries the subscription charge and updates the subscription when the settlement fails" do
|
367
|
+
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
368
|
+
subscription["status"] = Braintree::Subscription::Status::PastDue
|
369
|
+
subscription["next_billing_date"] = 1.day.from_now
|
370
|
+
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Failed,
|
371
|
+
:subscription_id => subject.subscription_token }
|
372
|
+
transaction = FakeBraintree.generated_transaction
|
373
|
+
subscription["transactions"] = [transaction]
|
374
|
+
|
375
|
+
retry_transaction = stub(:id => "12345", :status => "")
|
376
|
+
retry_authorization = stub(:transaction => retry_transaction)
|
377
|
+
|
378
|
+
Braintree::Subscription.expects(:retry_charge).with(subject.subscription_token).returns(retry_authorization)
|
379
|
+
Braintree::Transaction.expects(:submit_for_settlement).with(retry_transaction.id).returns(stub(:success? => false, :errors => [stub(:attribute => 'number', :message => 'no good')]))
|
380
|
+
|
381
|
+
subject.save_customer_and_subscription!(:card_number => "4111111111",
|
382
|
+
:verification_code => "124",
|
383
|
+
:expiration_month => 6,
|
384
|
+
:expiration_year => 2012).should_not be
|
385
|
+
|
386
|
+
subject.errors[:card_number].should include("no good")
|
387
|
+
subject.reload.subscription_status.should == Braintree::Subscription::Status::PastDue
|
388
|
+
subject.next_billing_date.to_s.should == subscription["next_billing_date"].to_s
|
389
|
+
end
|
390
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: saucy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 51
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 2
|
9
|
-
-
|
10
|
-
version: 0.2.
|
9
|
+
- 18
|
10
|
+
version: 0.2.18
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- thoughtbot, inc.
|
@@ -17,14 +17,13 @@ autorequire:
|
|
17
17
|
bindir: bin
|
18
18
|
cert_chain: []
|
19
19
|
|
20
|
-
date: 2011-02-
|
20
|
+
date: 2011-02-09 00:00:00 -05:00
|
21
21
|
default_executable:
|
22
22
|
dependencies:
|
23
23
|
- !ruby/object:Gem::Dependency
|
24
|
-
type: :runtime
|
25
|
-
prerelease: false
|
26
24
|
name: formtastic
|
27
|
-
|
25
|
+
prerelease: false
|
26
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
28
27
|
none: false
|
29
28
|
requirements:
|
30
29
|
- - ">="
|
@@ -34,12 +33,12 @@ dependencies:
|
|
34
33
|
- 1
|
35
34
|
- 2
|
36
35
|
version: "1.2"
|
37
|
-
requirement: *id001
|
38
|
-
- !ruby/object:Gem::Dependency
|
39
36
|
type: :runtime
|
40
|
-
|
37
|
+
version_requirements: *id001
|
38
|
+
- !ruby/object:Gem::Dependency
|
41
39
|
name: railties
|
42
|
-
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
43
42
|
none: false
|
44
43
|
requirements:
|
45
44
|
- - ">="
|
@@ -50,12 +49,12 @@ dependencies:
|
|
50
49
|
- 0
|
51
50
|
- 3
|
52
51
|
version: 3.0.3
|
53
|
-
requirement: *id002
|
54
|
-
- !ruby/object:Gem::Dependency
|
55
52
|
type: :runtime
|
56
|
-
|
53
|
+
version_requirements: *id002
|
54
|
+
- !ruby/object:Gem::Dependency
|
57
55
|
name: braintree
|
58
|
-
|
56
|
+
prerelease: false
|
57
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
59
58
|
none: false
|
60
59
|
requirements:
|
61
60
|
- - ">="
|
@@ -66,12 +65,12 @@ dependencies:
|
|
66
65
|
- 6
|
67
66
|
- 2
|
68
67
|
version: 2.6.2
|
69
|
-
requirement: *id003
|
70
|
-
- !ruby/object:Gem::Dependency
|
71
68
|
type: :runtime
|
72
|
-
|
69
|
+
version_requirements: *id003
|
70
|
+
- !ruby/object:Gem::Dependency
|
73
71
|
name: sham_rack
|
74
|
-
|
72
|
+
prerelease: false
|
73
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
75
74
|
none: false
|
76
75
|
requirements:
|
77
76
|
- - "="
|
@@ -82,12 +81,12 @@ dependencies:
|
|
82
81
|
- 3
|
83
82
|
- 3
|
84
83
|
version: 1.3.3
|
85
|
-
requirement: *id004
|
86
|
-
- !ruby/object:Gem::Dependency
|
87
84
|
type: :runtime
|
88
|
-
|
85
|
+
version_requirements: *id004
|
86
|
+
- !ruby/object:Gem::Dependency
|
89
87
|
name: sinatra
|
90
|
-
|
88
|
+
prerelease: false
|
89
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
91
90
|
none: false
|
92
91
|
requirements:
|
93
92
|
- - "="
|
@@ -98,12 +97,12 @@ dependencies:
|
|
98
97
|
- 1
|
99
98
|
- 2
|
100
99
|
version: 1.1.2
|
101
|
-
|
100
|
+
type: :runtime
|
101
|
+
version_requirements: *id005
|
102
102
|
- !ruby/object:Gem::Dependency
|
103
|
-
type: :development
|
104
|
-
prerelease: false
|
105
103
|
name: aruba
|
106
|
-
|
104
|
+
prerelease: false
|
105
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
107
106
|
none: false
|
108
107
|
requirements:
|
109
108
|
- - "="
|
@@ -114,7 +113,8 @@ dependencies:
|
|
114
113
|
- 2
|
115
114
|
- 6
|
116
115
|
version: 0.2.6
|
117
|
-
|
116
|
+
type: :development
|
117
|
+
version_requirements: *id006
|
118
118
|
description: Clearance-based Rails engine for Software as a Service (Saas) that provides account and project management
|
119
119
|
email: support@thoughtbot.com
|
120
120
|
executables: []
|
@@ -205,6 +205,7 @@ files:
|
|
205
205
|
- lib/saucy/project.rb
|
206
206
|
- lib/saucy/projects_controller.rb
|
207
207
|
- lib/saucy/routing_extensions.rb
|
208
|
+
- lib/saucy/subscription.rb
|
208
209
|
- lib/saucy/user.rb
|
209
210
|
- lib/saucy.rb
|
210
211
|
- lib/saucy/railties/tasks.rake
|
@@ -242,6 +243,7 @@ files:
|
|
242
243
|
- spec/models/plan_spec.rb
|
243
244
|
- spec/models/project_spec.rb
|
244
245
|
- spec/models/signup_spec.rb
|
246
|
+
- spec/models/subscription_spec.rb
|
245
247
|
- spec/models/user_spec.rb
|
246
248
|
- spec/route_extensions_spec.rb
|
247
249
|
- spec/saucy_spec.rb
|