saucy 0.13.0 → 0.13.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +5 -0
- data/README.md +6 -1
- data/lib/saucy/configuration.rb +2 -0
- data/lib/saucy/engine.rb +4 -1
- data/lib/saucy/fake_braintree.rb +1 -1
- data/lib/saucy/subscription.rb +6 -3
- data/spec/models/subscription_spec.rb +62 -15
- data/spec/saucy/configuration_spec.rb +14 -0
- metadata +12 -59
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -66,7 +66,6 @@ followup emails being sent to users that have already set up their accounts.
|
|
66
66
|
It will also send an "Activated" event to Kissmetrics through Saucy-kiss, if
|
67
67
|
Saucy-kiss is installed.
|
68
68
|
|
69
|
-
|
70
69
|
Development environment
|
71
70
|
-----------------------
|
72
71
|
|
@@ -142,3 +141,9 @@ Make sure you don't do this in ApplicationController:
|
|
142
141
|
|
143
142
|
Saucy's internal controllers don't skip any before filters.
|
144
143
|
|
144
|
+
If your billing merchant account is not configured to run in the Eastern
|
145
|
+
timezone, you can override the timezone in Saucy configuration as
|
146
|
+
merchant_account_time_zone. You'll want this set accurately so that the daily
|
147
|
+
cronjob which syncs the Braintree subscription billing cycle dates runs for the
|
148
|
+
correct accounts. If this is set incorrectly, you will occasionally see
|
149
|
+
duplicate receipt emails being delivered.
|
data/lib/saucy/configuration.rb
CHANGED
@@ -11,6 +11,7 @@ module Saucy
|
|
11
11
|
cattr_accessor :trial_length
|
12
12
|
cattr_accessor :expiring_notice_on
|
13
13
|
cattr_accessor :unactivated_notice_on
|
14
|
+
cattr_accessor :merchant_account_time_zone
|
14
15
|
|
15
16
|
def initialize
|
16
17
|
@@manager_email_address = 'manager@example.com'
|
@@ -21,6 +22,7 @@ module Saucy
|
|
21
22
|
@@trial_length = 30
|
22
23
|
@@expiring_notice_on = 7
|
23
24
|
@@unactivated_notice_on = 7
|
25
|
+
@@merchant_account_time_zone = 'Eastern Time (US & Canada)'
|
24
26
|
end
|
25
27
|
|
26
28
|
end
|
data/lib/saucy/engine.rb
CHANGED
data/lib/saucy/fake_braintree.rb
CHANGED
@@ -102,7 +102,7 @@ ShamRack.at("www.braintreegateway.com", 443).sinatra do
|
|
102
102
|
subscription["transactions"] = []
|
103
103
|
subscription["add_ons"] = []
|
104
104
|
subscription["discounts"] = []
|
105
|
-
subscription["next_billing_date"] = 1.month.from_now
|
105
|
+
subscription["next_billing_date"] = 1.month.from_now.to_s(:braintree_date)
|
106
106
|
subscription["status"] = Braintree::Subscription::Status::Active
|
107
107
|
FakeBraintree.subscriptions[subscription["id"]] = subscription
|
108
108
|
[201, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(subscription.to_xml(:root => 'subscription'))]
|
data/lib/saucy/subscription.rb
CHANGED
@@ -94,7 +94,8 @@ module Saucy
|
|
94
94
|
def update_subscription_cache!
|
95
95
|
flush_cache :subscription
|
96
96
|
update_attribute(:subscription_status, subscription.status)
|
97
|
-
update_attribute(:next_billing_date, subscription.next_billing_date)
|
97
|
+
update_attribute(:next_billing_date, Time.parse(subscription.next_billing_date).in_time_zone(
|
98
|
+
Saucy::Configuration.merchant_account_time_zone))
|
98
99
|
end
|
99
100
|
|
100
101
|
def changing_plan?(attributes)
|
@@ -204,7 +205,8 @@ module Saucy
|
|
204
205
|
result = Braintree::Subscription.create(subscription_attributes)
|
205
206
|
if result.success?
|
206
207
|
self.subscription_token = result.subscription.id
|
207
|
-
self.next_billing_date = result.subscription.next_billing_date
|
208
|
+
self.next_billing_date = Time.parse(result.subscription.next_billing_date).in_time_zone(
|
209
|
+
Saucy::Configuration.merchant_account_time_zone)
|
208
210
|
self.subscription_status = result.subscription.status
|
209
211
|
else
|
210
212
|
false
|
@@ -232,7 +234,8 @@ module Saucy
|
|
232
234
|
recently_billed.each do |account|
|
233
235
|
begin
|
234
236
|
account.subscription_status = account.subscription.status
|
235
|
-
account.next_billing_date = account.subscription.next_billing_date
|
237
|
+
account.next_billing_date = Time.parse(account.subscription.next_billing_date).in_time_zone(
|
238
|
+
Saucy::Configuration.merchant_account_time_zone)
|
236
239
|
account.save!
|
237
240
|
if account.past_due?
|
238
241
|
BillingMailer.problem(account).deliver!
|
@@ -122,8 +122,9 @@ describe Account, "with a paid plan" do
|
|
122
122
|
subject.subscription.should_not be_nil
|
123
123
|
end
|
124
124
|
|
125
|
-
it "has a next_billing_date" do
|
126
|
-
|
125
|
+
it "has a next_billing_date in the merchant account timezone" do
|
126
|
+
Time.stubs(:now => Time.parse("2011-09-16 02:00 -0000"))
|
127
|
+
subject.next_billing_date.should == Time.parse("Sun, 16 Oct 2011 00:00:00 EDT -04:00")
|
127
128
|
end
|
128
129
|
|
129
130
|
it "has an active subscription status" do
|
@@ -295,14 +296,16 @@ describe Account, "with a paid subscription" do
|
|
295
296
|
end
|
296
297
|
|
297
298
|
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
|
299
|
+
next_billing_date_string = 2.months.from_now.to_s(:braintree_date)
|
298
300
|
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
299
301
|
subscription["status"] = Braintree::Subscription::Status::PastDue
|
300
|
-
subscription["next_billing_date"] =
|
302
|
+
subscription["next_billing_date"] = next_billing_date_string
|
301
303
|
|
302
304
|
Timecop.travel(subject.next_billing_date + 1.day) do
|
303
305
|
Account.update_subscriptions!
|
304
306
|
subject.reload.subscription_status.should == Braintree::Subscription::Status::PastDue
|
305
|
-
subject.next_billing_date.
|
307
|
+
subject.next_billing_date.should == Time.parse(next_billing_date_string).in_time_zone(
|
308
|
+
Saucy::Configuration.merchant_account_time_zone)
|
306
309
|
subject.past_due?.should be
|
307
310
|
end
|
308
311
|
end
|
@@ -343,7 +346,7 @@ describe Account, "with a paid subscription" do
|
|
343
346
|
|
344
347
|
before do
|
345
348
|
subscription["status"] = Braintree::Subscription::Status::PastDue
|
346
|
-
subscription["next_billing_date"] = 2.months.from_now
|
349
|
+
subscription["next_billing_date"] = 2.months.from_now.to_s(:braintree_date)
|
347
350
|
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Failed,
|
348
351
|
:subscription_id => subject.subscription_token }
|
349
352
|
subscription["transactions"] = [FakeBraintree.generated_transaction]
|
@@ -387,7 +390,7 @@ describe Account, "with a paid subscription" do
|
|
387
390
|
end
|
388
391
|
|
389
392
|
before do
|
390
|
-
subscription["next_billing_date"] = 2.months.from_now
|
393
|
+
subscription["next_billing_date"] = 2.months.from_now.to_s(:braintree_date)
|
391
394
|
subscription["status"] = Braintree::Subscription::Status::Active
|
392
395
|
subscription["transactions"] = [
|
393
396
|
build_transaction(:created_at => later_transaction_date),
|
@@ -408,12 +411,49 @@ describe Account, "with a paid subscription" do
|
|
408
411
|
end
|
409
412
|
end
|
410
413
|
|
414
|
+
context "with a merchant account timezone different from the system timezone" do
|
415
|
+
def build_subscription(account)
|
416
|
+
subscription = FakeBraintree.subscriptions[account.subscription_token]
|
417
|
+
subscription["status"] = Braintree::Subscription::Status::Active
|
418
|
+
subscription["transactions"] = [FakeBraintree.generated_transaction]
|
419
|
+
subscription
|
420
|
+
end
|
421
|
+
|
422
|
+
before do
|
423
|
+
Saucy::Configuration.merchant_account_time_zone = "Eastern Time (US & Canada)"
|
424
|
+
subject.next_billing_date = "2011-08-15"
|
425
|
+
subject.save!
|
426
|
+
|
427
|
+
subscription = build_subscription(subject)
|
428
|
+
subscription["next_billing_date"] = "2011-09-15"
|
429
|
+
|
430
|
+
Time.stubs(:now => Time.parse("2011-08-16 02:00 -0000"))
|
431
|
+
Account.update_subscriptions!
|
432
|
+
ActionMailer::Base.deliveries.clear
|
433
|
+
|
434
|
+
subject.reload.next_billing_date.should == Time.parse("2011-09-15 00:00 -0400")
|
435
|
+
end
|
436
|
+
|
437
|
+
it "does not update accounts if their billing date hasn't elapsed from the merchant account's perspective" do
|
438
|
+
Time.stubs(:now => Time.parse("2011-09-15 02:00 -0000"))
|
439
|
+
Account.update_subscriptions!
|
440
|
+
ActionMailer::Base.deliveries.should be_empty
|
441
|
+
end
|
442
|
+
|
443
|
+
it "does update accounts if their billing date has elapsed from the merchant account's perspective" do
|
444
|
+
Time.stubs(:now => Time.parse("2011-09-16 02:00 -0000"))
|
445
|
+
Account.update_subscriptions!
|
446
|
+
ActionMailer::Base.deliveries.should_not be_empty
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
411
450
|
context "and an active subscription due 2 months from now" do
|
412
451
|
let(:subscription) { FakeBraintree.subscriptions[subject.subscription_token] }
|
452
|
+
let(:next_billing_date_string) { 2.months.from_now.to_s(:braintree_date) }
|
413
453
|
|
414
454
|
before do
|
415
455
|
subscription["status"] = Braintree::Subscription::Status::Active
|
416
|
-
subscription["next_billing_date"] =
|
456
|
+
subscription["next_billing_date"] = next_billing_date_string
|
417
457
|
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Settled,
|
418
458
|
:subscription_id => subject.subscription_token }
|
419
459
|
subscription["transactions"] = [FakeBraintree.generated_transaction]
|
@@ -423,7 +463,8 @@ describe Account, "with a paid subscription" do
|
|
423
463
|
Timecop.travel(subject.next_billing_date + 1.day) do
|
424
464
|
Account.update_subscriptions!
|
425
465
|
subject.reload.subscription_status.should == Braintree::Subscription::Status::Active
|
426
|
-
subject.next_billing_date.
|
466
|
+
subject.next_billing_date.should == Time.parse(next_billing_date_string).in_time_zone(
|
467
|
+
Saucy::Configuration.merchant_account_time_zone)
|
427
468
|
end
|
428
469
|
end
|
429
470
|
|
@@ -484,7 +525,7 @@ describe Account, "with a paid subscription that is past due" do
|
|
484
525
|
before do
|
485
526
|
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
486
527
|
subscription["status"] = Braintree::Subscription::Status::PastDue
|
487
|
-
subscription["next_billing_date"] = 2.months.from_now
|
528
|
+
subscription["next_billing_date"] = 2.months.from_now.to_s(:braintree_date)
|
488
529
|
|
489
530
|
Timecop.travel(subject.next_billing_date + 1.day) do
|
490
531
|
Account.update_subscriptions!
|
@@ -493,9 +534,10 @@ describe Account, "with a paid subscription that is past due" do
|
|
493
534
|
end
|
494
535
|
|
495
536
|
it "retries the subscription charge and updates the subscription when the billing information is correctly updated" do
|
537
|
+
next_billing_date_string = 1.day.from_now.to_s(:braintree_date)
|
496
538
|
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
497
539
|
subscription["status"] = Braintree::Subscription::Status::Active
|
498
|
-
subscription["next_billing_date"] =
|
540
|
+
subscription["next_billing_date"] = next_billing_date_string
|
499
541
|
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Settled,
|
500
542
|
:subscription_id => subject.subscription_token }
|
501
543
|
transaction = FakeBraintree.generated_transaction
|
@@ -513,13 +555,15 @@ describe Account, "with a paid subscription that is past due" do
|
|
513
555
|
:expiration_year => 2012).should be
|
514
556
|
|
515
557
|
subject.reload.subscription_status.should == Braintree::Subscription::Status::Active
|
516
|
-
subject.next_billing_date.
|
558
|
+
subject.next_billing_date.should == Time.parse(next_billing_date_string).in_time_zone(
|
559
|
+
Saucy::Configuration.merchant_account_time_zone)
|
517
560
|
end
|
518
561
|
|
519
562
|
it "retries the subscription charge and updates the subscription when the payment processing fails" do
|
563
|
+
next_billing_date_string = 2.months.from_now.to_s(:braintree_date)
|
520
564
|
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
521
565
|
subscription["status"] = Braintree::Subscription::Status::PastDue
|
522
|
-
subscription["next_billing_date"] =
|
566
|
+
subscription["next_billing_date"] = next_billing_date_string
|
523
567
|
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Failed,
|
524
568
|
:subscription_id => subject.subscription_token }
|
525
569
|
transaction = FakeBraintree.generated_transaction
|
@@ -538,13 +582,15 @@ describe Account, "with a paid subscription that is past due" do
|
|
538
582
|
|
539
583
|
subject.errors[:card_number].should include("was denied by the payment processor with the message: no good")
|
540
584
|
subject.reload.subscription_status.should == Braintree::Subscription::Status::PastDue
|
541
|
-
subject.next_billing_date.
|
585
|
+
subject.next_billing_date.should == Time.parse(next_billing_date_string).in_time_zone(
|
586
|
+
Saucy::Configuration.merchant_account_time_zone)
|
542
587
|
end
|
543
588
|
|
544
589
|
it "retries the subscription charge and updates the subscription when the settlement fails" do
|
590
|
+
next_billing_date_string = 1.day.from_now.to_s(:braintree_date)
|
545
591
|
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
546
592
|
subscription["status"] = Braintree::Subscription::Status::PastDue
|
547
|
-
subscription["next_billing_date"] =
|
593
|
+
subscription["next_billing_date"] = next_billing_date_string
|
548
594
|
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Failed,
|
549
595
|
:subscription_id => subject.subscription_token }
|
550
596
|
transaction = FakeBraintree.generated_transaction
|
@@ -563,7 +609,8 @@ describe Account, "with a paid subscription that is past due" do
|
|
563
609
|
|
564
610
|
subject.errors[:card_number].should include("no good")
|
565
611
|
subject.reload.subscription_status.should == Braintree::Subscription::Status::PastDue
|
566
|
-
subject.next_billing_date.
|
612
|
+
subject.next_billing_date.should == Time.parse(next_billing_date_string).in_time_zone(
|
613
|
+
Saucy::Configuration.merchant_account_time_zone)
|
567
614
|
end
|
568
615
|
end
|
569
616
|
|
@@ -78,4 +78,18 @@ describe Saucy::Configuration do
|
|
78
78
|
subject.unactivated_notice_on = old_length
|
79
79
|
end
|
80
80
|
end
|
81
|
+
|
82
|
+
it "can assign a merchant_account_time_zone" do
|
83
|
+
old_merchant_account_time_zone = subject.merchant_account_time_zone
|
84
|
+
begin
|
85
|
+
subject.merchant_account_time_zone = "International Date Line West"
|
86
|
+
subject.merchant_account_time_zone.should == "International Date Line West"
|
87
|
+
ensure
|
88
|
+
subject.merchant_account_time_zone = old_merchant_account_time_zone
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
it "defaults merchant_account_time_zone to 'Eastern Time (US & Canada)'" do
|
93
|
+
subject.merchant_account_time_zone.should == "Eastern Time (US & Canada)"
|
94
|
+
end
|
81
95
|
end
|
metadata
CHANGED
@@ -1,13 +1,8 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: saucy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash: 43
|
5
4
|
prerelease:
|
6
|
-
|
7
|
-
- 0
|
8
|
-
- 13
|
9
|
-
- 0
|
10
|
-
version: 0.13.0
|
5
|
+
version: 0.13.1
|
11
6
|
platform: ruby
|
12
7
|
authors:
|
13
8
|
- thoughtbot, inc.
|
@@ -20,134 +15,95 @@ autorequire:
|
|
20
15
|
bindir: bin
|
21
16
|
cert_chain: []
|
22
17
|
|
23
|
-
date: 2011-10-
|
18
|
+
date: 2011-10-17 00:00:00 Z
|
24
19
|
dependencies:
|
25
20
|
- !ruby/object:Gem::Dependency
|
26
21
|
name: clearance
|
27
|
-
prerelease: false
|
28
22
|
requirement: &id001 !ruby/object:Gem::Requirement
|
29
23
|
none: false
|
30
24
|
requirements:
|
31
25
|
- - ~>
|
32
26
|
- !ruby/object:Gem::Version
|
33
|
-
hash: 55
|
34
|
-
segments:
|
35
|
-
- 0
|
36
|
-
- 11
|
37
|
-
- 2
|
38
27
|
version: 0.11.2
|
39
28
|
type: :runtime
|
29
|
+
prerelease: false
|
40
30
|
version_requirements: *id001
|
41
31
|
- !ruby/object:Gem::Dependency
|
42
32
|
name: formtastic
|
43
|
-
prerelease: false
|
44
33
|
requirement: &id002 !ruby/object:Gem::Requirement
|
45
34
|
none: false
|
46
35
|
requirements:
|
47
36
|
- - ">="
|
48
37
|
- !ruby/object:Gem::Version
|
49
|
-
hash: 11
|
50
|
-
segments:
|
51
|
-
- 1
|
52
|
-
- 2
|
53
38
|
version: "1.2"
|
54
39
|
type: :runtime
|
40
|
+
prerelease: false
|
55
41
|
version_requirements: *id002
|
56
42
|
- !ruby/object:Gem::Dependency
|
57
43
|
name: railties
|
58
|
-
prerelease: false
|
59
44
|
requirement: &id003 !ruby/object:Gem::Requirement
|
60
45
|
none: false
|
61
46
|
requirements:
|
62
47
|
- - ">="
|
63
48
|
- !ruby/object:Gem::Version
|
64
|
-
hash: 1
|
65
|
-
segments:
|
66
|
-
- 3
|
67
|
-
- 0
|
68
|
-
- 3
|
69
49
|
version: 3.0.3
|
70
50
|
type: :runtime
|
51
|
+
prerelease: false
|
71
52
|
version_requirements: *id003
|
72
53
|
- !ruby/object:Gem::Dependency
|
73
54
|
name: braintree
|
74
|
-
prerelease: false
|
75
55
|
requirement: &id004 !ruby/object:Gem::Requirement
|
76
56
|
none: false
|
77
57
|
requirements:
|
78
58
|
- - ">="
|
79
59
|
- !ruby/object:Gem::Version
|
80
|
-
hash: 19
|
81
|
-
segments:
|
82
|
-
- 2
|
83
|
-
- 6
|
84
|
-
- 2
|
85
60
|
version: 2.6.2
|
86
61
|
type: :runtime
|
62
|
+
prerelease: false
|
87
63
|
version_requirements: *id004
|
88
64
|
- !ruby/object:Gem::Dependency
|
89
65
|
name: sham_rack
|
90
|
-
prerelease: false
|
91
66
|
requirement: &id005 !ruby/object:Gem::Requirement
|
92
67
|
none: false
|
93
68
|
requirements:
|
94
69
|
- - "="
|
95
70
|
- !ruby/object:Gem::Version
|
96
|
-
hash: 29
|
97
|
-
segments:
|
98
|
-
- 1
|
99
|
-
- 3
|
100
|
-
- 3
|
101
71
|
version: 1.3.3
|
102
72
|
type: :runtime
|
73
|
+
prerelease: false
|
103
74
|
version_requirements: *id005
|
104
75
|
- !ruby/object:Gem::Dependency
|
105
76
|
name: sinatra
|
106
|
-
prerelease: false
|
107
77
|
requirement: &id006 !ruby/object:Gem::Requirement
|
108
78
|
none: false
|
109
79
|
requirements:
|
110
80
|
- - ">="
|
111
81
|
- !ruby/object:Gem::Version
|
112
|
-
hash: 23
|
113
|
-
segments:
|
114
|
-
- 1
|
115
|
-
- 1
|
116
|
-
- 2
|
117
82
|
version: 1.1.2
|
118
83
|
type: :runtime
|
84
|
+
prerelease: false
|
119
85
|
version_requirements: *id006
|
120
86
|
- !ruby/object:Gem::Dependency
|
121
87
|
name: airbrake
|
122
|
-
prerelease: false
|
123
88
|
requirement: &id007 !ruby/object:Gem::Requirement
|
124
89
|
none: false
|
125
90
|
requirements:
|
126
91
|
- - ~>
|
127
92
|
- !ruby/object:Gem::Version
|
128
|
-
hash: 15
|
129
|
-
segments:
|
130
|
-
- 3
|
131
|
-
- 0
|
132
|
-
- 4
|
133
93
|
version: 3.0.4
|
134
94
|
type: :runtime
|
95
|
+
prerelease: false
|
135
96
|
version_requirements: *id007
|
136
97
|
- !ruby/object:Gem::Dependency
|
137
98
|
name: aruba
|
138
|
-
prerelease: false
|
139
99
|
requirement: &id008 !ruby/object:Gem::Requirement
|
140
100
|
none: false
|
141
101
|
requirements:
|
142
102
|
- - "="
|
143
103
|
- !ruby/object:Gem::Version
|
144
|
-
hash: 27
|
145
|
-
segments:
|
146
|
-
- 0
|
147
|
-
- 2
|
148
|
-
- 6
|
149
104
|
version: 0.2.6
|
150
105
|
type: :development
|
106
|
+
prerelease: false
|
151
107
|
version_requirements: *id008
|
152
108
|
description: Clearance-based Rails engine for Software as a Service (Saas) that provides account and project management
|
153
109
|
email: support@thoughtbot.com
|
@@ -344,7 +300,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
344
300
|
requirements:
|
345
301
|
- - ">="
|
346
302
|
- !ruby/object:Gem::Version
|
347
|
-
hash:
|
303
|
+
hash: 3518640004043339834
|
348
304
|
segments:
|
349
305
|
- 0
|
350
306
|
version: "0"
|
@@ -353,14 +309,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
353
309
|
requirements:
|
354
310
|
- - ">="
|
355
311
|
- !ruby/object:Gem::Version
|
356
|
-
hash: 3
|
357
|
-
segments:
|
358
|
-
- 0
|
359
312
|
version: "0"
|
360
313
|
requirements: []
|
361
314
|
|
362
315
|
rubyforge_project:
|
363
|
-
rubygems_version: 1.8.
|
316
|
+
rubygems_version: 1.8.10
|
364
317
|
signing_key:
|
365
318
|
specification_version: 3
|
366
319
|
summary: Clearance-based Rails engine for SaaS
|