633 lines
26 KiB
JavaScript
633 lines
26 KiB
JavaScript
"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
|