chore: wire TripsService into the plugin RPC host and retire the legacy tripService

Swap the factory's six tripService imports for the injected TripsService; delete services/tripService.ts and the collab/vacay bridges with it; prune the days/budget bridges to their surviving exports; update the migration docs and regenerate migration-graph.md.
This commit is contained in:
jubnl
2026-07-29 01:53:54 +02:00
parent 530c7cb005
commit 287dceb532
18 changed files with 179 additions and 1370 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ Wave 4 — the coupled cluster (order matters here)
dayService; the budget/day coupling lives in the Nest wrapper's budget-sync seam and the MCP registrars, which keep their legacy imports until those domains migrate. Reservations went first as the frontier residue fold; dayService
(done 2026-07 — the 592-line service folded into the wrapper `DaysService`, the accommodations seam in `nest/reservations/` now injects it, and the hand-rolled reorder/insert transactions became `db.transaction()`) followed;
budgetService followed (done 2026-07 — the 755-line money core folded into the wrapper `BudgetService` (settlement maths, FX freeze/rebase, items/members/payers CRUD; the raw settlement writes became `insertSettlement`/`applySettlementUpdate` so the MCP paths keep skipping the freeze); the 11-tool mcp registrar + 3 budget resources moved to the decorator-driven `budget.mcp.ts`; TripsService/ReservationsService/BookingImportService now inject `BudgetService`; a 4-export `budget.bridge.ts` serves tripService, userCleanupService and the legacy trips/transports registrars; `exchange-rates.bridge.ts` was deleted with its last consumers; and the controller adopted `budget.dto.ts` over the shared schemas, clearing all nine budget allow-list entries — **tripService is now unblocked**).
9. Then placeService (7 internal deps, mostly on things migrated by now) and tripService (10 deps — the biggest hub; last in this wave, since nearly everything it needs will already be injectable).
9. tripService (done 2026-07 — the 1121-line hub folded into the wrapper `TripsService`: generateDays, the updateTrip date-shift transaction, members/guests (#973/#1362), the ICS export, copyTripById and getTripSummary; its six bridge imports became injected services (CollabService/VacayService joined the constructor); the 10-tool trips registrar + 3 trip resources + the trip-summary prompt moved to the decorator-driven `trips.mcp.ts` (first `@Prompt`; the static-token deprecation notice rides the `registry.attach` ctx) and the 3 share-link tools to `share.mcp.ts` on the `canShareTrips` predicate; FeedsService and the plugin RPC host inject `TripsService`; a 3-export `trips.bridge.ts` serves the legacy prompts registrar and budget.mcp.ts's owner/member seam; todo/share/collab/vacay bridges deleted with their last consumers; the controller adopted `trips.dto.ts`, clearing all seven trip allow-list entries). Then placeService (7 internal deps, mostly on things migrated by now — the remaining Wave-4 pick).
Wave 5 — the heavyweights, last
+44 -28
View File
@@ -1,8 +1,14 @@
# Legacy `src/services/` dependency graph
Generated from the actual imports in `server/src` on **2026-07-28** (after the
budgetService migration + its quirk-fix pass — completing step 1 of the
dependency-honest order below and unblocking the tripService hub — and the
Generated from the actual imports in `server/src` on **2026-07-29** (after the
tripService migration + its quirk-fix pass — completing step 2 of the
dependency-honest order below: the 1121-line hub folded into the DI-native
`TripsService`, its MCP surface moved to the decorator-driven
`trips.mcp.ts`/`share.mcp.ts`, the plugin RPC host injects it, and the bridge
cascade landed — `todo.bridge`, `share.bridge`, `collab.bridge` and
`vacay.bridge` deleted with their last consumers, the days/budget bridges
pruned to their surviving exports. Earlier context: the budgetService
migration (step 1) and the
Phase 0 quick wins: `getAppUrl`/`getMcpSafeUrl` → `src/app-config` deleted the
fake `→ notifications` edges for the identity/MCP stack and freed mapsService;
the addon-enablement reads moved from adminService into `nest/addons/`
@@ -44,14 +50,20 @@ How to read it:
`ExchangeRatesService` — module-scoped rate cache retained on purpose, permissions-style,
so out-of-container instances and the DI singleton share one cached upstream feed;
its `exchange-rates.bridge` was deleted with the budgetService migration),
budget (the 755-line money core folded into `BudgetService`; `budget.bridge.ts` carries
the four exports still consumed outside the container — tripService, userCleanupService
and the legacy trips/transports MCP registrars).
budget (the 755-line money core folded into `BudgetService`; `budget.bridge.ts` is down
to the two exports still consumed outside the container — userCleanupService and the
legacy transports registrar),
trip (the 1121-line hub folded into `TripsService`; its six bridge imports became
injected services, and the fold deleted `todo.bridge`, `share.bridge`, `collab.bridge`
and `vacay.bridge` with their last consumers; a 3-export `trips.bridge.ts` serves the
legacy prompts registrar's getTripSummary and `budget.mcp.ts`'s getTripOwner/listMembers
seam — injecting there would need a forwardRef'd TripsModule↔BudgetModule cycle;
`days.bridge.ts` shrank to getDay/listDays for the transit/transports registrars).
- **Domain migration targets** (the wave material): adminService, airportService, atlasService,
authService, backupService, collectionsService,
journeyService, journeyShareService, mapsService, notificationService, oauthService,
oidcService, passkeyService, placeService, transitService,
transitItineraryService, tripService, weatherService, wikiService.
transitItineraryService, weatherService, wikiService.
- **Cross-cutting Wave-2 targets:** permissions and auditLog are done (2026-07) — see the
DI-native list above; only tripAccess remains (delete, don't migrate).
- **Helpers that stay as plain modules** (pure/infra, not wave material): avatarUrl,
@@ -74,7 +86,6 @@ flowchart TD
classDef infra fill:#455a64,color:#fff
subgraph frontier["READY FRONTIER (no unmigrated domain deps)"]
trip["tripService (the hub)"]:::hub
atlas[atlasService]:::ready
weather[weatherService]:::ready
airport["airportService (boot special case)"]:::ready
@@ -99,7 +110,6 @@ flowchart TD
memories["memories/ cluster"]:::infra
cleanup[userCleanupService]:::infra
trip --> cleanup
collections -.-> notifSvc
place --> maps
transit --> maps
@@ -122,10 +132,11 @@ journey/memories corner tangle with the admin corner. The former
`auth/collections/backup → permissions` and `notifSvc/oauth → auditLog` edges are gone since the
2026-07 Wave-2 pair: the permissions consumers repointed to `nest/permissions/permissions.bridge`,
the writeAudit consumers to `nest/audit/audit.bridge`, and the log*-only consumers to the plain
`nest/audit/audit-log.logger` — none of them block a migration anymore. The former
`trip → budget` and `cleanup → budget` edges are gone since the 2026-07 budgetService
migration: both repointed to `nest/budget/budget.bridge`, leaving tripService with no
unmigrated domain dependency — the hub is frontier-ready. The dotted
`nest/audit/audit-log.logger` — none of them block a migration anymore. The
tripService hub node is gone since the 2026-07 trip fold — the DI-native
`TripsService` injects its former bridge targets and keeps the legacy
avatarUrl/timezoneService/userCleanupService helpers as plain imports (helpers
never block). The dotted
`collections -.-> notifSvc` edge is a call-time `import()` for the collection-invite
notification: it stays as-is through a migration, exactly like the identical lazy sends in the
already-migrated collab/packing/trips/reservations/vacay services.)
@@ -138,8 +149,8 @@ already-migrated collab/packing/trips/reservations/vacay services.)
| `airportService` | (none) | (none) | nest/airports/airports.service.ts, nest/booking-import/kitinerary-mapper.ts | db/database.ts, mcp/tools/mapsWeather.ts, mcp/tools/transports.ts |
| `apiKeyCrypto` | (none) | adminService, airtrail/airtrailService, authService, llmConfig, mapsService, memories/helpersService, memories/immichService, memories/photoResolverService, memories/synologyService, memories/unifiedService, notifications, oidcService, unsplashService | nest/plugins/plugin-oauth.service.ts, nest/plugins/plugin-runtime.service.ts, nest/plugins/plugins.service.ts, nest/settings/settings.service.ts | db/migrations.ts |
| `atlasService` | (none) | authService | nest/atlas/atlas.service.ts, nest/plugins/host/plugin-host-deps.factory.ts | mcp/resources.ts, mcp/tools/atlas.ts |
| `authService` | apiKeyCrypto, atlasService, avatarUrl, demo, distanceService, ephemeralTokens, mfaCrypto, passwordPolicy, tripMembership, userCleanupService, webauthnConfig (+ `permissions.bridge`) | adminService, oidcService, passkeyService | nest/assignments/assignments.mcp.ts, nest/auth/auth.service.ts, nest/auth/passkey-enabled.guard.ts, nest/budget/budget.mcp.ts, nest/collab/collab.mcp.ts, nest/days/day-notes.mcp.ts, nest/days/days.mcp.ts, nest/oidc/oidc.service.ts, nest/packing/packing.mcp.ts, nest/reservations/reservations.mcp.ts, nest/tags/tags.mcp.ts, nest/todo/todo.mcp.ts, nest/vacay/vacay.mcp.ts | mcp/index.ts, mcp/tools/atlas.ts, mcp/tools/collections.ts, mcp/tools/journey.ts, mcp/tools/notifications.ts, mcp/tools/places.ts, mcp/tools/transit.ts, mcp/tools/transports.ts, mcp/tools/trips.ts |
| `avatarUrl` | (none) | adminService, authService, inAppNotifications, journeyService, tripService | nest/budget/budget.service.ts, nest/collab/collab.service.ts, nest/files/files.service.ts, nest/packing/packing.service.ts, nest/reservations/reservations.service.ts | (none) |
| `authService` | apiKeyCrypto, atlasService, avatarUrl, demo, distanceService, ephemeralTokens, mfaCrypto, passwordPolicy, tripMembership, userCleanupService, webauthnConfig (+ `permissions.bridge`) | adminService, oidcService, passkeyService | nest/assignments/assignments.mcp.ts, nest/auth/auth.service.ts, nest/auth/passkey-enabled.guard.ts, nest/budget/budget.mcp.ts, nest/collab/collab.mcp.ts, nest/days/day-notes.mcp.ts, nest/days/days.mcp.ts, nest/oidc/oidc.service.ts, nest/packing/packing.mcp.ts, nest/reservations/reservations.mcp.ts, nest/share/share.mcp.ts, nest/tags/tags.mcp.ts, nest/todo/todo.mcp.ts, nest/trips/trips.mcp.ts, nest/vacay/vacay.mcp.ts | mcp/index.ts, mcp/tools/atlas.ts, mcp/tools/collections.ts, mcp/tools/journey.ts, mcp/tools/notifications.ts, mcp/tools/places.ts, mcp/tools/transit.ts, mcp/tools/transports.ts |
| `avatarUrl` | (none) | adminService, authService, inAppNotifications, journeyService | nest/budget/budget.service.ts, nest/collab/collab.service.ts, nest/files/files.service.ts, nest/packing/packing.service.ts, nest/reservations/reservations.service.ts, nest/trips/trips.service.ts | (none) |
| `backupService` | (none — `permissions.bridge`, plugin backup/paths infra only) | (none) | nest/backup/backup.controller.ts, nest/backup/backup.service.ts | (none) |
| `collectionsService` | notificationService (lazy), placeImage (+ `permissions.bridge`) | (none) | nest/collections/collections.service.ts, nest/plugins/host/plugin-host-deps.factory.ts | mcp/tools/collections.ts |
| `conflictResult` | (none) | placeService | nest/packing/packing.controller.ts, nest/packing/packing.service.ts, nest/places/places.controller.ts, nest/plugins/host/plugin-host-deps.factory.ts | (none) |
@@ -167,14 +178,13 @@ already-migrated collab/packing/trips/reservations/vacay services.)
| `placePhotoCache` | (none) | mapsService, placeService | nest/maps/maps.service.ts, nest/share/share.service.ts | scheduler.ts |
| `placeService` | conflictResult, kmlImport, placeEnrichment, placeImage, placePhotoCache, queryHelpers, unsplashService | (none) | nest/booking-import/booking-import.service.ts, nest/days/days.mcp.ts, nest/places/places.service.ts, nest/plugins/host/plugin-host-deps.factory.ts, nest/trips/trips.service.ts | mcp/resources.ts, mcp/tools/places.ts |
| `queryHelpers` | (none) | placeService | nest/assignments/assignments.service.ts, nest/days/days.service.ts, nest/share/share.service.ts | (none) |
| `timezoneService` | (none) | airtrail/airtrailMapper, transitItineraryService, tripService | (none) | (none) |
| `timezoneService` | (none) | airtrail/airtrailMapper, transitItineraryService | nest/trips/trips.service.ts | (none) |
| `transitItineraryService` | distanceService, timezoneService, transitService (+ type-only `reservations.bridge`) | (none) | (none) | mcp/tools/transit.ts |
| `transitService` | mapsService | transitItineraryService | nest/transit/transit.controller.ts | mcp/tools/transit.ts |
| `tripAccess` | (none) | tripService | nest/booking-import/booking-import.service.ts, nest/budget/budget.service.ts, nest/collab/collab.service.ts, nest/integrations/airtrail-import.controller.ts, nest/packing/packing.service.ts, nest/reservations/reservations.service.ts, nest/todo/todo.service.ts | (none) |
| `tripAccess` | (none) | (none) | nest/booking-import/booking-import.service.ts, nest/budget/budget.service.ts, nest/collab/collab.service.ts, nest/days/day-notes.service.ts, nest/integrations/airtrail-import.controller.ts, nest/packing/packing.service.ts, nest/reservations/reservations.service.ts, nest/todo/todo.service.ts | (none) |
| `tripMembership` | (none) | authService, oidcService | nest/plugins/host/plugin-host-deps.factory.ts, nest/trip-invite/trip-invite.service.ts | (none) |
| `tripService` | avatarUrl, timezoneService, tripAccess, userCleanupService (+ `budget.bridge`, `collab.bridge`, `days.bridge`, `packing.bridge`, `reservations.bridge`, `vacay.bridge`) | (none) | nest/budget/budget.mcp.ts, nest/feeds/feeds.service.ts, nest/plugins/host/plugin-host-deps.factory.ts, nest/trips/trips.controller.ts, nest/trips/trips.service.ts | mcp/resources.ts, mcp/tools/prompts.ts, mcp/tools/trips.ts |
| `unsplashService` | apiKeyCrypto | placeService | nest/trips/trips.controller.ts, nest/trips/trips.service.ts | (none) |
| `userCleanupService` | (none — `budget.bridge`, plugin paths infra only) | adminService, authService, tripService | (none) | (none) |
| `userCleanupService` | (none — `budget.bridge`, plugin paths infra only) | adminService, authService | nest/trips/trips.service.ts | (none) |
| `weatherService` | (none) | (none) | nest/plugins/host/plugin-host-deps.factory.ts, nest/weather/weather.controller.ts, nest/weather/weather.service.ts | mcp/tools/mapsWeather.ts |
| `webauthnConfig` | (none) | authService, passkeyService | (none) | (none) |
| `wikiService` | (none) | (none) | nest/help/help.controller.ts | (none) |
@@ -205,19 +215,14 @@ already-migrated collab/packing/trips/reservations/vacay services.)
| Candidate | Why now / why not | Bridge tax (legacy dependents + out-of-container) |
|---|---|---|
| **tripService** ← pick | budgetService landed (2026-07), so the hub's last unmigrated domain edge is gone (its remaining legacy deps are helpers + the budget/collab/days/packing/reservations/vacay bridges); every other migration keeps paying it a repoint tax until it moves | no legacy dependents; mcp resources/prompts/trips registrars + `budget.mcp.ts`'s getTripOwner/listMembers seam |
| **mapsService** ← pick | Frontier-ready since the Phase 0 `getAppUrl` → app-config move; heads the step-4 chain that unblocks the placeService Wave-4 remainder (tripService — the hub — landed 2026-07) | `placeEnrichment`, `transitService`; `mcp/tools/mapsWeather.ts`, `mcp/tools/places.ts` |
| **collectionsService** | Frontier-ready — legacy deps are the placeImage helper + `permissions.bridge`, plus a call-time lazy `import()` of notificationService that a migrated service keeps (collab precedent) | `mcp/tools/collections.ts` |
| **atlasService** | Zero deps, but its legacy dependent is `authService` (Wave-5) → bridge lives long | `authService`; `mcp/tools/atlas.ts`, `mcp/resources.ts` |
| **mapsService** | Frontier-ready since the Phase 0 `getAppUrl` → app-config move (remaining legacy deps are the apiKeyCrypto/placePhotoCache helpers); unblocks transitService → placeService → transitItineraryService | `placeEnrichment`, `transitService`; `mcp/tools/mapsWeather.ts`, `mcp/tools/places.ts` |
| **oauthService** | Dependency-free since the Phase 0 addons extraction (adminService edge was only `isAddonEnabled` → `addons.bridge`) — but the coherence order keeps it after admin so the `mcp/oauthProvider.ts` merge (`mcp-2`) lands with it | `mcp/index.ts`, `mcp/oauthProvider.ts` |
| **weatherService / wikiService / airportService** | Independent leaves; airport has the `db/database.ts` boot lazy-require special case | little / none |
**Blocked, and by what (shortest unblock path):**
- `tripService` (the hub) — **unblocked since the 2026-07 budgetService
migration** (its budget edge and `userCleanupService`'s are now
`budget.bridge` repoints, joining the collab/vacay/reservations/days ones).
See the frontier table above.
- `placeService` ← `mapsService` (via placeEnrichment) — mapsService itself is
frontier-ready since the Phase 0 app-url move deleted its notifications edge.
- `notificationService` ← notifications cluster (its auditLog edge is now the plain
@@ -246,7 +251,9 @@ Wave-2 `permissions` + `auditLog` pair were the first frontier picks — all don
1. `exchangeRateService` fold → `budgetService` (both done 2026-07 — `ExchangeRatesService`
then the full `BudgetService` fold in `nest/budget/`; `userCleanupService` is free,
repointed to `budget.bridge`)
2. `tripService` — all remaining domain edges + userCleanup now gone (frontier `← pick`)
2. `tripService` (done 2026-07 — the hub folded into `TripsService`; the trips/share MCP
surfaces moved to the decorator registry, the plugin host injects it, and the
todo/share/collab/vacay bridges died with their last consumers)
3. notifications cluster → `notificationService`
4. `mapsService` → `transitService` → `placeService` → `transitItineraryService`
5. `atlasService` → `authService` (+ oidc/passkey) → `adminService` → `oauthService`
@@ -262,8 +269,10 @@ Wave-2 `permissions` + `auditLog` pair were the first frontier picks — all don
frontier-ready. **Borne out by the 2026-07 fold**: the budget/day coupling lives in the
Nest wrapper's budget-sync seam and the MCP surface, which keep their legacy imports until
those domains migrate.
- Every remaining Wave-3/4 domain has legacy `tripService` as a dependent — each migration
before tripService pays a small bridge/repoint tax the files migration didn't have.
- Every remaining Wave-3/4 domain had legacy `tripService` as a dependent — each migration
before tripService paid a small bridge/repoint tax the files migration didn't have.
**Resolved by the 2026-07 trip fold**: that recurring tax is gone, and the fold cashed in
the accumulated bridges (todo/share/collab/vacay deleted, days/budget pruned).
- (2026-07-28 regeneration) The earlier grep-based analysis only saw static imports, so it
missed every lazy edge: the scheduler's `require()` jobs (admin, notificationService,
placePhotoCache, airtrailSync, trekPhotoCache), the `db/database.ts` boot backfill's
@@ -272,3 +281,10 @@ Wave-2 `permissions` + `auditLog` pair were the first frontier picks — all don
also under-reported `tripService`'s bridge repoints (six, not three: budget, collab, days,
packing, reservations, vacay). None of this changes the order — lazy edges don't block —
but the bridge-pressure column was undercounting scheduler/system-notice consumers.
- (2026-07-29 regeneration, post trip fold) The predicted bridge tax was right in shape but
the fold went further than a bridge: the whole trips MCP surface (10 tools + 3 resources
+ the first `@Prompt`) and the 3 share-link tools moved onto the decorator registry, so
the only bridge left is the 3-export `trips.bridge.ts` for the legacy prompts registrar
and `budget.mcp.ts`'s owner/member seam. `verifyTripAccess`'s `tripAccess` re-export died
with the legacy file, leaving `services/tripAccess` with zero legacy importers — it is
now purely Nest-consumed (Wave-2 "delete, don't migrate" is down to those repoints).
+25 -4
View File
@@ -28,8 +28,8 @@ mount to Nest and leaves the sibling trip routes (days, places, ...) on Express.
- **Phase 2 (trip sub-domains):** vacay (addon), packing, todo.
- **DI-native services (legacy `src/services/*` deleted):** tags, categories,
todo, packing, day-notes, trip-invite, assignments, share, settings, files,
collab, vacay, reservations, day, permissions, audit — see the migration
recipe below.
collab, vacay, reservations, day, permissions, audit, budget, trip — see the
migration recipe below.
## Cross-cutting Foundation pieces
@@ -203,9 +203,30 @@ MCP class) and BookingImportService inject `BudgetService`; a 4-export
trips/transports registrars; `exchange-rates.bridge.ts` was deleted with its
last consumers, and the controller adopted `budget.dto.ts` — all nine
allow-list entries removed).
tripService followed (the 1121-line Wave-4 hub — the biggest fold — moved into
the wrapper `TripsService`: TRIP_SELECT + list/create/get, the
`generateDays` two-phase renumber engine, the updateTrip date-shift
transaction, the member/guest lifecycle (#973/#1362), the ICS export with its
module-scoped tz-validity cache, `copyTripById` and `getTripSummary`; its six
bridge imports became injected services (CollabService + VacayService joined
the constructor); the 10-tool trips registrar + 3 `resources.ts` trip
resources + the trip-summary prompt moved to `trips.mcp.ts` — the first
`@Prompt` use, with the fire-once static-token deprecation notice now riding
the `registry.attach` ctx — and the 3 share-link tools it carried moved to
`share.mcp.ts` on the `canShareTrips` predicate (delete_trip and the
canReadTrips reads are predicates too — the broadened legacy gates have no
declarative equivalent); FeedsService and the plugin host inject
`TripsService` (its 20th constructor dep); a 3-export `trips.bridge.ts`
serves the legacy prompts registrar and budget.mcp.ts's owner/member seam
(injecting there would need a forwardRef'd TripsModule↔BudgetModule cycle);
`todo.bridge.ts`, `share.bridge.ts`, `collab.bridge.ts` and `vacay.bridge.ts`
were deleted with their last consumers and the unused days/budget bridge
exports pruned; the controller adopted `trips.dto.ts` — all seven allow-list
entries removed).
Repeat these steps per
service (next up: **tripService** — the budget fold was its unblock, per the
dependency-honest order in `migration-graph.md`). This is a
service (next up: the **mapsService → transitService → placeService** chain —
or the notifications fan-in — per the dependency-honest order in
`migration-graph.md`). This is a
**pure relocation** — byte-identical
SQL, statuses, bodies, and error strings. The plugin RPC host is **no longer a
bridge consumer**: since Option A of `src/nest/plugins/DI-MIGRATION.md` it
+10 -17
View File
@@ -7,15 +7,16 @@ import { ExchangeRatesService } from './exchange-rates.service';
/**
* Non-Nest entry point for the budget domain — for code running OUTSIDE the
* Nest container (the legacy services/tripService.ts and
* services/userCleanupService.ts, and the legacy update_trip / create_transport
* registrars in src/mcp/tools/trips.ts and src/mcp/tools/transports.ts; the
* budget MCP tools and resources moved to the DI-discovered budget.mcp.ts, and
* the plugin RPC host injects BudgetService via PluginHostDepsFactory).
* Exports only the legacy services/budgetService names still consumed outside
* the container, 1:1, so repointing a consumer is an import-path-only diff.
* Inside the container, inject BudgetService instead. Delete this file when
* tripService/userCleanupService and the legacy MCP registrars migrate.
* Nest container (services/userCleanupService.ts and the legacy
* create_transport registrar in src/mcp/tools/transports.ts; the budget MCP
* tools and resources moved to the DI-discovered budget.mcp.ts, the plugin
* RPC host injects BudgetService via PluginHostDepsFactory, and the trip
* domain folded into the DI-native TripsService, which injects BudgetService —
* its bridge exports were pruned with it). Exports only the legacy
* services/budgetService names still consumed outside the container, 1:1, so
* repointing a consumer is an import-path-only diff. Inside the container,
* inject BudgetService instead. Delete this file when
* userCleanupService and the transports registrar migrate.
*
* Module-level construction is safe: `db` is the reinitialize-proof Proxy onto
* the shared better-sqlite3 singleton, and ExchangeRatesService keeps its FX
@@ -24,18 +25,10 @@ import { ExchangeRatesService } from './exchange-rates.service';
*/
const budget = new BudgetService(new DatabaseService(db), new PermissionsService(new DatabaseService(db)), new ExchangeRatesService(), new RealtimeService());
export function listBudgetItems(tripId: string | number) {
return budget.listBudgetItems(tripId);
}
export function removeUserFromBudgetItems(userId: number): void {
budget.removeUserFromBudgetItems(userId);
}
export function rebaseTripCurrency(tripId: string | number, newCurrency: string | null | undefined): Promise<void> {
return budget.rebaseTripCurrency(tripId, newCurrency);
}
export function linkBudgetItemToReservation(
tripId: string | number,
reservationId: number,
-33
View File
@@ -1,33 +0,0 @@
import { db } from '../../db/database';
import { DatabaseService } from '../database/database.service';
import { RealtimeService } from '../realtime/realtime.service';
import { CollabService } from './collab.service';
import { PermissionsService } from '../permissions/permissions.service';
/**
* Non-Nest entry point for the collab domain — for code running OUTSIDE the
* Nest container (the legacy trip-summary builder in src/services/tripService.ts
* and the legacy get_trip_summary registrar in src/mcp/tools/trips.ts; the
* collab MCP tools and resources moved to the DI-discovered collab.mcp.ts, and
* the plugin RPC host injects CollabService via PluginHostDepsFactory). Exports
* only the legacy services/collabService names still consumed outside the
* container, 1:1, so repointing a consumer is an import-path-only diff. Inside
* the container, inject CollabService instead. Delete this file when
* tripService and the legacy MCP trips registrar migrate.
*
* Module-level construction is safe: `db` is the reinitialize-proof Proxy onto
* the shared better-sqlite3 singleton.
*/
const collab = new CollabService(new DatabaseService(db), new PermissionsService(new DatabaseService(db)), new RealtimeService());
export function listNotes(tripId: string | number) {
return collab.listNotes(tripId);
}
export function listPolls(tripId: string | number) {
return collab.listPolls(tripId);
}
export function countMessages(tripId: string | number): number {
return collab.countMessages(tripId);
}
+3 -2
View File
@@ -58,8 +58,9 @@ export interface LinkPreviewResult {
* run in db.transaction(), getFormattedNoteById is trip-scoped and null-safe,
* votePoll rejects non-integer indexes, and linkPreview absorbs malformed URLs
* instead of throwing.
* Non-Nest consumers (legacy tripService, the legacy MCP trips registrar) go
* through collab.bridge.ts instead of importing this class directly.
* All consumers are in-container since the trip fold (TripsService and
* TripsMcp inject this class); collab.bridge.ts was deleted with its last
* outside-container consumers.
*/
@Injectable()
export class CollabService {
+10 -30
View File
@@ -1,27 +1,26 @@
import { db } from '../../db/database';
import { DatabaseService } from '../database/database.service';
import { RealtimeService } from '../realtime/realtime.service';
import { DaysService, addDays } from './days.service';
import { DaysService } from './days.service';
import { PermissionsService } from '../permissions/permissions.service';
/**
* Non-Nest entry point for the day domain — for code running OUTSIDE the
* Nest container (the legacy services/tripService.ts and the legacy
* transit/transports MCP registrars in src/mcp/tools/; the day MCP tools and
* resources moved to the DI-discovered days.mcp.ts, and the plugin RPC host
* injects DaysService via PluginHostDepsFactory). Exports only the legacy
* services/dayService names still consumed outside the container, 1:1, so
* repointing a consumer is an import-path-only diff. Inside the container,
* inject DaysService instead. Delete this file when tripService and the
* transit/transports registrars migrate.
* Nest container (the legacy transit/transports MCP registrars in
* src/mcp/tools/; the day MCP tools and resources moved to the DI-discovered
* days.mcp.ts, the plugin RPC host injects DaysService via
* PluginHostDepsFactory, and the trip domain folded into the DI-native
* TripsService, which injects DaysService — its bridge exports were pruned
* with it). Exports only the legacy services/dayService names still consumed
* outside the container, 1:1, so repointing a consumer is an import-path-only
* diff. Inside the container, inject DaysService instead. Delete this file
* when the transit/transports registrars migrate.
*
* Module-level construction is safe: `db` is the reinitialize-proof Proxy onto
* the shared better-sqlite3 singleton.
*/
const days = new DaysService(new DatabaseService(db), new PermissionsService(new DatabaseService(db)), new RealtimeService());
export { addDays };
export function getDay(id: string | number, tripId: string | number) {
return days.getDay(id, tripId);
}
@@ -29,22 +28,3 @@ export function getDay(id: string | number, tripId: string | number) {
export function listDays(tripId: string | number) {
return days.list(tripId);
}
export function listAccommodations(tripId: string | number) {
return days.listAccommodations(tripId);
}
export function restampReservationDates(
tripId: string | number,
oldDateById: Map<number, string | null>,
newDateById: Map<number, string | null>,
): void {
days.restampReservationDates(tripId, oldDateById, newDateById);
}
export function resyncAccommodationDays(
tripId: string | number,
prevDateByDayId: Map<number, string | null>,
): void {
days.resyncAccommodationDays(tripId, prevDateByDayId);
}
+15 -7
View File
@@ -36,10 +36,11 @@ DI-only service and the legacy function module was deleted, but the wiring
layer didn't inject — so a module-level shim re-exported the legacy names over
a hand-built instance. Option A deleted `tags.bridge.ts` and
`categories.bridge.ts` (the plugin host was their only consumer).
**`todo.bridge.ts` survives** for exactly one remaining consumer that genuinely
runs outside the container: the legacy `get_trip_summary` registrar in
`src/mcp/tools/trips.ts`. Delete it when that registrar migrates to the
DI-discovered MCP registry. New bridges should only be added for genuinely
`todo.bridge.ts`, `share.bridge.ts`, `collab.bridge.ts` and `vacay.bridge.ts`
followed with the 2026-07 trip fold — their last consumers (the legacy
`src/mcp/tools/trips.ts` registrar and the legacy tripService itself) migrated
into the DI-discovered `trips.mcp.ts`/`share.mcp.ts` and the DI-native
`TripsService`. New bridges should only be added for genuinely
non-Nest consumers (legacy MCP registrars, scheduler, websocket) — the plugin
host no longer needs them, ever.
@@ -118,8 +119,12 @@ host's audit trail is the separate `plugin-audit.ts`); exchangeRateService
swapped in 2026-07 with the budget-domain fold — its `getRates` import became
the injected `ExchangeRatesService`; budgetService swapped in 2026-07 with the
budget migration — its `listBudgetItems` import became the already-injected
`BudgetService`, so no constructor change at all; the remaining factory
imports are all Wave-4+ domains: trips, places, journeys, atlas, collections).
`BudgetService`, so no constructor change at all; tripService swapped in
2026-07 with the trip fold — its six imported symbols (`listTrips`,
`updateTrip`, `createTrip`, `removeMember`, `NotFoundError`, `ValidationError`)
became the injected `TripsService`, with the error classes now imported from
`nest/trips/trips.service`; the remaining factory
imports are all Wave-4+/5 domains: places, journeys, atlas, collections).
### Test impact (as landed)
@@ -127,7 +132,10 @@ imports are all Wave-4+ domains: trips, places, journeys, atlas, collections).
`PluginRpcHost` directly with hand-built `HostDeps`).
- `tests/unit/plugins/plugin-host-deps.factory.test.ts` (was
`create-rpc-host.test.ts`) — the six DI-domain path mocks became constructor
stubs (sixteen stubs as of the 2026-07 exchange-rates fold); the ~22
stubs (sixteen stubs as of the 2026-07 exchange-rates fold, plus `tripsStub`
with the 2026-07 trip fold — the tripService path mock's sentinel behaviors
moved onto the stub, its throws now built from the real
`nest/trips/trips.service` error classes); the ~22
legacy-service path mocks shrank by one with the 2026-07 budget migration
(the budgetService satisfy-the-import mock died with the legacy file — the
`budgetStub` gained `listBudgetItems` for the two swapped closures); a
@@ -14,7 +14,7 @@ import fsMod from 'node:fs';
import pathMod from 'node:path';
import { randomUUID } from 'node:crypto';
import { PermissionsService } from '../../permissions/permissions.service';
import { listTrips, updateTrip, createTrip, removeMember as removeTripMemberSvc, NotFoundError, ValidationError } from '../../../services/tripService';
import { TripsService, NotFoundError, ValidationError } from '../../trips/trips.service';
import { createPlace, updatePlace, deletePlace } from '../../../services/placeService';
import { AddonsService } from '../../addons/addons.service';
import { isDemoEmail } from '../../../services/demo';
@@ -125,7 +125,7 @@ export interface PluginCallRouter {
*
* DI-native domain services (budget, exchange-rates, reservations, tags,
* categories, todo, packing, day-notes, assignments, oauth, files, collab,
* vacay, the LLM config resolver) and
* vacay, trips, the LLM config resolver) and
* the DatabaseService (all inline SQL + the trip-access helper) are
* constructor-injected; legacy `services/*` domains stay plain function
* imports until their own migration lands (DI-MIGRATION.md), at which point
@@ -153,6 +153,7 @@ export class PluginHostDepsFactory {
private readonly exchangeRates: ExchangeRatesService,
private readonly addons: AddonsService,
private readonly realtime: RealtimeService,
private readonly trips: TripsService,
) {}
/**
@@ -347,7 +348,7 @@ export class PluginHostDepsFactory {
// transfer is a separate, deliberate action, not a member-management side effect.
const trip = this.db.prepare('SELECT user_id FROM trips WHERE id = ?').get(tripId) as { user_id: number } | undefined;
if (trip && trip.user_id === targetUserId) throw new ForbiddenResource('cannot remove the trip owner');
removeTripMemberSvc(tripId, targetUserId);
this.trips.removeMember(tripId, targetUserId);
return { removed: true };
},
// --- Host-mediated notifications. Recipient resolution + channel fan-out +
@@ -431,7 +432,7 @@ export class PluginHostDepsFactory {
// Cross-trip: every accessible trip's budget items (membership predicate is
// baked into listTrips). Reuses the hydrated list so members/payers come too.
listCostsForUser: (userId) => {
const trips = listTrips(userId, null) as Array<{ id: number }>;
const trips = this.trips.list(userId, null) as Array<{ id: number }>;
return trips.flatMap((t) => this.budget.listBudgetItems(t.id));
},
// Reuses BudgetService.create (frozen FX + members/payers), then broadcasts
@@ -528,7 +529,7 @@ export class PluginHostDepsFactory {
},
createTripForUser: (userId, input) => {
try {
const result = createTrip(userId, input as unknown as Parameters<typeof createTrip>[1]);
const result = this.trips.create(userId, input as unknown as Parameters<TripsService['create']>[1]);
return result.trip;
} catch (e) {
if (e instanceof ValidationError) throw new BadParams(e.message);
@@ -552,7 +553,9 @@ export class PluginHostDepsFactory {
}
const u = this.db.prepare('SELECT role FROM users WHERE id = ?').get(userId) as { role?: string } | undefined;
try {
const result = updateTrip(tripId, userId, input as Parameters<typeof updateTrip>[2], u?.role ?? 'user');
// The no-rebase updateTrip core — parity with the legacy host path,
// which never re-anchored the budget currency.
const result = this.trips.updateTrip(tripId, userId, input as Parameters<TripsService['updateTrip']>[2], u?.role ?? 'user');
this.realtime.broadcast(tripId, 'trip:updated', { trip: result.updatedTrip });
return result.updatedTrip;
} catch (e) {
@@ -564,9 +567,9 @@ export class PluginHostDepsFactory {
// --- Cross-trip reads. The membership predicate is baked into listTrips, so a
// plugin only ever sees the acting user's own trips/reservations (no raw
// cross-tenant SELECT). Reuses the same hydrated services as the REST paths. ---
listTripsForUser: (userId) => listTrips(userId, null),
listTripsForUser: (userId) => this.trips.list(userId, null),
listReservationsForUser: (userId) => {
const trips = listTrips(userId, null) as Array<{ id: number }>;
const trips = this.trips.list(userId, null) as Array<{ id: number }>;
return trips.flatMap((t) => this.reservations.list(String(t.id)));
},
// --- Trip-scoped hydrated reads (membership already checked by tripRead). Same
+2 -1
View File
@@ -36,6 +36,7 @@ import { LlmParseModule } from '../llm-parse/llm-parse.module';
import { FilesModule } from '../files/files.module';
import { CollabModule } from '../collab/collab.module';
import { VacayModule } from '../vacay/vacay.module';
import { TripsModule } from '../trips/trips.module';
import { PermissionsModule } from '../permissions/permissions.module';
import { AuditModule } from '../audit/audit.module';
import { AddonsModule } from '../addons/addons.module';
@@ -50,7 +51,7 @@ import { AddonsModule } from '../addons/addons.module';
@Module({
// The DI-native domain services the plugin host wiring injects
// (PluginHostDepsFactory); DatabaseModule is @Global, so not listed.
imports: [TagsModule, CategoriesModule, BudgetModule, ReservationsModule, TodoModule, PackingModule, DaysModule, AssignmentsModule, LlmParseModule, FilesModule, CollabModule, VacayModule, PermissionsModule, AuditModule, AddonsModule],
imports: [TagsModule, CategoriesModule, BudgetModule, ReservationsModule, TodoModule, PackingModule, DaysModule, AssignmentsModule, LlmParseModule, FilesModule, CollabModule, VacayModule, TripsModule, PermissionsModule, AuditModule, AddonsModule],
controllers: [PluginsController, PluginsFeedController, PluginsProxyController, PluginFrameController, PlaceDetailsController, TripWarningsController, ViewContributionsController, TripCardContributionsController, PluginPhotosController, PluginCalendarController, MapMarkersController, MapLayersController, PluginRoutesController, DayScheduleController, PdfSectionsController, AtlasLayersController, JournalEntryRowsController, PluginUserSettingsController, PluginOAuthController, PluginActivityController],
providers: [PluginsService, PluginRuntimeService, PluginRegistryService, PluginOAuthService, PluginHostDepsFactory],
// Exported so the admin addon-toggle handler can cascade-disable plugins whose
-28
View File
@@ -1,28 +0,0 @@
import { db } from '../../db/database';
import { DatabaseService } from '../database/database.service';
import { RealtimeService } from '../realtime/realtime.service';
import { VacayService } from './vacay.service';
/**
* Non-Nest entry point for the vacay domain — for code running OUTSIDE the
* Nest container (the legacy services/tripService.ts, which shifts a trip
* owner's vacation entries when the trip window moves; the vacay MCP tools and
* resources moved to the DI-discovered vacay.mcp.ts, and the plugin RPC host
* injects VacayService via PluginHostDepsFactory). Exports only the legacy
* services/vacayService names still consumed outside the container, 1:1, so
* repointing a consumer is an import-path-only diff. Inside the container,
* inject VacayService instead. Delete this file when tripService migrates.
*
* Module-level construction is safe: `db` is the reinitialize-proof Proxy onto
* the shared better-sqlite3 singleton.
*/
const vacay = new VacayService(new DatabaseService(db), new RealtimeService());
export function shiftOwnerEntriesForTripWindow(
ownerId: number,
oldStart: string,
oldEnd: string,
newStart: string
): void {
return vacay.shiftOwnerEntriesForTripWindow(ownerId, oldStart, oldEnd, newStart);
}
+3 -2
View File
@@ -155,8 +155,9 @@ function windowEndYear(end: string): number {
* an AbortSignal timeout and the nager.at responses are ok-checked;
* applyHolidayCalendars honors the cache TTL; addYear no longer swallows real
* errors; the holiday cache is instance state instead of a module-level map.
* The only consumer outside the Nest container is the legacy tripService,
* which goes through vacay.bridge.ts instead of importing this class.
* All consumers are in-container since the trip fold (TripsService injects
* this class); vacay.bridge.ts was deleted with its last outside-container
* consumer.
*/
@Injectable()
export class VacayService {
File diff suppressed because it is too large Load Diff
+16 -7
View File
@@ -18,6 +18,7 @@ import { SettingsService } from '../../src/nest/settings/settings.service';
import { FilesService } from '../../src/nest/files/files.service';
import { CollabService } from '../../src/nest/collab/collab.service';
import { VacayService } from '../../src/nest/vacay/vacay.service';
import { TripsService } from '../../src/nest/trips/trips.service';
import { PermissionsService } from '../../src/nest/permissions/permissions.service';
import { AuditService } from '../../src/nest/audit/audit.service';
import { AddonsService } from '../../src/nest/addons/addons.service';
@@ -35,26 +36,34 @@ export function createHostDepsFactory(dbs: DatabaseService): PluginHostDepsFacto
const realtime = new RealtimeService();
const budget = new BudgetService(dbs, permissions, exchangeRates, realtime);
const addons = new AddonsService(dbs);
const todos = new TodoService(dbs, permissions, realtime);
const packing = new PackingService(dbs, permissions, realtime);
const files = new FilesService(dbs, permissions, realtime);
const reservations = new ReservationsService(dbs, permissions, budget, realtime);
const collab = new CollabService(dbs, permissions, realtime);
const vacay = new VacayService(dbs, realtime);
const days = new DaysService(dbs, permissions, realtime);
return new PluginHostDepsFactory(
budget,
new ReservationsService(dbs, permissions, budget, realtime),
reservations,
new TagsService(dbs),
new CategoriesService(dbs),
new TodoService(dbs, permissions, realtime),
new PackingService(dbs, permissions, realtime),
todos,
packing,
new PluginOAuthService(dbs),
new DayNotesService(dbs, permissions, realtime),
new AssignmentsService(dbs, permissions, realtime),
new LlmConfigResolver(new SettingsService(dbs), dbs, addons),
dbs,
new FilesService(dbs, permissions, realtime),
new CollabService(dbs, permissions, realtime),
new VacayService(dbs, realtime),
new DaysService(dbs, permissions, realtime),
files,
collab,
vacay,
days,
permissions,
exchangeRates,
addons,
realtime,
new TripsService(dbs, todos, packing, files, reservations, days, permissions, budget, collab, vacay, realtime),
);
}
+4 -31
View File
@@ -1,7 +1,7 @@
/**
* Unit tests for the DI-native CollabService — COLLAB-SVC-001 to COLLAB-SVC-038
* (001–030 moved 1:1 from the legacy tests/unit/services/collabService.test.ts;
* 031–033 pin the collab.bridge delegation; 034–038 pin the post-migration
* 031–033 (collab.bridge delegation) died with the bridge; 034–038 pin the post-migration
* hardening: transactional writes, trip-scoped getFormattedNoteById, the
* integer vote guard and malformed-URL absorption). Covers votePoll edge
* cases, listMessages pagination, deleteMessage ownership, updateNote partial
@@ -61,7 +61,6 @@ import { avatarUrl } from '../../../src/services/avatarUrl';
import { DatabaseService } from '../../../src/nest/database/database.service';
import { PermissionsService } from '../../../src/nest/permissions/permissions.service';
import { CollabService } from '../../../src/nest/collab/collab.service';
import { listNotes as bridgeListNotes, listPolls as bridgeListPolls, countMessages as bridgeCountMessages } from '../../../src/nest/collab/collab.bridge';
import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
const svc = new CollabService(new DatabaseService(testDb), new PermissionsService(new DatabaseService(testDb)), new RealtimeService());
@@ -406,35 +405,9 @@ describe('linkPreview', () => {
});
});
// ── collab.bridge delegation (out-of-container consumers) ─────────────────────
describe('collab.bridge', () => {
it('COLLAB-SVC-031: listNotes delegates to CollabService over the shared db', () => {
const { user1, trip } = setup();
svc.createNote(trip.id, user1.id, { title: 'Bridged note' });
const notes = bridgeListNotes(trip.id);
expect(notes).toHaveLength(1);
expect(notes[0].title).toBe('Bridged note');
});
it('COLLAB-SVC-032: listPolls delegates to CollabService over the shared db', () => {
const { user1, trip } = setup();
svc.createPoll(trip.id, user1.id, { question: 'Bridged?', options: ['A', 'B'] });
const polls = bridgeListPolls(trip.id);
expect(polls).toHaveLength(1);
expect(polls[0]!.question).toBe('Bridged?');
});
it('COLLAB-SVC-033: countMessages delegates to CollabService over the shared db', () => {
const { user1, trip } = setup();
svc.createMessage(trip.id, user1.id, 'one');
svc.createMessage(trip.id, user1.id, 'two');
expect(bridgeCountMessages(trip.id)).toBe(2);
});
});
// COLLAB-SVC-031..033 (collab.bridge delegation) were deleted with the bridge —
// its last consumers (the legacy tripService and the legacy get_trip_summary
// registrar) migrated into the DI-native TripsService/TripsMcp.
// ── Post-migration hardening (transactions, scoping, guards) ──────────────────
+9 -11
View File
@@ -52,13 +52,7 @@ import { createUser, createTrip, createDay, createPlace, createDayAssignment, cr
import { DatabaseService } from '../../../src/nest/database/database.service';
import { PermissionsService } from '../../../src/nest/permissions/permissions.service';
import { DaysService, addDays } from '../../../src/nest/days/days.service';
import {
getDay as bridgeGetDay,
listDays as bridgeListDays,
listAccommodations as bridgeListAccommodations,
restampReservationDates as bridgeRestampReservationDates,
resyncAccommodationDays as bridgeResyncAccommodationDays,
} from '../../../src/nest/days/days.bridge';
import { getDay as bridgeGetDay, listDays as bridgeListDays } from '../../../src/nest/days/days.bridge';
import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
const svc = new DaysService(new DatabaseService(testDb), new PermissionsService(new DatabaseService(testDb)), new RealtimeService());
@@ -406,6 +400,10 @@ describe('deleteAccommodation', () => {
});
// ── days.bridge delegation (out-of-container consumers) ───────────────────────
// The listAccommodations / restampReservationDates / resyncAccommodationDays /
// addDays bridge exports were pruned when their last outside-container
// consumer (legacy tripService) folded into the DI-native TripsService —
// 029/031/032 pin the same behavior on the service.
describe('days.bridge', () => {
it('DAY-SVC-027 — getDay delegates to DaysService.getDay', () => {
@@ -425,13 +423,13 @@ describe('days.bridge', () => {
expect(Array.isArray(result.days[0].assignments)).toBe(true);
});
it('DAY-SVC-029 — listAccommodations delegates to DaysService.listAccommodations', () => {
it('DAY-SVC-029 — listAccommodations returns the hydrated stays', () => {
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 = bridgeListAccommodations(trip.id) as { place_name: string }[];
const rows = svc.listAccommodations(trip.id) as { place_name: string }[];
expect(rows).toHaveLength(1);
expect(rows[0].place_name).toBe('Ryokan');
});
@@ -450,7 +448,7 @@ describe('days.bridge', () => {
'INSERT INTO reservations (trip_id, day_id, title, reservation_time) VALUES (?, ?, ?, ?)'
).run(trip.id, day.id, 'Dinner', '2026-01-01T19:00');
bridgeRestampReservationDates(
svc.restampReservationDates(
trip.id,
new Map([[day.id, '2026-01-01']]),
new Map([[day.id, '2026-01-05']]),
@@ -475,7 +473,7 @@ describe('days.bridge', () => {
testDb.prepare('UPDATE days SET date = ? WHERE id = ?').run('2026-01-01', d2.id);
testDb.prepare('UPDATE days SET day_number = 0 WHERE id = ?').run(d2.id);
bridgeResyncAccommodationDays(trip.id, new Map([[d1.id, '2026-01-01'], [d2.id, '2026-01-02']]));
svc.resyncAccommodationDays(trip.id, new Map([[d1.id, '2026-01-01'], [d2.id, '2026-01-02']]));
const row = testDb.prepare('SELECT start_day_id, end_day_id FROM day_accommodations WHERE id = ?').get(accom.id) as { start_day_id: number; end_day_id: number };
expect(row.start_day_id).toBe(d2.id);
+3 -17
View File
@@ -35,14 +35,14 @@ import { createUser } from '../../helpers/factories';
import { DatabaseService } from '../../../src/nest/database/database.service';
import { VacayService } from '../../../src/nest/vacay/vacay.service';
import { shiftOwnerEntriesForTripWindow } from '../../../src/nest/vacay/vacay.bridge';
import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
// VACAY-SVC-001 through VACAY-SVC-066 moved 1:1 from the legacy
// tests/unit/services/vacayService.test.ts (the named-function imports became
// method calls on a directly constructed VacayService; the legacy
// updateUserYearSettings is the class's updateYearSettings). VACAY-SVC-067
// pins the vacay.bridge delegation for the one outside-container consumer.
// updateUserYearSettings is the class's updateYearSettings).
// VACAY-SVC-067 (vacay.bridge delegation) died with the bridge — its only
// consumer, the legacy tripService, folded into the DI-native TripsService.
const svc = new VacayService(new DatabaseService(testDb), new RealtimeService());
// ── Lifecycle ─────────────────────────────────────────────────────────────────
@@ -1367,20 +1367,6 @@ describe('getSharedCalendars', () => {
});
});
// ── vacay.bridge delegation ───────────────────────────────────────────────────
describe('vacay.bridge', () => {
it('VACAY-SVC-067: shiftOwnerEntriesForTripWindow shifts the owner entries through the bridge', () => {
const { user, plan } = setupUserWithPlan();
svc.toggleEntry(user.id, plan.id, '2025-05-10', 1);
shiftOwnerEntriesForTripWindow(user.id, '2025-05-08', '2025-05-12', '2025-05-15');
const dates = testDb.prepare('SELECT date FROM vacay_entries WHERE plan_id = ? ORDER BY date').all(plan.id);
expect(dates).toEqual([{ date: '2025-05-17' }]);
});
});
// ── Quirk fixes (transactions, fetch hygiene, cache TTL, addYear errors) ──────
describe('quirk fixes', () => {
@@ -66,27 +66,10 @@ const budgetStub = {
const checkPermission = vi.fn((..._a: any[]) => true as boolean);
const permissionsStub = { checkPermission } as unknown as PermissionsService;
// The core write services are delegated to; mock them so the host deps'
// wiring + error branches run without the full core schema. The error classes must
// be defined INSIDE the factory (vi.mock is hoisted above module-scope code).
vi.mock('../../../src/services/tripService', () => {
class NotFoundError extends Error {}
class ValidationError extends Error {}
return {
updateTrip: (tripId: number, _u: number, input: Record<string, unknown>) => {
if (input.title === 'boom') throw new ValidationError('bad dates');
if (input.title === 'gone') throw new NotFoundError('no trip');
if (input.title === 'crash') throw new Error('unexpected');
return { updatedTrip: { id: tripId, ...input } };
},
createTrip: (userId: number, input: Record<string, unknown>) => {
if (input.title === 'boom') throw new ValidationError('bad dates');
return { trip: { id: 99, user_id: userId, ...input }, tripId: 99, reminderDays: 3 };
},
listTrips: () => [{ id: 1 }],
NotFoundError, ValidationError,
};
});
// Trips are a constructor-injected stub since the trip fold (same behaviors as
// the old services/tripService path mock). The throws use the REAL error
// classes from trips.service so the factory's instanceof mapping keeps
// matching — see the imports below the mocks.
// Exchange rates are a constructor-injected stub since the budget-domain fold
// (was a path mock of the deleted services/exchangeRateService — same behavior).
const exchangeRatesStub = {
@@ -96,6 +79,9 @@ vi.mock('../../../src/services/placeService', () => ({
createPlace: vi.fn((tid: string, body: Record<string, unknown>) => ({ id: 10, trip_id: Number(tid), ...body })),
updatePlace: vi.fn((_tid: string, pid: string) => (pid === '99' ? null : { id: Number(pid) })),
deletePlace: vi.fn((_tid: string, pid: string) => pid !== '99'),
// trips.service (loaded for real since the trip fold) imports listPlaces for
// its offline bundle — the mock must carry every named export it pulls.
listPlaces: vi.fn(() => []),
}));
// Days are a constructor-injected stub (same behaviors as the old path mock,
// mapped onto the DaysService method names).
@@ -320,12 +306,27 @@ import type { VacayService } from '../../../src/nest/vacay/vacay.service';
import type { PermissionsService } from '../../../src/nest/permissions/permissions.service';
import { DatabaseService } from '../../../src/nest/database/database.service';
import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
import { NotFoundError, ValidationError } from '../../../src/nest/trips/trips.service';
// The factory under test, wired exactly like PluginsModule does — but with the
// DI-native domain services replaced by the stubs above. The shim keeps the
// ~45 historical call sites unchanged and supplies a default no-op router.
const addonsStub = { isAddonEnabled } as unknown as import('../../../src/nest/addons/addons.service').AddonsService;
const factory = new PluginHostDepsFactory(budgetStub, reservationsStub, tagsStub, categoriesStub, todoStub, packingStub, oauthStub, dayNotesStub, assignmentsStub, llmConfigStub, new DatabaseService(mockDb), filesStub, collabStub, vacayStub, daysStub, permissionsStub, exchangeRatesStub, addonsStub, new RealtimeService());
const tripsStub = {
updateTrip: (tripId: number, _u: number, input: Record<string, unknown>) => {
if (input.title === 'boom') throw new ValidationError('bad dates');
if (input.title === 'gone') throw new NotFoundError('no trip');
if (input.title === 'crash') throw new Error('unexpected');
return { updatedTrip: { id: tripId, ...input } };
},
create: (userId: number, input: Record<string, unknown>) => {
if (input.title === 'boom') throw new ValidationError('bad dates');
return { trip: { id: 99, user_id: userId, ...input }, tripId: 99, reminderDays: 3 };
},
list: () => [{ id: 1 }],
removeMember: vi.fn(),
} as unknown as import('../../../src/nest/trips/trips.service').TripsService;
const factory = new PluginHostDepsFactory(budgetStub, reservationsStub, tagsStub, categoriesStub, todoStub, packingStub, oauthStub, dayNotesStub, assignmentsStub, llmConfigStub, new DatabaseService(mockDb), filesStub, collabStub, vacayStub, daysStub, permissionsStub, exchangeRatesStub, addonsStub, new RealtimeService(), tripsStub);
const stubRouter: PluginCallRouter = { callPlugin: async () => undefined, emitPluginEvent: () => {} };
const createRealRpcHost = (id: string, granted: ReadonlySet<string>, router: PluginCallRouter = stubRouter) => factory.create(id, granted, router);