返回 DeepSeek-Reasonix
migrate.go
根目录 / internal / billing / migrate.go
1 package billing
2
3 import (
4 "strings"
5 "time"
6 )
7
8 // LegacyUsageRecord is the minimum shape needed to migrate old telemetry.
9 type LegacyUsageRecord struct {
10 SessionCost float64
11 SessionCurrency string
12 // EndedAt is retained for historical ordering and identity valuation dates.
13 EndedAt time.Time
14 // Tokens optional for ledger completeness.
15 PromptTokens int
16 CompletionTokens int
17 ModelRef string
18 UsageSource string
19 }
20
21 // MigrateLegacyUsage builds a CostQuote from pre-CostQuote telemetry.
22 // Records that cannot be valued are marked legacy_estimate; wiped mixed-currency
23 // zeros are not reconstructed from current price tables.
24 func MigrateLegacyUsage(rec LegacyUsageRecord) CostQuote {
25 cur := NormalizeCurrency(rec.SessionCurrency)
26 if cur == "" {
27 // Historic defaults often used ¥ symbol without code.
28 if strings.Contains(rec.SessionCurrency, "¥") || strings.Contains(rec.SessionCurrency, "¥") {
29 cur = "CNY"
30 } else if strings.Contains(rec.SessionCurrency, "$") {
31 cur = "USD"
32 }
33 }
34 // Wiped mixed-currency totals (legacy code zeroed them) and empty rows must
35 // not be reconstructed from current price tables.
36 if rec.SessionCost <= 0 || cur == "" {
37 reason := "legacy_unrecoverable"
38 if rec.SessionCost == 0 && strings.TrimSpace(rec.SessionCurrency) != "" {
39 // Explicit zero with a currency often means a prior mixed-currency wipe.
40 reason = "legacy_wiped_or_zero"
41 } else if rec.SessionCost < 0 {
42 reason = "legacy_invalid_amount"
43 }
44 return CostQuote{
45 Original: MoneyOf(Zero, cur),
46 Estimated: true,
47 CostComplete: false,
48 DisplayComplete: false,
49 Complete: false,
50 DisplayStatus: DisplayStatusUnavailable,
51 LegacyEstimate: true,
52 IncompleteReason: reason,
53 ModelRef: rec.ModelRef,
54 UsageSource: rec.UsageSource,
55 }
56 }
57 amount := NewAmountFromFloat(rec.SessionCost)
58 q := CostQuote{
59 Original: MoneyOf(amount, cur),
60 Valuations: map[string]Valuation{},
61 Estimated: true,
62 CostComplete: true,
63 DisplayComplete: true,
64 Complete: true,
65 DisplayStatus: DisplayStatusMatched,
66 AggregateMode: AggregateModeSingleCurrency,
67 LegacyEstimate: true,
68 ModelRef: rec.ModelRef,
69 UsageSource: rec.UsageSource,
70 BillingMode: BillingModePAYG,
71 }
72 ended := rec.EndedAt
73 if ended.IsZero() {
74 ended = time.Now().UTC()
75 }
76 asOf := ended.UTC().Format("2006-01-02")
77 q.Valuations[cur] = Valuation{
78 Money: q.Original,
79 Basis: BasisIdentity,
80 Source: "legacy_telemetry",
81 AsOf: asOf,
82 }
83 m := q.Original
84 q.Selected = &m
85 return q
86 }
87
87 lines GO