/** * InMemoryCalendarClient โ€” deterministic in-memory CalendarClient for local dev. * * Implements the same CalendarClient interface as GoogleCalendarClient but holds * a small set of FAKE seed events in memory, partitioned by userSub so a user * only ever sees their own calendar. No network, no AWS, no token provider. * * This is for build-plan ยง4 "in-memory dev clients (tools return real data * locally)" โ€” wire it into a dev server instead of GoogleCalendarClient. */ import type { AvailabilityResult, CalendarClient, CalendarEvent, CheckAvailabilityOptions, CreateEventOptions, GetEventsOptions, } from './client.js'; /** Seed events keyed by userSub. Each user sees only their own list. */ function seedEvents(): Map { const map = new Map(); map.set('lauren@seahavenind.com', [ { id: 'evt-lauren-001', summary: 'Vendor onboarding sync', description: 'Walk through the new vendor intake checklist.', start: '2026-06-25T15:00:00Z', end: '2026-06-25T15:30:00Z', attendees: [ { email: 'lauren@seahavenind.com', responseStatus: 'accepted', self: true, organizer: true, }, { email: 'adam@seahavenind.com', responseStatus: 'accepted' }, ], hasExternalAttendees: false, status: 'confirmed', htmlLink: 'https://calendar.google.com/event?eid=evt-lauren-001', }, { id: 'evt-lauren-002', summary: 'Site walkthrough โ€” Marina pier', start: '2026-06-26T13:00:00Z', end: '2026-06-26T14:00:00Z', attendees: [ { email: 'lauren@seahavenind.com', responseStatus: 'accepted', self: true, organizer: true, }, { email: 'contractor@example.com', responseStatus: 'needsAction' }, ], hasExternalAttendees: true, status: 'confirmed', }, ]); map.set('adam@seahavenind.com', [ { id: 'evt-adam-001', summary: 'Quarterly finance review', description: 'Review payment dashboard and QBO vendor balances.', start: '2026-06-24T17:00:00Z', end: '2026-06-24T18:00:00Z', attendees: [ { email: 'adam@seahavenind.com', responseStatus: 'accepted', self: true, organizer: true }, { email: 'lauren@seahavenind.com', responseStatus: 'tentative' }, ], hasExternalAttendees: false, status: 'confirmed', }, ]); return map; } /** * In-memory CalendarClient. Events are partitioned by userSub; createEvent * appends to the caller's own list and returns the created event. */ export class InMemoryCalendarClient implements CalendarClient { private readonly eventsBySub: Map; private counter = 1000; constructor() { this.eventsBySub = seedEvents(); } async getEvents(userSub: string, opts: GetEventsOptions): Promise { const all = this.eventsBySub.get(userSub) ?? []; const min = Date.parse(opts.timeMin); const max = Date.parse(opts.timeMax); const inWindow = all.filter((e) => { const start = Date.parse(e.start); return start >= min && start <= max; }); const limit = opts.maxResults ?? 50; return inWindow.slice(0, limit); } async checkAvailability( userSub: string, opts: CheckAvailabilityOptions, ): Promise { const min = Date.parse(opts.timeMin); const max = Date.parse(opts.timeMax); const all = this.eventsBySub.get(userSub) ?? []; const busy = all .filter((e) => Date.parse(e.start) < max && Date.parse(e.end) > min) .map((e) => ({ start: e.start, end: e.end })) .sort((a, b) => Date.parse(a.start) - Date.parse(b.start)); const free: Array<{ start: string; end: string }> = []; let cursor = min; for (const slot of busy) { const slotStart = Date.parse(slot.start); if (slotStart > cursor) { free.push({ start: new Date(cursor).toISOString(), end: new Date(slotStart).toISOString(), }); } cursor = Math.max(cursor, Date.parse(slot.end)); } if (cursor < max) { free.push({ start: new Date(cursor).toISOString(), end: new Date(max).toISOString() }); } return { busy, free }; } async createEvent(userSub: string, opts: CreateEventOptions): Promise { const event: CalendarEvent = { id: `evt-dev-${this.counter++}`, summary: opts.summary, start: opts.start, end: opts.end, status: 'confirmed', htmlLink: `https://calendar.google.com/event?eid=evt-dev-${this.counter}`, }; if (opts.description !== undefined) { event.description = opts.description; } if (opts.attendees !== undefined) { event.attendees = opts.attendees.map((a) => a.displayName !== undefined ? { email: a.email, displayName: a.displayName, responseStatus: 'needsAction' } : { email: a.email, responseStatus: 'needsAction' }, ); } const list = this.eventsBySub.get(userSub) ?? []; list.push(event); this.eventsBySub.set(userSub, list); return event; } }