saasy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (146) hide show
  1. data/CHANGELOG.md +114 -0
  2. data/Gemfile +26 -0
  3. data/README.md +118 -0
  4. data/Rakefile +38 -0
  5. data/app/controllers/accounts_controller.rb +68 -0
  6. data/app/controllers/billings_controller.rb +25 -0
  7. data/app/controllers/invitations_controller.rb +65 -0
  8. data/app/controllers/memberships_controller.rb +45 -0
  9. data/app/controllers/plans_controller.rb +24 -0
  10. data/app/controllers/profiles_controller.rb +19 -0
  11. data/app/helpers/limits_helper.rb +13 -0
  12. data/app/mailers/billing_mailer.rb +53 -0
  13. data/app/mailers/invitation_mailer.rb +18 -0
  14. data/app/models/invitation.rb +113 -0
  15. data/app/models/limit.rb +49 -0
  16. data/app/models/membership.rb +26 -0
  17. data/app/models/permission.rb +19 -0
  18. data/app/models/signup.rb +163 -0
  19. data/app/views/accounts/_account.html.erb +9 -0
  20. data/app/views/accounts/_blank_slate.html.erb +6 -0
  21. data/app/views/accounts/_projects.html.erb +12 -0
  22. data/app/views/accounts/_subnav.html.erb +10 -0
  23. data/app/views/accounts/edit.html.erb +34 -0
  24. data/app/views/accounts/index.html.erb +9 -0
  25. data/app/views/accounts/new.html.erb +36 -0
  26. data/app/views/billing_mailer/completed_trial.text.erb +13 -0
  27. data/app/views/billing_mailer/expiring_trial.text.erb +15 -0
  28. data/app/views/billing_mailer/new_unactivated.text.erb +1 -0
  29. data/app/views/billing_mailer/problem.html.erb +13 -0
  30. data/app/views/billing_mailer/problem.text.erb +14 -0
  31. data/app/views/billing_mailer/receipt.html.erb +41 -0
  32. data/app/views/billing_mailer/receipt.text.erb +25 -0
  33. data/app/views/billings/_form.html.erb +8 -0
  34. data/app/views/billings/edit.html.erb +13 -0
  35. data/app/views/billings/show.html.erb +29 -0
  36. data/app/views/invitation_mailer/invitation.text.erb +6 -0
  37. data/app/views/invitations/new.html.erb +17 -0
  38. data/app/views/invitations/show.html.erb +22 -0
  39. data/app/views/layouts/saucy.html.erb +36 -0
  40. data/app/views/limits/_meter.html.erb +13 -0
  41. data/app/views/memberships/edit.html.erb +21 -0
  42. data/app/views/memberships/index.html.erb +17 -0
  43. data/app/views/plans/_plan.html.erb +32 -0
  44. data/app/views/plans/_terms.html.erb +15 -0
  45. data/app/views/plans/edit.html.erb +33 -0
  46. data/app/views/plans/index.html.erb +12 -0
  47. data/app/views/profiles/_inputs.html.erb +5 -0
  48. data/app/views/profiles/edit.html.erb +36 -0
  49. data/app/views/projects/_form.html.erb +36 -0
  50. data/app/views/projects/edit.html.erb +22 -0
  51. data/app/views/projects/index.html.erb +28 -0
  52. data/app/views/projects/new.html.erb +13 -0
  53. data/app/views/projects/show.html.erb +0 -0
  54. data/app/views/shared/_project_dropdown.html.erb +55 -0
  55. data/app/views/shared/_saucy_javascript.html.erb +33 -0
  56. data/config/locales/en.yml +37 -0
  57. data/config/routes.rb +19 -0
  58. data/features/run_features.feature +83 -0
  59. data/features/step_definitions/clearance_steps.rb +45 -0
  60. data/features/step_definitions/rails_steps.rb +73 -0
  61. data/features/step_definitions/saucy_steps.rb +8 -0
  62. data/features/support/env.rb +4 -0
  63. data/features/support/file.rb +11 -0
  64. data/lib/generators/saucy/base.rb +18 -0
  65. data/lib/generators/saucy/features/features_generator.rb +91 -0
  66. data/lib/generators/saucy/features/templates/README +3 -0
  67. data/lib/generators/saucy/features/templates/factories.rb +71 -0
  68. data/lib/generators/saucy/features/templates/features/edit_profile.feature +9 -0
  69. data/lib/generators/saucy/features/templates/features/edit_project_permissions.feature +37 -0
  70. data/lib/generators/saucy/features/templates/features/edit_user_permissions.feature +47 -0
  71. data/lib/generators/saucy/features/templates/features/manage_account.feature +35 -0
  72. data/lib/generators/saucy/features/templates/features/manage_billing.feature +93 -0
  73. data/lib/generators/saucy/features/templates/features/manage_plan.feature +143 -0
  74. data/lib/generators/saucy/features/templates/features/manage_projects.feature +139 -0
  75. data/lib/generators/saucy/features/templates/features/manage_users.feature +142 -0
  76. data/lib/generators/saucy/features/templates/features/new_account.feature +33 -0
  77. data/lib/generators/saucy/features/templates/features/project_dropdown.feature +77 -0
  78. data/lib/generators/saucy/features/templates/features/sign_up.feature +32 -0
  79. data/lib/generators/saucy/features/templates/features/sign_up_paid.feature +65 -0
  80. data/lib/generators/saucy/features/templates/features/trial_plans.feature +82 -0
  81. data/lib/generators/saucy/features/templates/step_definitions/account_steps.rb +30 -0
  82. data/lib/generators/saucy/features/templates/step_definitions/braintree_steps.rb +25 -0
  83. data/lib/generators/saucy/features/templates/step_definitions/cron_steps.rb +23 -0
  84. data/lib/generators/saucy/features/templates/step_definitions/email_steps.rb +40 -0
  85. data/lib/generators/saucy/features/templates/step_definitions/factory_girl_steps.rb +1 -0
  86. data/lib/generators/saucy/features/templates/step_definitions/html_steps.rb +51 -0
  87. data/lib/generators/saucy/features/templates/step_definitions/plan_steps.rb +16 -0
  88. data/lib/generators/saucy/features/templates/step_definitions/project_steps.rb +4 -0
  89. data/lib/generators/saucy/features/templates/step_definitions/session_steps.rb +37 -0
  90. data/lib/generators/saucy/features/templates/step_definitions/user_steps.rb +100 -0
  91. data/lib/generators/saucy/features/templates/support/braintree.rb +5 -0
  92. data/lib/generators/saucy/install/install_generator.rb +40 -0
  93. data/lib/generators/saucy/install/templates/controllers/projects_controller.rb +3 -0
  94. data/lib/generators/saucy/install/templates/create_saucy_tables.rb +115 -0
  95. data/lib/generators/saucy/install/templates/models/account.rb +3 -0
  96. data/lib/generators/saucy/install/templates/models/plan.rb +3 -0
  97. data/lib/generators/saucy/install/templates/models/project.rb +3 -0
  98. data/lib/generators/saucy/specs/specs_generator.rb +20 -0
  99. data/lib/generators/saucy/specs/templates/support/braintree.rb +5 -0
  100. data/lib/generators/saucy/views/views_generator.rb +23 -0
  101. data/lib/saucy.rb +10 -0
  102. data/lib/saucy/account.rb +132 -0
  103. data/lib/saucy/account_authorization.rb +67 -0
  104. data/lib/saucy/configuration.rb +29 -0
  105. data/lib/saucy/engine.rb +35 -0
  106. data/lib/saucy/fake_braintree.rb +134 -0
  107. data/lib/saucy/layouts.rb +36 -0
  108. data/lib/saucy/plan.rb +54 -0
  109. data/lib/saucy/project.rb +125 -0
  110. data/lib/saucy/projects_controller.rb +94 -0
  111. data/lib/saucy/railties/tasks.rake +28 -0
  112. data/lib/saucy/routing_extensions.rb +121 -0
  113. data/lib/saucy/subscription.rb +237 -0
  114. data/lib/saucy/user.rb +30 -0
  115. data/spec/controllers/accounts_controller_spec.rb +228 -0
  116. data/spec/controllers/application_controller_spec.rb +32 -0
  117. data/spec/controllers/invitations_controller_spec.rb +215 -0
  118. data/spec/controllers/memberships_controller_spec.rb +117 -0
  119. data/spec/controllers/plans_controller_spec.rb +13 -0
  120. data/spec/controllers/profiles_controller_spec.rb +48 -0
  121. data/spec/controllers/projects_controller_spec.rb +216 -0
  122. data/spec/environment.rb +95 -0
  123. data/spec/layouts_spec.rb +21 -0
  124. data/spec/mailers/billing_mailer_spec.rb +68 -0
  125. data/spec/mailers/invitiation_mailer_spec.rb +19 -0
  126. data/spec/models/account_spec.rb +218 -0
  127. data/spec/models/invitation_spec.rb +320 -0
  128. data/spec/models/limit_spec.rb +70 -0
  129. data/spec/models/membership_spec.rb +37 -0
  130. data/spec/models/permission_spec.rb +30 -0
  131. data/spec/models/plan_spec.rb +81 -0
  132. data/spec/models/project_spec.rb +223 -0
  133. data/spec/models/signup_spec.rb +177 -0
  134. data/spec/models/subscription_spec.rb +481 -0
  135. data/spec/models/user_spec.rb +72 -0
  136. data/spec/route_extensions_spec.rb +51 -0
  137. data/spec/saucy_spec.rb +62 -0
  138. data/spec/scaffold/config/routes.rb +5 -0
  139. data/spec/spec_helper.rb +39 -0
  140. data/spec/support/authentication_helpers.rb +81 -0
  141. data/spec/support/authorization_helpers.rb +56 -0
  142. data/spec/support/braintree.rb +7 -0
  143. data/spec/support/clearance_matchers.rb +55 -0
  144. data/spec/support/notifications.rb +57 -0
  145. data/spec/views/accounts/_account.html.erb_spec.rb +37 -0
  146. metadata +325 -0
