Files
NianAIGC/tests/billing-postgres-mapping.test.ts

202 lines
6.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const { queryDatabase } = vi.hoisted(() => ({ queryDatabase: vi.fn() }));
vi.mock("@/lib/server/database", () => ({
getDataBackend: () => "postgres",
isPostgresBackend: () => true,
queryDatabase,
withDatabaseTransaction: vi.fn()
}));
import {
BillingStoreError,
createBillingPriceRule,
listBillingPriceRules,
postWalletEntry,
updateBillingPriceRule
} from "@/lib/server/billing-store";
describe("billing PostgreSQL mapping", () => {
beforeEach(() => queryDatabase.mockReset());
it("maps pg bigint, numeric, and timestamptz values safely", async () => {
queryDatabase.mockResolvedValueOnce({
rows: [{
id: "price-1",
provider: "bailian",
capability: "image.generate",
req_key: "image",
variant_key: null,
unit: "image",
standard_unit_price_fen: "9007199254740991",
markup_multiplier: "1.2500",
enabled: true,
conditions: {},
quantity_source: "image_count",
priority: 1,
note: null,
source: null,
parameter_dimensions: [],
created_at: new Date("2026-08-12T01:02:03.000Z"),
updated_at: new Date("2026-08-12T04:05:06.000Z")
}]
});
const [rule] = await listBillingPriceRules();
expect(rule).toMatchObject({
standardUnitPriceFen: Number.MAX_SAFE_INTEGER,
markupMultiplier: 1.25,
createdAt: "2026-08-12T01:02:03.000Z",
updatedAt: "2026-08-12T04:05:06.000Z"
});
expect(queryDatabase.mock.calls[0][0]).toContain("$1::boolean");
expect(queryDatabase.mock.calls[0][1]).toEqual([false]);
});
it("rejects bigint values that cannot be represented without precision loss", async () => {
queryDatabase.mockResolvedValueOnce({
rows: [{
id: "price-unsafe",
provider: "bailian",
capability: "image.generate",
unit: "image",
standard_unit_price_fen: "9007199254740992",
markup_multiplier: "1.1",
enabled: true,
conditions: {},
priority: 0,
parameter_dimensions: [],
created_at: new Date(),
updated_at: new Date()
}]
});
await expect(listBillingPriceRules()).rejects.toMatchObject({ status: 500 } satisfies Partial<BillingStoreError>);
});
it("canonicalizes nested condition objects and arrays before PostgreSQL inserts", async () => {
queryDatabase.mockResolvedValueOnce({
rows: [{
id: "price-canonical",
provider: "bailian",
capability: "image.generate",
req_key: "image",
variant_key: null,
unit: "image",
standard_unit_price_fen: "100",
markup_multiplier: "1.2",
enabled: true,
conditions: { quality: { values: ["high", "standard"] } },
quantity_source: "image_count",
priority: 0,
note: null,
source: null,
parameter_dimensions: [],
created_at: new Date("2026-08-12T01:00:00.000Z"),
updated_at: new Date("2026-08-12T01:00:00.000Z")
}]
});
await createBillingPriceRule({
id: "price-canonical",
provider: "bailian",
capability: "image.generate",
reqKey: "image",
unit: "image",
standardUnitPriceFen: 100,
markupMultiplier: 1.2,
enabled: true,
conditions: { quality: { values: ["standard", "high"] } },
quantitySource: "image_count"
});
expect(queryDatabase.mock.calls[0][1][9]).toBe(JSON.stringify({ quality: { values: ["high", "standard"] } }));
});
it("maps PostgreSQL unique violations during rule updates to conflict", async () => {
queryDatabase
.mockResolvedValueOnce({
rows: [{
id: "price-update",
provider: "bailian",
capability: "image.generate",
req_key: "image",
variant_key: null,
unit: "image",
standard_unit_price_fen: "100",
markup_multiplier: "1.2",
enabled: true,
conditions: {},
quantity_source: "image_count",
priority: 0,
note: null,
source: null,
parameter_dimensions: [],
created_at: new Date("2026-08-12T01:00:00.000Z"),
updated_at: new Date("2026-08-12T01:00:00.000Z")
}]
})
.mockRejectedValueOnce(Object.assign(new Error("duplicate key value violates unique constraint"), { code: "23505" }));
await expect(updateBillingPriceRule("price-update", {
conditions: { quality: { values: ["standard", "high"] } }
})).rejects.toMatchObject({ status: 409 } satisfies Partial<BillingStoreError>);
expect(queryDatabase.mock.calls[1][1][9]).toBe(JSON.stringify({ quality: { values: ["high", "standard"] } }));
});
it("calls the atomic wallet function with positional parameters and maps its row", async () => {
queryDatabase.mockResolvedValueOnce({
rows: [{
ledger_id: "ledger-1",
balance_after_fen: "1250",
balance_fen: "1250",
total_recharged_fen: "1500",
total_charged_fen: "250",
delta_fen: "-250",
created_at: new Date("2026-08-12T06:00:00.000Z"),
updated_at: new Date("2026-08-12T06:00:00.000Z")
}]
});
const result = await postWalletEntry({
organizationId: "org-1",
accountId: "account-1",
jobId: "job-1",
kind: "charge",
deltaFen: -250,
idempotencyKey: "charge:job-1",
description: "generation charge",
metadata: { provider: "bailian" }
});
expect(result.entry).toMatchObject({
id: "ledger-1",
deltaFen: -250,
balanceAfterFen: 1250,
createdAt: "2026-08-12T06:00:00.000Z"
});
expect(result.wallet).toMatchObject({ balanceFen: 1250, totalRechargedFen: 1500, totalChargedFen: 250 });
expect(queryDatabase.mock.calls[0][0]).toContain("billing_post_wallet_entry");
expect(queryDatabase.mock.calls[0][0]).toContain("$10::jsonb");
expect(queryDatabase.mock.calls[0][1].slice(1)).toEqual([
"org-1", "account-1", "job-1", "charge", -250, "CNY", "charge:job-1",
"generation charge", JSON.stringify({ provider: "bailian" })
]);
});
it("maps idempotency payload drift to a conflict", async () => {
queryDatabase.mockRejectedValueOnce(new Error("BILLING_IDEMPOTENCY_PAYLOAD_MISMATCH"));
await expect(postWalletEntry({
organizationId: "org-1",
kind: "recharge",
deltaFen: 100,
idempotencyKey: "same-key",
description: "request"
})).rejects.toMatchObject({ status: 409 } satisfies Partial<BillingStoreError>);
});
});