mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
724c88c201
A mechanical prettier --write over server/src, server/tests, server/scripts and shared/src with the workspaces' .prettierrc; nothing but layout and import order changes, and every file keeps the same set of imports. shared is formatted throughout and its format baseline is empty. The server baseline keeps the 35 files that Prettier would grow past their lint:size limit (they need a split first, which is not a formatting change), and the generated entity files stay as their generator writes them.
1893 lines
85 KiB
TypeScript
1893 lines
85 KiB
TypeScript
/**
|
|
* Unit tests for AccommodationsService. DAY-SVC-016..026, 029 and 035 moved here
|
|
* 1:1 with the SQL they cover when accommodations became their own domain; the
|
|
* case ids are unchanged so the diff reads as a move. Real in-memory SQLite.
|
|
*/
|
|
import { db as testDb } from '../../../src/db/database';
|
|
import { AppSettings } from '../../../src/db/entities/AppSettings.entity';
|
|
import { AssignmentParticipants } from '../../../src/db/entities/AssignmentParticipants.entity';
|
|
import { BudgetItems } from '../../../src/db/entities/BudgetItems.entity';
|
|
import { DayAccommodations } from '../../../src/db/entities/DayAccommodations.entity';
|
|
import { DayAssignments } from '../../../src/db/entities/DayAssignments.entity';
|
|
import { Places } from '../../../src/db/entities/Places.entity';
|
|
import { Reservations } from '../../../src/db/entities/Reservations.entity';
|
|
import { RoadtripVias } from '../../../src/db/entities/RoadtripVias.entity';
|
|
import { AccommodationsDomainModule } from '../../../src/nest/accommodations/accommodations-domain.module';
|
|
import { AccommodationsController } from '../../../src/nest/accommodations/accommodations.controller';
|
|
import { AccommodationsModule } from '../../../src/nest/accommodations/accommodations.module';
|
|
import { AccommodationsService } from '../../../src/nest/accommodations/accommodations.service';
|
|
import { legacyBoundIntegerText } from '../../../src/nest/common/row-id';
|
|
import { PermissionsService } from '../../../src/nest/permissions/permissions.service';
|
|
import { makeAccommodationsService } from '../../helpers/accommodations-service';
|
|
import {
|
|
createUser,
|
|
createTrip,
|
|
createDay,
|
|
createPlace,
|
|
createDayAccommodation,
|
|
createDayAssignment,
|
|
addTripMember,
|
|
} from '../../helpers/factories';
|
|
import { countRows, deleteRows, findRow, findRows, insertRow, updateRows } from '../../helpers/factories/rows';
|
|
import { FakeRealtimeService } from '../../helpers/fake-realtime';
|
|
import { expectRegisteredProvider, expectRegisteredController } from '../../helpers/module-providers';
|
|
import { resetTestDb } from '../../helpers/test-db';
|
|
import { createTestUnitOfWork, createTestAppSettingsRepo, createTestReservationsRepo } from '../../helpers/test-uow';
|
|
import { sharedTestOrm } from '../../helpers/test-uow';
|
|
import type { FilterQuery } from '@mikro-orm/core';
|
|
|
|
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
|
|
|
|
vi.mock('../../../src/db/database', async () => {
|
|
const { createSnapshotTestDb } = await import('../../helpers/db-mock');
|
|
const db = createSnapshotTestDb();
|
|
return {
|
|
db,
|
|
closeDb: () => {},
|
|
reinitialize: () => {},
|
|
};
|
|
});
|
|
|
|
const realtime = new FakeRealtimeService();
|
|
const broadcast = realtime.broadcastMock;
|
|
|
|
const orm = () => sharedTestOrm(testDb);
|
|
|
|
/** The first booking linked to a stay, or null. */
|
|
async function linkedReservation(accommodationId: number) {
|
|
return findRow(await orm(), Reservations, { accommodation_id: legacyBoundIntegerText(accommodationId) });
|
|
}
|
|
|
|
/** Every booking linked to a stay. */
|
|
async function linkedReservations(accommodationId: number) {
|
|
return findRows(
|
|
await orm(),
|
|
Reservations,
|
|
{ accommodation_id: legacyBoundIntegerText(accommodationId) },
|
|
{ id: 'asc' },
|
|
);
|
|
}
|
|
|
|
/** How many bookings a stay holds. */
|
|
async function linkedCount(accommodationId: number): Promise<{ n: number }> {
|
|
return {
|
|
n: await countRows(await orm(), Reservations, { accommodation_id: legacyBoundIntegerText(accommodationId) }),
|
|
};
|
|
}
|
|
|
|
/** A second hotel booking on the same stay. */
|
|
async function addSecondBooking(tripId: number, accommodationId: number): Promise<number> {
|
|
return insertRow(await orm(), Reservations, {
|
|
trip: tripId,
|
|
type: 'hotel',
|
|
title: 'Second booking',
|
|
accommodation_id: legacyBoundIntegerText(accommodationId),
|
|
});
|
|
}
|
|
|
|
/** An expense, optionally on a booking. */
|
|
async function addExpense(
|
|
tripId: number,
|
|
name: string,
|
|
category: string,
|
|
totalPrice: number,
|
|
reservationId: number | null,
|
|
): Promise<number> {
|
|
return insertRow(await orm(), BudgetItems, {
|
|
trip: tripId,
|
|
name,
|
|
category,
|
|
total_price: totalPrice,
|
|
reservation: reservationId,
|
|
});
|
|
}
|
|
|
|
/** Sets columns on day assignments. */
|
|
async function setStops(
|
|
where: FilterQuery<DayAssignments>,
|
|
data: Partial<Pick<DayAssignments, 'assignment_time' | 'order_index' | 'notes' | 'end_day'>>,
|
|
) {
|
|
await updateRows(await orm(), DayAssignments, where, data);
|
|
}
|
|
|
|
// Named `svc` so the moved cases read exactly as they did on DaysService.
|
|
let svc: Awaited<ReturnType<typeof makeAccommodationsService>>;
|
|
beforeAll(async () => {
|
|
svc = await makeAccommodationsService(testDb, realtime);
|
|
});
|
|
|
|
beforeEach(() => {
|
|
resetTestDb(testDb);
|
|
});
|
|
|
|
afterAll(() => {
|
|
testDb.close();
|
|
});
|
|
|
|
describe('validateAccommodationRefs', () => {
|
|
it('DAY-SVC-016 — returns no errors when all refs are valid', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as any;
|
|
const errors = await svc.validateAccommodationRefs(trip.id, place.id, day.id, day.id);
|
|
expect(errors).toHaveLength(0);
|
|
});
|
|
|
|
it('DAY-SVC-017 — returns error when place does not exist in trip', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const errors = await svc.validateAccommodationRefs(trip.id, 99999, day.id, day.id);
|
|
expect(errors.some((e: any) => e.field === 'place_id')).toBe(true);
|
|
});
|
|
|
|
it('DAY-SVC-018 — returns error when start_day_id is invalid', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as any;
|
|
const errors = await svc.validateAccommodationRefs(trip.id, place.id, 99999, day.id);
|
|
expect(errors.some((e: any) => e.field === 'start_day_id')).toBe(true);
|
|
});
|
|
|
|
it('ACC-003 — reports every bad ref, end_day_id included', async () => {
|
|
// The controller only surfaces errors[0], so an unchecked end_day_id would let a
|
|
// stay point at a day from another trip and never be reported.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const errors = await svc.validateAccommodationRefs(trip.id, 99999, 99998, 99997);
|
|
expect(errors.map((e: any) => e.field)).toEqual(['place_id', 'start_day_id', 'end_day_id']);
|
|
});
|
|
|
|
it('ACC-004 — skips the refs the caller left undefined', async () => {
|
|
// The plugin update path (accommodations.rpc) passes only the fields it changes.
|
|
// If an absent ref were validated, every partial update would 403 "Place not found".
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
expect(await svc.validateAccommodationRefs(trip.id)).toHaveLength(0);
|
|
expect(await svc.validateAccommodationRefs(trip.id, undefined, undefined, undefined)).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('createAccommodation', () => {
|
|
it('DAY-SVC-019 — creates accommodation and returns it with place info', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Grand Hotel' }) as any;
|
|
|
|
const { accommodation: accom } = (await svc.createAccommodation(trip.id, {
|
|
place_id: place.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '15:00',
|
|
check_out: '11:00',
|
|
})) as any;
|
|
|
|
expect(accom).toBeDefined();
|
|
expect(accom.place_name).toBe('Grand Hotel');
|
|
});
|
|
|
|
it('DAY-SVC-020 — auto-creates a linked reservation', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'City Hotel' }) as any;
|
|
|
|
const { accommodation: accom } = (await svc.createAccommodation(trip.id, {
|
|
place_id: place.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
})) as any;
|
|
|
|
const reservation = await linkedReservation(accom.id);
|
|
expect(reservation).not.toBeNull();
|
|
expect(reservation.type).toBe('hotel');
|
|
expect(reservation.status).toBe('confirmed');
|
|
});
|
|
|
|
it('ACC-005 — titles the partner reservation "Hotel" when the stay has no place', async () => {
|
|
// day_accommodations.place_id is nullable (ON DELETE SET NULL) while
|
|
// reservations.title is NOT NULL: without the fallback the reservation insert
|
|
// fails and takes the whole transaction — and the stay — down with it.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
|
|
const { accommodation: accom } = (await svc.createAccommodation(trip.id, {
|
|
place_id: null as unknown as number,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
})) as any;
|
|
|
|
expect(accom.place_name).toBeNull();
|
|
const reservation = (await linkedReservation(accom.id))!;
|
|
expect(reservation.title).toBe('Hotel');
|
|
});
|
|
});
|
|
|
|
describe('getAccommodation', () => {
|
|
it('DAY-SVC-021 — returns accommodation for valid id and trip', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as any;
|
|
const accom = createDayAccommodation(testDb, trip.id, place.id, day.id, day.id) as any;
|
|
const found = (await svc.getAccommodation(accom.id, trip.id)) as any;
|
|
expect(found).toBeDefined();
|
|
expect(found.id).toBe(accom.id);
|
|
});
|
|
|
|
it('DAY-SVC-022 — returns undefined for non-existent accommodation', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
expect(await svc.getAccommodation(99999, trip.id)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('updateAccommodation', () => {
|
|
it('DAY-SVC-TX-001: a failing booking sync rolls the stay edit back with it', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' });
|
|
await svc.createAccommodation(trip.id, {
|
|
place_id: place.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '14:00',
|
|
});
|
|
const { id } = (await findRow(await orm(), DayAccommodations, { trip: trip.id }))!;
|
|
const existing = (await svc.getAccommodation(id, trip.id))!;
|
|
const reservations = await createTestReservationsRepo(testDb);
|
|
const spy = vi.spyOn(reservations, 'setMetadataAndConfirmation').mockRejectedValueOnce(new Error('boom'));
|
|
|
|
await expect(svc.updateAccommodation(id, existing, { check_in: '18:00' })).rejects.toThrow('boom');
|
|
|
|
expect((await findRow(await orm(), DayAccommodations, { id }))!.check_in).toBe('14:00');
|
|
spy.mockRestore();
|
|
});
|
|
|
|
it('DAY-SVC-023b — a linked booking with unreadable metadata no longer fails the update; it gets the new times', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as { id: number };
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as { id: number };
|
|
const { accommodation: accom } = (await svc.createAccommodation(trip.id, {
|
|
place_id: place.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
})) as { accommodation: { id: number } };
|
|
const before = await linkedReservation(accom.id);
|
|
await updateRows(await orm(), Reservations, { id: before.id }, { metadata: '{not json' });
|
|
|
|
const existing = await svc.getAccommodation(accom.id, trip.id);
|
|
if (!existing) throw new Error('accommodation not found');
|
|
await svc.updateAccommodation(accom.id, existing as Parameters<typeof svc.updateAccommodation>[1], {
|
|
check_in: '16:00',
|
|
});
|
|
|
|
expect(JSON.parse((await linkedReservation(accom.id)).metadata)).toEqual({ check_in_time: '16:00' });
|
|
});
|
|
|
|
it('DAY-SVC-023 — updates check-in and check-out times', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as any;
|
|
const { accommodation: accom } = (await svc.createAccommodation(trip.id, {
|
|
place_id: place.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
})) as any;
|
|
|
|
const existing = (await svc.getAccommodation(accom.id, trip.id))!;
|
|
const { accommodation: updated } = (await svc.updateAccommodation(accom.id, existing as any, {
|
|
check_in: '16:00',
|
|
check_out: '12:00',
|
|
})) as any;
|
|
expect(updated).toBeDefined();
|
|
|
|
// Verify linked reservation metadata was synced
|
|
const reservation = await linkedReservation(accom.id);
|
|
expect(reservation).not.toBeNull();
|
|
const meta = JSON.parse(reservation.metadata || '{}');
|
|
expect(meta.check_in_time).toBe('16:00');
|
|
expect(meta.check_out_time).toBe('12:00');
|
|
});
|
|
|
|
it('DAY-SVC-024 — preserves existing fields when not updated', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as any;
|
|
const { accommodation: accom } = (await svc.createAccommodation(trip.id, {
|
|
place_id: place.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
confirmation: 'ABC123',
|
|
})) as any;
|
|
|
|
const existing = (await svc.getAccommodation(accom.id, trip.id))!;
|
|
await svc.updateAccommodation(accom.id, existing as any, { check_in: '14:00' });
|
|
|
|
const row = (await svc.getAccommodation(accom.id, trip.id)) as any;
|
|
expect(row.confirmation).toBe('ABC123');
|
|
});
|
|
|
|
it('ACC-006 — updates a stay that has no linked reservation', async () => {
|
|
// Stays imported before the auto-reservation existed have no partner row. The sync
|
|
// block must be skipped for them, not throw on a missing reservation.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as any;
|
|
// The factory writes the stay row directly, without the partner reservation.
|
|
const accom = createDayAccommodation(testDb, trip.id, place.id, day.id, day.id) as any;
|
|
|
|
const existing = (await svc.getAccommodation(accom.id, trip.id))!;
|
|
const { accommodation: updated } = (await svc.updateAccommodation(accom.id, existing as any, {
|
|
check_in: '15:00',
|
|
})) as any;
|
|
|
|
expect(updated.check_in).toBe('15:00');
|
|
expect(await linkedCount(accom.id)).toMatchObject({ n: 0 });
|
|
});
|
|
|
|
it('ACC-007 — merges into the reservation metadata instead of replacing it', async () => {
|
|
// The stay is created with a check-in, so the reservation already carries metadata.
|
|
// An update that only moves the check-out must parse and extend that object —
|
|
// writing a fresh one would silently drop the check-in window.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as any;
|
|
const { accommodation: accom } = (await svc.createAccommodation(trip.id, {
|
|
place_id: place.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '15:00',
|
|
})) as any;
|
|
|
|
const existing = (await svc.getAccommodation(accom.id, trip.id))!;
|
|
await svc.updateAccommodation(accom.id, existing as any, { check_in_end: '20:00', check_out: '11:00' });
|
|
|
|
const reservation = (await linkedReservation(accom.id))!;
|
|
expect(JSON.parse(reservation.metadata)).toEqual({
|
|
check_in_time: '15:00',
|
|
check_in_end_time: '20:00',
|
|
check_out_time: '11:00',
|
|
});
|
|
});
|
|
|
|
it('ACC-008 — leaves the reservation confirmation number alone when the stay has none', async () => {
|
|
// COALESCE(?, confirmation_number): a stay without a confirmation must not null out
|
|
// a number that was typed on the reservation itself.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as any;
|
|
const { accommodation: accom } = (await svc.createAccommodation(trip.id, {
|
|
place_id: place.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
})) as any;
|
|
await updateRows(
|
|
await orm(),
|
|
Reservations,
|
|
{ accommodation_id: legacyBoundIntegerText(accom.id) },
|
|
{ confirmation_number: 'RES-9' },
|
|
);
|
|
|
|
const existing = (await svc.getAccommodation(accom.id, trip.id))!;
|
|
await svc.updateAccommodation(accom.id, existing as any, { check_in: '15:00' });
|
|
|
|
const reservation = (await linkedReservation(accom.id))!;
|
|
expect(reservation.confirmation_number).toBe('RES-9');
|
|
});
|
|
|
|
it("H1 (Plan 3h Task 7 review): a new confirmation on the stay OVERRIDES the linked reservation's existing, different confirmation — the new value wins, matching legacy COALESCE(?, confirmation_number)", async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as any;
|
|
const { accommodation: accom } = (await svc.createAccommodation(trip.id, {
|
|
place_id: place.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
})) as any;
|
|
// The linked reservation already carries a DIFFERENT, non-null confirmation —
|
|
// if the stay-side update used `coalesceParam`'s (existing-wins) direction
|
|
// instead of `coalesceOverride`'s (new-wins), it would keep this value.
|
|
await updateRows(
|
|
await orm(),
|
|
Reservations,
|
|
{ accommodation_id: legacyBoundIntegerText(accom.id) },
|
|
{ confirmation_number: 'RES-OLD' },
|
|
);
|
|
|
|
const existing = (await svc.getAccommodation(accom.id, trip.id))!;
|
|
await svc.updateAccommodation(accom.id, existing as any, { confirmation: 'ACC-CHANGED' });
|
|
|
|
const reservation = (await linkedReservation(accom.id))!;
|
|
expect(reservation.confirmation_number).toBe('ACC-CHANGED');
|
|
});
|
|
});
|
|
|
|
describe('deleteAccommodation', () => {
|
|
it('DAY-SVC-025 — deletes accommodation and its linked reservation', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as any;
|
|
const { accommodation: accom } = (await svc.createAccommodation(trip.id, {
|
|
place_id: place.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
})) as any;
|
|
|
|
const reservation = (await linkedReservation(accom.id))!;
|
|
|
|
const result = await svc.deleteAccommodation(accom.id);
|
|
expect(result.linkedReservationId).toBe(reservation.id);
|
|
|
|
// Accommodation is gone
|
|
expect(await svc.getAccommodation(accom.id, trip.id)).toBeUndefined();
|
|
|
|
// Reservation is gone
|
|
const deletedRes = await findRow(await orm(), Reservations, { id: reservation.id });
|
|
expect(deletedRes).toBeNull();
|
|
});
|
|
|
|
it('DAY-SVC-026 — returns null linkedReservationId when no reservation was linked', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as any;
|
|
const accom = createDayAccommodation(testDb, trip.id, place.id, day.id, day.id) as any;
|
|
|
|
// Remove the auto-created reservation so there's no linked one
|
|
await deleteRows(await orm(), Reservations, { accommodation_id: legacyBoundIntegerText(accom.id) });
|
|
|
|
const result = await svc.deleteAccommodation(accom.id);
|
|
expect(result.linkedReservationId).toBeNull();
|
|
});
|
|
|
|
it('ACC-009 — also deletes the budget item that hangs off the linked reservation', async () => {
|
|
// The cost row references the reservation, not the stay, so the delete has to
|
|
// cascade twice. Skipping the second hop strands a budget line whose reservation
|
|
// is gone, and the costs view can no longer reach it to remove it.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as any;
|
|
const { accommodation: accom } = (await svc.createAccommodation(trip.id, {
|
|
place_id: place.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
})) as any;
|
|
|
|
const reservation = (await linkedReservation(accom.id))!;
|
|
const budgetItemId = await addExpense(trip.id, 'Hotel stay', 'Accommodation', 240, reservation.id);
|
|
|
|
const result = await svc.deleteAccommodation(accom.id);
|
|
|
|
expect(result).toEqual({
|
|
linkedReservationId: reservation.id,
|
|
deletedBudgetItemId: budgetItemId,
|
|
linkedReservationIds: [reservation.id],
|
|
deletedBudgetItemIds: [budgetItemId],
|
|
mirror: {
|
|
created: null,
|
|
moved: null,
|
|
updated: [],
|
|
removed: [{ id: expect.any(Number), dayId: day.id }],
|
|
stamped: null,
|
|
},
|
|
});
|
|
expect(await findRow(await orm(), BudgetItems, { id: budgetItemId })).toBeNull();
|
|
});
|
|
|
|
it('ACC-035 — every expense on every linked booking goes with the stay (#2084)', async () => {
|
|
// A booking can carry several expenses now; taking only the first stranded the
|
|
// rest on a reservation id that no longer exists.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as any;
|
|
const { accommodation: accom } = (await svc.createAccommodation(trip.id, {
|
|
place_id: place.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
})) as any;
|
|
const first = (await linkedReservation(accom.id))!;
|
|
const second = await addSecondBooking(trip.id, accom.id);
|
|
const expense = (reservationId: number | null, name: string) =>
|
|
addExpense(trip.id, name, 'accommodation', 50, reservationId);
|
|
const room = await expense(first.id, 'Room');
|
|
const breakfast = await expense(first.id, 'Breakfast');
|
|
const parking = await expense(second, 'Parking');
|
|
const unrelated = await expense(null, 'Coffee');
|
|
|
|
const result = await svc.deleteAccommodation(accom.id);
|
|
|
|
expect(result.deletedBudgetItemIds).toEqual([room, breakfast, parking]);
|
|
expect(result.deletedBudgetItemId).toBe(room);
|
|
expect(
|
|
(await findRows(await orm(), BudgetItems, { trip: trip.id }, { id: 'asc' })).map((row) => ({ id: row.id })),
|
|
).toEqual([{ id: unrelated }]);
|
|
});
|
|
|
|
it('ACC-010 — a second booking pointed at the same stay goes too, instead of being orphaned', async () => {
|
|
// reservations.accommodation_id carries no foreign key and no unique constraint,
|
|
// and the booking form lets a hotel booking pick an existing stay. Deleting only
|
|
// the first left the second behind, referencing a stay that no longer exists.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as any;
|
|
const { accommodation: accom } = (await svc.createAccommodation(trip.id, {
|
|
place_id: place.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
})) as any;
|
|
|
|
const first = (await linkedReservation(accom.id))!;
|
|
const second = await addSecondBooking(trip.id, accom.id);
|
|
|
|
const result = await svc.deleteAccommodation(accom.id);
|
|
|
|
expect(result.linkedReservationIds).toEqual([first.id, second]);
|
|
expect(result.linkedReservationId).toBe(first.id);
|
|
expect(await linkedCount(accom.id)).toMatchObject({ n: 0 });
|
|
});
|
|
|
|
it('ACC-011 — updating the stay syncs the times onto every linked booking', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as any;
|
|
const { accommodation: accom } = (await svc.createAccommodation(trip.id, {
|
|
place_id: place.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
})) as any;
|
|
await addSecondBooking(trip.id, accom.id);
|
|
|
|
const existing = (await findRow(await orm(), DayAccommodations, { id: accom.id }))!;
|
|
await svc.updateAccommodation(accom.id, existing as Parameters<typeof svc.updateAccommodation>[1], {
|
|
check_in: '15:00',
|
|
check_out: '11:00',
|
|
});
|
|
|
|
const rows = await linkedReservations(accom.id);
|
|
expect(rows).toHaveLength(2);
|
|
for (const row of rows) {
|
|
expect(JSON.parse(row.metadata || '{}')).toMatchObject({ check_in_time: '15:00', check_out_time: '11:00' });
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('listAccommodations', () => {
|
|
it('DAY-SVC-029 — listAccommodations returns the hydrated stays', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const place = createPlace(testDb, trip.id, { name: 'Ryokan' });
|
|
createDayAccommodation(testDb, trip.id, place.id, day.id, day.id);
|
|
const rows = (await svc.listAccommodations(trip.id)) as { place_name: string }[];
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0].place_name).toBe('Ryokan');
|
|
});
|
|
});
|
|
|
|
describe('quirk fixes', () => {
|
|
it('DAY-SVC-035 — deleteAccommodation is atomic: a failed stay delete keeps the linked reservation', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' });
|
|
const { accommodation: accom } = (await svc.createAccommodation(trip.id, {
|
|
place_id: place.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
})) as { accommodation: { id: number } };
|
|
testDb.exec("CREATE TRIGGER boom BEFORE DELETE ON day_accommodations BEGIN SELECT RAISE(ABORT, 'boom'); END");
|
|
try {
|
|
await expect(svc.deleteAccommodation(accom.id)).rejects.toThrow();
|
|
// The earlier reservation delete inside the transaction rolled back.
|
|
expect(await linkedCount(accom.id)).toMatchObject({ n: 1 });
|
|
} finally {
|
|
testDb.exec('DROP TRIGGER boom');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('route-facing delegators', () => {
|
|
// list/validateRefs/get/create/update/remove are what the controller, the plugin RPC
|
|
// and trip-read-model call; the MCP tools call the long names. Both sets have to land
|
|
// on the same SQL, and the short ones get raw route params — strings where the long
|
|
// ones get numbers.
|
|
|
|
it('ACC-010 — list() returns what listAccommodations() returns, for a string and a number trip id', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Pension Alpen' }) as any;
|
|
createDayAccommodation(testDb, trip.id, place.id, day.id, day.id);
|
|
|
|
expect(await svc.list(String(trip.id))).toEqual(await svc.listAccommodations(trip.id));
|
|
expect(((await svc.list(trip.id)) as any[])[0].place_name).toBe('Pension Alpen');
|
|
});
|
|
|
|
it('ACC-011 — validateRefs() returns the same errors validateAccommodationRefs() does', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as any;
|
|
|
|
expect(await svc.validateRefs(trip.id, place.id, day.id, day.id)).toHaveLength(0);
|
|
// The controller 404s with errors[0].message, so the shape matters, not just the count.
|
|
expect(await svc.validateRefs(trip.id, 99999, day.id, day.id)).toEqual([
|
|
{ field: 'place_id', message: 'Place not found' },
|
|
]);
|
|
});
|
|
|
|
it('ACC-012 — create() writes the stay, get() reads it back trip-scoped', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const otherTrip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as any;
|
|
|
|
// Both ids arrive from the route as strings.
|
|
const { accommodation: created } = (await svc.create(String(trip.id), {
|
|
place_id: place.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
confirmation: 'XY-1',
|
|
})) as any;
|
|
|
|
expect(await svc.get(String(created.id), String(trip.id))).toMatchObject({ id: created.id, confirmation: 'XY-1' });
|
|
// Trip-scoped: a stay must not be readable through another trip's URL.
|
|
expect(await svc.get(created.id, otherTrip.id)).toBeUndefined();
|
|
});
|
|
|
|
it('ACC-013 — update() syncs the partner reservation even when the id is a string', async () => {
|
|
// updateAccommodation looks the reservation up with Number(id); if that coercion
|
|
// regresses, the REST path silently stops syncing check-in times.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as any;
|
|
const { accommodation: accom } = (await svc.create(trip.id, {
|
|
place_id: place.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
})) as any;
|
|
|
|
const existing = (await svc.get(accom.id, trip.id))!;
|
|
const { accommodation: updated } = (await svc.update(String(accom.id), existing as any, {
|
|
check_in: '16:00',
|
|
notes: 'late arrival',
|
|
})) as any;
|
|
|
|
expect(updated).toMatchObject({ check_in: '16:00', notes: 'late arrival' });
|
|
const reservation = (await linkedReservation(accom.id))!;
|
|
expect(JSON.parse(reservation.metadata).check_in_time).toBe('16:00');
|
|
});
|
|
|
|
it('ACC-014 — remove() reports the partner reservation it took with it', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id) as any;
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel' }) as any;
|
|
const { accommodation: accom } = (await svc.create(trip.id, {
|
|
place_id: place.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
})) as any;
|
|
const reservation = (await linkedReservation(accom.id))!;
|
|
|
|
// The controller broadcasts reservation:deleted off this return value.
|
|
expect(await svc.remove(String(accom.id))).toEqual({
|
|
linkedReservationId: reservation.id,
|
|
deletedBudgetItemId: null,
|
|
linkedReservationIds: [reservation.id],
|
|
deletedBudgetItemIds: [],
|
|
mirror: {
|
|
created: null,
|
|
moved: null,
|
|
updated: [],
|
|
removed: [{ id: expect.any(Number), dayId: day.id }],
|
|
stamped: null,
|
|
},
|
|
});
|
|
expect(await svc.get(accom.id, trip.id)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('trip access and edit permission', () => {
|
|
it('ACC-015 — verifyTripAccess resolves the trip from a string id and from a number', async () => {
|
|
// The REST path hands over the raw :tripId param, the MCP tools the parsed number.
|
|
// Without the Number() coercion one of the two callers stops finding its trip.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
|
|
expect(await svc.verifyTripAccess(String(trip.id), user.id)).toMatchObject({ id: trip.id, user_id: user.id });
|
|
expect(await svc.verifyTripAccess(trip.id, user.id)).toMatchObject({ id: trip.id, user_id: user.id });
|
|
});
|
|
|
|
it('ACC-016 — verifyTripAccess returns nothing for a user who is neither owner nor member', async () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: stranger } = createUser(testDb);
|
|
const trip = createTrip(testDb, owner.id);
|
|
|
|
expect(await svc.verifyTripAccess(trip.id, stranger.id)).toBeUndefined();
|
|
});
|
|
|
|
it('ACC-017 — canEdit lets the owner through on the default day_edit level', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const access = (await svc.verifyTripAccess(trip.id, user.id))!;
|
|
|
|
expect(await svc.canEdit(access, user as any)).toBe(true);
|
|
});
|
|
|
|
it('ACC-018 — canEdit lets a member through: it derives isMember from the trip owner id', async () => {
|
|
// day_edit defaults to trip_member. canEdit passes `trip.user_id !== user.id` as the
|
|
// isMember flag, so inverting that comparison would lock every invited traveller out
|
|
// of the day plan while the owner still edits fine.
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: member } = createUser(testDb);
|
|
const trip = createTrip(testDb, owner.id);
|
|
addTripMember(testDb, trip.id, member.id);
|
|
const access = (await svc.verifyTripAccess(trip.id, member.id))!;
|
|
|
|
expect(await svc.canEdit(access, member as any)).toBe(true);
|
|
});
|
|
|
|
it('ACC-019 — canEdit denies a member once day_edit is raised to trip_owner', async () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: member } = createUser(testDb);
|
|
const trip = createTrip(testDb, owner.id);
|
|
addTripMember(testDb, trip.id, member.id);
|
|
const permissions = new PermissionsService(
|
|
await createTestAppSettingsRepo(testDb),
|
|
await createTestUnitOfWork(testDb),
|
|
);
|
|
|
|
try {
|
|
await permissions.savePermissions({ day_edit: 'trip_owner' });
|
|
expect(await svc.canEdit((await svc.verifyTripAccess(trip.id, member.id))!, member as any)).toBe(false);
|
|
// The owner keeps the right, otherwise the setting would lock out the whole trip.
|
|
expect(await svc.canEdit((await svc.verifyTripAccess(trip.id, owner.id))!, owner as any)).toBe(true);
|
|
} finally {
|
|
// The permission cache is module-scoped and outlives resetTestDb — drop the row and
|
|
// the cache together, or every later case in this file inherits the raised level.
|
|
await deleteRows(await orm(), AppSettings, { key: 'perm_day_edit' });
|
|
await permissions.invalidatePermissionsCache();
|
|
}
|
|
});
|
|
|
|
it('ACC-020 — broadcast forwards the socket id so the originating client is not echoed', () => {
|
|
// The X-Socket-Id contract: the facade must pass the id straight through as the
|
|
// fourth argument. Dropping it makes the sender apply its own change twice.
|
|
broadcast.mockClear();
|
|
|
|
svc.broadcast('5', 'accommodation:deleted', { accommodationId: 9 }, 'sock-1');
|
|
expect(broadcast).toHaveBeenCalledWith('5', 'accommodation:deleted', { accommodationId: 9 }, 'sock-1');
|
|
|
|
svc.broadcast('5', 'accommodation:updated', { accommodation: { id: 9 } }, undefined);
|
|
expect(broadcast).toHaveBeenLastCalledWith('5', 'accommodation:updated', { accommodation: { id: 9 } }, undefined);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Booking a night also puts its place on the check-in day, because that stop is what
|
|
* the road trip routes and the map draws. Reported from Discord: a hotel entered in
|
|
* the day planner never reached the road-trip view, so the same place had to be
|
|
* entered a second time as an ordinary stop.
|
|
*/
|
|
describe('the day stop a booking implies', () => {
|
|
const stopsOn = async (dayId: number) =>
|
|
(await findRows(await orm(), DayAssignments, { day: dayId }, { order_index: 'asc' })).map((row) => ({
|
|
id: row.id,
|
|
place_id: row.place_id,
|
|
order_index: row.order_index,
|
|
accommodation_id: row.accommodation_id ?? null,
|
|
}));
|
|
|
|
const book = async (tripId: number, placeId: number | null, startDayId: number, endDayId: number) =>
|
|
(await svc.createAccommodation(tripId, {
|
|
place_id: placeId as number,
|
|
start_day_id: startDayId,
|
|
end_day_id: endDayId,
|
|
})) as any;
|
|
|
|
it('ACC-021 the booked night puts its place on the check-in day and marks the stop as its own', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel Adlon' });
|
|
|
|
const { accommodation, mirror } = await book(trip.id, place.id, day.id, day.id);
|
|
|
|
const stops = await stopsOn(day.id);
|
|
expect(stops).toHaveLength(1);
|
|
expect(stops[0]).toMatchObject({ place_id: place.id, accommodation_id: accommodation.id });
|
|
expect(mirror.created).toMatchObject({ id: stops[0].id, day_id: day.id, place_id: place.id });
|
|
// On the answer, not only in the table. This copy is what the client that booked
|
|
// the night puts in its store, and accommodation_id is the only thing telling the
|
|
// day list this row is the booking it already shows in the header. Without it the
|
|
// hotel appears a second time among the day's places until the next reload.
|
|
expect(mirror.created.accommodation_id).toBe(accommodation.id);
|
|
});
|
|
|
|
it('ACC-022 the stop lands first: the hotel is where the day is based', async () => {
|
|
// It used to go last, and a night booked for the morning sat behind a whole day
|
|
// of unpinned stops. The stops the traveller placed without an hour follow the
|
|
// hotel; only a clock of their own can put one ahead of it (ACC-022c).
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const museum = createPlace(testDb, trip.id, { name: 'Pergamon' });
|
|
const hotel = createPlace(testDb, trip.id, { name: 'Hotel Adlon' });
|
|
createDayAssignment(testDb, day.id, museum.id);
|
|
|
|
await book(trip.id, hotel.id, day.id, day.id);
|
|
|
|
expect((await stopsOn(day.id)).map((a) => a.place_id)).toEqual([hotel.id, museum.id]);
|
|
expect((await stopsOn(day.id)).map((a) => a.order_index)).toEqual([0, 1]);
|
|
});
|
|
|
|
it('ACC-022b a check-in puts the night before whatever is pinned to a later hour', async () => {
|
|
// A stop pinned to the afternoon cannot stand ahead of a check-in at eleven.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const afternoon = createPlace(testDb, trip.id, { name: 'Mercure' });
|
|
const hotel = createPlace(testDb, trip.id, { name: 'Billstedt' });
|
|
const pinned = createDayAssignment(testDb, day.id, afternoon.id);
|
|
await setStops({ id: pinned.id }, { assignment_time: '16:00' });
|
|
|
|
await svc.createAccommodation(trip.id, {
|
|
place_id: hotel.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '11:00',
|
|
});
|
|
|
|
expect((await stopsOn(day.id)).map((a) => a.place_id)).toEqual([hotel.id, afternoon.id]);
|
|
});
|
|
|
|
it('ACC-022c a stop pinned to an earlier hour stays ahead of the check-in', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const morning = createPlace(testDb, trip.id, { name: 'Museum' });
|
|
const hotel = createPlace(testDb, trip.id, { name: 'Billstedt' });
|
|
const pinned = createDayAssignment(testDb, day.id, morning.id);
|
|
await setStops({ id: pinned.id }, { assignment_time: '09:00' });
|
|
|
|
await svc.createAccommodation(trip.id, {
|
|
place_id: hotel.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '18:00',
|
|
});
|
|
|
|
expect((await stopsOn(day.id)).map((a) => a.place_id)).toEqual([morning.id, hotel.id]);
|
|
});
|
|
|
|
it('ACC-022d a stop without an hour of its own follows the night, whatever the check-in', async () => {
|
|
// A stop with no clock is planned from the hotel, not the other way round. This
|
|
// is the day from Discord: two unpinned stops and a night booked for ten in the
|
|
// morning, which the drive used to reach at a quarter past twelve.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const fuel = createPlace(testDb, trip.id, { name: 'Aral' });
|
|
const farm = createPlace(testDb, trip.id, { name: 'Karls Erdbeerhof' });
|
|
const hotel = createPlace(testDb, trip.id, { name: 'Rostock' });
|
|
createDayAssignment(testDb, day.id, fuel.id);
|
|
createDayAssignment(testDb, day.id, farm.id);
|
|
|
|
await svc.createAccommodation(trip.id, {
|
|
place_id: hotel.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '10:00',
|
|
});
|
|
|
|
expect((await stopsOn(day.id)).map((a) => a.place_id)).toEqual([hotel.id, fuel.id, farm.id]);
|
|
});
|
|
|
|
it('ACC-022f the night goes behind the last stop whose own hour is at or before the check-in', async () => {
|
|
// Pinned eight, unpinned, pinned two: a check-in at ten sits behind the eight and
|
|
// ahead of everything else, the unpinned stop included.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const [early, loose, late, hotel] = ['Aral', 'Hafen', 'Museum', 'Rostock'].map((name) =>
|
|
createPlace(testDb, trip.id, { name }),
|
|
);
|
|
const first = createDayAssignment(testDb, day.id, early.id);
|
|
createDayAssignment(testDb, day.id, loose.id);
|
|
const third = createDayAssignment(testDb, day.id, late.id);
|
|
await setStops({ id: first.id }, { assignment_time: '08:00' });
|
|
await setStops({ id: third.id }, { assignment_time: '14:00' });
|
|
|
|
await svc.createAccommodation(trip.id, {
|
|
place_id: hotel.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '10:00',
|
|
});
|
|
|
|
expect((await stopsOn(day.id)).map((a) => a.place_id)).toEqual([early.id, hotel.id, loose.id, late.id]);
|
|
});
|
|
|
|
it('ACC-022g a new check-in seats the night afresh, a change of notes leaves a dragged night alone', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const [harbour, market, hotel] = ['Hafen', 'Markt', 'Rostock'].map((name) =>
|
|
createPlace(testDb, trip.id, { name }),
|
|
);
|
|
createDayAssignment(testDb, day.id, harbour.id);
|
|
createDayAssignment(testDb, day.id, market.id);
|
|
const { accommodation } = (await svc.createAccommodation(trip.id, {
|
|
place_id: hotel.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
})) as any;
|
|
expect((await stopsOn(day.id)).map((a) => a.place_id)).toEqual([hotel.id, harbour.id, market.id]);
|
|
|
|
// Dragged to the end of the day by hand.
|
|
const own = (await stopsOn(day.id)).find((a) => a.place_id === hotel.id)!;
|
|
for (const later of await findRows(await orm(), DayAssignments, {
|
|
day: day.id,
|
|
order_index: { $gt: own.order_index },
|
|
})) {
|
|
await setStops({ id: later.id }, { order_index: later.order_index - 1 });
|
|
}
|
|
await setStops({ id: own.id }, { order_index: 2 });
|
|
expect((await stopsOn(day.id)).map((a) => a.place_id)).toEqual([harbour.id, market.id, hotel.id]);
|
|
|
|
let existing = (await svc.getAccommodation(accommodation.id, trip.id))!;
|
|
const quiet = (await svc.updateAccommodation(accommodation.id, existing, { notes: 'late arrival' })) as any;
|
|
expect(quiet.mirror.moved).toBeNull();
|
|
expect((await stopsOn(day.id)).map((a) => a.place_id)).toEqual([harbour.id, market.id, hotel.id]);
|
|
|
|
existing = (await svc.getAccommodation(accommodation.id, trip.id))!;
|
|
const reseated = (await svc.updateAccommodation(accommodation.id, existing, { check_in: '10:00' })) as any;
|
|
expect(reseated.mirror.moved).toMatchObject({ oldDayId: day.id, assignment: { id: own.id } });
|
|
expect((await stopsOn(day.id)).map((a) => a.place_id)).toEqual([hotel.id, harbour.id, market.id]);
|
|
});
|
|
|
|
it('ACC-022e moving the check-in later re-seats the night', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const afternoon = createPlace(testDb, trip.id, { name: 'Mercure' });
|
|
const hotel = createPlace(testDb, trip.id, { name: 'Billstedt' });
|
|
const pinned = createDayAssignment(testDb, day.id, afternoon.id);
|
|
await setStops({ id: pinned.id }, { assignment_time: '16:00' });
|
|
const { accommodation } = (await svc.createAccommodation(trip.id, {
|
|
place_id: hotel.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '11:00',
|
|
})) as any;
|
|
expect((await stopsOn(day.id)).map((a) => a.place_id)).toEqual([hotel.id, afternoon.id]);
|
|
|
|
const existing = (await svc.getAccommodation(accommodation.id, trip.id))!;
|
|
await svc.updateAccommodation(accommodation.id, existing, { check_in: '20:00' });
|
|
|
|
expect((await stopsOn(day.id)).map((a) => a.place_id)).toEqual([afternoon.id, hotel.id]);
|
|
});
|
|
|
|
it('ACC-022i a second night on the day counts by its check-in', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const [noon, morning] = ['Rue de Paris', 'Brandenburger Tor'].map((name) => createPlace(testDb, trip.id, { name }));
|
|
await svc.createAccommodation(trip.id, {
|
|
place_id: noon.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '12:00',
|
|
});
|
|
await svc.createAccommodation(trip.id, {
|
|
place_id: morning.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '10:00',
|
|
});
|
|
expect((await stopsOn(day.id)).map((a) => a.place_id)).toEqual([morning.id, noon.id]);
|
|
|
|
const evening = createPlace(testDb, trip.id, { name: 'Late' });
|
|
await svc.createAccommodation(trip.id, {
|
|
place_id: evening.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '18:00',
|
|
});
|
|
expect((await stopsOn(day.id)).map((a) => a.place_id)).toEqual([morning.id, noon.id, evening.id]);
|
|
});
|
|
|
|
it('ACC-022h an edit that leaves the check-in alone still corrects a night the clocks contradict', async () => {
|
|
// Dragged behind a stop pinned to the afternoon, with a check-in at ten: a change of
|
|
// notes puts it back ahead of that stop, because a night sitting after an afternoon it
|
|
// was booked before is the plan reading back wrong. Dragged behind a stop without an
|
|
// hour, the same edit leaves it where the traveller put it.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const [harbour, museum, hotel] = ['Hafen', 'Museum', 'Rostock'].map((name) =>
|
|
createPlace(testDb, trip.id, { name }),
|
|
);
|
|
createDayAssignment(testDb, day.id, harbour.id);
|
|
const pinned = createDayAssignment(testDb, day.id, museum.id);
|
|
await setStops({ id: pinned.id }, { assignment_time: '14:00' });
|
|
const { accommodation } = (await svc.createAccommodation(trip.id, {
|
|
place_id: hotel.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '10:00',
|
|
})) as any;
|
|
expect((await stopsOn(day.id)).map((a) => a.place_id)).toEqual([hotel.id, harbour.id, museum.id]);
|
|
const own = (await stopsOn(day.id)).find((a) => a.place_id === hotel.id)!;
|
|
|
|
// Behind the harbour only: allowed, and left alone.
|
|
await setStops({ day: day.id, place: harbour.id }, { order_index: 0 });
|
|
await setStops({ id: own.id }, { order_index: 1 });
|
|
let existing = (await svc.getAccommodation(accommodation.id, trip.id))!;
|
|
expect(
|
|
((await svc.updateAccommodation(accommodation.id, existing, { notes: 'late' })) as any).mirror.moved,
|
|
).toBeNull();
|
|
expect((await stopsOn(day.id)).map((a) => a.place_id)).toEqual([harbour.id, hotel.id, museum.id]);
|
|
|
|
// Behind the afternoon: contradicted by the clocks, and seated afresh, which is
|
|
// the front, ahead of the harbour that has no hour of its own.
|
|
await setStops({ day: day.id, place: museum.id }, { order_index: 1 });
|
|
await setStops({ id: own.id }, { order_index: 2 });
|
|
existing = (await svc.getAccommodation(accommodation.id, trip.id))!;
|
|
expect(
|
|
((await svc.updateAccommodation(accommodation.id, existing, { notes: 'later' })) as any).mirror.moved,
|
|
).not.toBeNull();
|
|
expect((await stopsOn(day.id)).map((a) => a.place_id)).toEqual([hotel.id, harbour.id, museum.id]);
|
|
});
|
|
|
|
it('ACC-022j two nights with the same check-in settle by their booking and stay settled through a change of notes', async () => {
|
|
// Each used to read the other as "at or before" its own check-in, so a notes edit on
|
|
// the front one moved it behind the other, took the road between them with it, and
|
|
// a notes edit on the other swapped them back.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const [first, second] = ['Adlon', 'Mercure'].map((name) => createPlace(testDb, trip.id, { name }));
|
|
const a = (await svc.createAccommodation(trip.id, {
|
|
place_id: first.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '15:00',
|
|
})) as any;
|
|
const b = (await svc.createAccommodation(trip.id, {
|
|
place_id: second.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '15:00',
|
|
})) as any;
|
|
expect((await stopsOn(day.id)).map((s) => s.place_id)).toEqual([first.id, second.id]);
|
|
const between = await insertRow(await orm(), RoadtripVias, {
|
|
day: day.id,
|
|
after_order_index: 0,
|
|
sequence: 0,
|
|
lat: 52.5,
|
|
lng: 13.4,
|
|
});
|
|
|
|
for (const booked of [a, b]) {
|
|
const existing = (await svc.getAccommodation(booked.accommodation.id, trip.id))!;
|
|
const quiet = (await svc.updateAccommodation(booked.accommodation.id, existing, {
|
|
notes: 'late arrival',
|
|
})) as any;
|
|
expect(quiet.mirror.moved).toBeNull();
|
|
expect(quiet.mirror.vias).toBeUndefined();
|
|
}
|
|
expect((await stopsOn(day.id)).map((s) => s.place_id)).toEqual([first.id, second.id]);
|
|
expect({
|
|
after_order_index: (await findRow(await orm(), RoadtripVias, { id: between }))!.after_order_index,
|
|
}).toEqual({ after_order_index: 0 });
|
|
|
|
// Dragged the other way round on purpose: an equal clock is no contradiction.
|
|
await setStops({ day: day.id, place: first.id }, { order_index: 1 });
|
|
await setStops({ day: day.id, place: { $ne: first.id } }, { order_index: 0 });
|
|
for (const booked of [a, b]) {
|
|
const existing = (await svc.getAccommodation(booked.accommodation.id, trip.id))!;
|
|
expect(
|
|
((await svc.updateAccommodation(booked.accommodation.id, existing, { notes: 'later' })) as any).mirror.moved,
|
|
).toBeNull();
|
|
}
|
|
expect((await stopsOn(day.id)).map((s) => s.place_id)).toEqual([second.id, first.id]);
|
|
});
|
|
|
|
it('ACC-022k two nights without a check-in: the earlier booking leads', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const [museum, first, second] = ['Pergamon', 'Adlon', 'Mercure'].map((name) =>
|
|
createPlace(testDb, trip.id, { name }),
|
|
);
|
|
createDayAssignment(testDb, day.id, museum.id);
|
|
await svc.createAccommodation(trip.id, { place_id: first.id, start_day_id: day.id, end_day_id: day.id });
|
|
const b = (await svc.createAccommodation(trip.id, {
|
|
place_id: second.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
})) as any;
|
|
|
|
expect((await stopsOn(day.id)).map((s) => s.place_id)).toEqual([first.id, second.id, museum.id]);
|
|
const existing = (await svc.getAccommodation(b.accommodation.id, trip.id))!;
|
|
expect(
|
|
((await svc.updateAccommodation(b.accommodation.id, existing, { notes: 'late' })) as any).mirror.moved,
|
|
).toBeNull();
|
|
expect((await stopsOn(day.id)).map((s) => s.place_id)).toEqual([first.id, second.id, museum.id]);
|
|
});
|
|
|
|
it('ACC-022l a night on a day dragged out of clock order is settled where a fresh seat puts it', async () => {
|
|
// Pinned two, then pinned eight, on purpose. A check-in at ten goes behind the eight,
|
|
// and nothing else satisfies both clocks; a change of notes used to relocate it to
|
|
// the very same seat and report a move for it.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const [late, early, hotel] = ['Museum', 'Aral', 'Rostock'].map((name) => createPlace(testDb, trip.id, { name }));
|
|
await setStops({ id: createDayAssignment(testDb, day.id, late.id).id }, { assignment_time: '14:00' });
|
|
await setStops({ id: createDayAssignment(testDb, day.id, early.id).id }, { assignment_time: '08:00' });
|
|
const { accommodation } = (await svc.createAccommodation(trip.id, {
|
|
place_id: hotel.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '10:00',
|
|
})) as any;
|
|
expect((await stopsOn(day.id)).map((s) => s.place_id)).toEqual([late.id, early.id, hotel.id]);
|
|
|
|
const existing = (await svc.getAccommodation(accommodation.id, trip.id))!;
|
|
const quiet = (await svc.updateAccommodation(accommodation.id, existing, { notes: 'late arrival' })) as any;
|
|
|
|
expect(quiet.mirror.moved).toBeNull();
|
|
expect((await stopsOn(day.id)).map((s) => s.place_id)).toEqual([late.id, early.id, hotel.id]);
|
|
});
|
|
|
|
it('ACC-023 the place is typed as lodging, so the rail draws it as a service stop', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel Adlon' });
|
|
|
|
const { mirror } = await book(trip.id, place.id, day.id, day.id);
|
|
|
|
expect(await findRow(await orm(), Places, { id: place.id })).toMatchObject({ stop_type: 'hotel' });
|
|
// Announced, or the places list keeps the stale null until a reload.
|
|
expect(mirror.stamped).toMatchObject({ id: place.id, stop_type: 'hotel' });
|
|
// And the stop carries it, so the rail does not number it.
|
|
expect(mirror.created.place.stop_type).toBe('hotel');
|
|
});
|
|
|
|
it('ACC-024 a stop type the traveller picked is never overwritten', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const place = createPlace(testDb, trip.id, { name: 'Camping Isar' });
|
|
await updateRows(await orm(), Places, { id: place.id }, { stop_type: 'campsite' });
|
|
|
|
const { mirror } = await book(trip.id, place.id, day.id, day.id);
|
|
|
|
expect(await findRow(await orm(), Places, { id: place.id })).toMatchObject({ stop_type: 'campsite' });
|
|
expect(mirror.stamped).toBeNull();
|
|
});
|
|
|
|
it('ACC-025 a place already planned for that day gets no second stop, and the booking claims neither', async () => {
|
|
// This is the road-trip flow: it assigns the place to the day and only then books
|
|
// the night. A second stop would draw the hotel twice, and claiming the existing
|
|
// one would let cancelling the booking delete a stop the traveller placed.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel Adlon' });
|
|
const own = createDayAssignment(testDb, day.id, place.id);
|
|
|
|
const { mirror } = await book(trip.id, place.id, day.id, day.id);
|
|
|
|
expect(await stopsOn(day.id)).toEqual([expect.objectContaining({ id: own.id, accommodation_id: null })]);
|
|
expect(mirror.created).toBeNull();
|
|
});
|
|
|
|
it('ACC-026 only the check-in day, not every night of a long stay', async () => {
|
|
// You drive there once. The later nights ride on the stay row, which is where the
|
|
// rail reads check-out from.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const arrive = createDay(testDb, trip.id);
|
|
const middle = createDay(testDb, trip.id);
|
|
const leave = createDay(testDb, trip.id);
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel Adlon' });
|
|
|
|
await book(trip.id, place.id, arrive.id, leave.id);
|
|
|
|
expect(await stopsOn(arrive.id)).toHaveLength(1);
|
|
expect(await stopsOn(middle.id)).toHaveLength(0);
|
|
expect(await stopsOn(leave.id)).toHaveLength(0);
|
|
});
|
|
|
|
it('ACC-027 a stay with no place writes no stop instead of throwing', async () => {
|
|
// day_accommodations.place_id is nullable (ON DELETE SET NULL) and the booking form
|
|
// writes stays that never had one.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
|
|
const { mirror } = await book(trip.id, null, day.id, day.id);
|
|
|
|
expect(mirror).toEqual({ created: null, moved: null, updated: [], removed: [], stamped: null });
|
|
expect(await stopsOn(day.id)).toHaveLength(0);
|
|
});
|
|
|
|
it('ACC-028 moving the booking to another day moves its stop with it', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day1 = createDay(testDb, trip.id);
|
|
const day2 = createDay(testDb, trip.id);
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel Adlon' });
|
|
const { accommodation } = await book(trip.id, place.id, day1.id, day1.id);
|
|
const before = (await stopsOn(day1.id))[0];
|
|
|
|
const existing = (await svc.getAccommodation(accommodation.id, trip.id))!;
|
|
const { mirror } = (await svc.updateAccommodation(accommodation.id, existing, {
|
|
start_day_id: day2.id,
|
|
end_day_id: day2.id,
|
|
})) as any;
|
|
|
|
expect(await stopsOn(day1.id)).toHaveLength(0);
|
|
// The same row, carried over. Not a delete and a fresh insert: everything the
|
|
// traveller hung on this stop is keyed by its id.
|
|
expect(await stopsOn(day2.id)).toEqual([
|
|
expect.objectContaining({ id: before.id, place_id: place.id, accommodation_id: accommodation.id }),
|
|
]);
|
|
expect(mirror.removed).toEqual([]);
|
|
expect(mirror.created).toBeNull();
|
|
expect(mirror.moved).toMatchObject({ oldDayId: day1.id, assignment: { id: before.id, day_id: day2.id } });
|
|
});
|
|
|
|
it('ACC-028b moving the booking keeps the note, the hour and the end-of-day flag on the stop', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day1 = createDay(testDb, trip.id);
|
|
const day2 = createDay(testDb, trip.id);
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel Adlon' });
|
|
const { accommodation } = await book(trip.id, place.id, day1.id, day1.id);
|
|
const stop = (await stopsOn(day1.id))[0];
|
|
await setStops({ id: stop.id }, { notes: 'ask for the quiet side', assignment_time: '15:30', end_day: 1 });
|
|
|
|
const existing = (await svc.getAccommodation(accommodation.id, trip.id))!;
|
|
await svc.updateAccommodation(accommodation.id, existing, { start_day_id: day2.id, end_day_id: day2.id });
|
|
|
|
const after = (await findRow(await orm(), DayAssignments, { id: stop.id }))!;
|
|
expect(after).toMatchObject({
|
|
day_id: day2.id,
|
|
notes: 'ask for the quiet side',
|
|
assignment_time: '15:30',
|
|
end_day: 1,
|
|
});
|
|
});
|
|
|
|
it('ACC-028c moving the booking keeps the people assigned to the stop', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day1 = createDay(testDb, trip.id);
|
|
const day2 = createDay(testDb, trip.id);
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel Adlon' });
|
|
const { accommodation } = await book(trip.id, place.id, day1.id, day1.id);
|
|
const stop = (await stopsOn(day1.id))[0];
|
|
// assignment_participants.assignment_id is ON DELETE CASCADE, so a delete and
|
|
// re-insert would take these with it without a word.
|
|
await insertRow(await orm(), AssignmentParticipants, { assignment: stop.id, user: user.id });
|
|
|
|
const existing = (await svc.getAccommodation(accommodation.id, trip.id))!;
|
|
await svc.updateAccommodation(accommodation.id, existing, { start_day_id: day2.id, end_day_id: day2.id });
|
|
|
|
const kept = (await findRows(await orm(), AssignmentParticipants, { assignment: stop.id })).map((row) => ({
|
|
user_id: row.user_id,
|
|
}));
|
|
expect(kept).toEqual([{ user_id: user.id }]);
|
|
});
|
|
|
|
it('ACC-028d a move onto a day that already holds the place by hand drops the booking stop instead of doubling it', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day1 = createDay(testDb, trip.id);
|
|
const day2 = createDay(testDb, trip.id);
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel Adlon' });
|
|
const { accommodation } = await book(trip.id, place.id, day1.id, day1.id);
|
|
const own = (await stopsOn(day1.id))[0];
|
|
const byHand = createDayAssignment(testDb, day2.id, place.id) as { id: number };
|
|
|
|
const existing = (await svc.getAccommodation(accommodation.id, trip.id))!;
|
|
const { mirror } = (await svc.updateAccommodation(accommodation.id, existing, {
|
|
start_day_id: day2.id,
|
|
end_day_id: day2.id,
|
|
})) as any;
|
|
|
|
expect(await stopsOn(day1.id)).toHaveLength(0);
|
|
expect((await stopsOn(day2.id)).map((row: { id: number }) => row.id)).toEqual([byHand.id]);
|
|
expect(mirror.removed).toEqual([{ id: own.id, dayId: day1.id }]);
|
|
expect(mirror.moved).toBeNull();
|
|
});
|
|
|
|
it('ACC-028e the day the stop left closes the gap it made in the order', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day1 = createDay(testDb, trip.id);
|
|
const day2 = createDay(testDb, trip.id);
|
|
const hotel = createPlace(testDb, trip.id, { name: 'Hotel Adlon' });
|
|
const museum = createPlace(testDb, trip.id, { name: 'Pergamon' });
|
|
const { accommodation } = await book(trip.id, hotel.id, day1.id, day1.id);
|
|
createDayAssignment(testDb, day1.id, museum.id);
|
|
|
|
const existing = (await svc.getAccommodation(accommodation.id, trip.id))!;
|
|
await svc.updateAccommodation(accommodation.id, existing, { start_day_id: day2.id, end_day_id: day2.id });
|
|
|
|
expect((await stopsOn(day1.id)).map((row: { order_index: number }) => row.order_index)).toEqual([0]);
|
|
});
|
|
|
|
it('ACC-029 editing an unrelated field leaves the stop exactly where it is', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel Adlon' });
|
|
const { accommodation } = await book(trip.id, place.id, day.id, day.id);
|
|
const before = await stopsOn(day.id);
|
|
|
|
const existing = (await svc.getAccommodation(accommodation.id, trip.id))!;
|
|
const { mirror } = (await svc.updateAccommodation(accommodation.id, existing, { check_in: '16:00' })) as any;
|
|
|
|
expect(await stopsOn(day.id)).toEqual(before);
|
|
expect(mirror).toEqual({ created: null, moved: null, updated: [], removed: [], stamped: null });
|
|
});
|
|
|
|
it('ACC-029b a night sitting behind a later afternoon is re-seated, keeping its row', async () => {
|
|
// The state the migration backfill leaves: every pre-existing stay's stop was
|
|
// appended last, whatever its check-in says. Giving the booking an hour has to
|
|
// move it ahead of the evening it was booked around.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const dinner = createPlace(testDb, trip.id, { name: 'Osteria' });
|
|
const hotel = createPlace(testDb, trip.id, { name: 'Hotel Adlon' });
|
|
await updateRows(await orm(), Places, { id: dinner.id }, { place_time: '19:00' });
|
|
createDayAssignment(testDb, day.id, dinner.id);
|
|
const { accommodation } = await book(trip.id, hotel.id, day.id, day.id);
|
|
const stop = (await stopsOn(day.id)).find((row: { place_id: number }) => row.place_id === hotel.id)!;
|
|
// The backfill's placement, written by hand: the night behind the dinner.
|
|
await setStops({ day: day.id, place: dinner.id }, { order_index: 0 });
|
|
await setStops({ id: stop.id }, { order_index: 1 });
|
|
expect((await stopsOn(day.id)).map((row: { place_id: number }) => row.place_id)).toEqual([dinner.id, hotel.id]);
|
|
|
|
const existing = (await svc.getAccommodation(accommodation.id, trip.id))!;
|
|
const { mirror } = (await svc.updateAccommodation(accommodation.id, existing, { check_in: '15:00' })) as any;
|
|
|
|
expect((await stopsOn(day.id)).map((row: { place_id: number }) => row.place_id)).toEqual([hotel.id, dinner.id]);
|
|
// Re-seated, not rebuilt: the same row, one place further forward.
|
|
expect(mirror.moved).toMatchObject({ oldDayId: day.id, assignment: { id: stop.id } });
|
|
expect(mirror.removed).toEqual([]);
|
|
});
|
|
|
|
it('ACC-030 a stay whose stop belongs to the traveller does not get a second one', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const second = createDay(testDb, trip.id);
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel Adlon' });
|
|
// The road trip books a night on a place it put on the day first, so the stop is
|
|
// the traveller's and the booking owns nothing.
|
|
createDayAssignment(testDb, day.id, place.id);
|
|
const stay = createDayAccommodation(testDb, trip.id, place.id, day.id, day.id) as any;
|
|
|
|
const existing = (await svc.getAccommodation(stay.id, trip.id))!;
|
|
await svc.updateAccommodation(stay.id, existing, { start_day_id: second.id, end_day_id: second.id });
|
|
|
|
expect(await stopsOn(second.id)).toEqual([]);
|
|
expect(await stopsOn(day.id)).toEqual([expect.objectContaining({ place_id: place.id, accommodation_id: null })]);
|
|
});
|
|
|
|
it('ACC-031 cancelling a booking takes back its own stop and leaves the other one', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const hotel = createPlace(testDb, trip.id, { name: 'Hotel Adlon' });
|
|
const museum = createPlace(testDb, trip.id, { name: 'Pergamon' });
|
|
const own = createDayAssignment(testDb, day.id, museum.id);
|
|
const { accommodation } = await book(trip.id, hotel.id, day.id, day.id);
|
|
const mirrored = (await stopsOn(day.id)).find((a) => a.place_id === hotel.id)!;
|
|
|
|
const { mirror } = await svc.deleteAccommodation(accommodation.id);
|
|
|
|
expect(await stopsOn(day.id)).toEqual([expect.objectContaining({ id: own.id })]);
|
|
expect(mirror.removed).toEqual([{ id: mirrored.id, dayId: day.id }]);
|
|
});
|
|
|
|
it('ACC-032 cancelling a night booked in the road trip keeps the stop standing', async () => {
|
|
// The rail turns a night back into a pause by deleting the stay alone; the stop is
|
|
// the thing being edited there and has to survive.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel Adlon' });
|
|
const own = createDayAssignment(testDb, day.id, place.id);
|
|
const { accommodation } = await book(trip.id, place.id, day.id, day.id);
|
|
|
|
const { mirror } = await svc.deleteAccommodation(accommodation.id);
|
|
|
|
expect(await stopsOn(day.id)).toEqual([expect.objectContaining({ id: own.id })]);
|
|
expect(mirror.removed).toEqual([]);
|
|
});
|
|
|
|
it('ACC-032b keepStop hands the mirrored stop to the traveller instead of taking it away', async () => {
|
|
// The road trip popup switching a night back to an ordinary pause: the booking is
|
|
// what was cancelled, not the place, and the stop sits mid-drive where re-adding it
|
|
// would land it at the end of the day instead.
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel Adlon' });
|
|
const { accommodation } = await book(trip.id, place.id, day.id, day.id);
|
|
const mirrored = (await stopsOn(day.id))[0];
|
|
|
|
const { mirror } = await svc.deleteAccommodation(accommodation.id, { keepStop: true });
|
|
|
|
// Still there, and now unmarked: nothing may move or delete it on the next edit.
|
|
expect(await stopsOn(day.id)).toEqual([
|
|
expect.objectContaining({ id: mirrored.id, place_id: place.id, accommodation_id: null }),
|
|
]);
|
|
// Nothing to announce either — no session has to take a stop off its day plan.
|
|
expect(mirror.removed).toEqual([]);
|
|
expect(await findRow(await orm(), DayAccommodations, { id: accommodation.id })).toBeNull();
|
|
});
|
|
|
|
it('ACC-032c remove() forwards keepStop, which is the only door the REST route uses', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel Adlon' });
|
|
const { accommodation } = await book(trip.id, place.id, day.id, day.id);
|
|
|
|
await svc.remove(accommodation.id, { keepStop: true });
|
|
|
|
expect(await stopsOn(day.id)).toEqual([expect.objectContaining({ place_id: place.id, accommodation_id: null })]);
|
|
});
|
|
|
|
it('ACC-033 announceMirror sends the removal before the arrival', async () => {
|
|
// Order matters on a move: the day plan would briefly hold the place twice if the
|
|
// arrival went first.
|
|
const sent: string[] = [];
|
|
await svc.announceMirror(
|
|
5,
|
|
{
|
|
created: { id: 78, day_id: 11 } as never,
|
|
moved: null,
|
|
updated: [],
|
|
removed: [{ id: 77, dayId: 10 }],
|
|
stamped: { id: 3 } as never,
|
|
},
|
|
(event) => {
|
|
sent.push(event);
|
|
},
|
|
);
|
|
// The two days whose order changed follow, arrival first (see touchedDays).
|
|
expect(sent).toEqual([
|
|
'assignment:deleted',
|
|
'assignment:created',
|
|
'place:updated',
|
|
'assignment:reordered',
|
|
'assignment:reordered',
|
|
]);
|
|
});
|
|
|
|
it('ACC-033b announceMirror reports a move as one event, not a delete and a create', async () => {
|
|
const sent: Array<{ event: string; payload: unknown }> = [];
|
|
await svc.announceMirror(
|
|
5,
|
|
{
|
|
created: null,
|
|
moved: { assignment: { id: 78, day_id: 11 } as never, oldDayId: 10 },
|
|
updated: [],
|
|
removed: [],
|
|
stamped: null,
|
|
},
|
|
(event, payload) => {
|
|
sent.push({ event, payload });
|
|
},
|
|
);
|
|
expect(sent.map((e) => e.event)).toEqual(['assignment:moved', 'assignment:reordered', 'assignment:reordered']);
|
|
expect(sent[0].payload).toMatchObject({ oldDayId: 10, newDayId: 11 });
|
|
});
|
|
|
|
it('ACC-033c a kept stop is announced as updated, so the day list stops hiding it', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const place = createPlace(testDb, trip.id, { name: 'Hotel Adlon' });
|
|
const { accommodation } = await book(trip.id, place.id, day.id, day.id);
|
|
const stop = (await stopsOn(day.id))[0];
|
|
|
|
const { mirror } = (await svc.remove(accommodation.id, { keepStop: true })) as any;
|
|
|
|
// Days hides a stop that carries an accommodation_id, so a client still holding
|
|
// the old row keeps the place invisible on a day it is standing on.
|
|
expect(mirror.updated).toEqual([expect.objectContaining({ id: stop.id, accommodation_id: null })]);
|
|
const sent: string[] = [];
|
|
await svc.announceMirror(trip.id, mirror, (event) => {
|
|
sent.push(event);
|
|
});
|
|
expect(sent).toContain('assignment:updated');
|
|
});
|
|
});
|
|
|
|
describe('the drawn roads a booking moves', () => {
|
|
// A via is pinned to a POSITION among the day's located stops, not to a row. A
|
|
// night seated by its check-in ahead of the afternoon renumbers every stop behind
|
|
// it, and every road drawn behind those stops has to move with them, or the
|
|
// detour is driven on the wrong leg for everyone.
|
|
const stopsOn = async (dayId: number) =>
|
|
(await findRows(await orm(), DayAssignments, { day: dayId }, { order_index: 'asc' })).map((row) => row.place_id);
|
|
const addVia = async (dayId: number, afterOrderIndex: number): Promise<number> =>
|
|
insertRow(await orm(), RoadtripVias, {
|
|
day: dayId,
|
|
after_order_index: afterOrderIndex,
|
|
sequence: 0,
|
|
lat: 48.1,
|
|
lng: 11.5,
|
|
});
|
|
const viaAnchors = async (dayId: number) =>
|
|
(await findRows(await orm(), RoadtripVias, { day: dayId }, { id: 'asc' })).map((row) => ({
|
|
id: row.id,
|
|
after_order_index: row.after_order_index,
|
|
}));
|
|
/** The day's roads as the route reads them: leg by leg, and in order within a leg. */
|
|
const legOrder = async (dayId: number) =>
|
|
(
|
|
await findRows(
|
|
await orm(),
|
|
RoadtripVias,
|
|
{ day: dayId },
|
|
{ after_order_index: 'asc', sequence: 'asc', id: 'asc' },
|
|
)
|
|
).map((row) => ({ id: row.id, after_order_index: row.after_order_index, sequence: row.sequence }));
|
|
const pin = (assignmentId: number, time: string) => setStops({ id: assignmentId }, { assignment_time: time });
|
|
|
|
/** A(0) pinned to nine, B(1) pinned to noon, C(2): the day a check-in at eleven lands in the middle of. */
|
|
async function afternoonDay() {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const [a, b, c, hotel] = ['Harbour', 'Mercure', 'Museum', 'Billstedt'].map((name) =>
|
|
createPlace(testDb, trip.id, { name }),
|
|
);
|
|
await pin(createDayAssignment(testDb, day.id, a.id).id, '09:00');
|
|
await pin(createDayAssignment(testDb, day.id, b.id).id, '12:00');
|
|
createDayAssignment(testDb, day.id, c.id);
|
|
return { trip, day, a, b, c, hotel };
|
|
}
|
|
|
|
it('ACC-034 a night seated ahead of the afternoon carries the roads behind it along', async () => {
|
|
const { trip, day, a, b, c, hotel } = await afternoonDay();
|
|
const afterA = await addVia(day.id, 0);
|
|
const afterB = await addVia(day.id, 1);
|
|
// Behind the last stop: the drive into the next day. It stays with C, which is
|
|
// still last, rather than being dropped as a leg C never had.
|
|
const intoTomorrow = await addVia(day.id, 2);
|
|
|
|
const { mirror } = (await svc.createAccommodation(trip.id, {
|
|
place_id: hotel.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '11:00',
|
|
})) as any;
|
|
|
|
expect(await stopsOn(day.id)).toEqual([a.id, hotel.id, b.id, c.id]);
|
|
expect(await viaAnchors(day.id)).toEqual([
|
|
{ id: afterA, after_order_index: 0 },
|
|
{ id: afterB, after_order_index: 2 },
|
|
{ id: intoTomorrow, after_order_index: 3 },
|
|
]);
|
|
// Reported on the mirror in the shape the road trip broadcasts, so every planner
|
|
// routes the new anchors against the new order.
|
|
expect(mirror.vias).toEqual([
|
|
{
|
|
dayId: day.id,
|
|
vias: [
|
|
expect.objectContaining({ id: afterA, day_id: day.id, after_order_index: 0 }),
|
|
expect.objectContaining({ id: afterB, day_id: day.id, after_order_index: 2 }),
|
|
expect.objectContaining({ id: intoTomorrow, day_id: day.id, after_order_index: 3 }),
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('ACC-034b a night seated behind every drawn road moves none and reports none', async () => {
|
|
const { trip, day, a, b, c, hotel } = await afternoonDay();
|
|
const afterA = await addVia(day.id, 0);
|
|
const afterB = await addVia(day.id, 1);
|
|
|
|
const { mirror } = (await svc.createAccommodation(trip.id, {
|
|
place_id: hotel.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '18:00',
|
|
})) as any;
|
|
|
|
expect(await stopsOn(day.id)).toEqual([a.id, b.id, hotel.id, c.id]);
|
|
expect(await viaAnchors(day.id)).toEqual([
|
|
{ id: afterA, after_order_index: 0 },
|
|
{ id: afterB, after_order_index: 1 },
|
|
]);
|
|
expect(mirror.vias).toBeUndefined();
|
|
});
|
|
|
|
it('ACC-034c re-seating the night by a later check-in re-pins the roads around it', async () => {
|
|
const { trip, day, a, b, c, hotel } = await afternoonDay();
|
|
// C pinned to the afternoon as well, so an evening check-in puts the night last.
|
|
await setStops({ day: day.id, place: c.id }, { assignment_time: '15:00' });
|
|
const { accommodation } = (await svc.createAccommodation(trip.id, {
|
|
place_id: hotel.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '11:00',
|
|
})) as any;
|
|
expect(await stopsOn(day.id)).toEqual([a.id, hotel.id, b.id, c.id]);
|
|
const afterA = await addVia(day.id, 0);
|
|
const afterHotel = await addVia(day.id, 1);
|
|
const afterB = await addVia(day.id, 2);
|
|
const afterC = await addVia(day.id, 3);
|
|
|
|
const existing = (await svc.getAccommodation(accommodation.id, trip.id))!;
|
|
const { mirror } = (await svc.updateAccommodation(accommodation.id, existing, { check_in: '20:00' })) as any;
|
|
|
|
// The night is last now. The road out of it has no leg left and goes; the road
|
|
// behind B follows it one number up. The one behind C was the drive into the next
|
|
// day, which leaves from the hotel now, so it goes as well rather than bending the
|
|
// drive from C to the hotel through a point on the road to tomorrow.
|
|
expect(await stopsOn(day.id)).toEqual([a.id, b.id, c.id, hotel.id]);
|
|
expect(await viaAnchors(day.id)).toEqual([
|
|
{ id: afterA, after_order_index: 0 },
|
|
{ id: afterB, after_order_index: 1 },
|
|
]);
|
|
expect((await viaAnchors(day.id)).map((via) => via.id)).not.toContain(afterHotel);
|
|
expect((await viaAnchors(day.id)).map((via) => via.id)).not.toContain(afterC);
|
|
expect(mirror.moved).not.toBeNull();
|
|
expect(mirror.vias).toEqual([
|
|
{ dayId: day.id, vias: expect.arrayContaining([expect.objectContaining({ id: afterB, after_order_index: 1 })]) },
|
|
]);
|
|
});
|
|
|
|
it('ACC-034d cancelling the booking takes its stop out and re-pins the roads behind it', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day = createDay(testDb, trip.id);
|
|
const [a, b, hotel] = ['Harbour', 'Mercure', 'Billstedt'].map((name) => createPlace(testDb, trip.id, { name }));
|
|
await pin(createDayAssignment(testDb, day.id, a.id).id, '09:00');
|
|
await pin(createDayAssignment(testDb, day.id, b.id).id, '12:00');
|
|
const { accommodation } = (await svc.createAccommodation(trip.id, {
|
|
place_id: hotel.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '11:00',
|
|
})) as any;
|
|
expect(await stopsOn(day.id)).toEqual([a.id, hotel.id, b.id]);
|
|
// The road out of the hotel is drawn first: by id alone it would lead the leg
|
|
// it ends up sharing with A's road.
|
|
const afterHotel = await addVia(day.id, 1);
|
|
const afterA = await addVia(day.id, 0);
|
|
const intoTomorrow = await addVia(day.id, 2);
|
|
|
|
const { mirror } = await svc.deleteAccommodation(accommodation.id);
|
|
|
|
expect(await stopsOn(day.id)).toEqual([a.id, b.id]);
|
|
// The two legs around the hotel are one leg now, A to B, and the road out of the
|
|
// hotel joins A's road on it, behind it: the drive runs A's point first, then the
|
|
// hotel's, then B. The drive into the next day stays behind B, which is still last.
|
|
expect(await legOrder(day.id)).toEqual([
|
|
{ id: afterA, after_order_index: 0, sequence: 0 },
|
|
{ id: afterHotel, after_order_index: 0, sequence: 1 },
|
|
{ id: intoTomorrow, after_order_index: 1, sequence: 0 },
|
|
]);
|
|
expect(mirror.vias).toEqual([
|
|
{
|
|
dayId: day.id,
|
|
vias: [
|
|
expect.objectContaining({ id: afterA, after_order_index: 0, sequence: 0 }),
|
|
expect.objectContaining({ id: afterHotel, after_order_index: 0, sequence: 1 }),
|
|
expect.objectContaining({ id: intoTomorrow, after_order_index: 1 }),
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('ACC-034e moving the booking to another day re-pins the roads on both days', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const day1 = createDay(testDb, trip.id);
|
|
const day2 = createDay(testDb, trip.id);
|
|
const [a, b, c, hotel] = ['Harbour', 'Mercure', 'Museum', 'Billstedt'].map((name) =>
|
|
createPlace(testDb, trip.id, { name }),
|
|
);
|
|
await pin(createDayAssignment(testDb, day1.id, a.id).id, '09:00');
|
|
await pin(createDayAssignment(testDb, day1.id, b.id).id, '12:00');
|
|
await pin(createDayAssignment(testDb, day2.id, c.id).id, '14:00');
|
|
const { accommodation } = (await svc.createAccommodation(trip.id, {
|
|
place_id: hotel.id,
|
|
start_day_id: day1.id,
|
|
end_day_id: day1.id,
|
|
check_in: '11:00',
|
|
})) as any;
|
|
expect(await stopsOn(day1.id)).toEqual([a.id, hotel.id, b.id]);
|
|
const afterA = await addVia(day1.id, 0);
|
|
const afterHotel = await addVia(day1.id, 1);
|
|
const outOfC = await addVia(day2.id, 0);
|
|
|
|
const existing = (await svc.getAccommodation(accommodation.id, trip.id))!;
|
|
const { mirror } = (await svc.updateAccommodation(accommodation.id, existing, {
|
|
start_day_id: day2.id,
|
|
end_day_id: day2.id,
|
|
})) as any;
|
|
|
|
expect(await stopsOn(day1.id)).toEqual([a.id, b.id]);
|
|
expect(await stopsOn(day2.id)).toEqual([hotel.id, c.id]);
|
|
// The hotel's road on day one joins A's on the leg that is left, behind it.
|
|
expect(await legOrder(day1.id)).toEqual([
|
|
{ id: afterA, after_order_index: 0, sequence: 0 },
|
|
{ id: afterHotel, after_order_index: 0, sequence: 1 },
|
|
]);
|
|
// C is still the last stop of its day, one number further on.
|
|
expect(await viaAnchors(day2.id)).toEqual([{ id: outOfC, after_order_index: 1 }]);
|
|
expect(mirror.vias.map((day: { dayId: number }) => day.dayId).sort()).toEqual([day1.id, day2.id].sort());
|
|
});
|
|
|
|
it('ACC-034f a stop the booking hands to the traveller moves no road', async () => {
|
|
const { trip, day, hotel } = await afternoonDay();
|
|
const { accommodation } = (await svc.createAccommodation(trip.id, {
|
|
place_id: hotel.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '11:00',
|
|
})) as any;
|
|
const afterB = await addVia(day.id, 2);
|
|
|
|
const { mirror } = await svc.deleteAccommodation(accommodation.id, { keepStop: true });
|
|
|
|
expect(await viaAnchors(day.id)).toEqual([{ id: afterB, after_order_index: 2 }]);
|
|
expect(mirror.vias).toBeUndefined();
|
|
});
|
|
|
|
it('ACC-034h a booking rebuilt from two stops reports its day once, as it stands at the end', async () => {
|
|
// A stay that owns two stops on one day cannot be carried across, so both go and
|
|
// one comes back, and the day's roads are re-pinned at each step. What the mirror
|
|
// reports is the state that was left behind, not both steps.
|
|
const { trip, day, a, b, c, hotel } = await afternoonDay();
|
|
const { accommodation } = (await svc.createAccommodation(trip.id, {
|
|
place_id: hotel.id,
|
|
start_day_id: day.id,
|
|
end_day_id: day.id,
|
|
check_in: '11:00',
|
|
})) as any;
|
|
await insertRow(await orm(), DayAssignments, {
|
|
day: day.id,
|
|
place: hotel.id,
|
|
order_index: 4,
|
|
accommodation_id: accommodation.id,
|
|
});
|
|
const afterA = await addVia(day.id, 0);
|
|
const afterHotel = await addVia(day.id, 1);
|
|
const afterB = await addVia(day.id, 2);
|
|
await addVia(day.id, 3);
|
|
|
|
const existing = (await svc.getAccommodation(accommodation.id, trip.id))!;
|
|
const { mirror } = (await svc.updateAccommodation(accommodation.id, existing, { notes: 'late arrival' })) as any;
|
|
|
|
expect(await stopsOn(day.id)).toEqual([a.id, hotel.id, b.id, c.id]);
|
|
expect(mirror.removed).toHaveLength(2);
|
|
expect(mirror.created).not.toBeNull();
|
|
// Both stops went, so the hotel's road joined A's; then the night came back
|
|
// behind A, and B's road moved up with B. C's road was the last stop's leg
|
|
// once the second stop was gone, and stayed gone.
|
|
expect(mirror.vias).toEqual([
|
|
{
|
|
dayId: day.id,
|
|
vias: [
|
|
expect.objectContaining({ id: afterA, after_order_index: 0, sequence: 0 }),
|
|
expect.objectContaining({ id: afterHotel, after_order_index: 0, sequence: 1 }),
|
|
expect.objectContaining({ id: afterB, after_order_index: 2 }),
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('ACC-034g announceMirror sends the re-pinned roads behind the day order', async () => {
|
|
const sent: Array<{ event: string; payload: unknown }> = [];
|
|
await svc.announceMirror(
|
|
5,
|
|
{
|
|
created: { id: 78, day_id: 11 } as never,
|
|
moved: null,
|
|
updated: [],
|
|
removed: [],
|
|
stamped: null,
|
|
vias: [{ dayId: 11, vias: [] }],
|
|
},
|
|
(event, payload) => {
|
|
sent.push({ event, payload });
|
|
},
|
|
);
|
|
expect(sent.map((e) => e.event)).toEqual(['assignment:created', 'assignment:reordered', 'roadtripVia:changed']);
|
|
expect(sent[2].payload).toEqual({ dayId: 11, vias: [] });
|
|
});
|
|
});
|
|
|
|
describe('AccommodationsService wiring', () => {
|
|
it('ACC-001: the module carries the controller and the RPC surface, and re-exports the service', () => {
|
|
expectRegisteredController(AccommodationsModule, AccommodationsController);
|
|
// The service itself lives in the domain module now, so places can delete the
|
|
// nights booked at a place without the surfaces coming along and closing a loop
|
|
// back through PlacesModule. Still exported from here: everything that imported
|
|
// AccommodationsModule for the service keeps working.
|
|
expectRegisteredProvider(AccommodationsDomainModule, AccommodationsService);
|
|
const imports = Reflect.getMetadata('imports', AccommodationsModule) as unknown[];
|
|
expect(imports).toEqual(expect.arrayContaining([AccommodationsDomainModule]));
|
|
// The MODULE is re-exported, not the provider: Nest refuses to export a provider
|
|
// belonging to an imported module, and a boot that fails on that only shows up when
|
|
// the container is actually built.
|
|
const exports = Reflect.getMetadata('exports', AccommodationsModule) as unknown[];
|
|
expect(Array.isArray(exports)).toBe(true);
|
|
expect(exports).toEqual(expect.arrayContaining([AccommodationsDomainModule]));
|
|
});
|
|
|
|
it('ACC-001b: the domain module reaches neither places nor the surfaces', () => {
|
|
// The whole point of the split: PlacesModule imports this one, so a path back to
|
|
// places from here would be the loop it exists to avoid.
|
|
const imports = (Reflect.getMetadata('imports', AccommodationsDomainModule) as { name?: string }[]) ?? [];
|
|
expect(imports.map((m) => m?.name)).not.toContain('PlacesModule');
|
|
});
|
|
|
|
it('ACC-002: it does not import the days or reservations modules', () => {
|
|
const imports = (Reflect.getMetadata('imports', AccommodationsModule) as { name?: string }[]) ?? [];
|
|
expect(Array.isArray(imports)).toBe(true);
|
|
const names = imports.map((m) => m?.name);
|
|
expect(names).not.toContain('DaysModule');
|
|
expect(names).not.toContain('ReservationsModule');
|
|
});
|
|
});
|