add review and booking policies

This commit is contained in:
root
2026-05-25 18:29:05 -04:00
parent 8ed572b3bd
commit 0d969ab095
37 changed files with 3775 additions and 113 deletions
@@ -0,0 +1,84 @@
import { prisma } from '../../lib/prisma'
const COMPLAINT_INCLUDE = {
reservation: {
select: {
id: true,
startDate: true,
endDate: true,
status: true,
vehicle: { select: { make: true, model: true, year: true } },
},
},
customer: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
phone: true,
},
},
review: {
select: {
id: true,
overallRating: true,
comment: true,
},
},
}
export async function findMany(
companyId: string,
where: Record<string, unknown>,
skip: number,
take: number,
) {
const baseWhere = { companyId, ...where }
return Promise.all([
prisma.complaint.findMany({
where: baseWhere,
include: COMPLAINT_INCLUDE,
skip,
take,
orderBy: { createdAt: 'desc' },
}),
prisma.complaint.count({ where: baseWhere }),
])
}
export async function findById(id: string, companyId: string) {
return prisma.complaint.findFirst({
where: { id, companyId },
include: COMPLAINT_INCLUDE,
})
}
export async function create(data: {
companyId: string
reservationId?: string
reviewId?: string
customerId?: string
severity: string
category: string
subject: string
description?: string
assignedTo?: string
}) {
return prisma.complaint.create({
data: data as any,
include: COMPLAINT_INCLUDE,
})
}
export async function updateById(id: string, data: Record<string, unknown>) {
return prisma.complaint.update({
where: { id },
data,
include: COMPLAINT_INCLUDE,
})
}
export async function deleteById(id: string) {
return prisma.complaint.delete({ where: { id } })
}
@@ -0,0 +1,55 @@
import { Router } from 'express'
import { requireCompanyAuth } from '../../middleware/requireCompanyAuth'
import { requireTenant } from '../../middleware/requireTenant'
import { requireSubscription } from '../../middleware/requireSubscription'
import { parseBody, parseQuery, parseParams } from '../../http/validate'
import { ok, created } from '../../http/respond'
import * as service from './complaint.service'
import { createSchema, updateSchema, listQuerySchema, idParamSchema } from './complaint.schemas'
const router = Router()
router.use(requireCompanyAuth, requireTenant, requireSubscription)
router.get('/', async (req, res, next) => {
try {
const query = parseQuery(listQuerySchema, req)
const result = await service.listComplaints(req.companyId, query)
ok(res, result)
} catch (err) { next(err) }
})
router.post('/', async (req, res, next) => {
try {
const body = parseBody(createSchema, req)
const complaint = await service.createComplaint(req.companyId, body)
created(res, complaint)
} catch (err) { next(err) }
})
router.get('/:id', async (req, res, next) => {
try {
const { id } = parseParams(idParamSchema, req)
const complaint = await service.getComplaint(id, req.companyId)
ok(res, complaint)
} catch (err) { next(err) }
})
router.patch('/:id', async (req, res, next) => {
try {
const { id } = parseParams(idParamSchema, req)
const body = parseBody(updateSchema, req)
const updated = await service.updateComplaint(id, req.companyId, body)
ok(res, updated)
} catch (err) { next(err) }
})
router.delete('/:id', async (req, res, next) => {
try {
const { id } = parseParams(idParamSchema, req)
const result = await service.deleteComplaint(id, req.companyId)
ok(res, result)
} catch (err) { next(err) }
})
export default router
@@ -0,0 +1,44 @@
import { z } from 'zod'
const CATEGORIES = [
'BOOKING', 'PICKUP', 'VEHICLE_CLEANLINESS', 'VEHICLE_CONDITION', 'STAFF_SERVICE',
'PRICING', 'DEPOSIT', 'INSURANCE', 'DAMAGE_CLAIM', 'RETURN_PROCESS',
'COMMUNICATION', 'ROADSIDE_ASSISTANCE', 'BILLING', 'OTHER',
] as const
const SEVERITIES = ['LEVEL_1', 'LEVEL_2', 'LEVEL_3'] as const
const STATUSES = ['OPEN', 'INVESTIGATING', 'RESOLVED', 'CLOSED'] as const
export const createSchema = z.object({
reservationId: z.string().optional(),
reviewId: z.string().optional(),
customerId: z.string().optional(),
severity: z.enum(SEVERITIES).default('LEVEL_1'),
category: z.enum(CATEGORIES),
subject: z.string().min(1),
description: z.string().optional(),
assignedTo: z.string().optional(),
})
export const updateSchema = z.object({
status: z.enum(STATUSES).optional(),
severity: z.enum(SEVERITIES).optional(),
category: z.enum(CATEGORIES).optional(),
subject: z.string().min(1).optional(),
description: z.string().optional(),
notes: z.string().optional(),
resolution: z.string().optional(),
assignedTo: z.string().optional(),
})
export const listQuerySchema = z.object({
status: z.string().optional(),
severity: z.string().optional(),
category: z.string().optional(),
page: z.coerce.number().int().min(1).default(1),
pageSize: z.coerce.number().int().min(1).max(100).default(20),
})
export const idParamSchema = z.object({
id: z.string(),
})
@@ -0,0 +1,75 @@
import { NotFoundError } from '../../http/errors'
import * as repo from './complaint.repo'
export async function listComplaints(
companyId: string,
query: { status?: string; severity?: string; category?: string; page: number; pageSize: number },
) {
const { status, severity, category, page, pageSize } = query
const where: Record<string, unknown> = {}
if (status) where.status = status
if (severity) where.severity = severity
if (category) where.category = category
const [complaints, total] = await repo.findMany(companyId, where, (page - 1) * pageSize, pageSize)
return {
data: complaints,
meta: { total, page, pageSize, totalPages: Math.ceil(total / pageSize) },
}
}
export async function getComplaint(id: string, companyId: string) {
const complaint = await repo.findById(id, companyId)
if (!complaint) throw new NotFoundError('Complaint not found')
return complaint
}
export async function createComplaint(
companyId: string,
data: {
reservationId?: string
reviewId?: string
customerId?: string
severity: string
category: string
subject: string
description?: string
assignedTo?: string
},
) {
return repo.create({ companyId, ...data })
}
export async function updateComplaint(
id: string,
companyId: string,
data: {
status?: string
severity?: string
category?: string
subject?: string
description?: string
notes?: string
resolution?: string
assignedTo?: string
},
) {
const existing = await repo.findById(id, companyId)
if (!existing) throw new NotFoundError('Complaint not found')
const updateData: Record<string, unknown> = { ...data }
// When status changes to RESOLVED, set resolvedAt
if (data.status === 'RESOLVED' && existing.status !== 'RESOLVED') {
updateData.resolvedAt = new Date()
}
return repo.updateById(id, updateData)
}
export async function deleteComplaint(id: string, companyId: string) {
const existing = await repo.findById(id, companyId)
if (!existing) throw new NotFoundError('Complaint not found')
await repo.deleteById(id)
return { success: true }
}