// Package billing models GuestGuard's subscription tiers and Stripe // integration. Plan limits live here so handler + middleware layers don't // hard-code numbers — change a value, restart the API, and the cap moves. package billing import "fmt" // Tier is the user's current subscription tier. Stored as text in the // subscriptions table. type Tier string const ( TierFree Tier = "free" TierPro Tier = "pro" TierBusiness Tier = "business" ) func (t Tier) Valid() bool { switch t { case TierFree, TierPro, TierBusiness: return true } return false } // Limits enforces what a tier may do. -1 means unlimited. type Limits struct { EventsPerMonth int GuestsPerEvent int } // TierLimits is the canonical plan-limits table. Matches docs/TIER1_PLAN.md // Block F pricing (placeholder until market validation). var TierLimits = map[Tier]Limits{ TierFree: {EventsPerMonth: 1, GuestsPerEvent: 50}, TierPro: {EventsPerMonth: 10, GuestsPerEvent: 1000}, TierBusiness: {EventsPerMonth: -1, GuestsPerEvent: 5000}, } // LimitsFor returns the limits for a tier, defaulting to Free for unknown // strings — defensive, so a typo or future-tier in the DB never grants // unlimited access. func LimitsFor(t Tier) Limits { if l, ok := TierLimits[t]; ok { return l } return TierLimits[TierFree] } // StatusGrantsAccess returns true if a subscription with this status // should be treated as paid. Mirrors the unique-index predicate in // 0005_billing.up.sql. func StatusGrantsAccess(status string) bool { switch status { case "active", "past_due", "trialing": return true } return false } // LimitError describes a denied-by-policy outcome. The handler layer // turns this into a 402 with a JSON body the frontend uses to render // the upgrade modal. type LimitError struct { Reason string Tier Tier Limit int Used int } func (e *LimitError) Error() string { return fmt.Sprintf("billing: %s (tier=%s used=%d limit=%d)", e.Reason, e.Tier, e.Used, e.Limit) }