88 lines
2.6 KiB
Go
88 lines
2.6 KiB
Go
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
|
|
}
|