pricing_plans 0.4.1 → 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.
@@ -18,11 +18,11 @@ module PricingPlans
18
18
  def resolution_for(plan_owner)
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
- assignment = assignment_for(plan_owner)
22
- subscription = current_subscription_for(plan_owner)
21
+ assignment = pricing_plan_override_for(plan_owner)
22
+ subscription = current_subscription_for(plan_owner, preferred_plan_key: assignment&.plan_key)
23
23
 
24
24
  if assignment
25
- log_debug "[PricingPlans::PlanResolver] Returning assignment-backed resolution: #{assignment.plan_key}"
25
+ log_debug "[PricingPlans::PlanResolver] Returning explicit-override resolution: #{assignment.plan_key}"
26
26
  return PlanResolution.new(
27
27
  plan: Registry.plan(assignment.plan_key),
28
28
  source: :assignment,
@@ -31,20 +31,8 @@ module PricingPlans
31
31
  )
32
32
  end
33
33
 
34
- if subscription
35
- processor_plan = subscription.processor_plan
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'}"
@@ -56,12 +44,46 @@ module PricingPlans
56
44
  )
57
45
  end
58
46
 
47
+ def override_pricing_plan_for!(plan_owner, plan_key, source:)
48
+ Assignment.create_or_update_pricing_plan_override_for!(
49
+ plan_owner,
50
+ plan_key,
51
+ source: source
52
+ )
53
+ end
54
+
55
+ def clear_pricing_plan_override_for!(plan_owner)
56
+ Assignment.clear_pricing_plan_override_for!(plan_owner)
57
+ end
58
+
59
59
  def assign_plan_manually!(plan_owner, plan_key, source: "manual")
60
- Assignment.assign_plan_to(plan_owner, plan_key, source: source)
60
+ LegacyPlanAssignmentApi.create_or_update_override!(
61
+ plan_owner,
62
+ plan_key,
63
+ source: source,
64
+ called_method_name: "PricingPlans::PlanResolver.assign_plan_manually!"
65
+ )
61
66
  end
62
67
 
63
68
  def remove_manual_assignment!(plan_owner)
64
- Assignment.remove_assignment_for(plan_owner)
69
+ LegacyPlanAssignmentApi.clear_override!(
70
+ plan_owner,
71
+ called_method_name: "PricingPlans::PlanResolver.remove_manual_assignment!"
72
+ )
73
+ end
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
65
87
  end
66
88
 
67
89
  private
@@ -71,25 +93,41 @@ module PricingPlans
71
93
  PaySupport.pay_available?
72
94
  end
73
95
 
74
- def assignment_for(plan_owner)
75
- log_debug "[PricingPlans::PlanResolver] Checking for manual assignment..."
96
+ def pricing_plan_override_for(plan_owner)
97
+ log_debug "[PricingPlans::PlanResolver] Checking for an explicit pricing plan override..."
76
98
  return nil unless plan_owner.respond_to?(:id)
77
99
 
78
100
  assignment = Assignment.find_by(
79
- plan_owner_type: plan_owner.class.name,
80
- plan_owner_id: plan_owner.id
101
+ PlanOwnerIdentity.conditions_for(plan_owner)
81
102
  )
82
103
 
83
104
  if assignment
84
- log_debug "[PricingPlans::PlanResolver] Found manual assignment: #{assignment.plan_key}"
105
+ log_debug "[PricingPlans::PlanResolver] Found explicit pricing plan override: #{assignment.plan_key}"
85
106
  else
86
- log_debug "[PricingPlans::PlanResolver] No manual assignment found"
107
+ log_debug "[PricingPlans::PlanResolver] No explicit pricing plan override found"
87
108
  end
88
109
 
89
110
  assignment
90
111
  end
91
112
 
92
- def current_subscription_for(plan_owner)
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)
93
131
  return nil unless plan_owner
94
132
 
95
133
  pay_available = pay_available?
@@ -97,32 +135,30 @@ module PricingPlans
97
135
 
98
136
  return nil unless pay_available
99
137
 
100
- # This intentionally delegates the active/trial/grace filtering contract
101
- # to PaySupport.current_subscription_for so resolution_for can preserve
102
- # the same billing context wherever it is called.
103
- 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)
104
140
  log_debug "[PricingPlans::PlanResolver] current_subscription_for returned: #{subscription ? subscription.class.name : 'nil'}"
105
141
  subscription
106
142
  end
107
143
 
108
- def plan_from_processor_plan(processor_plan)
109
- # Look through all plans to find one matching this Stripe price
110
- Registry.plans.values.find do |plan|
111
- stripe_price = plan.stripe_price
112
- next unless stripe_price
144
+ def select_subscription(subscriptions, preferred_plan_key: nil)
145
+ subscriptions = Array(subscriptions)
113
146
 
114
- case stripe_price
115
- when String
116
- stripe_price == processor_plan
117
- when Hash
118
- stripe_price[:id] == processor_plan ||
119
- stripe_price[:month] == processor_plan ||
120
- stripe_price[:year] == processor_plan ||
121
- stripe_price.values.include?(processor_plan)
122
- else
123
- 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
124
151
  end
