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

404 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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): 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.