package service import ( "errors" "fmt" "github.com/prabhupadasays/spsays-bff/internal/model" ) // Plan identifiers. The numeric caps live in model.ResolveLimits (so the self // user payload and these enforcers agree), and per-user admin overrides take // precedence over the plan default. const ( PlanFree = "free" PlanPremium = "premium" ) // ErrPlanLimit signals that a free-plan limit blocks the requested action. // Handlers surface this as HTTP 402 (Payment Required) so clients can show // an upsell. The message tells the user what cap was hit. var ErrPlanLimit = errors.New("plan limit reached") // PlanLimitError carries detail about which limit was hit so the frontend // can render a helpful message instead of a generic "upgrade to premium". type PlanLimitError struct { Resource string // e.g. "bookmarks", "private collections" Limit int Plan string } func (e *PlanLimitError) Error() string { return fmt.Sprintf("%s limit reached (%d on %s plan)", e.Resource, e.Limit, e.Plan) } func (e *PlanLimitError) Is(target error) bool { return target == ErrPlanLimit } // EnforceBookmarkLimit returns nil if the user is allowed to create one more // bookmark, or a PlanLimitError otherwise. The cap is the user's effective // bookmark limit (plan default, or an admin override). A negative cap means // unlimited. func EnforceBookmarkLimit(u *model.User, currentCount int) error { limit := u.EffectiveLimits().Bookmarks if limit >= 0 && currentCount >= limit { return &PlanLimitError{Resource: "bookmarks", Limit: limit, Plan: u.Plan} } return nil } // EnforceCollectionLimit returns nil if the user is allowed to create one more // collection of the given visibility. The cap is the user's effective // public/private collection limit (plan default, or an admin override); a // negative cap means unlimited. func EnforceCollectionLimit(u *model.User, isPublic bool, currentCount int) error { l := u.EffectiveLimits() limit, resource := l.PrivateCollections, "private collections" if isPublic { limit, resource = l.PublicCollections, "public collections" } if limit >= 0 && currentCount >= limit { return &PlanLimitError{Resource: resource, Limit: limit, Plan: u.Plan} } return nil }