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.
- checksums.yaml +7 -0
- data/Rakefile +2 -0
- data/lib/mapper.rb +203 -0
- data/lib/plan.rb +270 -0
- data/spec/mapper_spec.rb +77 -0
- data/spec/plans_spec.rb +489 -0
- data/zillion.gemspec +21 -0
- metadata +84 -0
checksums.yaml
ADDED
@@ -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
|
data/Rakefile
ADDED
data/lib/mapper.rb
ADDED
@@ -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
|
data/lib/plan.rb
ADDED
@@ -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
|
data/spec/mapper_spec.rb
ADDED
@@ -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
|
data/spec/plans_spec.rb
ADDED
@@ -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
|
+
|
data/zillion.gemspec
ADDED
@@ -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:
|