Class: PricingPlans::StatusContext

Inherits:
Object
  • Object
show all
Includes:
ExceededStateUtils
Defined in:
lib/pricing_plans/status_context.rb

Overview

Request-scoped context that caches computed values within a single status() call. This eliminates the N+1 query problem where helper methods like severity_for(), message_for(), overage_for() etc. all re-call limit_status() internally.

Thread-safe by design: each call to status() gets its own context instance.

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from ExceededStateUtils

#clear_exceeded_flags!, #exceeded_now?

Constructor Details

#initialize(plan_owner) ⇒ StatusContext

Returns a new instance of StatusContext.



14
15
16
17
18
19
20
21
22
23
24
25
26
# File 'lib/pricing_plans/status_context.rb', line 14

def initialize(plan_owner)
  @plan_owner = plan_owner
  @plan_cache = nil
  @limit_config_cache = {}
  @limit_status_cache = {}
  @usage_cache = {}
  @grace_active_cache = {}
  @grace_ends_at_cache = {}
  @should_block_cache = {}
  @percent_used_cache = {}
  @warning_thresholds_cache = {}
  @severity_cache = {}
end

Instance Attribute Details

#plan_ownerObject (readonly)

Returns the value of attribute plan_owner.



12
13
14
# File 'lib/pricing_plans/status_context.rb', line 12

def plan_owner
  @plan_owner
end

Instance Method Details

#current_usage_for(limit_key) ⇒ Object

Cached current usage lookup



43
44
45
46
47
48
49
# File 'lib/pricing_plans/status_context.rb', line 43

def current_usage_for(limit_key)
  key = limit_key.to_sym
  return @usage_cache[key] if @usage_cache.key?(key)

  limit_config = limit_config_for(limit_key)
  @usage_cache[key] = limit_config ? LimitChecker.current_usage_for(@plan_owner, limit_key, limit_config) : 0
end

#effective_planObject

Cached plan resolution - called once per context



31
32
33
# File 'lib/pricing_plans/status_context.rb', line 31

def effective_plan
  @plan_cache ||= PlanResolver.effective_plan_for(@plan_owner)
end

#grace_active?(limit_key) ⇒ Boolean

Cached grace active check - implemented directly to avoid GraceManager's plan resolution

Returns:

  • (Boolean)


67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/pricing_plans/status_context.rb', line 67

def grace_active?(limit_key)
  key = limit_key.to_sym
  return @grace_active_cache[key] if @grace_active_cache.key?(key)

  state = fresh_enforcement_state(limit_key)

  # If no state exists but we're over limit for grace_then_block, lazily create grace
  unless state&.exceeded?
    if should_lazily_start_grace?(limit_key)
      limit_config = limit_config_for(limit_key)
      GraceManager.mark_exceeded!(@plan_owner, limit_key, grace_period: limit_config[:grace])
      # Clear caches to get fresh state
      @fresh_enforcement_state_cache&.delete(key)
      @enforcement_state_cache&.delete(key)
      state = fresh_enforcement_state(limit_key)
    else
      return @grace_active_cache[key] = false
    end
  end

  unless currently_exceeded?(limit_key)
    clear_exceeded_flags!(state)
    return @grace_active_cache[key] = false
  end

  @grace_active_cache[key] = !state.grace_expired?
end

#grace_ends_at(limit_key) ⇒ Object

Cached grace ends at - uses fresh_enforcement_state to avoid stale data



96
97
98
99
100
101
102
103
104
105
# File 'lib/pricing_plans/status_context.rb', line 96

def grace_ends_at(limit_key)
  key = limit_key.to_sym
  return @grace_ends_at_cache[key] if @grace_ends_at_cache.key?(key)

  state = fresh_enforcement_state(limit_key)
  return @grace_ends_at_cache[key] = nil unless state&.exceeded?
  return @grace_ends_at_cache[key] = nil unless grace_active?(limit_key)

  @grace_ends_at_cache[key] = state.grace_ends_at
end

#highest_severity_for(*limit_keys) ⇒ Object

