superfeature 0.1.5 → 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 +50 -0
- data/lib/superfeature/discount.rb +44 -0
- 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
|
@@ -370,6 +370,56 @@ price = Superfeature::Price.new(99.999, amount_precision: 3, percent_precision:
|
|
|
370
370
|
price.to_formatted_s # => "99.999"
|
|
371
371
|
```
|
|
372
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
|
+
|
|
373
423
|
## Comparable libraries
|
|
374
424
|
|
|
375
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/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
|