Keep api/client.ts and trips.service.ts within their size baselines

The admin switch for wanderer moves to api/wanderer.ts and the recorded
track restore to api/recordedTrack.ts; trips.service.ts keeps dev's
formatting with only the derived-from remap on copy.
This commit is contained in:
Maurice
2026-10-15 18:16:25 +02:00
parent f690065de8
commit f1c437b8b8
8 changed files with 124 additions and 254 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
{
"e2e/help/fixtures.ts": 2291,
"src/api/client.ts": 1881,
"src/api/client.ts": 1916,
"src/components/Admin/AdminPluginsPanel.test.tsx": 2704,
"src/components/Admin/AdminPluginsPanel.tsx": 1667,
"src/components/Budget/CostsPanel.test.tsx": 2764,
@@ -24,7 +24,7 @@
"src/mobile/screens/admin/MAdminPluginsPanel.tsx": 1507,
"src/pages/JourneyDetailPage.test.tsx": 4030,
"src/pages/LoginPage.tsx": 1348,
"src/pages/TripPlannerPage.tsx": 1148,
"src/pages/TripPlannerPage.tsx": 1150,
"src/pages/atlas/useAtlas.ts": 1287,
"src/pages/tripPlanner/useTripPlanner.roadtrip.test.tsx": 3518,
"src/pages/tripPlanner/useTripPlanner.test.tsx": 3309,
+1 -7
View File
@@ -2,7 +2,7 @@ import { isEffectivelyOffline } from '../sync/networkMode'
import axios, { AxiosInstance } from 'axios'
import type { z } from 'zod'
import type { Day, Place, Trip } from '../types'
import type { AdminFeatureToggleResponse, MapsNearbyRequest, JourneyReorderEntryPhotosRequest, TransitProvider, GoogleQuotaUpdateRequest, NotificationDefaultsUpdateRequest, TourListItem, TourImportGpxResponse, TourCreateRequest, TourCreateResponse, TourDetailResponse } from '@trek/shared'
import type { MapsNearbyRequest, JourneyReorderEntryPhotosRequest, TransitProvider, GoogleQuotaUpdateRequest, NotificationDefaultsUpdateRequest, TourListItem, TourImportGpxResponse, TourCreateRequest, TourCreateResponse, TourDetailResponse } from '@trek/shared'
import { randomId } from '../utils/randomId'
import { postProviderPhotosInBatches } from './providerPhotoBatches'
import {
@@ -525,8 +525,6 @@ export const toursApi = {
apiClient.post(`/trips/${tripId}/tours`, body).then(r => r.data),
update: (tripId: number | string, placeId: number | string, body: TourCreateRequest): Promise<TourDetailResponse> =>
apiClient.put(`/trips/${tripId}/tours/${placeId}`, body).then(r => r.data),
restoreRecorded: (tripId: number | string, placeId: number | string): Promise<TourDetailResponse> =>
apiClient.post(`/trips/${tripId}/tours/${placeId}/restore-recorded`).then(r => r.data),
importGpx: (tripId: number | string, file: File): Promise<TourImportGpxResponse> => {
const fd = new FormData()
fd.append('file', file)
@@ -708,10 +706,6 @@ export const adminApi = {
checkVersion: () => apiClient.get('/admin/version-check').then(r => r.data),
getBagTracking: () => apiClient.get('/admin/bag-tracking').then(r => r.data),
updateBagTracking: (enabled: boolean) => apiClient.put('/admin/bag-tracking', { enabled }).then(r => r.data),
getToursWanderer: (): Promise<AdminFeatureToggleResponse> =>
apiClient.get('/admin/tours-wanderer').then(r => r.data),
updateToursWanderer: (enabled: boolean): Promise<AdminFeatureToggleResponse> =>
apiClient.put('/admin/tours-wanderer', { enabled }).then(r => r.data),
getPlacesPhotos: () => apiClient.get('/admin/places-photos').then(r => r.data),
updatePlacesPhotos: (enabled: boolean) => apiClient.put('/admin/places-photos', { enabled }).then(r => r.data),
getPlaceShadow: () => apiClient.get('/admin/place-shadow').then(r => r.data),
+8
View File
@@ -0,0 +1,8 @@
import type { TourDetailResponse } from '@trek/shared';
import apiClient from './client';
/** An import that was planned over in place keeps its recorded track; this brings it back. */
export const recordedTrackApi = {
restore: (tripId: number | string, placeId: number | string): Promise<TourDetailResponse> =>
apiClient.post(`/trips/${tripId}/tours/${placeId}/restore-recorded`).then((r) => r.data),
};
+8
View File
@@ -1,4 +1,5 @@
import type {
AdminFeatureToggleResponse,
WandererConnection,
WandererImportPreview,
WandererImportRequest,
@@ -39,3 +40,10 @@ export const wandererApi = {
importTrail: (tripId: number, body: WandererImportRequest): Promise<WandererImportResponse> =>
apiClient.post(`/trips/${tripId}/tours/wanderer/import`, body, upstream).then((r) => r.data),
};
/** The admin's switch for wanderer under the Tours addon. */
export const wandererAdminApi = {
get: (): Promise<AdminFeatureToggleResponse> => apiClient.get('/admin/tours-wanderer').then((r) => r.data),
update: (enabled: boolean): Promise<AdminFeatureToggleResponse> =>
apiClient.put('/admin/tours-wanderer', { enabled }).then((r) => r.data),
};
@@ -1,6 +1,7 @@
import { useEffect, useEffectEvent, useState } from 'react';
import { adminApi } from '../../../api/client';
import { wandererAdminApi } from '../../../api/wanderer';
import { useTranslation } from '../../../i18n';
import { useAddonStore } from '../../../store/addonStore';
import { useToast } from '../../shared/Toast';
@@ -31,8 +32,8 @@ export function useAddonManager() {
const loadOnMount = useEffectEvent(() => {
void loadAddons().finally(() => setLoading(false));
adminApi
.getToursWanderer()
wandererAdminApi
.get()
.then((flag) => setToursWanderer(flag.enabled))
.catch(() => toast.error(t('admin.addons.toast.error')));
});
@@ -67,7 +68,7 @@ export function useAddonManager() {
const next = !toursWanderer;
setToursWanderer(next);
try {
await adminApi.updateToursWanderer(next);
await wandererAdminApi.update(next);
} catch {
setToursWanderer(!next);
toast.error(t('admin.addons.toast.error'));
+2 -1
View File
@@ -1,5 +1,6 @@
import { computeTourMetrics, type GeometryPoint, type TourCreateRequest, type TourCreateResponse, type TourDetailResponse, type TourImportGpxResponse, type TourListItem, type TourWaypoint } from '@trek/shared'
import { toursApi } from '../api/client'
import { recordedTrackApi } from '../api/recordedTrack'
import { offlineDb, replaceTripTours, type CachedTour } from '../db/offlineDb'
import { mutationQueue, generateUUID, nextTempId } from '../sync/mutationQueue'
import { isEffectivelyOffline } from '../sync/networkMode'
@@ -178,7 +179,7 @@ export const tourRepo = {
/** Online only, like the import: the recorded track is kept on the server, not in the cache. */
async restoreRecorded(tripId: number | string, placeId: number): Promise<TourDetailResponse> {
const result = await toursApi.restoreRecorded(tripId, placeId)
const result = await recordedTrackApi.restore(tripId, placeId)
await offlineDb.tours.put({ ...result.tour, trip_id: Number(tripId), waypoints: result.waypoints })
return result
},
+1
View File
@@ -20,6 +20,7 @@
"src/nest/plugins/runtime/plugin-sdk.ts",
"src/nest/reservations/reservations.mcp.ts",
"src/nest/reservations/reservations.service.ts",
"src/nest/trips/trips.service.ts",
"src/nest/vacay/vacay.service.ts",
"tests/unit/db/repositories/Places.repository.test.ts",
"tests/unit/nest/atlas.service.test.ts",
+98 -241
View File
@@ -1,46 +1,31 @@
import { AssignmentParticipants } from '../../db/entities/AssignmentParticipants.entity';
import { BudgetCategoryOrder } from '../../db/entities/BudgetCategoryOrder.entity';
import { BudgetItemMembers } from '../../db/entities/BudgetItemMembers.entity';
import { BudgetItemPayers } from '../../db/entities/BudgetItemPayers.entity';
import { BudgetItems } from '../../db/entities/BudgetItems.entity';
import { DayAccommodations } from '../../db/entities/DayAccommodations.entity';
import { DayAssignments } from '../../db/entities/DayAssignments.entity';
import { DayNotes } from '../../db/entities/DayNotes.entity';
import { Injectable } from '@nestjs/common';
import path from 'path';
import { EntityManager } from '@mikro-orm/core';
import { Trips } from '../../db/entities/Trips.entity';
import { Days } from '../../db/entities/Days.entity';
import { JourneyEntries } from '../../db/entities/JourneyEntries.entity';
import { PackingBags } from '../../db/entities/PackingBags.entity';
import { PackingItems } from '../../db/entities/PackingItems.entity';
import { Users } from '../../db/entities/Users.entity';
import { Places } from '../../db/entities/Places.entity';
import { Reservations } from '../../db/entities/Reservations.entity';
import { RoadtripDayBoundaries } from '../../db/entities/RoadtripDayBoundaries.entity';
import { DayAssignments } from '../../db/entities/DayAssignments.entity';
import { AssignmentParticipants } from '../../db/entities/AssignmentParticipants.entity';
import { Tags } from '../../db/entities/Tags.entity';
import { DayNotes } from '../../db/entities/DayNotes.entity';
import { RoadtripVias } from '../../db/entities/RoadtripVias.entity';
import { RoadtripDayTracks } from '../../db/entities/RoadtripDayTracks.entity';
import { RoadtripPreferences } from '../../db/entities/RoadtripPreferences.entity';
import { RoadtripVias } from '../../db/entities/RoadtripVias.entity';
import { Tags } from '../../db/entities/Tags.entity';
import { RoadtripDayBoundaries } from '../../db/entities/RoadtripDayBoundaries.entity';
import { DayAccommodations } from '../../db/entities/DayAccommodations.entity';
import { Reservations } from '../../db/entities/Reservations.entity';
import { BudgetItems } from '../../db/entities/BudgetItems.entity';
import { BudgetItemMembers } from '../../db/entities/BudgetItemMembers.entity';
import { BudgetItemPayers } from '../../db/entities/BudgetItemPayers.entity';
import { BudgetCategoryOrder } from '../../db/entities/BudgetCategoryOrder.entity';
import { PackingBags } from '../../db/entities/PackingBags.entity';
import { PackingItems } from '../../db/entities/PackingItems.entity';
import { TodoItems } from '../../db/entities/TodoItems.entity';
import { TourWaypoints } from '../../db/entities/TourWaypoints.entity';
import { Tours } from '../../db/entities/Tours.entity';
import { Trips } from '../../db/entities/Trips.entity';
import { Users } from '../../db/entities/Users.entity';
import { TourWaypoints } from '../../db/entities/TourWaypoints.entity';
import type { TourWaypointRow } from '../../db/repositories/TourWaypoints.repository';
import type { Trip, User } from '../../types';
import { BudgetService, type CurrencyRebase } from '../budget/budget.service';
import { NotFoundError, ValidationError } from '../common/domain-errors';
import { legacyBoundIntegerText } from '../common/row-id';
import { appClock } from '../common/timezoneService';
import { UnitOfWork } from '../database/unit-of-work';
import { DaysService } from '../days/days.service';
import { PermissionsService } from '../permissions/permissions.service';
import { escapeLikePattern } from '../places/places.helpers';
import { RealtimeService } from '../realtime/realtime.service';
import { ReservationsService } from '../reservations/reservations.service';
import { SettingsService } from '../settings/settings.service';
import { StorageService } from '../storage/storage.service';
import { UnsplashService } from '../unsplash/unsplash.service';
import { VacayService } from '../vacay/vacay.service';
import { assertTripSpan } from './trips.helpers';
import { EntityManager } from '@mikro-orm/core';
import { Injectable } from '@nestjs/common';
import {
addIsoDays,
planDayGrid,
@@ -51,8 +36,22 @@ import {
type TrekWsPayload,
type TrekWsTripEventName,
} from '@trek/shared';
import path from 'path';
import { RealtimeService } from '../realtime/realtime.service';
import { PermissionsService } from '../permissions/permissions.service';
import type { Trip, User } from '../../types';
import { DaysService } from '../days/days.service';
import { BudgetService, type CurrencyRebase } from '../budget/budget.service';
import { ReservationsService } from '../reservations/reservations.service';
import { VacayService } from '../vacay/vacay.service';
import { UnsplashService } from '../unsplash/unsplash.service';
import { StorageService } from '../storage/storage.service';
import { SettingsService } from '../settings/settings.service';
import { escapeLikePattern } from '../places/places.helpers';
import { NotFoundError, ValidationError } from '../common/domain-errors';
import { UnitOfWork } from '../database/unit-of-work';
import { legacyBoundIntegerText } from '../common/row-id';
import { appClock } from '../common/timezoneService';
import { assertTripSpan } from './trips.helpers';
interface CreateTripData {
title: string;
@@ -100,14 +99,7 @@ export interface DeleteTripInfo {
}
export interface AddMemberResult {
member: {
id: number;
username: string;
email: string;
avatar?: string | null;
role: string;
avatar_url: string | null;
};
member: { id: number; username: string; email: string; avatar?: string | null; role: string; avatar_url: string | null };
targetUserId: number;
tripTitle: string;
}
@@ -329,12 +321,7 @@ export class TripsService {
return this.permissions.checkPermission(action, role, ownerId, userId, isMember);
}
broadcast<E extends TrekWsTripEventName>(
tripId: string,
event: E,
payload: TrekWsPayload<E>,
socketId: string | undefined,
): void {
broadcast<E extends TrekWsTripEventName>(tripId: string, event: E, payload: TrekWsPayload<E>, socketId: string | undefined): void {
this.realtime.broadcast(tripId, event, payload, socketId);
}
@@ -354,40 +341,28 @@ export class TripsService {
* runs in one `uow.transactional` — a savepoint when `updateTrip`'s own
* block already holds the transaction, its own transaction from `create`.
*/
async generateDays(
tripId: number | bigint | string,
startDate: string | null,
endDate: string | null,
dayCount?: number,
): Promise<DayGridPlan> {
async generateDays(tripId: number | bigint | string, startDate: string | null, endDate: string | null, dayCount?: number): Promise<DayGridPlan> {
const trip_id = Number(tripId);
return await this.uow.transactional(async () => {
const existing = await this.daysRepo.listForDayGrid(trip_id); // TP77
const stays = await this.daysRepo.listDayGridStays(trip_id); // TP78
const plan = planDayGrid({
days: existing.map((d) => ({
id: d.id,
day_number: d.day_number,
date: d.date,
hasPlanItems: !!d.has_plan_items,
})),
days: existing.map(d => ({ id: d.id, day_number: d.day_number, date: d.date, hasPlanItems: !!d.has_plan_items })),
stays,
startDate,
endDate,
dayCount,
});
const dateBefore = new Map(existing.map((d) => [d.id, d.date]));
const dateBefore = new Map(existing.map(d => [d.id, d.date]));
for (let i = 0; i < existing.length; i++) await this.daysRepo.setDayNumber(existing[i].id, -(i + 1)); // TP2
for (let i = 0; i < plan.rows.length; i++) {
const row = plan.rows[i];
if (row.id === null)
await this.daysRepo.insertDay({ trip_id, day_number: i + 1, date: row.date }); // TP10
if (row.id === null) await this.daysRepo.insertDay({ trip_id, day_number: i + 1, date: row.date }); // TP10
// A row that had no date and gets none is only renumbered, so an odd
// stored value is left as it was, the way the rebuild always treated it.
else if (row.date === null && !dateBefore.get(row.id))
await this.daysRepo.setDayNumber(row.id, i + 1); // TP2
else if (row.date === null && !dateBefore.get(row.id)) await this.daysRepo.setDayNumber(row.id, i + 1); // TP2
else await this.daysRepo.setDayNumberAndDate(row.id, i + 1, row.date); // TP9
}
for (const gone of plan.removed) await this.daysRepo.deleteById(gone.id); // TP11
@@ -410,19 +385,15 @@ export class TripsService {
if (startDate && !endDate) endDate = addIsoDays(startDate, 6);
else if (!startDate && endDate) startDate = addIsoDays(endDate, -6);
if (startDate && endDate) assertTripSpan(startDate, endDate);
const rd =
data.reminder_days !== undefined
? Number(data.reminder_days) >= 0 && Number(data.reminder_days) <= 30
? Number(data.reminder_days)
: 3
: 3;
const rd = data.reminder_days !== undefined
? (Number(data.reminder_days) >= 0 && Number(data.reminder_days) <= 30 ? Number(data.reminder_days) : 3)
: 3;
const currency = data.currency || (await this.defaultCurrencyFor(userId));
// The trip and its days commit together: a failure in between used to leave
// a dated trip without a single day.
const tripId = await this.uow.transactional(async () => {
const id = await this.tripsRepo.insertTrip({
// TP18
const id = await this.tripsRepo.insertTrip({ // TP18
user_id: userId,
title: data.title,
description: data.description || null,
@@ -441,7 +412,7 @@ export class TripsService {
/** TP20 — `TripsRepository.findForViewer`: `TRIP_SELECT` scoped to one trip AND the access predicate. */
async get(tripId: string | number, userId: number) {
return (await this.tripsRepo.findForViewer(tripId, userId)) as Trip | undefined;
return await this.tripsRepo.findForViewer(tripId, userId) as Trip | undefined;
}
/**
@@ -457,7 +428,7 @@ export class TripsService {
*/
/** TP21 — `TripsRepository.activeTrip`: the triple `CASE WHEN … relevance` projection and the double-`CASE WHEN` `ORDER BY`. */
async activeTrip(userId: number, today = appClock().date) {
return (await this.tripsRepo.activeTrip(userId, today)) as (ActiveTrip & { relevance: number }) | undefined;
return await this.tripsRepo.activeTrip(userId, today) as ActiveTrip & { relevance: number } | undefined;
}
/**
@@ -501,14 +472,8 @@ export class TripsService {
* rebase, the trip row (TP25), the leave entries that follow its dates and the rebuilt day grid
* are one transaction, so a failing `generateDays` or booking resync leaves all of them as they were.
*/
async updateTrip(
tripId: string | number,
userId: number,
data: UpdateTripData,
userRole: string,
rebase: CurrencyRebase | null = null,
): Promise<UpdateTripResult> {
const trip = (await this.tripsRepo.findRaw(tripId)) as (Trip & { reminder_days?: number }) | null; // TP24
async updateTrip(tripId: string | number, userId: number, data: UpdateTripData, userRole: string, rebase: CurrencyRebase | null = null): Promise<UpdateTripResult> {
const trip = await this.tripsRepo.findRaw(tripId) as (Trip & { reminder_days?: number }) | null; // TP24
if (!trip) throw new NotFoundError('Trip not found');
const { title, description, currency, is_archived, cover_image, reminder_days } = data;
@@ -520,12 +485,9 @@ export class TripsService {
const newArchived = is_archived !== undefined ? (is_archived ? 1 : 0) : trip.is_archived;
const newCover = cover_image !== undefined ? cover_image : trip.cover_image;
const oldReminder = (trip as any).reminder_days ?? 3;
const newReminder =
reminder_days !== undefined
? Number(reminder_days) >= 0 && Number(reminder_days) <= 30
? Number(reminder_days)
: oldReminder
: oldReminder;
const newReminder = reminder_days !== undefined
? (Number(reminder_days) >= 0 && Number(reminder_days) <= 30 ? Number(reminder_days) : oldReminder)
: oldReminder;
const tripIdNum = Number(tripId); // safe: `trip` above only resolved through the raw-bind seam on a real row (Task 6 review's own ruling on this exact conversion)
// The row, the leave entries that follow its dates and the rebuilt day grid are
@@ -535,8 +497,7 @@ export class TripsService {
await this.uow.transactional(async () => {
// Before the row: the rebase reads the outgoing currency off it (#1543).
if (rebase) await this.budget.applyCurrencyRebase(tripId, rebase);
await this.tripsRepo.updateTripRow(tripIdNum, {
// TP25
await this.tripsRepo.updateTripRow(tripIdNum, { // TP25
title: newTitle,
description: newDesc ?? null,
start_date: newStart || null,
@@ -553,24 +514,19 @@ export class TripsService {
});
if (trip.start_date && trip.end_date && newStart && newStart !== trip.start_date)
vacayId = await this.vacay.shiftOwnerEntriesForTripWindow(
trip.user_id,
trip.start_date,
trip.end_date,
newStart,
);
vacayId = await this.vacay.shiftOwnerEntriesForTripWindow(trip.user_id, trip.start_date, trip.end_date, newStart);
if (regenerate) {
// Accommodations have no absolute date columns, so their pre-change dates must be
// snapshotted before generateDays re-dates the day rows in place.
const prevDays = await this.daysRepo.listOrderedForReorder(tripIdNum); // TP26
const prevDateByDayId = new Map(prevDays.map((d) => [d.id, d.date]));
const prevDateByDayId = new Map(prevDays.map(d => [d.id, d.date]));
removedDays = (await this.generateDays(tripId, newStart || null, newEnd || null, dayCount)).removed;
if (data.date_shift_mode === 'shift_all') {
// Explicit "shift everything": bookings stay glued to their (re-dated) day rows,
// so re-stamp reservation_time to follow — same rules as reorderDays/insertDay.
const newDays = await this.daysRepo.listOrderedForReorder(tripIdNum); // TP27
const newDateByDayId = new Map(newDays.map((d) => [d.id, d.date]));
const newDateByDayId = new Map(newDays.map(d => [d.id, d.date]));
await this.days.restampReservationDates(tripIdNum, prevDateByDayId, newDateByDayId);
} else {
// Default: generateDays re-dates day rows positionally; re-anchor dated bookings to
@@ -705,8 +661,7 @@ export class TripsService {
const newTitle = title || src.title;
return await this.uow.transactional(async () => {
const newTripId = await this.tripsRepo.insertTripCopy({
// TP37
const newTripId = await this.tripsRepo.insertTripCopy({ // TP37
user_id: newOwnerId,
title: newTitle,
description: src.description,
@@ -720,13 +675,8 @@ export class TripsService {
const oldDays = await this.daysRepo.listByTrip(Number(sourceTripId)); // TP38
const dayMap = new Map<number, number>();
for (const d of oldDays) {
const newDayId = await this.daysRepo.insertDayCopy({
// TP39
trip_id: newTripId,
day_number: d.day_number,
date: d.date,
notes: d.notes,
title: d.title,
const newDayId = await this.daysRepo.insertDayCopy({ // TP39
trip_id: newTripId, day_number: d.day_number, date: d.date, notes: d.notes, title: d.title,
});
dayMap.set(d.id, newDayId);
}
@@ -734,35 +684,15 @@ export class TripsService {
const oldPlaces = await this.placesRepo.listAllForTrip(sourceTripId); // TP40
const placeMap = new Map<number, number>();
for (const p of oldPlaces) {
const newPlaceId = await this.placesRepo.insertPlaceCopy({
// TP41
trip_id: newTripId,
name: p.name,
description: p.description,
lat: p.lat,
lng: p.lng,
address: p.address,
category_id: p.category_id,
price: p.price,
currency: p.currency,
reservation_status: p.reservation_status,
reservation_notes: p.reservation_notes,
reservation_datetime: p.reservation_datetime,
place_time: p.place_time,
end_time: p.end_time,
duration_minutes: p.duration_minutes,
notes: p.notes,
image_url: p.image_url,
google_place_id: p.google_place_id,
google_ftid: p.google_ftid,
website: p.website,
phone: p.phone,
transport_mode: p.transport_mode,
osm_id: p.osm_id,
amap_poi_id: p.amap_poi_id,
route_geometry: p.route_geometry,
route_color: p.route_color,
stop_type: p.stop_type,
const newPlaceId = await this.placesRepo.insertPlaceCopy({ // TP41
trip_id: newTripId, name: p.name, description: p.description, lat: p.lat, lng: p.lng,
address: p.address, category_id: p.category_id, price: p.price, currency: p.currency,
reservation_status: p.reservation_status, reservation_notes: p.reservation_notes,
reservation_datetime: p.reservation_datetime, place_time: p.place_time, end_time: p.end_time,
duration_minutes: p.duration_minutes, notes: p.notes, image_url: p.image_url,
google_place_id: p.google_place_id, google_ftid: p.google_ftid, website: p.website, phone: p.phone,
transport_mode: p.transport_mode, osm_id: p.osm_id, amap_poi_id: p.amap_poi_id,
route_geometry: p.route_geometry, route_color: p.route_color, stop_type: p.stop_type,
fill_percent: p.fill_percent,
});
placeMap.set(p.id, newPlaceId);
@@ -778,13 +708,7 @@ export class TripsService {
for (const v of oldVias) {
const newDayId = dayMap.get(v.day_id);
if (newDayId) {
await this.roadtripViasRepo.insertVia({
day_id: newDayId,
after_order_index: v.after_order_index,
sequence: v.sequence,
lat: v.lat,
lng: v.lng,
}); // TP43
await this.roadtripViasRepo.insertVia({ day_id: newDayId, after_order_index: v.after_order_index, sequence: v.sequence, lat: v.lat, lng: v.lng }); // TP43
}
}
@@ -843,18 +767,11 @@ export class TripsService {
const newDayId = dayMap.get(a.day_id);
const newPlaceId = placeMap.get(a.place_id);
if (newDayId && newPlaceId) {
const newAssignmentId = await this.dayAssignmentsRepo.insertAssignmentCopy({
// TP49
day_id: newDayId,
place_id: newPlaceId,
order_index: a.order_index,
notes: a.notes,
reservation_status: a.reservation_status,
reservation_notes: a.reservation_notes,
reservation_datetime: a.reservation_datetime,
assignment_time: a.assignment_time,
assignment_end_time: a.assignment_end_time,
end_day: a.end_day ?? 0,
const newAssignmentId = await this.dayAssignmentsRepo.insertAssignmentCopy({ // TP49
day_id: newDayId, place_id: newPlaceId, order_index: a.order_index, notes: a.notes,
reservation_status: a.reservation_status, reservation_notes: a.reservation_notes,
reservation_datetime: a.reservation_datetime, assignment_time: a.assignment_time,
assignment_end_time: a.assignment_end_time, end_day: a.end_day ?? 0,
});
assignmentMap.set(a.id, newAssignmentId);
}
@@ -873,12 +790,7 @@ export class TripsService {
// new trip's `roadtrip_day_boundaries` starts empty, same reasoning
// as the via/track upserts above.
if (from && to !== undefined) {
await this.roadtripDayBoundariesRepo.upsertBoundary(newTripId, {
day_number: boundary.day_number,
from_assignment_id: from,
to_assignment_id: to,
fraction: boundary.fraction,
}); // TP52
await this.roadtripDayBoundariesRepo.upsertBoundary(newTripId, { day_number: boundary.day_number, from_assignment_id: from, to_assignment_id: to, fraction: boundary.fraction }); // TP52
}
}
@@ -896,17 +808,9 @@ export class TripsService {
const newStartDay = dayMap.get(a.start_day_id);
const newEndDay = dayMap.get(a.end_day_id);
if (newPlaceId && newStartDay && newEndDay) {
const newAccomId = await this.dayAccommodationsRepo.insertStay({
// TP56
trip_id: newTripId,
place_id: newPlaceId,
start_day_id: newStartDay,
end_day_id: newEndDay,
check_in: a.check_in,
check_in_end: a.check_in_end,
check_out: a.check_out,
confirmation: a.confirmation,
notes: a.notes,
const newAccomId = await this.dayAccommodationsRepo.insertStay({ // TP56
trip_id: newTripId, place_id: newPlaceId, start_day_id: newStartDay, end_day_id: newEndDay,
check_in: a.check_in, check_in_end: a.check_in_end, check_out: a.check_out, confirmation: a.confirmation, notes: a.notes,
});
accomMap.set(a.id, newAccomId);
}
@@ -943,22 +847,12 @@ export class TripsService {
// the NEW value is re-formatted to the legacy raw-bound `'<id>.0'`
// TEXT shape on the way back out — see `legacyBoundIntegerText`.
accommodation_id: newAccomId != null ? legacyBoundIntegerText(newAccomId) : null,
title: r.title,
reservation_time: r.reservation_time,
reservation_end_time: r.reservation_end_time,
location: r.location,
confirmation_number: r.confirmation_number,
notes: r.notes,
url: r.url,
status: r.status,
type: r.type,
title: r.title, reservation_time: r.reservation_time, reservation_end_time: r.reservation_end_time,
location: r.location, confirmation_number: r.confirmation_number, notes: r.notes, url: r.url, status: r.status, type: r.type,
// ingest_state travels with the copy: a staged booking must not turn
// 'live' just because the trip was duplicated, or it lands in the
// duplicate's public feed.
metadata: r.metadata,
day_plan_position: r.day_plan_position,
needs_review: r.needs_review ?? 0,
ingest_state: r.ingest_state ?? 'live',
metadata: r.metadata, day_plan_position: r.day_plan_position, needs_review: r.needs_review ?? 0, ingest_state: r.ingest_state ?? 'live',
}); // TP59
reservationMap.set(r.id, newReservationId);
}
@@ -976,7 +870,7 @@ export class TripsService {
for (const row of [...oldBudgetMembers, ...oldBudgetPayers]) {
if (row.user_id !== newOwnerId) sharedWithOthers.add(row.budget_item_id);
}
const hasSplitRows = new Set(oldBudgetMembers.map((bm) => bm.budget_item_id));
const hasSplitRows = new Set(oldBudgetMembers.map(bm => bm.budget_item_id));
// TP60 — Plan 3e Task 2, converted.
const oldBudget = await this.budgetItemsRepo.listAllForTrip(sourceTripId);
@@ -987,18 +881,11 @@ export class TripsService {
// split is its member count and goes with it; a typed one stays. A receipt
// split names who had which line, so it goes as well.
const newItemId = await this.budgetItemsRepo.insertCopy({
trip_id: newTripId,
category: b.category,
name: b.name,
total_price: b.total_price,
persons: planningOnly && hasSplitRows.has(b.id) ? null : b.persons,
days: b.days,
note: b.note,
sort_order: b.sort_order,
trip_id: newTripId, category: b.category, name: b.name, total_price: b.total_price,
persons: planningOnly && hasSplitRows.has(b.id) ? null : b.persons, days: b.days,
note: b.note, sort_order: b.sort_order,
reservation_id: b.reservation_id ? (reservationMap.get(b.reservation_id) ?? null) : null,
currency: b.currency,
exchange_rate: b.exchange_rate ?? 1,
expense_date: b.expense_date,
currency: b.currency, exchange_rate: b.exchange_rate ?? 1, expense_date: b.expense_date,
ticket_json: planningOnly ? null : b.ticket_json,
paid_by_user_id: b.paid_by_user_id === newOwnerId ? b.paid_by_user_id : null,
});
@@ -1009,25 +896,14 @@ export class TripsService {
if (sharedWithOthers.has(bm.budget_item_id)) continue;
const newItemId = budgetMap.get(bm.budget_item_id);
// TP63 — Plan 3e Task 2, converted.
if (newItemId)
await this.budgetItemMembersRepo.insertIgnore({
budget_item_id: newItemId,
user_id: bm.user_id,
paid: bm.paid ?? 0,
amount: bm.amount,
});
if (newItemId) await this.budgetItemMembersRepo.insertIgnore({ budget_item_id: newItemId, user_id: bm.user_id, paid: bm.paid ?? 0, amount: bm.amount });
}
for (const bp of oldBudgetPayers) {
if (sharedWithOthers.has(bp.budget_item_id)) continue;
const newItemId = budgetMap.get(bp.budget_item_id);
// TP65 — Plan 3e Task 2, converted.
if (newItemId)
await this.budgetItemPayersRepo.insertIgnore({
budget_item_id: newItemId,
user_id: bp.user_id,
amount: bp.amount ?? 0,
});
if (newItemId) await this.budgetItemPayersRepo.insertIgnore({ budget_item_id: newItemId, user_id: bp.user_id, amount: bp.amount ?? 0 });
}
// TP66 — Plan 3e Task 3, converted.
@@ -1036,11 +912,7 @@ export class TripsService {
for (const bag of oldBags) {
// TP67 — Plan 3e Task 3, converted (`insertBag`, PK42's own column set).
const newBagId = await this.packingBagsRepo.insertBag({
trip_id: newTripId,
name: bag.name,
color: bag.color,
sort_order: bag.sort_order,
weight_limit_grams: bag.weight_limit_grams,
trip_id: newTripId, name: bag.name, color: bag.color, sort_order: bag.sort_order, weight_limit_grams: bag.weight_limit_grams,
});
bagMap.set(bag.id, newBagId);
}
@@ -1057,14 +929,9 @@ export class TripsService {
const isPrivate = p.is_private ? 1 : 0;
// TP69 — Plan 3e Task 3, converted.
await this.packingItemsRepo.insertCopy({
trip_id: newTripId,
name: p.name,
category: p.category,
sort_order: p.sort_order,
weight_grams: p.weight_grams,
trip_id: newTripId, name: p.name, category: p.category, sort_order: p.sort_order, weight_grams: p.weight_grams,
bag_id: p.bag_id ? (bagMap.get(p.bag_id) ?? null) : null,
is_private: isPrivate,
owner_id: isPrivate ? newOwnerId : null,
is_private: isPrivate, owner_id: isPrivate ? newOwnerId : null,
});
}
@@ -1072,14 +939,8 @@ export class TripsService {
for (const n of oldNotes) {
const newDayId = dayMap.get(n.day_id);
if (newDayId) {
await this.dayNotesRepo.insertNoteCopy({
// TP71
day_id: newDayId,
trip_id: newTripId,
text: n.text,
time: n.time,
icon: n.icon,
sort_order: n.sort_order,
await this.dayNotesRepo.insertNoteCopy({ // TP71
day_id: newDayId, trip_id: newTripId, text: n.text, time: n.time, icon: n.icon, sort_order: n.sort_order,
});
}
}
@@ -1089,13 +950,8 @@ export class TripsService {
for (const t of oldTodos) {
// TP73 — Plan 3e Task 4, converted (`assigned_user_id` deliberately not carried over).
await this.todoItemsRepo.insertCopy({
trip_id: newTripId,
name: t.name,
category: t.category,
sort_order: t.sort_order,
due_date: t.due_date,
description: t.description,
priority: t.priority,
trip_id: newTripId, name: t.name, category: t.category, sort_order: t.sort_order,
due_date: t.due_date, description: t.description, priority: t.priority,
});
}
@@ -1114,6 +970,7 @@ export class TripsService {
async getCopiedTrip(newTripId: number, userId: number) {
return await this.tripsRepo.findForViewer(newTripId, userId);
}
}
// Defined in common/ so calendar and maps can raise them without importing the