pricing_plans 0.2.1 → 0.3.1

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.
data/lib/pricing_plans.rb CHANGED
@@ -28,6 +28,7 @@ module PricingPlans
28
28
  autoload :LimitChecker, "pricing_plans/limit_checker"
29
29
  autoload :LimitableRegistry, "pricing_plans/limit_checker"
30
30
  autoload :GraceManager, "pricing_plans/grace_manager"
31
+ autoload :Callbacks, "pricing_plans/callbacks"
31
32
  autoload :PeriodCalculator, "pricing_plans/period_calculator"
32
33
  autoload :ControllerGuards, "pricing_plans/controller_guards"
33
34
  autoload :JobGuards, "pricing_plans/job_guards"
@@ -39,6 +40,7 @@ module PricingPlans
39
40
  autoload :OverageReporter, "pricing_plans/overage_reporter"
40
41
  autoload :PriceComponents, "pricing_plans/price_components"
41
42
  autoload :ViewHelpers, "pricing_plans/view_helpers"
43
+ autoload :StatusContext, "pricing_plans/status_context"
42
44
 
43
45
  # Models
44
46
  autoload :EnforcementState, "pricing_plans/models/enforcement_state"
@@ -227,8 +229,12 @@ module PricingPlans
227
229
  )
228
230
 
229
231
  def status(plan_owner, limits: [])
232
+ # Use StatusContext to cache all computed values and eliminate N+1 queries.
233
+ # Each call to status() gets its own context - thread-safe by design.
234
+ ctx = StatusContext.new(plan_owner)
235
+
230
236
  items = Array(limits).map do |limit_key|
231
- st = limit_status(limit_key, plan_owner: plan_owner)
237
+ st = ctx.limit_status(limit_key)
232
238
  if !st[:configured]
233
239
  StatusItem.new(
234
240
  key: limit_key,
@@ -257,7 +263,7 @@ module PricingPlans
257
263
  period_seconds_remaining: nil
258
264
  )
259
265
  else
260
- sev = severity_for(plan_owner, limit_key)
266
+ sev = ctx.severity_for(limit_key)
261
267
  allowed = st[:limit_amount]
262
268
  current = st[:current_usage].to_i
263
269
  unlimited = (allowed == :unlimited)
@@ -266,7 +272,7 @@ module PricingPlans
266
272
  else
267
273
  nil
268
274
  end
269
- warn_thresholds = LimitChecker.warning_thresholds(plan_owner, limit_key)
275
+ warn_thresholds = ctx.warning_thresholds(limit_key)
270
276
  percent = st[:percent_used].to_f
271
277
  next_warn = begin
272
278
  thresholds = warn_thresholds.map { |t| t.to_f * 100.0 }.uniq.sort
@@ -277,7 +283,7 @@ module PricingPlans
277
283
  period_seconds_remaining = nil
278
284
  if st[:per]
279
285
  begin
280
- period_start, period_end = PeriodCalculator.window_for(plan_owner, limit_key)
286
+ period_start, period_end = ctx.period_window_for(limit_key)
281
287
  if period_end
282
288
  period_seconds_remaining = [0, (period_end - Time.current).to_i].max
283
289
  end
@@ -312,8 +318,8 @@ module PricingPlans
312
318
  when :blocked then 4
313
319
  else 0
314
320
  end,
315
- message: (sev == :ok ? nil : message_for(plan_owner, limit_key)),
316
- overage: overage_for(plan_owner, limit_key),
321
+ message: (sev == :ok ? nil : ctx.message_for(limit_key)),
322
+ overage: ctx.overage_for(limit_key),
317
323
  configured: true,
318
324
  unlimited: unlimited,
319
325
  remaining: remaining,
@@ -329,12 +335,12 @@ module PricingPlans
329
335
  end
330
336
  end
331
337
 
332
- # Compute and attach overall helpers directly on the returned array
338
+ # Compute overall helpers using context (all values already cached)
333
339
  keys = items.map(&:key)
334
- sev = highest_severity_for(plan_owner, *keys)
340
+ sev = ctx.highest_severity_for(*keys)
335
341
  title = summary_title_for(sev)
