archetecture security fix
This commit is contained in:
+633
@@ -0,0 +1,633 @@
|
||||
"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
|
||||
Reference in New Issue
Block a user