feat: settle course attendance automatically
This commit is contained in:
@@ -74,8 +74,18 @@ describe('AttendanceService \u2014 DingTalk course attendance', () => {
|
||||
{ studentId: 4, student: { id: 4, name: '\u8D75\u516D' } },
|
||||
]);
|
||||
dingRawRepo.find.mockResolvedValue([
|
||||
{ matchedStudentId: 1, attendanceType: 'OnDuty', timeResult: 'Normal' },
|
||||
{ matchedStudentId: 2, attendanceType: 'OnDuty', timeResult: 'Late' },
|
||||
{
|
||||
matchedStudentId: 1,
|
||||
attendanceType: 'OnDuty',
|
||||
timeResult: 'Normal',
|
||||
checkInTime: new Date('2026-07-11T08:55:00+08:00'),
|
||||
},
|
||||
{
|
||||
matchedStudentId: 2,
|
||||
attendanceType: 'OnDuty',
|
||||
timeResult: 'Late',
|
||||
checkInTime: new Date('2026-07-11T09:05:00+08:00'),
|
||||
},
|
||||
{ matchedStudentId: 3, attendanceType: 'OnDuty', timeResult: 'NotSigned' },
|
||||
]);
|
||||
|
||||
@@ -91,13 +101,34 @@ describe('AttendanceService \u2014 DingTalk course attendance', () => {
|
||||
);
|
||||
expect(attendanceRepo.save).toHaveBeenCalledWith([
|
||||
expect.objectContaining({ studentId: 1, status: 'present', source: 'dingtalk' }),
|
||||
expect.objectContaining({ studentId: 2, status: 'late', source: 'dingtalk' }),
|
||||
expect.objectContaining({ studentId: 3, status: 'absent', source: 'dingtalk' }),
|
||||
expect.objectContaining({ studentId: 2, status: 'present', source: 'dingtalk' }),
|
||||
expect.objectContaining({ studentId: 3, status: 'pending', source: 'dingtalk' }),
|
||||
expect.objectContaining({ studentId: 4, status: 'pending', source: 'dingtalk' }),
|
||||
]);
|
||||
expect(result.records).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('finalizes missing punches as absent and completes the session', async () => {
|
||||
const { service, attendanceRepo, dingRawRepo, scheduleRepo, classStudentRepo, sessionRepo } =
|
||||
createService();
|
||||
scheduleRepo.findOne.mockResolvedValue(endedSchedule);
|
||||
sessionRepo.findOne.mockResolvedValue(null);
|
||||
classStudentRepo.find.mockResolvedValue([
|
||||
{ studentId: 1, student: { id: 1, name: '张三' } },
|
||||
]);
|
||||
dingRawRepo.find.mockResolvedValue([]);
|
||||
|
||||
const result = await service.createLessonAttendanceFromDingTalk(4, '2026-07-11', 21, true);
|
||||
|
||||
expect(attendanceRepo.save).toHaveBeenCalledWith([
|
||||
expect.objectContaining({ studentId: 1, status: 'absent' }),
|
||||
]);
|
||||
expect(sessionRepo.save).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ status: 'completed', completedBy: 21 }),
|
||||
);
|
||||
expect(result.session.status).toBe('completed');
|
||||
});
|
||||
|
||||
it('returns student relations after the first pull', async () => {
|
||||
const { service, attendanceRepo, dingRawRepo, scheduleRepo, classStudentRepo, sessionRepo } =
|
||||
createService();
|
||||
@@ -211,8 +242,8 @@ describe('AttendanceService \u2014 DingTalk course attendance', () => {
|
||||
{ studentId: 2, student: { id: 2, name: '\u674E\u56DB' } },
|
||||
]);
|
||||
dingRawRepo.find.mockResolvedValue([
|
||||
{ matchedStudentId: 1, attendanceType: 'OnDuty', timeResult: 'Normal' },
|
||||
{ matchedStudentId: 2, attendanceType: 'OnDuty', timeResult: 'Late' },
|
||||
{ matchedStudentId: 1, attendanceType: 'OnDuty', timeResult: 'Normal', checkInTime: new Date('2026-07-11T08:55:00+08:00') },
|
||||
{ matchedStudentId: 2, attendanceType: 'OnDuty', timeResult: 'Late', checkInTime: new Date('2026-07-11T09:05:00+08:00') },
|
||||
]);
|
||||
|
||||
const result = await service.createLessonAttendanceFromDingTalk(4, '2026-07-11', 21);
|
||||
@@ -225,7 +256,7 @@ describe('AttendanceService \u2014 DingTalk course attendance', () => {
|
||||
expect(savedRecords).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ studentId: 1, status: 'present' }),
|
||||
expect.objectContaining({ studentId: 2, status: 'late' }),
|
||||
expect.objectContaining({ studentId: 2, status: 'present' }),
|
||||
]),
|
||||
);
|
||||
expect(result.records).toHaveLength(2);
|
||||
@@ -278,8 +309,8 @@ describe('AttendanceService \u2014 DingTalk course attendance', () => {
|
||||
{ studentId: 2, student: { id: 2, name: '\u674E\u56DB' } },
|
||||
]);
|
||||
dingRawRepo.find.mockResolvedValue([
|
||||
{ matchedStudentId: 1, attendanceType: 'OnDuty', timeResult: 'Late' },
|
||||
{ matchedStudentId: 2, attendanceType: 'OnDuty', timeResult: 'Late' },
|
||||
{ matchedStudentId: 1, attendanceType: 'OnDuty', timeResult: 'Late', checkInTime: new Date('2026-07-11T09:05:00+08:00') },
|
||||
{ matchedStudentId: 2, attendanceType: 'OnDuty', timeResult: 'Late', checkInTime: new Date('2026-07-11T09:05:00+08:00') },
|
||||
]);
|
||||
|
||||
const result = await service.createLessonAttendanceFromDingTalk(4, '2026-07-11', 21);
|
||||
@@ -289,12 +320,38 @@ describe('AttendanceService \u2014 DingTalk course attendance', () => {
|
||||
expect(savedRecords).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ studentId: 1, status: 'leave' }),
|
||||
expect.objectContaining({ studentId: 2, status: 'late' }),
|
||||
expect.objectContaining({ studentId: 2, status: 'present' }),
|
||||
]),
|
||||
);
|
||||
expect(result.records).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('final settlement overrides interim manual status using the final punch result', async () => {
|
||||
const { service, attendanceRepo, dingRawRepo, scheduleRepo, classStudentRepo, sessionRepo } =
|
||||
createService();
|
||||
scheduleRepo.findOne.mockResolvedValue(endedSchedule);
|
||||
sessionRepo.findOne.mockResolvedValue({
|
||||
id: 90,
|
||||
scheduleId: 4,
|
||||
classId: 8,
|
||||
lessonDate: '2026-07-11',
|
||||
status: 'in_progress',
|
||||
});
|
||||
attendanceRepo.find.mockResolvedValue([
|
||||
{ id: 101, studentId: 1, attendanceSessionId: 90, status: 'present', source: 'manual' },
|
||||
]);
|
||||
classStudentRepo.find.mockResolvedValue([
|
||||
{ studentId: 1, student: { id: 1, name: '张三' } },
|
||||
]);
|
||||
dingRawRepo.find.mockResolvedValue([]);
|
||||
|
||||
await service.createLessonAttendanceFromDingTalk(4, '2026-07-11', 21, true);
|
||||
|
||||
expect(attendanceRepo.save).toHaveBeenCalledWith([
|
||||
expect.objectContaining({ studentId: 1, status: 'absent' }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects completion when pending records exist', async () => {
|
||||
const { service, sessionRepo, attendanceRepo } = createService();
|
||||
sessionRepo.findOne.mockResolvedValue({
|
||||
@@ -368,8 +425,8 @@ describe('AttendanceService \u2014 DingTalk course attendance', () => {
|
||||
{ studentId: 2, student: { id: 2, name: '李四' } },
|
||||
]);
|
||||
dingRawRepo.find.mockResolvedValue([
|
||||
{ matchedStudentId: 1, attendanceType: 'OnDuty', timeResult: 'Normal' },
|
||||
{ matchedStudentId: 2, attendanceType: 'OnDuty', timeResult: 'Normal' },
|
||||
{ matchedStudentId: 1, attendanceType: 'OnDuty', timeResult: 'Normal', checkInTime: new Date('2026-07-11T08:55:00+08:00') },
|
||||
{ matchedStudentId: 2, attendanceType: 'OnDuty', timeResult: 'Normal', checkInTime: new Date('2026-07-11T08:55:00+08:00') },
|
||||
]);
|
||||
|
||||
// Step 1: update the record to absent via generic update()
|
||||
|
||||
Reference in New Issue
Block a user