stripe_saas 0.0.1 → 0.0.2
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.
- checksums.yaml +4 -4
- data/README.asc +1 -1
- data/Rakefile +1 -13
- data/app/assets/javascripts/stripe_saas/application.js +1 -1
- data/app/concerns/stripe_saas/plan.rb +22 -3
- data/app/concerns/stripe_saas/plan_feature.rb +4 -4
- data/app/concerns/stripe_saas/subscription.rb +1 -2
- data/lib/generators/stripe_saas/install_generator.rb +1 -1
- data/lib/generators/stripe_saas/templates/app/models/plan_feature.rb +2 -0
- data/lib/stripe_saas/engine.rb +4 -0
- data/lib/stripe_saas/version.rb +1 -1
- data/spec/dummy/app/assets/javascripts/application.js +1 -1
- data/spec/dummy/app/models/feature.rb +8 -0
- data/spec/dummy/app/models/plan.rb +4 -2
- data/spec/dummy/app/models/plan_feature.rb +6 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20150102001930_create_plans.rb +0 -1
- data/spec/dummy/db/migrate/20151220161809_create_features.rb +14 -0
- data/spec/dummy/db/migrate/20151220161817_create_plan_features.rb +12 -0
- data/spec/dummy/db/schema.rb +21 -2
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/test.log +3215 -0
- data/spec/models/feature_spec.rb +36 -0
- data/spec/models/plan_feature_spec.rb +89 -0
- data/spec/models/plan_spec.rb +137 -0
- data/spec/models/subscription_spec.rb +73 -0
- data/spec/spec_helper.rb +11 -3
- metadata +20 -9
- data/spec/concerns/plan_spec.rb +0 -50
- data/spec/dummy/log/development.log +0 -170
- data/spec/integration/navigation_test.rb +0 -9
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
describe Feature, type: :model do
|
4
|
+
|
5
|
+
it { is_expected.to have_many(:plan_features) }
|
6
|
+
it { is_expected.to have_many(:plans) }
|
7
|
+
|
8
|
+
before do
|
9
|
+
@feature = Feature.find_or_create_by(name: 'signals')
|
10
|
+
@feature.update({
|
11
|
+
description: "Inbound Signals",
|
12
|
+
feature_type: :number,
|
13
|
+
display_order: 1
|
14
|
+
})
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#feature_type' do
|
18
|
+
it 'correctly assigns the feature type and unit given a symbol' do
|
19
|
+
expect(@feature.feature_type).to eq('number')
|
20
|
+
expect(@feature.unit).to eq('Number')
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'throws an error given an invalid feature type' do
|
24
|
+
feature = Feature.new
|
25
|
+
|
26
|
+
expect { feature.feature_type = :foo }.to raise_error(ArgumentError, "foo is not a valid feature type")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#to_s' do
|
31
|
+
it 'returns the feature\'s name' do
|
32
|
+
expect(@feature.to_s).to eq('signals')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
describe PlanFeature, type: :model do
|
4
|
+
|
5
|
+
it { is_expected.to belong_to(:feature) }
|
6
|
+
it { is_expected.to belong_to(:plan) }
|
7
|
+
|
8
|
+
before(:all) do
|
9
|
+
@number_feature = Feature.find_or_create_by(name: 'number_feature')
|
10
|
+
@number_feature.update({
|
11
|
+
description: "Data Retention",
|
12
|
+
feature_type: :number,
|
13
|
+
unit: "months",
|
14
|
+
display_order: 1
|
15
|
+
})
|
16
|
+
|
17
|
+
@boolean_feature = Feature.find_or_create_by(name: 'boolean_feature')
|
18
|
+
@boolean_feature.update({
|
19
|
+
description: "My boolean feature",
|
20
|
+
feature_type: :boolean,
|
21
|
+
display_order: 2
|
22
|
+
})
|
23
|
+
|
24
|
+
@another_number_feature = Feature.find_or_create_by(name: 'another_number_feature')
|
25
|
+
@another_number_feature.update({
|
26
|
+
description: "Data Retention",
|
27
|
+
feature_type: :number,
|
28
|
+
unit: "month",
|
29
|
+
use_unit: true,
|
30
|
+
display_order: 3
|
31
|
+
})
|
32
|
+
|
33
|
+
@plan = Plan.find_or_create_by(stripe_id: 'my_plan')
|
34
|
+
@plan.update({
|
35
|
+
name: 'My Plan',
|
36
|
+
price: 0.0,
|
37
|
+
interval: 'month',
|
38
|
+
interval_count: 1,
|
39
|
+
statement_descriptor: 'My Awesome Plan',
|
40
|
+
trial_period_days: 30,
|
41
|
+
display_order: 1
|
42
|
+
})
|
43
|
+
|
44
|
+
@plan.add_feature(:number_feature, 100)
|
45
|
+
@plan.add_feature(:boolean_feature, true)
|
46
|
+
@plan.add_feature(:another_number_feature, 314)
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#value=' do
|
50
|
+
it 'correctly marshalls the given type to a string for storage' do
|
51
|
+
boolean_plan_feature = @plan.plan_features.joins(:feature).find_by(features: { name: 'boolean_feature'})
|
52
|
+
number_plan_feature = @plan.plan_features.joins(:feature).find_by(features: { name: 'number_feature'})
|
53
|
+
|
54
|
+
expect(boolean_plan_feature['value']).to eq('true')
|
55
|
+
expect(number_plan_feature['value']).to eq('100')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe '#value' do
|
60
|
+
it 'correctly un-marshalls to the correct data type' do
|
61
|
+
boolean_plan_feature = @plan.plan_features.joins(:feature).find_by(features: { name: 'boolean_feature'})
|
62
|
+
number_plan_feature = @plan.plan_features.joins(:feature).find_by(features: { name: 'number_feature'})
|
63
|
+
|
64
|
+
expect(boolean_plan_feature.value).to eq(true)
|
65
|
+
expect(number_plan_feature.value).to eq(100)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe '#to_s' do
|
70
|
+
it 'returns the qty and description for non-boolean features' do
|
71
|
+
number_plan_feature = @plan.plan_features.joins(:feature).find_by(features: { name: 'number_feature'})
|
72
|
+
|
73
|
+
expect(number_plan_feature.to_s).to eq('100 Data Retention')
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'returns the description for boolean features' do
|
77
|
+
boolean_plan_feature = @plan.plan_features.joins(:feature).find_by(features: { name: 'boolean_feature'})
|
78
|
+
|
79
|
+
expect(boolean_plan_feature.to_s).to eq('boolean_feature')
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'returns the qty and description for non-boolean features' do
|
83
|
+
number_plan_feature = @plan.plan_features.joins(:feature).find_by(features: { name: 'another_number_feature'})
|
84
|
+
|
85
|
+
expect(number_plan_feature.to_s).to eq('314 Months Data Retention')
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
describe Plan, type: :model do
|
4
|
+
|
5
|
+
it { is_expected.to have_many(:subscriptions) }
|
6
|
+
it { is_expected.to have_many(:plan_features) }
|
7
|
+
it { is_expected.to have_many(:features) }
|
8
|
+
|
9
|
+
describe '#is_upgrade_from?' do
|
10
|
+
|
11
|
+
it 'returns true if the price is higher' do
|
12
|
+
plan = Plan.new(price: 123.23)
|
13
|
+
cheaper_plan = Plan.new(price: 61.61)
|
14
|
+
|
15
|
+
expect(plan.is_upgrade_from?(cheaper_plan)).to be true
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'returns true if the price is the same' do
|
19
|
+
plan = Plan.new(price: 123.23)
|
20
|
+
|
21
|
+
expect(plan.is_upgrade_from?(plan)).to be true
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'returns false if the price is the same or higher' do
|
25
|
+
plan = Plan.new(price: 61.61)
|
26
|
+
more_expensive_plan = Plan.new(price: 123.23)
|
27
|
+
|
28
|
+
expect(plan.is_upgrade_from?(more_expensive_plan)).to be false
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'handles a nil value gracefully' do
|
32
|
+
plan = Plan.new(price: 123.23)
|
33
|
+
cheaper_plan = Plan.new
|
34
|
+
|
35
|
+
expect(plan.is_upgrade_from?(cheaper_plan)).to be true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe 'metadata' do
|
40
|
+
it 'can set plan JSON metadata as a hash' do
|
41
|
+
plan = Plan.new(price: 0.0)
|
42
|
+
plan.metadata = { 'foo': 'bar' }
|
43
|
+
|
44
|
+
expect(plan.metadata).to eq({'foo' => 'bar'})
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '#free?' do
|
49
|
+
it 'returns whether the plan is a free plan' do
|
50
|
+
plan = Plan.new(price: 0.0)
|
51
|
+
expect(plan).to be_free
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '#is_downgrade_from?' do
|
56
|
+
it 'is the opposite of #is_upgrade_from?' do
|
57
|
+
plan = Plan.new(price: 123.23)
|
58
|
+
cheaper_plan = Plan.new
|
59
|
+
more_expensive_plan = Plan.new(price: 123.23)
|
60
|
+
|
61
|
+
expect(plan.is_downgrade_from?(cheaper_plan)).to be false
|
62
|
+
expect(plan.is_downgrade_from?(more_expensive_plan)).to be false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe 'features' do
|
67
|
+
before(:all) do
|
68
|
+
@number_feature = Feature.find_or_create_by(name: 'number_feature')
|
69
|
+
@number_feature.update({
|
70
|
+
description: "My numeric feature",
|
71
|
+
feature_type: :number,
|
72
|
+
unit: "some_unit",
|
73
|
+
display_order: 1
|
74
|
+
})
|
75
|
+
|
76
|
+
@boolean_feature = Feature.find_or_create_by(name: 'boolean_feature')
|
77
|
+
@boolean_feature.update({
|
78
|
+
description: "My boolean feature",
|
79
|
+
feature_type: :boolean,
|
80
|
+
display_order: 2
|
81
|
+
})
|
82
|
+
|
83
|
+
@plan = Plan.find_or_create_by(stripe_id: 'my_plan')
|
84
|
+
@plan.update({
|
85
|
+
name: 'My Plan',
|
86
|
+
price: 0.0,
|
87
|
+
interval: 'month',
|
88
|
+
interval_count: 1,
|
89
|
+
statement_descriptor: 'My Awesome Plan',
|
90
|
+
trial_period_days: 30,
|
91
|
+
display_order: 1
|
92
|
+
})
|
93
|
+
|
94
|
+
@plan.add_feature(:number_feature, 100)
|
95
|
+
@plan.add_feature(:boolean_feature, true)
|
96
|
+
end
|
97
|
+
|
98
|
+
describe '#add_feature' do
|
99
|
+
it 'adds a PlanFeature to a Plan' do
|
100
|
+
expect(@plan.features.map(&:name)).to include('boolean_feature', 'number_feature')
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe '#has_feature?' do
|
105
|
+
it 'determines if a plan as certain feature' do
|
106
|
+
expect(@plan.has_feature?('boolean_feature')).to be true
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe '#feature_value' do
|
111
|
+
it 'returns the value of a PlanFeature' do
|
112
|
+
expect(@plan.feature_value('number_feature')).to eq(100)
|
113
|
+
expect(@plan.feature_value('boolean_feature')).to be true
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe '#allows?' do
|
118
|
+
it 'returns whether a feature is enabled' do
|
119
|
+
expect(@plan.allows?('number_feature')).to be true
|
120
|
+
expect(@plan.allows?('boolean_feature')).to be true
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe '#boolean_plan_features' do
|
125
|
+
it 'returns the plan features of boolean type' do
|
126
|
+
expect(@plan.boolean_plan_features.map { |pf| pf.feature.name }).to contain_exactly('boolean_feature')
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe '#non_boolean_plan_features' do
|
131
|
+
it 'returns the plan features that are not of boolean type' do
|
132
|
+
expect(@plan.non_boolean_plan_features.map { |pf| pf.feature.name }).to contain_exactly('number_feature')
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
describe Subscription, type: :model do
|
4
|
+
|
5
|
+
it { is_expected.to belong_to(:plan) }
|
6
|
+
|
7
|
+
let(:user) { User.new({email: 'bsbodden@integrallis.com'}) }
|
8
|
+
|
9
|
+
let(:less_expensive_plan) { Plan.new({
|
10
|
+
stripe_id: 'free_plan',
|
11
|
+
name: 'My Plan',
|
12
|
+
price: 0.0,
|
13
|
+
interval: 'month',
|
14
|
+
interval_count: 1,
|
15
|
+
statement_descriptor: 'Cheaper Awesome Plan',
|
16
|
+
trial_period_days: 30,
|
17
|
+
display_order: 1
|
18
|
+
})}
|
19
|
+
|
20
|
+
let(:plan) { Plan.new({
|
21
|
+
stripe_id: 'my_plan',
|
22
|
+
name: 'My Plan',
|
23
|
+
price: 10.0,
|
24
|
+
interval: 'month',
|
25
|
+
interval_count: 1,
|
26
|
+
statement_descriptor: 'My Awesome Plan',
|
27
|
+
trial_period_days: 30,
|
28
|
+
display_order: 1
|
29
|
+
})}
|
30
|
+
|
31
|
+
let(:more_expensive_plan) { Plan.new({
|
32
|
+
stripe_id: 'another_plan',
|
33
|
+
name: 'Another Plan',
|
34
|
+
price: 100.0,
|
35
|
+
interval: 'month',
|
36
|
+
interval_count: 1,
|
37
|
+
statement_descriptor: 'More Expensive Awesome Plan',
|
38
|
+
trial_period_days: 30,
|
39
|
+
display_order: 2
|
40
|
+
})}
|
41
|
+
|
42
|
+
let(:subscription) { Subscription.new(user: user, plan: plan) }
|
43
|
+
|
44
|
+
describe '#subscription_owner' do
|
45
|
+
it 'returns the owner of the subscription' do
|
46
|
+
expect(subscription.subscription_owner).to be(user)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#subscription_owner_email' do
|
51
|
+
it 'returns the owner of the subscription' do
|
52
|
+
expect(subscription.subscription_owner_email).to eq(user.email)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#subscription_owner_description' do
|
57
|
+
it 'returns a descriptive field for the owner of the subscription' do
|
58
|
+
expect(subscription.subscription_owner_description).to eq(user.email)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe '#describe_difference' do
|
63
|
+
it 'returns whether it would be an Upgrade to a given plan' do
|
64
|
+
expect(subscription.describe_difference(more_expensive_plan)).to eq('Upgrade')
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'returns whether it would be a Downgrade to a given plan' do
|
68
|
+
expect(subscription.describe_difference(less_expensive_plan)).to eq('Downgrade')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,8 +1,16 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
require 'codeclimate-test-reporter'
|
2
|
+
CodeClimate::TestReporter.start
|
3
|
+
require 'simplecov'
|
4
|
+
SimpleCov.start
|
5
|
+
|
6
|
+
# ENV['BUNDLE_GEMFILE'] = File.expand_path('../../Gemfile', __FILE__)
|
7
|
+
# require "bundler"
|
8
|
+
require "shoulda-matchers"
|
9
|
+
# Bundler.setup
|
4
10
|
|
5
11
|
RSpec.configure do |config|
|
12
|
+
config.include(Shoulda::Matchers::ActiveModel, type: :model)
|
13
|
+
config.include(Shoulda::Matchers::ActiveRecord, type: :model)
|
6
14
|
|
7
15
|
config.expect_with :rspec do |expectations|
|
8
16
|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stripe_saas
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brian Sam-Bodden
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-12-
|
11
|
+
date: 2015-12-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -106,7 +106,8 @@ dependencies:
|
|
106
106
|
- - ">="
|
107
107
|
- !ruby/object:Gem::Version
|
108
108
|
version: 3.4.1
|
109
|
-
description:
|
109
|
+
description: A Rails Engine that provides everything you need to Stripe enable your
|
110
|
+
app.
|
110
111
|
email:
|
111
112
|
- bsbodden@integrallis.com
|
112
113
|
executables: []
|
@@ -147,14 +148,15 @@ files:
|
|
147
148
|
- lib/stripe_saas/engine.rb
|
148
149
|
- lib/stripe_saas/version.rb
|
149
150
|
- lib/tasks/stripe_saas_tasks.rake
|
150
|
-
- spec/concerns/plan_spec.rb
|
151
151
|
- spec/dummy/README.rdoc
|
152
152
|
- spec/dummy/Rakefile
|
153
153
|
- spec/dummy/app/assets/javascripts/application.js
|
154
154
|
- spec/dummy/app/assets/stylesheets/application.css
|
155
155
|
- spec/dummy/app/controllers/application_controller.rb
|
156
156
|
- spec/dummy/app/helpers/application_helper.rb
|
157
|
+
- spec/dummy/app/models/feature.rb
|
157
158
|
- spec/dummy/app/models/plan.rb
|
159
|
+
- spec/dummy/app/models/plan_feature.rb
|
158
160
|
- spec/dummy/app/models/subscription.rb
|
159
161
|
- spec/dummy/app/models/user.rb
|
160
162
|
- spec/dummy/app/views/layouts/application.html.erb
|
@@ -188,15 +190,19 @@ files:
|
|
188
190
|
- spec/dummy/db/migrate/20150101233243_devise_create_users.rb
|
189
191
|
- spec/dummy/db/migrate/20150102001921_create_subscriptions.rb
|
190
192
|
- spec/dummy/db/migrate/20150102001930_create_plans.rb
|
193
|
+
- spec/dummy/db/migrate/20151220161809_create_features.rb
|
194
|
+
- spec/dummy/db/migrate/20151220161817_create_plan_features.rb
|
191
195
|
- spec/dummy/db/schema.rb
|
192
196
|
- spec/dummy/db/test.sqlite3
|
193
|
-
- spec/dummy/log/development.log
|
194
197
|
- spec/dummy/log/test.log
|
195
198
|
- spec/dummy/public/404.html
|
196
199
|
- spec/dummy/public/422.html
|
197
200
|
- spec/dummy/public/500.html
|
198
201
|
- spec/dummy/public/favicon.ico
|
199
|
-
- spec/
|
202
|
+
- spec/models/feature_spec.rb
|
203
|
+
- spec/models/plan_feature_spec.rb
|
204
|
+
- spec/models/plan_spec.rb
|
205
|
+
- spec/models/subscription_spec.rb
|
200
206
|
- spec/rails_helper.rb
|
201
207
|
- spec/spec_helper.rb
|
202
208
|
homepage: https://github.com/integrallis/stripe_saas
|
@@ -224,12 +230,13 @@ signing_key:
|
|
224
230
|
specification_version: 4
|
225
231
|
summary: Stripe Payments/Subscription Engine for Rails 4.
|
226
232
|
test_files:
|
227
|
-
- spec/concerns/plan_spec.rb
|
228
233
|
- spec/dummy/app/assets/javascripts/application.js
|
229
234
|
- spec/dummy/app/assets/stylesheets/application.css
|
230
235
|
- spec/dummy/app/controllers/application_controller.rb
|
231
236
|
- spec/dummy/app/helpers/application_helper.rb
|
237
|
+
- spec/dummy/app/models/feature.rb
|
232
238
|
- spec/dummy/app/models/plan.rb
|
239
|
+
- spec/dummy/app/models/plan_feature.rb
|
233
240
|
- spec/dummy/app/models/subscription.rb
|
234
241
|
- spec/dummy/app/models/user.rb
|
235
242
|
- spec/dummy/app/views/layouts/application.html.erb
|
@@ -263,9 +270,10 @@ test_files:
|
|
263
270
|
- spec/dummy/db/migrate/20150101233243_devise_create_users.rb
|
264
271
|
- spec/dummy/db/migrate/20150102001921_create_subscriptions.rb
|
265
272
|
- spec/dummy/db/migrate/20150102001930_create_plans.rb
|
273
|
+
- spec/dummy/db/migrate/20151220161809_create_features.rb
|
274
|
+
- spec/dummy/db/migrate/20151220161817_create_plan_features.rb
|
266
275
|
- spec/dummy/db/schema.rb
|
267
276
|
- spec/dummy/db/test.sqlite3
|
268
|
-
- spec/dummy/log/development.log
|
269
277
|
- spec/dummy/log/test.log
|
270
278
|
- spec/dummy/public/404.html
|
271
279
|
- spec/dummy/public/422.html
|
@@ -273,6 +281,9 @@ test_files:
|
|
273
281
|
- spec/dummy/public/favicon.ico
|
274
282
|
- spec/dummy/Rakefile
|
275
283
|
- spec/dummy/README.rdoc
|
276
|
-
- spec/
|
284
|
+
- spec/models/feature_spec.rb
|
285
|
+
- spec/models/plan_feature_spec.rb
|
286
|
+
- spec/models/plan_spec.rb
|
287
|
+
- spec/models/subscription_spec.rb
|
277
288
|
- spec/rails_helper.rb
|
278
289
|
- spec/spec_helper.rb
|