完善借款、编号、付款与预算并发及验收(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项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
这个提交包含在:
@@ -228,7 +228,7 @@ async function buildApp(options = {}) {
|
||||
const actor = options.actor ?? buildEmployee({ id: EMPLOYEE_ID, permissionCodes: ["expense.view_own"] });
|
||||
const app = Fastify({ logger: false });
|
||||
|
||||
app.decorate("config", { announcementsEnabled: true });
|
||||
app.decorate("config", { announcementsEnabled: options.enabled ?? true });
|
||||
app.decorate("prisma", state.prisma);
|
||||
app.decorate("hasPermission", (employee, permissionCode) => (employee?.permissionCodes ?? []).includes(permissionCode));
|
||||
app.decorate("assertPermission", (employee, permissionCode) => {
|
||||
@@ -283,6 +283,7 @@ describe("announcements policy acknowledgement routes", () => {
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.json().error.details.acknowledgementStatement).toBe("required_for_policy_acknowledgement");
|
||||
expect(response.json().error.details.reasonCode).toBe("announcement.statement_required");
|
||||
await app.close();
|
||||
});
|
||||
|
||||
@@ -346,6 +347,7 @@ describe("announcements policy acknowledgement routes", () => {
|
||||
|
||||
expect(general.statusCode).toBe(409);
|
||||
expect(general.json().error.details.acknowledgement).toBe("not_required");
|
||||
expect(general.json().error.details.reasonCode).toBe("announcement.acknowledgement_not_required");
|
||||
expect(hidden.statusCode).toBe(404);
|
||||
await app.close();
|
||||
});
|
||||
@@ -421,6 +423,106 @@ describe("announcements policy acknowledgement routes", () => {
|
||||
|
||||
expect(response.statusCode).toBe(409);
|
||||
expect(response.json().error.details.acknowledgement).toBe("create_new_version_after_acknowledgement");
|
||||
expect(response.json().error.details.reasonCode).toBe("announcement.acknowledged_content_locked");
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("announcement time and reason contracts", () => {
|
||||
const admin = buildEmployee({ id: ADMIN_ID, permissionCodes: ["system.settings"] });
|
||||
const fields = ["startsAt", "endsAt", "acknowledgementDueAt"];
|
||||
const invalid = ["2026-02-29T12:00:00Z", "2024-02-30T08:00:00+08:00", "2026-04-31T23:59", "2026-13-01T00:00:00Z", "2026-01-01Tgarbage", "2026-01-01T25:00:00Z", "2026-01-01T12:60:00Z", "2026-01-01T"];
|
||||
function expectNoBusinessCalls(state) {
|
||||
for (const group of [state.prisma.portalAnnouncement, state.prisma.portalAnnouncementAcknowledgement, state.prisma.employee, state.prisma.auditLog]) {
|
||||
for (const fn of Object.values(group)) expect(fn).not.toHaveBeenCalled();
|
||||
}
|
||||
expect(state.prisma.$transaction).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
it.each(["POST", "PATCH"].flatMap((method) => fields.flatMap((field) => invalid.map((value) => [method, field, value]))))("rejects %s %s=%s before business access", async (method, field, value) => {
|
||||
const { app, state } = await buildApp({ actor: admin });
|
||||
try {
|
||||
const response = await app.inject({ method, url: method === "POST" ? "/api/announcements" : `/api/announcements/${POLICY_ID}`, payload: { title: "合成公告", summary: "合成内容", [field]: value } });
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.json().error).toMatchObject({ code: "VALID_001", details: { reasonCode: method === "POST" ? "announcement.create_input_invalid" : "announcement.update_input_invalid", fieldErrors: { [field]: ["invalid_calendar_datetime"] } } });
|
||||
expectNoBusinessCalls(state);
|
||||
} finally { await app.close(); }
|
||||
});
|
||||
|
||||
it.each(["2024-02-29T23:59:59.123Z", "2024-02-29T23:30:00-12:00", "2024-03-01T00:30:00+08:00", "2024-04-30T12:34", "2024-04-30T12:34:56.789", "2024-04-30T24:00:00Z"])("retains valid offset/local/rollover compatibility: %s", async (value) => {
|
||||
const { app, state } = await buildApp({ actor: admin });
|
||||
try {
|
||||
const response = await app.inject({ method: "POST", url: "/api/announcements", payload: { title: "合成公告", summary: "合成内容", category: "policy", acknowledgementStatement: "合成签收声明", ...Object.fromEntries(fields.map((field) => [field, value])) } });
|
||||
expect(response.statusCode).toBe(201);
|
||||
const data = state.prisma.portalAnnouncement.create.mock.calls[0][0].data;
|
||||
for (const field of ["starts_at", "ends_at", "acknowledgement_due_at"]) expect(data[field]).toEqual(new Date(value));
|
||||
const patched = await app.inject({ method: "PATCH", url: `/api/announcements/${POLICY_ID}`, payload: Object.fromEntries(fields.map((field) => [field, value])) });
|
||||
expect(patched.statusCode).toBe(200);
|
||||
const patchData = state.prisma.portalAnnouncement.update.mock.calls[0][0].data;
|
||||
for (const field of ["starts_at", "ends_at", "acknowledgement_due_at"]) expect(patchData[field]).toEqual(new Date(value));
|
||||
} finally { await app.close(); }
|
||||
});
|
||||
|
||||
it("preserves null clearing and omitted timestamps", async () => {
|
||||
const { app, state } = await buildApp({ actor: admin });
|
||||
try {
|
||||
const created = await app.inject({ method: "POST", url: "/api/announcements", payload: { title: "合成公告", summary: "合成内容" } });
|
||||
expect(created.statusCode).toBe(201);
|
||||
expect(state.prisma.portalAnnouncement.create.mock.calls[0][0].data).toMatchObject({ starts_at: null, ends_at: null, acknowledgement_due_at: null });
|
||||
const currentDue = state.announcements.find((item) => item.id === POLICY_ID).acknowledgement_due_at;
|
||||
const omitted = await app.inject({ method: "PATCH", url: `/api/announcements/${POLICY_ID}`, payload: { priority: 11 } });
|
||||
expect(omitted.statusCode).toBe(200);
|
||||
expect(state.prisma.portalAnnouncement.update.mock.calls[0][0].data).not.toHaveProperty("starts_at");
|
||||
expect(state.prisma.portalAnnouncement.update.mock.calls[0][0].data.acknowledgement_due_at).toEqual(currentDue);
|
||||
const cleared = await app.inject({ method: "PATCH", url: `/api/announcements/${POLICY_ID}`, payload: { startsAt: null, endsAt: null, acknowledgementDueAt: null } });
|
||||
expect(cleared.statusCode).toBe(200);
|
||||
expect(state.prisma.portalAnnouncement.update.mock.calls[1][0].data).toMatchObject({ starts_at: null, ends_at: null, acknowledgement_due_at: null });
|
||||
} finally { await app.close(); }
|
||||
});
|
||||
|
||||
it.each([
|
||||
["GET", "/api/announcements?manage=invalid", undefined, "announcement.query_invalid"],
|
||||
["PATCH", "/api/announcements/invalid", { enabled: false }, "announcement.id_invalid"],
|
||||
["PATCH", `/api/announcements/${POLICY_ID}`, {}, "announcement.update_input_invalid"],
|
||||
["POST", `/api/announcements/${POLICY_ID}/acknowledge`, { confirmed: false }, "announcement.acknowledgement_input_invalid"],
|
||||
])("explains %s %s before reads/writes", async (method, url, payload, reasonCode) => {
|
||||
const { app, state } = await buildApp({ actor: admin });
|
||||
try {
|
||||
const response = await app.inject({ method, url, ...(payload ? { payload } : {}) });
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.json().error).toMatchObject({ code: "VALID_001", details: { reasonCode } });
|
||||
expectNoBusinessCalls(state);
|
||||
} finally { await app.close(); }
|
||||
});
|
||||
|
||||
it("explains the disabled feature before business access", async () => {
|
||||
const { app, state } = await buildApp({ actor: admin, enabled: false });
|
||||
try {
|
||||
const response = await app.inject({ method: "GET", url: "/api/announcements" });
|
||||
expect(response.statusCode).toBe(409);
|
||||
expect(response.json().error.details).toEqual({ reasonCode: "announcement.disabled", featureFlag: "announcements_v1_disabled", reason: "env_hard_off" });
|
||||
expectNoBusinessCalls(state);
|
||||
} finally { await app.close(); }
|
||||
});
|
||||
|
||||
it("explains a legacy policy missing its statement without acknowledgement writes", async () => {
|
||||
const { app, state } = await buildApp({ announcements: [buildAnnouncement({ acknowledgement_statement: null })] });
|
||||
try {
|
||||
const response = await app.inject({ method: "POST", url: `/api/announcements/${POLICY_ID}/acknowledge`, payload: { confirmed: true } });
|
||||
expect(response.statusCode).toBe(409);
|
||||
expect(response.json().error.details).toEqual({ reasonCode: "announcement.statement_missing", acknowledgementStatement: "missing_for_policy_acknowledgement" });
|
||||
expect(state.prisma.$transaction).not.toHaveBeenCalled();
|
||||
expect(state.prisma.portalAnnouncementAcknowledgement.create).not.toHaveBeenCalled();
|
||||
expect(state.prisma.auditLog.create).not.toHaveBeenCalled();
|
||||
} finally { await app.close(); }
|
||||
});
|
||||
|
||||
it("retains permission guards with valid dates", async () => {
|
||||
const { app, state } = await buildApp();
|
||||
try {
|
||||
const response = await app.inject({ method: "POST", url: "/api/announcements", payload: { title: "合成公告", summary: "合成内容", startsAt: "2024-02-29T00:00:00Z" } });
|
||||
expect(response.statusCode).toBe(403);
|
||||
expectNoBusinessCalls(state);
|
||||
} finally { await app.close(); }
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户