返回 DeepSeek-Reasonix
quote_test.go
根目录 / internal / billing / quote_test.go
1 package billing
2
3 import (
4 "testing"
5 "time"
6 )
7
8 func testInput(currency, provider, model string) QuoteInput {
9 rates := RateCard{CacheHit: 0.10, Input: 3, Output: 9, Currency: currency}
10 if currency == "USD" {
11 rates = RateCard{CacheHit: 0.014, Input: 0.44, Output: 1.32, Currency: currency}
12 }
13 return QuoteInput{
14 Usage: UsageTokens{PromptTokens: 1000, CompletionTokens: 2000},
15 Rates: rates,
16 DisplayCurrency: currency,
17 ModelRef: model,
18 ProviderKind: provider,
19 ModelID: model,
20 }
21 }
22
23 func TestBuildQuoteIdentityAndOfficialTableOnly(t *testing.T) {
24 in := testInput("USD", "deepseek", "deepseek-v4-flash")
25 in.DisplayCurrency = "CNY"
26 q := BuildQuote(in)
27 if q.Original.Currency != "USD" || q.Original.Amount == "0" {
28 t.Fatalf("original = %+v", q.Original)
29 }
30 v, ok := q.Valuations["CNY"]
31 if !ok || v.Basis != BasisOfficialTable {
32 t.Fatalf("CNY valuation = %+v, ok=%v", v, ok)
33 }
34 if q.Selected == nil || q.Selected.Currency != "CNY" || !q.Complete || !q.CostComplete || !q.DisplayComplete {
35 t.Fatalf("quote completeness/selection = %+v", q)
36 }
37 }
38
39 func TestBuildQuoteCustomPriceFallsBackWithoutFX(t *testing.T) {
40 in := testInput("CNY", "deepseek", "deepseek-v4-flash")
41 in.Rates.Input = 99 // no longer matches the official table
42 in.DisplayCurrency = "USD"
43 q := BuildQuote(in)
44 if len(q.Valuations) != 1 || q.Valuations["CNY"].Basis != BasisIdentity {
45 t.Fatalf("unexpected valuations: %+v", q.Valuations)
46 }
47 if q.Selected == nil || q.Selected.Currency != "CNY" || q.Complete || q.DisplayComplete || !q.CostComplete {
48 t.Fatalf("fallback quote = %+v", q)
49 }
50 if q.DisplayStatus != DisplayStatusFallbackOriginal || q.Valuations["USD"].Basis == BasisFX {
51 t.Fatalf("fallback status/FX = %+v", q)
52 }
53 }
54
55 func TestAggregateMixedCurrenciesProducesBuckets(t *testing.T) {
56 a := BuildQuote(QuoteInput{Usage: UsageTokens{PromptTokens: 1}, Rates: RateCard{Input: 1, Currency: "CNY"}})
57 b := BuildQuote(QuoteInput{Usage: UsageTokens{PromptTokens: 1}, Rates: RateCard{Input: 1, Currency: "USD"}})
58 q := AggregateQuotes([]CostQuote{a, b}, "")
59 if q.DisplayStatus != DisplayStatusBucketed || q.AggregateMode != AggregateModeCurrencyBuckets || q.Selected != nil {
60 t.Fatalf("mixed aggregate = %+v", q)
61 }
62 if len(q.OriginalTotals) != 2 || q.OriginalTotals[0].Currency != "CNY" || q.OriginalTotals[1].Currency != "USD" {
63 t.Fatalf("original totals = %+v", q.OriginalTotals)
64 }
65 if !q.CostComplete || q.Complete || q.DisplayComplete {
66 t.Fatalf("mixed completeness = %+v", q)
67 }
68 }
69
70 func TestAggregateExplicitDisplayFallsBackToSameOriginal(t *testing.T) {
71 a := BuildQuote(QuoteInput{Usage: UsageTokens{PromptTokens: 1}, Rates: RateCard{Input: 1, Currency: "CNY"}})
72 b := BuildQuote(QuoteInput{Usage: UsageTokens{PromptTokens: 1}, Rates: RateCard{Input: 2, Currency: "CNY"}})
73 q := AggregateQuotes([]CostQuote{a, b}, "USD")
74 if q.Selected == nil || q.Selected.Currency != "CNY" || q.DisplayStatus != DisplayStatusFallbackOriginal {
75 t.Fatalf("fallback aggregate = %+v", q)
76 }
77 if !q.CostComplete || q.DisplayComplete || q.Complete {
78 t.Fatalf("fallback completeness = %+v", q)
79 }
80 }
81
82 func TestAggregateRateBands(t *testing.T) {
83 base := func(band string) CostQuote {
84 return CostQuote{Original: Money{Amount: "1", Currency: "CNY"}, Valuations: map[string]Valuation{
85 "CNY": {Money: Money{Amount: "1", Currency: "CNY"}, Basis: BasisIdentity},
86 }, CostComplete: true, DisplayComplete: true, Complete: true, RateBand: band}
87 }
88 if got := AggregateQuotes([]CostQuote{base(RateBandPeak), base(RateBandPeak)}, ""); got.RateBand != RateBandPeak {
89 t.Fatalf("same band = %q", got.RateBand)
90 }
91 if got := AggregateQuotes([]CostQuote{base(RateBandPeak), base(RateBandOffPeak)}, ""); got.RateBand != RateBandMixed {
92 t.Fatalf("mixed band = %q", got.RateBand)
93 }
94 if got := AggregateQuotes([]CostQuote{base(RateBandPeak), base("")}, ""); got.RateBand != "" {
95 t.Fatalf("unknown member band = %q", got.RateBand)
96 }
97 }
98
99 func TestLedgerBucketAggregationClearsSingleRatedAt(t *testing.T) {
100 l := NewLedger()
101 q := CostQuote{
102 Original: Money{Amount: "1", Currency: "CNY"}, Valuations: map[string]Valuation{
103 "CNY": {Money: Money{Amount: "1", Currency: "CNY"}, Basis: BasisIdentity},
104 },
105 CostComplete: true, DisplayComplete: true, Complete: true,
106 PricingFingerprint: "peak-card", RateBand: RateBandPeak, RatedAt: "2026-08-17T01:00:00Z",
107 }
108 l.Add(q, UsageTokens{PromptTokens: 1}, time.Date(2026, 8, 17, 1, 0, 0, 0, time.UTC))
109 l.Add(q, UsageTokens{PromptTokens: 1}, time.Date(2026, 8, 17, 2, 0, 0, 0, time.UTC))
110 for _, entry := range l.Entries {
111 if entry.Quote.RateBand != RateBandPeak || entry.Quote.RatedAt != "" {
112 t.Fatalf("aggregated bucket quote = %+v", entry.Quote)
113 }
114 }
115 }
116
117 func TestNormalizeOldQuote(t *testing.T) {
118 q := NormalizeQuote(CostQuote{Original: MoneyOf(NewAmountFromFloat(1), "USD"), Complete: true})
119 if !q.CostComplete || !q.DisplayComplete || !q.Complete || q.DisplayStatus != DisplayStatusMatched {
120 t.Fatalf("normalized old quote = %+v", q)
121 }
122 }
123
124 func TestBuildQuoteNoPriceIsUnavailable(t *testing.T) {
125 q := CostQuote{Estimated: true, CostComplete: false, DisplayComplete: false, Complete: false, DisplayStatus: DisplayStatusUnavailable, IncompleteReason: "no_price"}
126 q = NormalizeQuote(q)
127 if q.DisplayStatus != DisplayStatusUnavailable || q.Complete || q.CostComplete {
128 t.Fatalf("unavailable = %+v", q)
129 }
130 }
131
132 func TestBuildQuoteMissingUsageIsUnavailable(t *testing.T) {
133 q := BuildQuote(QuoteInput{Rates: RateCard{Input: 1, Currency: "USD"}, DisplayCurrency: "USD"})
134 if q.CostComplete || q.DisplayComplete || q.Complete || q.Selected != nil || q.DisplayStatus != DisplayStatusUnavailable {
135 t.Fatalf("missing usage = %+v", q)
136 }
137 }
138
139 func TestAggregateNoUsageIsUnavailable(t *testing.T) {
140 q := AggregateQuotes(nil, "USD")
141 if q.CostComplete || q.DisplayComplete || q.Complete || q.Selected != nil || q.DisplayStatus != DisplayStatusUnavailable {
142 t.Fatalf("empty aggregate = %+v", q)
143 }
144 }
145
146 func TestLedgerMixedOriginalBucketsContinueAccumulating(t *testing.T) {
147 l := NewLedger()
148 base := func(currency string, amount string) CostQuote {
149 return CostQuote{
150 Original: Money{Amount: amount, Currency: currency},
151 Valuations: map[string]Valuation{currency: {Money: Money{Amount: amount, Currency: currency}, Basis: BasisIdentity}},
152 CostComplete: true, DisplayComplete: true, Complete: true,
153 DisplayStatus: DisplayStatusMatched, ModelRef: "m", PricingFingerprint: "same",
154 }
155 }
156 l.Add(base("CNY", "1"), UsageTokens{PromptTokens: 1}, time.Time{})
157 l.Add(base("USD", "2"), UsageTokens{PromptTokens: 1}, time.Time{})
158 l.Add(base("CNY", "3"), UsageTokens{PromptTokens: 1}, time.Time{})
159 q := l.Total("")
160 if len(q.OriginalTotals) != 2 || q.OriginalTotals[0].Amount != "4" || q.OriginalTotals[1].Amount != "2" || q.Selected != nil {
161 t.Fatalf("ledger buckets = %+v", q)
162 }
163 }
164
164 lines GO