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); }); 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); 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); }); });