"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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.listVehicles = listVehicles; exports.getVehicle = getVehicle; exports.createVehicle = createVehicle; exports.updateVehicle = updateVehicle; exports.setStatus = setStatus; exports.deleteVehicle = deleteVehicle; exports.uploadPhotos = uploadPhotos; exports.deletePhoto = deletePhoto; exports.setPublished = setPublished; exports.checkAvailability = checkAvailability; exports.getCalendar = getCalendar; exports.createCalendarBlock = createCalendarBlock; exports.deleteCalendarBlock = deleteCalendarBlock; exports.getMaintenanceLogs = getMaintenanceLogs; exports.createMaintenanceLog = createMaintenanceLog; exports.getVehiclePricing = getVehiclePricing; exports.updateVehiclePricing = updateVehiclePricing; exports.createVehiclePricingRule = createVehiclePricingRule; exports.updateVehiclePricingRule = updateVehiclePricingRule; exports.deleteVehiclePricingRule = deleteVehiclePricingRule; const storage_1 = require("../../lib/storage"); const errors_1 = require("../../http/errors"); const vehicle_presenter_1 = require("./vehicle.presenter"); const repo = __importStar(require("./vehicle.repo")); const DAY_MS = 24 * 60 * 60 * 1000; const RULE_PRIORITY = { MANUAL_OVERRIDE: 6, HOLIDAY: 5, SPECIAL_EVENT: 4, SEASONAL: 3, WEEKEND: 2, DATE_RANGE: 1, }; function isPricingStorageMissing(error) { if (!error || typeof error !== 'object') return false; const candidate = error; return (candidate.code === 'P2021' || candidate.message?.includes('vehicle_pricing_configurations') === true || candidate.message?.includes('vehicle_pricing_rules') === true || candidate.message?.includes('vehicle_price_history') === true); } function normalizeLocations(locations) { if (!Array.isArray(locations)) return []; const seen = new Set(); const normalized = []; for (const entry of locations) { if (typeof entry !== 'string') continue; const value = entry.trim(); const key = value.toLocaleLowerCase(); if (!value || seen.has(key)) continue; seen.add(key); normalized.push(value); } return normalized; } function applyLocationSettings(data) { const patch = { ...data }; if (patch.pickupLocations !== undefined) { patch.pickupLocations = normalizeLocations(patch.pickupLocations); } if (patch.dropoffLocations !== undefined) { patch.dropoffLocations = normalizeLocations(patch.dropoffLocations); } if (patch.allowDifferentDropoff === false) { patch.dropoffLocations = []; } if (patch.allowDifferentDropoff === true && patch.dropoffLocations !== undefined && patch.dropoffLocations.length === 0) { throw new errors_1.ValidationError('Drop-off locations are required when different drop-off is enabled'); } return patch; } function toStartOfDay(value) { const date = new Date(value); date.setHours(0, 0, 0, 0); return date; } function toEndOfDay(value) { const date = new Date(value); date.setHours(23, 59, 59, 999); return date; } function isWeekend(date) { const day = date.getDay(); return day === 0 || day === 6; } function clampRate(value, min, max) { if (min != null && value < min) return min; if (max != null && value > max) return max; return value; } function validateRateBounds(minimum, maximum, label = 'Daily') { if (minimum != null && maximum != null && minimum > maximum) { throw new errors_1.ValidationError(`${label} minimum rate must be less than or equal to the maximum rate`); } } function normalizePricingConfigurationInput(data) { const normalized = { pricingMode: data.pricingMode, baseDailyRate: data.baseDailyRate, weeklyRate: data.weeklyRate ?? null, weekendRate: data.weekendRate ?? null, holidayRate: data.holidayRate ?? null, monthlyRate: data.monthlyRate ?? null, longTermDailyRate: data.longTermDailyRate ?? null, minimumDailyRate: data.minimumDailyRate ?? null, maximumDailyRate: data.maximumDailyRate ?? null, maxDailyPriceMovementPct: data.maxDailyPriceMovementPct, automaticPricingEnabled: data.automaticPricingEnabled, approvalRequired: data.approvalRequired, }; validateRateBounds(normalized.minimumDailyRate, normalized.maximumDailyRate); validateRateBounds(normalized.baseDailyRate, normalized.maximumDailyRate, 'Base'); return normalized; } function normalizePricingRuleInput(data) { const normalized = { name: data.name, ruleType: data.ruleType, startDate: toStartOfDay(data.startDate), endDate: toEndOfDay(data.endDate), dailyRate: data.dailyRate ?? null, weeklyRate: data.weeklyRate ?? null, weekendRate: data.weekendRate ?? null, monthlyRate: data.monthlyRate ?? null, minDailyRate: data.minDailyRate ?? null, maxDailyRate: data.maxDailyRate ?? null, automaticAdjustmentPct: data.automaticAdjustmentPct ?? null, priceAdjustment: data.priceAdjustment ?? null, isActive: data.isActive, sortOrder: data.sortOrder, }; if (normalized.endDate < normalized.startDate) { throw new errors_1.ValidationError('Rule end date must be on or after the start date'); } validateRateBounds(normalized.minDailyRate, normalized.maxDailyRate); const hasPricingEffect = [ normalized.dailyRate, normalized.weeklyRate, normalized.weekendRate, normalized.monthlyRate, normalized.minDailyRate, normalized.maxDailyRate, normalized.automaticAdjustmentPct, normalized.priceAdjustment, ].some((value) => value != null); if (!hasPricingEffect) { throw new errors_1.ValidationError('Pricing rules must define at least one rate, bound, or adjustment'); } return normalized; } async function ensurePricingConfigurationForVehicle(vehicleId, companyId) { const vehicle = await repo.findById(vehicleId, companyId); if (!vehicle) throw new errors_1.NotFoundError('Vehicle not found'); let configuration = null; let persistenceAvailable = true; try { configuration = await repo.findPricingConfiguration(vehicleId); } catch (error) { if (!isPricingStorageMissing(error)) throw error; persistenceAvailable = false; } if (!persistenceAvailable) { return { vehicle, persistenceAvailable, configuration: { id: `transient-${vehicle.id}`, vehicleId: vehicle.id, pricingMode: 'MANUAL', baseDailyRate: vehicle.dailyRate, weeklyRate: vehicle.dailyRate * 7, weekendRate: null, holidayRate: null, monthlyRate: null, longTermDailyRate: null, minimumDailyRate: null, maximumDailyRate: null, maxDailyPriceMovementPct: 10, automaticPricingEnabled: false, approvalRequired: false, rules: [], history: [], createdAt: vehicle.createdAt, updatedAt: vehicle.updatedAt, }, }; } if (!configuration) { configuration = await repo.createPricingConfiguration({ vehicleId, pricingMode: 'MANUAL', baseDailyRate: vehicle.dailyRate, weeklyRate: vehicle.dailyRate * 7, automaticPricingEnabled: false, approvalRequired: false, maxDailyPriceMovementPct: 10, }); } return { vehicle, configuration, persistenceAvailable }; } async function syncPricingBaseRateFromVehicle(vehicleId, previousDailyRate, nextDailyRate) { const configuration = await repo.findPricingConfiguration(vehicleId); if (!configuration) return; const currentBaseRate = configuration.baseDailyRate ?? previousDailyRate; if (currentBaseRate === nextDailyRate) return; await repo.updatePricingConfiguration(configuration.id, { baseDailyRate: nextDailyRate }); await repo.createPriceHistory({ configurationId: configuration.id, vehicleId, source: 'CONFIG_UPDATE', previousDailyRate: currentBaseRate, nextDailyRate, previousWeeklyRate: configuration.weeklyRate ?? null, nextWeeklyRate: configuration.weeklyRate ?? null, note: 'Base daily rate synced from vehicle details', }); } function buildPricePreview(vehicle, configuration) { const baseDailyRate = configuration.baseDailyRate ?? vehicle.dailyRate; const activeRules = (configuration.rules ?? []).filter((rule) => rule.isActive); const items = Array.from({ length: 14 }, (_, index) => { const date = new Date(Date.now() + (index * DAY_MS)); date.setHours(12, 0, 0, 0); let dailyRate = baseDailyRate; let pricingType = configuration.pricingMode === 'AUTOMATIC' && configuration.automaticPricingEnabled ? 'AUTOMATIC_BASE' : 'MANUAL_BASE'; if (isWeekend(date) && configuration.weekendRate != null) { dailyRate = configuration.weekendRate; pricingType = 'WEEKEND_RATE'; } const matchingRule = activeRules .filter((rule) => date >= new Date(rule.startDate) && date <= new Date(rule.endDate)) .sort((a, b) => { const priorityDiff = (RULE_PRIORITY[b.ruleType] ?? 0) - (RULE_PRIORITY[a.ruleType] ?? 0); if (priorityDiff !== 0) return priorityDiff; if (a.sortOrder !== b.sortOrder) return b.sortOrder - a.sortOrder; return new Date(b.startDate).getTime() - new Date(a.startDate).getTime(); })[0]; if (matchingRule) { if (matchingRule.dailyRate != null) { dailyRate = matchingRule.dailyRate; } else { if (matchingRule.weekendRate != null && isWeekend(date)) { dailyRate = matchingRule.weekendRate; } if (matchingRule.priceAdjustment != null) { dailyRate += matchingRule.priceAdjustment; } if (matchingRule.automaticAdjustmentPct != null) { dailyRate = Math.round(dailyRate * (100 + matchingRule.automaticAdjustmentPct) / 100); } } pricingType = configuration.pricingMode === 'AUTOMATIC' ? 'AUTOMATIC_RULE' : 'RULE_RATE'; dailyRate = clampRate(dailyRate, matchingRule.minDailyRate ?? configuration.minimumDailyRate, matchingRule.maxDailyRate ?? configuration.maximumDailyRate); } else { dailyRate = clampRate(dailyRate, configuration.minimumDailyRate, configuration.maximumDailyRate); } return { date: date.toISOString().slice(0, 10), dailyRate, pricingType, matchedRuleName: matchingRule?.name ?? null, }; }); return { today: items[0], next14Days: items, }; } function presentVehiclePricing(vehicle, configuration) { return { configuration: { id: configuration.id, vehicleId: configuration.vehicleId, pricingMode: configuration.pricingMode, baseDailyRate: configuration.baseDailyRate ?? vehicle.dailyRate, weeklyRate: configuration.weeklyRate ?? null, weekendRate: configuration.weekendRate ?? null, holidayRate: configuration.holidayRate ?? null, monthlyRate: configuration.monthlyRate ?? null, longTermDailyRate: configuration.longTermDailyRate ?? null, minimumDailyRate: configuration.minimumDailyRate ?? null, maximumDailyRate: configuration.maximumDailyRate ?? null, maxDailyPriceMovementPct: configuration.maxDailyPriceMovementPct, automaticPricingEnabled: configuration.automaticPricingEnabled, approvalRequired: configuration.approvalRequired, createdAt: configuration.createdAt, updatedAt: configuration.updatedAt, }, rules: (configuration.rules ?? []).map((rule) => ({ id: rule.id, name: rule.name, ruleType: rule.ruleType, startDate: rule.startDate, endDate: rule.endDate, dailyRate: rule.dailyRate, weeklyRate: rule.weeklyRate, weekendRate: rule.weekendRate, monthlyRate: rule.monthlyRate, minDailyRate: rule.minDailyRate, maxDailyRate: rule.maxDailyRate, automaticAdjustmentPct: rule.automaticAdjustmentPct, priceAdjustment: rule.priceAdjustment, isActive: rule.isActive, sortOrder: rule.sortOrder, createdAt: rule.createdAt, updatedAt: rule.updatedAt, })), history: (configuration.history ?? []).map((entry) => ({ id: entry.id, source: entry.source, changedByEmployeeId: entry.changedByEmployeeId, previousDailyRate: entry.previousDailyRate, nextDailyRate: entry.nextDailyRate, previousWeeklyRate: entry.previousWeeklyRate, nextWeeklyRate: entry.nextWeeklyRate, note: entry.note, effectiveFrom: entry.effectiveFrom, createdAt: entry.createdAt, })), preview: buildPricePreview(vehicle, configuration), }; } async function listVehicles(companyId, query) { const page = query.page ?? 1; const pageSize = query.pageSize ?? 20; const { status, category, published } = query; const where = { companyId }; if (status) where.status = status; if (category) where.category = category; if (published !== undefined) where.isPublished = published === 'true'; const [vehicles, total] = await repo.findMany(where, (page - 1) * pageSize, pageSize); return (0, vehicle_presenter_1.presentVehicleList)(vehicles.map(vehicle_presenter_1.presentVehicle), { total, page, pageSize, totalPages: Math.ceil(total / pageSize) }); } async function getVehicle(id, companyId) { const vehicle = await repo.findById(id, companyId); if (!vehicle) throw new errors_1.NotFoundError('Vehicle not found'); return (0, vehicle_presenter_1.presentVehicle)(vehicle); } async function createVehicle(data, companyId) { return (0, vehicle_presenter_1.presentVehicle)(await repo.create({ dailyRate: 0, ...applyLocationSettings(data), companyId, })); } const PUBLISHED_STATUSES = new Set(['AVAILABLE', 'RESERVED', 'READY', 'RENTED']); async function updateVehicle(id, companyId, data) { const patch = applyLocationSettings(data); if (patch.status) { patch.isPublished = PUBLISHED_STATUSES.has(patch.status); } const existing = await repo.findFirst(id, companyId); if (!existing) throw new errors_1.NotFoundError('Vehicle not found'); const updated = await repo.updateById(id, patch); if (patch.dailyRate !== undefined && typeof patch.dailyRate === 'number') { await syncPricingBaseRateFromVehicle(id, existing.dailyRate, patch.dailyRate); } return (0, vehicle_presenter_1.presentVehicle)(updated); } async function setStatus(id, companyId, status) { const existing = await repo.findFirst(id, companyId); if (!existing) throw new errors_1.NotFoundError('Vehicle not found'); const isPublished = PUBLISHED_STATUSES.has(status); return (0, vehicle_presenter_1.presentVehicle)(await repo.updateById(id, { status, isPublished })); } async function deleteVehicle(id, companyId) { return repo.softDelete(id, companyId); } async function uploadPhotos(id, companyId, files) { const vehicle = await repo.findById(id, companyId); if (!vehicle) throw new errors_1.NotFoundError('Vehicle not found'); const urls = await Promise.all(files.map((f) => (0, storage_1.uploadImage)(f.buffer, `companies/${companyId}/vehicles`))); return (0, vehicle_presenter_1.presentVehicle)(await repo.updateById(id, { photos: [...vehicle.photos, ...urls] })); } async function deletePhoto(id, companyId, idx) { const vehicle = await repo.findById(id, companyId); if (!vehicle) throw new errors_1.NotFoundError('Vehicle not found'); const photos = vehicle.photos.filter((_, i) => i !== idx); return (0, vehicle_presenter_1.presentVehicle)(await repo.updateById(id, { photos })); } async function setPublished(id, companyId, isPublished) { await repo.setPublished(id, companyId, isPublished); } async function checkAvailability(id, companyId, startDate, endDate) { const start = new Date(startDate); const end = new Date(endDate); const [conflicts, blocks] = await Promise.all([ repo.findReservationConflicts(id, companyId, start, end), repo.findCalendarBlockConflicts(id, start, end), ]); return { available: conflicts.length === 0 && blocks.length === 0, conflicts, blocks }; } async function getCalendar(id, companyId, year, month) { const vehicle = await repo.findById(id, companyId); if (!vehicle) throw new errors_1.NotFoundError('Vehicle not found'); const rangeStart = new Date(year, month - 1, 1); const rangeEnd = new Date(year, month, 1); const [reservations, blocks] = await repo.findCalendarEvents(id, companyId, rangeStart, rangeEnd); return [ ...reservations.map((r) => ({ id: r.id, type: 'RESERVATION', startDate: r.startDate, endDate: r.endDate, status: r.status, label: r.customer ? `${r.customer.firstName} ${r.customer.lastName}` : 'Reserved', })), ...blocks.map((b) => ({ id: b.id, type: b.type === 'MAINTENANCE' ? 'MAINTENANCE' : 'BLOCK', startDate: b.startDate, endDate: b.endDate, status: null, label: b.reason ?? (b.type === 'MAINTENANCE' ? 'Maintenance' : 'Blocked'), })), ]; } async function createCalendarBlock(id, companyId, data) { const start = new Date(data.startDate); const end = new Date(data.endDate); if (end <= start) throw new errors_1.ValidationError('endDate must be after startDate'); const vehicle = await repo.findById(id, companyId); if (!vehicle) throw new errors_1.NotFoundError('Vehicle not found'); return repo.createCalendarBlock({ vehicleId: id, startDate: start, endDate: end, reason: data.reason, type: data.type ?? 'MANUAL' }); } async function deleteCalendarBlock(vehicleId, companyId, blockId) { const vehicle = await repo.findById(vehicleId, companyId); if (!vehicle) throw new errors_1.NotFoundError('Vehicle not found'); await repo.deleteCalendarBlock(blockId, vehicleId); } async function getMaintenanceLogs(id, companyId) { const vehicle = await repo.findById(id, companyId); if (!vehicle) throw new errors_1.NotFoundError('Vehicle not found'); return repo.findMaintenanceLogs(id); } async function createMaintenanceLog(id, companyId, data) { const vehicle = await repo.findById(id, companyId); if (!vehicle) throw new errors_1.NotFoundError('Vehicle not found'); return repo.createMaintenanceLog({ ...data, vehicleId: id, performedAt: new Date(data.performedAt), nextDueAt: data.nextDueAt ? new Date(data.nextDueAt) : undefined, }); } async function getVehiclePricing(id, companyId) { const { vehicle, configuration } = await ensurePricingConfigurationForVehicle(id, companyId); return presentVehiclePricing(vehicle, configuration); } async function updateVehiclePricing(id, companyId, employeeId, data) { const { vehicle, configuration, persistenceAvailable } = await ensurePricingConfigurationForVehicle(id, companyId); if (!persistenceAvailable) { throw new errors_1.AppError('Vehicle pricing tables are not available yet. Run the latest database migration, then retry.', 503, 'pricing_migration_required'); } const nextConfig = normalizePricingConfigurationInput(data); const currentBaseDailyRate = configuration.baseDailyRate ?? vehicle.dailyRate; const currentWeeklyRate = configuration.weeklyRate ?? null; const changed = [ 'pricingMode', 'baseDailyRate', 'weeklyRate', 'weekendRate', 'holidayRate', 'monthlyRate', 'longTermDailyRate', 'minimumDailyRate', 'maximumDailyRate', 'maxDailyPriceMovementPct', 'automaticPricingEnabled', 'approvalRequired', ].some((key) => (configuration[key] ?? null) !== (nextConfig[key] ?? null)); await repo.updatePricingConfiguration(configuration.id, nextConfig); if (vehicle.dailyRate !== nextConfig.baseDailyRate) { await repo.updateById(id, { dailyRate: nextConfig.baseDailyRate }); } if (changed) { await repo.createPriceHistory({ configurationId: configuration.id, vehicleId: id, source: 'CONFIG_UPDATE', changedByEmployeeId: employeeId, previousDailyRate: currentBaseDailyRate, nextDailyRate: nextConfig.baseDailyRate, previousWeeklyRate: currentWeeklyRate, nextWeeklyRate: nextConfig.weeklyRate ?? null, note: data.note, }); } return getVehiclePricing(id, companyId); } async function createVehiclePricingRule(id, companyId, employeeId, data) { const { configuration, persistenceAvailable } = await ensurePricingConfigurationForVehicle(id, companyId); if (!persistenceAvailable) { throw new errors_1.AppError('Vehicle pricing tables are not available yet. Run the latest database migration, then retry.', 503, 'pricing_migration_required'); } const payload = normalizePricingRuleInput(data); const rule = await repo.createPricingRule({ configurationId: configuration.id, ...payload, }); await repo.createPriceHistory({ configurationId: configuration.id, vehicleId: id, source: payload.ruleType === 'MANUAL_OVERRIDE' ? 'MANUAL_OVERRIDE' : 'RULE_CREATED', changedByEmployeeId: employeeId, previousDailyRate: configuration.baseDailyRate ?? null, nextDailyRate: payload.dailyRate, previousWeeklyRate: configuration.weeklyRate ?? null, nextWeeklyRate: payload.weeklyRate, note: `Created pricing rule: ${rule.name}`, effectiveFrom: rule.startDate, }); return getVehiclePricing(id, companyId); } async function updateVehiclePricingRule(id, companyId, ruleId, employeeId, data) { const { configuration, persistenceAvailable } = await ensurePricingConfigurationForVehicle(id, companyId); if (!persistenceAvailable) { throw new errors_1.AppError('Vehicle pricing tables are not available yet. Run the latest database migration, then retry.', 503, 'pricing_migration_required'); } const existingRule = await repo.findPricingRule(ruleId, configuration.id); if (!existingRule) throw new errors_1.NotFoundError('Pricing rule not found'); const mergedRule = normalizePricingRuleInput({ ...existingRule, ...data, startDate: data.startDate ?? existingRule.startDate, endDate: data.endDate ?? existingRule.endDate, }); const updatedRule = await repo.updatePricingRule(ruleId, mergedRule); await repo.createPriceHistory({ configurationId: configuration.id, vehicleId: id, source: mergedRule.ruleType === 'MANUAL_OVERRIDE' ? 'MANUAL_OVERRIDE' : 'RULE_UPDATED', changedByEmployeeId: employeeId, previousDailyRate: existingRule.dailyRate, nextDailyRate: updatedRule.dailyRate, previousWeeklyRate: existingRule.weeklyRate, nextWeeklyRate: updatedRule.weeklyRate, note: `Updated pricing rule: ${updatedRule.name}`, effectiveFrom: updatedRule.startDate, }); return getVehiclePricing(id, companyId); } async function deleteVehiclePricingRule(id, companyId, ruleId, employeeId) { const { configuration, persistenceAvailable } = await ensurePricingConfigurationForVehicle(id, companyId); if (!persistenceAvailable) { throw new errors_1.AppError('Vehicle pricing tables are not available yet. Run the latest database migration, then retry.', 503, 'pricing_migration_required'); } const existingRule = await repo.findPricingRule(ruleId, configuration.id); if (!existingRule) throw new errors_1.NotFoundError('Pricing rule not found'); await repo.deletePricingRule(ruleId, configuration.id); await repo.createPriceHistory({ configurationId: configuration.id, vehicleId: id, source: existingRule.ruleType === 'MANUAL_OVERRIDE' ? 'MANUAL_OVERRIDE' : 'RULE_DELETED', changedByEmployeeId: employeeId, previousDailyRate: existingRule.dailyRate, nextDailyRate: configuration.baseDailyRate ?? null, previousWeeklyRate: existingRule.weeklyRate, nextWeeklyRate: configuration.weeklyRate ?? null, note: `Deleted pricing rule: ${existingRule.name}`, effectiveFrom: existingRule.startDate, }); return getVehiclePricing(id, companyId); } //# sourceMappingURL=vehicle.service.js.map