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 { reviewRequestEmail, type Lang } from '../../lib/emailTranslations' import { coerceNotificationLocale } from '../../services/notificationLocalizationService' 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, company, vehicle] = await Promise.all([ repo.findCustomerById(reservation.customerId), repo.findCompanyWithBrand(companyId), repo.findVehicle(reservation.vehicleId, companyId), ]) const fallbackLocale = coerceNotificationLocale(company?.brand?.defaultLocale) await sendNotification({ type: 'BOOKING_CONFIRMED', companyId, renterId: reservation.renterId ?? undefined, email: customer.email, channels: ['EMAIL', 'IN_APP'], locale: reservation.renterId ? undefined : fallbackLocale, templateKey: 'booking.confirmed', templateVariables: { firstName: customer.firstName, companyName: company?.brand?.displayName ?? company?.name ?? 'RentalDriveGo', startDate: reservation.startDate, endDate: reservation.endDate, vehicleName: `${vehicle.year} ${vehicle.make} ${vehicle.model}`, }, }).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 }