plans: shared Rank() helper for tier ordering#10
Merged
Conversation
Two package-private rank functions used to live in the api repo (internal/handlers/billing.go::tierRank and internal/handlers/admin_customers.go::adminTierRank). They had subtly different orderings — billing.go covered 6 tiers (anonymous .. team), admin_customers.go covered 4 (free .. team) and was off-by-one against billing for the same names. The discrepancy never bit production because the admin surface never sees anonymous/growth, but it's a footgun. Promote a single canonical ordering here so all modules share one rank function. Returns -1 for unknown tiers; callers must guard against the sentinel when comparing ranks (a negative rank means "no transition direction"). Yearly variants are NOT auto-normalised — callers pass them through CanonicalTier first if they want "pro_yearly" to rank as "pro". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
plans.Rank(tier string) int— single canonical tier ordering, shared across api/, worker/, and any future module.anonymous=0, free=1, hobby=2, growth=3, pro=4, team=5. Unknown tiers return-1so callers can short-circuit "no transition direction" rather than guess.CanonicalTierfirst if they want "pro_yearly" to rank the same as "pro" (billing.go does exactly that pattern).Why
Two package-private rank functions used to live in the api repo with subtly different orderings:
internal/handlers/billing.go::tierRank— 6 tiers (anonymous .. team).internal/handlers/admin_customers.go::adminTierRank— 4 tiers (free .. team), off-by-one against billing for the same names.The discrepancy never bit production because the admin surface never sees
anonymous/growth, but it's a footgun the moment the admin surface widens. Promoting a single canonical function eliminates the drift risk.The follow-up api PR (
handlers: use plans.Rank + consolidate agent_action constants) deletes both local copies and routes callers throughplans.Rank. It depends on this PR landing first.Test plan
go test ./plans/...green (3 new tests:TestRank_AllStandardTiers,TestRank_UnknownReturnsMinusOne,TestRank_MonotonicallyIncreasing, plusTestRank_CaseInsensitive)go test ./...green across the common moduleGenerated with Claude Code