add review and booking policies
This commit is contained in:
@@ -50,6 +50,7 @@ export async function confirmReservation(id: string, companyId: string) {
|
||||
await assertLicenseCompliance(reservation.id, companyId)
|
||||
|
||||
const updated = await repo.updateById(id, { status: 'CONFIRMED' })
|
||||
await repo.updateVehicleStatus(reservation.vehicleId, 'RESERVED')
|
||||
|
||||
const [customer, company, vehicle] = await Promise.all([
|
||||
repo.findCustomerById(reservation.customerId),
|
||||
@@ -117,30 +118,14 @@ export async function checkoutReservation(id: string, companyId: string, mileage
|
||||
const reservation = await repo.findByIdForCheckout(id, companyId)
|
||||
if (reservation.status !== 'ACTIVE') throw new AppError('Only ACTIVE reservations can be checked out', 400, 'invalid_status')
|
||||
|
||||
const reviewToken = crypto.randomBytes(32).toString('hex')
|
||||
const reviewToken = reservation.reviewToken ?? crypto.randomBytes(32).toString('hex')
|
||||
const updated = await repo.updateById(id, {
|
||||
status: 'COMPLETED',
|
||||
checkedOutAt: new Date(),
|
||||
checkOutMileage: mileage ?? null,
|
||||
reviewToken,
|
||||
})
|
||||
await repo.updateVehicleStatus(reservation.vehicleId, 'AVAILABLE', mileage)
|
||||
|
||||
const [customer, company] = await Promise.all([
|
||||
repo.findCustomerById(reservation.customerId),
|
||||
repo.findCompanyWithBrand(companyId),
|
||||
])
|
||||
const lang = (company?.brand?.defaultLocale ?? 'fr') as Lang
|
||||
const companyName = company?.brand?.displayName ?? company?.name ?? 'the rental company'
|
||||
const vehicleLabel = `${reservation.vehicle.year} ${reservation.vehicle.make} ${reservation.vehicle.model}`
|
||||
const reviewUrl = `${process.env.MARKETPLACE_URL ?? process.env.NEXT_PUBLIC_MARKETPLACE_URL ?? 'http://localhost:3000'}/review?token=${reviewToken}`
|
||||
|
||||
sendTransactionalEmail({
|
||||
to: customer.email,
|
||||
subject: reviewRequestEmail.subject(vehicleLabel, lang),
|
||||
html: reviewRequestEmail.html({ firstName: customer.firstName, companyName, vehicleLabel, reviewUrl }, lang),
|
||||
text: reviewRequestEmail.text({ firstName: customer.firstName, companyName, vehicleLabel, reviewUrl }, lang),
|
||||
}).catch(() => null)
|
||||
await repo.updateVehicleStatus(reservation.vehicleId, 'RETURNED', mileage)
|
||||
|
||||
return updated
|
||||
}
|
||||
@@ -161,16 +146,81 @@ export async function closeReservation(id: string, companyId: string, closedBy:
|
||||
data: { extras: extras as any },
|
||||
include: { vehicle: true, customer: true, insurances: true, additionalDrivers: true, rentalPayments: true, damageReports: true },
|
||||
})
|
||||
|
||||
// Send review request email if eligible
|
||||
if (reservation.reviewToken && !(reservation as any).reviewPaused) {
|
||||
try {
|
||||
const customer = updated.customer as any
|
||||
if (!customer.reviewOptOut && customer.email) {
|
||||
const company = await repo.findCompanyWithBrand(companyId)
|
||||
const lang = (company?.brand?.defaultLocale ?? 'fr') as Lang
|
||||
const companyName = company?.brand?.displayName ?? company?.name ?? 'the rental company'
|
||||
const vehicleLabel = `${updated.vehicle.year} ${updated.vehicle.make} ${updated.vehicle.model}`
|
||||
const reviewUrl = `${process.env.MARKETPLACE_URL ?? 'http://localhost:3000'}/review?token=${reservation.reviewToken}`
|
||||
|
||||
sendTransactionalEmail({
|
||||
to: customer.email,
|
||||
subject: reviewRequestEmail.subject(vehicleLabel, lang),
|
||||
html: reviewRequestEmail.html({ firstName: customer.firstName, companyName, vehicleLabel, reviewUrl }, lang),
|
||||
text: reviewRequestEmail.text({ firstName: customer.firstName, companyName, vehicleLabel, reviewUrl }, lang),
|
||||
}).catch(() => null)
|
||||
|
||||
await prisma.reservation.update({
|
||||
where: { id: reservation.id },
|
||||
data: { reviewRequestSentAt: new Date() },
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// Non-blocking: do not fail close if email fails
|
||||
}
|
||||
}
|
||||
|
||||
return serializeReservationForDashboard(updated)
|
||||
}
|
||||
|
||||
export async function extendReservation(id: string, companyId: string, newEndDate: Date, reason: string) {
|
||||
const reservation = await repo.findByIdSimple(id, companyId)
|
||||
if (!['CONFIRMED', 'ACTIVE'].includes(reservation.status)) {
|
||||
throw new AppError('Only CONFIRMED or ACTIVE reservations can be extended', 400, 'invalid_status')
|
||||
}
|
||||
if (newEndDate <= reservation.endDate) {
|
||||
throw new AppError('New end date must be after the current end date', 400, 'invalid_end_date')
|
||||
}
|
||||
const conflict = await repo.findConflict(reservation.vehicleId, reservation.endDate, newEndDate, id)
|
||||
if (conflict) {
|
||||
throw new AppError('Vehicle is unavailable for the requested extension period', 409, 'vehicle_unavailable')
|
||||
}
|
||||
const additionalDays = Math.ceil((newEndDate.getTime() - reservation.endDate.getTime()) / (1000 * 60 * 60 * 24))
|
||||
const additionalAmount = additionalDays * reservation.dailyRate
|
||||
const extras = parseReservationExtras(reservation.extras)
|
||||
if (!Array.isArray(extras.extensions)) extras.extensions = []
|
||||
;(extras.extensions as unknown[]).push({
|
||||
previousEndDate: reservation.endDate.toISOString(),
|
||||
newEndDate: newEndDate.toISOString(),
|
||||
additionalDays,
|
||||
additionalAmount,
|
||||
reason,
|
||||
approvedAt: new Date().toISOString(),
|
||||
})
|
||||
const updated = await prisma.reservation.update({
|
||||
where: { id },
|
||||
data: {
|
||||
endDate: newEndDate,
|
||||
totalDays: reservation.totalDays + additionalDays,
|
||||
totalAmount: reservation.totalAmount + additionalAmount,
|
||||
extras: extras as any,
|
||||
},
|
||||
})
|
||||
return updated
|
||||
}
|
||||
|
||||
export async function cancelReservation(id: string, companyId: string, reason?: string) {
|
||||
const reservation = await repo.findByIdSimple(id, companyId)
|
||||
if (['COMPLETED', 'CANCELLED'].includes(reservation.status)) {
|
||||
throw new AppError('Reservation cannot be cancelled', 400, 'invalid_status')
|
||||
}
|
||||
const updated = await repo.updateById(id, { status: 'CANCELLED', cancelReason: reason ?? null, cancelledBy: 'COMPANY' })
|
||||
if (['CONFIRMED', 'ACTIVE'].includes(reservation.status)) {
|
||||
if (['CONFIRMED', 'ACTIVE', 'DRAFT'].includes(reservation.status)) {
|
||||
await repo.updateVehicleStatus(reservation.vehicleId, 'AVAILABLE')
|
||||
}
|
||||
return updated
|
||||
|
||||
Reference in New Issue
Block a user