R50 通知推送:dispatcher 投递服务、业务事件接入与员工推送开关

这个提交包含在:
2026-04-05 11:21:39 +08:00
父节点 ff0a677bf3
当前提交 47e10e6acc
共修改 24 个文件,包含 1701 行新增和 197 行删除
@@ -31,11 +31,15 @@ describe('purchase submit side effects helper', () => {
},
}),
resolveRecipients: vi.fn().mockResolvedValue(['manager-1']),
createAppNotifications: vi.fn().mockResolvedValue({ count: 1 }),
enqueueIm: vi.fn().mockResolvedValue({
queued: 1,
duplicated: 0,
matchedChannels: [{ id: 'channel-1' }],
createAppNotifications: vi.fn().mockResolvedValue([
{
id: 'notification-1',
recipient_employee_id: 'manager-1',
},
]),
dispatchNotifications: vi.fn().mockResolvedValue({
deliveredChannels: [{ channelId: 'channel-1', status: 'success' }],
skippedReason: null,
}),
sendEmailSafe: vi.fn().mockResolvedValue({
email: { skipped: true, sent: 0, failed: 0 },
@@ -50,7 +54,7 @@ describe('purchase submit side effects helper', () => {
warn: vi.fn(),
};
const createAppNotifications = vi.fn();
const enqueueIm = vi.fn();
const dispatchNotifications = vi.fn();
const sendEmailSafe = vi.fn();
const result = await finalizePurchaseSubmitSideEffects(
@@ -58,20 +62,20 @@ describe('purchase submit side effects helper', () => {
logger,
resolveRecipients: vi.fn().mockRejectedValue(new Error('resolve failed')),
createAppNotifications,
enqueueIm,
dispatchNotifications,
sendEmailSafe,
}),
);
expect(logger.error).toHaveBeenCalledTimes(1);
expect(createAppNotifications).not.toHaveBeenCalled();
expect(enqueueIm).not.toHaveBeenCalled();
expect(dispatchNotifications).not.toHaveBeenCalled();
expect(sendEmailSafe).not.toHaveBeenCalled();
expect(result).toEqual(
expect.objectContaining({
recipientEmployeeIds: [],
notification: expect.objectContaining({ skipped: true, reason: 'recipient_resolution_failed' }),
im: expect.objectContaining({ queued: 0, skippedReason: 'recipient_resolution_failed' }),
dispatch: expect.objectContaining({ skippedReason: 'recipient_resolution_failed' }),
email: expect.objectContaining({ skipped: true, reason: 'recipient_resolution_failed' }),
}),
);
@@ -82,7 +86,7 @@ describe('purchase submit side effects helper', () => {
error: vi.fn(),
warn: vi.fn(),
};
const enqueueIm = vi.fn().mockRejectedValue(new Error('im failed'));
const dispatchNotifications = vi.fn().mockRejectedValue(new Error('dispatch failed'));
const sendEmailSafe = vi.fn().mockResolvedValue({
email: { skipped: true, sent: 0, failed: 0 },
});
@@ -91,30 +95,34 @@ describe('purchase submit side effects helper', () => {
createParams({
logger,
createAppNotifications: vi.fn().mockRejectedValue(new Error('notifications failed')),
enqueueIm,
dispatchNotifications,
sendEmailSafe,
}),
);
expect(logger.error).toHaveBeenCalledTimes(2);
expect(enqueueIm).toHaveBeenCalledTimes(1);
expect(dispatchNotifications).toHaveBeenCalledTimes(1);
expect(sendEmailSafe).toHaveBeenCalledTimes(1);
expect(result).toEqual(
expect.objectContaining({
recipientEmployeeIds: ['manager-1'],
notification: expect.objectContaining({ skipped: true, reason: 'notification_failed' }),
im: expect.objectContaining({ queued: 0, skippedReason: 'enqueue_im_failed' }),
dispatch: expect.objectContaining({ skippedReason: 'dispatch_failed' }),
email: expect.objectContaining({ skipped: true, sent: 0, failed: 0 }),
}),
);
});
it('builds structured purchase copy for app notification, IM and email', async () => {
const createAppNotifications = vi.fn().mockResolvedValue({ count: 1 });
const enqueueIm = vi.fn().mockResolvedValue({
queued: 1,
duplicated: 0,
matchedChannels: [{ id: 'channel-1' }],
const createAppNotifications = vi.fn().mockResolvedValue([
{
id: 'notification-1',
recipient_employee_id: 'manager-1',
},
]);
const dispatchNotifications = vi.fn().mockResolvedValue({
deliveredChannels: [{ channelId: 'channel-1', status: 'success' }],
skippedReason: null,
});
const sendEmailSafe = vi.fn().mockResolvedValue({
email: { skipped: true, sent: 0, failed: 0 },
@@ -123,7 +131,7 @@ describe('purchase submit side effects helper', () => {
await finalizePurchaseSubmitSideEffects(
createParams({
createAppNotifications,
enqueueIm,
dispatchNotifications,
sendEmailSafe,
}),
);
@@ -135,11 +143,16 @@ describe('purchase submit side effects helper', () => {
body: '申请人:李经理\n金额:5600.00\n单号:CG-20260324-001\n动作:请及时处理审批任务。',
}),
);
expect(enqueueIm).toHaveBeenCalledWith(
{},
expect(dispatchNotifications).toHaveBeenCalledWith(
expect.objectContaining({
prisma: {},
title: '【待办】采购申请 CG-20260324-001 待审批',
body: '申请人:李经理\n金额:5600.00\n单号:CG-20260324-001\n动作:请及时处理审批任务。',
notifications: [
expect.objectContaining({
id: 'notification-1',
}),
],
}),
);
expect(sendEmailSafe).toHaveBeenCalledWith(