Task 3: sync RENTAL class_schedule on rental create/update/delete
- ClassroomRentalsService.create/update now upsert a ClassSchedule row with schedule_type='RENTAL' and rental_id set. - ClassroomRentalsService.remove deletes the synced schedule row; update to 'cancelled' also removes it. - SchedulesService.getClassroomOccupancy explicitly returns INTERNAL and RENTAL schedules. - Make classId/teacherId/rentalId nullable in ClassSchedule entity to support rental schedules. - Add unit tests for rental schedule sync and mixed-type occupancy.
This commit is contained in:
@@ -85,7 +85,7 @@ export class SchedulesController {
|
||||
const conflicts = await this.service.checkConflict(
|
||||
dto.classroomId, dto.weekDay, dto.startTime, dto.endTime, dto.startDate, dto.endDate,
|
||||
);
|
||||
const teacherIds = [...new Set(conflicts.map(c => c.teacherId).filter(Boolean))];
|
||||
const teacherIds = [...new Set(conflicts.map(c => c.teacherId).filter((id): id is number => id != null))];
|
||||
if (teacherIds.length > 0) {
|
||||
void this.notificationsService.create({
|
||||
recipientIds: teacherIds,
|
||||
@@ -129,7 +129,7 @@ export class SchedulesController {
|
||||
const conflicts = await this.service.checkConflict(
|
||||
existing.classroomId, existing.weekDay, existing.startTime, existing.endTime, existing.startDate, existing.endDate,
|
||||
);
|
||||
const teacherIds = [...new Set(conflicts.map(c => c.teacherId).filter(Boolean))];
|
||||
const teacherIds = [...new Set(conflicts.map(c => c.teacherId).filter((id): id is number => id != null))];
|
||||
if (teacherIds.length > 0) {
|
||||
void this.notificationsService.create({
|
||||
recipientIds: teacherIds,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { ConflictException } from '@nestjs/common';
|
||||
import { Repository } from 'typeorm';
|
||||
import { SchedulesService } from './schedules.service';
|
||||
import { ClassSchedule } from '../entities/class-schedule.entity';
|
||||
import { ClassSchedule, ScheduleType } from '../entities/class-schedule.entity';
|
||||
import { ClassroomRental } from '../entities/classroom-rental.entity';
|
||||
import { Class } from '../entities/class.entity';
|
||||
import { CampusScope } from '../common/campus-scope';
|
||||
@@ -13,6 +13,8 @@ function mockQueryBuilder<T>(results: T[] = []) {
|
||||
const qb = {
|
||||
where: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
orderBy: jest.fn().mockReturnThis(),
|
||||
addOrderBy: jest.fn().mockReturnThis(),
|
||||
getMany: jest.fn().mockResolvedValue(results),
|
||||
};
|
||||
return qb;
|
||||
@@ -124,3 +126,57 @@ describe('SchedulesService — checkConflict', () => {
|
||||
).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SchedulesService — getClassroomOccupancy', () => {
|
||||
let service: SchedulesService;
|
||||
let scheduleRepo: jest.Mocked<Pick<Repository<ClassSchedule>, 'createQueryBuilder'>>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SchedulesService,
|
||||
{ provide: getRepositoryToken(ClassSchedule), useValue: { createQueryBuilder: jest.fn() } },
|
||||
{ provide: getRepositoryToken(Class), useValue: { find: jest.fn().mockResolvedValue([]) } },
|
||||
{ provide: getRepositoryToken(ClassroomRental), useValue: { createQueryBuilder: jest.fn() } },
|
||||
{
|
||||
provide: CampusScope,
|
||||
useValue: {
|
||||
getScopeDepartmentIds: jest.fn().mockResolvedValue(null),
|
||||
filter: jest.fn((w: unknown) => w),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<SchedulesService>(SchedulesService);
|
||||
scheduleRepo = module.get(getRepositoryToken(ClassSchedule));
|
||||
});
|
||||
|
||||
it('returns both INTERNAL and RENTAL schedules', async () => {
|
||||
const internalSchedule = {
|
||||
id: 1,
|
||||
scheduleType: ScheduleType.INTERNAL,
|
||||
subject: '数学',
|
||||
} as ClassSchedule;
|
||||
const rentalSchedule = {
|
||||
id: 2,
|
||||
scheduleType: ScheduleType.RENTAL,
|
||||
subject: 'Tenant A 租赁',
|
||||
} as ClassSchedule;
|
||||
const qb = mockQueryBuilder<ClassSchedule>([internalSchedule, rentalSchedule]);
|
||||
(scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);
|
||||
|
||||
const result = await service.getClassroomOccupancy(1, '2026-03-15');
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((s) => s.scheduleType)).toContain(ScheduleType.INTERNAL);
|
||||
expect(result.map((s) => s.scheduleType)).toContain(ScheduleType.RENTAL);
|
||||
expect(qb.where).toHaveBeenCalledWith('cs.classroomId = :classroomId', { classroomId: 1 });
|
||||
expect(qb.andWhere).toHaveBeenCalledWith('cs.status = :status', { status: 'active' });
|
||||
expect(qb.andWhere).toHaveBeenCalledWith('cs.scheduleType IN (:...scheduleTypes)', {
|
||||
scheduleTypes: [ScheduleType.INTERNAL, ScheduleType.RENTAL],
|
||||
});
|
||||
expect(qb.andWhere).toHaveBeenCalledWith('cs.startDate <= :date', { date: '2026-03-15' });
|
||||
expect(qb.andWhere).toHaveBeenCalledWith('cs.endDate >= :date', { date: '2026-03-15' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,7 +74,7 @@ export class SchedulesService {
|
||||
|
||||
await this.checkConflict(classroomId, weekDay, startTime, endTime, startDate, endDate, id);
|
||||
|
||||
await this.scheduleRepo.update(id, dto as Record<string, unknown>);
|
||||
await this.scheduleRepo.update(id, dto);
|
||||
return this.findOne(id);
|
||||
}
|
||||
|
||||
@@ -168,7 +168,8 @@ export class SchedulesService {
|
||||
const qb = this.scheduleRepo
|
||||
.createQueryBuilder('cs')
|
||||
.where('cs.classroomId = :classroomId', { classroomId })
|
||||
.andWhere('cs.status = :status', { status: 'active' });
|
||||
.andWhere('cs.status = :status', { status: 'active' })
|
||||
.andWhere('cs.scheduleType IN (:...scheduleTypes)', { scheduleTypes: ['INTERNAL', 'RENTAL'] });
|
||||
|
||||
if (date) {
|
||||
qb.andWhere('cs.startDate <= :date', { date })
|
||||
|
||||
Reference in New Issue
Block a user