Highest severity across multiple limits



178
179
180
181
182
183
184
185
186
# File 'lib/pricing_plans/status_context.rb', line 178

def highest_severity_for(*limit_keys)
  keys = limit_keys.flatten
  per_key = keys.map { |k| severity_for(k) }

  return :blocked if per_key.include?(:blocked)
  return :grace if per_key.include?(:grace)
  return :at_limit if per_key.include?(:at_limit)
  per_key.include?(:warning) ? :warning : :ok
end

#limit_config_for(limit_key) ⇒ Object

Cached limit config lookup



36
37
38
39
40
# File 'lib/pricing_plans/status_context.rb', line 36

def limit_config_for(limit_key)
  key = limit_key.to_sym
  return @limit_config_cache[key] if @limit_config_cache.key?(key)
  @limit_config_cache[key] = effective_plan&.limit_for(limit_key)
end

#limit_status(limit_key) ⇒ Object

Full limit status hash - cached, computed from other cached values



162
163
164
165
166
167
# File 'lib/pricing_plans/status_context.rb', line 162

def limit_status(limit_key)
  key = limit_key.to_sym
  return @limit_status_cache[key] if @limit_status_cache.key?(key)

  @limit_status_cache[key] = compute_limit_status(limit_key)
end

#message_for(limit_key) ⇒ Object

Message for a single limit



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/pricing_plans/status_context.rb', line 189

def message_for(limit_key)
  st = limit_status(limit_key)
  return nil unless st[:configured]

  severity = severity_for(limit_key)
  return nil if severity == :ok

  cfg = PricingPlans.configuration
  current_usage = st[:current_usage]
  limit_amount = st[:limit_amount]
  ends_at = st[:grace_ends_at]

  if cfg.message_builder
    context = case severity
              when :blocked then :over_limit
              when :grace then :grace
              when :at_limit then :at_limit
              else :warning
              end
    begin
      custom = cfg.message_builder.call(
        context: context,
        limit_key: limit_key,
        current_usage: current_usage,
        limit_amount: limit_amount,
        grace_ends_at: ends_at
      )
      return custom if custom
    rescue StandardError
      # fall through to defaults
    end
  end

  noun = begin
    PricingPlans.noun_for(limit_key)
  rescue StandardError
    "limit"
  end

  case severity
  when :blocked
    if limit_amount.is_a?(Numeric)
      "You've gone over your #{noun} for #{limit_key.to_s.humanize.downcase} (#{current_usage}/#{limit_amount}). Please upgrade your plan."
    else
      "You've gone over your #{noun} for #{limit_key.to_s.humanize.downcase}. Please upgrade your plan."
    end
  when :grace
    deadline = ends_at ? ", and your grace period ends #{ends_at.strftime('%B %d at %I:%M%p')}" : ""
    if limit_amount.is_a?(Numeric)
      "Heads up! You're currently over your #{noun} for #{limit_key.to_s.humanize.downcase} (#{current_usage}/#{limit_amount})#{deadline}. Please upgrade soon to avoid any interruptions."
    else
      "Heads up! You're currently over your #{noun} for #{limit_key.to_s.humanize.downcase}#{deadline}. Please upgrade soon to avoid any interruptions."
    end
  when :at_limit
    if limit_amount.is_a?(Numeric)
      "You've reached your #{noun} for #{limit_key.to_s.humanize.downcase} (#{current_usage}/#{limit_amount}). Upgrade your plan to unlock more."
    else
      "You're at the maximum allowed for #{limit_key.to_s.humanize.downcase}. Want more? Consider upgrading your plan."
    end
  else # :warning
    if limit_amount.is_a?(Numeric)
      "You're getting close to your #{noun} for #{limit_key.to_s.humanize.downcase} (#{current_usage}/#{limit_amount}). Keep an eye on your usage, or upgrade your plan now to stay ahead."
    else
      "You're getting close to your #{noun} for #{limit_key.to_s.humanize.downcase}. Keep an eye on your usage, or upgrade your plan now to stay ahead."
    end
  end
end

