feat(tours): add non-motorized day tour planning

This commit is contained in:
elmocito
2026-10-04 12:59:55 +02:00
parent c93cf2bce9
commit 9e475f447a
202 changed files with 16129 additions and 383 deletions
@@ -3,11 +3,44 @@ import {
assignmentEndDayRequestSchema,
assignmentMoveRequestSchema,
assignmentParticipantsRequestSchema,
assignmentSchema,
assignmentTransportRequestSchema,
} from './assignment.schema';
import { describe, it, expect } from 'vitest';
describe('assignment Tours read-model merge', () => {
const ordinary = { id: 1, day_id: 2, place_id: 3, order_index: 0, place: { id: 3, name: 'Place' } };
it('keeps ordinary and legacy track places non-Tours without a facet identity', () => {
expect(assignmentSchema.parse(ordinary).tour_place_id).toBeUndefined();
const legacy = assignmentSchema.parse({ ...ordinary, tour_place_id: null, tour_route_geometry: null });
expect(legacy.tour_place_id).toBeNull();
expect(legacy.tour_route_geometry).toBeNull();
});
it('preserves nullable Tour fields, exclusion and incoming/outgoing segment modes', () => {
const fields = {
tour_place_id: 3,
tour_route_geometry: '[[48,11],[49,12]]',
route_excluded: true,
leg_transport_mode: 'walking',
incoming_leg_transport_mode: 'cycling',
};
expect(assignmentSchema.parse({ ...ordinary, ...fields })).toMatchObject(fields);
expect(assignmentSchema.parse({ ...ordinary, tour_place_id: null, tour_route_geometry: null })).toMatchObject({
tour_place_id: null,
tour_route_geometry: null,
});
});
it('does not copy read-only joined Tours fields into create or move requests', () => {
const joined = { tour_place_id: 3, tour_route_geometry: '[[48,11],[49,12]]' };
expect(assignmentCreateRequestSchema.parse({ place_id: 3, ...joined })).toEqual({ place_id: 3 });
expect(assignmentMoveRequestSchema.parse({ new_day_id: 2, ...joined })).toEqual({ new_day_id: 2 });
});
});
it('requires a boolean for an explicit day end', () => {
expect(assignmentEndDayRequestSchema.parse({ end_day: true })).toEqual({ end_day: true });
expect(assignmentEndDayRequestSchema.parse({ end_day: false })).toEqual({ end_day: false });
@@ -55,6 +55,8 @@ export const assignmentSchema = z.object({
// this row and leaves it to road trip mode, which is the view that needs the
// hotel in the driving chain. Null for every stop a traveller placed.
accommodation_id: z.number().nullable().optional(),
tour_place_id: z.number().nullable().optional(),
tour_route_geometry: z.string().nullable().optional(),
participants: z.array(assignmentParticipantSchema).optional(),
created_at: z.string().optional(),
place: assignmentPlaceSchema,