汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、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项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
93 行
6.8 KiB
JavaScript
93 行
6.8 KiB
JavaScript
const Fastify = require("fastify");
|
|
const { AppError } = require("../../../src/utils/AppError");
|
|
const routes = Object.fromEntries(["payroll", "budgets", "performance", "attendance", "attendanceCorrections", "leave"].map((name) => [name, require(`../../../src/routes/${name}`)]));
|
|
const ID = "11111111-1111-4111-8111-111111111111";
|
|
const budgetBody = { code: "SYNTHETIC", name: "合成预算", periodStart: "2024-02-29", periodEnd: "2026-04-30", allocatedAmount: "100.00" };
|
|
const cycleBody = { name: "合成周期", cycleType: "quarterly", startDate: "2024-02-29", endDate: "2026-04-30" };
|
|
const leaveBody = { leaveTypeId: ID, startDate: "2024-02-29", endDate: "2026-04-30", startSession: "am", endSession: "pm", reason: "synthetic" };
|
|
const cases = [
|
|
["payroll", "PUT", `/payroll/employees/${ID}/profile`, { effectiveDate: "2024-02-29", items: [{ salaryItemId: ID, monthlyAmount: "100.00" }] }, "effectiveDate"],
|
|
...["periodStart", "periodEnd"].map((field) => ["budgets", "POST", "/budgets", budgetBody, field]),
|
|
...["startDate", "endDate"].flatMap((field) => [["performance", "POST", "/performance/cycles", cycleBody, field], ["performance", "PATCH", `/performance/cycles/${ID}`, {}, field]]),
|
|
["attendance", "GET", "/attendance/team", {}, "date"],
|
|
["attendance", "POST", "/attendance/admin/backfill", { employeeId: ID, clockInAt: "2026-04-30T09:00:00+08:00" }, "workDate"],
|
|
...["startDate", "endDate"].map((field) => ["attendance", "POST", "/attendance/wecom/sync", { startDate: "2026-04-01", endDate: "2026-04-30" }, field]),
|
|
["attendanceCorrections", "POST", "/attendance-corrections", { kind: "missed_punch", missedPunchAt: "2026-04-30T09:00:00+08:00", reason: "synthetic" }, "workDate"],
|
|
...["startDate", "endDate"].flatMap((field) => [["leave", "POST", "/leave-requests", leaveBody, field], ["leave", "PATCH", `/leave-requests/${ID}`, leaveBody, field], ["leave", "GET", "/leave-requests/working-days", { startDate: "2024-02-29", endDate: "2026-04-30", startSession: "am", endSession: "pm" }, field]]),
|
|
...["dateFrom", "dateTo"].map((field) => ["leave", "GET", "/leave-requests", {}, field]),
|
|
];
|
|
|
|
async function buildApp(name, { allowed = true, budgetSuccess = false } = {}) {
|
|
const app = Fastify();
|
|
const businessCalls = [];
|
|
const rejectCall = (...args) => { businessCalls.push(args); throw new Error("unexpected_business_access"); };
|
|
const prisma = new Proxy({
|
|
featureFlag: { findUnique: vi.fn(async ({ where }) => ({ flag_key: where.flag_key, override_mode: where.flag_key.includes("cutover") ? "force_off" : "force_on", rollout_type: "global", department_ids: [] })) },
|
|
$transaction: vi.fn(rejectCall),
|
|
}, { get(target, key) {
|
|
if (!(key in target)) target[key] = new Proxy({}, { get(model, operation) { return model[operation] ??= vi.fn(rejectCall); } });
|
|
return target[key];
|
|
} });
|
|
if (budgetSuccess) {
|
|
prisma.budgetPool = { create: vi.fn(async ({ data }) => ({ id: ID, ...data })) };
|
|
prisma.auditLog = { create: vi.fn().mockResolvedValue({}) };
|
|
}
|
|
const authenticate = async (request) => { request.employee = { id: ID, status: "active", roleCodes: ["admin"], permissionCodes: ["*"], department_id: null }; };
|
|
const permission = async (request) => { await authenticate(request); if (!allowed) throw new AppError({ code: "PERM_001", statusCode: 403, message: "无权限" }); };
|
|
app.decorate("config", { appTimezone: "Asia/Shanghai", employeeLifecycleEnabled: true, attendanceUnifiedEnabled: true });
|
|
app.decorate("prisma", prisma);
|
|
app.decorate("authenticate", permission);
|
|
app.decorate("requirePermission", () => permission);
|
|
app.decorate("requireEntityPermission", () => permission);
|
|
app.decorate("assertEntityPermission", () => {});
|
|
app.decorate("hasPermission", () => allowed);
|
|
await app.register(require("@fastify/multipart"));
|
|
app.setErrorHandler((error, request, reply) => reply.code(error.statusCode ?? 500).send({ error: { code: error.code, details: error.details } }));
|
|
await app.register(routes[name], { prefix: "/api" });
|
|
return { app, prisma, businessCalls };
|
|
}
|
|
|
|
describe("remaining calendar consumers", () => {
|
|
it.each(cases)("%s %s %s rejects calendar overflow before business access (%j / %s)", async (name, method, path, body, field) => {
|
|
const { app, prisma, businessCalls } = await buildApp(name);
|
|
try {
|
|
for (const date of ["2026-02-29", "2026-04-31"]) {
|
|
const input = { ...body, [field]: date };
|
|
const response = await app.inject({ method, url: `/api${path}${method === "GET" ? `?${new URLSearchParams(input)}` : ""}`, ...(method === "GET" ? {} : { payload: input }) });
|
|
const legacyDisabled = name === "attendance" && ["/attendance/team", "/attendance/admin/backfill"].includes(path);
|
|
expect(response.statusCode).toBe(legacyDisabled ? 409 : 400);
|
|
expect(response.json().error.code).toBe("VALID_001");
|
|
if (legacyDisabled) expect(response.json().error.details).toEqual({ featureFlag: "attendance_v1_disabled", reason: "env_hard_off", reasonCode: "attendance.feature_disabled" });
|
|
else expect(response.json().error.details.fieldErrors[field]).toContain("invalid_calendar_date");
|
|
expect(prisma.$transaction).not.toHaveBeenCalled();
|
|
expect(businessCalls).toEqual([]);
|
|
}
|
|
} finally { await app.close(); }
|
|
});
|
|
|
|
it("budget stores exact leap-day/month-end dates and retains reverse-range refusal", async () => {
|
|
const { app, prisma, businessCalls } = await buildApp("budgets", { budgetSuccess: true });
|
|
try {
|
|
const response = await app.inject({ method: "POST", url: "/api/budgets", payload: budgetBody });
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.json().data).toMatchObject({ periodStart: "2024-02-29", periodEnd: "2026-04-30", allocatedAmount: "100.00" });
|
|
expect(prisma.budgetPool.create.mock.calls[0][0].data).toMatchObject({ period_start: new Date("2024-02-29T00:00:00Z"), period_end: new Date("2026-04-30T00:00:00Z") });
|
|
expect(prisma.auditLog.create).toHaveBeenCalledTimes(1);
|
|
const reversed = await app.inject({ method: "POST", url: "/api/budgets", payload: { ...budgetBody, periodStart: "2026-05-01" } });
|
|
expect(reversed.statusCode).toBe(400);
|
|
expect(reversed.json().error.details).toEqual({ periodEnd: "must_be_after_periodStart", reasonCode: "budget.period_invalid" });
|
|
expect(prisma.budgetPool.create).toHaveBeenCalledTimes(1);
|
|
expect(prisma.auditLog.create).toHaveBeenCalledTimes(1);
|
|
expect(businessCalls).toEqual([]);
|
|
} finally { await app.close(); }
|
|
});
|
|
|
|
it("budget retains permission refusal without writes", async () => {
|
|
const { app, businessCalls } = await buildApp("budgets", { allowed: false });
|
|
try {
|
|
expect((await app.inject({ method: "POST", url: "/api/budgets", payload: budgetBody })).statusCode).toBe(403);
|
|
expect(businessCalls).toEqual([]);
|
|
} finally { await app.close(); }
|
|
});
|
|
});
|