#overage_for(limit_key) ⇒ Object

Overage for a limit



258
259
260
261
262
263
264
265
266
267
# File 'lib/pricing_plans/status_context.rb', line 258

def overage_for(limit_key)
  st = limit_status(limit_key)
  return 0 unless st[:configured]

  allowed = st[:limit_amount]
  current = st[:current_usage].to_i
  return 0 unless allowed.is_a?(Numeric)

  [current - allowed.to_i, 0].max
end

#percent_used_for(limit_key) ⇒ Object

Cached percent used



52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/pricing_plans/status_context.rb', line 52

def percent_used_for(limit_key)
  key = limit_key.to_sym
  return @percent_used_cache[key] if @percent_used_cache.key?(key)

  limit_config = limit_config_for(limit_key)
  return @percent_used_cache[key] = 0.0 unless limit_config

  limit_amount = limit_config[:to]
  return @percent_used_cache[key] = 0.0 if limit_amount == :unlimited || limit_amount.to_i.zero?

  usage = current_usage_for(limit_key)
  @percent_used_cache[key] = [(usage.to_f / limit_amount) * 100, 100.0].min
end

#period_window_for(limit_key) ⇒ Object

Cached period window calculation - delegates to PeriodCalculator with pre-resolved period_type to avoid redundant effective_plan_for calls



149
150
151
152
153
154
155
156
157
158
159
# File 'lib/pricing_plans/status_context.rb', line 149

def period_window_for(limit_key)
  key = limit_key.to_sym
  @period_window_cache ||= {}
  return @period_window_cache[key] if @period_window_cache.key?(key)

  limit_config = limit_config_for(limit_key)
  return @period_window_cache[key] = [nil, nil] unless limit_config && limit_config[:per]

  period_type = limit_config[:per] || PricingPlans::Registry.configuration.period_cycle
  @period_window_cache[key] = PeriodCalculator.window_for_period_type(@plan_owner, period_type)
end

#severity_for(limit_key) ⇒ Object

Severity for a single limit - computed from cached data



170
171
172
173
174
175
# File 'lib/pricing_plans/status_context.rb', line 170

def severity_for(limit_key)
  key = limit_key.to_sym
  return @severity_cache[key] if @severity_cache.key?(key)

  @severity_cache[key] = compute_severity(limit_key)
end

#should_block?(limit_key) ⇒ Boolean

Cached should block check - implemented directly to avoid GraceManager's plan resolution

Returns:

  • (Boolean)


108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/pricing_plans/status_context.rb', line 108

def should_block?(limit_key)
  key = limit_key.to_sym
  return @should_block_cache[key] if @should_block_cache.key?(key)

  limit_config = limit_config_for(limit_key)
  return @should_block_cache[key] = false unless limit_config

  after_limit = limit_config[:after_limit]
  return @should_block_cache[key] = false if after_limit == :just_warn

  limit_amount = limit_config[:to]
  return @should_block_cache[key] = false if limit_amount == :unlimited

  current_usage = current_usage_for(limit_key)
  exceeded = exceeded_now?(current_usage, limit_amount, after_limit: after_limit)

  return @should_block_cache[key] = exceeded if after_limit == :block_usage

  # For :grace_then_block, check if grace period expired
  unless exceeded
    state = fresh_enforcement_state(limit_key)
    clear_exceeded_flags!(state) if state
    return @should_block_cache[key] = false
  end

  state = fresh_enforcement_state(limit_key)
  return @should_block_cache[key] = false unless state&.exceeded?
  @should_block_cache[key] = state.grace_expired?
end

#warning_thresholds(limit_key) ⇒ Object

Cached warning thresholds



139
140
141
142
143
144
145
# File 'lib/pricing_plans/status_context.rb', line 139

def warning_thresholds(limit_key)
  key = limit_key.to_sym
  return @warning_thresholds_cache[key] if @warning_thresholds_cache.key?(key)

  limit_config = limit_config_for(limit_key)
  @warning_thresholds_cache[key] = limit_config ? (limit_config[:warn_at] || []) : []
end