汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、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项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
91 行
3.5 KiB
JavaScript
91 行
3.5 KiB
JavaScript
|
|
const outbox = require("../../../../src/services/integrations/outbox");
|
|
|
|
describe("Integration Outbox", () => {
|
|
it("rejects retired ERP enqueue before touching history", async () => {
|
|
await expect(outbox.enqueueIntegrationEvent({}, { eventType: "erp.journal.export" })).rejects.toMatchObject({ code: "erp_retired" });
|
|
});
|
|
|
|
it("enqueues new event", async () => {
|
|
const mockPrisma = {
|
|
integrationOutbox: {
|
|
findUnique: vi.fn().mockResolvedValue(null),
|
|
create: vi.fn().mockResolvedValue({ id: 1, status: "pending" }),
|
|
},
|
|
};
|
|
const result = await outbox.enqueueIntegrationEvent(mockPrisma, { eventType: "test" });
|
|
expect(result.id).toBe(1);
|
|
expect(result.duplicated).toBe(false);
|
|
});
|
|
|
|
it("detects duplicated event", async () => {
|
|
const mockPrisma = {
|
|
integrationOutbox: {
|
|
findUnique: vi.fn().mockResolvedValue({ id: 2, status: "pending" }),
|
|
},
|
|
};
|
|
const result = await outbox.enqueueIntegrationEvent(mockPrisma, { eventType: "test", dedupeKey: "key" });
|
|
expect(result.id).toBe(2);
|
|
expect(result.duplicated).toBe(true);
|
|
});
|
|
|
|
it("marks as processing", async () => {
|
|
const mockPrisma = {
|
|
integrationOutbox: {
|
|
update: vi.fn().mockResolvedValue({ id: 1, status: "processing" }),
|
|
},
|
|
};
|
|
const res = await outbox.markOutboxProcessing(mockPrisma, 1, "actor");
|
|
expect(res.status).toBe("processing");
|
|
});
|
|
|
|
it("fetches pending events", async () => {
|
|
const mockPrisma = {
|
|
integrationOutbox: {
|
|
findMany: vi.fn().mockResolvedValue([{ id: 1 }]),
|
|
},
|
|
};
|
|
const items = await outbox.fetchPendingOutboxEvents(mockPrisma, { batchSize: 5 });
|
|
expect(items.length).toBe(1);
|
|
expect(mockPrisma.integrationOutbox.findMany).toHaveBeenCalledWith(expect.objectContaining({
|
|
where: expect.objectContaining({ NOT: { event_type: { startsWith: "erp." } } }),
|
|
}));
|
|
});
|
|
|
|
it("marks outbox succeeded", async () => {
|
|
const mockPrisma = {
|
|
integrationOutbox: {
|
|
update: vi.fn().mockResolvedValue({ id: 1, status: "succeeded" }),
|
|
},
|
|
};
|
|
const res = await outbox.markOutboxSucceeded(mockPrisma, 1, "actor");
|
|
expect(res.status).toBe("succeeded");
|
|
});
|
|
|
|
it("handles failed outbox and schedules retry", async () => {
|
|
const mockPrisma = {
|
|
integrationOutbox: {
|
|
update: vi.fn().mockImplementation(async ({ data }) => {
|
|
return { status: data.status, retry: data.next_retry_at };
|
|
}),
|
|
},
|
|
};
|
|
const res = await outbox.markOutboxFailed(mockPrisma, {}, { id: 1, attempts: 0 }, { retryable: true }, "actor");
|
|
expect(res.status).toBe("failed");
|
|
expect(res.retry).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it("moves to dead_letter if max attempts reached", async () => {
|
|
const mockPrisma = {
|
|
integrationOutbox: {
|
|
update: vi.fn().mockImplementation(async ({ data }) => {
|
|
return { status: data.status, retry: data.next_retry_at };
|
|
}),
|
|
},
|
|
};
|
|
const res = await outbox.markOutboxFailed(mockPrisma, {}, { id: 1, attempts: 5, max_attempts: 5 }, { retryable: true }, "actor");
|
|
expect(res.status).toBe("dead_letter");
|
|
expect(res.retry).toBeNull();
|
|
});
|
|
});
|