Tours: copy a planned tour's import link in the repository, and teach the tests the new columns

This commit is contained in:
Maurice
2026-10-15 18:38:44 +02:00
parent f1c437b8b8
commit 11f23997e8
6 changed files with 48 additions and 8 deletions
+14 -3
View File
@@ -220,12 +220,23 @@ export class ToursRepository extends TrekRepository<Tours> {
/**
* TO8 — a copied tour keeps every column, `created_at` included, so the
* copy lists its tours in the source's order.
* copy lists its tours in the source's order. A tour planned from an import
* points at the copied import (`copiedPlaceIds` maps source to copy), never
* back into the source trip.
*/
async insertCopy(place_id: number, row: Omit<TourRow, 'place_id'>): Promise<void> {
async insertCopy(
place_id: number,
row: Omit<TourRow, 'place_id'>,
copiedPlaceIds: ReadonlyMap<number, number>,
): Promise<void> {
const derivedFrom = row.derived_from_place_id;
await this.db()
.insertInto('tours')
.values({ ...row, place_id })
.values({
...row,
place_id,
derived_from_place_id: derivedFrom === null ? null : (copiedPlaceIds.get(derivedFrom) ?? null),
})
.execute();
}
+1 -4
View File
@@ -743,10 +743,7 @@ export class TripsService {
for (const { place_id, ...tour } of oldTours) {
const newPlaceId = placeMap.get(place_id);
if (!newPlaceId) continue;
// A tour planned from an import points at the copied import, never back into the source trip.
const derivedFrom =
tour.derived_from_place_id === null ? null : (placeMap.get(tour.derived_from_place_id) ?? null);
await this.toursRepo.insertCopy(newPlaceId, { ...tour, derived_from_place_id: derivedFrom });
await this.toursRepo.insertCopy(newPlaceId, tour, placeMap);
copiedTours.add(place_id);
}
+2
View File
@@ -134,6 +134,8 @@ describe('GET /api/addons e2e (real auth guard + temp SQLite)', () => {
expect(res.body).toEqual({
collabFeatures: { chat: true, notes: true, links: true, polls: true, whatsnext: true },
bagTracking: true,
// Tours is off in this database, so wanderer is not offered either.
toursWanderer: false,
addons: [
{ id: 'packing', name: 'Packing', type: 'trip', icon: 'Backpack', enabled: true },
{ id: 'journey', name: 'Journey', type: 'global', icon: 'Compass', enabled: true },
@@ -89,6 +89,8 @@ describe('ToursRepository reads', () => {
max_hiking_difficulty: 5,
planned: 0,
has_waypoints: 1,
derived_from_place_id: null,
has_recorded_track: 0,
},
{
place_id: older.id,
@@ -108,6 +110,8 @@ describe('ToursRepository reads', () => {
max_hiking_difficulty: 2,
planned: 1,
has_waypoints: 0,
derived_from_place_id: null,
has_recorded_track: 0,
},
]);
expect((await repo.listForTrip(trip.id)).map((r) => r.place_id)).not.toContain(plain.id);
@@ -183,6 +187,8 @@ describe('ToursRepository writes', () => {
wanderer_ref: null,
planned: 0,
has_waypoints: 0,
derived_from_place_id: null,
has_recorded_track: 0,
});
});
@@ -241,13 +247,34 @@ describe('ToursRepository writes', () => {
match_confidence: null,
created_at: '2026-03-04 05:06:07',
max_hiking_difficulty: 4,
derived_from_place_id: null,
recorded_route_geometry: null,
},
]);
expect(await repo.listRowsForTrip(other.id)).toEqual([]);
const copy = createPlace(testDb, other.id);
const { place_id: _sourceId, ...columns } = rows[0];
await repo.insertCopy(copy.id, columns);
await repo.insertCopy(copy.id, columns, new Map());
expect(await repo.listRowsForTrip(other.id)).toEqual([{ ...rows[0], place_id: copy.id }]);
});
it('TOURREPO-009: insertCopy points a planned tour at the copied import, and drops a link the copy has no import for', async () => {
const { trip, other } = fixture();
const sourceImport = createPlace(testDb, trip.id);
const planned = createPlace(testDb, trip.id);
createTour(testDb, planned.id);
const [row] = await repo.listRowsForTrip(trip.id);
const { place_id: _sourceId, ...columns } = { ...row, derived_from_place_id: sourceImport.id };
const copiedImport = createPlace(testDb, other.id);
const copy = createPlace(testDb, other.id);
const orphan = createPlace(testDb, other.id);
await repo.insertCopy(copy.id, columns, new Map([[sourceImport.id, copiedImport.id]]));
await repo.insertCopy(orphan.id, columns, new Map());
const copied = await repo.listRowsForTrip(other.id);
expect(copied.find((r) => r.place_id === copy.id)?.derived_from_place_id).toBe(copiedImport.id);
expect(copied.find((r) => r.place_id === orphan.id)?.derived_from_place_id).toBeNull();
});
});
@@ -261,6 +261,7 @@ function tourRepositoryFixture(ownerTripId: number) {
tripId === ownerTripId && placeId === 42 ? row : undefined,
),
updateInTrip: vi.fn(async () => true),
keepRecordedTrack: vi.fn(async () => undefined),
};
const waypointsRepo = {
listForPlace: vi.fn(async () => input.waypoints),
@@ -89,6 +89,7 @@ function makeService() {
findInTrip: vi.fn(async (): Promise<typeof row | undefined> => row),
insertTour: inTx(undefined),
updateInTrip: inTx(true),
keepRecordedTrack: inTx(undefined),
};
const waypointsRepo = {
listForPlace: vi.fn(async () => request.waypoints),
@@ -147,6 +148,7 @@ describe('ToursService planner contracts (mock-only; SQLite rollback semantics a
expect(setup.toursRepo.insertTour).toHaveBeenCalledExactlyOnceWith({
place_id: 42,
tour_type: 'hike',
derived_from_place_id: null,
distance: expect.any(Number),
elevation_gain: 50,
elevation_loss: 20,