152
+ return preferred if preferred
125
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)
126
162
  end
127
163
  end
128
164
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PricingPlans
4
- VERSION = "0.4.1"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/pricing_plans.rb CHANGED
@@ -1,12 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "pricing_plans/version"
4
+ require "active_support/deprecation"
4
5
  require_relative "pricing_plans/engine" if defined?(Rails::Engine)
5
6
 
6
7
  module PricingPlans
7
8
  class Error < StandardError; end
8
9
  class ConfigurationError < Error; end
9
10
  class PlanNotFoundError < Error; end
11
+
12
+ class LegacyDefaultPlanAssignmentError < Error
13
+ attr_reader :configured_default_plan_key, :legacy_assignment_method_name
14
+
15
+ def initialize(message, configured_default_plan_key:, legacy_assignment_method_name:)
16
+ super(message)
17
+ @configured_default_plan_key = configured_default_plan_key.to_sym
18
+ @legacy_assignment_method_name = legacy_assignment_method_name
19
+ end
20
+ end
21
+
10
22
  class FeatureDenied < Error
11
23
  attr_reader :feature_key, :plan_owner
12
24
 
@@ -25,6 +37,8 @@ module PricingPlans
25
37
  autoload :DSL, "pricing_plans/dsl"
26
38
  autoload :IntegerRefinements, "pricing_plans/integer_refinements"
27
39
  autoload :PlanResolver, "pricing_plans/plan_resolver"
40
+ autoload :PlanOwnerIdentity, "pricing_plans/plan_owner_identity"
41
+ autoload :LegacyPlanAssignmentApi, "pricing_plans/legacy_plan_assignment_api"
28
42
  autoload :PaySupport, "pricing_plans/pay_support"
29
43
  autoload :LimitChecker, "pricing_plans/limit_checker"
30
44
  autoload :LimitableRegistry, "pricing_plans/limit_checker"
@@ -48,6 +62,7 @@ module PricingPlans
48
62
  autoload :EnforcementState, "pricing_plans/models/enforcement_state"
49
63
  autoload :Usage, "pricing_plans/models/usage"
50
64
  autoload :Assignment, "pricing_plans/models/assignment"
65
+ autoload :FeatureGrant, "pricing_plans/models/feature_grant"
51
66
 
52
67
  class << self
53
68
  attr_writer :configuration
@@ -56,6 +71,13 @@ module PricingPlans
56
71
  @configuration ||= Configuration.new
57
72
  end
58
73
 
74
+ # A gem-specific deprecator lets host applications configure pricing_plans
75
+ # warnings independently and lets Rails include them in its deprecator
76
+ # collection.
77
+ def deprecator
78
+ @deprecator ||= ActiveSupport::Deprecation.new("0.7.0", "pricing_plans")
79
+ end
80
+
59
81
  def configure(&block)
60
82
  # Support both styles simultaneously inside the block:
61
83
  # - Bare DSL: plan :free { ... }
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.1
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: 2026-08-24 00:00:00.000000000 Z
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
@@ -98,16 +102,19 @@ files:
98
102
  - lib/pricing_plans/grace_manager.rb
99
103
  - lib/pricing_plans/integer_refinements.rb
100
104
  - lib/pricing_plans/job_guards.rb
105
+ - lib/pricing_plans/legacy_plan_assignment_api.rb
101
106
  - lib/pricing_plans/limit_checker.rb
102
107
  - lib/pricing_plans/limitable.rb
103
108
  - lib/pricing_plans/models/assignment.rb
104
109
  - lib/pricing_plans/models/enforcement_state.rb
110
+ - lib/pricing_plans/models/feature_grant.rb
105
111
  - lib/pricing_plans/models/usage.rb
106
112
  - lib/pricing_plans/overage_reporter.rb
107
113
  - lib/pricing_plans/pay_support.rb
108
114
  - lib/pricing_plans/period_calculator.rb
109
115
  - lib/pricing_plans/plan.rb
110
116
  - lib/pricing_plans/plan_owner.rb
117
+ - lib/pricing_plans/plan_owner_identity.rb
111
118
  - lib/pricing_plans/plan_resolution.rb
112
119
  - lib/pricing_plans/plan_resolver.rb
113
120
  - lib/pricing_plans/price_components.rb
@@ -122,7 +129,7 @@ licenses:
122
129
  - MIT
123
130
  metadata:
124
131
  homepage_uri: https://github.com/rameerez/pricing_plans
125
- source_code_uri: https://github.com/rameerez/pricing_plans
132
+ source_code_uri: https://github.com/rameerez/pricing_plans/tree/v0.6.0
126
133
  changelog_uri: https://github.com/rameerez/pricing_plans/blob/main/CHANGELOG.md
127
134
  bug_tracker_uri: https://github.com/rameerez/pricing_plans/issues
128
135
  documentation_uri: https://github.com/rameerez/pricing_plans#readme