zillion 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.
@@ -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: