Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

36 KiB
Raw Blame History

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 collectiononline_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): F001F013.
  • Phase B (creation, both paths): F016F036.
  • Phase C (capture Phase 1 + UI): F037F052.
  • Phase D (gating): F053.
  • Phase E (Phase 2 subscriptions): F054F057. 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 <folder> (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: <url> 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=<interaction_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:<tenant>:<recordings|transcripts>.
    • 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.