pricing_plans 0.5.0 → 0.6.0
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/.rubocop.yml +4 -7
- data/.simplecov +6 -5
- data/Appraisals +5 -1
- data/CHANGELOG.md +19 -0
- data/README.md +48 -1
- data/docs/01-define-pricing-plans.md +6 -1
- data/docs/03-model-helpers.md +16 -0
- data/docs/06-gem-compatibility.md +35 -0
- data/docs/07-repricing.md +98 -0
- data/gemfiles/rails_7.1.gemfile +26 -0
- data/gemfiles/rails_7.2.gemfile +3 -2
- data/gemfiles/rails_8.1.gemfile +2 -1
- data/lib/generators/pricing_plans/grants/grants_generator.rb +43 -0
- data/lib/generators/pricing_plans/grants/templates/create_pricing_plans_feature_grants.rb.erb +32 -0
- data/lib/generators/pricing_plans/install/templates/create_pricing_plans_tables.rb.erb +22 -8
- data/lib/generators/pricing_plans/install/templates/initializer.rb +6 -0
- data/lib/pricing_plans/engine.rb +1 -0
- data/lib/pricing_plans/models/assignment.rb +2 -4
- data/lib/pricing_plans/models/feature_grant.rb +139 -0
- data/lib/pricing_plans/overage_reporter.rb +7 -1
- data/lib/pricing_plans/pay_support.rb +92 -100
- data/lib/pricing_plans/plan.rb +108 -7
- data/lib/pricing_plans/plan_owner.rb +73 -7
- data/lib/pricing_plans/plan_owner_identity.rb +30 -0
- data/lib/pricing_plans/plan_resolver.rb +53 -37
- data/lib/pricing_plans/version.rb +1 -1
- data/lib/pricing_plans.rb +3 -1
- metadata +10 -4
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PricingPlans
|
|
4
|
+
# Builds the identity Rails stores for polymorphic plan-owner associations.
|
|
5
|
+
#
|
|
6
|
+
# Rails stores an Active Record model's polymorphic_name (normally its STI
|
|
7
|
+
# base class), not necessarily `record.class.name`. Keeping this in one place
|
|
8
|
+
# prevents reads and writes from disagreeing for STI subclasses and respects
|
|
9
|
+
# the host application's `store_full_class_name` configuration.
|
|
10
|
+
module PlanOwnerIdentity
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def type_for(plan_owner_or_class)
|
|
14
|
+
plan_owner_class = plan_owner_or_class.is_a?(Class) ? plan_owner_or_class : plan_owner_or_class.class
|
|
15
|
+
|
|
16
|
+
if plan_owner_class.respond_to?(:polymorphic_name)
|
|
17
|
+
plan_owner_class.polymorphic_name
|
|
18
|
+
else
|
|
19
|
+
plan_owner_class.name
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def conditions_for(plan_owner)
|
|
24
|
+
{
|
|
25
|
+
plan_owner_type: type_for(plan_owner),
|
|
26
|
+
plan_owner_id: plan_owner.respond_to?(:id) ? plan_owner.id : nil
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -19,7 +19,7 @@ module PricingPlans
|
|
|
19
19
|
log_debug "[PricingPlans::PlanResolver] resolution_for called for #{plan_owner.class.name}##{plan_owner.respond_to?(:id) ? plan_owner.id : 'N/A'}"
|
|
20
20
|
|
|
21
21
|
assignment = pricing_plan_override_for(plan_owner)
|
|
22
|
-
subscription = current_subscription_for(plan_owner)
|
|
22
|
+
subscription = current_subscription_for(plan_owner, preferred_plan_key: assignment&.plan_key)
|
|
23
23
|
|
|
24
24
|
if assignment
|
|
25
25
|
log_debug "[PricingPlans::PlanResolver] Returning explicit-override resolution: #{assignment.plan_key}"
|
|
@@ -31,20 +31,8 @@ module PricingPlans
|
|
|
31
31
|
)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
log_debug "[PricingPlans::PlanResolver] resolution_for subscription processor_plan = #{processor_plan.inspect}"
|
|
37
|
-
|
|
38
|
-
if processor_plan && (plan = plan_from_processor_plan(processor_plan))
|
|
39
|
-
log_debug "[PricingPlans::PlanResolver] Returning subscription-backed resolution: #{plan.key}"
|
|
40
|
-
return PlanResolution.new(
|
|
41
|
-
plan: plan,
|
|
42
|
-
source: :subscription,
|
|
43
|
-
assignment: nil,
|
|
44
|
-
subscription: subscription
|
|
45
|
-
)
|
|
46
|
-
end
|
|
47
|
-
end
|
|
34
|
+
subscription_resolution = subscription_resolution_for(subscription)
|
|
35
|
+
return subscription_resolution if subscription_resolution
|
|
48
36
|
|
|
49
37
|
default = Registry.default_plan
|
|
50
38
|
log_debug "[PricingPlans::PlanResolver] Returning default-backed resolution: #{default ? default.key : 'nil'}"
|
|
@@ -84,6 +72,20 @@ module PricingPlans
|
|
|
84
72
|
)
|
|
85
73
|
end
|
|
86
74
|
|
|
75
|
+
# Resolve a processor price identifier to the configured plan that owns
|
|
76
|
+
# it. Public so entitlement provenance can verify that an underlying
|
|
77
|
+
# subscription belongs to the same plan as a manual override.
|
|
78
|
+
def plan_for_processor_plan(processor_plan)
|
|
79
|
+
return nil if processor_plan.blank?
|
|
80
|
+
|
|
81
|
+
Registry.plans.values.find do |plan|
|
|
82
|
+
stripe_price = plan.stripe_price
|
|
83
|
+
next unless stripe_price
|
|
84
|
+
|
|
85
|
+
stripe_price.is_a?(Hash) ? stripe_price.value?(processor_plan) : stripe_price == processor_plan
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
87
89
|
private
|
|
88
90
|
|
|
89
91
|
# Backward-compatible shim for tests that stub pay_available?
|
|
@@ -96,8 +98,7 @@ module PricingPlans
|
|
|
96
98
|
return nil unless plan_owner.respond_to?(:id)
|
|
97
99
|
|
|
98
100
|
assignment = Assignment.find_by(
|
|
99
|
-
|
|
100
|
-
plan_owner_id: plan_owner.id
|
|
101
|
+
PlanOwnerIdentity.conditions_for(plan_owner)
|
|
101
102
|
)
|
|
102
103
|
|
|
103
104
|
if assignment
|
|
@@ -109,7 +110,24 @@ module PricingPlans
|
|
|
109
110
|
assignment
|
|
110
111
|
end
|
|
111
112
|
|
|
112
|
-
def
|
|
113
|
+
def subscription_resolution_for(subscription)
|
|
114
|
+
return nil unless subscription.respond_to?(:processor_plan)
|
|
115
|
+
|
|
116
|
+
processor_plan = subscription.processor_plan
|
|
117
|
+
log_debug "[PricingPlans::PlanResolver] resolution_for subscription processor_plan = #{processor_plan.inspect}"
|
|
118
|
+
plan = plan_for_processor_plan(processor_plan)
|
|
119
|
+
return nil unless plan
|
|
120
|
+
|
|
121
|
+
log_debug "[PricingPlans::PlanResolver] Returning subscription-backed resolution: #{plan.key}"
|
|
122
|
+
PlanResolution.new(
|
|
123
|
+
plan: plan,
|
|
124
|
+
source: :subscription,
|
|
125
|
+
assignment: nil,
|
|
126
|
+
subscription: subscription
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def current_subscription_for(plan_owner, preferred_plan_key: nil)
|
|
113
131
|
return nil unless plan_owner
|
|
114
132
|
|
|
115
133
|
pay_available = pay_available?
|
|
@@ -117,32 +135,30 @@ module PricingPlans
|
|
|
117
135
|
|
|
118
136
|
return nil unless pay_available
|
|
119
137
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
# the same billing context wherever it is called.
|
|
123
|
-
subscription = PaySupport.current_subscription_for(plan_owner)
|
|
138
|
+
subscriptions = PaySupport.current_subscriptions_for(plan_owner)
|
|
139
|
+
subscription = select_subscription(subscriptions, preferred_plan_key: preferred_plan_key)
|
|
124
140
|
log_debug "[PricingPlans::PlanResolver] current_subscription_for returned: #{subscription ? subscription.class.name : 'nil'}"
|
|
125
141
|
subscription
|
|
126
142
|
end
|
|
127
143
|
|
|
128
|
-
def
|
|
129
|
-
|
|
130
|
-
Registry.plans.values.find do |plan|
|
|
131
|
-
stripe_price = plan.stripe_price
|
|
132
|
-
next unless stripe_price
|
|
144
|
+
def select_subscription(subscriptions, preferred_plan_key: nil)
|
|
145
|
+
subscriptions = Array(subscriptions)
|
|
133
146
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
stripe_price[:id] == processor_plan ||
|
|
139
|
-
stripe_price[:month] == processor_plan ||
|
|
140
|
-
stripe_price[:year] == processor_plan ||
|
|
141
|
-
stripe_price.values.include?(processor_plan)
|
|
142
|
-
else
|
|
143
|
-
false
|
|
147
|
+
if preferred_plan_key
|
|
148
|
+
preferred = subscriptions.find do |subscription|
|
|
149
|
+
subscription_plan = plan_for_subscription(subscription)
|
|
150
|
+
subscription_plan&.key == preferred_plan_key.to_sym
|
|
144
151
|
end
|
|
152
|
+
return preferred if preferred
|
|
145
153
|
end
|
|
154
|
+
|
|
155
|
+
subscriptions.find { |subscription| plan_for_subscription(subscription) } || subscriptions.first
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def plan_for_subscription(subscription)
|
|
159
|
+
return nil unless subscription.respond_to?(:processor_plan)
|
|
160
|
+
|
|
161
|
+
plan_for_processor_plan(subscription.processor_plan)
|
|
146
162
|
end
|
|
147
163
|
end
|
|
148
164
|
end
|
data/lib/pricing_plans.rb
CHANGED
|
@@ -37,6 +37,7 @@ module PricingPlans
|
|
|
37
37
|
autoload :DSL, "pricing_plans/dsl"
|
|
38
38
|
autoload :IntegerRefinements, "pricing_plans/integer_refinements"
|
|
39
39
|
autoload :PlanResolver, "pricing_plans/plan_resolver"
|
|
40
|
+
autoload :PlanOwnerIdentity, "pricing_plans/plan_owner_identity"
|
|
40
41
|
autoload :LegacyPlanAssignmentApi, "pricing_plans/legacy_plan_assignment_api"
|
|
41
42
|
autoload :PaySupport, "pricing_plans/pay_support"
|
|
42
43
|
autoload :LimitChecker, "pricing_plans/limit_checker"
|
|
@@ -61,6 +62,7 @@ module PricingPlans
|
|
|
61
62
|
autoload :EnforcementState, "pricing_plans/models/enforcement_state"
|
|
62
63
|
autoload :Usage, "pricing_plans/models/usage"
|
|
63
64
|
autoload :Assignment, "pricing_plans/models/assignment"
|
|
65
|
+
autoload :FeatureGrant, "pricing_plans/models/feature_grant"
|
|
64
66
|
|
|
65
67
|
class << self
|
|
66
68
|
attr_writer :configuration
|
|
@@ -73,7 +75,7 @@ module PricingPlans
|
|
|
73
75
|
# warnings independently and lets Rails include them in its deprecator
|
|
74
76
|
# collection.
|
|
75
77
|
def deprecator
|
|
76
|
-
@deprecator ||= ActiveSupport::Deprecation.new("0.
|
|
78
|
+
@deprecator ||= ActiveSupport::Deprecation.new("0.7.0", "pricing_plans")
|
|
77
79
|
end
|
|
78
80
|
|
|
79
81
|
def configure(&block)
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pricing_plans
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-09-01 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activerecord
|
|
@@ -77,12 +77,16 @@ files:
|
|
|
77
77
|
- docs/04-views.md
|
|
78
78
|
- docs/05-semantic-pricing.md
|
|
79
79
|
- docs/06-gem-compatibility.md
|
|
80
|
+
- docs/07-repricing.md
|
|
80
81
|
- docs/images/pricing_plans_ruby_rails_gem_pricing_table.jpg
|
|
81
82
|
- docs/images/pricing_plans_ruby_rails_gem_usage_alert_upgrade.jpg
|
|
82
83
|
- docs/images/pricing_plans_ruby_rails_gem_usage_meter.jpg
|
|
83
84
|
- docs/images/product_creation_blocked.jpg
|
|
85
|
+
- gemfiles/rails_7.1.gemfile
|
|
84
86
|
- gemfiles/rails_7.2.gemfile
|
|
85
87
|
- gemfiles/rails_8.1.gemfile
|
|
88
|
+
- lib/generators/pricing_plans/grants/grants_generator.rb
|
|
89
|
+
- lib/generators/pricing_plans/grants/templates/create_pricing_plans_feature_grants.rb.erb
|
|
86
90
|
- lib/generators/pricing_plans/install/install_generator.rb
|
|
87
91
|
- lib/generators/pricing_plans/install/templates/create_pricing_plans_tables.rb.erb
|
|
88
92
|
- lib/generators/pricing_plans/install/templates/initializer.rb
|
|
@@ -103,12 +107,14 @@ files:
|
|
|
103
107
|
- lib/pricing_plans/limitable.rb
|
|
104
108
|
- lib/pricing_plans/models/assignment.rb
|
|
105
109
|
- lib/pricing_plans/models/enforcement_state.rb
|
|
110
|
+
- lib/pricing_plans/models/feature_grant.rb
|
|
106
111
|
- lib/pricing_plans/models/usage.rb
|
|
107
112
|
- lib/pricing_plans/overage_reporter.rb
|
|
108
113
|
- lib/pricing_plans/pay_support.rb
|
|
109
114
|
- lib/pricing_plans/period_calculator.rb
|
|
110
115
|
- lib/pricing_plans/plan.rb
|
|
111
116
|
- lib/pricing_plans/plan_owner.rb
|
|
117
|
+
- lib/pricing_plans/plan_owner_identity.rb
|
|
112
118
|
- lib/pricing_plans/plan_resolution.rb
|
|
113
119
|
- lib/pricing_plans/plan_resolver.rb
|
|
114
120
|
- lib/pricing_plans/price_components.rb
|
|
@@ -123,7 +129,7 @@ licenses:
|
|
|
123
129
|
- MIT
|
|
124
130
|
metadata:
|
|
125
131
|
homepage_uri: https://github.com/rameerez/pricing_plans
|
|
126
|
-
source_code_uri: https://github.com/rameerez/pricing_plans
|
|
132
|
+
source_code_uri: https://github.com/rameerez/pricing_plans/tree/v0.6.0
|
|
127
133
|
changelog_uri: https://github.com/rameerez/pricing_plans/blob/main/CHANGELOG.md
|
|
128
134
|
bug_tracker_uri: https://github.com/rameerez/pricing_plans/issues
|
|
129
135
|
documentation_uri: https://github.com/rameerez/pricing_plans#readme
|
|
@@ -142,7 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
142
148
|
- !ruby/object:Gem::Version
|
|
143
149
|
version: '0'
|
|
144
150
|
requirements: []
|
|
145
|
-
rubygems_version: 3.6.
|
|
151
|
+
rubygems_version: 3.6.2
|
|
146
152
|
specification_version: 4
|
|
147
153
|
summary: Define and enforce pricing plan limits (entitlements, quotas, feature gating)
|
|
148
154
|
in your Rails SaaS
|