feat: enhance limits

This commit is contained in:
Krispire Admin
2026-06-23 14:21:45 +05:30
parent eadef33eac
commit 96ebbfd719
12 changed files with 183 additions and 53 deletions
+6
View File
@@ -157,6 +157,12 @@ func (s *AdminService) SetUserBanned(ctx context.Context, userID string, banned
return s.repo.SetUserBanned(ctx, userID, banned)
}
// SetUserLimits writes per-user save-limit overrides (a nil field resets that
// limit to the plan default). found=false when the id is unknown.
func (s *AdminService) SetUserLimits(ctx context.Context, userID string, bookmark, publicCollection, privateCollection *int) (bool, error) {
return s.repo.SetUserLimits(ctx, userID, bookmark, publicCollection, privateCollection)
}
// feedbackReplyEmailTpl renders the reply email. Reply text is auto-escaped by
// html/template, so admin-entered text can't inject markup.
var feedbackReplyEmailTpl = template.Must(template.New("feedbackReply").Parse(`<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#2a2118;max-width:560px;margin:0 auto;font-size:17px;line-height:1.7">
+23 -47
View File
@@ -3,19 +3,16 @@ package service
import (
"errors"
"fmt"
"github.com/prabhupadasays/spsays-bff/internal/model"
)
// Plan caps for the social features. Updates here propagate everywhere via
// EnforceBookmarkLimit / EnforceCollectionLimit.
// 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"
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.
@@ -38,50 +35,29 @@ func (e *PlanLimitError) Error() string {
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}
// 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.
//
// `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
// 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"
}
limit := freeCollectionPerVisibilityLimit
if currentCount >= limit {
resource := "private collections"
if isPublic {
resource = "public collections"
}
return &PlanLimitError{Resource: resource, Limit: limit, Plan: plan}
if limit >= 0 && currentCount >= limit {
return &PlanLimitError{Resource: resource, Limit: limit, Plan: u.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
}
+3 -3
View File
@@ -538,7 +538,7 @@ func (s *BookmarkService) Create(ctx context.Context, userID, sourceURL, sourceT
if err != nil {
return nil, err
}
if err := EnforceBookmarkLimit(u.Plan, current); err != nil {
if err := EnforceBookmarkLimit(u, current); err != nil {
return nil, err
}
@@ -602,7 +602,7 @@ func (s *CollectionService) Create(ctx context.Context, userID, title string, de
if err != nil {
return nil, err
}
if err := EnforceCollectionLimit(u.Plan, isPublic, current); err != nil {
if err := EnforceCollectionLimit(u, isPublic, current); err != nil {
return nil, err
}
return s.collections.Create(ctx, userID, title, description, coverURL, isPublic)
@@ -628,7 +628,7 @@ func (s *CollectionService) Update(ctx context.Context, id, userID string, title
if err != nil {
return nil, err
}
if err := EnforceCollectionLimit(u.Plan, *isPublic, current); err != nil {
if err := EnforceCollectionLimit(u, *isPublic, current); err != nil {
return nil, err
}
}