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:
2026-07-06 17:55:08 +08:00
parent 5ea49d0a9e
commit 38f6e18cca
6 changed files with 310 additions and 10 deletions

View File

@@ -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' });
});
});