@@ -0,0 +1,481 @@
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
+ :verification_code => "100",
13
+ :expiration_month => 5,
14
+ :expiration_year => 2012,
15
+ :plan => Factory(:paid_plan))
16
+ account.save.should_not be
17
+ FakeBraintree.customers.should be_empty
18
+ account.persisted?.should_not be
19
+ account.errors[:card_number].any? { |e| e =~ /denied/ }.should be
20
+ end
21
+
22
+ it "manifests braintree gateway_rejected errors as errors on number and doesn't save" do
23
+ FakeBraintree.failures["4111111111111112"] = { "message" => "Gateway Rejected: cvv", "code" => "N", "status" => "gateway_rejected" }
24
+ account = Factory.build(:account,
25
+ :cardholder_name => "Ralph Robot",
26
+ :billing_email => "ralph@example.com",
27
+ :card_number => "4111111111111112",
28
+ :expiration_month => 5,
29
+ :expiration_year => 2012,
30
+ :verification_code => 200,
31
+ :plan => Factory(:paid_plan))
32
+ account.save.should_not be
33
+ FakeBraintree.customers.should be_empty
34
+ account.persisted?.should_not be
35
+ account.errors[:verification_code].any? { |e| e =~ /did not match/ }.should be
36
+ end
37
+
38
+ it "manifests braintree gateway_rejected errors as errors on number and doesn't save" do
39
+ 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 }] }}}}
40
+ account = Factory.build(:account,
41
+ :cardholder_name => "Ralph Robot",
42
+ :billing_email => "ralph@example.com",
43
+ :card_number => "4111111111111111",
44
+ :expiration_month => 5,
45
+ :expiration_year => 2012,
46
+ :verification_code => 123,
47
+ :plan => Factory(:paid_plan))
48
+ account.save.should_not be
49
+ FakeBraintree.customers.should be_empty
50
+ account.persisted?.should_not be
51
+ account.errors[:card_number].any? { |e| e =~ /is invalid/ }.should be
52
+ end
53
+ end
54
+
55
+ describe Account, "given free and paid plans" do
56
+ let(:free) { Factory(:plan, :price => 0) }
57
+ let(:paid) { Factory(:plan, :price => 1) }
58
+
59
+ it "doesn't switch from free to paid without credit card info" do
60
+ account = Factory(:account, :plan => free)
61
+ account = Account.find(account.id)
62
+
63
+ result = account.save_customer_and_subscription!(:plan_id => paid.id)
64
+
65
+ result.should be_false
66
+ account.reload.plan.should == free
67
+ Saucy::Subscription::CUSTOMER_ATTRIBUTES.keys.each do |attribute|
68
+ account.errors[attribute].should_not be_blank
69
+ end
70
+ FakeBraintree.customers[account.customer_token].should_not be_nil
71
+ FakeBraintree.customers[account.customer_token]["credit_cards"].should be_blank
72
+ end
73
+
74
+ it "requires a billing email when upgrading to a paid plan" do
75
+ account = Factory(:account, :plan => free, :card_number => '123')
76
+ account.reload
77
+ account.plan = paid
78
+
79
+ account.should validate_presence_of(:billing_email)
80
+ end
81
+
82
+ it "requires a billing email for paid plans" do
83
+ account = Factory.build(:account, :plan => paid, :card_number => '123')
84
+ account.should validate_presence_of(:billing_email)
85
+ end
86
+
87
+ it "doesn't require a billing email for free plans" do
88
+ account = Factory.build(:account, :plan => free)
89
+ account.should_not validate_presence_of(:billing_email)
90
+ end
91
+ end
92
+
93
+ describe Account, "with a paid plan" do
94
+ subject do
95
+ Factory(:account,
96
+ :cardholder_name => "Ralph Robot",
97
+ :billing_email => "ralph@example.com",
98
+ :card_number => "4111111111111111",
99
+ :verification_code => "123",
100
+ :expiration_month => 5,
101
+ :expiration_year => 2012,
102
+ :plan => Factory(:paid_plan))
103
+ end
104
+
105
+ it "has a customer_token" do
106
+ subject.customer_token.should_not be_nil
107
+ end
108
+
109
+ it "has a subscription_token" do
110
+ subject.subscription_token.should_not be_nil
111
+ end
112
+
113
+ it "has a customer" do
114
+ subject.customer.should_not be_nil
115
+ end
116
+
117
+ it "has a credit card" do
118
+ subject.credit_card.should_not be_nil
119
+ end
120
+
121
+ it "has a subscription" do
122
+ subject.subscription.should_not be_nil
123
+ end
124
+
125
+ it "has a next_billing_date" do
126
+ subject.next_billing_date.should_not be_nil
127
+ end
128
+
129
+ it "has an active subscription status" do
130
+ subject.subscription_status.should == Braintree::Subscription::Status::Active
131
+ end
132
+
133
+ it "is not past due" do
134
+ subject.past_due?.should_not be
135
+ end
136
+
137
+ it "creates a braintree customer, credit card, and subscription" do
138
+ FakeBraintree.customers[subject.customer_token].should_not be_nil
139
+ FakeBraintree.customers[subject.customer_token]["credit_cards"].first.should_not be_nil
140
+ FakeBraintree.subscriptions[subject.subscription_token].should_not be_nil
141
+ end
142
+
143
+ it "changes the subscription when the plan is changed" do
144
+ new_plan = Factory(:paid_plan, :name => "New Plan")
145
+ subject.save_customer_and_subscription!(:plan_id => new_plan.id)
146
+ FakeBraintree.subscriptions[subject.subscription_token]["plan_id"].should == new_plan.id
147
+ end
148
+
149
+ it "updates the customer and credit card information when changed" do
150
+ subject.save_customer_and_subscription!(:billing_email => "jrobot@example.com",
151
+ :cardholder_name => "Jim Robot",
152
+ :card_number => "4111111111111115",
153
+ :verification_code => "123",
154
+ :expiration_month => 5,
155
+ :expiration_year => 2013)
156
+ subject.customer.email.should == "jrobot@example.com"
157
+ subject.credit_card.cardholder_name.should == "Jim Robot"
158
+ end
159
+
160
+ it "deletes the customer when deleted" do
161
+ subject.destroy
162
+ FakeBraintree.customers[subject.customer_token].should be_nil
163
+ end
164
+ end
165
+
166
+ describe Account, "with a free plan" do
167
+ subject do
168
+ Factory(:account, :plan => Factory(:plan))
169
+ end
170
+
171
+ it "has a customer_token" do
172
+ subject.customer_token.should_not be_nil
173
+ end
174
+
175
+ it "has a customer" do
176
+ subject.customer.should_not be_nil
177
+ end
178
+
179
+ it "doesn't have a credit_card" do
180
+ subject.credit_card.should be_nil
181
+ end
182
+
183
+ it "doesn't have a subscription_token" do
184
+ subject.subscription_token.should be_nil
185
+ end
186
+
187
+ it "doesn't have a subscription" do
188
+ subject.subscription.should be_nil
189
+ end
190
+
191
+ it "creates a braintree customer" do
192
+ FakeBraintree.customers[subject.customer_token].should_not be_nil
193
+ end
194
+
195
+ it "doesn't create a credit card, and subscription" do
196
+ FakeBraintree.customers[subject.customer_token]["credit_cards"].should be_nil
197
+ FakeBraintree.subscriptions[subject.subscription_token].should be_nil
198
+ end
199
+
200
+ it "creates a credit card, and subscription when the plan is changed to a paid plan and the billing info is supplied" do
201
+ new_plan = Factory(:paid_plan, :name => "New Plan")
202
+ subject.save_customer_and_subscription!(:plan_id => new_plan.id,
203
+ :cardholder_name => "Ralph Robot",
204
+ :billing_email => "ralph@example.com",
205
+ :card_number => "4111111111111111",
206
+ :verification_code => "123",
207
+ :expiration_month => 5,
208
+ :expiration_year => 2012)
209
+
210
+ FakeBraintree.customers[subject.customer_token]["credit_cards"].first.should_not be_nil
211
+ FakeBraintree.subscriptions[subject.subscription_token].should_not be_nil
212
+ FakeBraintree.subscriptions[subject.subscription_token]["plan_id"].should == new_plan.id
213
+ subject.credit_card.should_not be_nil
214
+ subject.subscription.should_not be_nil
215
+ end
216
+
217
+ it "passes up the merchant_account_id on the subscription when it's configured" do
218
+ begin
219
+ Saucy::Configuration.merchant_account_id = 'test'
220
+ new_plan = Factory(:paid_plan, :name => "New Plan")
221
+ subject.save_customer_and_subscription!(:plan_id => new_plan.id,
222
+ :cardholder_name => "Ralph Robot",
223
+ :billing_email => "ralph@example.com",
224
+ :card_number => "4111111111111111",
225
+ :verification_code => "123",
226
+ :expiration_month => 5,
227
+ :expiration_year => 2012)
228
+
229
+ FakeBraintree.subscriptions[subject.subscription_token]["merchant_account_id"].should == 'test'
230
+ ensure
231
+ Saucy::Configuration.merchant_account_id = nil
232
+ end
233
+ end
234
+
235
+ it "doesn't pass up the merchant_account_id on the subscription when it's not configured" do
236
+ Saucy::Configuration.merchant_account_id = nil
237
+ new_plan = Factory(:paid_plan, :name => "New Plan")
238
+ subject.save_customer_and_subscription!(:plan_id => new_plan.id,
239
+ :cardholder_name => "Ralph Robot",
240
+ :billing_email => "ralph@example.com",
241
+ :card_number => "4111111111111111",
242
+ :verification_code => "123",
243
+ :expiration_month => 5,
244
+ :expiration_year => 2012)
245
+
246
+ FakeBraintree.subscriptions[subject.subscription_token].keys.should_not include("merchant_account_id")
247
+ end
248
+
249
+ it "doesn't create a credit card, and subscription when the plan is changed to a different free plan" do
250
+ new_plan = Factory(:plan, :name => "New Plan")
251
+ subject.save_customer_and_subscription!(:plan_id => new_plan.id)
252
+
253
+ subject.credit_card.should be_nil
254
+ subject.subscription.should be_nil
255
+ end
256
+ end
257
+
258
+ describe Account, "with a plan and limits, and other plans" do
259
+ subject { Factory(:account) }
260
+
261
+ before do
262
+ Factory(:limit, :name => "users", :value => 1, :plan => subject.plan)
263
+ Factory(:limit, :name => "projects", :value => 1, :plan => subject.plan)
264
+ Factory(:limit, :name => "ssl", :value => 1, :value_type => :boolean, :plan => subject.plan)
265
+ @can_switch = Factory(:plan)
266
+ Factory(:limit, :name => "users", :value => 1, :plan => @can_switch)
267
+ Factory(:limit, :name => "projects", :value => 1, :plan => @can_switch)
268
+ Factory(:limit, :name => "ssl", :value => 0, :value_type => :boolean, :plan => @can_switch)
269
+ @cannot_switch = Factory(:plan)
270
+ Factory(:limit, :name => "users", :value => 0, :plan => @cannot_switch)
271
+ Factory(:limit, :name => "projects", :value => 0, :plan => @cannot_switch)
272
+ Factory(:limit, :name => "ssl", :value => 1, :value_type => :boolean, :plan => @cannot_switch)
273
+
274
+ Factory(:membership, :account => subject)
275
+ Factory(:project, :account => subject)
276
+ end
277
+
278
+ it "indicates whether the account can switch to another plan" do
279
+ subject.can_change_plan_to?(@can_switch).should be
280
+ subject.can_change_plan_to?(@cannot_switch).should_not be
281
+ end
282
+ end
283
+
284
+ describe Account, "with a paid subscription" do
285
+ subject do
286
+ Factory(:account,
287
+ :cardholder_name => "Ralph Robot",
288
+ :billing_email => "ralph@example.com",
289
+ :card_number => "4111111111111111",
290
+ :verification_code => "123",
291
+ :expiration_month => 5,
292
+ :expiration_year => 2012,
293
+ :plan => Factory(:paid_plan))
294
+ end
295
+
296
+ 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
297
+ subscription = FakeBraintree.subscriptions[subject.subscription_token]
298
+ subscription["status"] = Braintree::Subscription::Status::PastDue
299
+ subscription["next_billing_date"] = 2.months.from_now
300
+
301
+ Timecop.travel(subject.next_billing_date + 1.day) do
302
+ Account.update_subscriptions!
303
+ subject.reload.subscription_status.should == Braintree::Subscription::Status::PastDue
304
+ subject.next_billing_date.to_s.should == subscription["next_billing_date"].to_s
305
+ subject.past_due?.should be
306
+ end
307
+ end
308
+
309
+ it "gets marked as not past due and updates its next_billing_date when the subscription is active after its billing date" do
310
+ subscription = FakeBraintree.subscriptions[subject.subscription_token]
311
+ subscription["status"] = Braintree::Subscription::Status::Active
312
+ subscription["next_billing_date"] = 2.months.from_now
313
+ FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Settled,
314
+ :subscription_id => subject.subscription_token }
315
+ subscription["transactions"] = [FakeBraintree.generated_transaction]
316
+
317
+ Timecop.travel(subject.next_billing_date + 1.day) do
318
+ Account.update_subscriptions!
319
+ subject.reload.subscription_status.should == Braintree::Subscription::Status::Active
320
+ subject.next_billing_date.to_s.should == subscription["next_billing_date"].to_s
321
+ end
322
+ end
323
+
324
+ it "receives a receipt email at it's billing email with transaction details" do
325
+ subscription = FakeBraintree.subscriptions[subject.subscription_token]
326
+ subscription["status"] = Braintree::Subscription::Status::Active
327
+ subscription["next_billing_date"] = 2.months.from_now
328
+ FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Settled,
329
+ :subscription_id => subject.subscription_token }
330
+ subscription["transactions"] = [FakeBraintree.generated_transaction]
331
+
332
+ Timecop.travel(subject.next_billing_date + 1.day) do
333
+ ActionMailer::Base.deliveries.clear
334
+
335
+ Account.update_subscriptions!
336
+
337
+ ActionMailer::Base.deliveries.any? do |email|
338
+ email.to == [subject.billing_email] &&
339
+ email.subject =~ /receipt/i
340
+ end.should be
341
+ end
342
+ end
343
+
344
+ it "doesn't receive a receipt email when it's already been billed" do
345
+ subscription = FakeBraintree.subscriptions[subject.subscription_token]
346
+ subscription["status"] = Braintree::Subscription::Status::Active
347
+ subscription["next_billing_date"] = 2.months.from_now
348
+ FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Settled,
349
+ :subscription_id => subject.subscription_token }
350
+ subscription["transactions"] = [FakeBraintree.generated_transaction]
351
+
352
+ Timecop.travel(subject.next_billing_date - 1.day) do
353
+ ActionMailer::Base.deliveries.clear
354
+
355
+ Account.update_subscriptions!
356
+
357
+ ActionMailer::Base.deliveries.select do |email|
358
+ email.to == [subject.billing_email] &&
359
+ email.subject =~ /receipt/i
360
+ end.should be_empty
361
+ end
362
+ end
363
+
364
+ it "receives a receipt email at it's billing email with a notice that it failed when billing didn't work" do
365
+ subscription = FakeBraintree.subscriptions[subject.subscription_token]
366
+ subscription["status"] = Braintree::Subscription::Status::PastDue
367
+ subscription["next_billing_date"] = 2.months.from_now
368
+ FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Failed,
369
+ :subscription_id => subject.subscription_token }
370
+ subscription["transactions"] = [FakeBraintree.generated_transaction]
371
+
372
+ Timecop.travel(subject.next_billing_date + 1.day) do
373
+ ActionMailer::Base.deliveries.clear
374
+
375
+ Account.update_subscriptions!
376
+
377
+ ActionMailer::Base.deliveries.any? do |email|
378
+ email.to == [subject.billing_email] &&
379
+ email.subject =~ /problem/i
380
+ end.should be
381
+ end
382
+ end
383
+ end
384
+
385
+ describe Account, "with a paid subscription that is past due" do
386
+ subject do
387
+ Factory(:account,
388
+ :cardholder_name => "Ralph Robot",
389
+ :billing_email => "ralph@example.com",
390
+ :card_number => "4111111111111111",
391
+ :verification_code => "123",
392
+ :expiration_month => 5,
393
+ :expiration_year => 2012,
394
+ :plan => Factory(:paid_plan))
395
+ end
396
+
397
+ before do
398
+ subscription = FakeBraintree.subscriptions[subject.subscription_token]
399
+ subscription["status"] = Braintree::Subscription::Status::PastDue
400
+ subscription["next_billing_date"] = 2.months.from_now
401
+
402
+ Timecop.travel(subject.next_billing_date + 1.day) do
403
+ Account.update_subscriptions!
404
+ end
405
+ subject.reload
406
+ end
407
+
408
+ it "retries the subscription charge and updates the subscription when the billing information is correctly updated" do
409
+ subscription = FakeBraintree.subscriptions[subject.subscription_token]
410
+ subscription["status"] = Braintree::Subscription::Status::Active
411
+ subscription["next_billing_date"] = 2.months.from_now
412
+ FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Settled,
413
+ :subscription_id => subject.subscription_token }
414
+ transaction = FakeBraintree.generated_transaction
415
+ subscription["transactions"] = [transaction]
416
+
417
+ retry_transaction = stub(:id => "12345")
418
+ retry_authorization = stub(:transaction => retry_transaction)
419
+
420
+ Braintree::Subscription.expects(:retry_charge).with(subject.subscription_token).returns(retry_authorization)
421
+ Braintree::Transaction.expects(:submit_for_settlement).with(retry_transaction.id).returns(stub(:success? => true))
422
+
423
+ subject.save_customer_and_subscription!(:card_number => "4111111111111111",
424
+ :verification_code => "124",
425
+ :expiration_month => 6,
426
+ :expiration_year => 2012).should be
427
+
428
+ subject.reload.subscription_status.should == Braintree::Subscription::Status::Active
429
+ subject.next_billing_date.to_s.should == subscription["next_billing_date"].to_s
430
+ end
431
+
432
+ it "retries the subscription charge and updates the subscription when the payment processing fails" do
433
+ subscription = FakeBraintree.subscriptions[subject.subscription_token]
434
+ subscription["status"] = Braintree::Subscription::Status::PastDue
435
+ subscription["next_billing_date"] = 1.day.from_now
436
+ FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Failed,
437
+ :subscription_id => subject.subscription_token }
438
+ transaction = FakeBraintree.generated_transaction
439
+ subscription["transactions"] = [transaction]
440
+
441
+ retry_transaction = stub(:id => "12345", :status => "processor_declined", "processor_response_text" => "no good")
442
+ retry_authorization = stub(:transaction => retry_transaction)
443
+
444
+ Braintree::Subscription.expects(:retry_charge).with(subject.subscription_token).returns(retry_authorization)
445
+ Braintree::Transaction.expects(:submit_for_settlement).with(retry_transaction.id).returns(stub(:success? => false, :errors => []))
446
+
447
+ subject.save_customer_and_subscription!(:card_number => "4111111111",
448
+ :verification_code => "124",
449
+ :expiration_month => 6,
450
+ :expiration_year => 2012).should_not be
451
+
452
+ subject.errors[:card_number].should include("was denied by the payment processor with the message: no good")
453
+ subject.reload.subscription_status.should == Braintree::Subscription::Status::PastDue
454
+ subject.next_billing_date.to_s.should == subscription["next_billing_date"].to_s
455
+ end
456
+
457
+ it "retries the subscription charge and updates the subscription when the settlement fails" do
458
+ subscription = FakeBraintree.subscriptions[subject.subscription_token]
459
+ subscription["status"] = Braintree::Subscription::Status::PastDue
460
+ subscription["next_billing_date"] = 1.day.from_now
461
+ FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Failed,
462
+ :subscription_id => subject.subscription_token }
463
+ transaction = FakeBraintree.generated_transaction
464
+ subscription["transactions"] = [transaction]
465
+
466
+ retry_transaction = stub(:id => "12345", :status => "")
467
+ retry_authorization = stub(:transaction => retry_transaction)
468
+
469
+ Braintree::Subscription.expects(:retry_charge).with(subject.subscription_token).returns(retry_authorization)
470
+ Braintree::Transaction.expects(:submit_for_settlement).with(retry_transaction.id).returns(stub(:success? => false, :errors => [stub(:attribute => 'number', :message => 'no good')]))
471
+
472
+ subject.save_customer_and_subscription!(:card_number => "4111111111",
473
+ :verification_code => "124",
474
+ :expiration_month => 6,
475
+ :expiration_year => 2012).should_not be
476
+
477
+ subject.errors[:card_number].should include("no good")
478
+ subject.reload.subscription_status.should == Braintree::Subscription::Status::PastDue
479
+ subject.next_billing_date.to_s.should == subscription["next_billing_date"].to_s
480
+ end
481
+ end