package service import ( "errors" "fmt" ) // Plan caps for the social features. Updates here propagate everywhere via // EnforceBookmarkLimit / EnforceCollectionLimit. const ( PlanFree = "free" PlanPremium = "premium" freeBookmarkLimit = 1 premiumBookmarkLimit = 5 freeCollectionPerVisibilityLimit = 5 // 5 public AND 5 private separately // Premium has no collection cap. ) // 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. func EnforceBookmarkLimit(plan string, currentCount int) error { limit := bookmarkLimitFor(plan) if currentCount >= limit { return &PlanLimitError{Resource: "bookmarks", Limit: limit, Plan: plan} } return nil } // EnforceCollectionLimit returns nil if the user is allowed to create one // more collection of the given visibility. // // `isPublic=true` → checks against public-collection cap // `isPublic=false` → checks against private-collection cap // Premium users have no cap → always nil. func EnforceCollectionLimit(plan string, isPublic bool, currentCount int) error { if plan == PlanPremium { return nil } limit := freeCollectionPerVisibilityLimit if currentCount >= limit { resource := "private collections" if isPublic { resource = "public collections" } return &PlanLimitError{Resource: resource, Limit: limit, Plan: plan} } return nil } func bookmarkLimitFor(plan string) int { if plan == PlanPremium { return premiumBookmarkLimit } return freeBookmarkLimit } // BookmarkLimitFor returns the cap (for showing "X / Y used" in the UI). func BookmarkLimitFor(plan string) int { return bookmarkLimitFor(plan) } // CollectionLimitFor returns the per-visibility cap. -1 means unlimited. func CollectionLimitFor(plan string) int { if plan == PlanPremium { return -1 } return freeCollectionPerVisibilityLimit }