"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.confirmReservation = confirmReservation; exports.checkinReservation = checkinReservation; exports.checkoutReservation = checkoutReservation; exports.closeReservation = closeReservation; exports.extendReservation = extendReservation; exports.cancelReservation = cancelReservation; const crypto_1 = __importDefault(require("crypto")); const prisma_1 = require("../../lib/prisma"); const errors_1 = require("../../http/errors"); const licenseValidationService_1 = require("../../services/licenseValidationService"); const notificationService_1 = require("../../services/notificationService"); const emailTranslations_1 = require("../../lib/emailTranslations"); const notificationLocalizationService_1 = require("../../services/notificationLocalizationService"); const reservation_presenter_1 = require("./reservation.presenter"); const repo = __importStar(require("./reservation.repo")); function buildPickupAddress(value) { if (!value || typeof value !== 'object' || Array.isArray(value)) return ''; const address = value; return [ typeof address.streetAddress === 'string' ? address.streetAddress.trim() : '', typeof address.city === 'string' ? address.city.trim() : '', typeof address.country === 'string' ? address.country.trim() : '', ].filter(Boolean).join(', '); } async function assertLicenseCompliance(reservationId, companyId) { const reservation = await repo.findForLicenseCheck(reservationId, companyId); const customerLicense = (0, licenseValidationService_1.validateLicense)(reservation.customer.licenseExpiry); const customerDenied = ['DENIED', 'EXPIRED'].includes(reservation.customer.licenseValidationStatus); if (customerDenied || customerLicense.status === 'EXPIRED') { throw new errors_1.AppError('Primary driver license is not valid for this reservation', 400, 'invalid_primary_license'); } if (customerLicense.requiresApproval && reservation.customer.licenseValidationStatus !== 'APPROVED') { throw new errors_1.AppError('Primary driver license requires manager approval before this reservation can proceed', 400, 'primary_license_requires_approval'); } const blockedDriver = reservation.additionalDrivers.find((d) => { if (d.licenseExpired) return true; return d.requiresApproval && !d.approvedAt; }); if (blockedDriver) { throw new errors_1.AppError(`Additional driver ${blockedDriver.firstName} ${blockedDriver.lastName} requires approval before this reservation can proceed`, 400, 'additional_driver_requires_approval'); } } async function confirmReservation(id, companyId) { const reservation = await repo.findByIdSimple(id, companyId); if (reservation.status !== 'DRAFT') throw new errors_1.AppError('Only DRAFT reservations can be confirmed', 400, 'invalid_status'); 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), repo.findCompanyWithBrand(companyId), repo.findVehicle(reservation.vehicleId, companyId), ]); const fallbackLocale = (0, notificationLocalizationService_1.coerceNotificationLocale)(company?.brand?.defaultLocale); const pickupLocation = reservation.pickupLocation?.trim() || buildPickupAddress(company?.address); await (0, notificationService_1.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: { type: 'date', value: reservation.startDate, options: { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', }, }, endDate: { type: 'date', value: reservation.endDate, options: { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', }, }, pickupLocation, vehicleName: `${vehicle.year} ${vehicle.make} ${vehicle.model}`, }, }).catch(() => null); return updated; } async function checkinReservation(id, companyId, mileage) { const reservation = await repo.findByIdSimple(id, companyId); if (reservation.status !== 'CONFIRMED') throw new errors_1.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; } async function checkoutReservation(id, companyId, mileage) { const reservation = await repo.findByIdForCheckout(id, companyId); if (reservation.status !== 'ACTIVE') throw new errors_1.AppError('Only ACTIVE reservations can be checked out', 400, 'invalid_status'); const reviewToken = reservation.reviewToken ?? crypto_1.default.randomBytes(32).toString('hex'); const updated = await repo.updateById(id, { status: 'COMPLETED', checkedOutAt: new Date(), checkOutMileage: mileage ?? null, reviewToken, }); await repo.updateVehicleStatus(reservation.vehicleId, 'RETURNED', mileage); return updated; } async function closeReservation(id, companyId, closedBy) { const reservation = await repo.findForClose(id, companyId); const workflow = (0, reservation_presenter_1.buildReservationWorkflow)(reservation); if (reservation.status !== 'COMPLETED') throw new errors_1.AppError('Only completed reservations can be closed', 400, 'invalid_status'); if (workflow.closed) throw new errors_1.AppError('This reservation is already closed', 400, 'already_closed'); const extras = (0, reservation_presenter_1.parseReservationExtras)(reservation.extras); extras.reservationClosedAt = new Date().toISOString(); extras.reservationClosedBy = closedBy; const updated = await prisma_1.prisma.reservation.update({ where: { id: reservation.id }, data: { extras: extras }, include: { vehicle: true, customer: true, insurances: true, additionalDrivers: true, rentalPayments: true, damageReports: true }, }); // Send review request email if eligible if (reservation.reviewToken && !reservation.reviewPaused) { try { const customer = updated.customer; if (!customer.reviewOptOut && customer.email) { const company = await repo.findCompanyWithBrand(companyId); const lang = (company?.brand?.defaultLocale ?? 'fr'); 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}`; (0, notificationService_1.sendTransactionalEmail)({ to: customer.email, subject: emailTranslations_1.reviewRequestEmail.subject(vehicleLabel, lang), html: emailTranslations_1.reviewRequestEmail.html({ firstName: customer.firstName, companyName, vehicleLabel, reviewUrl }, lang), text: emailTranslations_1.reviewRequestEmail.text({ firstName: customer.firstName, companyName, vehicleLabel, reviewUrl }, lang), }).catch(() => null); await prisma_1.prisma.reservation.update({ where: { id: reservation.id }, data: { reviewRequestSentAt: new Date() }, }); } } catch { // Non-blocking: do not fail close if email fails } } return (0, reservation_presenter_1.serializeReservationForDashboard)(updated); } async function extendReservation(id, companyId, newEndDate, reason) { const reservation = await repo.findByIdSimple(id, companyId); if (!['CONFIRMED', 'ACTIVE'].includes(reservation.status)) { throw new errors_1.AppError('Only CONFIRMED or ACTIVE reservations can be extended', 400, 'invalid_status'); } if (newEndDate <= reservation.endDate) { throw new errors_1.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 errors_1.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 = (0, reservation_presenter_1.parseReservationExtras)(reservation.extras); if (!Array.isArray(extras.extensions)) extras.extensions = []; extras.extensions.push({ previousEndDate: reservation.endDate.toISOString(), newEndDate: newEndDate.toISOString(), additionalDays, additionalAmount, reason, approvedAt: new Date().toISOString(), }); const updated = await prisma_1.prisma.reservation.update({ where: { id }, data: { endDate: newEndDate, totalDays: reservation.totalDays + additionalDays, totalAmount: reservation.totalAmount + additionalAmount, extras: extras, }, }); return updated; } async function cancelReservation(id, companyId, reason) { const reservation = await repo.findByIdSimple(id, companyId); if (['COMPLETED', 'CANCELLED'].includes(reservation.status)) { throw new errors_1.AppError('Reservation cannot be cancelled', 400, 'invalid_status'); } const updated = await repo.updateById(id, { status: 'CANCELLED', cancelReason: reason ?? null, cancelledBy: 'COMPANY' }); if (['CONFIRMED', 'ACTIVE', 'DRAFT'].includes(reservation.status)) { await repo.updateVehicleStatus(reservation.vehicleId, 'AVAILABLE'); } return updated; } //# sourceMappingURL=reservation.lifecycle.service.js.map