文件
hyb-oa/server/tests/unit/routes/contractsRegistry.test.js
wangsli 1f8d5b658a 完善借款、编号、付款与预算并发及验收(09-06-release-gate-integrity)
汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。
统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。
纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。

关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。
预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。
完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
2026-09-19 13:11:04 +08:00

1067 行
41 KiB
JavaScript

const Fastify = require("fastify");
const contractsRegistryRoutes = require("../../../src/routes/contractsRegistry");
const { AppError } = require("../../../src/utils/AppError");
const { currentDateString, parseDateOnlyString } = require("../../../src/utils/date");
const ACTOR_ID = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
const PROJECT_ID = "11111111-1111-4111-8111-111111111111";
const OWNER_ID = "22222222-2222-4222-8222-222222222222";
const MANAGER_ID = "33333333-3333-4333-8333-333333333333";
const MEMBER_ID = "44444444-4444-4444-8444-444444444444";
const DEPARTMENT_ID = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
function buildFeatureFlagRecord(flagKey) {
return {
id: `flag-${flagKey}`,
flag_key: flagKey,
override_mode: "force_on",
rollout_type: "global",
department_ids: [],
note: "unit-test",
version: 1,
created_at: new Date("2026-04-04T08:00:00.000Z"),
created_by: null,
updated_at: new Date("2026-04-04T08:00:00.000Z"),
updated_by: null,
};
}
function buildContractRow(overrides = {}) {
return {
id: "55555555-5555-4555-8555-555555555551",
contract_no: "HT-2026-001",
project_id: PROJECT_ID,
contract_type: "service",
counterparty_name: "测试客户",
summary: "合同摘要",
amount: "100000.00",
currency: "CNY",
signed_date: new Date("2026-03-01T00:00:00.000Z"),
effective_date: new Date("2026-03-05T00:00:00.000Z"),
expiry_date: new Date("2026-03-31T00:00:00.000Z"),
confidentiality_level: "internal",
archive_location: "/archive/contracts/001",
offline_archive_no: "OFF-001",
owner_employee_id: OWNER_ID,
status: "active",
terminated_at: null,
terminated_reason: null,
terminated_by: null,
created_at: new Date("2026-03-01T08:00:00.000Z"),
updated_at: new Date("2026-04-01T08:00:00.000Z"),
owner: {
id: OWNER_ID,
employee_no: "E001",
name: "Owner",
},
project: {
id: PROJECT_ID,
project_no: "PRJ-001",
name: "试点项目",
},
terminator: null,
document_refs: [],
...overrides,
};
}
function mapSelectedRow(row, select) {
if (!select) {
return row;
}
return Object.entries(select).reduce((result, [key, enabled]) => {
if (enabled === true) {
result[key] = row[key];
}
return result;
}, {});
}
function cloneRow(row) {
return {
...row,
owner: row.owner ? { ...row.owner } : null,
project: row.project ? { ...row.project } : null,
terminator: row.terminator ? { ...row.terminator } : null,
document_refs: (row.document_refs ?? []).map((item) => ({
...item,
owner: item.owner ? { ...item.owner } : null,
})),
};
}
function findIdInWhere(where) {
if (!where || typeof where !== "object") return null;
if (typeof where.id === "string") return where.id;
for (const condition of where.AND ?? []) {
const id = findIdInWhere(condition);
if (id) return id;
}
return null;
}
function buildStatefulPrisma(initialContracts, options = {}) {
const state = {
contracts: initialContracts.map((item) => cloneRow(item)),
audits: [],
};
const auditCreate = vi.fn(async ({ data }) => {
if (options.failAuditAction === data.action) {
throw new Error("audit unavailable");
}
state.audits.push(data);
return data;
});
const contractRegistry = {
findMany: vi.fn(async ({ where, select, include }) => {
let rows = [...state.contracts];
if (where?.contract_no?.startsWith) {
rows = rows.filter((item) => item.contract_no.startsWith(where.contract_no.startsWith));
}
if (select) {
return rows.map((item) => mapSelectedRow(item, select));
}
if (include) {
return rows.map((item) => cloneRow(item));
}
return rows.map((item) => cloneRow(item));
}),
findUnique: vi.fn(async ({ where, select, include }) => {
const row = state.contracts.find((item) => item.id === where.id);
if (!row) {
return null;
}
if (select) {
return mapSelectedRow(row, select);
}
if (include) {
return cloneRow(row);
}
return cloneRow(row);
}),
findFirst: vi.fn(async ({ where, select, include }) => {
const id = findIdInWhere(where);
const row = id ? state.contracts.find((item) => item.id === id) : state.contracts[0];
if (!row) return null;
if (select) return mapSelectedRow(row, select);
return include ? cloneRow(row) : cloneRow(row);
}),
create: vi.fn(async ({ data, include }) => {
const created = buildContractRow({
id: `66666666-6666-4666-8666-${String(state.contracts.length + 1).padStart(12, "0")}`,
contract_no: data.contract_no,
project_id: data.project_id ?? null,
contract_type: data.contract_type,
counterparty_name: data.counterparty_name,
summary: data.summary,
amount: data.amount ?? null,
currency: data.currency,
signed_date: data.signed_date,
effective_date: data.effective_date,
expiry_date: data.expiry_date,
confidentiality_level: data.confidentiality_level,
archive_location: data.archive_location,
offline_archive_no: data.offline_archive_no,
owner_employee_id: data.owner_employee_id,
status: data.status,
terminated_at: data.terminated_at ?? null,
terminated_reason: data.terminated_reason ?? null,
terminated_by: data.terminated_by ?? null,
});
state.contracts.push(created);
return include ? cloneRow(created) : created;
}),
update: vi.fn(async ({ where, data, include }) => {
const index = state.contracts.findIndex((item) => item.id === where.id);
if (index < 0) {
return null;
}
state.contracts[index] = {
...state.contracts[index],
...data,
updated_at: new Date("2026-04-04T09:30:00.000Z"),
};
return include ? cloneRow(state.contracts[index]) : state.contracts[index];
}),
updateMany: vi.fn(async ({ where, data }) => {
const sameValue = (current, expected) =>
current instanceof Date && expected instanceof Date
? current.getTime() === expected.getTime()
: current === expected;
const index = state.contracts.findIndex((item) =>
Object.entries(where).every(([key, value]) => sameValue(item[key], value)),
);
if (index < 0) return { count: 0 };
state.contracts[index] = {
...state.contracts[index],
...data,
updated_at: new Date("2026-04-04T12:00:00.000Z"),
terminator: data.terminated_by
? { id: data.terminated_by, employee_no: "E-ACTOR", name: "Actor" }
: state.contracts[index].terminator,
};
return { count: 1 };
}),
};
return {
state,
prisma: {
featureFlag: {
findUnique: vi.fn().mockImplementation(async ({ where }) => buildFeatureFlagRecord(where.flag_key)),
},
auditLog: {
create: auditCreate,
findMany: vi.fn(async () =>
state.audits.map((item, index) => ({
id: `audit-${index + 1}`,
...item,
created_at: new Date(`2026-04-04T10:0${index}:00.000Z`),
actor: { id: ACTOR_ID, employee_no: "E-ACTOR", name: "Actor" },
})),
),
},
contractRegistry,
$transaction: async (callback) => {
const contractsBefore = state.contracts.map((item) => cloneRow(item));
const auditsBefore = [...state.audits];
try {
return await callback({
auditLog: { create: auditCreate },
contractRegistry,
});
} catch (error) {
state.contracts = contractsBefore;
state.audits = auditsBefore;
throw error;
}
},
},
auditCreate,
};
}
async function buildApp(options = {}) {
const app = Fastify();
const actor = options.actor ?? {
id: ACTOR_ID,
department_id: DEPARTMENT_ID,
permissionCodes: ["project.manage"],
};
const prisma =
options.prisma ??
(() => {
const fallback = {
featureFlag: {
findUnique: vi.fn().mockImplementation(async ({ where }) => buildFeatureFlagRecord(where.flag_key)),
},
auditLog: {
create: vi.fn().mockResolvedValue({}),
findMany: vi.fn().mockResolvedValue([]),
},
contractRegistry: {
findMany: vi.fn().mockResolvedValue([]),
findUnique: vi.fn().mockResolvedValue(null),
findFirst: vi.fn().mockResolvedValue(null),
create: vi.fn().mockResolvedValue(buildContractRow()),
update: vi.fn().mockResolvedValue(buildContractRow()),
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
},
};
fallback.$transaction = async (callback) => callback(fallback);
return fallback;
})();
app.decorate("authenticate", async (request) => {
request.employee = actor;
});
app.decorate("hasPermission", (employee, permissionCode) => {
const permissionCodes = employee.permissionCodes ?? [];
return permissionCodes.includes("*") || permissionCodes.includes(permissionCode);
});
app.decorate("config", {
appTimezone: "Asia/Shanghai",
projectLinkingEnabled: true,
});
app.decorate("prisma", prisma);
app.setErrorHandler((error, request, reply) => {
if (error instanceof AppError) {
return reply.status(error.statusCode).send({
requestId: request.id,
error: { code: error.code, message: error.message, details: error.details ?? {} },
});
}
return reply.status(500).send({
requestId: request.id,
error: { code: "SYS_001", message: "服务器内部错误", details: {} },
});
});
await app.register(contractsRegistryRoutes, { prefix: "/api" });
return { app, prisma };
}
describe("contracts registry routes", () => {
it("lists contracts with expired filtering and computed status", async () => {
const contractFindMany = vi.fn().mockResolvedValue([buildContractRow()]);
const { app } = await buildApp({
prisma: (() => {
const prisma = {
featureFlag: {
findUnique: vi.fn().mockImplementation(async ({ where }) => buildFeatureFlagRecord(where.flag_key)),
},
auditLog: { create: vi.fn().mockResolvedValue({}) },
contractRegistry: {
findMany: contractFindMany,
findUnique: vi.fn().mockResolvedValue(null),
create: vi.fn(),
update: vi.fn(),
},
};
prisma.$transaction = async (callback) => callback(prisma);
return prisma;
})(),
});
const today = parseDateOnlyString(currentDateString("Asia/Shanghai"));
const response = await app.inject({
method: "GET",
url: `/api/contracts-registry?status=expired&contractType=service&counterpartyName=%E6%B5%8B%E8%AF%95&expiryDateFrom=2026-03-01&expiryDateTo=2026-03-31`,
});
expect(response.statusCode).toBe(200);
const payload = response.json();
expect(payload.data.items[0]).toEqual(
expect.objectContaining({
status: "expired",
persistedStatus: "active",
contractType: "service",
amount: "100000.00",
}),
);
expect(payload.data.items[0]).not.toHaveProperty("terminatedAt");
expect(payload.data.items[0]).not.toHaveProperty("terminatedReason");
expect(payload.data.items[0]).not.toHaveProperty("terminatedBy");
expect(payload.data.items[0]).not.toHaveProperty("terminator");
expect(contractFindMany).toHaveBeenCalledWith(
expect.objectContaining({
where: {
AND: expect.arrayContaining([
expect.objectContaining({ status: "active", expiry_date: { lt: today } }),
expect.objectContaining({ contract_type: "service" }),
expect.objectContaining({
counterparty_name: expect.objectContaining({ contains: "测试", mode: "insensitive" }),
}),
expect.objectContaining({
expiry_date: expect.objectContaining({
gte: new Date("2026-03-01T00:00:00.000Z"),
lte: new Date("2026-03-31T00:00:00.000Z"),
}),
}),
]),
},
}),
);
await app.close();
});
it("keeps terminated contracts terminal even when their expiry date is in the past", async () => {
const contractFindMany = vi.fn().mockResolvedValue([
buildContractRow({
status: "terminated",
expiry_date: new Date("2026-03-31T00:00:00.000Z"),
terminated_at: new Date("2026-03-20T08:00:00.000Z"),
terminated_reason: "双方确认提前终止合同并保留原编号",
terminated_by: ACTOR_ID,
terminator: { id: ACTOR_ID, employee_no: "E-ACTOR", name: "Actor" },
}),
]);
const { app } = await buildApp({
prisma: {
featureFlag: {
findUnique: vi.fn().mockImplementation(async ({ where }) => buildFeatureFlagRecord(where.flag_key)),
},
auditLog: { create: vi.fn().mockResolvedValue({}) },
contractRegistry: {
findMany: contractFindMany,
findUnique: vi.fn().mockResolvedValue(null),
create: vi.fn(),
update: vi.fn(),
},
},
});
const response = await app.inject({ method: "GET", url: "/api/contracts-registry" });
expect(response.statusCode).toBe(200);
expect(response.json().data.items[0]).toEqual(expect.objectContaining({
status: "terminated",
persistedStatus: "terminated",
}));
await app.close();
});
it("rejects invalid expiry date range", async () => {
const { app } = await buildApp();
const response = await app.inject({
method: "GET",
url: "/api/contracts-registry?expiryDateFrom=2026-04-10&expiryDateTo=2026-04-01",
});
expect(response.statusCode).toBe(400);
expect(response.json().error).toEqual(
expect.objectContaining({
code: "VALID_001",
details: expect.objectContaining({
reasonCode: "contract_registry.expiry_date_range_invalid",
expiryDateTo: "must_be_after_or_equal_expiryDateFrom",
}),
}),
);
await app.close();
});
it("returns expiring contracts within the default window", async () => {
const contractFindMany = vi.fn().mockResolvedValue([
buildContractRow({
id: "contract-2",
contract_no: "HT-2026-002",
expiry_date: new Date("2026-04-10T00:00:00.000Z"),
}),
]);
const { app } = await buildApp({
prisma: (() => {
const prisma = {
featureFlag: {
findUnique: vi.fn().mockImplementation(async ({ where }) => buildFeatureFlagRecord(where.flag_key)),
},
auditLog: { create: vi.fn().mockResolvedValue({}) },
contractRegistry: {
findMany: contractFindMany,
findUnique: vi.fn().mockResolvedValue(null),
create: vi.fn(),
update: vi.fn(),
},
};
prisma.$transaction = async (callback) => callback(prisma);
return prisma;
})(),
});
const today = parseDateOnlyString(currentDateString("Asia/Shanghai"));
const end = new Date(today.getTime());
end.setUTCDate(end.getUTCDate() + 30);
const response = await app.inject({
method: "GET",
url: "/api/contracts-registry/expiring",
});
expect(response.statusCode).toBe(200);
expect(response.json().data).toEqual(
expect.objectContaining({
days: 30,
total: 1,
}),
);
expect(contractFindMany).toHaveBeenCalledWith(
expect.objectContaining({
where: {
AND: expect.arrayContaining([
{ status: "active" },
expect.objectContaining({
expiry_date: expect.objectContaining({
gte: today,
lte: end,
}),
}),
]),
},
}),
);
await app.close();
});
it("returns a scoped contract detail with safe document references and history", async () => {
const contractId = "55555555-5555-4555-8555-555555555551";
const stateful = buildStatefulPrisma([
buildContractRow({
id: contractId,
document_refs: [
{
id: "77777777-7777-4777-8777-777777777777",
title: "线下涉密资料索引",
document_no: "DOC-001",
confidentiality_level: "restricted",
storage_type: "offline_archive",
storage_path: "/sensitive/full/path",
offline_archive_no: "OFF-DOC-001",
access_rule_json: { raw: true },
created_at: new Date("2026-04-01T08:00:00.000Z"),
updated_at: new Date("2026-04-02T08:00:00.000Z"),
owner: { id: OWNER_ID, employee_no: "E001", name: "Owner" },
},
],
}),
]);
stateful.state.audits.push({
request_id: "request-1",
actor_id: ACTOR_ID,
action: "CONTRACT_REGISTRY_UPDATED",
target_type: "contract_registry",
target_id: contractId,
extra: { fields: ["summary"], rawPayload: "不得返回" },
});
const { app } = await buildApp({
prisma: stateful.prisma,
actor: {
id: OWNER_ID,
department_id: DEPARTMENT_ID,
permissionCodes: ["project.view_own"],
},
});
const response = await app.inject({ method: "GET", url: `/api/contracts-registry/${contractId}` });
expect(response.statusCode).toBe(200);
const item = response.json().data.item;
expect(item.documentRefs[0]).toEqual(
expect.objectContaining({
title: "线下涉密资料索引",
documentNo: "DOC-001",
storageType: "offline_archive",
offlineArchiveNo: "OFF-DOC-001",
}),
);
expect(item.documentRefs[0]).not.toHaveProperty("storagePath");
expect(item.documentRefs[0]).not.toHaveProperty("accessRuleJson");
expect(item.history[0]).toEqual(expect.objectContaining({ summary: "更新合同信息" }));
expect(item.history[0]).not.toHaveProperty("extra");
expect(stateful.prisma.contractRegistry.findFirst).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({ AND: expect.arrayContaining([{ id: contractId }]) }),
}),
);
await app.close();
});
it("terminates an active or expired contract with complete fields and required audit", async () => {
const contractId = "55555555-5555-4555-8555-555555555551";
const stateful = buildStatefulPrisma([buildContractRow({ id: contractId, status: "active" })]);
const { app } = await buildApp({ prisma: stateful.prisma });
const response = await app.inject({
method: "POST",
url: `/api/contracts-registry/${contractId}/terminate`,
payload: { reason: "客户项目已取消,双方确认不再继续履行" },
});
expect(response.statusCode).toBe(200);
expect(response.json().data.item).toEqual(
expect.objectContaining({
status: "terminated",
persistedStatus: "terminated",
terminatedReason: "客户项目已取消,双方确认不再继续履行",
terminatedBy: ACTOR_ID,
terminator: expect.objectContaining({ id: ACTOR_ID }),
}),
);
const stored = stateful.state.contracts.find((item) => item.id === contractId);
expect(stored).toEqual(
expect.objectContaining({
status: "terminated",
terminated_reason: "客户项目已取消,双方确认不再继续履行",
terminated_by: ACTOR_ID,
}),
);
expect(stored.terminated_at).toBeInstanceOf(Date);
expect(stateful.state.audits.map((item) => item.action)).toContain("CONTRACT_REGISTRY_TERMINATED");
const terminationAudit = stateful.state.audits.find((item) => item.action === "CONTRACT_REGISTRY_TERMINATED");
expect(terminationAudit?.extra).toEqual(expect.objectContaining({
reasonSummary: "客户项目已取消,双方确认不再继续履行",
}));
expect(terminationAudit?.extra).not.toHaveProperty("reason");
await app.close();
});
it("rolls back all termination fields when required audit creation fails", async () => {
const contractId = "55555555-5555-4555-8555-555555555551";
const stateful = buildStatefulPrisma([buildContractRow({ id: contractId })], {
failAuditAction: "CONTRACT_REGISTRY_TERMINATED",
});
const { app } = await buildApp({ prisma: stateful.prisma });
const response = await app.inject({
method: "POST",
url: `/api/contracts-registry/${contractId}/terminate`,
payload: { reason: "客户项目已取消,双方确认不再继续履行" },
});
expect(response.statusCode).toBe(500);
expect(stateful.state.contracts[0]).toEqual(
expect.objectContaining({
status: "active",
terminated_at: null,
terminated_reason: null,
terminated_by: null,
}),
);
expect(stateful.state.audits).toEqual([]);
await app.close();
});
it("rejects terminated through general PATCH and keeps terminated contracts final", async () => {
const contractId = "55555555-5555-4555-8555-555555555551";
const terminated = buildContractRow({
id: contractId,
status: "terminated",
terminated_at: new Date("2026-04-04T08:30:00.000Z"),
terminated_reason: "双方确认提前终止合同并保留原编号",
terminated_by: ACTOR_ID,
terminator: { id: ACTOR_ID, employee_no: "E-ACTOR", name: "Actor" },
});
const stateful = buildStatefulPrisma([terminated]);
const { app } = await buildApp({ prisma: stateful.prisma });
const bypassResponse = await app.inject({
method: "PATCH",
url: `/api/contracts-registry/${contractId}`,
payload: { status: "terminated" },
});
expect(bypassResponse.statusCode).toBe(400);
const editResponse = await app.inject({
method: "PATCH",
url: `/api/contracts-registry/${contractId}`,
payload: { summary: "试图覆盖已终止合同" },
});
expect(editResponse.statusCode).toBe(409);
expect(editResponse.json().error.details).toEqual(expect.objectContaining({ status: "terminated_is_final" }));
const renewResponse = await app.inject({
method: "POST",
url: `/api/contracts-registry/${contractId}/renew`,
});
expect(renewResponse.statusCode).toBe(409);
await app.close();
});
it("locks archived fields and appends corrections without overwriting the contract", async () => {
const contractId = "55555555-5555-4555-8555-555555555551";
const stateful = buildStatefulPrisma([buildContractRow({ id: contractId, status: "archived" })]);
const { app } = await buildApp({ prisma: stateful.prisma });
const editResponse = await app.inject({
method: "PATCH",
url: `/api/contracts-registry/${contractId}`,
payload: { archiveLocation: "新位置" },
});
expect(editResponse.statusCode).toBe(409);
expect(editResponse.json().error.details).toEqual(expect.objectContaining({ status: "archived_fields_locked" }));
const correctionResponse = await app.inject({
method: "POST",
url: `/api/contracts-registry/${contractId}/corrections`,
payload: { summary: "原归档位置登记有误,实际以线下档案号为准" },
});
expect(correctionResponse.statusCode).toBe(200);
expect(stateful.state.contracts[0].archive_location).toBe("/archive/contracts/001");
expect(stateful.state.audits).toEqual([
expect.objectContaining({
action: "CONTRACT_REGISTRY_CORRECTION_APPENDED",
extra: expect.objectContaining({ summary: "原归档位置登记有误,实际以线下档案号为准" }),
}),
]);
await app.close();
});
it("rejects a stale archived correction when the contract changed concurrently", async () => {
const contractId = "55555555-5555-4555-8555-555555555551";
const stateful = buildStatefulPrisma([buildContractRow({ id: contractId, status: "archived" })]);
stateful.prisma.contractRegistry.updateMany.mockResolvedValueOnce({ count: 0 });
const { app } = await buildApp({ prisma: stateful.prisma });
const response = await app.inject({
method: "POST",
url: `/api/contracts-registry/${contractId}/corrections`,
payload: { summary: "原归档位置登记有误,实际以线下档案号为准" },
});
expect(response.statusCode).toBe(409);
expect(response.json().error.details).toEqual(expect.objectContaining({ status: "correction_state_conflict" }));
expect(stateful.state.audits).toEqual([]);
await app.close();
});
it("rolls back an archive status change when required audit creation fails", async () => {
const contractId = "55555555-5555-4555-8555-555555555551";
const stateful = buildStatefulPrisma([buildContractRow({ id: contractId })], {
failAuditAction: "CONTRACT_REGISTRY_STATUS_CHANGED",
});
const { app } = await buildApp({ prisma: stateful.prisma });
const response = await app.inject({
method: "PATCH",
url: `/api/contracts-registry/${contractId}`,
payload: { status: "archived" },
});
expect(response.statusCode).toBe(500);
expect(stateful.state.contracts[0].status).toBe("active");
await app.close();
});
it("rolls back an archive status change when its history audit fails", async () => {
const contractId = "55555555-5555-4555-8555-555555555551";
const stateful = buildStatefulPrisma([buildContractRow({ id: contractId })], {
failAuditAction: "CONTRACT_REGISTRY_UPDATED",
});
const { app } = await buildApp({ prisma: stateful.prisma });
const response = await app.inject({
method: "PATCH",
url: `/api/contracts-registry/${contractId}`,
payload: { status: "archived" },
});
expect(response.statusCode).toBe(500);
expect(stateful.state.contracts[0].status).toBe("active");
expect(stateful.state.audits).toEqual([]);
await app.close();
});
it("fails closed when a contract row violates the termination field invariant", async () => {
const stateful = buildStatefulPrisma([
buildContractRow({
terminated_reason: "存在终止原因但状态尚未切换",
}),
]);
const { app } = await buildApp({ prisma: stateful.prisma });
const response = await app.inject({ method: "GET", url: "/api/contracts-registry" });
expect(response.statusCode).toBe(500);
expect(response.json().error).toEqual(expect.objectContaining({ code: "SYS_001" }));
await app.close();
});
it("rejects a stale general update instead of overwriting a concurrent terminal transition", async () => {
const contractId = "55555555-5555-4555-8555-555555555551";
const stateful = buildStatefulPrisma([buildContractRow({ id: contractId })]);
stateful.prisma.contractRegistry.updateMany.mockResolvedValueOnce({ count: 0 });
const { app } = await buildApp({ prisma: stateful.prisma });
const response = await app.inject({
method: "PATCH",
url: `/api/contracts-registry/${contractId}`,
payload: { summary: "并发状态变化后不得覆盖合同" },
});
expect(response.statusCode).toBe(409);
expect(response.json().error.details).toEqual(
expect.objectContaining({ status: "contract_state_conflict" }),
);
expect(stateful.state.contracts[0].summary).toBe("合同摘要");
await app.close();
});
it("uses the same non-enumerating 404 for an unavailable contract detail", async () => {
const { app } = await buildApp({
actor: { id: ACTOR_ID, department_id: DEPARTMENT_ID, permissionCodes: ["project.view_own"] },
});
const response = await app.inject({
method: "GET",
url: "/api/contracts-registry/55555555-5555-4555-8555-555555555551",
});
expect(response.statusCode).toBe(404);
expect(response.json().error).toEqual(expect.objectContaining({ code: "SYS_404", message: "Not Found" }));
expect(response.body).not.toContain("合同摘要");
await app.close();
});
it("renews an active contract into the next draft revision and archives the source", async () => {
const stateful = buildStatefulPrisma([
buildContractRow({
id: "55555555-5555-4555-8555-555555555551",
contract_no: "HT-2026-001",
status: "active",
}),
buildContractRow({
id: "55555555-5555-4555-8555-555555555552",
contract_no: "HT-2026-001-R1",
status: "archived",
}),
]);
const { app } = await buildApp({ prisma: stateful.prisma });
const response = await app.inject({
method: "POST",
url: "/api/contracts-registry/55555555-5555-4555-8555-555555555551/renew",
});
expect(response.statusCode).toBe(200);
expect(response.json().data).toEqual(
expect.objectContaining({
item: expect.objectContaining({
contractNo: "HT-2026-001-R2",
status: "draft",
persistedStatus: "draft",
}),
sourceItem: expect.objectContaining({
contractNo: "HT-2026-001",
status: "archived",
persistedStatus: "archived",
}),
}),
);
expect(stateful.state.contracts.find((item) => item.id === "55555555-5555-4555-8555-555555555551")?.status).toBe("archived");
expect(
stateful.state.contracts.some((item) => item.contract_no === "HT-2026-001-R2" && item.status === "draft"),
).toBe(true);
expect(stateful.state.audits.map((item) => item.action)).toEqual(
expect.arrayContaining(["CONTRACT_REGISTRY_RENEWED", "CONTRACT_REGISTRY_STATUS_CHANGED"]),
);
await app.close();
});
it("rolls back the renewal draft and source archive when renewal audit fails", async () => {
const sourceId = "55555555-5555-4555-8555-555555555551";
const stateful = buildStatefulPrisma([
buildContractRow({ id: sourceId, contract_no: "HT-2026-001", status: "active" }),
], {
failAuditAction: "CONTRACT_REGISTRY_RENEWED",
});
const { app } = await buildApp({ prisma: stateful.prisma });
const response = await app.inject({
method: "POST",
url: `/api/contracts-registry/${sourceId}/renew`,
});
expect(response.statusCode).toBe(500);
expect(stateful.state.contracts).toHaveLength(1);
expect(stateful.state.contracts[0]).toEqual(expect.objectContaining({ id: sourceId, status: "active" }));
expect(stateful.state.audits).toEqual([]);
await app.close();
});
it("rejects renewing a draft contract", async () => {
const stateful = buildStatefulPrisma([
buildContractRow({
id: "55555555-5555-4555-8555-555555555553",
contract_no: "HT-2026-003",
status: "draft",
}),
]);
const { app } = await buildApp({ prisma: stateful.prisma });
const response = await app.inject({
method: "POST",
url: "/api/contracts-registry/55555555-5555-4555-8555-555555555553/renew",
});
expect(response.statusCode).toBe(409);
expect(response.json().error).toEqual(
expect.objectContaining({
code: "VALID_001",
details: expect.objectContaining({ status: "renew_not_allowed" }),
}),
);
await app.close();
});
});
describe("contract calendar input", () => {
const body = { contractNo: "SYNTHETIC-CALENDAR", contractType: "service", counterpartyName: "合成对方", summary: "合成日期验证", archiveLocation: "synthetic-archive" };
const cases = ["POST", "PATCH"].flatMap((method) => ["signedDate", "effectiveDate", "expiryDate"].flatMap((field) => ["2026-02-29", "2026-04-31", "2026-13-01", ""].map((date) => [method, field, date])));
it.each(cases)("rejects %s %s=%s before writes", async (method, field, date) => {
const { app, prisma } = await buildApp();
const transaction = vi.spyOn(prisma, "$transaction");
try {
const response = await app.inject({ method, url: `/api/contracts-registry${method === "PATCH" ? "/55555555-5555-4555-8555-555555555551" : ""}`, payload: { ...(method === "POST" ? body : {}), [field]: date } });
expect(response.statusCode).toBe(400);
expect(response.json().error.details.fieldErrors[field].length).toBeGreaterThan(0);
expect(prisma.contractRegistry.create).not.toHaveBeenCalled();
expect(prisma.contractRegistry.updateMany).not.toHaveBeenCalled();
expect(prisma.auditLog.create).not.toHaveBeenCalled();
expect(transaction).not.toHaveBeenCalled();
} finally { await app.close(); }
});
it.each(["2024-02-29", "2026-04-30", null, undefined])("preserves create and patch nullable/optional dates: %s", async (date) => {
const { prisma, state } = buildStatefulPrisma([buildContractRow({ status: "draft" })]);
const { app } = await buildApp({ prisma });
try {
const dates = date === undefined ? {} : { signedDate: date, effectiveDate: date, expiryDate: date };
const created = await app.inject({ method: "POST", url: "/api/contracts-registry", payload: { ...body, ...dates } });
expect(created.statusCode).toBe(201);
const expected = date ? new Date(`${date}T00:00:00.000Z`) : null;
expect(prisma.contractRegistry.create.mock.calls[0][0].data).toMatchObject({ signed_date: expected, effective_date: expected, expiry_date: expected });
const patched = await app.inject({ method: "PATCH", url: `/api/contracts-registry/${state.contracts[0].id}`, payload: { summary: "合成修改", ...dates } });
expect(patched.statusCode).toBe(200);
expect(state.contracts[0].signed_date).toEqual(date === undefined ? new Date("2026-03-01T00:00:00.000Z") : expected);
} finally { await app.close(); }
});
it.each(["expiryDateFrom", "expiryDateTo"])("rejects invalid filter %s before contract reads", async (field) => {
const { app, prisma } = await buildApp();
try {
const response = await app.inject({ method: "GET", url: `/api/contracts-registry?${field}=2026-02-30` });
expect(response.statusCode).toBe(400);
expect(response.json().error).toMatchObject({ code: "VALID_001", details: { reasonCode: "contract_registry.query_invalid", formErrors: [], fieldErrors: { [field]: expect.arrayContaining(["invalid_calendar_date"]) } } });
expect(prisma.contractRegistry.findMany).not.toHaveBeenCalled();
expect(prisma.auditLog.create).not.toHaveBeenCalled();
} finally { await app.close(); }
});
});
describe("contract expiring query reasons", () => {
it.each(["days=0", "days=366", "days=1.5", "days=invalid", "unexpected=1"])("rejects %s before contract queries", async (query) => {
const { app, prisma } = await buildApp();
try {
const response = await app.inject({ method: "GET", url: `/api/contracts-registry/expiring?${query}` });
expect(response.statusCode).toBe(400);
expect(response.json().error).toMatchObject({ code: "VALID_001", details: { reasonCode: "contract_registry.expiring_query_invalid" } });
const details = response.json().error.details;
if (query.startsWith("days=")) expect(details.fieldErrors.days).toHaveLength(1);
else expect(details.formErrors).toHaveLength(1);
expect(prisma.contractRegistry.findMany).not.toHaveBeenCalled();
expect(prisma.auditLog.create).not.toHaveBeenCalled();
} finally { await app.close(); }
});
it.each([1, 365])("retains the legal %s day boundary", async (days) => {
const { app, prisma } = await buildApp();
try {
const response = await app.inject({ method: "GET", url: `/api/contracts-registry/expiring?days=${days}` });
expect(response.statusCode).toBe(200);
expect(prisma.contractRegistry.findMany).toHaveBeenCalledOnce();
} finally { await app.close(); }
});
});
describe("contract write error reasons", () => {
const id = "55555555-5555-4555-8555-555555555551";
it.each([
["POST", "", {}, "create"],
["PATCH", `/${id}`, {}, "update"],
["POST", `/${id}/terminate`, { reason: "short" }, "terminate"],
["POST", `/${id}/corrections`, { summary: "short" }, "correction"],
])("explains %s %s input before business access", async (method, path, payload, operation) => {
const { app, prisma } = await buildApp();
const transaction = vi.spyOn(prisma, "$transaction");
try {
const response = await app.inject({ method, url: `/api/contracts-registry${path}`, payload });
expect(response.statusCode).toBe(400);
expect(response.json().error).toMatchObject({ code: "VALID_001", details: { reasonCode: `contract_registry.${operation}_input_invalid`, fieldErrors: expect.any(Object), formErrors: expect.any(Array) } });
for (const fn of Object.values(prisma.contractRegistry)) expect(fn).not.toHaveBeenCalled();
expect(prisma.auditLog.create).not.toHaveBeenCalled();
expect(transaction).not.toHaveBeenCalled();
} finally { await app.close(); }
});
const terminated = { status: "terminated", terminated_at: new Date("2026-04-04T08:30:00.000Z"), terminated_reason: "合成终止原因用于测试", terminated_by: ACTOR_ID };
it.each([
["PATCH", "", terminated, { summary: "合成修改" }, "terminated_is_final"],
["PATCH", "", { status: "archived" }, { summary: "合成修改" }, "archived_fields_locked"],
["POST", "/terminate", { status: "draft" }, { reason: "合成终止原因满足原长度要求" }, "terminate_not_allowed"],
["POST", "/corrections", { status: "active" }, { summary: "合成纠错说明满足原长度要求" }, "correction_requires_archived"],
["POST", "/renew", { status: "draft" }, undefined, "renew_not_allowed"],
])("explains %s %s state guard %s", async (method, suffix, row, payload, reason) => {
const stateful = buildStatefulPrisma([buildContractRow(row)]);
const { app, prisma } = await buildApp({ prisma: stateful.prisma });
try {
const response = await app.inject({ method, url: `/api/contracts-registry/${id}${suffix}`, payload });
expect(response.statusCode).toBe(409);
expect(response.json().error).toMatchObject({ code: "VALID_001", details: { status: reason, reasonCode: `contract_registry.${reason}` } });
expect(prisma.contractRegistry.create).not.toHaveBeenCalled();
expect(prisma.contractRegistry.updateMany).not.toHaveBeenCalled();
expect(prisma.auditLog.create).not.toHaveBeenCalled();
expect(stateful.state.contracts).toHaveLength(1);
} finally { await app.close(); }
});
it.each([
["PATCH", "", "active", { summary: "合成修改" }, "contract_state_conflict", "state_conflict"],
["POST", "/terminate", "active", { reason: "合成终止原因满足原长度要求" }, "terminate_conflict", "terminate_conflict"],
["POST", "/corrections", "archived", { summary: "合成纠错说明满足原长度要求" }, "correction_state_conflict", "correction_state_conflict"],
])("explains optimistic conflict for %s %s without success audit", async (method, suffix, status, payload, detail, reason) => {
const stateful = buildStatefulPrisma([buildContractRow({ status })]);
stateful.prisma.contractRegistry.updateMany.mockResolvedValueOnce({ count: 0 });
const { app, prisma } = await buildApp({ prisma: stateful.prisma });
try {
const response = await app.inject({ method, url: `/api/contracts-registry/${id}${suffix}`, payload });
expect(response.statusCode).toBe(409);
expect(response.json().error).toMatchObject({ code: "VALID_001", details: { status: detail, reasonCode: `contract_registry.${reason}` } });
expect(prisma.contractRegistry.updateMany).toHaveBeenCalledOnce();
expect(prisma.contractRegistry.create).not.toHaveBeenCalled();
expect(prisma.auditLog.create).not.toHaveBeenCalled();
expect(stateful.state.contracts[0].status).toBe(status);
} finally { await app.close(); }
});
it("retains renewal's attempted draft before source-version conflict", async () => {
const stateful = buildStatefulPrisma([buildContractRow()]);
stateful.prisma.contractRegistry.updateMany.mockResolvedValueOnce({ count: 0 });
const { app, prisma } = await buildApp({ prisma: stateful.prisma });
try {
const response = await app.inject({ method: "POST", url: `/api/contracts-registry/${id}/renew` });
expect(response.statusCode).toBe(409);
expect(response.json().error.details).toEqual({ status: "contract_state_conflict", reasonCode: "contract_registry.state_conflict" });
expect(prisma.contractRegistry.create).toHaveBeenCalledOnce();
expect(prisma.auditLog.create).not.toHaveBeenCalled();
// This fixture restores its own memory; it does not prove a PostgreSQL rollback.
expect(stateful.state.contracts).toHaveLength(1);
} finally { await app.close(); }
});
it("keeps feature denial before business access", async () => {
const { app, prisma } = await buildApp();
prisma.featureFlag.findUnique.mockImplementation(async ({ where }) => ({ ...buildFeatureFlagRecord(where.flag_key), override_mode: "force_off" }));
try {
const response = await app.inject({ method: "GET", url: "/api/contracts-registry" });
expect(response.statusCode).toBe(409);
expect(response.json().error).toMatchObject({ code: "VALID_001", details: { reasonCode: "contract_registry.feature_disabled", featureFlag: "project_linking_v1_disabled", reason: expect.any(String) } });
for (const fn of Object.values(prisma.contractRegistry)) expect(fn).not.toHaveBeenCalled();
expect(prisma.auditLog.create).not.toHaveBeenCalled();
} finally { await app.close(); }
});
it("preserves management permission ahead of malformed input", async () => {
const { app, prisma } = await buildApp({ actor: { id: ACTOR_ID, permissionCodes: [] } });
try {
const response = await app.inject({ method: "POST", url: "/api/contracts-registry", payload: {} });
expect(response.statusCode).toBe(403);
expect(response.json().error.code).toBe("PERM_001");
expect(prisma.contractRegistry.create).not.toHaveBeenCalled();
} finally { await app.close(); }
});
});