chore: move vacay backend to proper DI

This commit is contained in:
jubnl
2026-07-27 18:39:52 +02:00
parent 29e89a5774
commit 588457710c
20 changed files with 2127 additions and 2119 deletions
+2 -37
View File
@@ -7,7 +7,6 @@ import { listBudgetItems, getPerPersonSummary, calculateSettlement } from '../se
import { listReservations } from '../services/reservationService';
import { listBucketList, listVisitedCountries, getStats as getAtlasStats, listManuallyVisitedRegions } from '../services/atlasService';
import { getNotifications } from '../services/inAppNotifications';
import { getActivePlanId, getActivePlan, getPlanData, getEntries as getVacayEntries, getHolidays } from '../services/vacayService';
import { isAddonEnabled } from '../services/adminService';
import { ADDON_IDS } from '../addons';
import { canAccessJourney, getJourneyFull, listEntries, listJourneys } from '../services/journeyService';
@@ -258,42 +257,8 @@ export function registerResources(server: McpServer, userId: number, scopes: str
);
}
// Vacay resources (addon-gated)
if (isAddonEnabled(ADDON_IDS.VACAY) && canRead(scopes, 'vacay')) {
server.registerResource(
'vacay-plan',
'trek://vacay/plan',
{ description: "Full snapshot of the user's active vacation plan (members, years, settings)", mimeType: 'application/json' },
async (uri) => {
const plan = getPlanData(userId);
return jsonContent(uri.href, plan);
}
);
server.registerResource(
'vacay-entries',
new ResourceTemplate('trek://vacay/entries/{year}', { list: undefined }),
{ description: 'All vacation entries for the active plan and a specific year', mimeType: 'application/json' },
async (uri, { year }) => {
const planId = getActivePlanId(userId);
const entries = getVacayEntries(planId, Array.isArray(year) ? year[0] : year, userId);
return jsonContent(uri.href, entries);
}
);
server.registerResource(
'vacay-holidays',
new ResourceTemplate('trek://vacay/holidays/{year}', { list: undefined }),
{ description: "Cached public holidays for the plan's configured region and year", mimeType: 'application/json' },
async (uri, { year }) => {
const plan = getActivePlan(userId);
if (!plan.holidays_enabled || !plan.holidays_region) return jsonContent(uri.href, []);
const yearStr = Array.isArray(year) ? year[0] : year;
const result = await getHolidays(yearStr, plan.holidays_region);
return jsonContent(uri.href, result.data ?? []);
}
);
}
// The vacay resources moved to the DI-discovered src/nest/vacay/vacay.mcp.ts
// (@McpController, attached via the nest-mcp registry in tools.ts).
// Journey resources (Journey addon)
if (isAddonEnabled(ADDON_IDS.JOURNEY) && canRead(scopes, 'journey')) {
+2 -2
View File
@@ -11,7 +11,6 @@ import { registerBudgetTools } from './tools/budget';
import { registerTripTools } from './tools/trips';
import { registerTransportTools } from './tools/transports';
import { registerTransitTools } from './tools/transit';
import { registerVacayTools } from './tools/vacay';
import { registerMcpPrompts } from './tools/prompts';
import { getMcpRegistry } from './registry-handoff';
@@ -49,7 +48,8 @@ export function registerTools(server: McpServer, userId: number, scopes: string[
registerJourneyTools(server, userId, scopes);
registerVacayTools(server, userId, scopes);
// The vacay tools moved to the DI-discovered src/nest/vacay/vacay.mcp.ts
// (@McpController, attached via the nest-mcp registry below).
// The todo tools moved to the DI-discovered src/nest/todo/todo.mcp.ts
// (@McpController, attached via the nest-mcp registry below).
-465
View File
@@ -1,465 +0,0 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
import { z } from 'zod';
import { isDemoUser, getCurrentUser } from '../../services/authService';
import {
getOwnPlan, getActivePlan, getActivePlanId, getPlanData,
updatePlan, setUserColor,
sendInvite as sendVacayInvite, acceptInvite, declineInvite, cancelInvite, dissolvePlan,
getAvailableUsers,
listYears, addYear, deleteYear,
getEntries as getVacayEntries, toggleEntry, toggleCompanyHoliday,
getStats as getVacayStats, updateStats as updateVacayStats,
addHolidayCalendar, updateHolidayCalendar, deleteHolidayCalendar,
getCountries as getHolidayCountries, getHolidays,
listShares, shareCalendar, removeShare, getSharedCalendars,
} from '../../services/vacayService';
import { isAddonEnabled } from '../../services/adminService';
import { ADDON_IDS } from '../../addons';
import {
TOOL_ANNOTATIONS_READONLY, TOOL_ANNOTATIONS_WRITE,
TOOL_ANNOTATIONS_DELETE, TOOL_ANNOTATIONS_NON_IDEMPOTENT,
demoDenied, ok,
} from './_shared';
import { canRead, canWrite } from '../scopes';
export function registerVacayTools(server: McpServer, userId: number, scopes: string[] | null): void {
const R = canRead(scopes, 'vacay');
const W = canWrite(scopes, 'vacay');
if (isAddonEnabled(ADDON_IDS.VACAY)) {
if (R) server.registerTool(
'get_vacay_plan',
{
description: "Get the current user's active vacation plan (own or joined).",
inputSchema: {},
annotations: TOOL_ANNOTATIONS_READONLY,
},
async () => {
const plan = getPlanData(userId);
return ok({ plan });
}
);
if (W) server.registerTool(
'update_vacay_plan',
{
description: 'Update vacation plan settings (weekends blocking, holidays, carry-over).',
inputSchema: {
block_weekends: z.boolean().optional(),
holidays_enabled: z.boolean().optional(),
holidays_region: z.string().nullable().optional(),
company_holidays_enabled: z.boolean().optional(),
carry_over_enabled: z.boolean().optional(),
},
annotations: TOOL_ANNOTATIONS_WRITE,
},
async ({ block_weekends, holidays_enabled, holidays_region, company_holidays_enabled, carry_over_enabled }) => {
if (isDemoUser(userId)) return demoDenied();
const planId = getActivePlanId(userId);
// updatePlan already returns the fully-hydrated { plan }; surface it so the
// AI consumer sees the updated plan, matching get_vacay_plan.
const result = await updatePlan(planId, { block_weekends, holidays_enabled, holidays_region, company_holidays_enabled, carry_over_enabled }, undefined);
return ok(result);
}
);
if (W) server.registerTool(
'set_vacay_color',
{
description: "Set the current user's color in the vacation plan calendar.",
inputSchema: {
color: z.string().describe('Hex color e.g. #6366f1'),
},
annotations: TOOL_ANNOTATIONS_WRITE,
},
async ({ color }) => {
if (isDemoUser(userId)) return demoDenied();
const planId = getActivePlanId(userId);
setUserColor(userId, planId, color, undefined);
// Echo the persisted color (mirrors the service default) so the AI consumer sees what was set.
return ok({ success: true, color: color || '#6366f1' });
}
);
if (R) server.registerTool(
'get_available_vacay_users',
{
description: 'List users who can be invited to the current vacation plan.',
inputSchema: {},
annotations: TOOL_ANNOTATIONS_READONLY,
},
async () => {
const planId = getActivePlanId(userId);
const users = getAvailableUsers(userId, planId);
return ok({ users });
}
);
if (W) server.registerTool(
'send_vacay_invite',
{
description: 'Invite a user to join the vacation plan by their user ID.',
inputSchema: {
targetUserId: z.number().int().positive(),
},
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
},
async ({ targetUserId }) => {
if (isDemoUser(userId)) return demoDenied();
const planId = getActivePlanId(userId);
const me = getCurrentUser(userId);
if (!me) return { content: [{ type: 'text' as const, text: 'User not found.' }], isError: true };
const result = sendVacayInvite(planId, userId, me.username, me.email, targetUserId);
if (result.error) return { content: [{ type: 'text' as const, text: result.error }], isError: true };
return ok({ success: true });
}
);
if (W) server.registerTool(
'accept_vacay_invite',
{
description: 'Accept a pending invitation to join another user\'s vacation plan.',
inputSchema: {
planId: z.number().int().positive(),
},
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
},
async ({ planId }) => {
if (isDemoUser(userId)) return demoDenied();
const result = acceptInvite(userId, planId, undefined);
if (result.error) return { content: [{ type: 'text' as const, text: result.error }], isError: true };
return ok({ success: true });
}
);
if (W) server.registerTool(
'decline_vacay_invite',
{
description: 'Decline a pending vacation plan invitation.',
inputSchema: {
planId: z.number().int().positive(),
},
annotations: TOOL_ANNOTATIONS_WRITE,
},
async ({ planId }) => {
declineInvite(userId, planId, undefined);
return ok({ success: true });
}
);
if (W) server.registerTool(
'cancel_vacay_invite',
{
description: 'Cancel an outgoing invitation (owner cancels invite they sent).',
inputSchema: {
targetUserId: z.number().int().positive(),
},
annotations: TOOL_ANNOTATIONS_WRITE,
},
async ({ targetUserId }) => {
if (isDemoUser(userId)) return demoDenied();
const planId = getActivePlanId(userId);
cancelInvite(planId, targetUserId);
return ok({ success: true });
}
);
if (W) server.registerTool(
'dissolve_vacay_plan',
{
description: 'Dissolve the shared plan — all members are removed and everyone returns to their own individual plan.',
inputSchema: {},
annotations: TOOL_ANNOTATIONS_DELETE,
},
async () => {
if (isDemoUser(userId)) return demoDenied();
dissolvePlan(userId, undefined);
return ok({ success: true });
}
);
if (R) server.registerTool(
'list_vacay_years',
{
description: 'List calendar years tracked in the current vacation plan.',
inputSchema: {},
annotations: TOOL_ANNOTATIONS_READONLY,
},
async () => {
const planId = getActivePlanId(userId);
const years = listYears(planId);
return ok({ years });
}
);
if (W) server.registerTool(
'add_vacay_year',
{
description: 'Add a calendar year to the vacation plan.',
inputSchema: {
year: z.number().int().min(2000).max(2100),
},
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
},
async ({ year }) => {
if (isDemoUser(userId)) return demoDenied();
const planId = getActivePlanId(userId);
const years = addYear(planId, year, undefined);
return ok({ years });
}
);
if (W) server.registerTool(
'delete_vacay_year',
{
description: 'Remove a calendar year from the vacation plan.',
inputSchema: {
year: z.number().int(),
},
annotations: TOOL_ANNOTATIONS_DELETE,
},
async ({ year }) => {
if (isDemoUser(userId)) return demoDenied();
const planId = getActivePlanId(userId);
const years = deleteYear(planId, year, undefined);
return ok({ years });
}
);
if (R) server.registerTool(
'get_vacay_entries',
{
description: 'Get all vacation day entries for a plan and year.',
inputSchema: {
year: z.number().int(),
},
annotations: TOOL_ANNOTATIONS_READONLY,
},
async ({ year }) => {
const planId = getActivePlanId(userId);
const entries = getVacayEntries(planId, String(year), userId);
return ok({ entries });
}
);
if (W) server.registerTool(
'toggle_vacay_entry',
{
description: 'Toggle a day on or off as a vacation day for the current user.',
inputSchema: {
date: z.string().describe('ISO date YYYY-MM-DD'),
},
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
},
async ({ date }) => {
if (isDemoUser(userId)) return demoDenied();
const planId = getActivePlanId(userId);
const result = toggleEntry(userId, planId, date, 1, 'vacation', undefined);
return ok(result);
}
);
if (W) server.registerTool(
'toggle_company_holiday',
{
description: 'Toggle a date as a company holiday for the whole plan.',
inputSchema: {
date: z.string(),
note: z.string().optional(),
},
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
},
async ({ date, note }) => {
if (isDemoUser(userId)) return demoDenied();
const planId = getActivePlanId(userId);
const result = toggleCompanyHoliday(planId, date, note, undefined);
return ok(result);
}
);
if (R) server.registerTool(
'get_vacay_stats',
{
description: 'Get vacation statistics for a specific year (days used, remaining, carried over).',
inputSchema: {
year: z.number().int(),
},
annotations: TOOL_ANNOTATIONS_READONLY,
},
async ({ year }) => {
const planId = getActivePlanId(userId);
const stats = getVacayStats(planId, year);
return ok({ stats });
}
);
if (W) server.registerTool(
'update_vacay_stats',
{
description: 'Update the vacation day allowance for a specific user and year.',
inputSchema: {
year: z.number().int(),
vacationDays: z.number().int().min(0),
},
annotations: TOOL_ANNOTATIONS_WRITE,
},
async ({ year, vacationDays }) => {
if (isDemoUser(userId)) return demoDenied();
const planId = getActivePlanId(userId);
updateVacayStats(userId, planId, year, vacationDays, undefined);
return ok({ success: true });
}
);
if (W) server.registerTool(
'add_holiday_calendar',
{
description: 'Add a public holiday calendar (by region code) to the vacation plan.',
inputSchema: {
region: z.string().describe('Country/region code e.g. US, GB, DE'),
label: z.string().nullable().optional(),
color: z.string().optional(),
sortOrder: z.number().int().optional(),
},
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
},
async ({ region, label, color, sortOrder }) => {
if (isDemoUser(userId)) return demoDenied();
const planId = getActivePlanId(userId);
const calendar = addHolidayCalendar(planId, region, label ?? null, color, sortOrder, undefined);
return ok({ calendar });
}
);
if (W) server.registerTool(
'update_holiday_calendar',
{
description: 'Update label or color for a holiday calendar.',
inputSchema: {
calendarId: z.number().int().positive(),
label: z.string().nullable().optional(),
color: z.string().optional(),
},
annotations: TOOL_ANNOTATIONS_WRITE,
},
async ({ calendarId, label, color }) => {
if (isDemoUser(userId)) return demoDenied();
const planId = getActivePlanId(userId);
const cal = updateHolidayCalendar(calendarId, planId, { label, color }, undefined);
if (!cal) return { content: [{ type: 'text' as const, text: 'Holiday calendar not found.' }], isError: true };
return ok({ calendar: cal });
}
);
if (W) server.registerTool(
'delete_holiday_calendar',
{
description: 'Remove a holiday calendar from the vacation plan.',
inputSchema: {
calendarId: z.number().int().positive(),
},
annotations: TOOL_ANNOTATIONS_DELETE,
},
async ({ calendarId }) => {
if (isDemoUser(userId)) return demoDenied();
const planId = getActivePlanId(userId);
deleteHolidayCalendar(calendarId, planId, undefined);
return ok({ success: true });
}
);
if (R) server.registerTool(
'list_holiday_countries',
{
description: 'List countries available for public holiday calendars.',
inputSchema: {},
annotations: TOOL_ANNOTATIONS_READONLY,
},
async () => {
const result = await getHolidayCountries();
if (result.error) return { content: [{ type: 'text' as const, text: result.error }], isError: true };
return ok({ countries: result.data });
}
);
if (R) server.registerTool(
'list_holidays',
{
description: 'List public holidays for a country and year.',
inputSchema: {
country: z.string().describe('ISO 3166-1 alpha-2 code'),
year: z.number().int(),
},
annotations: TOOL_ANNOTATIONS_READONLY,
},
async ({ country, year }) => {
const result = await getHolidays(String(year), country);
if (result.error) return { content: [{ type: 'text' as const, text: result.error }], isError: true };
return ok({ holidays: result.data });
}
);
if (R) server.registerTool(
'list_vacay_shares',
{
description: 'List read-only calendar shares: who the current user shares their vacation calendar with, and which calendars are shared with them.',
inputSchema: {},
annotations: TOOL_ANNOTATIONS_READONLY,
},
async () => {
return ok(listShares(userId));
}
);
if (W) server.registerTool(
'share_vacay_calendar',
{
description: "Share the current user's vacation calendar with another user (view only, no merge).",
inputSchema: {
targetUserId: z.number().int().positive(),
},
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
},
async ({ targetUserId }) => {
if (isDemoUser(userId)) return demoDenied();
const me = getCurrentUser(userId);
if (!me) return { content: [{ type: 'text' as const, text: 'User not found.' }], isError: true };
const result = shareCalendar(userId, me.email, targetUserId);
if (result.error) return { content: [{ type: 'text' as const, text: result.error }], isError: true };
return ok({ success: true });
}
);
if (W) server.registerTool(
'unshare_vacay_calendar',
{
description: 'Remove a read-only calendar share the current user is part of (revoke as owner, or remove a calendar shared with them).',
inputSchema: {
shareId: z.number().int().positive(),
},
annotations: TOOL_ANNOTATIONS_DELETE,
},
async ({ shareId }) => {
if (isDemoUser(userId)) return demoDenied();
if (!removeShare(shareId, userId, undefined)) {
return { content: [{ type: 'text' as const, text: 'Share not found.' }], isError: true };
}
return ok({ success: true });
}
);
if (R) server.registerTool(
'get_shared_vacay_calendars',
{
description: 'Get the read-only vacation calendars shared with the current user for a year (entries and company holidays per sharer).',
inputSchema: {
year: z.number().int(),
},
annotations: TOOL_ANNOTATIONS_READONLY,
},
async ({ year }) => {
return ok({ calendars: getSharedCalendars(userId, String(year)) });
}
);
}
}
+9 -3
View File
@@ -28,7 +28,7 @@ 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 — see the migration recipe below.
collab, vacay — see the migration recipe below.
## Cross-cutting Foundation pieces
@@ -129,9 +129,15 @@ outside DI — while every function consumer, including the plugin host and
addon AND the per-sub-feature `getCollabFeatures()` flags (notes/polls/chat)
that the legacy registrar and resources checked at registration time — plus
the plugin-host swap and a 3-export `collab.bridge.ts` for the two remaining
legacy consumers, `tripService`'s trip summary and `mcp/tools/trips.ts`).
legacy consumers, `tripService`'s trip summary and `mcp/tools/trips.ts`);
vacay followed (the largest MCP port yet — a 26-tool + 3-resource
`vacay.mcp.ts`, including the first fixed-URI `@Resource` behind a `when`
addon gate — plus the plugin-host swap and a 1-export `vacay.bridge.ts` for
`tripService`'s trip-window entry shift; the DTO ratchet for its 13
grandfathered body contracts landed as a sibling commit).
Repeat these steps per
service (next up: vacayService). This is a **pure relocation** — byte-identical
service (next up: reservationService, the residue fold). 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
injects domain services via `PluginHostDepsFactory`, so a migrated domain adds
+5 -3
View File
@@ -108,8 +108,10 @@ but its `resolveLlmConfig` import became the injected `LlmConfigResolver`;
fileService swapped in 2026-07 — its ten imported symbols became the injected
`FilesService` plus the load-time constants from `files.constants.ts`;
collabService swapped in 2026-07 — its seven imported symbols became the
injected `CollabService`, completing Wave 3; the remaining factory imports are
all Wave-4+ domains: trips, places, days, journeys, atlas, vacay, collections).
injected `CollabService`, completing Wave 3; vacayService swapped in 2026-07 —
its four imported symbols became the injected `VacayService`; the remaining
factory imports are all Wave-4+ domains: trips, places, days, journeys, atlas,
collections).
### Test impact (as landed)
@@ -117,7 +119,7 @@ all Wave-4+ domains: trips, places, days, journeys, atlas, vacay, 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 (nine stubs as of the 2026-07 assignments migration); the ~25
stubs (eleven stubs as of the 2026-07 vacay migration); the ~24
legacy-service path mocks remain; a file-local shim keeps the
historical `createRealRpcHost(id, granted)` call sites and supplies a default
no-op router.
@@ -23,7 +23,6 @@ import { isDemoEmail } from '../../../services/demo';
import { ADDON_IDS } from '../../../addons';
import { listJourneys, listEntries as listJournalEntriesSvc, createEntry as createJournalEntrySvc, updateEntry as updateJournalEntrySvc, deleteEntry as deleteJournalEntrySvc, createJourney as createJourneySvc, deleteJourney as deleteJourneySvc } from '../../../services/journeyService';
import { listVisitedCountries, listManuallyVisitedRegions, listBucketList, markCountryVisited, unmarkCountryVisited, markRegionVisited, unmarkRegionVisited, createBucketItem as createBucketItemSvc, deleteBucketItem as deleteBucketItemSvc } from '../../../services/atlasService';
import { getPlanData, getActivePlanId, toggleEntry as vacayToggleEntrySvc, toggleCompanyHoliday as vacayToggleCompanyHolidaySvc } from '../../../services/vacayService';
import { listCollections, getCollection, createCollection, updateCollection, savePlace as saveCollectionPlaceSvc, copyToTrip as copyCollectionToTripSvc, deletePlace as deleteCollectionPlaceSvc } from '../../../services/collectionsService';
import { BudgetService } from '../../budget/budget.service';
import { ReservationsService } from '../../reservations/reservations.service';
@@ -37,6 +36,7 @@ import { LlmConfigResolver } from '../../llm-parse/llm-config.resolver';
import { DatabaseService } from '../../database/database.service';
import { FilesService } from '../../files/files.service';
import { CollabService } from '../../collab/collab.service';
import { VacayService } from '../../vacay/vacay.service';
import { notifyBookingChange } from '../../../services/reservationService';
import { PluginRpcHost, ForbiddenResource, BadParams } from './rpc-host';
import { appendAudit } from './plugin-audit';
@@ -137,8 +137,8 @@ export interface PluginCallRouter {
* `plugin:{id}:{event}` so a plugin can't forge a core event.
*
* DI-native domain services (budget, reservations, tags, categories, todo,
* packing, day-notes, assignments, oauth, files, collab, the LLM config
* resolver) and
* packing, day-notes, assignments, oauth, files, collab, vacay, 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
@@ -160,6 +160,7 @@ export class PluginHostDepsFactory {
private readonly db: DatabaseService,
private readonly files: FilesService,
private readonly collab: CollabService,
private readonly vacay: VacayService,
) {}
/**
@@ -625,7 +626,7 @@ export class PluginHostDepsFactory {
return { countries: listVisitedCountries(userId), regions: listManuallyVisitedRegions(userId) };
},
atlasBucketForUser: (userId) => { requireAddon(ADDON_IDS.ATLAS, 'atlas'); return listBucketList(userId) as unknown[]; },
vacayForUser: (userId) => { requireAddon(ADDON_IDS.VACAY, 'vacay'); return getPlanData(userId); },
vacayForUser: (userId) => { requireAddon(ADDON_IDS.VACAY, 'vacay'); return this.vacay.getPlanData(userId); },
listCollectionsForUser: (userId) => { requireAddon(ADDON_IDS.COLLECTIONS, 'collections'); return listCollections(userId); },
getCollectionForUser: (userId, id) => { requireAddon(ADDON_IDS.COLLECTIONS, 'collections'); return getCollection(userId, id); },
// --- Collections write. The service self-gates per-collection role (assertAccess/
@@ -668,10 +669,10 @@ export class PluginHostDepsFactory {
},
// --- Vacay write: the plan is the ACTING USER's active plan (resolved host-side);
// the service broadcasts to plan users itself. ---
vacayToggleEntry: (userId, date) => { requireAddon(ADDON_IDS.VACAY, 'vacay'); return vacayToggleEntrySvc(userId, getActivePlanId(userId), date, 1, 'vacation', undefined); },
vacayToggleEntry: (userId, date) => { requireAddon(ADDON_IDS.VACAY, 'vacay'); return this.vacay.toggleEntry(userId, this.vacay.getActivePlanId(userId), date, 1, 'vacation', undefined); },
vacayToggleCompanyHoliday: (userId, date, note) => {
requireAddon(ADDON_IDS.VACAY, 'vacay');
return vacayToggleCompanyHolidaySvc(getActivePlanId(userId), date, note, undefined);
return this.vacay.toggleCompanyHoliday(this.vacay.getActivePlanId(userId), date, note, undefined);
},
// --- Journal write: journeyService.canEdit self-gates each call (owner/contributor). ---
createJournalEntry: (userId, journeyId, input) => {
+2 -1
View File
@@ -35,6 +35,7 @@ import { AssignmentsModule } from '../assignments/assignments.module';
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';
/**
* Plugin system (#plugins). M0 read side + M2 isolated runtime + M3 frontend:
@@ -46,7 +47,7 @@ import { CollabModule } from '../collab/collab.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],
imports: [TagsModule, CategoriesModule, BudgetModule, ReservationsModule, TodoModule, PackingModule, DaysModule, AssignmentsModule, LlmParseModule, FilesModule, CollabModule, VacayModule],
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
+27
View File
@@ -0,0 +1,27 @@
import { db } from '../../db/database';
import { DatabaseService } from '../database/database.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));
export function shiftOwnerEntriesForTripWindow(
ownerId: number,
oldStart: string,
oldEnd: string,
newStart: string
): void {
return vacay.shiftOwnerEntriesForTripWindow(ownerId, oldStart, oldEnd, newStart);
}
+544
View File
@@ -0,0 +1,544 @@
import {
McpController, Tool, Resource, ResourceTemplate, type McpContext,
TOOL_ANNOTATIONS_READONLY, TOOL_ANNOTATIONS_WRITE,
TOOL_ANNOTATIONS_DELETE, TOOL_ANNOTATIONS_NON_IDEMPOTENT,
demoDenied, errorResult, ok,
} from '@trek/nest-mcp';
import { z } from 'zod';
import { isDemoUser, getCurrentUser } from '../../services/authService';
import { isAddonEnabled } from '../../services/adminService';
import { ADDON_IDS } from '../../addons';
import { VacayService } from './vacay.service';
/** Legacy registrar gate: the whole vacay surface rides the vacay addon. */
const vacayAddonOn = () => isAddonEnabled(ADDON_IDS.VACAY);
/**
* Vacay MCP surface — ported 1:1 from the legacy registrars: the 26 tools from
* src/mcp/tools/vacay.ts and the trek://vacay/{plan,entries,holidays} resources
* from src/mcp/resources.ts (identical names, descriptions, schemas,
* annotations and error/payload shapes; broadcasts happen inside VacayService,
* driven by the socketId argument every tool passes as undefined). The
* registration-time gates map to `when` (the whole-registrar vacay-addon check)
* plus the declarative vacay read/write access markers (the legacy `if (R)` /
* `if (W)` checks, resolved by trekMcpAccessPolicy). Vacay is plan-scoped, not
* trip-scoped, so there is no trip-permission layer; the only per-call guard is
* the demo-user check — which decline_vacay_invite deliberately lacks, exactly
* like the legacy registrar (a quirk preserved for the fix commit).
*/
@McpController()
export class VacayMcp {
constructor(private readonly vacay: VacayService) {}
@Tool({
name: 'get_vacay_plan',
description: "Get the current user's active vacation plan (own or joined).",
inputSchema: {},
annotations: TOOL_ANNOTATIONS_READONLY,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'read' },
})
async getVacayPlan(_args: Record<string, never>, ctx: McpContext) {
const plan = this.vacay.getPlanData(ctx.userId);
return ok({ plan });
}
@Tool({
name: 'update_vacay_plan',
description: 'Update vacation plan settings (weekends blocking, holidays, carry-over).',
inputSchema: {
block_weekends: z.boolean().optional(),
holidays_enabled: z.boolean().optional(),
holidays_region: z.string().nullable().optional(),
company_holidays_enabled: z.boolean().optional(),
carry_over_enabled: z.boolean().optional(),
},
annotations: TOOL_ANNOTATIONS_WRITE,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'write' },
})
async updateVacayPlan(
{ block_weekends, holidays_enabled, holidays_region, company_holidays_enabled, carry_over_enabled }: {
block_weekends?: boolean; holidays_enabled?: boolean; holidays_region?: string | null; company_holidays_enabled?: boolean; carry_over_enabled?: boolean;
},
ctx: McpContext,
) {
if (isDemoUser(ctx.userId)) return demoDenied();
const planId = this.vacay.getActivePlanId(ctx.userId);
// updatePlan already returns the fully-hydrated { plan }; surface it so the
// AI consumer sees the updated plan, matching get_vacay_plan.
const result = await this.vacay.updatePlan(planId, { block_weekends, holidays_enabled, holidays_region, company_holidays_enabled, carry_over_enabled }, undefined);
return ok(result);
}
@Tool({
name: 'set_vacay_color',
description: "Set the current user's color in the vacation plan calendar.",
inputSchema: {
color: z.string().describe('Hex color e.g. #6366f1'),
},
annotations: TOOL_ANNOTATIONS_WRITE,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'write' },
})
async setVacayColor({ color }: { color: string }, ctx: McpContext) {
if (isDemoUser(ctx.userId)) return demoDenied();
const planId = this.vacay.getActivePlanId(ctx.userId);
this.vacay.setUserColor(ctx.userId, planId, color, undefined);
// Echo the persisted color (mirrors the service default) so the AI consumer sees what was set.
return ok({ success: true, color: color || '#6366f1' });
}
@Tool({
name: 'get_available_vacay_users',
description: 'List users who can be invited to the current vacation plan.',
inputSchema: {},
annotations: TOOL_ANNOTATIONS_READONLY,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'read' },
})
async getAvailableVacayUsers(_args: Record<string, never>, ctx: McpContext) {
const planId = this.vacay.getActivePlanId(ctx.userId);
const users = this.vacay.getAvailableUsers(ctx.userId, planId);
return ok({ users });
}
@Tool({
name: 'send_vacay_invite',
description: 'Invite a user to join the vacation plan by their user ID.',
inputSchema: {
targetUserId: z.number().int().positive(),
},
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'write' },
})
async sendVacayInvite({ targetUserId }: { targetUserId: number }, ctx: McpContext) {
if (isDemoUser(ctx.userId)) return demoDenied();
const planId = this.vacay.getActivePlanId(ctx.userId);
const me = getCurrentUser(ctx.userId);
if (!me) return errorResult('User not found.');
const result = this.vacay.sendInvite(planId, ctx.userId, me.username, me.email, targetUserId);
if (result.error) return errorResult(result.error);
return ok({ success: true });
}
@Tool({
name: 'accept_vacay_invite',
description: "Accept a pending invitation to join another user's vacation plan.",
inputSchema: {
planId: z.number().int().positive(),
},
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'write' },
})
async acceptVacayInvite({ planId }: { planId: number }, ctx: McpContext) {
if (isDemoUser(ctx.userId)) return demoDenied();
const result = this.vacay.acceptInvite(ctx.userId, planId, undefined);
if (result.error) return errorResult(result.error);
return ok({ success: true });
}
@Tool({
name: 'decline_vacay_invite',
description: 'Decline a pending vacation plan invitation.',
inputSchema: {
planId: z.number().int().positive(),
},
annotations: TOOL_ANNOTATIONS_WRITE,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'write' },
})
async declineVacayInvite({ planId }: { planId: number }, ctx: McpContext) {
this.vacay.declineInvite(ctx.userId, planId, undefined);
return ok({ success: true });
}
@Tool({
name: 'cancel_vacay_invite',
description: 'Cancel an outgoing invitation (owner cancels invite they sent).',
inputSchema: {
targetUserId: z.number().int().positive(),
},
annotations: TOOL_ANNOTATIONS_WRITE,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'write' },
})
async cancelVacayInvite({ targetUserId }: { targetUserId: number }, ctx: McpContext) {
if (isDemoUser(ctx.userId)) return demoDenied();
const planId = this.vacay.getActivePlanId(ctx.userId);
this.vacay.cancelInvite(planId, targetUserId);
return ok({ success: true });
}
@Tool({
name: 'dissolve_vacay_plan',
description: 'Dissolve the shared plan — all members are removed and everyone returns to their own individual plan.',
inputSchema: {},
annotations: TOOL_ANNOTATIONS_DELETE,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'write' },
})
async dissolveVacayPlan(_args: Record<string, never>, ctx: McpContext) {
if (isDemoUser(ctx.userId)) return demoDenied();
this.vacay.dissolvePlan(ctx.userId, undefined);
return ok({ success: true });
}
@Tool({
name: 'list_vacay_years',
description: 'List calendar years tracked in the current vacation plan.',
inputSchema: {},
annotations: TOOL_ANNOTATIONS_READONLY,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'read' },
})
async listVacayYears(_args: Record<string, never>, ctx: McpContext) {
const planId = this.vacay.getActivePlanId(ctx.userId);
const years = this.vacay.listYears(planId);
return ok({ years });
}
@Tool({
name: 'add_vacay_year',
description: 'Add a calendar year to the vacation plan.',
inputSchema: {
year: z.number().int().min(2000).max(2100),
},
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'write' },
})
async addVacayYear({ year }: { year: number }, ctx: McpContext) {
if (isDemoUser(ctx.userId)) return demoDenied();
const planId = this.vacay.getActivePlanId(ctx.userId);
const years = this.vacay.addYear(planId, year, undefined);
return ok({ years });
}
@Tool({
name: 'delete_vacay_year',
description: 'Remove a calendar year from the vacation plan.',
inputSchema: {
year: z.number().int(),
},
annotations: TOOL_ANNOTATIONS_DELETE,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'write' },
})
async deleteVacayYear({ year }: { year: number }, ctx: McpContext) {
if (isDemoUser(ctx.userId)) return demoDenied();
const planId = this.vacay.getActivePlanId(ctx.userId);
const years = this.vacay.deleteYear(planId, year, undefined);
return ok({ years });
}
@Tool({
name: 'get_vacay_entries',
description: 'Get all vacation day entries for a plan and year.',
inputSchema: {
year: z.number().int(),
},
annotations: TOOL_ANNOTATIONS_READONLY,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'read' },
})
async getVacayEntries({ year }: { year: number }, ctx: McpContext) {
const planId = this.vacay.getActivePlanId(ctx.userId);
const entries = this.vacay.getEntries(planId, String(year), ctx.userId);
return ok({ entries });
}
@Tool({
name: 'toggle_vacay_entry',
description: 'Toggle a day on or off as a vacation day for the current user.',
inputSchema: {
date: z.string().describe('ISO date YYYY-MM-DD'),
},
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'write' },
})
async toggleVacayEntry({ date }: { date: string }, ctx: McpContext) {
if (isDemoUser(ctx.userId)) return demoDenied();
const planId = this.vacay.getActivePlanId(ctx.userId);
const result = this.vacay.toggleEntry(ctx.userId, planId, date, 1, 'vacation', undefined);
return ok(result);
}
@Tool({
name: 'toggle_company_holiday',
description: 'Toggle a date as a company holiday for the whole plan.',
inputSchema: {
date: z.string(),
note: z.string().optional(),
},
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'write' },
})
async toggleCompanyHoliday({ date, note }: { date: string; note?: string }, ctx: McpContext) {
if (isDemoUser(ctx.userId)) return demoDenied();
const planId = this.vacay.getActivePlanId(ctx.userId);
const result = this.vacay.toggleCompanyHoliday(planId, date, note, undefined);
return ok(result);
}
@Tool({
name: 'get_vacay_stats',
description: 'Get vacation statistics for a specific year (days used, remaining, carried over).',
inputSchema: {
year: z.number().int(),
},
annotations: TOOL_ANNOTATIONS_READONLY,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'read' },
})
async getVacayStats({ year }: { year: number }, ctx: McpContext) {
const planId = this.vacay.getActivePlanId(ctx.userId);
const stats = this.vacay.getStats(planId, year);
return ok({ stats });
}
@Tool({
name: 'update_vacay_stats',
description: 'Update the vacation day allowance for a specific user and year.',
inputSchema: {
year: z.number().int(),
vacationDays: z.number().int().min(0),
},
annotations: TOOL_ANNOTATIONS_WRITE,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'write' },
})
async updateVacayStats({ year, vacationDays }: { year: number; vacationDays: number }, ctx: McpContext) {
if (isDemoUser(ctx.userId)) return demoDenied();
const planId = this.vacay.getActivePlanId(ctx.userId);
this.vacay.updateStats(ctx.userId, planId, year, vacationDays, undefined);
return ok({ success: true });
}
@Tool({
name: 'add_holiday_calendar',
description: 'Add a public holiday calendar (by region code) to the vacation plan.',
inputSchema: {
region: z.string().describe('Country/region code e.g. US, GB, DE'),
label: z.string().nullable().optional(),
color: z.string().optional(),
sortOrder: z.number().int().optional(),
},
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'write' },
})
async addHolidayCalendar(
{ region, label, color, sortOrder }: { region: string; label?: string | null; color?: string; sortOrder?: number },
ctx: McpContext,
) {
if (isDemoUser(ctx.userId)) return demoDenied();
const planId = this.vacay.getActivePlanId(ctx.userId);
const calendar = this.vacay.addHolidayCalendar(planId, region, label ?? null, color, sortOrder, undefined);
return ok({ calendar });
}
@Tool({
name: 'update_holiday_calendar',
description: 'Update label or color for a holiday calendar.',
inputSchema: {
calendarId: z.number().int().positive(),
label: z.string().nullable().optional(),
color: z.string().optional(),
},
annotations: TOOL_ANNOTATIONS_WRITE,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'write' },
})
async updateHolidayCalendar(
{ calendarId, label, color }: { calendarId: number; label?: string | null; color?: string },
ctx: McpContext,
) {
if (isDemoUser(ctx.userId)) return demoDenied();
const planId = this.vacay.getActivePlanId(ctx.userId);
const cal = this.vacay.updateHolidayCalendar(calendarId, planId, { label, color }, undefined);
if (!cal) return errorResult('Holiday calendar not found.');
return ok({ calendar: cal });
}
@Tool({
name: 'delete_holiday_calendar',
description: 'Remove a holiday calendar from the vacation plan.',
inputSchema: {
calendarId: z.number().int().positive(),
},
annotations: TOOL_ANNOTATIONS_DELETE,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'write' },
})
async deleteHolidayCalendar({ calendarId }: { calendarId: number }, ctx: McpContext) {
if (isDemoUser(ctx.userId)) return demoDenied();
const planId = this.vacay.getActivePlanId(ctx.userId);
this.vacay.deleteHolidayCalendar(calendarId, planId, undefined);
return ok({ success: true });
}
@Tool({
name: 'list_holiday_countries',
description: 'List countries available for public holiday calendars.',
inputSchema: {},
annotations: TOOL_ANNOTATIONS_READONLY,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'read' },
})
async listHolidayCountries(_args: Record<string, never>, _ctx: McpContext) {
const result = await this.vacay.getCountries();
if (result.error) return errorResult(result.error);
return ok({ countries: result.data });
}
@Tool({
name: 'list_holidays',
description: 'List public holidays for a country and year.',
inputSchema: {
country: z.string().describe('ISO 3166-1 alpha-2 code'),
year: z.number().int(),
},
annotations: TOOL_ANNOTATIONS_READONLY,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'read' },
})
async listHolidays({ country, year }: { country: string; year: number }, _ctx: McpContext) {
const result = await this.vacay.getHolidays(String(year), country);
if (result.error) return errorResult(result.error);
return ok({ holidays: result.data });
}
@Tool({
name: 'list_vacay_shares',
description: 'List read-only calendar shares: who the current user shares their vacation calendar with, and which calendars are shared with them.',
inputSchema: {},
annotations: TOOL_ANNOTATIONS_READONLY,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'read' },
})
async listVacayShares(_args: Record<string, never>, ctx: McpContext) {
return ok(this.vacay.listShares(ctx.userId));
}
@Tool({
name: 'share_vacay_calendar',
description: "Share the current user's vacation calendar with another user (view only, no merge).",
inputSchema: {
targetUserId: z.number().int().positive(),
},
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'write' },
})
async shareVacayCalendar({ targetUserId }: { targetUserId: number }, ctx: McpContext) {
if (isDemoUser(ctx.userId)) return demoDenied();
const me = getCurrentUser(ctx.userId);
if (!me) return errorResult('User not found.');
const result = this.vacay.shareCalendar(ctx.userId, me.email, targetUserId);
if (result.error) return errorResult(result.error);
return ok({ success: true });
}
@Tool({
name: 'unshare_vacay_calendar',
description: 'Remove a read-only calendar share the current user is part of (revoke as owner, or remove a calendar shared with them).',
inputSchema: {
shareId: z.number().int().positive(),
},
annotations: TOOL_ANNOTATIONS_DELETE,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'write' },
})
async unshareVacayCalendar({ shareId }: { shareId: number }, ctx: McpContext) {
if (isDemoUser(ctx.userId)) return demoDenied();
if (!this.vacay.removeShare(shareId, ctx.userId, undefined)) {
return errorResult('Share not found.');
}
return ok({ success: true });
}
@Tool({
name: 'get_shared_vacay_calendars',
description: 'Get the read-only vacation calendars shared with the current user for a year (entries and company holidays per sharer).',
inputSchema: {
year: z.number().int(),
},
annotations: TOOL_ANNOTATIONS_READONLY,
when: vacayAddonOn,
access: { group: 'vacay', mode: 'read' },
})
async getSharedVacayCalendars({ year }: { year: number }, ctx: McpContext) {
return ok({ calendars: this.vacay.getSharedCalendars(ctx.userId, String(year)) });
}
// -------------------------------------------------------------------------
// Resources (from src/mcp/resources.ts — payloads reproduce jsonContent verbatim)
// -------------------------------------------------------------------------
@Resource({
name: 'vacay-plan',
uri: 'trek://vacay/plan',
description: "Full snapshot of the user's active vacation plan (members, years, settings)",
mimeType: 'application/json',
when: vacayAddonOn,
access: { group: 'vacay', mode: 'read' },
})
async vacayPlanResource(uri: URL, ctx: McpContext) {
const plan = this.vacay.getPlanData(ctx.userId);
return {
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify(plan, null, 2),
}],
};
}
@ResourceTemplate({
name: 'vacay-entries',
uriTemplate: 'trek://vacay/entries/{year}',
description: 'All vacation entries for the active plan and a specific year',
mimeType: 'application/json',
when: vacayAddonOn,
access: { group: 'vacay', mode: 'read' },
})
async vacayEntriesResource(uri: URL, { year }: { year: string | string[] }, ctx: McpContext) {
const planId = this.vacay.getActivePlanId(ctx.userId);
const entries = this.vacay.getEntries(planId, Array.isArray(year) ? year[0] : year, ctx.userId);
return {
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify(entries, null, 2),
}],
};
}
@ResourceTemplate({
name: 'vacay-holidays',
uriTemplate: 'trek://vacay/holidays/{year}',
description: "Cached public holidays for the plan's configured region and year",
mimeType: 'application/json',
when: vacayAddonOn,
access: { group: 'vacay', mode: 'read' },
})
async vacayHolidaysResource(uri: URL, { year }: { year: string | string[] }, ctx: McpContext) {
const plan = this.vacay.getActivePlan(ctx.userId);
const json = (data: unknown) => ({
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify(data, null, 2),
}],
});
if (!plan.holidays_enabled || !plan.holidays_region) return json([]);
const yearStr = Array.isArray(year) ? year[0] : year;
const result = await this.vacay.getHolidays(yearStr, plan.holidays_region);
return json(result.data ?? []);
}
}
+8 -2
View File
@@ -1,10 +1,16 @@
import { Module } from '@nestjs/common';
import { VacayController } from './vacay.controller';
import { VacayService } from './vacay.service';
import { VacayMcp } from './vacay.mcp';
/** Vacay addon domain (S1 — Phase 2 trip sub-domain). Registered in AppModule. */
/**
* Vacay addon domain (S1 — Phase 2 trip sub-domain). Registered in AppModule.
* VacayService is exported for the plugin host wiring (PluginHostDepsFactory);
* VacayMcp carries the DI-discovered MCP tools/resources.
*/
@Module({
controllers: [VacayController],
providers: [VacayService],
providers: [VacayService, VacayMcp],
exports: [VacayService],
})
export class VacayModule {}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -11,7 +11,7 @@ import { listBudgetItems, removeUserFromBudgetItems } from './budgetService';
import { listItems as listPackingItems } from '../nest/packing/packing.bridge';
import { listReservations, loadEndpointsByTrip, resyncReservationDays } from './reservationService';
import { listNotes as listCollabNotes } from '../nest/collab/collab.bridge';
import { shiftOwnerEntriesForTripWindow } from './vacayService';
import { shiftOwnerEntriesForTripWindow } from '../nest/vacay/vacay.bridge';
import { resolveTimeZone } from './timezoneService';
export const MS_PER_DAY = 86400000;
File diff suppressed because it is too large Load Diff