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

243 lines
7.7 KiB
Go
Raw Permalink 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 domain
import (
"errors"
"strings"
"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")
// 分項點數上限(與 FE soft_caps 對齊platform only
ErrMeterCapExceeded = errors.New("usage.err.meterCap")
// 平台 key 無容量/限流:僅 BYOK 可繼續(不扣平台點)
ErrPlatformCapacity = errors.New("usage.err.platformCapacity")
ErrInvalidPlan = errors.New("invalid plan")
ErrInvalidRange = errors.New("invalid usage analytics range")
ErrPurchaseDisabled = errors.New("self-service plan purchase is disabled; use billing checkout")
)
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"`
}
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"`
}
type MemberPrefs struct {
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"`
}
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
// SoftCaps各 meter 點數硬上限platform加總 = MonthlyCredits
SoftCaps map[string]int
}
// Plans — 與 FE PLANS 同步2026-07 · 組合毛利 ≥50%
// 安全單價約 $0.012Starter NT$590≈$18.4 / Pro NT$1990≈$62.2
// Free 120 點:夠用試用(可虧 ~$1.44 滿用);付費扛回組合毛利。
var Plans = map[string]PlanDef{
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,
},
},
}
var DefaultCreditCost = map[string]int{
MeterAICopy: 1, MeterAIResearch: 3, MeterWebSearch: 1, MeterAIImage: 5,
}
// 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
}
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 {
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"`
}
type TenantSummary struct {
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
}