pricing_plans 0.1.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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/settings.local.json +16 -0
  3. data/.rubocop.yml +137 -0
  4. data/CHANGELOG.md +83 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +241 -0
  7. data/Rakefile +15 -0
  8. data/docs/01-define-pricing-plans.md +372 -0
  9. data/docs/02-controller-helpers.md +223 -0
  10. data/docs/03-model-helpers.md +318 -0
  11. data/docs/04-views.md +121 -0
  12. data/docs/05-semantic-pricing.md +159 -0
  13. data/docs/06-gem-compatibility.md +99 -0
  14. data/docs/images/pricing_plans_ruby_rails_gem_pricing_table.jpg +0 -0
  15. data/docs/images/pricing_plans_ruby_rails_gem_usage_alert_upgrade.jpg +0 -0
  16. data/docs/images/pricing_plans_ruby_rails_gem_usage_meter.jpg +0 -0
  17. data/docs/images/product_creation_blocked.jpg +0 -0
  18. data/lib/generators/pricing_plans/install/install_generator.rb +42 -0
  19. data/lib/generators/pricing_plans/install/templates/create_pricing_plans_tables.rb.erb +91 -0
  20. data/lib/generators/pricing_plans/install/templates/initializer.rb +100 -0
  21. data/lib/pricing_plans/association_limit_registry.rb +45 -0
  22. data/lib/pricing_plans/configuration.rb +189 -0
  23. data/lib/pricing_plans/controller_guards.rb +574 -0
  24. data/lib/pricing_plans/controller_rescues.rb +115 -0
  25. data/lib/pricing_plans/dsl.rb +44 -0
  26. data/lib/pricing_plans/engine.rb +69 -0
  27. data/lib/pricing_plans/grace_manager.rb +227 -0
  28. data/lib/pricing_plans/integer_refinements.rb +48 -0
  29. data/lib/pricing_plans/job_guards.rb +24 -0
  30. data/lib/pricing_plans/limit_checker.rb +157 -0
  31. data/lib/pricing_plans/limitable.rb +286 -0
  32. data/lib/pricing_plans/models/assignment.rb +55 -0
  33. data/lib/pricing_plans/models/enforcement_state.rb +45 -0
  34. data/lib/pricing_plans/models/usage.rb +51 -0
  35. data/lib/pricing_plans/overage_reporter.rb +77 -0
  36. data/lib/pricing_plans/pay_support.rb +85 -0
  37. data/lib/pricing_plans/period_calculator.rb +183 -0
  38. data/lib/pricing_plans/plan.rb +653 -0
  39. data/lib/pricing_plans/plan_owner.rb +287 -0
  40. data/lib/pricing_plans/plan_resolver.rb +85 -0
  41. data/lib/pricing_plans/price_components.rb +16 -0
  42. data/lib/pricing_plans/registry.rb +182 -0
  43. data/lib/pricing_plans/result.rb +109 -0
  44. data/lib/pricing_plans/version.rb +5 -0
  45. data/lib/pricing_plans/view_helpers.rb +58 -0
  46. data/lib/pricing_plans.rb +645 -0
  47. data/sig/pricing_plans.rbs +4 -0
  48. metadata +236 -0
