zillion 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 97bd070693be154816a594da56bcbd34e3b301d9
4
+ data.tar.gz: 4056c680b9d5daa7f370ded1082fd29a8b3223d2
5
+ SHA512:
6
+ metadata.gz: c9518fc1995e843a57fb144abecb227d48465edd39e72101600b20fe595aa75b676952be2e4b29d79e3ed165e7c8bcc6926cae354687c25ef7aa9ce25163ed6f
7
+ data.tar.gz: b79c43fc71310b60779a092dafe9ba3c5d23ff764f6069abf191858105307e4a83adf46daebbab3e7c961adfdbb01fbaf2ce48de497fc0ef5c54c5edf4c31f15
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,203 @@
1
+ require_relative './plan'
2
+
3
+ module Zillion
4
+
5
+ class PlanFinder
6
+
7
+ def self.init(mapper)
8
+ @mapper = mapper
9
+ self
10
+ end
11
+
12
+ def self.mapper
13
+ @mapper
14
+ end
15
+
16
+ def self.available_plans_for(counts)
17
+ mapper.all.select { |key, plan| plan.available_for?(counts) }
18
+ end
19
+
20
+ def self.unavailable_plans_for(counts)
21
+ mapper.all.select { |key, plan| !plan.available_for?(counts) }
22
+ end
23
+
24
+ def initialize(counts)
25
+ raise "Mapper not set!" if self.class.mapper.nil?
26
+ @counts = counts
27
+ end
28
+
29
+ def is_upgrade?(from, to)
30
+ compare(from, to, '<')
31
+ end
32
+
33
+ def is_downgrade?(from, to)
34
+ compare(from, to, '>')
35
+ end
36
+
37
+ def available_plans
38
+ # @available_plans ||=
39
+ self.class.available_plans_for(@counts)
40
+ end
41
+
42
+ def sorted_available_plans
43
+ sorted_keys = available_plans.keys.sort do |a,b|
44
+ mapper.get(a).monthly_fee_for(@counts) <=> mapper.get(b).monthly_fee_for(@counts)
45
+ end
46
+
47
+ sorted_keys.map { |key| mapper.get(key) }
48
+ end
49
+
50
+ def best_plan
51
+ return nil if available_plans.empty?
52
+ sorted_available_plans.first
53
+ end
54
+
55
+ private
56
+
57
+ def mapper
58
+ self.class.mapper
59
+ end
60
+
61
+ def compare(one, two, direction)
62
+ one = PlanMapper.get(one) unless one.is_a?(Plan)
63
+ two = PlanMapper.get(two) unless two.is_a?(Plan)
64
+ one.monthly_fee_for(@counts).send(direction, two.monthly_fee_for(@counts))
65
+ end
66
+
67
+ end
68
+
69
+ class PlanMapper
70
+
71
+ attr_accessor :plans
72
+
73
+ def initialize(specs, opts = {})
74
+ @plans = {}
75
+ specs.each do |key, spec|
76
+ add(key, Plan.factory({ name: key }.merge(deep_symbolize_keys(spec))))
77
+ end
78
+ plans.keys
79
+ end
80
+
81
+ def all
82
+ raise "Plans not loaded yet!" if plans.empty?
83
+ plans
84
+ end
85
+
86
+ def keys
87
+ plans.keys
88
+ end
89
+
90
+ def add(key, plan)
91
+ @plans[key.to_sym] = plan
92
+ end
93
+
94
+ def del(key)
95
+ plans.delete(key.to_sym)
96
+ end
97
+
98
+ def get(key)
99
+ raise "Please load first. No need to be an ass." if plans.empty?
100
+ plans[key.to_sym]
101
+ end
102
+
103
+ def exists?(key)
104
+ !!plans[key.to_sym]
105
+ end
106
+
107
+ def all_free
108
+ all.select { |plan| plan.is_a?(FreePlan) }
109
+ end
110
+
111
+ def all_paid
112
+ all.select { |plan| !plan.is_a?(FreePlan) }
113
+ end
114
+
115
+ private
116
+
117
+ def symbolize_keys(obj)
118
+ obj.inject({}) { |h, (k,v)| h[k.to_sym] = v; h }
119
+ end
120
+
121
+ def deep_symbolize_keys(obj)
122
+ symbolize_keys(obj).reduce({}) do |h, (k,v)|
123
+ new_val = v.is_a?(Hash) ? deep_symbolize_keys(v) : v
124
+ h.merge({k => new_val})
125
+ end
126
+ end
127
+
128
+ =begin
129
+
130
+ def self.init(defs, levels = nil, mappings = {})
131
+ raise 'Already initialized!' if @defs
132
+ @levels, @mappings = levels, mappings
133
+ @defs = deep_symbolize_keys(defs)
134
+ @levels = @defs.keys if @levels.nil?
135
+ end
136
+
137
+ def self.defs
138
+ @defs
139
+ end
140
+
141
+ def self.levels
142
+ @levels
143
+ end
144
+
145
+ def self.mappings
146
+ @mappings || {}
147
+ end
148
+
149
+ def self.all
150
+ @list ||= levels.collect { |key| self.get(key) }
151
+ end
152
+
153
+ def self.all_free
154
+ all.select { |plan| plan.is_a?(FreePlan) }
155
+ end
156
+
157
+ def self.all_paid
158
+ all.select { |plan| !plan.is_a?(FreePlan) }
159
+ end
160
+
161
+ def self.exists?(key)
162
+ !!index_of(key)
163
+ end
164
+
165
+ def self.is_upgrade?(from, to)
166
+ # Plan.new(from) < Plan.new(to)
167
+ index_of(from) < index_of(to)
168
+ end
169
+
170
+ def self.is_downgrade?(from, to)
171
+ # Plan.new(from) > Plan.new(to)
172
+ index_of(from) > index_of(to)
173
+ end
174
+
175
+ def self.get(key)
176
+ raise "Please initialize first" if @defs.nil?
177
+ key = map(key)
178
+
179
+ @@instances[key] ||= lambda do
180
+ raise "Plan not found: #{key}" if @defs[key].nil?
181
+ type = @defs[key][:type] || 'fixed'
182
+
183
+ klass = TYPES[type.to_s] or raise "Invalid type!"
184
+ Kernel.const_get('PlanMapper::' + klass).new(key, @defs[key])
185
+ end.call
186
+ end
187
+
188
+ private
189
+
190
+ def self.index_of(key)
191
+ levels.index(map(key))
192
+ end
193
+
194
+ # returns mapped plan key for older plans
195
+ def self.map(key)
196
+ mappings[key.to_sym] || key.to_sym
197
+ end
198
+
199
+ =end
200
+
201
+ end
202
+
203
+ end
@@ -0,0 +1,270 @@
1
+ module Zillion
2
+
3
+ class Plan
4
+
5
+ class PlanArgumentError < ArgumentError; end
6
+ class MissingCountError < PlanArgumentError; end
7
+ class OverLimitsError < PlanArgumentError; end
8
+ class InvalidFeeError < PlanArgumentError; end
9
+ class InvalidCountError < PlanArgumentError; end
10
+
11
+ class TierError < PlanArgumentError; end
12
+ class TierNotFoundError < TierError; end
13
+ class MultipleTiersFoundError < TierError; end
14
+
15
+ TYPES = {
16
+ 'free' => 'FreePlan',
17
+ 'fixed' => 'FixedPlan',
18
+ 'custom' => 'CustomPlan',
19
+ 'tiered' => 'TieredPlan',
20
+ 'metered' => 'MeteredPlan',
21
+ 'fixed-metered' => 'FixedMeteredPlan'
22
+ }
23
+
24
+ def self.factory(spec)
25
+ unless type = spec[:type]
26
+ fee = spec[:monthly_fee].to_i
27
+ type = fee == 0 ? 'free' : fee > 0 ? 'fixed' : 'unknown'
28
+
29
+ type = 'fixed-metered' if type == 'fixed' && spec[:tiers]
30
+ end
31
+
32
+ klass = TYPES[type.to_s] or raise "Invalid type: #{type}"
33
+ Kernel.const_get('Zillion::' + klass).new(spec)
34
+ end
35
+
36
+ def initialize(spec = {})
37
+ @spec = spec
38
+ end
39
+
40
+ def name
41
+ spec[:name]
42
+ end
43
+
44
+ def has?(feature)
45
+ features.include?(feature.to_s) # features is an array of strings
46
+ end
47
+
48
+ def limit_for(what)
49
+ limits[what.to_sym]
50
+ end
51
+
52
+ def <=>(other_plan)
53
+ # other_plan = Plan.factory(other_plan) unless other_plan.is_a?(Plan)
54
+ self.monthly_fee <=> other_plan.monthly_fee
55
+ end
56
+
57
+ def amounts_for(counts)
58
+ { monthly_fee: monthly_fee }
59
+ end
60
+
61
+ def monthly_fee_for(counts)
62
+ ensure_available!(counts)
63
+ sum(amounts_for(counts))
64
+ end
65
+
66
+ def available_for?(counts)
67
+ limits.each do |key, limit|
68
+ val = counts[key] or raise MissingCountError.new("Count required for #{key}")
69
+ raise InvalidCountError.new("Negative count for #{key}: #{val}") if val.to_i < 0
70
+
71
+ return false if limit.to_i < val.to_i
72
+ end
73
+ true
74
+ end
75
+
76
+ def features
77
+ spec[:features] || []
78
+ end
79
+
80
+ def limits
81
+ spec[:limits] || {}
82
+ end
83
+
84
+ private
85
+
86
+ attr_reader :spec
87
+
88
+ def ensure_available!(counts)
89
+ unless available_for?(counts)
90
+ raise OverLimitsError.new("This plan has lower limits than: #{counts}")
91
+ end
92
+ end
93
+
94
+ def sum(amounts)
95
+ total = 0
96
+ amounts.each { |what, fee| total += fee }
97
+ total
98
+ end
99
+
100
+ end
101
+
102
+ class FreePlan < Plan
103
+
104
+ def monthly_fee
105
+ 0
106
+ end
107
+
108
+ end
109
+
110
+ class FixedPlan < Plan
111
+
112
+ def monthly_fee
113
+ unless spec[:monthly_fee].is_a?(Numeric)
114
+ raise InvalidFeeError.new("Invalid monthly_fee: #{spec[:monthly_fee]}")
115
+ end
116
+
117
+ spec[:monthly_fee].to_f
118
+ end
119
+
120
+ end
121
+
122
+ class TieredPlan < Plan
123
+
124
+ def amounts_for(counts)
125
+ tier = matching_tier_for(counts)
126
+ obj = {}
127
+ obj[tier.last.to_sym] = tier[1]
128
+ obj
129
+ end
130
+
131
+ def matching_tier_for(counts)
132
+ if tiers = matching_tiers_for(counts) and tiers.any?
133
+ # raise "More than one tier matches for #{counts}" if tiers.count > 1
134
+ tiers.first
135
+ else
136
+ raise "No matching tiers found for #{counts.inspect} in #{name} plan."
137
+ end
138
+ end
139
+
140
+ def available_for?(counts)
141
+ super
142
+ matching_tiers_for(counts).any?
143
+ end
144
+
145
+ private
146
+
147
+ def matching_tiers_for(counts)
148
+ # sort by amount (format is [range, monthly_cost, index, type])
149
+ get_matching_tiers(counts).sort { |a, b| b[1] <=> a[1] }
150
+ end
151
+
152
+ def find_tier_for(tiers, units)
153
+ # raise "Units not set!" unless units
154
+ arr = tiers.select { |t| t[0].include?(units) }
155
+
156
+ if arr.empty?
157
+ raise TierNotFoundError.new("No matching tiers found for #{units} units.")
158
+ elsif arr.count > 1
159
+ raise MultipleTiersFoundError.new("Unit count is in more than one tier!")
160
+ end
161
+
162
+ arr.first # + [tiers.index(arr.first)]
163
+ end
164
+
165
+ def tier_groups
166
+ spec[:tiers].each_with_object({}) do |(key, list), obj|
167
+ obj[key] = list.map do |t|
168
+ h = symbolize_keys(t)
169
+ [ h[:from]..h[:to], h[tier_cost_key] ]
170
+ end
171
+ end
172
+ end
173
+
174
+ def tier_cost_key
175
+ :monthly_cost
176
+ end
177
+
178
+ def get_matching_tiers(counts)
179
+ tier_groups.map do |key, tiers|
180
+ units = counts[key] or raise "Unit count required for #{key}"
181
+ find_tier_for(tiers, units.to_i) + [key]
182
+ end.compact
183
+ end
184
+
185
+ def symbolize_keys(obj)
186
+ obj.inject({}) { |h, (k,v)| h[k.to_sym] = v; h }
187
+ end
188
+
189
+ end
190
+
191
+ class MeteredPlan < TieredPlan
192
+
193
+ def amounts_for(counts)
194
+ obj = {}
195
+ matching_tiers_for(counts).map do |tier|
196
+ units = units_for(tier.last, counts)
197
+ obj[tier.last.to_sym] = units * tier[1]
198
+ end
199
+
200
+ obj
201
+ end
202
+
203
+ def units_for(type, counts)
204
+ counts[type]
205
+ end
206
+
207
+ def tier_cost_key
208
+ :monthly_unit_cost
209
+ end
210
+
211
+ end
212
+
213
+ class FixedMeteredPlan < MeteredPlan
214
+
215
+ def amounts_for(counts)
216
+ obj = super
217
+ obj[:base_fee] = spec[:monthly_fee] ? spec[:monthly_fee].to_f : 0
218
+ obj
219
+ end
220
+
221
+ # a fixed tiered plan is available either if the count is below the fixed limit, or,
222
+ # if above that limit but a tier exists for that type
223
+ def available_for?(counts)
224
+ limits.each do |key, limit|
225
+ val = counts[key] or raise MissingCountError.new("Count required for #{key}")
226
+
227
+ # above limit, so see if a matching tier is found
228
+ if val.to_i <= limit.to_i # below limit
229
+ next
230
+ elsif tiers = tier_groups[key]
231
+ begin
232
+ find_tier_for(tiers, val.to_i)
233
+ next # tier found, so next one please
234
+ rescue TierError => e
235
+ # no tier found
236
+ end
237
+ end
238
+
239
+ return false
240
+ end
241
+
242
+ true
243
+ end
244
+
245
+ def units_for(type, counts)
246
+ units = counts[type]
247
+
248
+ # if a hard limit exists for that type, it means it's included
249
+ # so we should reduce the count of actual units
250
+ units -= limits[type] if limits[type]
251
+
252
+ units
253
+ end
254
+
255
+ def tier_cost_key
256
+ :monthly_unit_cost
257
+ end
258
+
259
+ def get_matching_tiers(counts)
260
+ tier_groups.map do |key, tiers|
261
+ units = counts[key] or raise "Unit count required for #{key}"
262
+ next if limits[key] and limits[key] > units.to_i
263
+
264
+ find_tier_for(tiers, units.to_i) + [key]
265
+ end.compact
266
+ end
267
+
268
+ end
269
+
270
+ end
@@ -0,0 +1,77 @@
1
+ require_relative '../lib/mapper'
2
+
3
+ describe 'PlanMapper' do
4
+
5
+ describe '.load' do
6
+
7
+ it 'initializes plans and builds a map' do
8
+ specs = {
9
+ small: { monthly_fee: 0, limits: { products: 100 } },
10
+ large: { monthly_fee: 10, limits: { products: 200 } }
11
+ }
12
+
13
+ mapper = PlanMapper.new(specs)
14
+ expect(mapper.keys).to eql([:small, :large])
15
+ end
16
+
17
+ end
18
+
19
+ end
20
+
21
+ describe 'PlanFinder' do
22
+
23
+ let (:mapper) {
24
+ PlanMapper.new({
25
+ small: { monthly_fee: 0, limits: { products: 100 } },
26
+ large: { monthly_fee: 10, limits: { products: 200 } }
27
+ })
28
+ }
29
+
30
+ it 'explodes if no mapper set' do
31
+ expect { PlanFinder.new({}) }.to raise_error # MissingCountError
32
+ end
33
+
34
+ describe '#available_plans' do
35
+
36
+ it 'explodes if no limits passed' do
37
+ finder = PlanFinder.init(mapper).new({})
38
+ expect { finder.available_plans }.to raise_error Plan::MissingCountError
39
+ end
40
+
41
+ it 'explodes if limit doesnt match' do
42
+ finder = PlanFinder.init(mapper).new({ foo: 123 })
43
+ expect { finder.available_plans }.to raise_error Plan::MissingCountError
44
+ end
45
+
46
+ it 'returns empty object if over all limits' do
47
+ finder = PlanFinder.init(mapper).new({ products: 250 })
48
+ expect(finder.available_plans).to eql({})
49
+ end
50
+
51
+ it 'returns array with higher plan if over within both of them' do
52
+ finder = PlanFinder.init(mapper).new({ products: 150 })
53
+ expect(finder.available_plans).to eql({ large: mapper.get(:large) })
54
+ end
55
+
56
+ it 'returns array with both plans if under the lowest limit' do
57
+ finder = PlanFinder.init(mapper).new({ products: 50 })
58
+ expect(finder.available_plans).to eql(mapper.all)
59
+ end
60
+
61
+ end
62
+
63
+ describe '#best_plan' do
64
+
65
+ it 'returns nil if over all limits' do
66
+ finder = PlanFinder.init(mapper).new({ products: 250 })
67
+ expect(finder.best_plan).to eql(nil)
68
+ end
69
+
70
+ it 'returns lower plan if under the lowest limit' do
71
+ finder = PlanFinder.init(mapper).new({ products: 50 })
72
+ expect(finder.best_plan).to eql(mapper.get(:small))
73
+ end
74
+
75
+ end
76
+
77
+ end
@@ -0,0 +1,489 @@
1
+ require_relative '../lib/plan'
2
+ include Zillion
3
+
4
+ RSpec::Expectations.configuration.on_potential_false_positives = :nothing
5
+
6
+ describe 'FreePlan' do
7
+
8
+ let(:plan) { FreePlan.new }
9
+
10
+ describe '#monthly_fee' do
11
+
12
+ it 'returns 0 as monthly_fee' do
13
+ expect(plan.monthly_fee).to eql(0)
14
+ end
15
+
16
+ end
17
+
18
+ describe '#monthly_fee_for(counts)' do
19
+
20
+ it 'returns 0 if no limits' do
21
+ expect(plan.monthly_fee_for({ apples: 10 })).to eql(0)
22
+ end
23
+
24
+ it 'returns 0 if within limits' do
25
+ other = FreePlan.new(limits: { bananas: 10 })
26
+ expect(other.monthly_fee_for({ bananas: 10 })).to eql(0)
27
+ end
28
+
29
+ it 'raises if not within limits' do
30
+ other = FreePlan.new(limits: { bananas: 10 })
31
+ expect { other.monthly_fee_for({ bananas: 11 }) }.to raise_error
32
+ end
33
+
34
+ it 'raises if not all counts present' do
35
+ other = FreePlan.new(limits: { bananas: 10, apples: 10 })
36
+ expect { other.monthly_fee_for({ apples: 5 }) }.to raise_error
37
+ end
38
+
39
+ end
40
+
41
+ describe '#available_for?(counts)' do
42
+
43
+ it 'returns true if no limits' do
44
+ expect(plan.available_for?({ bananas: 100 }) ).to eql(true)
45
+ end
46
+
47
+ it 'raises not all units for limits are fed' do
48
+ other = FreePlan.new(limits: { bananas: 10 })
49
+ expect { other.available_for?({ apples: 10 }) }.to raise_error
50
+ end
51
+
52
+ it 'returns false if limits/units match, but over the limit' do
53
+ other = FreePlan.new(limits: { bananas: 10 })
54
+ expect(other.available_for?({ bananas: 20 })).to eql(false)
55
+ end
56
+
57
+ it 'returns true if limits/units match, and within the limit' do
58
+ other = FreePlan.new(limits: { bananas: 10, apples: 10 })
59
+ expect(other.available_for?({ bananas: 5, apples: 2 })).to eql(true)
60
+ end
61
+
62
+ end
63
+
64
+ end
65
+
66
+ describe 'FixedPlan' do
67
+
68
+ let(:plan) { FixedPlan.new(monthly_fee: 10) }
69
+
70
+ describe '#monthly_fee' do
71
+
72
+ it 'raises if monthly_fee not set' do
73
+ other = FixedPlan.new(limits: { bananas: 10 })
74
+ expect { other.monthly_fee }.to raise_error
75
+ end
76
+
77
+ it 'returns monthly_fee, if present' do
78
+ expect(plan.monthly_fee).to eql(10.0)
79
+ end
80
+
81
+ end
82
+
83
+ describe '#monthly_fee_for(counts)' do
84
+
85
+ it 'returns monthly_fee if no limits' do
86
+ expect(plan.monthly_fee_for({ apples: 10 })).to eql(10.0)
87
+ end
88
+
89
+ it 'returns monthly_fee if within limits' do
90
+ other = FixedPlan.new(monthly_fee: 10, limits: { bananas: 10 })
91
+ expect(other.monthly_fee_for({ bananas: 10 })).to eql(10.0)
92
+ end
93
+
94
+ it 'raises if not within limits' do
95
+ other = FixedPlan.new(monthly_fee: 10, limits: { bananas: 10 })
96
+ expect { other.monthly_fee_for({ bananas: 11 }) }.to raise_error
97
+ end
98
+
99
+ it 'raises if not all counts present' do
100
+ other = FixedPlan.new(monthly_fee: 10, limits: { bananas: 10, apples: 10 })
101
+ expect { other.monthly_fee_for({ apples: 5 }) }.to raise_error
102
+ end
103
+
104
+ end
105
+
106
+ describe '#available_for?(counts)' do
107
+
108
+ it 'returns true if no limits' do
109
+ expect(plan.available_for?({ bananas: 100 }) ).to eql(true)
110
+ end
111
+
112
+ it 'raises not all units for limits are fed' do
113
+ other = FixedPlan.new(monthly_fee: 10, limits: { bananas: 10 })
114
+ expect { other.available_for?({ apples: 10 }) }.to raise_error
115
+ end
116
+
117
+ it 'returns false if limits/units match, but over the limit' do
118
+ other = FixedPlan.new(monthly_fee: 10, limits: { bananas: 10 })
119
+ expect(other.available_for?({ bananas: 20 })).to eql(false)
120
+ end
121
+
122
+ it 'returns true if limits/units match, and within the limit' do
123
+ other = FixedPlan.new(monthly_fee: 10, limits: { bananas: 10, apples: 10 })
124
+ expect(other.available_for?({ bananas: 5, apples: 2 })).to eql(true)
125
+ end
126
+
127
+ end
128
+
129
+ end
130
+
131
+
132
+ describe 'TieredPlan', 'Single tier' do
133
+
134
+ let(:plan) {
135
+ TieredPlan.new(
136
+ limits: { bananas: 10 },
137
+ tiers: {
138
+ products: [
139
+ { from: 0, to: 100, monthly_cost: 20 },
140
+ { from: 101, to: 200, monthly_cost: 30 },
141
+ ]
142
+ })
143
+ }
144
+
145
+ describe '#monthly_fee' do
146
+
147
+ it 'does not respond to it' do
148
+ expect { plan.monthly_fee }.to raise_error(NameError)
149
+ end
150
+
151
+ end
152
+
153
+ describe '#monthly_fee_for(counts)' do
154
+
155
+ it 'raises if missing unit for tier' do
156
+ expect { plan.monthly_fee_for({ bananas: 5 }) }.to raise_error
157
+ end
158
+
159
+ it 'raises if missing unit for fixed limit' do
160
+ expect { plan.monthly_fee_for({ products: 25 }) }.to raise_error
161
+ end
162
+
163
+ it 'raises if not within limits' do
164
+ expect { plan.monthly_fee_for({ bananas: 5, products: -5 }) }.to raise_error
165
+ end
166
+
167
+ it 'returns monthly fee for tier if within its limit' do
168
+ expect(plan.monthly_fee_for({ bananas: 5, products: 25 })).to eql(20)
169
+ end
170
+
171
+ end
172
+
173
+ describe '#available_for?(counts)' do
174
+
175
+ it 'raises if missing unit for tier' do
176
+ expect { plan.available_for?({ bananas: 5 }) }.to raise_error
177
+ end
178
+
179
+ it 'raises if missing unit for fixed limit' do
180
+ expect { plan.available_for?({ products: 25 }) }.to raise_error
181
+ end
182
+
183
+ it 'raises if not within limits' do
184
+ expect { plan.available_for?({ bananas: 5, products: -5 }) }.to raise_error
185
+ end
186
+
187
+ it 'returns monthly fee for tier if within its limit' do
188
+ expect(plan.available_for?({ bananas: 5, products: 25 })).to eql(true)
189
+ end
190
+
191
+ end
192
+
193
+ end
194
+
195
+ describe 'TieredPlan', 'Multiple tiers' do
196
+
197
+ let(:plan) {
198
+ TieredPlan.new(
199
+ limits: { bananas: 10 },
200
+ tiers: {
201
+ orders: [
202
+ { from: 0, to: 10, monthly_cost: 15 },
203
+ { from: 11, to: 20, monthly_cost: 35 }
204
+ ],
205
+ products: [
206
+ { from: 0, to: 100, monthly_cost: 20 },
207
+ { from: 101, to: 200, monthly_cost: 30 }
208
+ ]
209
+ })
210
+ }
211
+
212
+ describe '#monthly_fee' do
213
+
214
+ it 'does not respond to it' do
215
+ expect { plan.monthly_fee }.to raise_error(NameError)
216
+ end
217
+
218
+ end
219
+
220
+ describe '#monthly_fee_for(counts)' do
221
+
222
+ it 'raises if missing unit for tier' do
223
+ expect { plan.monthly_fee_for({ bananas: 5, products: 25 }) }.to raise_error
224
+ end
225
+
226
+ it 'raises if missing unit for fixed limit' do
227
+ expect { plan.monthly_fee_for({ products: 25 }) }.to raise_error
228
+ end
229
+
230
+ it 'raises if not within limits' do
231
+ expect { plan.monthly_fee_for({ bananas: 5, products: -5 }) }.to raise_error
232
+ end
233
+
234
+ it 'returns monthly fee for tier if within its limit' do
235
+ expect(plan.monthly_fee_for({ bananas: 1, orders: 5, products: 25 })).to eql(20)
236
+ end
237
+
238
+ end
239
+
240
+ describe '#available_for?(counts)' do
241
+
242
+ it 'raises if missing unit for tier' do
243
+ expect { plan.available_for?({ bananas: 5 }) }.to raise_error
244
+ end
245
+
246
+ it 'raises if missing unit for fixed limit' do
247
+ expect { plan.available_for?({ products: 25 }) }.to raise_error
248
+ end
249
+
250
+ it 'raises if not within limits' do
251
+ expect { plan.available_for?({ bananas: 5, products: -5 }) }.to raise_error
252
+ end
253
+
254
+ it 'returns monthly fee for tier if within its limit' do
255
+ expect(plan.available_for?({ bananas: 5, orders: 5, products: 25 })).to eql(true)
256
+ end
257
+
258
+ end
259
+
260
+ end
261
+
262
+ describe 'MeteredPlan', 'Single tier' do
263
+
264
+ let(:plan) {
265
+ MeteredPlan.new(
266
+ limits: { bananas: 10 },
267
+ tiers: {
268
+ products: [
269
+ { from: 0, to: 100, monthly_unit_cost: 2 },
270
+ { from: 101, to: 200, monthly_unit_cost: 1.5 }
271
+ ]
272
+ })
273
+ }
274
+
275
+ describe '#monthly_fee' do
276
+
277
+ it 'does not respond to it' do
278
+ expect { plan.monthly_fee }.to raise_error(NameError)
279
+ end
280
+
281
+ end
282
+
283
+ describe '#monthly_fee_for(counts)' do
284
+
285
+ it 'raises if missing unit for tier' do
286
+ expect { plan.monthly_fee_for({ bananas: 5 }) }.to raise_error
287
+ end
288
+
289
+ it 'raises if missing unit for fixed limit' do
290
+ expect { plan.monthly_fee_for({ products: 25 }) }.to raise_error
291
+ end
292
+
293
+ it 'raises if not within limits' do
294
+ expect { plan.monthly_fee_for({ bananas: 5, products: -5 }) }.to raise_error
295
+ end
296
+
297
+ it 'returns monthly fee for tier if within its limit' do
298
+ expect(plan.monthly_fee_for({ bananas: 5, products: 25 })).to eql(50)
299
+ end
300
+
301
+ end
302
+
303
+ describe '#available_for?(counts)' do
304
+
305
+ it 'raises if missing unit for tier' do
306
+ expect { plan.available_for?({ bananas: 5 }) }.to raise_error
307
+ end
308
+
309
+ it 'raises if missing unit for fixed limit' do
310
+ expect { plan.available_for?({ products: 25 }) }.to raise_error
311
+ end
312
+
313
+ it 'raises if not within limits' do
314
+ expect { plan.available_for?({ bananas: 5, products: -5 }) }.to raise_error
315
+ end
316
+
317
+ it 'returns monthly fee for tier if within its limit' do
318
+ expect(plan.available_for?({ bananas: 5, products: 25 })).to eql(true)
319
+ end
320
+
321
+ end
322
+
323
+
324
+ end
325
+
326
+ describe 'MeteredPlan', 'Multiple tiers' do
327
+
328
+ let(:plan) {
329
+ MeteredPlan.new(
330
+ limits: { bananas: 10 },
331
+ tiers: {
332
+ orders: [
333
+ { from: 0, to: 10, monthly_unit_cost: 3 },
334
+ { from: 11, to: 20, monthly_unit_cost: 1 }
335
+ ],
336
+ products: [
337
+ { from: 0, to: 100, monthly_unit_cost: 2 },
338
+ { from: 101, to: 200, monthly_unit_cost: 1.5 }
339
+ ]
340
+ })
341
+ }
342
+
343
+ describe '#monthly_fee' do
344
+
345
+ it 'does not respond to it' do
346
+ expect { plan.monthly_fee }.to raise_error(NameError)
347
+ end
348
+
349
+ end
350
+
351
+ describe '#monthly_fee_for(counts)' do
352
+
353
+ it 'raises if missing unit for tier' do
354
+ expect { plan.monthly_fee_for({ bananas: 5 }) }.to raise_error
355
+ end
356
+
357
+ it 'raises if missing unit for fixed limit' do
358
+ expect { plan.monthly_fee_for({ products: 25 }) }.to raise_error
359
+ end
360
+
361
+ it 'raises if not within limits' do
362
+ expect { plan.monthly_fee_for({ bananas: 5, products: -5 }) }.to raise_error
363
+ end
364
+
365
+ it 'returns monthly fee for tier if within its limit' do
366
+ expect(plan.monthly_fee_for({ bananas: 5, orders: 5, products: 25 })).to eql(65)
367
+ end
368
+
369
+ end
370
+
371
+ describe '#available_for?(counts)' do
372
+
373
+ it 'raises if missing unit for tier' do
374
+ expect { plan.available_for?({ bananas: 5 }) }.to raise_error
375
+ end
376
+
377
+ it 'raises if missing unit for fixed limit' do
378
+ expect { plan.available_for?({ products: 25 }) }.to raise_error
379
+ end
380
+
381
+ it 'raises if not within limits' do
382
+ expect { plan.available_for?({ bananas: 5, products: -5 }) }.to raise_error
383
+ end
384
+
385
+ it 'returns monthly fee for tier if within its limit' do
386
+ expect(plan.available_for?({ bananas: 5, orders: 5, products: 25 })).to eql(true)
387
+ end
388
+
389
+ end
390
+
391
+ end
392
+
393
+ describe 'FixedMeteredPlan' do
394
+
395
+ let(:plan_opts) {
396
+ obj = {
397
+ monthly_fee: 10,
398
+ limits: { bananas: 10, orders: 10, products: 10 },
399
+ tiers: {
400
+ orders: [
401
+ { from: 10, to: 15, monthly_unit_cost: 3 },
402
+ { from: 16, to: 20, monthly_unit_cost: 1 }
403
+ ],
404
+ products: [
405
+ { from: 5, to: 100, monthly_unit_cost: 2 },
406
+ { from: 101, to: 200, monthly_unit_cost: 1.5 }
407
+ ]
408
+ }
409
+ }
410
+ }
411
+
412
+ let(:plan) { FixedMeteredPlan.new(plan_opts) }
413
+
414
+ describe '#monthly_fee' do
415
+
416
+ it 'does not respond to it' do
417
+ expect { plan.monthly_fee }.to raise_error(NameError)
418
+ end
419
+
420
+ end
421
+
422
+ describe '#monthly_fee_for(counts)' do
423
+
424
+ it 'raises if missing unit for tier' do
425
+ expect { plan.monthly_fee_for({ bananas: 5 }) }.to raise_error
426
+ end
427
+
428
+ it 'raises if missing unit for fixed limit' do
429
+ expect { plan.monthly_fee_for({ products: 5 }) }.to raise_error
430
+ end
431
+
432
+ it 'doent explode if negative key for tiered count, but under fixed limit' do
433
+ expect(plan.monthly_fee_for({ bananas: 5, orders: 5, products: -5 })).to eql(10.0)
434
+ end
435
+
436
+ it 'returns monthly fee for tier if below fixed limits' do
437
+ expect(plan.monthly_fee_for({ bananas: 5, orders: 5, products: 5 })).to eql(10.0)
438
+ end
439
+
440
+ it 'returns monthly fee for tier if within its limit' do
441
+ expect(plan.monthly_fee_for({ bananas: 5, orders: 5, products: 25 })).to eql(40.0)
442
+ end
443
+
444
+ it 'returns monthly fee for tier if within its limit' do
445
+ expect(plan.monthly_fee_for({ bananas: 5, orders: 15, products: 25 })).to eql(55.0)
446
+ end
447
+
448
+ end
449
+
450
+ describe '#available_for?(counts)' do
451
+
452
+ it 'raises if missing units for tier' do
453
+ expect { plan.available_for?({ bananas: 5 }) }.to raise_error
454
+ end
455
+
456
+ it 'raises if missing unit for fixed limit' do
457
+ expect { plan.available_for?({ products: 25 }) }.to raise_error
458
+ end
459
+
460
+ it 'doent explode if negative key for limit with tier' do
461
+ expect(plan.available_for?({ bananas: 5, orders: 5, products: -5 })).to eql(true)
462
+ end
463
+
464
+ it 'returns true if below fixed limits' do
465
+ expect(plan.available_for?({ bananas: 5, orders: 5, products: 5 })).to eql(true)
466
+ end
467
+
468
+ it 'returns true if below fixed limits, even if no matching tier exists' do
469
+ plan_opts[:tiers].delete(:products)
470
+ expect(plan.available_for?({ bananas: 5, orders: 5, products: 5 })).to eql(true)
471
+ end
472
+
473
+ it 'returns false if above fixed limit and no tier found' do
474
+ plan_opts[:tiers].delete(:products)
475
+ expect(plan.available_for?({ bananas: 5, orders: 5, products: 15 })).to eql(false)
476
+ end
477
+
478
+ it 'returns true if within its limit' do
479
+ expect(plan.available_for?({ bananas: 5, orders: 5, products: 15 })).to eql(true)
480
+ end
481
+
482
+ it 'returns true if within its limit' do
483
+ expect(plan.available_for?({ bananas: 5, orders: 15, products: 25 })).to eql(true)
484
+ end
485
+
486
+ end
487
+
488
+ end
489
+
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # require File.expand_path("../lib/version", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "zillion"
6
+ s.version = '0.0.1'
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ['Tomás Pollak']
9
+ s.email = ['tomas@forkhq.com']
10
+ # s.homepage = "https://github.com/tomas/zillion"
11
+ s.summary = "A model for SaaS plans with fixed, tiered and metered pricing."
12
+ s.description = "A model for SaaS plans with fixed, tiered and metered pricing."
13
+
14
+ s.required_rubygems_version = ">= 1.3.6"
15
+ s.add_development_dependency "bundler", ">= 1.0.0"
16
+ s.add_development_dependency "rspec", '~> 3.0', '>= 3.0.0'
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
20
+ s.require_path = 'lib'
21
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zillion
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Tomás Pollak
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-04-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 3.0.0
37
+ type: :development
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '3.0'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 3.0.0
47
+ description: A model for SaaS plans with fixed, tiered and metered pricing.
48
+ email:
49
+ - tomas@forkhq.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - Rakefile
55
+ - lib/mapper.rb
56
+ - lib/plan.rb
57
+ - spec/mapper_spec.rb
58
+ - spec/plans_spec.rb
59
+ - zillion.gemspec
60
+ homepage:
61
+ licenses: []
62
+ metadata: {}
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 1.3.6
77
+ requirements: []
78
+ rubyforge_project:
79
+ rubygems_version: 2.2.0
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: A model for SaaS plans with fixed, tiered and metered pricing.
83
+ test_files: []
84
+ has_rdoc: