superfeature 0.1.4 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +54 -0
- data/lib/superfeature/discount.rb +44 -0
- data/lib/superfeature/feature.rb +4 -5
- data/lib/superfeature/plan.rb +2 -2
- data/lib/superfeature/price.rb +44 -24
- data/lib/superfeature/version.rb +1 -1
- data/lib/superfeature.rb +9 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 49aa8d4e0fec4908fb0ef75f2775154ed4824e79565ffde44f39ccbf723f0e7f
|
|
4
|
+
data.tar.gz: e9e7e20b7eb9c9ac39baeeb68d1311ccad33b843e88270b0cefd7fc023ac251a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 865e638f07b0667a2bfe1eec952877489f75fa8e5dfdf891289fa559de9dae9dab421d71929962bca15c12525ecb8626a8daaa46223803302d455621db1d15c5
|
|
7
|
+
data.tar.gz: '08b0e605470738e1be904119b2e1db03fb1b413b386b3cc19ebfb53e0e710f2e7db23d7f0e276302d1a3b9b596ae85e99c3f891f68e3c650a75f489755882519'
|
data/README.md
CHANGED
|
@@ -301,6 +301,10 @@ module Plans
|
|
|
301
301
|
|
|
302
302
|
# Override features from Base to enable them
|
|
303
303
|
# def priority_support = super.enable
|
|
304
|
+
#
|
|
305
|
+
# Conditionally enable/disable based on a boolean:
|
|
306
|
+
# def dark_mode = super.enable(user.premium?)
|
|
307
|
+
# def legacy_feature = super.disable(user.migrated?)
|
|
304
308
|
|
|
305
309
|
# Link to adjacent plans for navigation
|
|
306
310
|
# def next = plan NextPlan
|
|
@@ -366,6 +370,56 @@ price = Superfeature::Price.new(99.999, amount_precision: 3, percent_precision:
|
|
|
366
370
|
price.to_formatted_s # => "99.999"
|
|
367
371
|
```
|
|
368
372
|
|
|
373
|
+
### Discount objects
|
|
374
|
+
|
|
375
|
+
For more control, use typed Discount objects directly:
|
|
376
|
+
|
|
377
|
+
```ruby
|
|
378
|
+
# Fixed dollar discount
|
|
379
|
+
Superfeature::Discount::Fixed(20) # $20 off
|
|
380
|
+
|
|
381
|
+
# Percentage discount
|
|
382
|
+
Superfeature::Discount::Percent(25) # 25% off
|
|
383
|
+
Superfeature::Discount::Percent(10.5) # 10.5% off
|
|
384
|
+
|
|
385
|
+
# Bundle multiple discounts (note: splat args, not array)
|
|
386
|
+
Superfeature::Discount::Bundle(
|
|
387
|
+
Superfeature::Discount::Fixed(5),
|
|
388
|
+
Superfeature::Discount::Percent(20)
|
|
389
|
+
)
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Or with `include Superfeature`:
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
include Superfeature
|
|
396
|
+
|
|
397
|
+
Price(100).discount(Percent(25))
|
|
398
|
+
Price(100).discount(Fixed(20))
|
|
399
|
+
Price(100).discount(Bundle(Fixed(5), Percent(20)))
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Custom discount sources with `to_discount`
|
|
403
|
+
|
|
404
|
+
Any object can be passed to `Price#discount` if it implements `to_discount`:
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
class Promotion < ApplicationRecord
|
|
408
|
+
def to_discount
|
|
409
|
+
Superfeature::Discount::Percent.new(percent_off)
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
promo = Promotion.find(1)
|
|
414
|
+
price = Superfeature::Price.new(100).discount(promo)
|
|
415
|
+
|
|
416
|
+
price.amount # => discounted amount
|
|
417
|
+
price.discount_source # => the Promotion instance
|
|
418
|
+
price.discount_source.name # => access promotion metadata
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
This allows rich domain objects (promotions, deals, coupons) to be used directly with Price while preserving access to their metadata via `discount_source`.
|
|
422
|
+
|
|
369
423
|
## Comparable libraries
|
|
370
424
|
|
|
371
425
|
There's a few pretty great feature flag libraries that are worth mentioning so you can better evaluate what's right for you.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Superfeature
|
|
2
|
+
module Discount
|
|
3
|
+
class Base
|
|
4
|
+
def to_discount = self
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
class Fixed < Base
|
|
8
|
+
attr_reader :amount
|
|
9
|
+
|
|
10
|
+
def initialize(amount)
|
|
11
|
+
@amount = amount
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def apply(price) = price - amount
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class Percent < Base
|
|
18
|
+
attr_reader :percent
|
|
19
|
+
|
|
20
|
+
def initialize(percent)
|
|
21
|
+
@percent = percent
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def apply(price) = price * (1 - percent / 100.0)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Bundle < Base
|
|
28
|
+
attr_reader :discounts
|
|
29
|
+
|
|
30
|
+
def initialize(*discounts)
|
|
31
|
+
@discounts = discounts.flatten
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def apply(price)
|
|
35
|
+
discounts.reduce(price) { |amt, d| d.to_discount.apply(amt) }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Convenience methods: Discount::Fixed(20) instead of Discount::Fixed.new(20)
|
|
40
|
+
def self.Fixed(amount) = Fixed.new(amount)
|
|
41
|
+
def self.Percent(percent) = Percent.new(percent)
|
|
42
|
+
def self.Bundle(...) = Bundle.new(...)
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/superfeature/feature.rb
CHANGED
|
@@ -12,14 +12,13 @@ module Superfeature
|
|
|
12
12
|
@limit = limit
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def enable
|
|
16
|
-
@limit = Limit::Boolean.new(enabled:
|
|
15
|
+
def enable(value = true)
|
|
16
|
+
@limit = Limit::Boolean.new(enabled: value)
|
|
17
17
|
self
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def disable
|
|
21
|
-
|
|
22
|
-
self
|
|
20
|
+
def disable(value = true)
|
|
21
|
+
enable(!value)
|
|
23
22
|
end
|
|
24
23
|
|
|
25
24
|
def boolean?
|
data/lib/superfeature/plan.rb
CHANGED
|
@@ -39,8 +39,8 @@ module Superfeature
|
|
|
39
39
|
Feature.new(*, **, &)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def enable(*, **)
|
|
43
|
-
feature(*, **, limit: Limit::Boolean.new(enabled:
|
|
42
|
+
def enable(flag = true, *, **)
|
|
43
|
+
feature(*, **, limit: Limit::Boolean.new(enabled: flag))
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def disable(*, **)
|
data/lib/superfeature/price.rb
CHANGED
|
@@ -11,38 +11,39 @@ module Superfeature
|
|
|
11
11
|
DEFAULT_AMOUNT_PRECISION = 2
|
|
12
12
|
DEFAULT_PERCENT_PRECISION = 4
|
|
13
13
|
|
|
14
|
-
attr_reader :amount, :original, :amount_precision, :percent_precision
|
|
14
|
+
attr_reader :amount, :original, :discount_source, :amount_precision, :percent_precision
|
|
15
15
|
|
|
16
|
-
def initialize(amount, original: nil, amount_precision: DEFAULT_AMOUNT_PRECISION, percent_precision: DEFAULT_PERCENT_PRECISION)
|
|
16
|
+
def initialize(amount, original: nil, discount_source: nil, amount_precision: DEFAULT_AMOUNT_PRECISION, percent_precision: DEFAULT_PERCENT_PRECISION)
|
|
17
17
|
@amount = amount.to_f
|
|
18
18
|
@original = original
|
|
19
|
+
@discount_source = discount_source
|
|
19
20
|
@amount_precision = amount_precision
|
|
20
21
|
@percent_precision = percent_precision
|
|
21
22
|
end
|
|
22
23
|
|
|
23
|
-
# Apply a discount
|
|
24
|
-
# - "25%" → 25% off
|
|
25
|
-
# -
|
|
26
|
-
# -
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
24
|
+
# Apply a discount from various sources:
|
|
25
|
+
# - String: "25%" → 25% off, "$20" → $20 off
|
|
26
|
+
# - Numeric: 20 → $20 off
|
|
27
|
+
# - Discount object: Discount::Percent.new(25) → 25% off
|
|
28
|
+
# - Any object responding to to_discount
|
|
29
|
+
# - nil: no discount, returns self
|
|
30
|
+
def discount(source)
|
|
31
|
+
return self if source.nil?
|
|
32
|
+
|
|
33
|
+
discount_obj = coerce_discount(source)
|
|
34
|
+
new_amount = [discount_obj.apply(@amount), 0].max.round(@amount_precision)
|
|
35
|
+
|
|
36
|
+
Price.new(new_amount,
|
|
37
|
+
original: self,
|
|
38
|
+
discount_source: source,
|
|
39
|
+
amount_precision: @amount_precision,
|
|
40
|
+
percent_precision: @percent_precision
|
|
41
|
+
)
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
# Apply a fixed dollar discount
|
|
43
45
|
def discount_fixed(amount)
|
|
44
|
-
|
|
45
|
-
Price.new(new_amount, original: self, amount_precision: @amount_precision, percent_precision: @percent_precision)
|
|
46
|
+
discount(Discount::Fixed.new(amount.to_f))
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
# Set the price to a specific amount (calculates discount from current amount)
|
|
@@ -53,9 +54,7 @@ module Superfeature
|
|
|
53
54
|
|
|
54
55
|
# Apply a percentage discount (decimal, e.g., 0.25 for 25%)
|
|
55
56
|
def discount_percent(percent)
|
|
56
|
-
|
|
57
|
-
new_amount = (@amount - discount_amount).round(@amount_precision)
|
|
58
|
-
Price.new(new_amount, original: self, amount_precision: @amount_precision, percent_precision: @percent_precision)
|
|
57
|
+
discount(Discount::Percent.new(percent.to_f * 100))
|
|
59
58
|
end
|
|
60
59
|
|
|
61
60
|
# Dollars saved from original price
|
|
@@ -99,5 +98,26 @@ module Superfeature
|
|
|
99
98
|
"#<Price #{to_formatted_s}>"
|
|
100
99
|
end
|
|
101
100
|
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def coerce_discount(source)
|
|
105
|
+
case source
|
|
106
|
+
when String then parse_discount_string(source)
|
|
107
|
+
when Numeric then Discount::Fixed.new(source)
|
|
108
|
+
else source.to_discount
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def parse_discount_string(str)
|
|
113
|
+
case str
|
|
114
|
+
when /\A(\d+(?:\.\d+)?)\s*%\z/
|
|
115
|
+
Discount::Percent.new($1.to_f)
|
|
116
|
+
when /\A\$?\s*(\d+(?:\.\d+)?)\z/
|
|
117
|
+
Discount::Fixed.new($1.to_f)
|
|
118
|
+
else
|
|
119
|
+
raise ArgumentError, "Invalid discount format: #{str.inspect}"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
102
122
|
end
|
|
103
123
|
end
|
data/lib/superfeature/version.rb
CHANGED
data/lib/superfeature.rb
CHANGED
|
@@ -4,7 +4,15 @@ require "superfeature/limit"
|
|
|
4
4
|
require "superfeature/feature"
|
|
5
5
|
require "superfeature/plan"
|
|
6
6
|
require "superfeature/plan/collection"
|
|
7
|
+
require "superfeature/discount"
|
|
7
8
|
require "superfeature/price"
|
|
8
9
|
|
|
9
10
|
module Superfeature
|
|
10
|
-
|
|
11
|
+
# Convenience methods for creating Discount objects.
|
|
12
|
+
# Use Superfeature::Fixed(20) or after `include Superfeature`, just Fixed(20)
|
|
13
|
+
def Fixed(...) = Discount::Fixed.new(...)
|
|
14
|
+
def Percent(...) = Discount::Percent.new(...)
|
|
15
|
+
def Bundle(...) = Discount::Bundle.new(...)
|
|
16
|
+
module_function :Fixed, :Percent, :Bundle
|
|
17
|
+
public :Fixed, :Percent, :Bundle
|
|
18
|
+
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: superfeature
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brad Gessler
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-01-
|
|
10
|
+
date: 2026-01-09 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|
|
@@ -53,6 +53,7 @@ files:
|
|
|
53
53
|
- lib/generators/superfeature/plan/plan_generator.rb
|
|
54
54
|
- lib/generators/superfeature/plan/templates/plan.rb.tt
|
|
55
55
|
- lib/superfeature.rb
|
|
56
|
+
- lib/superfeature/discount.rb
|
|
56
57
|
- lib/superfeature/engine.rb
|
|
57
58
|
- lib/superfeature/feature.rb
|
|
58
59
|
- lib/superfeature/limit.rb
|