fix: preserve billing response shape
This commit is contained in:
147
tests/billing-api-contract.test.ts
Normal file
147
tests/billing-api-contract.test.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseAdminBillingPayload, parseMemberBillingPayload } from "@/lib/client/billing-api";
|
||||
|
||||
const wallet = {
|
||||
organizationId: "org-1",
|
||||
balanceFen: 0,
|
||||
totalRechargedFen: 0,
|
||||
totalChargedFen: 0,
|
||||
updatedAt: "2026-08-17T00:00:00.000Z"
|
||||
};
|
||||
|
||||
const ledgerEntry = {
|
||||
id: "ledger-1",
|
||||
organizationId: "org-1",
|
||||
kind: "recharge",
|
||||
deltaFen: 100,
|
||||
balanceAfterFen: 100,
|
||||
description: "充值",
|
||||
metadata: {},
|
||||
createdAt: "2026-08-17T00:00:00.000Z"
|
||||
};
|
||||
|
||||
function validAdminPayload() {
|
||||
return {
|
||||
billingAccount: {},
|
||||
organizations: [],
|
||||
members: [],
|
||||
ledger: [],
|
||||
priceRules: []
|
||||
};
|
||||
}
|
||||
|
||||
function validMemberPayload() {
|
||||
return {
|
||||
organization: { id: "org-1", name: "测试组织" },
|
||||
billingAccount: {},
|
||||
wallet,
|
||||
ledger: [],
|
||||
summary: { rechargeFen: 0, chargedFen: 0, refundedFen: 0, netConsumedFen: 0 },
|
||||
personal: { rechargeFen: 0, chargedFen: 0, refundedFen: 0, netConsumedFen: 0 }
|
||||
};
|
||||
}
|
||||
|
||||
describe("billing API payload contract", () => {
|
||||
it("accepts valid empty collection payloads", () => {
|
||||
const admin = validAdminPayload();
|
||||
const member = validMemberPayload();
|
||||
|
||||
expect(parseAdminBillingPayload(admin)).toBe(admin);
|
||||
expect(parseMemberBillingPayload(member)).toBe(member);
|
||||
});
|
||||
|
||||
it.each(["organizations", "members", "ledger", "priceRules"])(
|
||||
"rejects a missing admin %s collection",
|
||||
(field) => {
|
||||
const payload: Record<string, unknown> = validAdminPayload();
|
||||
delete payload[field];
|
||||
|
||||
expect(() => parseAdminBillingPayload(payload))
|
||||
.toThrow(`超管计费数据格式错误:${field} 必须是数组。`);
|
||||
}
|
||||
);
|
||||
|
||||
it("rejects a malformed admin price-rule element", () => {
|
||||
expect(() => parseAdminBillingPayload({ ...validAdminPayload(), priceRules: [null] }))
|
||||
.toThrow("超管计费数据格式错误:priceRules[0] 必须是对象。");
|
||||
});
|
||||
|
||||
it("rejects malformed admin organization elements and wallets", () => {
|
||||
expect(() => parseAdminBillingPayload({ ...validAdminPayload(), organizations: [null] }))
|
||||
.toThrow("超管计费数据格式错误:organizations[0] 必须是对象。");
|
||||
expect(() => parseAdminBillingPayload({
|
||||
...validAdminPayload(),
|
||||
organizations: [{ id: "org-1", name: "测试组织", status: "active", wallet: { ...wallet, balanceFen: "0" } }]
|
||||
})).toThrow("超管计费数据格式错误:organizations[0].wallet.balanceFen 必须是有限数字。");
|
||||
});
|
||||
|
||||
it("rejects a malformed admin member element", () => {
|
||||
expect(() => parseAdminBillingPayload({ ...validAdminPayload(), members: [null] }))
|
||||
.toThrow("超管计费数据格式错误:members[0] 必须是对象。");
|
||||
});
|
||||
|
||||
it("accepts a fresh super administrator without organizationId", () => {
|
||||
const payload = {
|
||||
...validAdminPayload(),
|
||||
members: [{ id: "root", displayName: "管理员", phone: "13800000000", role: "super_admin", status: "active" }]
|
||||
};
|
||||
|
||||
expect(parseAdminBillingPayload(payload)).toBe(payload);
|
||||
});
|
||||
|
||||
it("rejects a malformed admin ledger element", () => {
|
||||
expect(() => parseAdminBillingPayload({ ...validAdminPayload(), ledger: [null] }))
|
||||
.toThrow("超管计费数据格式错误:ledger[0] 必须是对象。");
|
||||
});
|
||||
|
||||
it("rejects malformed member objects used by the renderer", () => {
|
||||
expect(() => parseMemberBillingPayload({ ...validMemberPayload(), organization: null }))
|
||||
.toThrow("成员计费数据格式错误:organization 必须是对象。");
|
||||
expect(() => parseMemberBillingPayload({ ...validMemberPayload(), wallet: null }))
|
||||
.toThrow("成员计费数据格式错误:wallet 必须是对象。");
|
||||
expect(() => parseMemberBillingPayload({ ...validMemberPayload(), summary: null }))
|
||||
.toThrow("成员计费数据格式错误:summary 必须是对象。");
|
||||
expect(() => parseMemberBillingPayload({ ...validMemberPayload(), personal: null }))
|
||||
.toThrow("成员计费数据格式错误:personal 必须是对象。");
|
||||
});
|
||||
|
||||
it("rejects a malformed member ledger element", () => {
|
||||
expect(() => parseMemberBillingPayload({ ...validMemberPayload(), ledger: [null] }))
|
||||
.toThrow("成员计费数据格式错误:ledger[0] 必须是对象。");
|
||||
});
|
||||
|
||||
it("accepts fully populated collection elements", () => {
|
||||
const admin = {
|
||||
...validAdminPayload(),
|
||||
organizations: [{ id: "org-1", name: "测试组织", status: "active", wallet }],
|
||||
members: [{ id: "member-1", displayName: "成员", phone: "13800000000", role: "user", organizationId: "org-1", status: "active" }],
|
||||
ledger: [ledgerEntry],
|
||||
priceRules: [{
|
||||
id: "rule-1",
|
||||
provider: "evolink",
|
||||
capability: "image.generate",
|
||||
unit: "image",
|
||||
standardUnitPriceFen: 100,
|
||||
markupMultiplier: 1.2,
|
||||
enabled: true,
|
||||
parameterDimensions: [{
|
||||
key: "quality",
|
||||
label: "质量",
|
||||
baselineValue: "standard",
|
||||
tiers: [{ value: "standard", label: "标准", standardFactor: 1, markupMultiplier: 1.2, enabled: true }]
|
||||
}]
|
||||
}]
|
||||
};
|
||||
const member = { ...validMemberPayload(), ledger: [ledgerEntry] };
|
||||
|
||||
expect(parseAdminBillingPayload(admin)).toBe(admin);
|
||||
expect(parseMemberBillingPayload(member)).toBe(member);
|
||||
});
|
||||
|
||||
it("rejects non-object responses", () => {
|
||||
expect(() => parseAdminBillingPayload("unexpected"))
|
||||
.toThrow("超管计费数据格式错误:响应必须是对象。");
|
||||
expect(() => parseMemberBillingPayload(null))
|
||||
.toThrow("成员计费数据格式错误:响应必须是对象。");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user