forked from wangziqi/gongxue-base
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:
@@ -8,6 +8,7 @@ import { Classroom } from '../entities/classroom.entity';
|
||||
import { Tenant } from '../entities/tenant.entity';
|
||||
import { ClassSchedule } from '../entities/class-schedule.entity';
|
||||
import { CampusScope } from '../common/campus-scope';
|
||||
import { CreateRentalDto, UpdateRentalDto } from './dto/rental.dto';
|
||||
|
||||
function mockQueryBuilder<T>(results: T[] = []) {
|
||||
const qb = {
|
||||
@@ -87,3 +88,206 @@ describe('ClassroomRentalsService — findConflicts', () => {
|
||||
expect(rentalQb.andWhere).toHaveBeenCalledWith('r.id != :excludeId', { excludeId: 99 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('ClassroomRentalsService — rental schedule sync', () => {
|
||||
let service: ClassroomRentalsService;
|
||||
let rentalRepo: jest.Mocked<
|
||||
Pick<Repository<ClassroomRental>, 'findOne' | 'save' | 'create' | 'update' | 'delete' | 'createQueryBuilder'>
|
||||
>;
|
||||
let classroomRepo: jest.Mocked<Pick<Repository<Classroom>, 'findOne'>>;
|
||||
let tenantRepo: jest.Mocked<Pick<Repository<Tenant>, 'findOne'>>;
|
||||
let scheduleRepo: jest.Mocked<
|
||||
Pick<Repository<ClassSchedule>, 'findOne' | 'save' | 'create' | 'update' | 'delete' | 'createQueryBuilder'>
|
||||
>;
|
||||
|
||||
beforeEach(async () => {
|
||||
rentalRepo = {
|
||||
findOne: jest.fn(),
|
||||
save: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
createQueryBuilder: jest.fn(),
|
||||
} as jest.Mocked<
|
||||
Pick<Repository<ClassroomRental>, 'findOne' | 'save' | 'create' | 'update' | 'delete' | 'createQueryBuilder'>
|
||||
>;
|
||||
|
||||
classroomRepo = { findOne: jest.fn() } as jest.Mocked<Pick<Repository<Classroom>, 'findOne'>>;
|
||||
|
||||
tenantRepo = { findOne: jest.fn() } as jest.Mocked<Pick<Repository<Tenant>, 'findOne'>>;
|
||||
|
||||
scheduleRepo = {
|
||||
findOne: jest.fn(),
|
||||
save: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
createQueryBuilder: jest.fn(),
|
||||
} as jest.Mocked<
|
||||
Pick<Repository<ClassSchedule>, 'findOne' | 'save' | 'create' | 'update' | 'delete' | 'createQueryBuilder'>
|
||||
>;
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ClassroomRentalsService,
|
||||
{ provide: getRepositoryToken(ClassroomRental), useValue: rentalRepo },
|
||||
{ provide: getRepositoryToken(Classroom), useValue: classroomRepo },
|
||||
{ provide: getRepositoryToken(Tenant), useValue: tenantRepo },
|
||||
{ provide: getRepositoryToken(ClassSchedule), useValue: scheduleRepo },
|
||||
{ provide: CampusScope, useValue: { getScopeDepartmentIds: jest.fn().mockResolvedValue(null) } },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ClassroomRentalsService>(ClassroomRentalsService);
|
||||
});
|
||||
|
||||
describe('create()', () => {
|
||||
it('saves the rental and creates a RENTAL class_schedule row', async () => {
|
||||
const dto: CreateRentalDto = {
|
||||
classroomId: 1,
|
||||
tenantId: 2,
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2026-03-31',
|
||||
};
|
||||
const classroom = { id: 1, departmentId: 10 } as Classroom;
|
||||
const tenant = { id: 2, name: 'Tenant A' } as Tenant;
|
||||
|
||||
classroomRepo.findOne.mockResolvedValue(classroom);
|
||||
tenantRepo.findOne.mockResolvedValue(tenant);
|
||||
rentalRepo.create.mockImplementation((entity) => ({ ...(entity as object) } as ClassroomRental));
|
||||
rentalRepo.save.mockImplementation((entity) => Promise.resolve({ ...(entity as object), id: 1 } as ClassroomRental));
|
||||
rentalRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder<ClassroomRental>([]));
|
||||
scheduleRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder<ClassSchedule>([]));
|
||||
scheduleRepo.findOne.mockResolvedValue(null);
|
||||
scheduleRepo.create.mockImplementation((entity) => ({ ...(entity as object) } as ClassSchedule));
|
||||
scheduleRepo.save.mockImplementation((entity) => Promise.resolve({ ...(entity as object), id: 100 } as ClassSchedule));
|
||||
|
||||
const result = await service.create(dto);
|
||||
|
||||
expect(result.id).toBe(1);
|
||||
expect(rentalRepo.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
classroomId: 1,
|
||||
tenantId: 2,
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2026-03-31',
|
||||
status: 'active',
|
||||
departmentId: 10,
|
||||
}),
|
||||
);
|
||||
expect(scheduleRepo.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
classroomId: 1,
|
||||
classId: null,
|
||||
startTime: '00:00',
|
||||
endTime: '23:59',
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2026-03-31',
|
||||
subject: 'Tenant A 租赁',
|
||||
teacherId: null,
|
||||
scheduleType: 'RENTAL',
|
||||
rentalId: 1,
|
||||
status: 'active',
|
||||
departmentId: 10,
|
||||
}),
|
||||
);
|
||||
expect(scheduleRepo.save).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('update()', () => {
|
||||
it('updates the existing RENTAL schedule row when dates or classroom change', async () => {
|
||||
const existingRental = {
|
||||
id: 1,
|
||||
classroomId: 1,
|
||||
tenantId: 2,
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2026-03-31',
|
||||
status: 'active',
|
||||
notes: '',
|
||||
departmentId: 10,
|
||||
tenant: { id: 2, name: 'Tenant A' } as Tenant,
|
||||
classroom: { id: 1 } as Classroom,
|
||||
} as ClassroomRental;
|
||||
const updatedRental = {
|
||||
...existingRental,
|
||||
startDate: '2026-04-01',
|
||||
endDate: '2026-04-30',
|
||||
} as ClassroomRental;
|
||||
const existingSchedule = { id: 50, rentalId: 1, scheduleType: 'RENTAL', classroomId: 1 } as ClassSchedule;
|
||||
|
||||
rentalRepo.findOne.mockResolvedValueOnce(existingRental).mockResolvedValueOnce(updatedRental);
|
||||
rentalRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder<ClassroomRental>([]));
|
||||
scheduleRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder<ClassSchedule>([]));
|
||||
scheduleRepo.findOne.mockResolvedValue(existingSchedule);
|
||||
|
||||
const dto: UpdateRentalDto = { startDate: '2026-04-01', endDate: '2026-04-30' };
|
||||
await service.update(1, dto);
|
||||
|
||||
expect(rentalRepo.update).toHaveBeenCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ startDate: '2026-04-01', endDate: '2026-04-30' }),
|
||||
);
|
||||
expect(scheduleRepo.update).toHaveBeenCalledWith(
|
||||
50,
|
||||
expect.objectContaining({
|
||||
scheduleType: 'RENTAL',
|
||||
rentalId: 1,
|
||||
classroomId: 1,
|
||||
startDate: '2026-04-01',
|
||||
endDate: '2026-04-30',
|
||||
status: 'active',
|
||||
subject: 'Tenant A 租赁',
|
||||
}),
|
||||
);
|
||||
expect(scheduleRepo.create).not.toHaveBeenCalled();
|
||||
expect(scheduleRepo.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('deletes the RENTAL schedule row when status changes to cancelled', async () => {
|
||||
const rental = {
|
||||
id: 1,
|
||||
classroomId: 1,
|
||||
tenantId: 2,
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2026-03-31',
|
||||
status: 'active',
|
||||
departmentId: 10,
|
||||
tenant: { id: 2, name: 'Tenant A' } as Tenant,
|
||||
} as ClassroomRental;
|
||||
const cancelledRental = { ...rental, status: 'cancelled' } as ClassroomRental;
|
||||
|
||||
rentalRepo.findOne.mockResolvedValueOnce(rental).mockResolvedValueOnce(cancelledRental);
|
||||
|
||||
await service.update(1, { status: 'cancelled' });
|
||||
|
||||
expect(rentalRepo.update).toHaveBeenCalledWith(1, { status: 'cancelled' });
|
||||
expect(scheduleRepo.delete).toHaveBeenCalledWith({ rentalId: 1, scheduleType: 'RENTAL' });
|
||||
expect(scheduleRepo.findOne).not.toHaveBeenCalled();
|
||||
expect(scheduleRepo.update).not.toHaveBeenCalled();
|
||||
expect(scheduleRepo.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove()', () => {
|
||||
it('deletes the RENTAL schedule row and the rental', async () => {
|
||||
const rental = {
|
||||
id: 1,
|
||||
classroomId: 1,
|
||||
tenantId: 2,
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2026-03-31',
|
||||
status: 'active',
|
||||
departmentId: 10,
|
||||
tenant: { id: 2, name: 'Tenant A' } as Tenant,
|
||||
} as ClassroomRental;
|
||||
|
||||
rentalRepo.findOne.mockResolvedValue(rental);
|
||||
|
||||
await service.remove(1);
|
||||
|
||||
expect(scheduleRepo.delete).toHaveBeenCalledWith({ rentalId: 1, scheduleType: 'RENTAL' });
|
||||
expect(rentalRepo.delete).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -198,6 +198,45 @@ export class ClassroomRentalsService {
|
||||
return { message: '删除成功' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步租赁订单到 class_schedules(schedule_type = 'RENTAL')
|
||||
*/
|
||||
private async syncScheduleFromRental(rental: ClassroomRental, tenantName?: string) {
|
||||
const name = tenantName || rental.tenant?.name || '租赁方';
|
||||
const weekDay = this.dateToWeekDay(rental.startDate);
|
||||
let schedule = await this.scheduleRepo.findOne({
|
||||
where: { rentalId: rental.id, scheduleType: 'RENTAL' },
|
||||
});
|
||||
const data = {
|
||||
classroomId: rental.classroomId,
|
||||
classId: null,
|
||||
weekDay,
|
||||
startTime: '00:00',
|
||||
endTime: '23:59',
|
||||
startDate: rental.startDate,
|
||||
endDate: rental.endDate,
|
||||
subject: `${name} 租赁`,
|
||||
teacherId: null,
|
||||
scheduleType: 'RENTAL',
|
||||
rentalId: rental.id,
|
||||
status: 'active',
|
||||
notes: rental.notes,
|
||||
departmentId: rental.departmentId,
|
||||
};
|
||||
if (schedule) {
|
||||
await this.scheduleRepo.update(schedule.id, data);
|
||||
} else {
|
||||
schedule = this.scheduleRepo.create(data);
|
||||
await this.scheduleRepo.save(schedule);
|
||||
}
|
||||
}
|
||||
|
||||
private dateToWeekDay(date: string): number {
|
||||
const d = new Date(date);
|
||||
const day = d.getDay();
|
||||
return day === 0 ? 7 : day;
|
||||
}
|
||||
|
||||
async attachContract(id: number, file: Express.Multer.File) {
|
||||
const rental = await this.findOne(id);
|
||||
this.ensureUploadDir();
|
||||
|
||||
Reference in New Issue
Block a user