Files
lunarfront-app/packages/backend/src/routes/v1/repairs.ts
Ryan Moon 9400828f62 Rename Forte to LunarFront, generalize for any small business
Rebrand from Forte (music-store-specific) to LunarFront (any small business):
- Package namespace @forte/* → @lunarfront/*
- Database forte/forte_test → lunarfront/lunarfront_test
- Docker containers, volumes, connection strings
- UI branding, localStorage keys, test emails
- All documentation and planning docs

Generalize music-specific terminology:
- instrumentDescription → itemDescription
- instrumentCount → itemCount
- instrumentType → itemCategory (on service templates)
- New migration 0027_generalize_terminology for column renames
- Seed data updated with generic examples
- RBAC descriptions updated
2026-03-30 08:51:54 -05:00

260 lines
14 KiB
TypeScript

import type { FastifyPluginAsync } from 'fastify'
import {
PaginationSchema,
RepairTicketCreateSchema,
RepairTicketUpdateSchema,
RepairTicketStatusUpdateSchema,
RepairLineItemCreateSchema,
RepairLineItemUpdateSchema,
RepairBatchCreateSchema,
RepairBatchUpdateSchema,
RepairBatchStatusUpdateSchema,
RepairNoteCreateSchema,
RepairServiceTemplateCreateSchema,
RepairServiceTemplateUpdateSchema,
} from '@lunarfront/shared/schemas'
import { RepairTicketService, RepairLineItemService, RepairBatchService, RepairNoteService, RepairServiceTemplateService } from '../../services/repair.service.js'
export const repairRoutes: FastifyPluginAsync = async (app) => {
// --- Repair Tickets ---
app.post('/repair-tickets', { preHandler: [app.authenticate, app.requirePermission('repairs.edit')] }, async (request, reply) => {
const parsed = RepairTicketCreateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const ticket = await RepairTicketService.create(app.db, parsed.data)
return reply.status(201).send(ticket)
})
app.get('/repair-tickets', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
const query = request.query as Record<string, string | undefined>
const params = PaginationSchema.parse(query)
const filters = {
status: query.status?.split(',').filter(Boolean),
conditionIn: query.conditionIn?.split(',').filter(Boolean),
isBatch: query.isBatch === 'true' ? true : query.isBatch === 'false' ? false : undefined,
batchNumber: query.batchNumber,
intakeDateFrom: query.intakeDateFrom,
intakeDateTo: query.intakeDateTo,
promisedDateFrom: query.promisedDateFrom,
promisedDateTo: query.promisedDateTo,
completedDateFrom: query.completedDateFrom,
completedDateTo: query.completedDateTo,
}
const result = await RepairTicketService.list(app.db, params, filters)
return reply.send(result)
})
app.get('/repair-tickets/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const ticket = await RepairTicketService.getById(app.db, id)
if (!ticket) return reply.status(404).send({ error: { message: 'Repair ticket not found', statusCode: 404 } })
return reply.send(ticket)
})
app.patch('/repair-tickets/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = RepairTicketUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const ticket = await RepairTicketService.update(app.db, id, parsed.data)
if (!ticket) return reply.status(404).send({ error: { message: 'Repair ticket not found', statusCode: 404 } })
return reply.send(ticket)
})
app.post('/repair-tickets/:id/status', { preHandler: [app.authenticate, app.requirePermission('repairs.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = RepairTicketStatusUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const ticket = await RepairTicketService.updateStatus(app.db, id, parsed.data.status)
if (!ticket) return reply.status(404).send({ error: { message: 'Repair ticket not found', statusCode: 404 } })
return reply.send(ticket)
})
app.delete('/repair-tickets/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const ticket = await RepairTicketService.delete(app.db, id)
if (!ticket) return reply.status(404).send({ error: { message: 'Repair ticket not found', statusCode: 404 } })
return reply.send(ticket)
})
// --- Repair Line Items ---
app.post('/repair-tickets/:ticketId/line-items', { preHandler: [app.authenticate, app.requirePermission('repairs.edit')] }, async (request, reply) => {
const { ticketId } = request.params as { ticketId: string }
const parsed = RepairLineItemCreateSchema.safeParse({ ...(request.body as object), repairTicketId: ticketId })
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const item = await RepairLineItemService.create(app.db, parsed.data)
return reply.status(201).send(item)
})
app.get('/repair-tickets/:ticketId/line-items', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
const { ticketId } = request.params as { ticketId: string }
const params = PaginationSchema.parse(request.query)
const result = await RepairLineItemService.listByTicket(app.db, ticketId, params)
return reply.send(result)
})
app.patch('/repair-line-items/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = RepairLineItemUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const item = await RepairLineItemService.update(app.db, id, parsed.data)
if (!item) return reply.status(404).send({ error: { message: 'Line item not found', statusCode: 404 } })
return reply.send(item)
})
app.delete('/repair-line-items/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const item = await RepairLineItemService.delete(app.db, id)
if (!item) return reply.status(404).send({ error: { message: 'Line item not found', statusCode: 404 } })
return reply.send(item)
})
// --- Repair Batches ---
app.post('/repair-batches', { preHandler: [app.authenticate, app.requirePermission('repairs.edit')] }, async (request, reply) => {
const parsed = RepairBatchCreateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const batch = await RepairBatchService.create(app.db, parsed.data)
return reply.status(201).send(batch)
})
app.get('/repair-batches', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
const params = PaginationSchema.parse(request.query)
const result = await RepairBatchService.list(app.db, params)
return reply.send(result)
})
app.get('/repair-batches/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const batch = await RepairBatchService.getById(app.db, id)
if (!batch) return reply.status(404).send({ error: { message: 'Repair batch not found', statusCode: 404 } })
return reply.send(batch)
})
app.patch('/repair-batches/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = RepairBatchUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const batch = await RepairBatchService.update(app.db, id, parsed.data)
if (!batch) return reply.status(404).send({ error: { message: 'Repair batch not found', statusCode: 404 } })
return reply.send(batch)
})
app.post('/repair-batches/:id/status', { preHandler: [app.authenticate, app.requirePermission('repairs.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = RepairBatchStatusUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const batch = await RepairBatchService.updateStatus(app.db, id, parsed.data.status)
if (!batch) return reply.status(404).send({ error: { message: 'Repair batch not found', statusCode: 404 } })
return reply.send(batch)
})
app.post('/repair-batches/:id/approve', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const batch = await RepairBatchService.approve(app.db, id, request.user.id)
if (!batch) return reply.status(404).send({ error: { message: 'Repair batch not found', statusCode: 404 } })
return reply.send(batch)
})
app.post('/repair-batches/:id/reject', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const batch = await RepairBatchService.reject(app.db, id)
if (!batch) return reply.status(404).send({ error: { message: 'Repair batch not found', statusCode: 404 } })
return reply.send(batch)
})
app.get('/repair-batches/:batchId/tickets', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
const { batchId } = request.params as { batchId: string }
const params = PaginationSchema.parse(request.query)
const result = await RepairTicketService.listByBatch(app.db, batchId, params)
return reply.send(result)
})
// --- Repair Notes ---
app.post('/repair-tickets/:ticketId/notes', { preHandler: [app.authenticate, app.requirePermission('repairs.edit')] }, async (request, reply) => {
const { ticketId } = request.params as { ticketId: string }
const parsed = RepairNoteCreateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const ticket = await RepairTicketService.getById(app.db, ticketId)
if (!ticket) return reply.status(404).send({ error: { message: 'Repair ticket not found', statusCode: 404 } })
// Look up author name from users table
const { users } = await import('../../db/schema/users.js')
const { eq } = await import('drizzle-orm')
const [author] = await app.db.select({ firstName: users.firstName, lastName: users.lastName }).from(users).where(eq(users.id, request.user.id)).limit(1)
const authorName = author ? `${author.firstName} ${author.lastName}` : 'Unknown'
const note = await RepairNoteService.create(app.db, ticketId, request.user.id, authorName, ticket.status, parsed.data)
return reply.status(201).send(note)
})
app.get('/repair-tickets/:ticketId/notes', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
const { ticketId } = request.params as { ticketId: string }
const params = PaginationSchema.parse(request.query)
const result = await RepairNoteService.listByTicket(app.db, ticketId, params)
return reply.send(result)
})
app.delete('/repair-notes/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const note = await RepairNoteService.delete(app.db, id)
if (!note) return reply.status(404).send({ error: { message: 'Note not found', statusCode: 404 } })
return reply.send(note)
})
// --- Repair Service Templates ---
app.post('/repair-service-templates', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
const parsed = RepairServiceTemplateCreateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const template = await RepairServiceTemplateService.create(app.db, parsed.data)
return reply.status(201).send(template)
})
app.get('/repair-service-templates', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
const params = PaginationSchema.parse(request.query)
const result = await RepairServiceTemplateService.list(app.db, params)
return reply.send(result)
})
app.patch('/repair-service-templates/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = RepairServiceTemplateUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const template = await RepairServiceTemplateService.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('/repair-service-templates/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const template = await RepairServiceTemplateService.delete(app.db, id)
if (!template) return reply.status(404).send({ error: { message: 'Template not found', statusCode: 404 } })
return reply.send(template)
})
}