feat(accommodations): put a booked night on its check-in day

A road-trip stop is a day assignment: the rail, the server-side plan and the
map all build their stops from day_assignments and only look the stay up
afterwards, to hang check-in and check-out on one. A stay written without an
assignment was therefore invisible to every routing surface, so a hotel entered
in the day planner never reached the road-trip view and had to be entered a
second time as an ordinary place. The road-trip side has always written both
halves in one go; this is the day planner catching up, in the domain that
writes the stay.

Booking a night now also assigns its place to the check-in day, inside the
transaction that already writes the stay and its partner hotel reservation, and
types the place as a hotel stop when it carries no type of its own, so it reads
exactly like a night booked in the road trip. Only the check-in day gets a
stop; the later nights ride on the stay row. A place already planned for that
day gets no second stop and the booking claims none, so cancelling it cannot
delete a stop somebody placed by hand. Moving the booking moves its own stop,
deleting it takes its own stop back, and a trip re-date carries it along with
the stay. All four write surfaces (REST, plugin RPC and both MCP tools) run the
same write and announce the same events; the answers carry the stop too,
because the broadcast skips the socket that sent the request.

New nullable day_assignments.accommodation_id marks a stop as a booking's own.
It stays NULL for every existing row on purpose: everything planned before this
was planned by hand, and no backfill rewrites a finished trip. A stay booked
earlier picks up its stop the next time somebody saves it.

The booking form keeps writing stays on its own: that surface is gated on
reservation_edit and the day stop is day_edit work.
This commit is contained in:
Maurice
2026-09-13 01:13:25 +02:00
parent 3ab5ddaacc
commit f0e1da2d81
19 changed files with 628 additions and 83 deletions
+4 -3
View File
@@ -168,7 +168,10 @@ export function createMcpTestRegistry(): McpRegistry {
generalStorage,
);
const reservationsService = new ReservationsService(dbService, permissionsService, budgetService, realtimeService, notificationsStub(), new ReservationsReadRepository(dbService));
const accommodationsService = new AccommodationsService(dbService, permissionsService, realtimeService);
// One instance, four consumers: AssignmentsMcp, ReservationsMcp, PlacesMcp and
// AccommodationsService, which writes the day stop a booked night implies.
const assignmentsService = new AssignmentsService(dbService, permissionsService, realtimeService, queryHelpersService, journeyDomain);
const accommodationsService = new AccommodationsService(dbService, permissionsService, realtimeService, assignmentsService);
const membersService = new TripMembersService(dbService, budgetService, new UserCleanupService(dbService, budgetService), permissionsService, realtimeService, notificationsStub());
const tripsService = new TripsService(
dbService,
@@ -192,8 +195,6 @@ export function createMcpTestRegistry(): McpRegistry {
// one — against the same test DB, which is what makes the `when:` gates
// answer truthfully here instead of against the process-wide singleton.
const addonsService = new AddonsService(dbService);
// One instance, three consumers: AssignmentsMcp, ReservationsMcp and PlacesMcp.
const assignmentsService = new AssignmentsService(dbService, permissionsService, realtimeService, queryHelpersService, journeyDomain);
// The two photo providers, shared by MemoriesMcp (which browses them) and by
// the capture backfill JourneyMcp schedules after a provider photo is attached
// (which asks them when and where it was taken). Built for real rather than