# SCRATCHPAD — Teams Online Meetings → Interactions Working memory for this plan. Append decisions, discoveries, gotchas, and commands. ## Source of truth - Long-form design: `.ai/teams-meetings-interactions-consolidated-plan.md` (incorporates two external review rounds). - This plan folder lives in `ee/docs/plans/` (per project convention) even though some artifacts are core — most of the meaningful logic (capture, subscriptions, Graph) is EE. ## Key decisions (with rationale) - **Calendar-backed creation** (`POST /users/{upn}/events`, `isOnlineMeeting:true`) instead of standalone `POST /onlineMeetings`. Reason: Graph recordings/transcripts APIs only work reliably for calendar-backed meetings. Cost: new `Calendars.ReadWrite` app permission + onlineMeeting-id resolution from joinUrl + invite-behavior decision. - **Provider-agnostic** "Online Meeting" interaction type (icon `video`), NOT "Teams Meeting" — the `online_meetings.provider` column allows future zoom/google_meet. - **One table, two creation paths.** Appointment approval and MSP-initiated both write `online_meetings` + an interaction; differ only by `appointment_request_id` vs `schedule_entry_id`. No backfill. - **Storage:** transcript → durable internal **document**; recording → internal **proxy/download** (Graph content URLs are auth-protected, not clickable), with opt-in blob download. Artifacts are a **collection** → `online_meeting_artifacts` child table (NOT singular columns). - **Single service-account organizer** in v1; per-user organizer + co-organizer roles deferred. - **Retrieval:** manual "Refresh recordings" (Phase 1) ships first; change-notification subscriptions (Phase 2) follow (encrypted resource data + protected/metered API approval). ## Must-fix items confirmed by review (do not regress) → mapped features 1. Scope `Calendars.ReadWrite` to the organizer mailbox via **Exchange** Application Access Policy / RBAC (Teams policy does NOT scope calendar). → F051, F052, T075. 2. Update/delete use `provider_event_id` for new rows; legacy `online_meeting_id` handling preserved. → F027, T038, T039. 3. Invite behavior locked: appointment approval = no external attendees; MSP-initiated = attendees allowed. → F018, T025, T026. 4. No "co-organizer" claim unless a real Graph meeting-options step is added (deferred). → F018 (attendee only), §9 deferred. 5. All Graph create/update/delete OUTSIDE DB transactions, with create→DB-fail compensation. → F028, F029, T040, T041. ## Discoveries about existing code (verified) - `approveAppointmentRequest` wraps work in `withTransaction`; existing `if (createdMeeting)` block ~`appointmentRequestManagementActions.ts:790-793`; writes `online_meeting_*` columns. - Reschedule `updateAppointmentRequestDateTime` calls `updateTeamsMeeting` **inside** `withTransaction` (~:1321) — MUST move out (F028). Cancel/delete paths (`scheduleActions.ts:732`, client-portal `appointmentRequestActions.ts:1337`) already call Graph outside the tx. - Facade `CreateTeamsMeetingResult` returns only `{joinWebUrl, meetingId}` (`teamsMeetingService.ts`), EE same (`createTeamsMeeting.ts:107`). Must add organizer UPN + AAD id + eventId (F020). - `InteractionModel.addInteraction/getById` open their own `createTenantKnex` connection (`interactions.ts:225`), so the action's `withTransaction` does NOT cover the model insert today — latent bug fixed by F012. Defaulting/resolution/events/revalidate live in the ACTION (`interactionActions.ts:~86-150`) — must be replicated in the shared helper (F013). - `uploadDocument` is `withAuth` + `FormData` (`documentActions.ts:2642`) — unusable from a job; need an internal helper (F038). Internal users inherit folder `is_client_visible` (`documentActions.ts:2764`) — set false explicitly. - `system_interaction_types` modification trigger was REMOVED by `server/migrations/20250613000000_remove_system_interaction_types_trigger.cjs` — no workaround needed. - `Meeting` (icon users) already seeded by `20241223015715_create_system_interaction_types.cjs`; we add a separate `Online Meeting` (icon video). - `ScheduleEntry.create` stores `work_item_id` for non-`ad_hoc` (`scheduleEntry.ts:407`) → set to interaction id (F033). - Calendar mapping uses `entry.notes` as description in BOTH `server/src/utils/calendar/eventMapping.ts:82` and `packages/integrations/src/utils/calendar/eventMapping.ts:80` (F036). - EE calendar sync (`ee/packages/calendar/`) auto-pushes schedule_entries to the user's connected calendar via `calendarSyncSubscriber` — partially compensates for the service-account-organizer model (meeting still shows on the creating user's own calendar). Possible Phase-2 reuse: `CalendarWebhookProcessor.ts` for the encrypted subscription machinery. - `scheduleRecurringJob` exists (`server/src/lib/jobs/jobScheduler.ts` + `index.ts`); there is already a `MicrosoftWebhookRenewalJobData` recurring job to mirror for Phase 2 (F055). - `microsoft_profiles` are tenant-level service accounts; `users` has no AAD columns → no per-user organizer today. ## Open questions - Exact `hasPermission` resource/action for `scheduleTeamsMeeting` (verify repo catalog) — F031. - Should MSP-initiated default to adding the contact as an attendee, or leave optional in the UI? ## Phasing for the implementation loop - Phase A (data + model): F001–F013. - Phase B (creation, both paths): F016–F036. - Phase C (capture Phase 1 + UI): F037–F052. - Phase D (gating): F053. - Phase E (Phase 2 subscriptions): F054–F057. Recommend the /loop pick the lowest-id `implemented:false` feature whose deps are met, implement + test, flip `implemented:true`, repeat. ## Commands / runbooks - Validate plan JSON: `python3 ~/.claude/skills/software-planner/scripts/validate_plan.py ` (if present). - Tests: `npm run test:unit` (unit), `npm run test:integration`. - Migrations: `npm run migrate`. ## 2026-06-01 — F034-F036 / T049-T051 - Implemented QuickAddInteraction Online Meeting scheduling without adding a clients -> scheduling package dependency: added optional Teams callbacks to `ClientCrossFeatureContext`, supplied them in `MspClientCrossFeatureProvider`, and had QuickAdd call `scheduleTeamsMeeting` only when the Online Meeting type is selected and `getTeamsMeetingCapability` reports available. Rationale: keeps clients package reusable while MSP composition owns cross-feature wiring. - Implemented direct schedule-entry Teams generation by having `EntryPopup` add a `generate_teams_meeting` save payload for new entries and `ScheduleCalendar` route that payload to `scheduleTeamsMeeting({ createScheduleEntry: true })`. Constraint: the backend requires client/contact context, so the UI requires a selected client-backed work item and shows a translated validation message otherwise. - Tightened `scheduleTeamsMeeting` notes behavior: stored interaction/schedule notes now append `Join Teams Meeting: ` even when caller-supplied notes exist. - Updated both calendar mappers (`server/src/utils/calendar/eventMapping.ts` and `packages/integrations/src/utils/calendar/eventMapping.ts`) to append the join URL from `online_meetings` by `schedule_entry_id` as a fallback/field mapping for external calendar pushes. - Added source-contract tests for QuickAdd Teams wiring, EntryPopup/ScheduleCalendar routing, and both eventMapping copies. - Verification: - `npm -w @alga-psa/clients run typecheck` - `npm -w @alga-psa/scheduling run typecheck` - `npm -w @alga-psa/integrations run typecheck` - `npx vitest run ../packages/clients/src/components/interactions/QuickAddInteraction.quick-add-contact.contract.test.ts ../packages/scheduling/src/components/schedule/EntryPopup.teams-meeting.contract.test.ts src/test/unit/calendar/eventMapping.onlineMeetingDescription.contract.test.ts src/test/integration/appointmentRequests.integration.test.ts -t "Schedule Teams Meeting|quick add|EntryPopup offers|online meeting calendar"` from `server/` - `packages/msp-composition` has no package typecheck script or tsconfig; composition compile coverage is indirect through package consumers. ## 2026-06-01 — F015, F037-F041 / T052-T062 - Implemented Phase-1 artifact capture in `packages/clients/src/lib/onlineMeetingArtifactCapture.ts`. Package-boundary decision: put the handler next to `OnlineMeetingModel` and dynamically load the EE Teams module instead of importing scheduling from clients, because scheduling already depends on clients and a static clients -> scheduling import would create a cycle. - `fetchAndPersistMeetingArtifacts` is session-agnostic and takes explicit `tenantId`, `meetingId`, and optional `actorUserId`. It resolves the meeting, skips cancelled rows, calls EE `fetchMeetingArtifacts`, upserts recording/transcript artifacts idempotently, updates bounded fetch status (`recording_ready`, `recording_pending`, `no_recording`, `failed`), and revalidates interaction + linked client/contact paths. - Transcript storage uses an internal document helper, not `uploadDocument`: inserts `documents`, `document_block_content`, and `document_associations` directly inside a transaction with explicit tenant/user metadata. Client/contact associations are resolved from the linked interaction because `online_meetings` intentionally stores `interaction_id`, not duplicate client/contact columns. - Recording capture stores `content_url` by default. When `download_recordings` is enabled, it resolves the EE Teams Graph config, fetches an app token, downloads the Graph content URL server-side, stores the blob with `StorageService.uploadFile`, and sets `file_id`. - `refreshMeetingRecordings(meetingId)` was added to `packages/clients/src/actions/onlineMeetingActions.ts` under `withAuth`, passing the authenticated user id to the shared handler. - Exported `meetings/meetingConfig` from `@alga-psa/ee-microsoft-teams/lib` so the recording download helper can reuse the existing Graph config resolver. - Verification: - `npm -w @alga-psa/clients run typecheck` - `npm -w @alga-psa/scheduling run typecheck` - `npm -w @alga-psa/ee-microsoft-teams run typecheck` - `npx vitest run ../packages/clients/src/lib/onlineMeetingArtifactCapture.test.ts` from `server/` ## 2026-06-01 — F042 / T063-T065 - Added internal recording proxy route: `server/src/app/api/online-meetings/recordings/[artifactId]/route.ts`. - Security shape: requires `getCurrentUser`, uses the authenticated user's tenant to create the DB context, looks up `online_meeting_artifacts` joined to `online_meetings` by `(tenant, meeting_id)`, and returns 404 for cross-tenant artifact ids because the query is tenant-scoped. - Proxy behavior: fetches the raw Graph `content_url` server-side with the EE Teams Graph config + app token, forwards `Range` when present, streams `graphResponse.body` with content headers, and never redirects or serializes the raw Graph URL to the caller. - Portal guard: `?portal=true` is denied unless `teams_integrations.expose_recordings_in_portal` is true; missing column/table defaults to false until F048 lands. - Verification: - `npx vitest run src/test/unit/onlineMeetingRecordingProxy.contract.test.ts` from `server/` - `npm -w @alga-psa/ee-microsoft-teams run typecheck` ## 2026-06-01 implementation notes - Implemented F012/F013: - `InteractionModel.addInteraction` and `getById` now accept an optional `Knex`/transaction and use it for both the insert and follow-up read; the default path still uses `createTenantKnex(tenantId)`. - Added `packages/clients/src/actions/interactionCreateHelper.ts` as the shared interaction creation path. It centralizes user/client/contact validation, default interaction status resolution, contact -> client resolution, `INTERACTION_LOGGED`, `INTERACTION_CREATED`, and cache revalidation. - `addInteraction` now uses the helper inside its transaction but defers the helper-provided side effects until after `withTransaction` returns, preserving post-commit event/revalidate behavior. - `getInteractionById` now passes its active transaction into `InteractionModel.getById`. - Implemented T017-T022 with focused unit tests: - `packages/clients/src/models/interactions.transaction.test.ts` covers trx-backed insertion and no-trx back-compat. - `packages/clients/src/actions/interactionCreateHelper.test.ts` covers default status, contact-client resolution/failure, event publishing, and revalidation. - Verification: - `npx vitest run ../packages/clients/src/models/interactions.transaction.test.ts ../packages/clients/src/actions/interactionCreateHelper.test.ts` from `server/` passed 6 tests. - `npm -w @alga-psa/clients run typecheck` passed. - Implemented F014: - Added `packages/clients/src/actions/onlineMeetingActions.ts` with `getOnlineMeetingForInteraction` under `withAuth`, delegating to `OnlineMeetingModel.getByInteractionId(interactionId, tenant)`. - Exported the action from `packages/clients/src/actions/index.ts`. - Corrected T037's description: it was assigned to F014 but described cancelled refresh/list-pending behavior already covered by T014; it now describes the F014 action contract. - Added `packages/clients/src/actions/onlineMeetingActions.test.ts` covering tenant-scoped lookup, absent rows, and missing id validation. - Implemented F016-F022: - `createTeamsMeeting` now creates calendar-backed Graph events through `POST /users/{organizerUpn}/events` with `isOnlineMeeting: true` and `onlineMeetingProvider: teamsForBusiness`. - It resolves the `onlineMeeting.id` from the event join URL via a URL-encoded `JoinWebUrl` filter, then returns `joinWebUrl`, `meetingId`, `organizerUpn`, `organizerUserId`, and `eventId`. - `organizerUserId` is read from `teams_integrations.default_meeting_organizer_object_id` when present; until F048 migration/UI lands, the config falls back to organizer UPN to preserve existing tenants/tests. - Appointment approval keeps the default no-attendee payload; MSP-created meetings can pass attendees explicitly. - `updateTeamsMeeting`/`deleteTeamsMeeting` now call `/events/{eventId}` (falling back to `meetingId` only for temporary compatibility until F027 legacy branching is implemented). - Added EE `fetchMeetingArtifacts` and facade `fetchMeetingArtifacts` no-op off enterprise. The EE implementation fetches recordings/transcripts collections, downloads transcript content with `Accept: text/vtt`, and URL-encodes organizer/meeting/artifact path segments. - T028's online_meetings persistence portion will become concrete in F023/F032 when creation paths write the returned facade fields into `online_meetings`; current coverage verifies the facade returns the fields required for persistence. - Verification: - `npx vitest run src/test/unit/teamsMeetingHelpers.test.ts` from `server/` passed 23 tests. - `npm -w @alga-psa/scheduling run typecheck` passed. - `npm -w @alga-psa/ee-microsoft-teams run typecheck` passed. ## 2026-06-01 implementation notes - Completed F001-F006 in `server/migrations/20260601120000_create_online_meetings.cjs`: added CE/core `online_meetings` and `online_meeting_artifacts` with tenant-first PKs, PRD status/artifact type checks, provider-meeting uniqueness, tenant-leading lookup indexes, Citus `transaction:false` distribution on `tenant`, and artifact colocation with `online_meetings`. Chose no SQL FKs in these tables to match the PRD's Citus/application-level integrity constraint. - Completed F007 in `server/migrations/20260601120100_add_online_meeting_interaction_type.cjs`: added an idempotent insert-only `Online Meeting` system interaction type with `video` icon, following the existing `add_general_interaction_type` guard style. - Completed T001-T007/T009-T010 in `server/src/test/unit/migrations/onlineMeetingsMigration.test.ts`: tests execute the migration against mocked Knex raw calls for plain Postgres and Citus paths, assert tenant-first keys, enum checks, uniqueness/index contracts, child-table colocation, rollback order, and idempotent interaction-type insertion. T008 intentionally remains open because it covers the later `OnlineMeetingModel.upsertArtifact` helper, not just the unique constraint. - Verification: `npx vitest run src/test/unit/migrations/onlineMeetingsMigration.test.ts` from `server/` passed (8 tests). An earlier `npm -w server run test:unit -- ...` accidentally ran the entire unit suite due to the package script prepending `src/test/unit`; it was terminated after unrelated existing failures and should not be treated as signal for this batch. - Completed F008-F009/T011 in `packages/types/src/interfaces/online-meeting.interfaces.ts` and `packages/types/src/interfaces/interaction.interfaces.ts`: added `IOnlineMeetingArtifact`, `IOnlineMeeting`, status/artifact/provider type aliases, `artifacts[]`, and optional `IInteraction.online_meeting`. Exported via `packages/types/src/interfaces/index.ts` and added `@alga-psa/types` smoke imports to `packages/types/src/exports.typecheck.test.ts`. - Verification: `npm -w @alga-psa/types test -- src/exports.typecheck.test.ts src/interfaces/barrel.test.ts` passed (2 tests). - Completed F010-F011/T008/T012-T016 in `packages/clients/src/models/onlineMeeting.ts`: added session-agnostic `OnlineMeetingModel` using `createTenantKnex`, tenant guard, create/get/update/provider and interaction/appointment lookups with artifacts, pending-recording listing for ended eligible statuses, idempotent artifact upsert on `(tenant, meeting_id, artifact_type, provider_artifact_id)`, and newest-first artifact listing. Exported from `packages/clients/src/models/index.ts`. - Verification: `npx vitest run ../packages/clients/src/models/onlineMeeting.test.ts` from `server/` passed (6 tests); `npm -w @alga-psa/clients run typecheck` passed. - Completed F023-F024 in `packages/scheduling/src/actions/appointmentRequestManagementActions.ts`: approval now uses the shared interaction helper to create an `Online Meeting` interaction and inserts a linked `online_meetings` row when Teams creation succeeds. The row stores the Graph calendar event id, organizer UPN/object id, join URL, schedule entry id, appointment request id, interaction id, and scheduled status. The legacy `appointment_requests.online_meeting_*` columns remain populated for existing email/portal consumers. - Package wiring: `@alga-psa/scheduling` now depends on `@alga-psa/clients`, and `@alga-psa/clients` exposes the narrow `./actions/interactionCreateHelper` subpath used by scheduling. `@alga-psa/licensing` exports were pointed at `src` to make Vite resolve the workspace package in the appointment integration test environment. - Completed T032-T034 in `server/src/test/integration/appointmentRequests.integration.test.ts`: the Teams approval test now asserts the interaction and `online_meetings` row, the capability-unavailable path asserts no row is written, and the legacy appointment request columns remain asserted. The fixture now tolerates current schemas where `service_types.billing_method` has been removed and seeds the staff user needed by the new interaction FK. - Verification: - `npx vitest run src/test/integration/appointmentRequests.integration.test.ts -t "creates and stores a Teams meeting|capability is unavailable|creation fails"` from `server/` passed (3 tests, 38 skipped). - `npm -w @alga-psa/scheduling run typecheck` passed. - A full `npx vitest run src/test/integration/appointmentRequests.integration.test.ts` run was attempted before the fixture compatibility fix and failed on the pre-existing `service_types.billing_method` fixture/schema mismatch across many unrelated tests; targeted coverage now passes. - Completed F025-F027: - `updateAppointmentRequestDateTime` now syncs linked `online_meetings` start/end fields and the generated interaction's date/start/end/duration, then calls `updateTeamsMeeting` after the DB transaction with `provider_event_id` for modern rows. Legacy appointment-only meetings pass `eventId: null` and keep the standalone-compatible fallback path. - Decline, MSP schedule deletion, and client-portal cancellation now mark linked `online_meetings` rows as `cancelled`, which keeps them out of later capture/polling flows. - Delete paths prefer `online_meetings.provider_meeting_id` + `provider_event_id`; legacy rows without `provider_event_id` still pass the appointment request's existing `online_meeting_id`. - F028/F029 intentionally remain open: reschedule update is now outside the transaction, but approval Graph create/DB compensation has not been reworked yet. - Completed T035-T036/T038-T039 in `server/src/test/integration/appointmentRequests.integration.test.ts`: reschedule asserts event-id updates plus local meeting/interaction time sync, legacy reschedule asserts `eventId: null`, and decline/cancel/delete tests assert `online_meetings.status='cancelled'`. - Verification: - `npm -w @alga-psa/scheduling run typecheck` passed. - `npm -w @alga-psa/client-portal run typecheck` passed. - `npx vitest run src/test/integration/appointmentRequests.integration.test.ts -t "should update status correctly when declined|reschedules the linked Teams meeting|reschedules a legacy Teams meeting|deletes the linked Teams meeting"` from `server/` passed (5 tests, 37 skipped). - The focused integration run still logs pre-existing non-fatal warnings in decline event publishing (`requestedDate` Date vs string) and client cancellation notifications (`contact_id` binding). - Completed F028-F029: - Appointment approval now performs a read/validation preflight, calls `createTeamsMeeting` outside the write transaction, then consumes the prepared Graph result inside the DB transaction. - If the post-create DB transaction fails, the action calls `deleteTeamsMeeting` with the created `meetingId` and `eventId`, leaving the appointment pending and without local meeting rows/links. - Existing update/delete paths are now covered by source-level transaction-discipline guards: reschedule updates happen after the DB transaction returns, and schedule deletion calls Teams after the appointment cleanup transaction returns. - Completed T040-T041: - Added `server/src/test/unit/scheduling/appointmentRequestTeamsTransaction.test.ts` to guard that create/update/delete Graph calls are outside transaction bodies. - Added an approval integration test that hides the `Online Meeting` interaction type after Graph create, forcing the DB transaction to fail and asserting the orphan Teams event is deleted. - Moved the requester-timezone conversion fixture from `2026-04-25` to `2026-08-25`; the old date is now in the past relative to the 2026-06-02 runtime clock and fails request creation before approval. - Verification: - `npm -w @alga-psa/scheduling run typecheck` passed. - `npx vitest run src/test/unit/scheduling/appointmentRequestTeamsTransaction.test.ts` from `server/` passed (3 tests). - `npx vitest run src/test/integration/appointmentRequests.integration.test.ts -t "creates and stores a Teams meeting|deletes the orphaned Teams event|capability is unavailable|creation fails|converts requester-local approval times"` from `server/` passed (5 tests, 38 skipped). - Gotcha: do not run two server Vitest commands with coverage in parallel; one parallel attempt hit a `coverage/.tmp/coverage-0.json` race even though the targeted assertions had passed. - Completed F030/T042: - Confirmed the rollout remains no-backfill: a legacy approved appointment with existing `appointment_requests.online_meeting_*` values keeps those links, but no `online_meetings` row and no `Online Meeting` timeline interaction are created just because the new schema exists. - Added the coverage to `server/src/test/integration/appointmentRequests.integration.test.ts`. - Verification: - `npx vitest run src/test/integration/appointmentRequests.integration.test.ts -t "does not backfill legacy approved Teams appointment links"` from `server/` passed (1 test, 43 skipped). - Completed F031-F033: - Added `packages/scheduling/src/actions/onlineMeetingSchedulingActions.ts` and exported `scheduleTeamsMeeting` from `@alga-psa/scheduling/actions`. - The action is `withAuth` and explicitly requires `user_schedule:update` (confirmed as the local schedule-create/update permission already used by schedule actions). - Teams Graph create happens before the DB write transaction; if the local transaction fails, the created calendar-backed Teams event is deleted via the facade using both `meetingId` and `eventId`. - The DB transaction creates the shared `Online Meeting` interaction, inserts the `online_meetings` row with organizer UPN/object id and event id, and optionally creates a `schedule_entries` row with `work_item_type='interaction'` and `work_item_id=`, linked from `online_meetings.schedule_entry_id`. - MSP-created meetings pass through provided attendees to the facade; appointment approval remains separate and still sends no attendees. - Completed T043-T048 in `server/src/test/integration/appointmentRequests.integration.test.ts`: - Happy path asserts Graph facade input, attendee pass-through, default organizer persistence from the facade result, interaction creation, and `online_meetings` persistence. - Permission-denied and capability-unavailable paths assert no Graph create and no local interaction. - Schedule-entry option asserts `work_item_type='interaction'`, `work_item_id` equals the created interaction id, assignee row creation, and `online_meetings.schedule_entry_id` linkage. - Test cleanup now tracks standalone online meeting and interaction ids because these rows are not appointment-request-linked. - Verification: - `npm -w @alga-psa/scheduling run typecheck` passed. - `npx vitest run src/test/integration/appointmentRequests.integration.test.ts -t "Schedule Teams Meeting"` from `server/` passed twice (4 tests, 44 skipped). The second run includes a package-level `@alga-psa/event-bus/publishers` mock so the new action tests do not touch Redis. ## 2026-06-01 - F043-F045 / T066-T068 - Implemented interaction read enrichment: `InteractionModel.getById` and `getForEntity` attach `online_meeting` by calling `OnlineMeetingModel.getByInteractionId`, which returns artifacts newest-first via the model helper. Kept the implementation scoped to interaction reads instead of duplicating artifact aggregation SQL. - Added the MSP `InteractionDetails` online meeting section with Join, status, Refresh recordings, transcript document links, and recording proxy links. Artifact links use internal routes only: `/api/documents/{documentId}/download` for transcripts and `/api/online-meetings/recordings/{artifactId}` for recordings. - Fixed `InteractionIcon` fallback so `online meeting` maps to the `video` icon before the generic meeting/users fallback. - Added source contract tests for model enrichment and UI wiring: `packages/clients/src/models/interactions.onlineMeeting.contract.test.ts` and `packages/clients/src/components/interactions/InteractionDetails.onlineMeeting.contract.test.ts`. - Verification: `npx vitest run ../packages/clients/src/models/interactions.onlineMeeting.contract.test.ts ../packages/clients/src/components/interactions/InteractionDetails.onlineMeeting.contract.test.ts` from `server/`; `npm -w @alga-psa/clients run typecheck`; `npm -w @alga-psa/ui run typecheck`. ## 2026-06-01 - F046-F047 / T069-T070 - Added sanitized online meeting artifact metadata to client-portal appointment actions. Portal actions return `online_meeting_artifacts` only when `teams_integrations.expose_recordings_in_portal=true`; missing settings/columns default to hidden. Returned rows intentionally omit raw Graph `content_url`. - Added MSP EntryPopup artifact metadata through `getAppointmentRequestById` without portal gating, so internal users can see recordings/transcripts next to the existing Join Teams Meeting action. - Added artifact buttons to `AppointmentsPage`, `AppointmentRequestDetailsPage`, and `EntryPopup`. Transcript buttons use `/api/documents/{documentId}/download`; recording buttons use `/api/online-meetings/recordings/{artifactId}` with `?portal=true` for portal surfaces so the proxy enforces portal visibility. - Stable ids added for artifact actions (`client-portal-appointment-artifact-*`, `entry-popup-online-meeting-artifact-*`), and all new labels use `t(...)` keys with defaults. - Verification: `npx vitest run ../packages/client-portal/src/components/appointments/onlineMeetingArtifacts.contract.test.ts ../packages/scheduling/src/components/schedule/EntryPopup.onlineMeetingArtifacts.contract.test.ts` from `server/`; `npm -w @alga-psa/client-portal run typecheck`; `npm -w @alga-psa/scheduling run typecheck`. ## 2026-06-01 - F048 / T071 - Added EE migration `ee/server/migrations/20260601120200_add_online_meeting_capture_settings_to_teams_integrations.cjs`. - Migration is idempotent and skips cleanly if `teams_integrations` is absent. It adds `default_meeting_organizer_object_id` as nullable text, plus `download_recordings` and `expose_recordings_in_portal` as non-null booleans defaulting to false; rollback drops those columns in reverse order if present. - Verification: `npx vitest run src/test/unit/migrations/teamsIntegrationCaptureSettingsMigration.test.ts` from `server/`. ## 2026-06-02 — F049/F050 Teams meeting settings UI - Moved the Teams meeting organizer controls out of `packages/scheduling/src/components/schedule/AvailabilitySettings.tsx`; the scheduling dialog no longer loads `getTeamsMeetingsTabState` or exposes the old `default-meeting-organizer-upn` / save / verify controls. - Added organizer + recording settings to the Teams integration settings page: `teams-default-meeting-organizer-upn`, `teams-download-recordings`, and `teams-expose-recordings-in-portal`, all using `integrations.teams.settings.*` i18n keys. - Extended Teams integration contracts/actions in both shared and EE packages with `defaultMeetingOrganizerUpn`, `defaultMeetingOrganizerObjectId`, `downloadRecordings`, and `exposeRecordingsInPortal`. - Save now resolves the organizer UPN to a Microsoft Entra object id via Graph `/users/{upn}` using the selected Microsoft profile app credentials, then persists `default_meeting_organizer_object_id`. Rationale: meeting execution can prefer object id while admins can enter a UPN. - Added runtime coverage in `packages/integrations/src/actions/integrations/teamsActions.test.ts` for organizer object-id resolution and recording toggle persistence; added source contract `server/src/test/unit/integrations/teamsIntegrationMeetingSettings.contract.test.ts` for backend/UI placement. - Verification: `npm -w @alga-psa/integrations run typecheck`, `npm -w @alga-psa/ee-microsoft-teams run typecheck`, `npm -w @alga-psa/scheduling run typecheck`, and `npx vitest run src/actions/integrations/teamsActions.test.ts` passed. The server source-contract Vitest assertions passed, but one parallel run ended after coverage with `ENOENT ... server/coverage/.tmp`; rerun before commit. - Rerun note: source-contract test passed cleanly when run alone; the earlier coverage `.tmp` error was from concurrent Vitest runs. ## 2026-06-02 — F051/F052 recording diagnostics and setup docs - Extended `getTeamsMeetingCapability` to return `recordingsAvailable` separately from `available`. Meeting creation can be available while recording capture is not ready; missing organizer object id now reports `recordingReason='missing_organizer_object_id'`. - Added a Teams diagnostics step `recording_permissions` that surfaces recording/transcript readiness and lists required Graph application permissions (`Calendars.ReadWrite`, `OnlineMeetingRecording.Read.All`, `OnlineMeetingTranscript.Read.All`) plus Exchange mailbox scoping. - Mapped the new diagnostics step and organizer remediation recommendations in `TeamsIntegrationSettings` so warnings render with i18n-backed copy. - Updated `docs/integrations/teams-meetings-setup.md` and the browser-served `server/public/docs/integrations/teams-meetings-setup.md` to document calendar-backed events, protected recording/transcript API consent, and Exchange Application Access Policy/RBAC scoping. Rationale: Teams Application Access Policy scopes online meetings, not calendar/mailbox access. - Added docs contract coverage in `server/src/test/unit/docs/teamsMeetingsSetupRecordingPermissions.contract.test.ts` and expanded diagnostics/capability unit coverage. - Verification: `npx vitest run src/test/unit/teamsMeetingHelpers.test.ts src/test/unit/lib/teams/actions/teamsDiagnosticsActions.test.ts src/test/unit/docs/teamsMeetingsSetupRecordingPermissions.contract.test.ts`; `npm -w @alga-psa/ee-microsoft-teams run typecheck`; `npm -w @alga-psa/scheduling run typecheck`; `npm -w @alga-psa/integrations run typecheck`; `npm -w @alga-psa/clients run typecheck` all passed. ## 2026-06-02 — F053 edition gating - Made recording capture truly inert in CE: `fetchAndPersistMeetingArtifacts` now returns the existing online meeting before loading settings, fetching Graph artifacts, upserting artifacts, incrementing `recording_fetch_attempts`, or changing status when `isEnterprise` is false. Added an injectable `isEnterpriseEdition` dependency so the boundary is runtime-tested. - Gated the internal recording proxy route with `isEnterprise` before loading EE Teams code; CE returns a controlled 404 instead of attempting Graph token/config resolution. - Kept online meeting records and join links edition-agnostic in `InteractionDetails`, but hid recording status, refresh, and artifact links unless `NEXT_PUBLIC_EDITION='enterprise'`. Rationale: CE must show the interaction join link without recording UI or runtime EE dependencies. - Fixed the MSP composition adapter for client-originated Teams scheduling: client UI attendees are converted from `{ emailAddress: string }` to the Graph attendee shape expected by the scheduling action. - Added `server/src/test/unit/onlineMeetingEditionGating.contract.test.ts` for CE UI/proxy/facade contracts and expanded `packages/clients/src/lib/onlineMeetingArtifactCapture.test.ts` with the CE capture no-op assertion. - Verification: `npx vitest run src/lib/onlineMeetingArtifactCapture.test.ts` from `packages/clients/`; `npx vitest run src/test/unit/onlineMeetingEditionGating.contract.test.ts` from `server/`; `npm -w @alga-psa/clients run typecheck`; `npm -w server run typecheck` all passed. `npm -w @alga-psa/msp-composition run typecheck` is unavailable because that workspace has no `typecheck` script; the server typecheck covers the composition import edge. ## 2026-06-02 — F054-F057 Phase 2 recording/transcript subscriptions - Added EE migration `20260601123000_add_teams_meeting_artifact_subscription_columns.cjs` with `recordings_subscription_id`, `recordings_subscription_expires_at`, `transcripts_subscription_id`, and `transcripts_subscription_expires_at` on `teams_integrations`. Rationale: Phase 2 needs independent Graph subscription state per collection. - Added `ee/packages/microsoft-teams/src/lib/meetings/artifactSubscriptions.ts`: - Creates Graph subscriptions for `communications/onlineMeetings/getAllRecordings` and `communications/onlineMeetings/getAllTranscripts`. - Persists subscription id/expiry on `teams_integrations`. - Renews near-expiry subscriptions and recreates a subscription when Graph returns 404. - Uses clientState only as routing data: `teams-online-meeting-artifacts::`. - Resolves the affected meeting id from `resourceData['@odata.id']`, with a dependency-injected encrypted resource-data decrypt path for encrypted notifications. - Added server job wrapper `teamsMeetingArtifactWebhookHandler.ts`, gated by the same Enterprise edition check as calendar webhooks. The notification job resolves the meeting, skips cancelled/missing rows, then calls the shared `fetchAndPersistMeetingArtifacts` handler so manual refresh and webhook refresh are idempotent. - Added job registration/scheduling: - `renew-teams-meeting-artifact-subscriptions` recurring job, scheduled per tenant only in Enterprise. - `process-teams-meeting-artifact-notification` immediate job for webhook payloads. - Exported `@alga-psa/clients/lib/onlineMeetingArtifactCapture` so server jobs can reuse the capture helper without copying capture logic. - Added route `server/src/app/api/teams/webhooks/recordings/route.ts`: echoes `validationToken` as `text/plain`, responds 202 for notifications, and enqueues one job per notification whose clientState contains tenant routing. - Verification: - `npx vitest run src/test/unit/teamsMeetingArtifactSubscriptions.test.ts src/test/unit/api/teamsRecordingWebhookRoute.test.ts` from `server/` passed. - `npm -w @alga-psa/ee-microsoft-teams run typecheck` passed. - `npm -w @alga-psa/clients run typecheck` passed. - `npm -w server run typecheck` passed.