168 lines
5.1 KiB
Go
168 lines
5.1 KiB
Go
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"`
|
||
}
|