saucy 0.10.7 → 0.10.8
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/CHANGELOG.md +4 -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 -2
- data/spec/environment.rb +2 -2
- data/spec/models/subscription_spec.rb +61 -14
- data/spec/saucy/configuration_spec.rb +14 -0
- metadata +18 -62
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -53,7 +53,6 @@ followup emails being sent to users that have already set up their accounts.
|
|
53
53
|
It will also send an "Activated" event to Kissmetrics through Saucy-kiss, if
|
54
54
|
Saucy-kiss is installed.
|
55
55
|
|
56
|
-
|
57
56
|
Development environment
|
58
57
|
-----------------------
|
59
58
|
|
@@ -123,3 +122,9 @@ Make sure you don't do this in ApplicationController:
|
|
123
122
|
|
124
123
|
Saucy's internal controllers don't skip any before filters.
|
125
124
|
|
125
|
+
If your billing merchant account is not configured to run in the Eastern
|
126
|
+
timezone, you can override the timezone in Saucy configuration as
|
127
|
+
merchant_account_time_zone. You'll want this set accurately so that the daily
|
128
|
+
cronjob which syncs the Braintree subscription billing cycle dates runs for the
|
129
|
+
correct accounts. If this is set incorrectly, you will occasionally see
|
130
|
+
duplicate receipt emails being delivered.
|
data/lib/saucy/configuration.rb
CHANGED
@@ -8,6 +8,7 @@ module Saucy
|
|
8
8
|
cattr_accessor :merchant_account_id
|
9
9
|
cattr_accessor :admin_username
|
10
10
|
cattr_accessor :admin_password
|
11
|
+
cattr_accessor :merchant_account_time_zone
|
11
12
|
|
12
13
|
def initialize
|
13
14
|
@@manager_email_address = 'manager@example.com'
|
@@ -15,6 +16,7 @@ module Saucy
|
|
15
16
|
@@layouts = Layouts.new
|
16
17
|
@@admin_username = 'admin'
|
17
18
|
@@admin_password = 'admin'
|
19
|
+
@@merchant_account_time_zone = 'Eastern Time (US & Canada)'
|
18
20
|
end
|
19
21
|
|
20
22
|
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
@@ -88,7 +88,8 @@ module Saucy
|
|
88
88
|
def update_subscription_cache!
|
89
89
|
flush_cache :subscription
|
90
90
|
update_attribute(:subscription_status, subscription.status)
|
91
|
-
update_attribute(:next_billing_date, subscription.next_billing_date)
|
91
|
+
update_attribute(:next_billing_date, Time.parse(subscription.next_billing_date).in_time_zone(
|
92
|
+
Saucy::Configuration.merchant_account_time_zone))
|
92
93
|
end
|
93
94
|
|
94
95
|
def changing_plan?(attributes)
|
@@ -198,7 +199,8 @@ module Saucy
|
|
198
199
|
result = Braintree::Subscription.create(subscription_attributes)
|
199
200
|
if result.success?
|
200
201
|
self.subscription_token = result.subscription.id
|
201
|
-
self.next_billing_date = result.subscription.next_billing_date
|
202
|
+
self.next_billing_date = Time.parse(result.subscription.next_billing_date).in_time_zone(
|
203
|
+
Saucy::Configuration.merchant_account_time_zone)
|
202
204
|
self.subscription_status = result.subscription.status
|
203
205
|
else
|
204
206
|
false
|
@@ -226,6 +228,8 @@ module Saucy
|
|
226
228
|
recently_billed.each do |account|
|
227
229
|
account.subscription_status = account.subscription.status
|
228
230
|
account.next_billing_date = account.subscription.next_billing_date
|
231
|
+
account.next_billing_date = Time.parse(account.subscription.next_billing_date).in_time_zone(
|
232
|
+
Saucy::Configuration.merchant_account_time_zone)
|
229
233
|
account.save!
|
230
234
|
if account.past_due?
|
231
235
|
BillingMailer.problem(account, account.subscription.transactions.last).deliver!
|
data/spec/environment.rb
CHANGED
@@ -49,8 +49,8 @@ end
|
|
49
49
|
|
50
50
|
Testapp::Application.initialize!
|
51
51
|
|
52
|
-
require "lib/generators/saucy/features/templates/factories"
|
53
|
-
require "lib/generators/saucy/install/templates/create_saucy_tables"
|
52
|
+
require "./lib/generators/saucy/features/templates/factories"
|
53
|
+
require "./lib/generators/saucy/install/templates/create_saucy_tables"
|
54
54
|
|
55
55
|
class ClearanceCreateUsers < ActiveRecord::Migration
|
56
56
|
def self.up
|
@@ -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
|
@@ -312,7 +315,7 @@ describe Account, "with a paid subscription" do
|
|
312
315
|
|
313
316
|
before do
|
314
317
|
subscription["status"] = Braintree::Subscription::Status::PastDue
|
315
|
-
subscription["next_billing_date"] = 2.months.from_now
|
318
|
+
subscription["next_billing_date"] = 2.months.from_now.to_s(:braintree_date)
|
316
319
|
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Failed,
|
317
320
|
:subscription_id => subject.subscription_token }
|
318
321
|
subscription["transactions"] = [FakeBraintree.generated_transaction]
|
@@ -339,12 +342,49 @@ describe Account, "with a paid subscription" do
|
|
339
342
|
end
|
340
343
|
end
|
341
344
|
|
345
|
+
context "with a merchant account timezone different from the system timezone" do
|
346
|
+
def build_subscription(account)
|
347
|
+
subscription = FakeBraintree.subscriptions[account.subscription_token]
|
348
|
+
subscription["status"] = Braintree::Subscription::Status::Active
|
349
|
+
subscription["transactions"] = [FakeBraintree.generated_transaction]
|
350
|
+
subscription
|
351
|
+
end
|
352
|
+
|
353
|
+
before do
|
354
|
+
Saucy::Configuration.merchant_account_time_zone = "Eastern Time (US & Canada)"
|
355
|
+
subject.next_billing_date = "2011-08-15"
|
356
|
+
subject.save!
|
357
|
+
|
358
|
+
subscription = build_subscription(subject)
|
359
|
+
subscription["next_billing_date"] = "2011-09-15"
|
360
|
+
|
361
|
+
Time.stubs(:now => Time.parse("2011-08-16 02:00 -0000"))
|
362
|
+
Account.update_subscriptions!
|
363
|
+
ActionMailer::Base.deliveries.clear
|
364
|
+
|
365
|
+
subject.reload.next_billing_date.should == Time.parse("2011-09-15 00:00 -0400")
|
366
|
+
end
|
367
|
+
|
368
|
+
it "does not update accounts if their billing date hasn't elapsed from the merchant account's perspective" do
|
369
|
+
Time.stubs(:now => Time.parse("2011-09-15 02:00 -0000"))
|
370
|
+
Account.update_subscriptions!
|
371
|
+
ActionMailer::Base.deliveries.should be_empty
|
372
|
+
end
|
373
|
+
|
374
|
+
it "does update accounts if their billing date has elapsed from the merchant account's perspective" do
|
375
|
+
Time.stubs(:now => Time.parse("2011-09-16 02:00 -0000"))
|
376
|
+
Account.update_subscriptions!
|
377
|
+
ActionMailer::Base.deliveries.should_not be_empty
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
342
381
|
context "and an active subscription due 2 months from now" do
|
343
382
|
let(:subscription) { FakeBraintree.subscriptions[subject.subscription_token] }
|
383
|
+
let(:next_billing_date_string) { 2.months.from_now.to_s(:braintree_date) }
|
344
384
|
|
345
385
|
before do
|
346
386
|
subscription["status"] = Braintree::Subscription::Status::Active
|
347
|
-
subscription["next_billing_date"] =
|
387
|
+
subscription["next_billing_date"] = next_billing_date_string
|
348
388
|
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Settled,
|
349
389
|
:subscription_id => subject.subscription_token }
|
350
390
|
subscription["transactions"] = [FakeBraintree.generated_transaction]
|
@@ -354,7 +394,8 @@ describe Account, "with a paid subscription" do
|
|
354
394
|
Timecop.travel(subject.next_billing_date + 1.day) do
|
355
395
|
Account.update_subscriptions!
|
356
396
|
subject.reload.subscription_status.should == Braintree::Subscription::Status::Active
|
357
|
-
subject.next_billing_date.
|
397
|
+
subject.next_billing_date.should == Time.parse(next_billing_date_string).in_time_zone(
|
398
|
+
Saucy::Configuration.merchant_account_time_zone)
|
358
399
|
end
|
359
400
|
end
|
360
401
|
|
@@ -415,7 +456,7 @@ describe Account, "with a paid subscription that is past due" do
|
|
415
456
|
before do
|
416
457
|
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
417
458
|
subscription["status"] = Braintree::Subscription::Status::PastDue
|
418
|
-
subscription["next_billing_date"] = 2.months.from_now
|
459
|
+
subscription["next_billing_date"] = 2.months.from_now.to_s(:braintree_date)
|
419
460
|
|
420
461
|
Timecop.travel(subject.next_billing_date + 1.day) do
|
421
462
|
Account.update_subscriptions!
|
@@ -424,9 +465,10 @@ describe Account, "with a paid subscription that is past due" do
|
|
424
465
|
end
|
425
466
|
|
426
467
|
it "retries the subscription charge and updates the subscription when the billing information is correctly updated" do
|
468
|
+
next_billing_date_string = 1.day.from_now.to_s(:braintree_date)
|
427
469
|
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
428
470
|
subscription["status"] = Braintree::Subscription::Status::Active
|
429
|
-
subscription["next_billing_date"] =
|
471
|
+
subscription["next_billing_date"] = next_billing_date_string
|
430
472
|
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Settled,
|
431
473
|
:subscription_id => subject.subscription_token }
|
432
474
|
transaction = FakeBraintree.generated_transaction
|
@@ -444,13 +486,15 @@ describe Account, "with a paid subscription that is past due" do
|
|
444
486
|
:expiration_year => 2012).should be
|
445
487
|
|
446
488
|
subject.reload.subscription_status.should == Braintree::Subscription::Status::Active
|
447
|
-
subject.next_billing_date.
|
489
|
+
subject.next_billing_date.should == Time.parse(next_billing_date_string).in_time_zone(
|
490
|
+
Saucy::Configuration.merchant_account_time_zone)
|
448
491
|
end
|
449
492
|
|
450
493
|
it "retries the subscription charge and updates the subscription when the payment processing fails" do
|
494
|
+
next_billing_date_string = 2.months.from_now.to_s(:braintree_date)
|
451
495
|
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
452
496
|
subscription["status"] = Braintree::Subscription::Status::PastDue
|
453
|
-
subscription["next_billing_date"] =
|
497
|
+
subscription["next_billing_date"] = next_billing_date_string
|
454
498
|
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Failed,
|
455
499
|
:subscription_id => subject.subscription_token }
|
456
500
|
transaction = FakeBraintree.generated_transaction
|
@@ -469,13 +513,15 @@ describe Account, "with a paid subscription that is past due" do
|
|
469
513
|
|
470
514
|
subject.errors[:card_number].should include("was denied by the payment processor with the message: no good")
|
471
515
|
subject.reload.subscription_status.should == Braintree::Subscription::Status::PastDue
|
472
|
-
subject.next_billing_date.
|
516
|
+
subject.next_billing_date.should == Time.parse(next_billing_date_string).in_time_zone(
|
517
|
+
Saucy::Configuration.merchant_account_time_zone)
|
473
518
|
end
|
474
519
|
|
475
520
|
it "retries the subscription charge and updates the subscription when the settlement fails" do
|
521
|
+
next_billing_date_string = 1.day.from_now.to_s(:braintree_date)
|
476
522
|
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
477
523
|
subscription["status"] = Braintree::Subscription::Status::PastDue
|
478
|
-
subscription["next_billing_date"] =
|
524
|
+
subscription["next_billing_date"] = next_billing_date_string
|
479
525
|
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Failed,
|
480
526
|
:subscription_id => subject.subscription_token }
|
481
527
|
transaction = FakeBraintree.generated_transaction
|
@@ -494,7 +540,8 @@ describe Account, "with a paid subscription that is past due" do
|
|
494
540
|
|
495
541
|
subject.errors[:card_number].should include("no good")
|
496
542
|
subject.reload.subscription_status.should == Braintree::Subscription::Status::PastDue
|
497
|
-
subject.next_billing_date.
|
543
|
+
subject.next_billing_date.should == Time.parse(next_billing_date_string).in_time_zone(
|
544
|
+
Saucy::Configuration.merchant_account_time_zone)
|
498
545
|
end
|
499
546
|
end
|
500
547
|
|
@@ -36,4 +36,18 @@ describe Saucy::Configuration do
|
|
36
36
|
subject.support_email_address = old_address
|
37
37
|
end
|
38
38
|
end
|
39
|
+
|
40
|
+
it "can assign a merchant_account_time_zone" do
|
41
|
+
old_merchant_account_time_zone = subject.merchant_account_time_zone
|
42
|
+
begin
|
43
|
+
subject.merchant_account_time_zone = "International Date Line West"
|
44
|
+
subject.merchant_account_time_zone.should == "International Date Line West"
|
45
|
+
ensure
|
46
|
+
subject.merchant_account_time_zone = old_merchant_account_time_zone
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
it "defaults merchant_account_time_zone to 'Eastern Time (US & Canada)'" do
|
51
|
+
subject.merchant_account_time_zone.should == "Eastern Time (US & Canada)"
|
52
|
+
end
|
39
53
|
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: 57
|
5
4
|
prerelease:
|
6
|
-
|
7
|
-
- 0
|
8
|
-
- 10
|
9
|
-
- 7
|
10
|
-
version: 0.10.7
|
5
|
+
version: 0.10.8
|
11
6
|
platform: ruby
|
12
7
|
authors:
|
13
8
|
- thoughtbot, inc.
|
@@ -20,119 +15,84 @@ autorequire:
|
|
20
15
|
bindir: bin
|
21
16
|
cert_chain: []
|
22
17
|
|
23
|
-
date: 2011-
|
24
|
-
default_executable:
|
18
|
+
date: 2011-10-17 00:00:00 Z
|
25
19
|
dependencies:
|
26
20
|
- !ruby/object:Gem::Dependency
|
27
21
|
name: clearance
|
28
|
-
prerelease: false
|
29
|
-
type: :runtime
|
30
22
|
requirement: &id001 !ruby/object:Gem::Requirement
|
31
23
|
none: false
|
32
24
|
requirements:
|
33
25
|
- - ~>
|
34
26
|
- !ruby/object:Gem::Version
|
35
|
-
hash: 55
|
36
|
-
segments:
|
37
|
-
- 0
|
38
|
-
- 11
|
39
|
-
- 2
|
40
27
|
version: 0.11.2
|
28
|
+
type: :runtime
|
29
|
+
prerelease: false
|
41
30
|
version_requirements: *id001
|
42
31
|
- !ruby/object:Gem::Dependency
|
43
32
|
name: formtastic
|
44
|
-
prerelease: false
|
45
|
-
type: :runtime
|
46
33
|
requirement: &id002 !ruby/object:Gem::Requirement
|
47
34
|
none: false
|
48
35
|
requirements:
|
49
36
|
- - ">="
|
50
37
|
- !ruby/object:Gem::Version
|
51
|
-
hash: 11
|
52
|
-
segments:
|
53
|
-
- 1
|
54
|
-
- 2
|
55
38
|
version: "1.2"
|
39
|
+
type: :runtime
|
40
|
+
prerelease: false
|
56
41
|
version_requirements: *id002
|
57
42
|
- !ruby/object:Gem::Dependency
|
58
43
|
name: railties
|
59
|
-
prerelease: false
|
60
|
-
type: :runtime
|
61
44
|
requirement: &id003 !ruby/object:Gem::Requirement
|
62
45
|
none: false
|
63
46
|
requirements:
|
64
47
|
- - ">="
|
65
48
|
- !ruby/object:Gem::Version
|
66
|
-
hash: 1
|
67
|
-
segments:
|
68
|
-
- 3
|
69
|
-
- 0
|
70
|
-
- 3
|
71
49
|
version: 3.0.3
|
50
|
+
type: :runtime
|
51
|
+
prerelease: false
|
72
52
|
version_requirements: *id003
|
73
53
|
- !ruby/object:Gem::Dependency
|
74
54
|
name: braintree
|
75
|
-
prerelease: false
|
76
|
-
type: :runtime
|
77
55
|
requirement: &id004 !ruby/object:Gem::Requirement
|
78
56
|
none: false
|
79
57
|
requirements:
|
80
58
|
- - ">="
|
81
59
|
- !ruby/object:Gem::Version
|
82
|
-
hash: 19
|
83
|
-
segments:
|
84
|
-
- 2
|
85
|
-
- 6
|
86
|
-
- 2
|
87
60
|
version: 2.6.2
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
88
63
|
version_requirements: *id004
|
89
64
|
- !ruby/object:Gem::Dependency
|
90
65
|
name: sham_rack
|
91
|
-
prerelease: false
|
92
|
-
type: :runtime
|
93
66
|
requirement: &id005 !ruby/object:Gem::Requirement
|
94
67
|
none: false
|
95
68
|
requirements:
|
96
69
|
- - "="
|
97
70
|
- !ruby/object:Gem::Version
|
98
|
-
hash: 29
|
99
|
-
segments:
|
100
|
-
- 1
|
101
|
-
- 3
|
102
|
-
- 3
|
103
71
|
version: 1.3.3
|
72
|
+
type: :runtime
|
73
|
+
prerelease: false
|
104
74
|
version_requirements: *id005
|
105
75
|
- !ruby/object:Gem::Dependency
|
106
76
|
name: sinatra
|
107
|
-
prerelease: false
|
108
|
-
type: :runtime
|
109
77
|
requirement: &id006 !ruby/object:Gem::Requirement
|
110
78
|
none: false
|
111
79
|
requirements:
|
112
80
|
- - ">="
|
113
81
|
- !ruby/object:Gem::Version
|
114
|
-
hash: 23
|
115
|
-
segments:
|
116
|
-
- 1
|
117
|
-
- 1
|
118
|
-
- 2
|
119
82
|
version: 1.1.2
|
83
|
+
type: :runtime
|
84
|
+
prerelease: false
|
120
85
|
version_requirements: *id006
|
121
86
|
- !ruby/object:Gem::Dependency
|
122
87
|
name: aruba
|
123
|
-
prerelease: false
|
124
|
-
type: :development
|
125
88
|
requirement: &id007 !ruby/object:Gem::Requirement
|
126
89
|
none: false
|
127
90
|
requirements:
|
128
91
|
- - "="
|
129
92
|
- !ruby/object:Gem::Version
|
130
|
-
hash: 27
|
131
|
-
segments:
|
132
|
-
- 0
|
133
|
-
- 2
|
134
|
-
- 6
|
135
93
|
version: 0.2.6
|
94
|
+
type: :development
|
95
|
+
prerelease: false
|
136
96
|
version_requirements: *id007
|
137
97
|
description: Clearance-based Rails engine for Software as a Service (Saas) that provides account and project management
|
138
98
|
email: support@thoughtbot.com
|
@@ -314,7 +274,6 @@ files:
|
|
314
274
|
- spec/support/clearance_matchers.rb
|
315
275
|
- spec/support/notifications.rb
|
316
276
|
- spec/views/accounts/_account.html.erb_spec.rb
|
317
|
-
has_rdoc: true
|
318
277
|
homepage: http://github.com/thoughtbot/saucy
|
319
278
|
licenses: []
|
320
279
|
|
@@ -328,7 +287,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
328
287
|
requirements:
|
329
288
|
- - ">="
|
330
289
|
- !ruby/object:Gem::Version
|
331
|
-
hash:
|
290
|
+
hash: 209378144174508178
|
332
291
|
segments:
|
333
292
|
- 0
|
334
293
|
version: "0"
|
@@ -337,14 +296,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
337
296
|
requirements:
|
338
297
|
- - ">="
|
339
298
|
- !ruby/object:Gem::Version
|
340
|
-
hash: 3
|
341
|
-
segments:
|
342
|
-
- 0
|
343
299
|
version: "0"
|
344
300
|
requirements: []
|
345
301
|
|
346
302
|
rubyforge_project:
|
347
|
-
rubygems_version: 1.
|
303
|
+
rubygems_version: 1.8.10
|
348
304
|
signing_key:
|
349
305
|
specification_version: 3
|
350
306
|
summary: Clearance-based Rails engine for SaaS
|