add review and booking policies
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user