Files
NianAIGC/tests/billing-catalog-seed.test.ts
T
2026-10-02 19:56:32 +08:00

124 lines
6.1 KiB
TypeScript

import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_BILLING_PRICE_RULES, ensureDefaultBillingPriceRules } from "@/lib/server/billing-catalog";
import { quoteGenerationCharge, settleMinimaxGenerationCharge } from "@/lib/server/billing-service";
import { createBillingPriceRule, getBillingPriceRule, updateBillingPriceTierMultiplier } from "@/lib/server/billing-store";
import { createGenerationJob } from "@/lib/server/data-store";
let runtimeDir = "";
describe("default billing catalog refresh", () => {
beforeEach(async () => {
runtimeDir = await mkdtemp(join(tmpdir(), "zhinian-billing-seed-"));
vi.stubEnv("ZHINIAN_RUNTIME_DIR", runtimeDir);
vi.stubEnv("ZHINIAN_DATA_BACKEND", "local");
});
afterEach(async () => {
vi.unstubAllEnvs();
await rm(runtimeDir, { recursive: true, force: true });
});
it("keeps a saved tier multiplier across catalog refresh and quotation", async () => {
await ensureDefaultBillingPriceRules();
await updateBillingPriceTierMultiplier({
ruleId: "base-seedream-5-0-pro",
dimensionKey: "size",
tierValue: "1.5K",
markupMultiplier: 1.3
});
const quote = await quoteGenerationCharge({
provider: "seedream",
capability: "image.generate",
reqKey: "doubao-seedream-5-0-pro-260628",
requestPayload: { settings: { size: "1.5K" } },
usageContext: { source: "platform", accountId: "user-1", displayName: "测试用户", organizationId: "org-1" }
});
expect(quote).toMatchObject({ standardUnitPriceFen: 30, markupMultiplier: 1.3, amountFen: 39 });
const saved = await getBillingPriceRule("base-seedream-5-0-pro");
const tiers = saved?.parameterDimensions?.find((dimension) => dimension.key === "size")?.tiers;
expect(tiers?.find((tier) => tier.value === "1.5K")?.markupMultiplier).toBe(1.3);
expect(tiers?.find((tier) => tier.value === "2K")?.markupMultiplier).toBe(1.2);
});
it("quotes MiniMax nine references and keeps Wan 3.0 resolution pricing independent", async () => {
const context = { source: "platform" as const, accountId: "user-1", displayName: "测试用户", organizationId: "org-1" };
const minimax = await quoteGenerationCharge({
provider: "minimax", capability: "video.generate", reqKey: "MiniMax-H3",
requestPayload: {
settings: { duration: 5, resolution: "2K" },
materials: Array.from({ length: 9 }, (_, index) => ({ type: "image", url: `https://cdn.test/${index}.png` }))
},
usageContext: context
});
expect(minimax).toMatchObject({ amountFen: 576, reservedAmountFen: 576, settlementStatus: "pending" });
expect(minimax?.parameters).toMatchObject({ inputImageCount: 9, inputImagePriceFen: 80 });
const wan3 = await quoteGenerationCharge({
provider: "bailian", capability: "video.generate", reqKey: "wan3.0-video",
requestPayload: { settings: { duration: 5, resolution: "1080P" } }, usageContext: context
});
expect(wan3).toMatchObject({
priceRuleId: "base-bailian-wan30-video-1080p", amountFen: 720,
reservedAmountFen: 720, settlementStatus: "pending"
});
});
it("retains quoted MiniMax image fees only when provider omits image usage", async () => {
const materials = Array.from({ length: 9 }, (_, index) => ({ type: "image", url: `https://cdn.test/${index}.png` }));
const makeJob = () => createGenerationJob({
ownerId: "user-1", capability: "video.generate", provider: "minimax", reqKey: "MiniMax-H3", status: "succeeded",
inputAssetIds: [], inputUrls: [], outputAssetIds: [], requestPayload: { materials },
usageContext: { source: "platform", accountId: "user-1", displayName: "管理员", role: "super_admin" },
billing: {
priceRuleId: "base-minimax-h3-2k", provider: "minimax", capability: "video.generate", reqKey: "MiniMax-H3",
unit: "video_second", quantity: 5, standardUnitPriceFen: 80, markupMultiplier: 1.2,
amountFen: 576, reservedAmountFen: 576, currency: "CNY", status: "not_charged", quotaExempt: true,
settlementStatus: "pending", parameters: { resolution: "2k", inputImageCount: 9 }
}
});
const missing = await settleMinimaxGenerationCharge(await makeJob(), { outputSeconds: 5, inputSeconds: 0 });
expect(missing?.billing).toMatchObject({ amountFen: 576, settlementStatus: "settled", providerUsage: { inputImageCount: 9 } });
const explicitZero = await settleMinimaxGenerationCharge(await makeJob(), { outputSeconds: 5, inputSeconds: 0, inputImageCount: 0 });
expect(explicitZero?.billing).toMatchObject({ amountFen: 480, settlementStatus: "settled", providerUsage: { inputImageCount: 0 } });
});
it("takes current default definitions while retaining valid matching tier markups", async () => {
const candidate = DEFAULT_BILLING_PRICE_RULES.find((rule) => rule.id === "base-seedream-5-0-pro")!;
const size = candidate.parameterDimensions!.find((dimension) => dimension.key === "size")!;
await createBillingPriceRule({
...candidate,
standardUnitPriceFen: 5,
markupMultiplier: 1.7,
parameterDimensions: [{
...size,
label: "旧分辨率",
tiers: [
{ ...size.tiers[0], standardFactor: 99, markupMultiplier: 0 },
{ ...size.tiers[1], standardFactor: 99, markupMultiplier: 1.4 },
{ value: "3K", label: "旧档位", standardFactor: 9, markupMultiplier: 3, enabled: true }
]
}]
});
await ensureDefaultBillingPriceRules();
const saved = await getBillingPriceRule(candidate.id!);
expect(saved?.standardUnitPriceFen).toBe(30);
expect(saved?.markupMultiplier).toBe(1.7);
const dimension = saved?.parameterDimensions?.find((item) => item.key === "size");
expect(dimension?.label).toBe("分辨率");
expect(dimension?.tiers.map((tier) => tier.value)).toEqual(["1K", "1.5K", "2K"]);
expect(dimension?.tiers.map((tier) => [tier.standardFactor, tier.markupMultiplier])).toEqual([
[1, 1.2],
[1, 1.4],
[2, 1.2]
]);
});
});