feat: add user centric features
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user