saucy 0.2.17 → 0.2.18
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.
- 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
|