Files
NianAIGC/backend/internal/billing/service_test.go

136 lines
5.6 KiB
Go

package billing
import (
"context"
"reflect"
"testing"
)
func TestSummarizeUsesFenAndDoesNotProduceNegativeConsumption(t *testing.T) {
got := Summarize([]LedgerEntry{{Kind: "recharge", DeltaFen: 1000}, {Kind: "adjustment", DeltaFen: 100}, {Kind: "charge", DeltaFen: -400}, {Kind: "refund", DeltaFen: 500}})
if got.RechargeFen != 1100 || got.ChargedFen != 400 || got.RefundedFen != 500 || got.NetConsumedFen != 0 {
t.Fatalf("summary=%+v", got)
}
}
func TestNormalizeBillingParametersUsesGenerationPayloadShapes(t *testing.T) {
payload := map[string]any{
"settings": map[string]any{"resolution": " 1080P ", "ratio": "16:9", "duration": 5.2},
"providerPayload": map[string]any{"parameters": map[string]any{"n": 1.2}, "width": 1024.0, "height": 768.0},
"imageUrls": []any{"a", "b"},
}
got := NormalizeBillingParameters(payload, Parameters{"resolution": "480p", "callerOnly": true})
want := Parameters{"resolution": "1080p", "size": "1024*768", "aspectRatio": "16:9", "duration": 5.2, "imageCount": 2.0, "referenceImageCount": 2.0, "callerOnly": true}
if !reflect.DeepEqual(got, want) {
t.Fatalf("parameters = %#v, want %#v", got, want)
}
}
func TestServiceQuoteNormalizesPayloadAndSeedsDefaultsThroughOptionalCapability(t *testing.T) {
store := &quoteStoreStub{rules: []PriceRule{{ID: "720", Provider: "seedance", Capability: "video.generate", ReqKey: "seedance-2", VariantKey: "resolution=720p", Unit: UnitVideoSecond, StandardUnitPriceFen: 99, MarkupMultiplier: 1.2, Enabled: true}}}
quote, err := NewService(store, nil).Quote(context.Background(), QuoteCommand{
Provider: "seedance", Capability: "video.generate", ReqKey: "seedance-2",
Parameters: Parameters{"resolution": "1080p"},
Payload: map[string]any{"settings": map[string]any{"resolution": "720P", "duration": 5.0}},
})
if err != nil {
t.Fatal(err)
}
if store.seedCalls != 1 || len(store.seeded) == 0 {
t.Fatalf("seed calls = %d, rules = %d", store.seedCalls, len(store.seeded))
}
if quote.PriceRuleID != "720" || quote.Quantity != 5 || quote.Parameters["resolution"] != "720p" {
t.Fatalf("quote = %#v", quote)
}
}
func TestServiceQuoteDisabledReturnsNilWithoutAccessingRules(t *testing.T) {
store := &quoteStoreStub{failOnAccess: true}
quote, err := NewService(store, nil).SetEnabled(false).Quote(context.Background(), QuoteCommand{Provider: "seedance", Capability: "video.generate"})
if err != nil || quote != nil {
t.Fatalf("quote = %#v, err = %v", quote, err)
}
if store.seedCalls != 0 || store.listCalls != 0 {
t.Fatalf("seed calls = %d, list calls = %d", store.seedCalls, store.listCalls)
}
}
func TestServiceQuoteFreezesConservativeSeedanceReserve(t *testing.T) {
store := &quoteStoreStub{rules: []PriceRule{{
ID: "seedance-720", Provider: "seedance", Capability: "video.generate",
ReqKey: "doubao-seedance-2-0-260128", VariantKey: "resolution=720p",
Unit: UnitVideoSecond, StandardUnitPriceFen: 99, MarkupMultiplier: 1.2, Enabled: true,
}}}
quote, err := NewService(store, nil).Quote(context.Background(), QuoteCommand{
Provider: "seedance", Capability: "video.generate", ReqKey: "doubao-seedance-2-0-260128",
Payload: map[string]any{
"settings": map[string]any{"duration": 5.0, "resolution": "720p", "ratio": "9:16"},
"promptAssembly": map[string]any{"materials": []any{map[string]any{
"type": "video", "url": "https://example.test/reference.mp4",
}}},
},
})
if err != nil {
t.Fatal(err)
}
if quote.AmountFen != 1452 || quote.ReservedAmountFen != 1452 || quote.SettlementStatus != "pending" || quote.Parameters["inputVideo"] != true {
t.Fatalf("quote = %#v", quote)
}
}
func TestSeedanceEstimateMatchesExecutableTypeScriptVectors(t *testing.T) {
withoutVideo, err := EstimateSeedanceAmountFen(SeedanceEstimateInput{
Resolution: "720p", AspectRatio: "16:9", OutputDurationSeconds: 5, MarkupMultiplier: 1.2,
})
if err != nil || withoutVideo != 597 {
t.Fatalf("without video = %d, %v", withoutVideo, err)
}
withUnknownVideo, err := EstimateSeedanceAmountFen(SeedanceEstimateInput{
Resolution: "720p", AspectRatio: "9:16", OutputDurationSeconds: 5,
InputVideo: true, MarkupMultiplier: 1.2,
})
if err != nil || withUnknownVideo != 1452 {
t.Fatalf("with video = %d, %v", withUnknownVideo, err)
}
}
type quoteStoreStub struct {
rules []PriceRule
seeded []PriceRule
seedCalls int
listCalls int
failOnAccess bool
}
func (s *quoteStoreStub) SeedBillingPriceRules(_ context.Context, rules []PriceRule) error {
s.seedCalls++
s.seeded = rules
return nil
}
func (s *quoteStoreStub) ListBillingPriceRules(context.Context, bool) ([]PriceRule, error) {
s.listCalls++
if s.failOnAccess {
panic("disabled billing accessed price rules")
}
return s.rules, nil
}
func (*quoteStoreStub) BillingWallet(context.Context, string) (Wallet, error) { return Wallet{}, nil }
func (*quoteStoreStub) BillingWallets(context.Context) ([]Wallet, error) { return nil, nil }
func (*quoteStoreStub) BillingLedger(context.Context, string, string, int) ([]LedgerEntry, error) {
return nil, nil
}
func (*quoteStoreStub) BillingOrganizations(context.Context) ([]Organization, error) { return nil, nil }
func (*quoteStoreStub) BillingMembers(context.Context) ([]Member, error) { return nil, nil }
func (*quoteStoreStub) BillingOrganizationExists(context.Context, string) (bool, error) {
return false, nil
}
func (*quoteStoreStub) GetBillingPriceRule(context.Context, string) (*PriceRule, error) {
return nil, nil
}
func (*quoteStoreStub) UpdateBillingPriceRule(context.Context, string, PricePatch) (*PriceRule, error) {
return nil, nil
}
func (*quoteStoreStub) PostBillingWalletEntry(context.Context, WalletPostParams) (WalletPosting, error) {
return WalletPosting{}, nil
}