Files
lunarfront-app/packages/backend/src/routes/v1/lessons.ts
Ryan Moon 7680a73d88 Add Phase 8: lesson plan templates with deep-copy instantiation
- New tables: lesson_plan_template, lesson_plan_template_section, lesson_plan_template_item
- skill_level enum: beginner, intermediate, advanced, all_levels
- Templates are reusable curriculum definitions independent of any member/enrollment
- POST /lesson-plan-templates/:id/create-plan deep-copies the template into a member plan
- Instantiation uses template name as default plan title, accepts custom title override
- Instantiation deactivates any existing active plan on the enrollment (one-active rule)
- Plan items are independent copies — renaming the template does not affect existing plans
- 11 new integration tests
2026-03-30 10:37:30 -05:00

541 lines
28 KiB
TypeScript

import type { FastifyPluginAsync } from 'fastify'
import {
PaginationSchema,
InstructorCreateSchema,
InstructorUpdateSchema,
LessonTypeCreateSchema,
LessonTypeUpdateSchema,
ScheduleSlotCreateSchema,
ScheduleSlotUpdateSchema,
EnrollmentCreateSchema,
EnrollmentUpdateSchema,
EnrollmentStatusUpdateSchema,
LessonSessionStatusUpdateSchema,
LessonSessionNotesSchema,
LessonSessionUpdateSchema,
GradingScaleCreateSchema,
GradingScaleUpdateSchema,
LessonPlanCreateSchema,
LessonPlanUpdateSchema,
LessonPlanItemUpdateSchema,
GradeCreateSchema,
SessionPlanItemsSchema,
LessonPlanTemplateCreateSchema,
LessonPlanTemplateUpdateSchema,
TemplateInstantiateSchema,
InstructorBlockedDateCreateSchema,
StoreClosureCreateSchema,
} from '@lunarfront/shared/schemas'
import { InstructorService, LessonTypeService, ScheduleSlotService, EnrollmentService, LessonSessionService, GradingScaleService, LessonPlanService, LessonPlanItemService, LessonPlanTemplateService, GradeHistoryService, SessionPlanItemService, InstructorBlockedDateService, StoreClosureService } from '../../services/lesson.service.js'
export const lessonRoutes: FastifyPluginAsync = async (app) => {
// --- Instructors ---
app.post('/instructors', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const parsed = InstructorCreateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const instructor = await InstructorService.create(app.db, parsed.data)
return reply.status(201).send(instructor)
})
app.get('/instructors', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const params = PaginationSchema.parse(request.query)
const result = await InstructorService.list(app.db, params)
return reply.send(result)
})
app.get('/instructors/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const instructor = await InstructorService.getById(app.db, id)
if (!instructor) return reply.status(404).send({ error: { message: 'Instructor not found', statusCode: 404 } })
return reply.send(instructor)
})
app.patch('/instructors/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = InstructorUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const instructor = await InstructorService.update(app.db, id, parsed.data)
if (!instructor) return reply.status(404).send({ error: { message: 'Instructor not found', statusCode: 404 } })
return reply.send(instructor)
})
app.delete('/instructors/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const instructor = await InstructorService.delete(app.db, id)
if (!instructor) return reply.status(404).send({ error: { message: 'Instructor not found', statusCode: 404 } })
return reply.send(instructor)
})
// --- Lesson Types ---
app.post('/lesson-types', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const parsed = LessonTypeCreateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const lessonType = await LessonTypeService.create(app.db, parsed.data)
return reply.status(201).send(lessonType)
})
app.get('/lesson-types', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const params = PaginationSchema.parse(request.query)
const result = await LessonTypeService.list(app.db, params)
return reply.send(result)
})
app.get('/lesson-types/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const lessonType = await LessonTypeService.getById(app.db, id)
if (!lessonType) return reply.status(404).send({ error: { message: 'Lesson type not found', statusCode: 404 } })
return reply.send(lessonType)
})
app.patch('/lesson-types/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = LessonTypeUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const lessonType = await LessonTypeService.update(app.db, id, parsed.data)
if (!lessonType) return reply.status(404).send({ error: { message: 'Lesson type not found', statusCode: 404 } })
return reply.send(lessonType)
})
app.delete('/lesson-types/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const lessonType = await LessonTypeService.delete(app.db, id)
if (!lessonType) return reply.status(404).send({ error: { message: 'Lesson type not found', statusCode: 404 } })
return reply.send(lessonType)
})
// --- Schedule Slots ---
app.post('/schedule-slots', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const parsed = ScheduleSlotCreateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const result = await ScheduleSlotService.create(app.db, parsed.data)
if ('error' in result) {
return reply.status(409).send({ error: { message: result.error, statusCode: 409 } })
}
return reply.status(201).send(result)
})
app.get('/schedule-slots', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const query = request.query as Record<string, string | undefined>
const params = PaginationSchema.parse(query)
const filters = {
instructorId: query.instructorId,
dayOfWeek: query.dayOfWeek !== undefined ? Number(query.dayOfWeek) : undefined,
}
const result = await ScheduleSlotService.list(app.db, params, filters)
return reply.send(result)
})
app.get('/schedule-slots/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const slot = await ScheduleSlotService.getById(app.db, id)
if (!slot) return reply.status(404).send({ error: { message: 'Schedule slot not found', statusCode: 404 } })
return reply.send(slot)
})
app.patch('/schedule-slots/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = ScheduleSlotUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const result = await ScheduleSlotService.update(app.db, id, parsed.data)
if (!result) return reply.status(404).send({ error: { message: 'Schedule slot not found', statusCode: 404 } })
if ('error' in result) {
return reply.status(409).send({ error: { message: result.error, statusCode: 409 } })
}
return reply.send(result)
})
app.delete('/schedule-slots/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const slot = await ScheduleSlotService.delete(app.db, id)
if (!slot) return reply.status(404).send({ error: { message: 'Schedule slot not found', statusCode: 404 } })
return reply.send(slot)
})
// --- Enrollments ---
app.post('/enrollments', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const parsed = EnrollmentCreateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const result = await EnrollmentService.create(app.db, parsed.data)
if ('error' in result) {
return reply.status(409).send({ error: { message: result.error, statusCode: 409 } })
}
return reply.status(201).send(result)
})
app.get('/enrollments', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const query = request.query as Record<string, string | undefined>
const params = PaginationSchema.parse(query)
const filters = {
memberId: query.memberId,
accountId: query.accountId,
instructorId: query.instructorId,
status: query.status?.split(',').filter(Boolean),
}
const result = await EnrollmentService.list(app.db, params, filters)
return reply.send(result)
})
app.get('/enrollments/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const enrollment = await EnrollmentService.getById(app.db, id)
if (!enrollment) return reply.status(404).send({ error: { message: 'Enrollment not found', statusCode: 404 } })
return reply.send(enrollment)
})
app.patch('/enrollments/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = EnrollmentUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const enrollment = await EnrollmentService.update(app.db, id, parsed.data)
if (!enrollment) return reply.status(404).send({ error: { message: 'Enrollment not found', statusCode: 404 } })
return reply.send(enrollment)
})
app.post('/enrollments/:id/status', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = EnrollmentStatusUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const enrollment = await EnrollmentService.updateStatus(app.db, id, parsed.data.status)
if (!enrollment) return reply.status(404).send({ error: { message: 'Enrollment not found', statusCode: 404 } })
return reply.send(enrollment)
})
app.post('/enrollments/:id/generate-sessions', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const query = request.query as Record<string, string | undefined>
const weeks = query.weeks ? Number(query.weeks) : 4
const sessions = await LessonSessionService.generateSessions(app.db, id, weeks)
return reply.send({ generated: sessions.length, sessions })
})
// --- Lesson Sessions ---
app.get('/lesson-sessions', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const query = request.query as Record<string, string | undefined>
const params = PaginationSchema.parse(query)
const filters = {
enrollmentId: query.enrollmentId,
instructorId: query.instructorId,
status: query.status?.split(',').filter(Boolean),
dateFrom: query.dateFrom,
dateTo: query.dateTo,
}
const result = await LessonSessionService.list(app.db, params, filters)
return reply.send(result)
})
app.get('/lesson-sessions/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const session = await LessonSessionService.getById(app.db, id)
if (!session) return reply.status(404).send({ error: { message: 'Lesson session not found', statusCode: 404 } })
return reply.send(session)
})
app.patch('/lesson-sessions/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = LessonSessionUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const result = await LessonSessionService.update(app.db, id, parsed.data)
if (!result) return reply.status(404).send({ error: { message: 'Lesson session not found', statusCode: 404 } })
if ('error' in result) return reply.status(409).send({ error: { message: result.error, statusCode: 409 } })
return reply.send(result)
})
app.post('/lesson-sessions/:id/status', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = LessonSessionStatusUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const session = await LessonSessionService.updateStatus(app.db, id, parsed.data.status)
if (!session) return reply.status(404).send({ error: { message: 'Lesson session not found', statusCode: 404 } })
return reply.send(session)
})
app.post('/lesson-sessions/:id/notes', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = LessonSessionNotesSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const session = await LessonSessionService.updateNotes(app.db, id, parsed.data)
if (!session) return reply.status(404).send({ error: { message: 'Lesson session not found', statusCode: 404 } })
return reply.send(session)
})
// --- Grading Scales ---
app.post('/grading-scales', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const parsed = GradingScaleCreateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const scale = await GradingScaleService.create(app.db, parsed.data, request.user.id)
return reply.status(201).send(scale)
})
app.get('/grading-scales', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const params = PaginationSchema.parse(request.query)
const result = await GradingScaleService.list(app.db, params)
return reply.send(result)
})
app.get('/grading-scales/all', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (_request, reply) => {
const scales = await GradingScaleService.listAll(app.db)
return reply.send(scales)
})
app.get('/grading-scales/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const scale = await GradingScaleService.getById(app.db, id)
if (!scale) return reply.status(404).send({ error: { message: 'Grading scale not found', statusCode: 404 } })
return reply.send(scale)
})
app.patch('/grading-scales/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = GradingScaleUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const scale = await GradingScaleService.update(app.db, id, parsed.data)
if (!scale) return reply.status(404).send({ error: { message: 'Grading scale not found', statusCode: 404 } })
return reply.send(scale)
})
app.delete('/grading-scales/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const scale = await GradingScaleService.delete(app.db, id)
if (!scale) return reply.status(404).send({ error: { message: 'Grading scale not found', statusCode: 404 } })
return reply.send(scale)
})
// --- Lesson Plans ---
app.post('/lesson-plans', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const parsed = LessonPlanCreateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const plan = await LessonPlanService.create(app.db, parsed.data, request.user.id)
return reply.status(201).send(plan)
})
app.get('/lesson-plans', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const query = request.query as Record<string, string | undefined>
const params = PaginationSchema.parse(query)
const filters = {
enrollmentId: query.enrollmentId,
memberId: query.memberId,
isActive: query.isActive === 'true' ? true : query.isActive === 'false' ? false : undefined,
}
const result = await LessonPlanService.list(app.db, params, filters)
return reply.send(result)
})
app.get('/lesson-plans/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const plan = await LessonPlanService.getById(app.db, id)
if (!plan) return reply.status(404).send({ error: { message: 'Lesson plan not found', statusCode: 404 } })
return reply.send(plan)
})
app.patch('/lesson-plans/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = LessonPlanUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const plan = await LessonPlanService.update(app.db, id, parsed.data)
if (!plan) return reply.status(404).send({ error: { message: 'Lesson plan not found', statusCode: 404 } })
return reply.send(plan)
})
// --- Lesson Plan Items ---
app.patch('/lesson-plan-items/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = LessonPlanItemUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const item = await LessonPlanItemService.update(app.db, id, parsed.data)
if (!item) return reply.status(404).send({ error: { message: 'Lesson plan item not found', statusCode: 404 } })
return reply.send(item)
})
// --- Lesson Plan Templates ---
app.post('/lesson-plan-templates', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const parsed = LessonPlanTemplateCreateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const template = await LessonPlanTemplateService.create(app.db, parsed.data, request.user.id)
return reply.status(201).send(template)
})
app.get('/lesson-plan-templates', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const query = request.query as Record<string, string | undefined>
const params = PaginationSchema.parse(query)
const filters = {
instrument: query.instrument,
skillLevel: query.skillLevel,
}
const result = await LessonPlanTemplateService.list(app.db, params, filters)
return reply.send(result)
})
app.get('/lesson-plan-templates/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const template = await LessonPlanTemplateService.getById(app.db, id)
if (!template) return reply.status(404).send({ error: { message: 'Template not found', statusCode: 404 } })
return reply.send(template)
})
app.patch('/lesson-plan-templates/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = LessonPlanTemplateUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const template = await LessonPlanTemplateService.update(app.db, id, parsed.data)
if (!template) return reply.status(404).send({ error: { message: 'Template not found', statusCode: 404 } })
return reply.send(template)
})
app.delete('/lesson-plan-templates/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const template = await LessonPlanTemplateService.delete(app.db, id)
if (!template) return reply.status(404).send({ error: { message: 'Template not found', statusCode: 404 } })
return reply.send(template)
})
app.post('/lesson-plan-templates/:id/create-plan', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = TemplateInstantiateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const plan = await LessonPlanTemplateService.instantiate(app.db, id, parsed.data, request.user.id)
if (!plan) return reply.status(404).send({ error: { message: 'Template not found', statusCode: 404 } })
return reply.status(201).send(plan)
})
// --- Grade History ---
app.post('/lesson-plan-items/:id/grades', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = GradeCreateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const result = await GradeHistoryService.create(app.db, id, parsed.data, request.user.id)
if (!result) return reply.status(404).send({ error: { message: 'Lesson plan item not found', statusCode: 404 } })
return reply.status(201).send(result)
})
app.get('/lesson-plan-items/:id/grade-history', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const history = await GradeHistoryService.list(app.db, id)
return reply.send(history)
})
// --- Session Plan Items ---
app.post('/lesson-sessions/:id/plan-items', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = SessionPlanItemsSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const result = await SessionPlanItemService.linkItems(app.db, id, parsed.data)
if (result === null) return reply.status(404).send({ error: { message: 'Lesson session not found', statusCode: 404 } })
return reply.status(201).send({ linked: result.length, items: result })
})
app.get('/lesson-sessions/:id/plan-items', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const items = await SessionPlanItemService.listForSession(app.db, id)
return reply.send(items)
})
// --- Instructor Blocked Dates ---
app.post('/instructors/:id/blocked-dates', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = InstructorBlockedDateCreateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const instructor = await InstructorService.getById(app.db, id)
if (!instructor) return reply.status(404).send({ error: { message: 'Instructor not found', statusCode: 404 } })
const result = await InstructorBlockedDateService.create(app.db, id, parsed.data)
if ('error' in result) return reply.status(409).send({ error: { message: result.error, statusCode: 409 } })
return reply.status(201).send(result)
})
app.get('/instructors/:id/blocked-dates', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const instructor = await InstructorService.getById(app.db, id)
if (!instructor) return reply.status(404).send({ error: { message: 'Instructor not found', statusCode: 404 } })
const dates = await InstructorBlockedDateService.list(app.db, id)
return reply.send(dates)
})
app.delete('/instructors/:instructorId/blocked-dates/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const { instructorId, id } = request.params as { instructorId: string; id: string }
const row = await InstructorBlockedDateService.delete(app.db, id, instructorId)
if (!row) return reply.status(404).send({ error: { message: 'Blocked date not found', statusCode: 404 } })
return reply.send(row)
})
// --- Store Closures ---
app.post('/store-closures', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const parsed = StoreClosureCreateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const result = await StoreClosureService.create(app.db, parsed.data)
if ('error' in result) return reply.status(409).send({ error: { message: result.error, statusCode: 409 } })
return reply.status(201).send(result)
})
app.get('/store-closures', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (_request, reply) => {
const closures = await StoreClosureService.list(app.db)
return reply.send(closures)
})
app.delete('/store-closures/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const row = await StoreClosureService.delete(app.db, id)
if (!row) return reply.status(404).send({ error: { message: 'Store closure not found', statusCode: 404 } })
return reply.send(row)
})
}