增加统一倍率调整按钮
This commit is contained in:
1 parent
be655b6569
commit
4195525dc7
23 files changed
+750
-16
No files matched your search
@@ -100,6 +100,33 @@ func (s *Store) UpdateBillingPriceRule(_ context.Context, id string, p billing.P
|
||||
r = cloneRule(r)
|
||||
return &r, nil
|
||||
}
|
||||
func (s *Store) UpdateBillingPriceRules(_ context.Context, patch billing.BulkPricePatch) ([]billing.PriceRule, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if len(patch.RuleIDs) == 0 {
|
||||
return nil, billing.ErrPriceNotFound
|
||||
}
|
||||
for _, id := range patch.RuleIDs {
|
||||
if _, ok := s.priceRules[id]; !ok {
|
||||
return nil, billing.ErrPriceNotFound
|
||||
}
|
||||
}
|
||||
now := s.now().UTC().Format(time.RFC3339Nano)
|
||||
out := make([]billing.PriceRule, 0, len(patch.RuleIDs))
|
||||
for _, id := range patch.RuleIDs {
|
||||
rule := cloneRule(s.priceRules[id])
|
||||
rule.MarkupMultiplier = patch.MarkupMultiplier
|
||||
for di := range rule.Dimensions {
|
||||
for ti := range rule.Dimensions[di].Tiers {
|
||||
rule.Dimensions[di].Tiers[ti].MarkupMultiplier = patch.MarkupMultiplier
|
||||
}
|
||||
}
|
||||
rule.UpdatedAt = now
|
||||
s.priceRules[id] = cloneRule(rule)
|
||||
out = append(out, cloneRule(rule))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (s *Store) SeedBillingPriceRules(_ context.Context, rules []billing.PriceRule) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package localstore_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/billing"
|
||||
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/localstore"
|
||||
)
|
||||
|
||||
func TestBulkPriceUpdateIsScopedAtomicAndSurvivesSingleEditAndReseed(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := localstore.New()
|
||||
defaults := []billing.PriceRule{
|
||||
{ID: "a", Provider: "a", Capability: "image.generate", Unit: billing.UnitImage, StandardUnitPriceFen: 20, MarkupMultiplier: 1.2, Enabled: true, Dimensions: []billing.ParameterDimension{{Key: "quality", Tiers: []billing.ParameterTier{{Value: "low", StandardFactor: 1, MarkupMultiplier: 1.2, Enabled: true}, {Value: "high", StandardFactor: 2, MarkupMultiplier: 1.2, Enabled: false}}}}},
|
||||
{ID: "b", Provider: "b", Capability: "image.generate", Unit: billing.UnitImage, StandardUnitPriceFen: 30, MarkupMultiplier: 1.2, Enabled: false, Dimensions: []billing.ParameterDimension{{Key: "quality", Tiers: []billing.ParameterTier{{Value: "low", StandardFactor: 1, MarkupMultiplier: 1.2, Enabled: true}}}}},
|
||||
{ID: "c", Provider: "c", Capability: "image.generate", Unit: billing.UnitImage, StandardUnitPriceFen: 40, MarkupMultiplier: 1.2, Enabled: true},
|
||||
}
|
||||
if err := store.SeedBillingPriceRules(ctx, defaults); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := store.UpdateBillingPriceRules(ctx, billing.BulkPricePatch{RuleIDs: []string{"a", "missing"}, MarkupMultiplier: 2}); !errors.Is(err, billing.ErrPriceNotFound) {
|
||||
t.Fatalf("missing ID error = %v", err)
|
||||
}
|
||||
if _, err := store.UpdateBillingPriceRules(ctx, billing.BulkPricePatch{MarkupMultiplier: 2}); !errors.Is(err, billing.ErrPriceNotFound) {
|
||||
t.Fatalf("empty ID error = %v", err)
|
||||
}
|
||||
before, _ := store.GetBillingPriceRule(ctx, "a")
|
||||
if before.MarkupMultiplier != 1.2 || before.Dimensions[0].Tiers[0].MarkupMultiplier != 1.2 {
|
||||
t.Fatalf("partial write on missing ID: %+v", before)
|
||||
}
|
||||
updated, err := store.UpdateBillingPriceRules(ctx, billing.BulkPricePatch{RuleIDs: []string{"a", "b"}, MarkupMultiplier: 1.8})
|
||||
if err != nil || len(updated) != 2 {
|
||||
t.Fatalf("bulk update = %+v, %v", updated, err)
|
||||
}
|
||||
for _, id := range []string{"a", "b"} {
|
||||
rule, _ := store.GetBillingPriceRule(ctx, id)
|
||||
if rule.MarkupMultiplier != 1.8 {
|
||||
t.Fatalf("%s rule markup = %g", id, rule.MarkupMultiplier)
|
||||
}
|
||||
for _, dimension := range rule.Dimensions {
|
||||
for _, tier := range dimension.Tiers {
|
||||
if tier.MarkupMultiplier != 1.8 {
|
||||
t.Fatalf("%s tier markup = %g", id, tier.MarkupMultiplier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
a, _ := store.GetBillingPriceRule(ctx, "a")
|
||||
if a.StandardUnitPriceFen != 20 || a.Dimensions[0].Tiers[1].Enabled || a.Dimensions[0].Tiers[1].StandardFactor != 2 {
|
||||
t.Fatalf("bulk update changed non-markup fields: %+v", a)
|
||||
}
|
||||
c, _ := store.GetBillingPriceRule(ctx, "c")
|
||||
if c.MarkupMultiplier != 1.2 {
|
||||
t.Fatalf("unselected rule changed: %+v", c)
|
||||
}
|
||||
if _, err := store.UpdateBillingPriceRule(ctx, "a", billing.PricePatch{DimensionKey: "quality", TierValue: "low", MarkupMultiplier: 2.3}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.SeedBillingPriceRules(ctx, defaults); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a, _ = store.GetBillingPriceRule(ctx, "a")
|
||||
b, _ := store.GetBillingPriceRule(ctx, "b")
|
||||
if a.MarkupMultiplier != 1.8 || a.Dimensions[0].Tiers[0].MarkupMultiplier != 2.3 || a.Dimensions[0].Tiers[1].MarkupMultiplier != 1.8 || b.MarkupMultiplier != 1.8 || b.Dimensions[0].Tiers[0].MarkupMultiplier != 1.8 {
|
||||
t.Fatalf("single edit or bulk update lost after reseed: a=%+v b=%+v", a, b)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package localstore_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/billing"
|
||||
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/localstore"
|
||||
)
|
||||
|
||||
func TestAdminPriceReadsRefreshOldCatalogWithoutLosingMarkups(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
disabled bool
|
||||
read func(context.Context, *billing.Service) ([]billing.PriceRule, error)
|
||||
}{
|
||||
{"overview", false, func(ctx context.Context, service *billing.Service) ([]billing.PriceRule, error) {
|
||||
overview, err := service.AdminOverview(ctx)
|
||||
return overview.PriceRules, err
|
||||
}},
|
||||
{"prices", false, func(ctx context.Context, service *billing.Service) ([]billing.PriceRule, error) {
|
||||
return service.ListPrices(ctx)
|
||||
}},
|
||||
{"overview billing disabled", true, func(ctx context.Context, service *billing.Service) ([]billing.PriceRule, error) {
|
||||
overview, err := service.AdminOverview(ctx)
|
||||
return overview.PriceRules, err
|
||||
}},
|
||||
{"prices billing disabled", true, func(ctx context.Context, service *billing.Service) ([]billing.PriceRule, error) {
|
||||
return service.ListPrices(ctx)
|
||||
}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := localstore.New()
|
||||
defaults := billing.DefaultBillingPriceRules()
|
||||
var old billing.PriceRule
|
||||
for _, rule := range defaults {
|
||||
if rule.ReqKey == "gpt-image-2" && rule.Provider == "evolink" {
|
||||
old = rule
|
||||
break
|
||||
}
|
||||
}
|
||||
if old.ID == "" {
|
||||
t.Fatal("Image 2 default rule missing")
|
||||
}
|
||||
if err := store.SeedBillingPriceRules(ctx, []billing.PriceRule{old}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, patch := range []billing.PricePatch{
|
||||
{DimensionKey: "quality", TierValue: "medium", MarkupMultiplier: 1.3},
|
||||
{DimensionKey: "referenceImageCount", TierValue: "1–4", MarkupMultiplier: 2},
|
||||
} {
|
||||
if _, err := store.UpdateBillingPriceRule(ctx, old.ID, patch); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
service := billing.NewService(store, nil).SetEnabled(!test.disabled)
|
||||
for read := 0; read < 2; read++ {
|
||||
rules, err := test.read(ctx, service)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rules) != len(defaults) {
|
||||
t.Fatalf("read %d: got %d rules, want %d", read, len(rules), len(defaults))
|
||||
}
|
||||
counts := map[string]int{}
|
||||
for _, rule := range rules {
|
||||
if rule.Provider != "evolink" || rule.Capability != "image.generate" {
|
||||
continue
|
||||
}
|
||||
counts[rule.ReqKey]++
|
||||
if rule.ReqKey == "gpt-image-2" {
|
||||
if got := priceTierMarkup(t, rule, "quality", "medium"); got != 1.3 {
|
||||
t.Fatalf("read %d: saved quality markup = %v, want 1.3", read, got)
|
||||
}
|
||||
if got := priceTierMarkup(t, rule, "referenceImageCount", "1–4"); got != 2 {
|
||||
t.Fatalf("read %d: saved reference markup = %v, want 2", read, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, model := range []string{"gpt-image-2", "gpt-image-2.5-flare", "gpt-image-2.5-sunburst"} {
|
||||
if counts[model] != 1 {
|
||||
t.Fatalf("read %d: %s rule count = %d, want 1", read, model, counts[model])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func priceTierMarkup(t *testing.T, rule billing.PriceRule, dimensionKey, tierValue string) float64 {
|
||||
t.Helper()
|
||||
for _, dimension := range rule.Dimensions {
|
||||
if dimension.Key != dimensionKey {
|
||||
continue
|
||||
}
|
||||
for _, tier := range dimension.Tiers {
|
||||
if value, ok := tier.Value.(string); ok && value == tierValue {
|
||||
return tier.MarkupMultiplier
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Fatalf("%s tier %s missing from %s", dimensionKey, tierValue, rule.ID)
|
||||
return 0
|
||||
}
|
||||
Reference in new issue
Block a user