@@ -0,0 +1,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PricingPlans
4
+ module Limitable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # Track all limited_by configurations for this model
9
+ class_attribute :pricing_plans_limits, default: {}
10
+
11
+ # Callbacks for automatic tracking
12
+ after_create :increment_per_period_counters
13
+ after_destroy :decrement_persistent_counters
14
+ # Add plan_owner-centric convenience methods to instances of the plan_owner class
15
+ # when possible. These are no-ops if the model isn't the plan_owner itself.
16
+ define_method :within_plan_limits? do |limit_key, by: 1|
17
+ plan_owner = self
18
+ LimitChecker.within_limit?(plan_owner, limit_key, by: by)
19
+ end
20
+
21
+ define_method :plan_limit_remaining do |limit_key|
22
+ plan_owner = self
23
+ LimitChecker.plan_limit_remaining(plan_owner, limit_key)
24
+ end
25
+
26
+ define_method :plan_limit_percent_used do |limit_key|
27
+ plan_owner = self
28
+ LimitChecker.plan_limit_percent_used(plan_owner, limit_key)
29
+ end
30
+
31
+ define_method :current_pricing_plan do
32
+ PlanResolver.effective_plan_for(self)
33
+ end
34
+
35
+ define_singleton_method :assign_pricing_plan! do |plan_owner, plan_key, source: "manual"|
36
+ Assignment.assign_plan_to(plan_owner, plan_key, source: source)
37
+ end
38
+ end
39
+
40
+ class_methods do
41
+ # New ergonomic macro: limited_by_pricing_plans
42
+ # - Auto-includes this concern if not already included
43
+ # - Infers limit_key from model's collection/table name when not provided
44
+ # - Infers plan owner association from configured plan_owner_class (or common conventions)
45
+ # - Accepts `per:` to declare per-period allowances
46
+ def limited_by_pricing_plans(limit_key = nil, plan_owner: nil, per: nil, on: nil, error_after_limit: nil, count_scope: nil)
47
+ include PricingPlans::Limitable unless ancestors.include?(PricingPlans::Limitable)
48
+
49
+ inferred_limit_key = (limit_key || inferred_limit_key_for_model).to_sym
50
+ effective_owner = plan_owner || on
51
+ inferred_owner = infer_plan_owner_association(effective_owner)
52
+
53
+ limited_by(inferred_limit_key, plan_owner: inferred_owner, per: per, error_after_limit: error_after_limit, count_scope: count_scope)
54
+ end
55
+
56
+ # Backing implementation used by both the classic and new macro
57
+ def limited_by(limit_key, plan_owner:, per: nil, error_after_limit: nil, source: nil, count_scope: nil)
58
+ limit_key = limit_key.to_sym
59
+ plan_owner_method = plan_owner.to_sym
60
+
61
+ # Store the configuration
62
+ self.pricing_plans_limits = pricing_plans_limits.merge(
63
+ limit_key => {
64
+ plan_owner_method: plan_owner_method,
65
+ per: per,
66
+ error_after_limit: error_after_limit,
67
+ source: source,
68
+ count_scope: count_scope
69
+ }
70
+ )
71
+
72
+ # Register counter only for persistent caps
73
+ unless per
74
+ source_proc = count_scope || source
75
+ PricingPlans::LimitableRegistry.register_counter(limit_key) do |plan_owner_instance|
76
+ # Base relation for this limited model and plan_owner
77
+ base_relation = relation_for_plan_owner(plan_owner_instance, plan_owner_method)
78
+
79
+ # Prefer plan-level count_scope if present; fallback to model-provided one
80
+ scope_cfg = begin
81
+ plan = PlanResolver.effective_plan_for(plan_owner_instance)
82
+ cfg = plan&.limit_for(limit_key)
83
+ cfg && cfg[:count_scope]
84
+ end
85
+ scope_cfg ||= source_proc if source_proc
86
+
87
+ relation = apply_count_scope(base_relation, scope_cfg, plan_owner_instance)
88
+ relation.respond_to?(:count) ? relation.count : base_relation.count
89
+ end
90
+ end
91
+
92
+ # Add validation to prevent creation when over limit
93
+ validate_limit_on_create(limit_key, plan_owner_method, per, error_after_limit)
94
+ end
95
+
96
+ def count_for_plan_owner(plan_owner_instance, plan_owner_method)
97
+ relation_for_plan_owner(plan_owner_instance, plan_owner_method).count
98
+ end
99
+
100
+ def relation_for_plan_owner(plan_owner_instance, plan_owner_method)
101
+ joins_condition = if plan_owner_method == :self
102
+ { id: plan_owner_instance.id }
103
+ else
104
+ { plan_owner_method => plan_owner_instance }
105
+ end
106
+ where(joins_condition)
107
+ end
108
+
109
+ # Apply a flexible count_scope to an ActiveRecord::Relation.
110
+ # Accepts Proc/Lambda, Symbol (scope name), Hash (where), or Array of these.
111
+ def apply_count_scope(relation, scope_cfg, plan_owner_instance)
112
+ return relation unless scope_cfg
113
+
114
+ case scope_cfg
115
+ when Array
116
+ scope_cfg.reduce(relation) { |rel, cfg| apply_count_scope(rel, cfg, plan_owner_instance) }
117
+ when Proc
118
+ # Support arity variants: (rel) or (rel, plan_owner)
119
+ case scope_cfg.arity
120
+ when 1 then scope_cfg.call(relation)
121
+ when 2 then scope_cfg.call(relation, plan_owner_instance)
122
+ else
123
+ relation.instance_exec(&scope_cfg)
124
+ end
125
+ when Symbol
126
+ if relation.respond_to?(scope_cfg)
127
+ relation.public_send(scope_cfg)
128
+ else
129
+ relation
130
+ end
131
+ when Hash
132
+ relation.where(scope_cfg)
133
+ else
134
+ relation
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ def inferred_limit_key_for_model
141
+ # Prefer table_name (works for anonymous AR classes with explicit table_name)
142
+ return table_name if respond_to?(:table_name) && table_name
143
+
144
+ # Fallback to model_name.collection only if the class has a real name
145
+ if respond_to?(:name) && name && respond_to?(:model_name) && model_name.respond_to?(:collection)
146
+ return model_name.collection
147
+ end
148
+
149
+ raise PricingPlans::ConfigurationError, "Cannot infer limit key: provide one explicitly"
150
+ end
151
+
152
+ def infer_plan_owner_association(explicit)
153
+ return explicit.to_sym if explicit
154
+
155
+ # Prefer configured plan_owner_class association name if present
156
+ begin
157
+ plan_owner_klass = PricingPlans::Registry.plan_owner_class
158
+ rescue StandardError
159
+ plan_owner_klass = nil
160
+ end
161
+
162
+ if plan_owner_klass
163
+ association_name = plan_owner_klass.name.underscore.to_sym
164
+ return association_name if reflect_on_association(association_name)
165
+ end
166
+
167
+ # Common conventions fallback
168
+ %i[organization account user team company workspace tenant].each do |candidate|
169
+ return candidate if reflect_on_association(candidate)
170
+ end
171
+
172
+ # If nothing found, assume the record limits itself
173
+ :self
174
+ end
175
+
176
+ def validate_limit_on_create(limit_key, plan_owner_method, per, error_after_limit)
177
+ method_name = :"check_limit_on_create_#{limit_key}"
178
+
179
+ # Only define the method if it doesn't already exist
180
+ unless method_defined?(method_name)
181
+ validate method_name, on: :create
182
+
183
+ define_method method_name do
184
+ plan_owner_instance = if plan_owner_method == :self
185
+ self
186
+ else
187
+ send(plan_owner_method)
188
+ end
189
+
190
+ return unless plan_owner_instance
191
+
192
+ # Skip validation if the plan_owner doesn't have limits configured
193
+ plan = PlanResolver.effective_plan_for(plan_owner_instance)
194
+ limit_config = plan&.limit_for(limit_key)
195
+ return unless limit_config
196
+ return if limit_config[:to] == :unlimited
197
+
198
+ # For persistent caps, check if we'd exceed the limit
199
+ if per.nil?
200
+ current_count = self.class.count_for_plan_owner(plan_owner_instance, plan_owner_method)
201
+ if current_count >= limit_config[:to]
202
+ # Check grace/block policy
203
+ case limit_config[:after_limit]
204
+ when :just_warn
205
+ # Allow creation with warning
206
+ return
207
+ when :block_usage, :grace_then_block
208
+ if limit_config[:after_limit] == :block_usage || GraceManager.should_block?(plan_owner_instance, limit_key)
209
+ message = error_after_limit || "Cannot create #{self.class.name.downcase}: #{limit_key} limit exceeded"
210
+ errors.add(:base, message)
211
+ end
212
+ end
213
+ end
214
+ else
215
+ # For per-period limits, check usage in current period
216
+ current_usage = LimitChecker.current_usage_for(plan_owner_instance, limit_key, limit_config)
217
+ if current_usage >= limit_config[:to]
218
+ case limit_config[:after_limit]
219
+ when :just_warn
220
+ return
221
+ when :block_usage, :grace_then_block
222
+ if limit_config[:after_limit] == :block_usage || GraceManager.should_block?(plan_owner_instance, limit_key)
223
+ message = error_after_limit || "Cannot create #{self.class.name.downcase}: #{limit_key} limit exceeded for this period"
224
+ errors.add(:base, message)
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
233
+
234
+ private
235
+
236
+ def increment_per_period_counters
237
+ self.class.pricing_plans_limits.each do |limit_key, config|
238
+ next unless config[:per] # Only per-period limits
239
+
240
+ plan_owner_instance = if config[:plan_owner_method] == :self
241
+ self
242
+ else
243
+ send(config[:plan_owner_method])
244
+ end
245
+
246
+ next unless plan_owner_instance
247
+
248
+ period_start, period_end = PeriodCalculator.window_for(plan_owner_instance, limit_key)
249
+
250
+ # Use upsert for better performance and concurrency handling
251
+ usage = Usage.find_or_initialize_by(
252
+ plan_owner: plan_owner_instance,
253
+ limit_key: limit_key.to_s,
254
+ period_start: period_start,
255
+ period_end: period_end
256
+ )
257
+
258
+ if usage.new_record?
259
+ usage.used = 1
260
+ usage.last_used_at = Time.current
261
+
262
+ begin
263
+ usage.save!
264
+ rescue ActiveRecord::RecordNotUnique
265
+ # Handle race condition - record was created by another process
266
+ usage = Usage.find_by(
267
+ plan_owner: plan_owner_instance,
268
+ limit_key: limit_key.to_s,
269
+ period_start: period_start,
270
+ period_end: period_end
271
+ )
272
+ usage&.increment!
273
+ end
274
+ else
275
+ usage.increment!
276
+ end
277
+ end
278
+ end
279
+
280
+ def decrement_persistent_counters
281
+ # For persistent caps, we don't need to do anything on destroy
282
+ # since the counter is computed live from the database
283
+ # The record being destroyed will automatically reduce the count
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PricingPlans
4
+ class Assignment < ActiveRecord::Base
5
+ self.table_name = "pricing_plans_assignments"
6
+
7
+ belongs_to :plan_owner, polymorphic: true
8
+
9
+ validates :plan_owner, presence: true
10
+ validates :plan_key, presence: true
11
+ validates :source, presence: true
12
+ validates :plan_owner_type, uniqueness: { scope: :plan_owner_id }
13
+
14
+ validate :plan_exists_in_registry
15
+
16
+ scope :manual, -> { where(source: "manual") }
17
+ scope :for_plan, ->(plan_key) { where(plan_key: plan_key.to_s) }
18
+
19
+ def plan
20
+ Registry.plan(plan_key.to_sym)
21
+ end
22
+
23
+ def self.assign_plan_to(plan_owner, plan_key, source: "manual")
24
+ assignment = find_or_initialize_by(
25
+ plan_owner_type: plan_owner.class.name,
26
+ plan_owner_id: plan_owner.id
27
+ )
28
+
29
+ assignment.assign_attributes(
30
+ plan_key: plan_key.to_s,
31
+ source: source.to_s
32
+ )
33
+
34
+ assignment.save!
35
+ assignment
36
+ end
37
+
38
+ def self.remove_assignment_for(plan_owner)
39
+ where(
40
+ plan_owner_type: plan_owner.class.name,
41
+ plan_owner_id: plan_owner.id
42
+ ).destroy_all
43
+ end
44
+
45
+ private
46
+
47
+ def plan_exists_in_registry
48
+ return unless plan_key.present?
49
+
50
+ unless Registry.plan_exists?(plan_key)
51
+ errors.add(:plan_key, "#{plan_key} is not a defined plan")
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PricingPlans
4
+ class EnforcementState < ActiveRecord::Base
5
+ self.table_name = "pricing_plans_enforcement_states"
6
+
7
+ belongs_to :plan_owner, polymorphic: true
8
+
9
+ validates :limit_key, presence: true
10
+ validates :plan_owner_type, :plan_owner_id, :limit_key, uniqueness: { scope: [:plan_owner_type, :plan_owner_id] }
11
+
12
+ scope :exceeded, -> { where.not(exceeded_at: nil) }
13
+ scope :blocked, -> { where.not(blocked_at: nil) }
14
+ scope :in_grace, -> { exceeded.where(blocked_at: nil) }
15
+
16
+ def exceeded?
17
+ exceeded_at.present?
18
+ end
19
+
20
+ def blocked?
21
+ blocked_at.present?
22
+ end
23
+
24
+ def in_grace?
25
+ exceeded? && !blocked?
26
+ end
27
+
28
+ def grace_ends_at
29
+ return nil unless exceeded_at && grace_period
30
+ exceeded_at + grace_period
31
+ end
32
+
33
+ def grace_expired?
34
+ return false unless grace_ends_at
35
+ Time.current >= grace_ends_at
36
+ end
37
+
38
+ private
39
+
40
+ def grace_period
41
+ # This will be set by the GraceManager based on the plan configuration
42
+ data&.dig("grace_period")&.seconds
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PricingPlans
4
+ class Usage < ActiveRecord::Base
5
+ self.table_name = "pricing_plans_usages"
6
+
7
+ belongs_to :plan_owner, polymorphic: true
8
+
9
+ validates :limit_key, presence: true
10
+ validates :period_start, :period_end, presence: true
11
+ validates :used, presence: true, numericality: { greater_than_or_equal_to: 0 }
12
+ validates :period_start, uniqueness: { scope: [:plan_owner_type, :plan_owner_id, :limit_key] }
13
+
14
+ validate :period_end_after_start
15
+
16
+ scope :current_period, ->(period_start, period_end) {
17
+ where(period_start: period_start, period_end: period_end)
18
+ }
19
+
20
+ scope :for_limit, ->(limit_key) { where(limit_key: limit_key.to_s) }
21
+
22
+ def increment!(amount = 1)
23
+ increment(:used, amount)
24
+ update!(last_used_at: Time.current)
25
+ end
26
+
27
+ def within_period?(timestamp = Time.current)
28
+ timestamp >= period_start && timestamp < period_end
29
+ end
30
+
31
+ def remaining(limit_amount)
32
+ return Float::INFINITY if limit_amount == :unlimited
33
+ [0, limit_amount - used].max
34
+ end
35
+
36
+ def percent_used(limit_amount)
37
+ return 0.0 if limit_amount == :unlimited || limit_amount.zero?
38
+ [(used.to_f / limit_amount) * 100, 100.0].min
39
+ end
40
+
41
+ private
42
+
43
+ def period_end_after_start
44
+ return unless period_start && period_end
45
+
46
+ if period_end <= period_start
47
+ errors.add(:period_end, "must be after period_start")
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PricingPlans
4
+ class OverageReporter
5
+ OverageItem = Struct.new(
6
+ :limit_key,
7
+ :kind,
8
+ :current_usage,
9
+ :allowed,
10
+ :overage,
11
+ :grace_active,
12
+ :grace_ends_at,
13
+ keyword_init: true
14
+ )
15
+
16
+ Report = Struct.new(:items, :message, keyword_init: true)
17
+
18
+ class << self
19
+ # Compute overage against a target plan for the given plan_owner.
20
+ # Returns an array of OverageItem for limits that are over the target.
21
+ # kind: :persistent or :per_period
22
+ def report(plan_owner, target_plan)
23
+ plan = target_plan.is_a?(PricingPlans::Plan) ? target_plan : Registry.plan(target_plan.to_sym)
24
+
25
+ plan.limits.map do |limit_key, limit_config|
26
+ next if limit_config[:to] == :unlimited
27
+
28
+ usage = LimitChecker.current_usage_for(plan_owner, limit_key, limit_config)
29
+ allowed = limit_config[:to]
30
+ over_by = [usage - allowed.to_i, 0].max
31
+ next if over_by <= 0
32
+
33
+ OverageItem.new(
34
+ limit_key: limit_key,
35
+ kind: (limit_config[:per] ? :per_period : :persistent),
36
+ current_usage: usage,
37
+ allowed: allowed,
38
+ overage: over_by,
39
+ grace_active: GraceManager.grace_active?(plan_owner, limit_key),
40
+ grace_ends_at: GraceManager.grace_ends_at(plan_owner, limit_key)
41
+ )
42
+ end.compact
43
+ end
44
+
45
+ # Returns a Report with items and a human message suitable for downgrade UX.
46
+ def report_with_message(plan_owner, target_plan)
47
+ items = report(plan_owner, target_plan)
48
+ return Report.new(items: [], message: "No overages on target plan") if items.empty?
49
+
50
+ parts = items.map do |i|
51
+ "#{i.limit_key}: #{i.current_usage} > #{i.allowed} (reduce by #{i.overage})"
52
+ end
53
+ grace_info = items.select(&:grace_active).map do |i|
54
+ ends = i.grace_ends_at&.utc&.iso8601
55
+ "#{i.limit_key} grace ends at #{ends}"
56
+ end
57
+
58
+ msg = if PricingPlans.configuration&.message_builder
59
+ begin
60
+ built = PricingPlans.configuration.message_builder.call(
61
+ context: :overage_report,
62
+ items: items
63
+ )
64
+ built if built
65
+ rescue StandardError
66
+ nil
67
+ end
68
+ end
69
+ msg ||= "Over target plan on: #{parts.join(', ')}. "
70
+ msg += "Grace active — #{grace_info.join(', ')}." unless grace_info.empty?
71
+
72
+ Report.new(items: items, message: msg)
73
+ end
74
+
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PricingPlans
4
+ module PaySupport
5
+ module_function
6
+
7
+ def pay_available?
8
+ defined?(Pay)
9
+ end
10
+
11
+ def subscription_active_for?(plan_owner)
12
+ return false unless plan_owner
13
+
14
+ # Prefer Pay's official API on the payment_processor
15
+ if plan_owner.respond_to?(:payment_processor) && (pp = plan_owner.payment_processor)
16
+ return true if (pp.respond_to?(:subscribed?) && pp.subscribed?) ||
17
+ (pp.respond_to?(:on_trial?) && pp.on_trial?) ||
18
+ (pp.respond_to?(:on_grace_period?) && pp.on_grace_period?)
19
+
20
+ if pp.respond_to?(:subscriptions) && (subs = pp.subscriptions)
21
+ return subs.any? { |sub| (sub.respond_to?(:active?) && sub.active?) || (sub.respond_to?(:on_trial?) && sub.on_trial?) || (sub.respond_to?(:on_grace_period?) && sub.on_grace_period?) }
22
+ end
23
+ end
24
+
25
+ # Fallbacks for apps that surface Pay state on the owner
26
+ individual_active = (plan_owner.respond_to?(:subscribed?) && plan_owner.subscribed?) ||
27
+ (plan_owner.respond_to?(:on_trial?) && plan_owner.on_trial?) ||
28
+ (plan_owner.respond_to?(:on_grace_period?) && plan_owner.on_grace_period?)
29
+ return true if individual_active
30
+
31
+ if plan_owner.respond_to?(:subscriptions) && (subs = plan_owner.subscriptions)
32
+ return subs.any? { |sub| (sub.respond_to?(:active?) && sub.active?) || (sub.respond_to?(:on_trial?) && sub.on_trial?) || (sub.respond_to?(:on_grace_period?) && sub.on_grace_period?) }
33
+ end
34
+
35
+ false
36
+ end
37
+
38
+ def current_subscription_for(plan_owner)
39
+ return nil unless pay_available?
40
+
41
+ # Prefer Pay's payment_processor API
42
+ if plan_owner.respond_to?(:payment_processor) && (pp = plan_owner.payment_processor)
43
+ if pp.respond_to?(:subscription)
44
+ subscription = pp.subscription
45
+ if subscription && (
46
+ (subscription.respond_to?(:active?) && subscription.active?) ||
47
+ (subscription.respond_to?(:on_trial?) && subscription.on_trial?) ||
48
+ (subscription.respond_to?(:on_grace_period?) && subscription.on_grace_period?)
49
+ )
50
+ return subscription
51
+ end
52
+ end
53
+
54
+ if pp.respond_to?(:subscriptions) && (subs = pp.subscriptions)
55
+ found = subs.find do |sub|
56
+ (sub.respond_to?(:on_trial?) && sub.on_trial?) ||
57
+ (sub.respond_to?(:on_grace_period?) && sub.on_grace_period?) ||
58
+ (sub.respond_to?(:active?) && sub.active?)
59
+ end
60
+ return found if found
61
+ end
62
+ end
63
+
64
+ # Fallbacks for apps that surface subscriptions on the owner
65
+ if plan_owner.respond_to?(:subscription)
66
+ subscription = plan_owner.subscription
67
+ if subscription && (
68
+ (subscription.respond_to?(:active?) && subscription.active?) ||
69
+ (subscription.respond_to?(:on_trial?) && subscription.on_trial?) ||
70
+ (subscription.respond_to?(:on_grace_period?) && subscription.on_grace_period?)
71
+ )
72
+ return subscription
73
+ end
74
+ end
75
+
76
+ if plan_owner.respond_to?(:subscriptions) && (subs = plan_owner.subscriptions)
77
+ subs.find do |sub|
78
+ (sub.respond_to?(:on_trial?) && sub.on_trial?) ||
79
+ (sub.respond_to?(:on_grace_period?) && sub.on_grace_period?) ||
80
+ (sub.respond_to?(:active?) && sub.active?)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end