thread-master/apps/backend/internal/module/usage/domain/usage.go

243 lines
7.7 KiB
Go
Raw Permalink Normal View History

2026-07-13 01:15:30 +00:00
package domain
import (
"errors"
2026-07-13 03:18:08 +00:00
"strings"
2026-07-13 01:15:30 +00:00
"time"
)
const (
KeyModePlatform = "platform"
KeyModeByok = "byok"
MeterAICopy = "ai_copy"
MeterAIResearch = "ai_research"
MeterWebSearch = "web_search"
MeterAIImage = "ai_image"
PlanFree = "free"
PlanStarter = "starter"
PlanPro = "pro"
)
var (
ErrNotFound = errors.New("usage not found")
ErrForbidden = errors.New("usage forbidden")
ErrNoKey = errors.New("no api key configured (platform or byok)")
ErrQuotaExceeded = errors.New("platform credits insufficient")
2026-07-13 03:18:08 +00:00
// 分項點數上限(與 FE soft_caps 對齊platform only
ErrMeterCapExceeded = errors.New("usage.err.meterCap")
// 平台 key 無容量/限流:僅 BYOK 可繼續(不扣平台點)
ErrPlatformCapacity = errors.New("usage.err.platformCapacity")
ErrInvalidPlan = errors.New("invalid plan")
2026-07-15 15:23:59 +00:00
ErrInvalidRange = errors.New("invalid usage analytics range")
ErrPurchaseDisabled = errors.New("self-service plan purchase is disabled; use billing checkout")
2026-07-13 01:15:30 +00:00
)
type Event struct {
ID string `bson:"_id" json:"id"`
UID int64 `bson:"uid" json:"uid"`
Meter string `bson:"meter" json:"meter"`
Credits int `bson:"credits" json:"credits"` // platform only; byok always 0
KeyMode string `bson:"key_mode" json:"key_mode"`
Label string `bson:"label" json:"label"`
Source string `bson:"source" json:"source"`
MonthKey string `bson:"month_key" json:"month_key"`
CreatedAt int64 `bson:"created_at" json:"created_at"`
}
2026-07-15 15:23:59 +00:00
type MonthlyCounter struct {
ID string `bson:"_id" json:"id"`
UID int64 `bson:"uid" json:"uid"`
MonthKey string `bson:"month_key" json:"month_key"`
TotalCredits int `bson:"total_credits" json:"total_credits"`
ByMeter map[string]int `bson:"by_meter" json:"by_meter"`
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
}
2026-07-13 01:15:30 +00:00
type MemberPrefs struct {
2026-07-15 15:23:59 +00:00
UID int64 `bson:"uid" json:"uid"`
PlanID string `bson:"plan_id" json:"plan_id"`
Unlimited bool `bson:"unlimited" json:"unlimited"`
AdminOverride bool `bson:"admin_override" json:"admin_override"`
PaidPlanID string `bson:"paid_plan_id,omitempty" json:"paid_plan_id,omitempty"`
SubscriptionStatus string `bson:"subscription_status,omitempty" json:"subscription_status,omitempty"`
StripeCustomerID string `bson:"stripe_customer_id,omitempty" json:"stripe_customer_id,omitempty"`
StripeSubscriptionID string `bson:"stripe_subscription_id,omitempty" json:"stripe_subscription_id,omitempty"`
CurrentPeriodStart int64 `bson:"current_period_start,omitempty" json:"current_period_start,omitempty"`
CurrentPeriodEnd int64 `bson:"current_period_end,omitempty" json:"current_period_end,omitempty"`
CancelAtPeriodEnd bool `bson:"cancel_at_period_end" json:"cancel_at_period_end"`
BillingEventID string `bson:"billing_event_id,omitempty" json:"-"`
BillingEventCreatedAt int64 `bson:"billing_event_created_at,omitempty" json:"-"`
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
2026-07-13 01:15:30 +00:00
}
type Purchase struct {
ID string `bson:"_id" json:"id"`
UID int64 `bson:"uid" json:"uid"`
PlanID string `bson:"plan_id" json:"plan_id"`
MockRef string `bson:"mock_ref,omitempty" json:"mock_ref,omitempty"`
CreatedAt int64 `bson:"created_at" json:"created_at"`
}
type PlanDef struct {
ID string
MonthlyCredits int
2026-07-13 03:18:08 +00:00
// SoftCaps各 meter 點數硬上限platform加總 = MonthlyCredits
SoftCaps map[string]int
2026-07-13 01:15:30 +00:00
}
2026-07-13 03:18:08 +00:00
// Plans — 與 FE PLANS 同步2026-07 · 組合毛利 ≥50%
// 安全單價約 $0.012Starter NT$590≈$18.4 / Pro NT$1990≈$62.2
// Free 120 點:夠用試用(可虧 ~$1.44 滿用);付費扛回組合毛利。
2026-07-13 01:15:30 +00:00
var Plans = map[string]PlanDef{
2026-07-13 03:18:08 +00:00
PlanFree: {
ID: PlanFree, MonthlyCredits: 120,
SoftCaps: map[string]int{
MeterAICopy: 60, MeterAIResearch: 15, MeterWebSearch: 30, MeterAIImage: 15,
},
},
PlanStarter: {
ID: PlanStarter, MonthlyCredits: 600,
SoftCaps: map[string]int{
MeterAICopy: 300, MeterAIResearch: 90, MeterWebSearch: 150, MeterAIImage: 60,
},
},
PlanPro: {
ID: PlanPro, MonthlyCredits: 2000,
SoftCaps: map[string]int{
MeterAICopy: 1000, MeterAIResearch: 300, MeterWebSearch: 500, MeterAIImage: 200,
},
},
2026-07-13 01:15:30 +00:00
}
var DefaultCreditCost = map[string]int{
MeterAICopy: 1, MeterAIResearch: 3, MeterWebSearch: 1, MeterAIImage: 5,
}
2026-07-13 03:18:08 +00:00
// ResolvePlan — 正規化 plan_id小寫並回傳方案未知 → Free
func ResolvePlan(planID string) PlanDef {
id := strings.ToLower(strings.TrimSpace(planID))
if p, ok := Plans[id]; ok && p.ID != "" {
return p
}
return Plans[PlanFree]
}
// MeterCost returns platform credit cost for one call.
func MeterCost(meter string) int {
if c := DefaultCreditCost[meter]; c > 0 {
return c
}
return 1
}
2026-07-13 01:15:30 +00:00
func NowNano() int64 { return time.Now().UTC().UnixNano() }
func MonthKey(nano int64) string {
t := time.Unix(0, nano).UTC()
return t.Format("2006-01")
}
func CurrentMonthKey() string { return MonthKey(NowNano()) }
// Summary split — never merge platform+byok into one total_calls only.
type MeterCount struct {
Count int `json:"count"`
Credits int `json:"credits"`
}
type PlatformBlock struct {
CreditsUsed int `json:"credits_used"`
CreditsRemaining int `json:"credits_remaining"`
CreditsTotal int `json:"credits_total"`
CallCount int `json:"call_count"`
ByMeter map[string]MeterCount `json:"by_meter"`
}
type ByokBlock struct {
CallCount int `json:"call_count"`
ByMeter map[string]MeterCount `json:"by_meter"`
}
type MonthSummary struct {
MonthKey string `json:"month_key"`
UID int64 `json:"uid"`
PlanID string `json:"plan_id"`
Unlimited bool `json:"unlimited"`
Platform PlatformBlock `json:"platform"`
Byok ByokBlock `json:"byok"`
// Legacy FE bars: only platform credits (not mixed)
TotalCredits int `json:"total_credits"`
RemainingCredits int `json:"remaining_credits"`
}
type TenantRow struct {
2026-07-15 15:23:59 +00:00
UID int64 `json:"uid"`
PlanID string `json:"plan_id"`
Unlimited bool `json:"unlimited"`
PlatformCreditsUsed int `json:"platform_credits_used"`
ByokCallCount int `json:"byok_call_count"`
2026-07-13 01:15:30 +00:00
}
type TenantSummary struct {
2026-07-15 15:23:59 +00:00
MonthKey string `json:"month_key"`
PlatformCreditsUsed int `json:"platform_credits_used"`
ByokCallCount int `json:"byok_call_count"`
Members []TenantRow `json:"members"`
}
type AnalyticsMember struct {
UID int64
Email string
DisplayName string
}
type AnalyticsQuery struct {
Granularity string
From time.Time
To time.Time
}
type AnalyticsBucket struct {
Key string
Label string
PurchasedCredits int
ConsumedCredits int
AICalls int
SearchCalls int
ByokCallCount int
}
type AnalyticsMemberRow struct {
UID int64
Email string
DisplayName string
Unlimited bool
PlanID string
PurchasedCredits int
TotalCredits int
AICalls int
SearchCalls int
ByokCallCount int
RemainingCredits int
Pct int
}
type TenantAnalytics struct {
Granularity string
From string
To string
RangeLabel string
PurchasedCredits int
ConsumedCredits int
RemainingCredits int
Pct int
AICalls int
SearchCalls int
ByMeter map[string]MeterCount
Byok ByokBlock
Series []AnalyticsBucket
Members []AnalyticsMemberRow
2026-07-13 01:15:30 +00:00
}