saasy 0.0.1

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