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") ) 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 MemberPrefs struct { UID int64 `bson:"uid" json:"uid"` PlanID string `bson:"plan_id" json:"plan_id"` Unlimited bool `bson:"unlimited" json:"unlimited"` 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.012/點;Starter 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"` }