202 lines
6.5 KiB
TypeScript
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>);
|
|
});
|
|
});
|