汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、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项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
130 行
6.9 KiB
JavaScript
130 行
6.9 KiB
JavaScript
const { normalizeWecomHrProfile, runWecomHrMasterSync } = require("../../src/services/integrations/wecomHrMaster");
|
|
|
|
function profileFor(value, userid = "synthetic-calendar") {
|
|
return normalizeWecomHrProfile({
|
|
contact: { userid, name: "合成员工", email: `${userid}@example.invalid`, department: [1] },
|
|
departmentMap: new Map([["1", { name: "合成部门" }]]),
|
|
staff: { staff_info: value === undefined ? [] : [{ fieldid: "入职日期", value }] },
|
|
});
|
|
}
|
|
|
|
function fixture(existing = null) {
|
|
const items = [];
|
|
let run;
|
|
const prisma = {
|
|
integrationBinding: { findFirst: vi.fn().mockResolvedValue(null), create: vi.fn().mockResolvedValue({}) },
|
|
department: {
|
|
findUnique: vi.fn().mockResolvedValue(null), findMany: vi.fn().mockResolvedValue([]),
|
|
create: vi.fn(async ({ data }) => ({ ...data, id: "synthetic-department" })), update: vi.fn(),
|
|
},
|
|
wecomIdentityBinding: { findUnique: vi.fn().mockResolvedValue(existing ? { employee: existing } : null), upsert: vi.fn().mockResolvedValue({}) },
|
|
employee: {
|
|
findUnique: vi.fn().mockResolvedValue(null), findMany: vi.fn().mockResolvedValue([]),
|
|
create: vi.fn(async ({ data }) => ({ ...data, id: "synthetic-created" })),
|
|
update: vi.fn(async ({ data }) => ({ ...data, id: existing?.id })),
|
|
},
|
|
employeeRole: { createMany: vi.fn().mockResolvedValue({ count: 1 }) },
|
|
wecomHrMasterSyncRun: {
|
|
create: vi.fn(async ({ data }) => (run = { ...data, id: "synthetic-run" })),
|
|
update: vi.fn(async ({ data }) => (run = { ...run, ...data })),
|
|
},
|
|
wecomHrMasterSyncItem: {
|
|
create: vi.fn(async ({ data }) => { const item = { ...data, id: `synthetic-item-${items.length}` }; items.push(item); return item; }),
|
|
findMany: vi.fn(async () => items), update: vi.fn(),
|
|
},
|
|
};
|
|
prisma.$transaction = vi.fn(async (callback) => callback(prisma));
|
|
const fetchImpl = vi.fn(() => { throw new Error("offline test must not fetch"); });
|
|
return {
|
|
prisma, fetchImpl,
|
|
run: (profiles, mode = "apply") => runWecomHrMasterSync({ prisma, config: {}, actorId: "synthetic-actor", mode, sourceProfiles: profiles, fetchImpl }),
|
|
};
|
|
}
|
|
|
|
const invalidInputs = ["2026-02-29", "2024/2/30", "2026.4.31", "20260230", "2026-13-01", "2026-01-00", "not-a-date", "0", "-1", "253402300800000", "999999999999999999999"];
|
|
const validInputs = [
|
|
["2024-02-29", "2024-02-29"], ["2024/2/29", "2024-02-29"], ["2024.2.29", "2024-02-29"],
|
|
["20240229", "2024-02-29"], ["2026-4-30", "2026-04-30"],
|
|
["1709164800", "2024-02-29"], [1709164800000, "2024-02-29"],
|
|
];
|
|
|
|
function expectNoBusinessCalls(prisma) {
|
|
for (const name of ["integrationBinding", "department", "employee", "employeeRole", "wecomIdentityBinding"]) {
|
|
for (const fn of Object.values(prisma[name])) expect(fn).not.toHaveBeenCalled();
|
|
}
|
|
expect(prisma.wecomHrMasterSyncItem.update).not.toHaveBeenCalled();
|
|
}
|
|
|
|
describe("offline HR hire-date calendar contract", () => {
|
|
afterEach(() => vi.useRealTimers());
|
|
|
|
it.each(invalidInputs.flatMap((value) => ["apply", "dry_run"].map((mode) => [value, mode])))("skips invalid %s in %s before business access", async (value, mode) => {
|
|
const f = fixture();
|
|
const profile = { ...profileFor(value), managerExternalUserId: "synthetic-manager" };
|
|
expect(profile.hireDate).toBeNull();
|
|
expect(profile.hireDateInvalid).toBe(true);
|
|
const result = await f.run([profile], mode);
|
|
expect(result.status).toBe("partial");
|
|
expect(result.items[0]).toMatchObject({ action: "skip", status: "skipped", errorCode: "invalid_hire_date", employeeId: null,
|
|
errorMessage: "入职日期无效,本条未同步;请修正来源日期后重试。" });
|
|
expect(result.items[0].fieldChanges).toBeNull();
|
|
expect(result.items[0].sourceSummary).not.toHaveProperty("hireDate");
|
|
expectNoBusinessCalls(f.prisma);
|
|
expect(f.prisma.wecomHrMasterSyncRun.create).toHaveBeenCalledOnce();
|
|
expect(f.prisma.wecomHrMasterSyncItem.create).toHaveBeenCalledOnce();
|
|
expect(f.fetchImpl).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each(validInputs)("creates exact UTC date for %s", async (input, canonical) => {
|
|
const f = fixture();
|
|
const profile = profileFor(input);
|
|
expect(profile.hireDate).toBe(canonical);
|
|
expect(profile.hireDateInvalid).toBe(false);
|
|
const result = await f.run([profile]);
|
|
expect(result.status).toBe("success");
|
|
expect(f.prisma.employee.create.mock.calls[0][0].data.hire_date).toEqual(new Date(`${canonical}T00:00:00.000Z`));
|
|
expect(f.prisma.employeeRole.createMany).toHaveBeenCalledOnce();
|
|
expect(f.prisma.wecomIdentityBinding.upsert).toHaveBeenCalledOnce();
|
|
expect(f.fetchImpl).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([undefined, null, "", " "])("keeps missing %s defaulted to the run time", async (value) => {
|
|
vi.useFakeTimers({ toFake: ["Date"] });
|
|
vi.setSystemTime(new Date("2026-09-17T01:02:03.000Z"));
|
|
const f = fixture();
|
|
const result = await f.run([profileFor(value)]);
|
|
expect(result.status).toBe("success");
|
|
expect(f.prisma.employee.create.mock.calls[0][0].data.hire_date).toEqual(new Date("2026-09-17T01:02:03.000Z"));
|
|
expect(f.fetchImpl).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each(["apply", "dry_run"])("rejects invalid directly supplied normalized profiles in %s", async (mode) => {
|
|
const f = fixture({ id: "synthetic-existing" });
|
|
const result = await f.run([{ ...profileFor(undefined), hireDate: "2026-02-30", managerExternalUserId: "synthetic-manager" }], mode);
|
|
expect(result.items[0].errorCode).toBe("invalid_hire_date");
|
|
expectNoBusinessCalls(f.prisma);
|
|
expect(f.fetchImpl).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("never adds hire_date to existing employee updates", async () => {
|
|
const f = fixture({ id: "synthetic-existing", name: "合成员工", email: "[email protected]", hire_date: new Date("2020-01-01T00:00:00Z") });
|
|
const result = await f.run([profileFor("2024-02-29")]);
|
|
expect(result.status).toBe("success");
|
|
expect(f.prisma.employee.create).not.toHaveBeenCalled();
|
|
expect(f.prisma.employee.update).toHaveBeenCalledOnce();
|
|
expect(f.prisma.employee.update.mock.calls[0][0].data).not.toHaveProperty("hire_date");
|
|
});
|
|
|
|
it.each(["apply", "dry_run"])("continues valid profiles after an invalid one in %s", async (mode) => {
|
|
const f = fixture();
|
|
const result = await f.run([profileFor("2026-02-30", "synthetic-invalid"), profileFor("2024-02-29", "synthetic-valid")], mode);
|
|
expect(result.status).toBe("partial");
|
|
expect(result.summary).toMatchObject({ total: 2, skipped: 1, failed: 0, created: 1 });
|
|
expect(result.items.map((item) => item.status)).toEqual(["skipped", mode === "apply" ? "success" : "planned"]);
|
|
for (const fn of [f.prisma.department.create, f.prisma.integrationBinding.create, f.prisma.employee.create, f.prisma.employeeRole.createMany, f.prisma.wecomIdentityBinding.upsert]) {
|
|
expect(fn).toHaveBeenCalledTimes(mode === "apply" ? 1 : 0);
|
|
}
|
|
expect(f.fetchImpl).not.toHaveBeenCalled();
|
|
});
|
|
});
|