refractor code,
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import crypto from 'crypto'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
import { AppError } from '../../http/errors'
|
||||
import { validateLicense } from '../../services/licenseValidationService'
|
||||
import { sendNotification, sendTransactionalEmail } from '../../services/notificationService'
|
||||
import { bookingConfirmedNotif, reviewRequestEmail, type Lang } from '../../lib/emailTranslations'
|
||||
import { buildReservationWorkflow, parseReservationExtras, serializeReservationForDashboard } from './reservation.presenter'
|
||||
import * as repo from './reservation.repo'
|
||||
|
||||
async function assertLicenseCompliance(reservationId: string, companyId: string) {
|
||||
const reservation = await repo.findForLicenseCheck(reservationId, companyId)
|
||||
|
||||
const customerLicense = validateLicense(reservation.customer.licenseExpiry)
|
||||
const customerDenied = ['DENIED', 'EXPIRED'].includes(reservation.customer.licenseValidationStatus)
|
||||
if (customerDenied || customerLicense.status === 'EXPIRED') {
|
||||
throw new AppError('Primary driver license is not valid for this reservation', 400, 'invalid_primary_license')
|
||||
}
|
||||
if (customerLicense.requiresApproval && reservation.customer.licenseValidationStatus !== 'APPROVED') {
|
||||
throw new AppError('Primary driver license requires manager approval before this reservation can proceed', 400, 'primary_license_requires_approval')
|
||||
}
|
||||
|
||||
const blockedDriver = reservation.additionalDrivers.find((d: any) => {
|
||||
if (d.licenseExpired) return true
|
||||
return d.requiresApproval && !d.approvedAt
|
||||
})
|
||||
if (blockedDriver) {
|
||||
throw new AppError(
|
||||
`Additional driver ${blockedDriver.firstName} ${blockedDriver.lastName} requires approval before this reservation can proceed`,
|
||||
400,
|
||||
'additional_driver_requires_approval',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function confirmReservation(id: string, companyId: string) {
|
||||
const reservation = await repo.findByIdSimple(id, companyId)
|
||||
if (reservation.status !== 'DRAFT') throw new AppError('Only DRAFT reservations can be confirmed', 400, 'invalid_status')
|
||||
await assertLicenseCompliance(reservation.id, companyId)
|
||||
|
||||
const updated = await repo.updateById(id, { status: 'CONFIRMED' })
|
||||
|
||||
const [customer, brand] = await Promise.all([
|
||||
repo.findCustomerById(reservation.customerId),
|
||||
repo.findBrandLocale(companyId),
|
||||
])
|
||||
const lang = (brand?.defaultLocale ?? 'fr') as Lang
|
||||
await sendNotification({
|
||||
type: 'BOOKING_CONFIRMED',
|
||||
title: bookingConfirmedNotif.title(lang),
|
||||
body: bookingConfirmedNotif.body(lang),
|
||||
companyId,
|
||||
email: customer.email,
|
||||
channels: ['EMAIL', 'IN_APP'],
|
||||
locale: lang,
|
||||
}).catch(() => null)
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
export async function checkinReservation(id: string, companyId: string, mileage?: number) {
|
||||
const reservation = await repo.findByIdSimple(id, companyId)
|
||||
if (reservation.status !== 'CONFIRMED') throw new AppError('Only CONFIRMED reservations can be checked in', 400, 'invalid_status')
|
||||
await assertLicenseCompliance(reservation.id, companyId)
|
||||
|
||||
const updated = await repo.updateById(id, {
|
||||
status: 'ACTIVE',
|
||||
checkedInAt: new Date(),
|
||||
checkInMileage: mileage ?? null,
|
||||
})
|
||||
await repo.updateVehicleStatus(reservation.vehicleId, 'RENTED')
|
||||
return updated
|
||||
}
|
||||
|
||||
export async function checkoutReservation(id: string, companyId: string, mileage?: number) {
|
||||
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 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)
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
export async function closeReservation(id: string, companyId: string, closedBy: string) {
|
||||
const reservation = await repo.findForClose(id, companyId)
|
||||
const workflow = buildReservationWorkflow(reservation)
|
||||
|
||||
if (reservation.status !== 'COMPLETED') throw new AppError('Only completed reservations can be closed', 400, 'invalid_status')
|
||||
if (workflow.closed) throw new AppError('This reservation is already closed', 400, 'already_closed')
|
||||
|
||||
const extras = parseReservationExtras(reservation.extras)
|
||||
extras.reservationClosedAt = new Date().toISOString()
|
||||
extras.reservationClosedBy = closedBy
|
||||
|
||||
const updated = await prisma.reservation.update({
|
||||
where: { id: reservation.id },
|
||||
data: { extras: extras as any },
|
||||
include: { vehicle: true, customer: true, insurances: true, additionalDrivers: true, rentalPayments: true, damageReports: true },
|
||||
})
|
||||
return serializeReservationForDashboard(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)) {
|
||||
await repo.updateVehicleStatus(reservation.vehicleId, 'AVAILABLE')
|
||||
}
|
||||
return updated
|
||||
}
|
||||
Reference in New Issue
Block a user