feat: 优化排课管理
This commit is contained in:
@@ -6,40 +6,14 @@ import {
|
||||
TimeSlotSource,
|
||||
MembershipStatus,
|
||||
BookingStatus,
|
||||
getDefaultTimeSlots,
|
||||
} from '@mp-pilates/shared'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Return a Date whose JS getDay() maps to the given ISO weekday (1=Mon…7=Sun) */
|
||||
function dateForIsoWeekday(isoWeekday: number): Date {
|
||||
const base = new Date('2026-04-06T00:00:00Z') // Monday
|
||||
const d = new Date(base)
|
||||
d.setDate(base.getDate() + (isoWeekday - 1))
|
||||
return d
|
||||
}
|
||||
|
||||
const makeTemplate = (overrides: Record<string, unknown> = {}) => ({
|
||||
id: 'tpl-1',
|
||||
dayOfWeek: 1,
|
||||
startTime: '09:00',
|
||||
endTime: '10:00',
|
||||
capacity: 1,
|
||||
isActive: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock PrismaService
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockPrisma = {
|
||||
weekTemplate: {
|
||||
findMany: jest.fn(),
|
||||
},
|
||||
timeSlot: {
|
||||
createMany: jest.fn(),
|
||||
updateMany: jest.fn(),
|
||||
@@ -77,26 +51,12 @@ describe('SlotGeneratorService', () => {
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
describe('generateSlots', () => {
|
||||
it('returns 0 when there are no active templates', async () => {
|
||||
mockPrisma.weekTemplate.findMany.mockResolvedValueOnce([])
|
||||
it('creates slots for every day using the default schedule (14 slots per day)', async () => {
|
||||
const defaultSlots = getDefaultTimeSlots()
|
||||
mockPrisma.timeSlot.createMany.mockResolvedValueOnce({ count: defaultSlots.length * 7 })
|
||||
|
||||
const count = await service.generateSlots(7)
|
||||
|
||||
expect(count).toBe(0)
|
||||
expect(mockPrisma.timeSlot.createMany).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('creates correct number of slots from templates', async () => {
|
||||
// 2 templates, both for Monday (ISO 1)
|
||||
mockPrisma.weekTemplate.findMany.mockResolvedValueOnce([
|
||||
makeTemplate({ id: 'tpl-1', dayOfWeek: 1, startTime: '09:00', endTime: '10:00' }),
|
||||
makeTemplate({ id: 'tpl-2', dayOfWeek: 1, startTime: '10:00', endTime: '11:00' }),
|
||||
])
|
||||
mockPrisma.timeSlot.createMany.mockResolvedValueOnce({ count: 2 })
|
||||
|
||||
// Use 7 days — will hit exactly one Monday
|
||||
const count = await service.generateSlots(7)
|
||||
|
||||
expect(mockPrisma.timeSlot.createMany).toHaveBeenCalledTimes(1)
|
||||
const { data, skipDuplicates } =
|
||||
mockPrisma.timeSlot.createMany.mock.calls[0][0] as {
|
||||
@@ -104,23 +64,30 @@ describe('SlotGeneratorService', () => {
|
||||
skipDuplicates: boolean
|
||||
}
|
||||
expect(skipDuplicates).toBe(true)
|
||||
// Both templates should appear in the batch (may include more days)
|
||||
const mondaySlots = (
|
||||
data as Array<{ startTime: string; source: TimeSlotSource }>
|
||||
).filter(
|
||||
(s) => s.startTime === '09:00' || s.startTime === '10:00',
|
||||
)
|
||||
expect(mondaySlots.length).toBeGreaterThanOrEqual(2)
|
||||
expect(count).toBe(2)
|
||||
// 7 days × 14 slots per day = 98
|
||||
expect(data).toHaveLength(defaultSlots.length * 7)
|
||||
expect(count).toBe(defaultSlots.length * 7)
|
||||
})
|
||||
|
||||
it('creates 14 slots per day (08:00-22:00 hourly)', async () => {
|
||||
mockPrisma.timeSlot.createMany.mockResolvedValueOnce({ count: 14 })
|
||||
|
||||
await service.generateSlots(1)
|
||||
|
||||
const { data } = mockPrisma.timeSlot.createMany.mock.calls[0][0] as {
|
||||
data: Array<{ startTime: string; endTime: string }>
|
||||
}
|
||||
expect(data).toHaveLength(14)
|
||||
expect(data[0].startTime).toBe('08:00')
|
||||
expect(data[0].endTime).toBe('09:00')
|
||||
expect(data[13].startTime).toBe('21:00')
|
||||
expect(data[13].endTime).toBe('22:00')
|
||||
})
|
||||
|
||||
it('passes skipDuplicates: true to handle existing date+time combinations', async () => {
|
||||
mockPrisma.weekTemplate.findMany.mockResolvedValueOnce([
|
||||
makeTemplate({ dayOfWeek: 1 }),
|
||||
])
|
||||
mockPrisma.timeSlot.createMany.mockResolvedValueOnce({ count: 0 })
|
||||
|
||||
await service.generateSlots(7)
|
||||
await service.generateSlots(1)
|
||||
|
||||
const call = mockPrisma.timeSlot.createMany.mock.calls[0][0] as {
|
||||
skipDuplicates: boolean
|
||||
@@ -128,54 +95,17 @@ describe('SlotGeneratorService', () => {
|
||||
expect(call.skipDuplicates).toBe(true)
|
||||
})
|
||||
|
||||
it('maps Sunday (JS getDay()=0) to ISO weekday 7', async () => {
|
||||
// Template for Sunday (ISO 7)
|
||||
mockPrisma.weekTemplate.findMany.mockResolvedValueOnce([
|
||||
makeTemplate({ id: 'tpl-sun', dayOfWeek: 7, startTime: '08:00', endTime: '09:00' }),
|
||||
])
|
||||
mockPrisma.timeSlot.createMany.mockResolvedValueOnce({ count: 1 })
|
||||
it('sets source to TEMPLATE for all generated slots', async () => {
|
||||
mockPrisma.timeSlot.createMany.mockResolvedValueOnce({ count: 14 })
|
||||
|
||||
// 7 days will cover exactly one Sunday
|
||||
const count = await service.generateSlots(7)
|
||||
|
||||
expect(mockPrisma.timeSlot.createMany).toHaveBeenCalledTimes(1)
|
||||
const { data } = mockPrisma.timeSlot.createMany.mock.calls[0][0] as {
|
||||
data: Array<{ startTime: string; source: TimeSlotSource }>
|
||||
}
|
||||
const sundaySlots = data.filter((s) => s.startTime === '08:00')
|
||||
expect(sundaySlots.length).toBeGreaterThanOrEqual(1)
|
||||
expect(sundaySlots[0].source).toBe(TimeSlotSource.TEMPLATE)
|
||||
expect(count).toBe(1)
|
||||
})
|
||||
|
||||
it('maps Monday (JS getDay()=1) to ISO weekday 1', async () => {
|
||||
mockPrisma.weekTemplate.findMany.mockResolvedValueOnce([
|
||||
makeTemplate({ id: 'tpl-mon', dayOfWeek: 1, startTime: '07:00', endTime: '08:00' }),
|
||||
])
|
||||
mockPrisma.timeSlot.createMany.mockResolvedValueOnce({ count: 1 })
|
||||
|
||||
await service.generateSlots(7)
|
||||
await service.generateSlots(1)
|
||||
|
||||
const { data } = mockPrisma.timeSlot.createMany.mock.calls[0][0] as {
|
||||
data: Array<{ startTime: string }>
|
||||
data: Array<{ source: TimeSlotSource }>
|
||||
}
|
||||
const mondaySlots = data.filter((s) => s.startTime === '07:00')
|
||||
expect(mondaySlots.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('attaches templateId from the matching template', async () => {
|
||||
mockPrisma.weekTemplate.findMany.mockResolvedValueOnce([
|
||||
makeTemplate({ id: 'tpl-xyz', dayOfWeek: 2 }),
|
||||
])
|
||||
mockPrisma.timeSlot.createMany.mockResolvedValueOnce({ count: 1 })
|
||||
|
||||
await service.generateSlots(7)
|
||||
|
||||
const { data } = mockPrisma.timeSlot.createMany.mock.calls[0][0] as {
|
||||
data: Array<{ templateId: string }>
|
||||
for (const slot of data) {
|
||||
expect(slot.source).toBe(TimeSlotSource.TEMPLATE)
|
||||
}
|
||||
const tuesdaySlots = data.filter((s) => s.templateId === 'tpl-xyz')
|
||||
expect(tuesdaySlots.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user