fixing platform admin

This commit is contained in:
root
2026-05-06 22:58:23 -04:00
parent 695a7f7cc7
commit 750ae56a29
175 changed files with 31249 additions and 328 deletions
+212 -4
View File
@@ -4,14 +4,47 @@ import { prisma } from '../lib/prisma'
import { requireCompanyAuth } from '../middleware/requireCompanyAuth'
import { requireTenant } from '../middleware/requireTenant'
import { requireSubscription } from '../middleware/requireSubscription'
import { applyAdditionalDriversToReservation } from '../services/additionalDriverService'
import { applyInsurancesToReservation } from '../services/insuranceService'
import { applyPricingRules } from '../services/pricingRuleService'
import { validateAndFlagLicense } from '../services/licenseValidationService'
import { validateAndFlagLicense, validateLicense } from '../services/licenseValidationService'
import { sendNotification } from '../services/notificationService'
const router = Router()
router.use(requireCompanyAuth, requireTenant, requireSubscription)
const additionalDriverSchema = z.object({
firstName: z.string().min(1),
lastName: z.string().min(1),
email: z.string().email().optional(),
phone: z.string().optional(),
driverLicense: z.string().min(1),
licenseExpiry: z.string().datetime().optional(),
licenseIssuedAt: z.string().datetime().optional(),
dateOfBirth: z.string().datetime().optional(),
nationality: z.string().optional(),
})
const inspectionSchema = z.object({
mileage: z.number().int().optional(),
fuelLevel: z.enum(['FULL', 'SEVEN_EIGHTHS', 'THREE_QUARTERS', 'FIVE_EIGHTHS', 'HALF', 'THREE_EIGHTHS', 'QUARTER', 'ONE_EIGHTH', 'EMPTY']),
fuelCharge: z.number().int().min(0).optional(),
generalCondition: z.string().optional(),
employeeNotes: z.string().optional(),
customerAgreed: z.boolean().default(false),
damagePoints: z.array(
z.object({
viewType: z.enum(['TOP', 'FRONT', 'REAR', 'LEFT', 'RIGHT']),
x: z.number(),
y: z.number(),
damageType: z.enum(['SCRATCH', 'DENT', 'CRACK', 'CHIP', 'MISSING', 'STAIN', 'OTHER']),
severity: z.enum(['MINOR', 'MODERATE', 'MAJOR']).default('MINOR'),
description: z.string().optional(),
isPreExisting: z.boolean().default(false),
}),
).default([]),
})
const createSchema = z.object({
vehicleId: z.string().cuid(),
customerId: z.string().cuid(),
@@ -24,8 +57,44 @@ const createSchema = z.object({
depositAmount: z.number().int().min(0).default(0),
notes: z.string().optional(),
selectedInsurancePolicyIds: z.array(z.string()).default([]),
additionalDrivers: z.array(additionalDriverSchema).default([]),
})
async function assertReservationLicenseCompliance(reservationId: string, companyId: string) {
const reservation = await prisma.reservation.findFirstOrThrow({
where: { id: reservationId, companyId },
include: { customer: true, additionalDrivers: true },
})
const customerLicense = validateLicense(reservation.customer.licenseExpiry)
const customerDenied = ['DENIED', 'EXPIRED'].includes(reservation.customer.licenseValidationStatus)
if (customerDenied || customerLicense.status === 'EXPIRED') {
throw Object.assign(new Error('Primary driver license is not valid for this reservation'), {
statusCode: 400,
code: 'invalid_primary_license',
})
}
if (customerLicense.requiresApproval && reservation.customer.licenseValidationStatus !== 'APPROVED') {
throw Object.assign(new Error('Primary driver license requires manager approval before this reservation can proceed'), {
statusCode: 400,
code: 'primary_license_requires_approval',
})
}
const blockedDriver = reservation.additionalDrivers.find((driver) => {
if (driver.licenseExpired) return true
return driver.requiresApproval && !driver.approvedAt
})
if (blockedDriver) {
throw Object.assign(new Error(`Additional driver ${blockedDriver.firstName} ${blockedDriver.lastName} requires approval before this reservation can proceed`), {
statusCode: 400,
code: 'additional_driver_requires_approval',
})
}
}
router.get('/', async (req, res, next) => {
try {
const { status, vehicleId, source, startDate, endDate, page = '1', pageSize = '20' } = req.query as Record<string, string>
@@ -87,7 +156,7 @@ router.post('/', async (req, res, next) => {
}
const baseAmount = vehicle.dailyRate * totalDays
const { applied, total: pricingTotal } = await applyPricingRules(req.companyId, body.customerId, [], vehicle.dailyRate, totalDays)
const { applied, total: pricingTotal } = await applyPricingRules(req.companyId, body.customerId, body.additionalDrivers, vehicle.dailyRate, totalDays)
const totalAmount = baseAmount - discountAmount + pricingTotal + body.depositAmount
const reservation = await prisma.reservation.create({
@@ -118,6 +187,10 @@ router.post('/', async (req, res, next) => {
await applyInsurancesToReservation(reservation.id, req.companyId, body.selectedInsurancePolicyIds, totalDays, baseAmount)
}
if (body.additionalDrivers.length > 0) {
await applyAdditionalDriversToReservation(reservation.id, req.companyId, body.additionalDrivers, totalDays)
}
// Validate customer license
await validateAndFlagLicense(body.customerId).catch(() => null)
@@ -139,6 +212,7 @@ router.post('/:id/confirm', async (req, res, next) => {
try {
const reservation = await prisma.reservation.findFirstOrThrow({ where: { id: req.params.id, companyId: req.companyId } })
if (reservation.status !== 'DRAFT') return res.status(400).json({ error: 'invalid_status', message: 'Only DRAFT reservations can be confirmed', statusCode: 400 })
await assertReservationLicenseCompliance(reservation.id, req.companyId)
const updated = await prisma.reservation.update({ where: { id: req.params.id }, data: { status: 'CONFIRMED' } })
@@ -155,6 +229,7 @@ router.post('/:id/checkin', async (req, res, next) => {
const { mileage } = z.object({ mileage: z.number().int().optional() }).parse(req.body)
const reservation = await prisma.reservation.findFirstOrThrow({ where: { id: req.params.id, companyId: req.companyId } })
if (reservation.status !== 'CONFIRMED') return res.status(400).json({ error: 'invalid_status', message: 'Only CONFIRMED reservations can be checked in', statusCode: 400 })
await assertReservationLicenseCompliance(reservation.id, req.companyId)
const updated = await prisma.reservation.update({ where: { id: req.params.id }, data: { status: 'ACTIVE', checkedInAt: new Date(), checkInMileage: mileage ?? null } })
await prisma.vehicle.update({ where: { id: reservation.vehicleId }, data: { status: 'RENTED' } })
res.json({ data: updated })
@@ -201,8 +276,8 @@ router.get('/:id/billing', async (req, res, next) => {
const baseAmount = reservation.dailyRate * reservation.totalDays
const lineItems = [
{ description: `${reservation.vehicle.year} ${reservation.vehicle.make} ${reservation.vehicle.model}${reservation.totalDays} day(s)`, qty: reservation.totalDays, unitPrice: reservation.dailyRate, total: baseAmount, category: 'RENTAL' },
...reservation.insurances.map((ins) => ({ description: ins.policyName, qty: ins.chargeType === 'PER_DAY' ? reservation.totalDays : 1, unitPrice: ins.chargeType === 'PER_DAY' ? ins.chargeValue : ins.totalCharge, total: ins.totalCharge, category: 'INSURANCE' })),
...reservation.additionalDrivers.filter((d) => d.totalCharge > 0).map((d) => ({ description: `Additional Driver — ${d.firstName} ${d.lastName}`, qty: 1, unitPrice: d.totalCharge, total: d.totalCharge, category: 'ADDITIONAL_DRIVER' })),
...reservation.insurances.map((ins: (typeof reservation.insurances)[number]) => ({ description: ins.policyName, qty: ins.chargeType === 'PER_DAY' ? reservation.totalDays : 1, unitPrice: ins.chargeType === 'PER_DAY' ? ins.chargeValue : ins.totalCharge, total: ins.totalCharge, category: 'INSURANCE' })),
...reservation.additionalDrivers.filter((d: (typeof reservation.additionalDrivers)[number]) => d.totalCharge > 0).map((d: (typeof reservation.additionalDrivers)[number]) => ({ description: `Additional Driver — ${d.firstName} ${d.lastName}`, qty: 1, unitPrice: d.totalCharge, total: d.totalCharge, category: 'ADDITIONAL_DRIVER' })),
...(reservation.depositAmount > 0 ? [{ description: 'Security Deposit (refundable)', qty: 1, unitPrice: reservation.depositAmount, total: reservation.depositAmount, category: 'DEPOSIT' }] : []),
]
@@ -212,4 +287,137 @@ router.get('/:id/billing', async (req, res, next) => {
} catch (err) { next(err) }
})
router.get('/:id/inspections', async (req, res, next) => {
try {
const inspections = await prisma.damageInspection.findMany({
where: { reservationId: req.params.id, companyId: req.companyId },
include: { damagePoints: true },
orderBy: { inspectedAt: 'asc' },
})
res.json({ data: inspections })
} catch (err) { next(err) }
})
router.put('/:id/inspections/:type', async (req, res, next) => {
try {
const type = z.enum(['CHECKIN', 'CHECKOUT']).parse(req.params.type.toUpperCase())
const body = inspectionSchema.parse(req.body)
const reservation = await prisma.reservation.findFirstOrThrow({
where: { id: req.params.id, companyId: req.companyId },
include: { vehicle: true, customer: true },
})
const inspection = await prisma.damageInspection.upsert({
where: { reservationId_type: { reservationId: reservation.id, type } },
update: {
mileage: body.mileage ?? null,
fuelLevel: body.fuelLevel,
fuelCharge: body.fuelCharge ?? null,
generalCondition: body.generalCondition ?? null,
employeeNotes: body.employeeNotes ?? null,
customerAgreed: body.customerAgreed,
inspectedBy: `${req.employee.firstName} ${req.employee.lastName}`,
inspectedAt: new Date(),
damagePoints: {
deleteMany: {},
create: body.damagePoints,
},
},
create: {
reservationId: reservation.id,
companyId: req.companyId,
type,
mileage: body.mileage ?? null,
fuelLevel: body.fuelLevel,
fuelCharge: body.fuelCharge ?? null,
generalCondition: body.generalCondition ?? null,
employeeNotes: body.employeeNotes ?? null,
customerAgreed: body.customerAgreed,
inspectedBy: `${req.employee.firstName} ${req.employee.lastName}`,
damagePoints: {
create: body.damagePoints,
},
},
include: { damagePoints: true },
})
await prisma.damageReport.upsert({
where: { reservationId_type: { reservationId: reservation.id, type } },
update: {
damages: body.damagePoints.map((point) => ({
viewType: point.viewType,
x: point.x,
y: point.y,
damageType: point.damageType,
severity: point.severity,
note: point.description ?? '',
isPreExisting: point.isPreExisting,
})),
fuelLevel: body.fuelLevel,
mileage: body.mileage ?? null,
inspectedAt: new Date(),
inspectedBy: `${req.employee.firstName} ${req.employee.lastName}`,
customerSignedAt: body.customerAgreed ? new Date() : null,
customerName: `${reservation.customer.firstName} ${reservation.customer.lastName}`,
},
create: {
reservationId: reservation.id,
companyId: req.companyId,
type,
damages: body.damagePoints.map((point) => ({
viewType: point.viewType,
x: point.x,
y: point.y,
damageType: point.damageType,
severity: point.severity,
note: point.description ?? '',
isPreExisting: point.isPreExisting,
})),
photos: [],
fuelLevel: body.fuelLevel,
mileage: body.mileage ?? null,
inspectedAt: new Date(),
inspectedBy: `${req.employee.firstName} ${req.employee.lastName}`,
customerSignedAt: body.customerAgreed ? new Date() : null,
customerName: `${reservation.customer.firstName} ${reservation.customer.lastName}`,
},
})
await prisma.reservation.update({
where: { id: reservation.id },
data: type === 'CHECKIN'
? {
checkInMileage: body.mileage ?? null,
checkInFuelLevel: body.fuelLevel,
}
: {
checkOutMileage: body.mileage ?? null,
checkOutFuelLevel: body.fuelLevel,
},
})
res.json({ data: inspection })
} catch (err) { next(err) }
})
router.patch('/:id/additional-drivers/:driverId/approval', async (req, res, next) => {
try {
const { approved, note } = z.object({ approved: z.boolean(), note: z.string().optional() }).parse(req.body)
const driver = await prisma.additionalDriver.findFirstOrThrow({
where: { id: req.params.driverId, reservationId: req.params.id, companyId: req.companyId },
})
const updated = await prisma.additionalDriver.update({
where: { id: driver.id },
data: {
approvedAt: approved ? new Date() : null,
approvedBy: approved ? `${req.employee.firstName} ${req.employee.lastName}` : null,
approvalNote: note ?? driver.approvalNote,
},
})
res.json({ data: updated })
} catch (err) { next(err) }
})
export default router