feat(extract): create a linked cost from the booking price on import

When a confirmation carries a total price, record it as a real expense
linked to the reservation (in the matching Costs category) instead of
leaving the amount in metadata only. Gated on the Costs addon.
This commit is contained in:
Maurice
2026-06-24 23:29:59 +02:00
parent 5fa79bba52
commit 223f5ce9bc
@@ -4,12 +4,16 @@ import { checkPermission } from '../../services/permissions';
import { verifyTripAccess } from '../../services/tripAccess';
import { createReservation } from '../../services/reservationService';
import { createPlace } from '../../services/placeService';
import { createBudgetItem } from '../../services/budgetService';
import { isAddonEnabled } from '../../services/adminService';
import { ADDON_IDS } from '../../addons';
import { searchNominatim } from '../../services/mapsService';
import { db } from '../../db/database';
import type { User } from '../../types';
import { KitineraryExtractorService } from './kitinerary-extractor.service';
import { LlmParseService } from '../llm-parse/llm-parse.service';
import { mapReservations } from './kitinerary-mapper';
import { typeToCostCategory } from '@trek/shared';
import type { BookingImportPreviewItem, BookingImportPreviewResponse, BookingImportConfirmResponse, BookingImportMode, BookingImportFileReport, Reservation } from '@trek/shared';
import type { ParsedBookingItem, KiReservation } from './kitinerary.types';
@@ -217,6 +221,33 @@ export class BookingImportService {
broadcast(tripId, 'accommodation:created', {}, socketId);
}
// Turn an extracted price into a real linked cost (Costs addon), so the
// booking shows up as an expense — not just a price in metadata.
if (isAddonEnabled(ADDON_IDS.BUDGET)) {
const meta =
reservationData.metadata && typeof reservationData.metadata === 'object'
? (reservationData.metadata as Record<string, unknown>)
: null;
const price = meta && meta.price != null ? Number(meta.price) : NaN;
if (Number.isFinite(price) && price > 0) {
try {
const budgetItem = createBudgetItem(tripId, {
category: typeToCostCategory(item.type),
name: item.title,
total_price: price,
currency: meta && typeof meta.priceCurrency === 'string' ? meta.priceCurrency : null,
reservation_id: reservation.id,
});
broadcast(tripId, 'budget:created', { item: budgetItem }, socketId);
} catch (err) {
console.error(
`[booking-import] Failed to create cost for "${item.title}":`,
err instanceof Error ? err.message : err,
);
}
}
}
created.push(reservation);
} catch (err) {
console.error(`[booking-import] Failed to create reservation "${item.title}":`, err instanceof Error ? err.message : err);