Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ The `pricing_plans` gem needs three new models in the schema in order to work: `

- `PricingPlans::Assignment` allow manual plan overrides independent of billing system (or before you wire up Stripe/Pay). Great for admin toggles, trials, demos.
- What: The arbitrary `plan_key` and a `source` label (default "manual"). Unique per plan_owner.
- How its used: `PlanResolver` checks Pay → manual assignment → default plan. You can call `assign_pricing_plan!` and `remove_pricing_plan!` on the plan_owner.
- How it's used: `PlanResolver` checks manual assignment → Pay → default plan. Manual assignments (admin overrides) take precedence over subscription-based plans. You can call `assign_pricing_plan!` and `remove_pricing_plan!` on the plan_owner.

- `PricingPlans::EnforcementState` tracks per-plan_owner per-limit enforcement state for persistent caps and per-period allowances (grace/warnings/block state) in a race-safe way.
- What: `exceeded_at`, `blocked_at`, last warning info, and a small JSON `data` column where we persist plan-derived parameters like grace period seconds.
Expand Down
26 changes: 13 additions & 13 deletions lib/pricing_plans/plan_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,7 @@ def log_debug(message)
def effective_plan_for(plan_owner)
log_debug "[PricingPlans::PlanResolver] effective_plan_for called for #{plan_owner.class.name}##{plan_owner.respond_to?(:id) ? plan_owner.id : 'N/A'}"

# 1. Check Pay subscription status first (no app-specific gate required)
pay_available = PaySupport.pay_available?
log_debug "[PricingPlans::PlanResolver] PaySupport.pay_available? = #{pay_available}"
log_debug "[PricingPlans::PlanResolver] defined?(Pay) = #{defined?(Pay)}"

if pay_available
log_debug "[PricingPlans::PlanResolver] Calling resolve_plan_from_pay..."
plan_from_pay = resolve_plan_from_pay(plan_owner)
log_debug "[PricingPlans::PlanResolver] resolve_plan_from_pay returned: #{plan_from_pay ? plan_from_pay.key : 'nil'}"
return plan_from_pay if plan_from_pay
end

# 2. Check manual assignment
# 1. Check manual assignment FIRST (admin overrides take precedence)
log_debug "[PricingPlans::PlanResolver] Checking for manual assignment..."
if plan_owner.respond_to?(:id)
assignment = Assignment.find_by(
Expand All @@ -37,6 +25,18 @@ def effective_plan_for(plan_owner)
end
end

# 2. Check Pay subscription status
pay_available = PaySupport.pay_available?
log_debug "[PricingPlans::PlanResolver] PaySupport.pay_available? = #{pay_available}"
log_debug "[PricingPlans::PlanResolver] defined?(Pay) = #{defined?(Pay)}"

if pay_available
log_debug "[PricingPlans::PlanResolver] Calling resolve_plan_from_pay..."
plan_from_pay = resolve_plan_from_pay(plan_owner)
log_debug "[PricingPlans::PlanResolver] resolve_plan_from_pay returned: #{plan_from_pay ? plan_from_pay.key : 'nil'}"
return plan_from_pay if plan_from_pay
end

# 3. Fall back to default plan
default = Registry.default_plan
log_debug "[PricingPlans::PlanResolver] Returning default plan: #{default ? default.key : 'nil'}"
Expand Down
14 changes: 7 additions & 7 deletions test/integration/complete_workflow_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -230,17 +230,17 @@ def test_manual_plan_assignment_override_workflow
# Should be on pro plan via subscription
assert_equal :pro, PricingPlans::PlanResolver.effective_plan_for(org).key

# But subscription overrides manual assignment
# Manual assignment overrides subscription (admin override takes precedence)
PricingPlans::Assignment.assign_plan_to(org, :enterprise)

# Still on pro plan (Pay takes precedence)
assert_equal :pro, PricingPlans::PlanResolver.effective_plan_for(org).key
# Now on enterprise plan (manual assignment wins)
assert_equal :enterprise, PricingPlans::PlanResolver.effective_plan_for(org).key

# Remove subscription
org.pay_subscription = { active: false }
# Remove manual assignment
PricingPlans::Assignment.remove_assignment_for(org)

# Now manual assignment takes effect
assert_equal :enterprise, PricingPlans::PlanResolver.effective_plan_for(org).key
# Back to subscription-based plan
assert_equal :pro, PricingPlans::PlanResolver.effective_plan_for(org).key
end

def test_grace_period_expiration_workflow
Expand Down
6 changes: 3 additions & 3 deletions test/services/plan_resolver_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,17 @@ def test_effective_plan_falls_back_to_default
assert_equal :free, plan.key
end

def test_effective_plan_prioritizes_pay_over_assignment
def test_manual_assignment_overrides_pay_subscription
org = create_organization(
pay_subscription: { active: true, processor_plan: "price_pro_123" }
)

# Manual assignment should be ignored when Pay subscription is active
# Manual assignment takes precedence over Pay subscription (admin override)
PricingPlans::Assignment.assign_plan_to(org, :enterprise)

plan = PricingPlans::PlanResolver.effective_plan_for(org)

assert_equal :pro, plan.key # Pay subscription wins
assert_equal :enterprise, plan.key # Manual assignment wins
end

def test_effective_plan_with_unknown_processor_plan
Expand Down