thread-master/apps/backend/internal/module/radar/usecase/watch_quota.go

115 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package usecase
import (
"context"
"errors"
"fmt"
"apps/backend/internal/module/radar/domain"
)
/*
PlanQuota 提供會員方案的雷達上限spec §4.9)。
介面留在 radar module 是為了不讓 radar 依賴 usage module正式環境的實作在
internal/svc 橋接既有方案定義,測試直接給固定值。
*/
type PlanQuota interface {
RadarQuota(ctx context.Context, ownerUID int64) (maxActiveWatches, maxDailyOpportunities int, err error)
}
// FixedQuota 是測試與離線工具用的固定上限。
type FixedQuota struct {
MaxActiveWatches int
MaxDailyOpportunities int
}
func (q FixedQuota) RadarQuota(context.Context, int64) (int, int, error) {
return q.MaxActiveWatches, q.MaxDailyOpportunities, nil
}
/*
沒接上 PlanQuota 時的保守預設 = 最低方案。
失敗方向要選「少給」而不是「白送」:漏接線的結果是使用者看到上限提示來問客服,
而不是所有人都拿到 Pro 的常駐監控量。
*/
var fallbackQuota = FixedQuota{MaxActiveWatches: 1, MaxDailyOpportunities: 5}
func (s *Service) quota(ctx context.Context, ownerUID int64) (FixedQuota, error) {
if s.Quota == nil {
return fallbackQuota, nil
}
maxActive, maxDaily, err := s.Quota.RadarQuota(ctx, ownerUID)
if err != nil {
return FixedQuota{}, err
}
if maxActive <= 0 {
maxActive = fallbackQuota.MaxActiveWatches
}
if maxDaily <= 0 {
maxDaily = fallbackQuota.MaxDailyOpportunities
}
return FixedQuota{MaxActiveWatches: maxActive, MaxDailyOpportunities: maxDaily}, nil
}
func (s *Service) MaxActiveWatches(ctx context.Context, ownerUID int64) (int, error) {
q, err := s.quota(ctx, ownerUID)
if err != nil {
return 0, err
}
return q.MaxActiveWatches, nil
}
func (s *Service) MaxDailyOpportunities(ctx context.Context, ownerUID int64) (int, error) {
q, err := s.quota(ctx, ownerUID)
if err != nil {
return 0, err
}
return q.MaxDailyOpportunities, nil
}
/*
assertCanActivate 是「變成 active」的兩道閘SP-01、RW-01
exceptWatchID 是正在恢復的那一筆:它目前不是 active所以不會被算進 CountActive
帶進來只為了在訊息與計算上表達清楚。
既有超額者不強制降級spec §3.1):這裡只擋「再多一個」。
*/
func (s *Service) assertCanActivate(ctx context.Context, ownerUID int64, exceptWatchID string) error {
hasProfile, err := s.HasServiceProfile(ctx, ownerUID)
if err != nil {
return err
}
if !hasProfile {
return fmt.Errorf(
"%w: service profile required before activating a radar watch; fill in /api/v1/radar/service-profile first",
domain.ErrValidation,
)
}
maxActive, err := s.MaxActiveWatches(ctx, ownerUID)
if err != nil {
return err
}
active, err := s.Repo.CountActiveWatches(ctx, ownerUID)
if err != nil {
return err
}
if exceptWatchID != "" {
if w, err := s.Repo.GetWatch(ctx, exceptWatchID); err == nil && w.Status == domain.WatchActive {
active--
} else if err != nil && !errors.Is(err, domain.ErrNotFound) {
return err
}
}
if active >= int64(maxActive) {
return fmt.Errorf(
"%w: active watch limit reached (%d of %d on your plan); pause an existing watch or upgrade your plan",
domain.ErrValidation, active, maxActive,
)
}
return nil
}