pricing_plans 0.2.0 → 0.3.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/.simplecov +35 -0
- data/AGENTS.md +5 -0
- data/Appraisals +9 -0
- data/CHANGELOG.md +10 -5
- data/CLAUDE.md +5 -1
- data/README.md +16 -3
- data/docs/01-define-pricing-plans.md +120 -16
- data/docs/03-model-helpers.md +59 -0
- data/docs/04-views.md +2 -1
- data/docs/06-gem-compatibility.md +31 -4
- data/gemfiles/rails_7.2.gemfile +25 -0
- data/gemfiles/rails_8.1.gemfile +25 -0
- data/lib/generators/pricing_plans/install/templates/initializer.rb +38 -4
- data/lib/pricing_plans/callbacks.rb +152 -0
- data/lib/pricing_plans/configuration.rb +23 -11
- data/lib/pricing_plans/limitable.rb +38 -1
- data/lib/pricing_plans/models/enforcement_state.rb +1 -1
- data/lib/pricing_plans/period_calculator.rb +6 -0
- data/lib/pricing_plans/plan.rb +5 -1
- data/lib/pricing_plans/plan_owner.rb +70 -0
- data/lib/pricing_plans/plan_resolver.rb +13 -13
- data/lib/pricing_plans/registry.rb +2 -2
- data/lib/pricing_plans/status_context.rb +343 -0
- data/lib/pricing_plans/version.rb +1 -1
- data/lib/pricing_plans.rb +52 -10
- metadata +9 -101
- data/.claude/settings.local.json +0 -20
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PricingPlans
|
|
4
|
+
# Centralized callback dispatch module with error isolation.
|
|
5
|
+
# Callbacks should never break the main operation - errors are logged but not raised.
|
|
6
|
+
module Callbacks
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Dispatch a callback event with error isolation.
|
|
10
|
+
# Fires specific handler first (if exists), then wildcard handler (if exists).
|
|
11
|
+
# Callbacks should never break the main operation.
|
|
12
|
+
#
|
|
13
|
+
# @param event_type [Symbol] The event type (:warning, :grace_start, :block)
|
|
14
|
+
# @param limit_key [Symbol] The limit key (e.g., :projects, :licenses)
|
|
15
|
+
# @param args [Array] Arguments to pass to the callback (plan_owner, plus event-specific args)
|
|
16
|
+
def dispatch(event_type, limit_key, *args)
|
|
17
|
+
handlers = Registry.event_handlers[event_type] || {}
|
|
18
|
+
|
|
19
|
+
# Build full args with limit_key injected after plan_owner
|
|
20
|
+
# Input args: [plan_owner, ...event_specific_args]
|
|
21
|
+
# Output: [plan_owner, limit_key, ...event_specific_args]
|
|
22
|
+
plan_owner = args.first
|
|
23
|
+
event_args = args.drop(1)
|
|
24
|
+
full_args = [plan_owner, limit_key, *event_args]
|
|
25
|
+
|
|
26
|
+
# Fire specific handler first
|
|
27
|
+
specific_handler = handlers[limit_key]
|
|
28
|
+
execute_safely(specific_handler, event_type, limit_key, full_args) if specific_handler.is_a?(Proc)
|
|
29
|
+
|
|
30
|
+
# Fire wildcard handler second
|
|
31
|
+
wildcard_handler = handlers[:_all]
|
|
32
|
+
execute_safely(wildcard_handler, event_type, limit_key, full_args) if wildcard_handler.is_a?(Proc)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Execute callback with error isolation and arity handling.
|
|
36
|
+
# Supports callbacks with varying argument counts for backwards compatibility.
|
|
37
|
+
#
|
|
38
|
+
# Backward compatibility:
|
|
39
|
+
# - Arity 2 (old style): receives (plan_owner, last_arg) - skips limit_key
|
|
40
|
+
# - Arity 3+ (new style): receives (plan_owner, limit_key, ...rest)
|
|
41
|
+
#
|
|
42
|
+
# @param handler [Proc] The callback to execute
|
|
43
|
+
# @param event_type [Symbol] For logging purposes
|
|
44
|
+
# @param limit_key [Symbol] For logging purposes
|
|
45
|
+
# @param args [Array] Full arguments array [plan_owner, limit_key, ...event_specific_args]
|
|
46
|
+
def execute_safely(handler, event_type, limit_key, args)
|
|
47
|
+
case handler.arity
|
|
48
|
+
when 0
|
|
49
|
+
handler.call
|
|
50
|
+
when 1
|
|
51
|
+
handler.call(args[0])
|
|
52
|
+
when 2
|
|
53
|
+
# Backward compatibility: old callbacks expect (plan_owner, event_arg)
|
|
54
|
+
# where event_arg is threshold for warnings, grace_ends_at for grace_start.
|
|
55
|
+
# Skip limit_key (args[1]) and pass plan_owner + last arg.
|
|
56
|
+
# For on_block (args = [plan_owner, limit_key]), this passes (plan_owner, limit_key).
|
|
57
|
+
handler.call(args[0], args.last)
|
|
58
|
+
when 3
|
|
59
|
+
handler.call(args[0], args[1], args[2])
|
|
60
|
+
when -1, -2, -3 # Variable arity (splat args)
|
|
61
|
+
handler.call(*args)
|
|
62
|
+
else
|
|
63
|
+
handler.call(*args.first(handler.arity.abs))
|
|
64
|
+
end
|
|
65
|
+
rescue StandardError => e
|
|
66
|
+
# Log but don't re-raise - callbacks should never break model creation
|
|
67
|
+
log_error("[PricingPlans] Callback error for #{event_type}:#{limit_key}: #{e.class}: #{e.message}")
|
|
68
|
+
log_debug(e.backtrace&.join("\n"))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Check warning thresholds and emit warning event if a new threshold is crossed.
|
|
72
|
+
# This is the main entry point for automatic warning detection.
|
|
73
|
+
#
|
|
74
|
+
# @param plan_owner [Object] The plan owner (e.g., Organization)
|
|
75
|
+
# @param limit_key [Symbol] The limit key
|
|
76
|
+
# @param current_usage [Integer] Current usage count (after the action)
|
|
77
|
+
# @param limit_amount [Integer] The configured limit
|
|
78
|
+
def check_and_emit_warnings!(plan_owner, limit_key, current_usage, limit_amount)
|
|
79
|
+
return if limit_amount == :unlimited || limit_amount.to_i.zero?
|
|
80
|
+
|
|
81
|
+
percent_used = (current_usage.to_f / limit_amount) * 100
|
|
82
|
+
thresholds = LimitChecker.warning_thresholds(plan_owner, limit_key)
|
|
83
|
+
|
|
84
|
+
# Find the highest threshold that has been crossed
|
|
85
|
+
crossed_threshold = thresholds.select { |t| percent_used >= (t * 100) }.max
|
|
86
|
+
return unless crossed_threshold
|
|
87
|
+
|
|
88
|
+
# Emit warning if this is a new higher threshold
|
|
89
|
+
GraceManager.maybe_emit_warning!(plan_owner, limit_key, crossed_threshold)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Check if limit is exceeded and handle grace state (for grace_then_block policy).
|
|
93
|
+
# This is called after a successful model creation, so it only handles:
|
|
94
|
+
# - :just_warn - emit additional warning
|
|
95
|
+
# - :grace_then_block - start grace period if exceeded and not already in grace
|
|
96
|
+
#
|
|
97
|
+
# NOTE: Block events are NOT emitted here because this runs after successful creation.
|
|
98
|
+
# Block events are emitted in Limitable validation when creation is actually blocked.
|
|
99
|
+
#
|
|
100
|
+
# @param plan_owner [Object] The plan owner
|
|
101
|
+
# @param limit_key [Symbol] The limit key
|
|
102
|
+
# @param current_usage [Integer] Current usage count
|
|
103
|
+
# @param limit_config [Hash] The limit configuration from the plan
|
|
104
|
+
def check_and_emit_limit_exceeded!(plan_owner, limit_key, current_usage, limit_config)
|
|
105
|
+
return unless limit_config
|
|
106
|
+
return if limit_config[:to] == :unlimited
|
|
107
|
+
|
|
108
|
+
limit_amount = limit_config[:to].to_i
|
|
109
|
+
return unless current_usage >= limit_amount
|
|
110
|
+
|
|
111
|
+
case limit_config[:after_limit]
|
|
112
|
+
when :just_warn
|
|
113
|
+
# Just emit warning, don't track grace/block
|
|
114
|
+
check_and_emit_warnings!(plan_owner, limit_key, current_usage, limit_amount)
|
|
115
|
+
when :block_usage
|
|
116
|
+
# Do NOT mark as blocked here - this callback runs after SUCCESSFUL creation.
|
|
117
|
+
# Block events are emitted from validation when creation is actually blocked.
|
|
118
|
+
nil
|
|
119
|
+
when :grace_then_block
|
|
120
|
+
# Start grace period if not already in grace/blocked
|
|
121
|
+
unless GraceManager.grace_active?(plan_owner, limit_key) || GraceManager.should_block?(plan_owner, limit_key)
|
|
122
|
+
GraceManager.mark_exceeded!(plan_owner, limit_key, grace_period: limit_config[:grace])
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Safe logging that works with or without Rails
|
|
128
|
+
def log_error(message)
|
|
129
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
130
|
+
Rails.logger.error(message)
|
|
131
|
+
elsif PricingPlans.configuration&.debug
|
|
132
|
+
warn message
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def log_warn(message)
|
|
137
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
138
|
+
Rails.logger.warn(message)
|
|
139
|
+
elsif PricingPlans.configuration&.debug
|
|
140
|
+
warn message
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def log_debug(message)
|
|
145
|
+
return unless message
|
|
146
|
+
|
|
147
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger&.debug?
|
|
148
|
+
Rails.logger.debug(message)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -12,11 +12,11 @@ module PricingPlans
|
|
|
12
12
|
# Debug mode - set to true to enable debug output
|
|
13
13
|
attr_accessor :debug
|
|
14
14
|
# Global controller ergonomics
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
# Optional global resolver for controller plan owner. Per-controller settings still win.
|
|
16
|
+
# Accepts:
|
|
17
|
+
# - Symbol: a controller helper to call (e.g., :current_organization)
|
|
18
|
+
# - Proc: instance-exec'd in the controller (self is the controller)
|
|
19
|
+
attr_reader :controller_plan_owner_method, :controller_plan_owner_proc
|
|
20
20
|
# When a limit check blocks, controllers can redirect to a global default target.
|
|
21
21
|
# Accepts:
|
|
22
22
|
# - Symbol: a controller helper to call (e.g., :pricing_path)
|
|
@@ -119,19 +119,31 @@ module PricingPlans
|
|
|
119
119
|
end
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
# Register a callback for warning events.
|
|
123
|
+
# @param limit_key [Symbol, nil] The specific limit key, or omit for wildcard (all limits)
|
|
124
|
+
# @yield [plan_owner, limit_key, threshold] Block to execute when warning fires
|
|
125
|
+
def on_warning(limit_key = nil, &block)
|
|
123
126
|
raise PricingPlans::ConfigurationError, "Block required for on_warning" unless block_given?
|
|
124
|
-
|
|
127
|
+
key = limit_key || :_all
|
|
128
|
+
@event_handlers[:warning][key] = block
|
|
125
129
|
end
|
|
126
130
|
|
|
127
|
-
|
|
131
|
+
# Register a callback for grace period start events.
|
|
132
|
+
# @param limit_key [Symbol, nil] The specific limit key, or omit for wildcard (all limits)
|
|
133
|
+
# @yield [plan_owner, limit_key, grace_ends_at] Block to execute when grace starts
|
|
134
|
+
def on_grace_start(limit_key = nil, &block)
|
|
128
135
|
raise PricingPlans::ConfigurationError, "Block required for on_grace_start" unless block_given?
|
|
129
|
-
|
|
136
|
+
key = limit_key || :_all
|
|
137
|
+
@event_handlers[:grace_start][key] = block
|
|
130
138
|
end
|
|
131
139
|
|
|
132
|
-
|
|
140
|
+
# Register a callback for block events.
|
|
141
|
+
# @param limit_key [Symbol, nil] The specific limit key, or omit for wildcard (all limits)
|
|
142
|
+
# @yield [plan_owner, limit_key] Block to execute when user is blocked
|
|
143
|
+
def on_block(limit_key = nil, &block)
|
|
133
144
|
raise PricingPlans::ConfigurationError, "Block required for on_block" unless block_given?
|
|
134
|
-
|
|
145
|
+
key = limit_key || :_all
|
|
146
|
+
@event_handlers[:block][key] = block
|
|
135
147
|
end
|
|
136
148
|
|
|
137
149
|
def validate!
|
|
@@ -8,8 +8,9 @@ module PricingPlans
|
|
|
8
8
|
# Track all limited_by configurations for this model
|
|
9
9
|
class_attribute :pricing_plans_limits, default: {}
|
|
10
10
|
|
|
11
|
-
# Callbacks for automatic tracking
|
|
11
|
+
# Callbacks for automatic tracking and event emission
|
|
12
12
|
after_create :increment_per_period_counters
|
|
13
|
+
after_commit :check_and_emit_limit_events, on: :create
|
|
13
14
|
after_destroy :decrement_persistent_counters
|
|
14
15
|
# Add plan_owner-centric convenience methods to instances of the plan_owner class
|
|
15
16
|
# when possible. These are no-ops if the model isn't the plan_owner itself.
|
|
@@ -213,6 +214,8 @@ module PricingPlans
|
|
|
213
214
|
return
|
|
214
215
|
when :block_usage, :grace_then_block
|
|
215
216
|
if limit_config[:after_limit] == :block_usage || GraceManager.should_block?(plan_owner_instance, limit_key)
|
|
217
|
+
# Emit block event before failing validation
|
|
218
|
+
GraceManager.mark_blocked!(plan_owner_instance, limit_key)
|
|
216
219
|
message = error_after_limit || "Cannot create #{self.class.name.downcase}: #{limit_key} limit exceeded"
|
|
217
220
|
errors.add(:base, message)
|
|
218
221
|
end
|
|
@@ -227,6 +230,8 @@ module PricingPlans
|
|
|
227
230
|
return
|
|
228
231
|
when :block_usage, :grace_then_block
|
|
229
232
|
if limit_config[:after_limit] == :block_usage || GraceManager.should_block?(plan_owner_instance, limit_key)
|
|
233
|
+
# Emit block event before failing validation
|
|
234
|
+
GraceManager.mark_blocked!(plan_owner_instance, limit_key)
|
|
230
235
|
message = error_after_limit || "Cannot create #{self.class.name.downcase}: #{limit_key} limit exceeded for this period"
|
|
231
236
|
errors.add(:base, message)
|
|
232
237
|
end
|
|
@@ -289,5 +294,37 @@ module PricingPlans
|
|
|
289
294
|
# since the counter is computed live from the database
|
|
290
295
|
# The record being destroyed will automatically reduce the count
|
|
291
296
|
end
|
|
297
|
+
|
|
298
|
+
# Automatically check warning thresholds and emit events after model creation.
|
|
299
|
+
# This ensures callbacks fire without requiring explicit controller guard calls.
|
|
300
|
+
def check_and_emit_limit_events
|
|
301
|
+
self.class.pricing_plans_limits.each do |limit_key, config|
|
|
302
|
+
plan_owner_instance = if config[:plan_owner_method] == :self
|
|
303
|
+
self
|
|
304
|
+
else
|
|
305
|
+
send(config[:plan_owner_method])
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
unless plan_owner_instance
|
|
309
|
+
Callbacks.log_debug("[PricingPlans] Skipping callback for #{self.class.name}##{id}: plan_owner is nil (#{config[:plan_owner_method]})")
|
|
310
|
+
next
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
plan = PlanResolver.effective_plan_for(plan_owner_instance)
|
|
314
|
+
limit_config = plan&.limit_for(limit_key)
|
|
315
|
+
|
|
316
|
+
next unless limit_config
|
|
317
|
+
next if limit_config[:to] == :unlimited
|
|
318
|
+
|
|
319
|
+
limit_amount = limit_config[:to].to_i
|
|
320
|
+
current_usage = LimitChecker.current_usage_for(plan_owner_instance, limit_key, limit_config)
|
|
321
|
+
|
|
322
|
+
# Check and emit warning events for thresholds crossed
|
|
323
|
+
Callbacks.check_and_emit_warnings!(plan_owner_instance, limit_key, current_usage, limit_amount)
|
|
324
|
+
|
|
325
|
+
# Check and emit grace/block events if limit is exceeded
|
|
326
|
+
Callbacks.check_and_emit_limit_exceeded!(plan_owner_instance, limit_key, current_usage, limit_config)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
292
329
|
end
|
|
293
330
|
end
|
|
@@ -7,7 +7,7 @@ module PricingPlans
|
|
|
7
7
|
belongs_to :plan_owner, polymorphic: true
|
|
8
8
|
|
|
9
9
|
validates :limit_key, presence: true
|
|
10
|
-
validates :
|
|
10
|
+
validates :limit_key, uniqueness: { scope: [:plan_owner_type, :plan_owner_id] }
|
|
11
11
|
|
|
12
12
|
scope :exceeded, -> { where.not(exceeded_at: nil) }
|
|
13
13
|
scope :blocked, -> { where.not(blocked_at: nil) }
|
|
@@ -11,6 +11,12 @@ module PricingPlans
|
|
|
11
11
|
calculate_window_for_period(plan_owner, period_type)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
# Calculate window with pre-resolved period_type to avoid redundant plan lookups.
|
|
15
|
+
# Used by StatusContext which has already resolved the limit config.
|
|
16
|
+
def window_for_period_type(plan_owner, period_type)
|
|
17
|
+
calculate_window_for_period(plan_owner, period_type)
|
|
18
|
+
end
|
|
19
|
+
|
|
14
20
|
private
|
|
15
21
|
|
|
16
22
|
# Backward-compatible shim for tests that stub pay_available?
|
data/lib/pricing_plans/plan.rb
CHANGED
|
@@ -139,6 +139,9 @@ module PricingPlans
|
|
|
139
139
|
end
|
|
140
140
|
end
|
|
141
141
|
|
|
142
|
+
alias_method :set_metadata, :set_meta
|
|
143
|
+
alias_method :metadata, :meta
|
|
144
|
+
|
|
142
145
|
# CTA helpers for pricing UI
|
|
143
146
|
def set_cta_text(value)
|
|
144
147
|
@cta_text = value&.to_s
|
|
@@ -169,7 +172,7 @@ module PricingPlans
|
|
|
169
172
|
default = PricingPlans.configuration.default_cta_url
|
|
170
173
|
return default if default
|
|
171
174
|
# New default: if host app defines subscribe_path, prefer that
|
|
172
|
-
if defined?(Rails) && Rails.application.routes.url_helpers.respond_to?(:subscribe_path)
|
|
175
|
+
if defined?(Rails) && Rails.respond_to?(:application) && Rails.application && Rails.application.routes.url_helpers.respond_to?(:subscribe_path)
|
|
173
176
|
return Rails.application.routes.url_helpers.subscribe_path(plan: key, interval: :month)
|
|
174
177
|
end
|
|
175
178
|
nil
|
|
@@ -480,6 +483,7 @@ module PricingPlans
|
|
|
480
483
|
name: name,
|
|
481
484
|
description: description,
|
|
482
485
|
features: bullets, # alias in this gem
|
|
486
|
+
metadata: metadata.dup,
|
|
483
487
|
highlighted: highlighted?,
|
|
484
488
|
default: default?,
|
|
485
489
|
free: free?,
|
|
@@ -110,6 +110,76 @@ module PricingPlans
|
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
module ClassMethods
|
|
113
|
+
# === Class-level scopes for admin dashboards ===
|
|
114
|
+
# These scopes allow querying plan owners by their limits status.
|
|
115
|
+
# Useful for admin dashboards to find "organizations needing attention".
|
|
116
|
+
|
|
117
|
+
# Plan owners with any limit that has been exceeded (exceeded_at is set)
|
|
118
|
+
# Includes both those in grace period and those that are blocked.
|
|
119
|
+
def with_exceeded_limits
|
|
120
|
+
joins_enforcement_states.where.not(pricing_plans_enforcement_states: { exceeded_at: nil }).distinct
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Plan owners with any limit that is blocked (blocked_at is set)
|
|
124
|
+
def with_blocked_limits
|
|
125
|
+
joins_enforcement_states.where.not(pricing_plans_enforcement_states: { blocked_at: nil }).distinct
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Plan owners with limits in grace period (exceeded but not yet blocked)
|
|
129
|
+
def in_grace_period
|
|
130
|
+
joins_enforcement_states
|
|
131
|
+
.where.not(pricing_plans_enforcement_states: { exceeded_at: nil })
|
|
132
|
+
.where(pricing_plans_enforcement_states: { blocked_at: nil })
|
|
133
|
+
.distinct
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Plan owners with no exceeded limits (complement of with_exceeded_limits).
|
|
137
|
+
# Uses LEFT OUTER JOIN for better performance than subquery on large tables.
|
|
138
|
+
def within_all_limits
|
|
139
|
+
left_outer_joins_exceeded_enforcement_states
|
|
140
|
+
.where(pricing_plans_enforcement_states: { id: nil })
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Alias for with_exceeded_limits - plan owners that need attention
|
|
144
|
+
# (either exceeded or blocked on any limit)
|
|
145
|
+
def needing_attention
|
|
146
|
+
with_exceeded_limits
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
# Helper to join with enforcement_states table using polymorphic association.
|
|
152
|
+
# Uses Arel for clean, database-agnostic SQL construction.
|
|
153
|
+
def joins_enforcement_states
|
|
154
|
+
enforcement_states = PricingPlans::EnforcementState.arel_table
|
|
155
|
+
owners = arel_table
|
|
156
|
+
|
|
157
|
+
joins(
|
|
158
|
+
owners.join(enforcement_states)
|
|
159
|
+
.on(
|
|
160
|
+
enforcement_states[:plan_owner_id].eq(owners[:id])
|
|
161
|
+
.and(enforcement_states[:plan_owner_type].eq(name))
|
|
162
|
+
)
|
|
163
|
+
.join_sources
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# LEFT OUTER JOIN with exceeded_at condition in the join clause.
|
|
168
|
+
# Returns all plan owners, with NULL for those without exceeded limits.
|
|
169
|
+
def left_outer_joins_exceeded_enforcement_states
|
|
170
|
+
enforcement_states = PricingPlans::EnforcementState.arel_table
|
|
171
|
+
owners = arel_table
|
|
172
|
+
|
|
173
|
+
joins(
|
|
174
|
+
owners.join(enforcement_states, Arel::Nodes::OuterJoin)
|
|
175
|
+
.on(
|
|
176
|
+
enforcement_states[:plan_owner_id].eq(owners[:id])
|
|
177
|
+
.and(enforcement_states[:plan_owner_type].eq(name))
|
|
178
|
+
.and(enforcement_states[:exceeded_at].not_eq(nil))
|
|
179
|
+
)
|
|
180
|
+
.join_sources
|
|
181
|
+
)
|
|
182
|
+
end
|
|
113
183
|
end
|
|
114
184
|
|
|
115
185
|
def within_plan_limits?(limit_key, by: 1)
|
|
@@ -10,19 +10,7 @@ module PricingPlans
|
|
|
10
10
|
def effective_plan_for(plan_owner)
|
|
11
11
|
log_debug "[PricingPlans::PlanResolver] effective_plan_for called for #{plan_owner.class.name}##{plan_owner.respond_to?(:id) ? plan_owner.id : 'N/A'}"
|
|
12
12
|
|
|
13
|
-
# 1. Check
|
|
14
|
-
pay_available = PaySupport.pay_available?
|
|
15
|
-
log_debug "[PricingPlans::PlanResolver] PaySupport.pay_available? = #{pay_available}"
|
|
16
|
-
log_debug "[PricingPlans::PlanResolver] defined?(Pay) = #{defined?(Pay)}"
|
|
17
|
-
|
|
18
|
-
if pay_available
|
|
19
|
-
log_debug "[PricingPlans::PlanResolver] Calling resolve_plan_from_pay..."
|
|
20
|
-
plan_from_pay = resolve_plan_from_pay(plan_owner)
|
|
21
|
-
log_debug "[PricingPlans::PlanResolver] resolve_plan_from_pay returned: #{plan_from_pay ? plan_from_pay.key : 'nil'}"
|
|
22
|
-
return plan_from_pay if plan_from_pay
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# 2. Check manual assignment
|
|
13
|
+
# 1. Check manual assignment FIRST (admin overrides take precedence)
|
|
26
14
|
log_debug "[PricingPlans::PlanResolver] Checking for manual assignment..."
|
|
27
15
|
if plan_owner.respond_to?(:id)
|
|
28
16
|
assignment = Assignment.find_by(
|
|
@@ -37,6 +25,18 @@ module PricingPlans
|
|
|
37
25
|
end
|
|
38
26
|
end
|
|
39
27
|
|
|
28
|
+
# 2. Check Pay subscription status
|
|
29
|
+
pay_available = PaySupport.pay_available?
|
|
30
|
+
log_debug "[PricingPlans::PlanResolver] PaySupport.pay_available? = #{pay_available}"
|
|
31
|
+
log_debug "[PricingPlans::PlanResolver] defined?(Pay) = #{defined?(Pay)}"
|
|
32
|
+
|
|
33
|
+
if pay_available
|
|
34
|
+
log_debug "[PricingPlans::PlanResolver] Calling resolve_plan_from_pay..."
|
|
35
|
+
plan_from_pay = resolve_plan_from_pay(plan_owner)
|
|
36
|
+
log_debug "[PricingPlans::PlanResolver] resolve_plan_from_pay returned: #{plan_from_pay ? plan_from_pay.key : 'nil'}"
|
|
37
|
+
return plan_from_pay if plan_from_pay
|
|
38
|
+
end
|
|
39
|
+
|
|
40
40
|
# 3. Fall back to default plan
|
|
41
41
|
default = Registry.default_plan
|
|
42
42
|
log_debug "[PricingPlans::PlanResolver] Returning default plan: #{default ? default.key : 'nil'}"
|
|
@@ -75,8 +75,8 @@ module PricingPlans
|
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
def emit_event(event_type, limit_key, *args)
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
# Delegate to Callbacks module for error-isolated execution
|
|
79
|
+
Callbacks.dispatch(event_type, limit_key, *args)
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
private
|