336
- msg = summary_message_for(plan_owner, *keys, severity: sev)
337
- highest_keys = keys.select { |k| severity_for(plan_owner, k) == sev }
342
+ msg = compute_summary_message_from_context(ctx, keys, sev)
343
+ highest_keys = keys.select { |k| ctx.severity_for(k) == sev }
338
344
  highest_limits = items.select { |it| highest_keys.include?(it.key) }
339
345
  human_keys = highest_keys.map { |k| k.to_s.humanize.downcase }
340
346
  keys_sentence = if human_keys.respond_to?(:to_sentence)
@@ -648,5 +654,39 @@ module PricingPlans
648
654
  def popular_plan_key
649
655
  highlighted_plan_key
650
656
  end
657
+
658
+ private
659
+
660
+ # Helper for status() that builds summary message using cached context data
661
+ def compute_summary_message_from_context(ctx, keys, sev)
662
+ return nil if keys.empty?
663
+ return nil if sev == :ok
664
+
665
+ affected = keys.select { |k| ctx.severity_for(k) == sev }
666
+ human_keys = affected.map { |k| k.to_s.humanize.downcase }
667
+ keys_list = if human_keys.respond_to?(:to_sentence)
668
+ human_keys.to_sentence
669
+ else
670
+ if human_keys.length <= 2
671
+ human_keys.join(" and ")
672
+ else
673
+ human_keys[0..-2].join(", ") + " and " + human_keys[-1]
674
+ end
675
+ end
676
+ noun = affected.size == 1 ? "plan limit" : "plan limits"
677
+
678
+ case sev
679
+ when :blocked
680
+ "Your #{noun} for #{keys_list} #{affected.size == 1 ? "has" : "have"} been exceeded. Please upgrade to continue."
681
+ when :grace
682
+ grace_end = keys.map { |k| ctx.grace_ends_at(k) }.compact.min
683
+ suffix = grace_end ? ", grace active until #{grace_end}" : ""
684
+ "You are over your #{noun} for #{keys_list}#{suffix}. Please upgrade to avoid service disruption."
685
+ when :at_limit
686
+ "You have reached your #{noun} for #{keys_list}."
687
+ else # :warning
688
+ "You are approaching your #{noun} for #{keys_list}."
689
+ end
690
+ end
651
691
  end
652
692
  end
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.2.1
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-01-15 00:00:00.000000000 Z
10
+ date: 2026-02-16 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord
@@ -61,8 +61,9 @@ executables: []
61
61
  extensions: []
62
62
  extra_rdoc_files: []
63
63
  files:
64
- - ".claude/settings.local.json"
65
64
  - ".rubocop.yml"
65
+ - ".simplecov"
66
+ - AGENTS.md
66
67
  - Appraisals
67
68
  - CHANGELOG.md
68
69
  - CLAUDE.md
@@ -86,6 +87,7 @@ files:
86
87
  - lib/generators/pricing_plans/install/templates/initializer.rb
87
88
  - lib/pricing_plans.rb
88
89
  - lib/pricing_plans/association_limit_registry.rb
90
+ - lib/pricing_plans/callbacks.rb
89
91
  - lib/pricing_plans/configuration.rb
90
92
  - lib/pricing_plans/controller_guards.rb
91
93
  - lib/pricing_plans/controller_rescues.rb
@@ -108,6 +110,7 @@ files:
108
110
  - lib/pricing_plans/price_components.rb
109
111
  - lib/pricing_plans/registry.rb
110
112
  - lib/pricing_plans/result.rb
113
+ - lib/pricing_plans/status_context.rb
111
114
  - lib/pricing_plans/version.rb
112
115
  - lib/pricing_plans/view_helpers.rb
113
116
  - sig/pricing_plans.rbs
@@ -1,19 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(sed:*)",
5
- "Bash(grep:*)",
6
- "Bash(find:*)",
7
- "mcp__context7__resolve-library-id",
8
- "mcp__context7__get-library-docs",
9
- "WebFetch(domain:github.com)",
10
- "WebSearch",
11
- "Bash(bundle exec rake test:*)",
12
- "Bash(bundle install:*)",
13
- "Bash(bundle exec appraisal:*)",
14
- "Bash(ls:*)",
15
- "Bash(done)"
16
- ],
17
- "deny": []
18
- }
19
- }