zillion 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|