feat: 结算时同步钉钉已审批请假并标记为请假

This commit is contained in:
2026-08-05 18:43:02 +08:00
parent 32343b271b
commit c8316e9e8e
19 changed files with 746 additions and 58 deletions

View File

@@ -9,10 +9,17 @@ describe('AttendanceImportService', () => {
findOne: jest.fn(),
save: jest.fn(),
};
const dingLeaveRawRepo = {
find: jest.fn(),
findOne: jest.fn(),
create: jest.fn((value: Record<string, unknown>) => value),
save: jest.fn(),
};
const studentRepo = { findOne: jest.fn() };
const studentDingMappingRepo = { findOne: jest.fn() };
const studentDingMappingRepo = { findOne: jest.fn(), find: jest.fn() };
const dingTalkService = {
fetchAttendanceResults: jest.fn(),
fetchDailyLeaveStatus: jest.fn(),
};
const attendanceService = {
autoMatchDingRecords: jest.fn(),
@@ -24,6 +31,7 @@ describe('AttendanceImportService', () => {
jest.clearAllMocks();
service = new AttendanceImportService(
dingRawRepo as never,
dingLeaveRawRepo as never,
studentRepo as never,
studentDingMappingRepo as never,
dingTalkService as unknown as DingTalkService,
@@ -304,4 +312,67 @@ describe('AttendanceImportService', () => {
expect(event.userId).toBeUndefined();
}
});
it('syncs approved DingTalk leaves per user per day and auto-matches them', async () => {
dingTalkService.fetchDailyLeaveStatus.mockImplementation(
async (userId: string, workDate: string) => [
{
userId,
workDate,
procInstId: `leave-${userId}-${workDate}`,
tagName: '请假',
leaveType: '事假',
beginTime: new Date(`${workDate}T08:00:00+08:00`),
endTime: new Date(`${workDate}T12:00:00+08:00`),
approvedAt: new Date(`${workDate}T09:00:00+08:00`),
duration: '0.5',
durationUnit: 'day',
},
],
);
dingLeaveRawRepo.findOne.mockResolvedValue(null);
dingLeaveRawRepo.save.mockImplementation(async (entities) => entities);
studentDingMappingRepo.find.mockResolvedValue([{ dingUserId: 'ding-1', studentId: 7 }]);
dingLeaveRawRepo.find.mockResolvedValue([
{ dingId: 'leave-ding-1-2026-07-01', dingUserId: 'ding-1', matchStatus: 'unmatched' },
]);
const result = await service.syncLeaveStatusForLesson({
startDate: '2026-07-01',
endDate: '2026-07-02',
userIds: ['ding-1', 'ding-2'],
autoMatch: true,
});
expect(dingTalkService.fetchDailyLeaveStatus).toHaveBeenCalledTimes(4);
expect(dingTalkService.fetchDailyLeaveStatus).toHaveBeenCalledWith(
'ding-1',
'2026-07-01',
);
expect(dingTalkService.fetchDailyLeaveStatus).toHaveBeenCalledWith(
'ding-2',
'2026-07-02',
);
expect(dingLeaveRawRepo.save).toHaveBeenCalled();
expect(result.synced).toBe(4);
expect(result.matched).toBe(1);
});
it('keeps syncing remaining users when one leave fetch fails', async () => {
dingTalkService.fetchDailyLeaveStatus
.mockRejectedValueOnce(new Error('DingTalk unavailable'))
.mockResolvedValue([]);
dingLeaveRawRepo.findOne.mockResolvedValue(null);
const result = await service.syncLeaveStatusForLesson({
startDate: '2026-07-01',
endDate: '2026-07-01',
userIds: ['ding-1', 'ding-2'],
autoMatch: false,
});
expect(dingTalkService.fetchDailyLeaveStatus).toHaveBeenCalledTimes(2);
expect(result.errors).toHaveLength(1);
expect(result.synced).toBe(0);
});
});