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

180 lines
11 KiB
Markdown

# PRD — Teams Online Meetings → Interactions, MSP scheduling, recording/transcript capture
- Slug: `teams-online-meetings-interactions`
- Date: `2026-06-01`
- Status: Draft (design converged; see `../../../../.ai/teams-meetings-interactions-consolidated-plan.md` for the long-form design)
## Summary
Make Microsoft Teams online meetings first-class **interaction** records in Alga PSA, let MSP users
**schedule** meetings themselves (not just as a side effect of a client appointment request), and
**capture the recording and transcript** after a meeting ends — surfaced on the contact/client
interactions timeline (and appointment views). Meetings are created as **calendar-backed events** so
their recordings/transcripts are retrievable from Microsoft Graph. Storage: transcript saved as a
durable internal **document**; recording streamed through an internal authenticated **proxy** (raw Graph
content URLs are never exposed), with an optional per-tenant blob-download.
## Problem
Today a Teams meeting only exists as a side effect of an end-client appointment request: the client
requests, an MSP user approves, and (if enabled) Graph creates a standalone online meeting whose join
link is stashed on `appointment_requests`. There is **no record on the interactions timeline**, MSP
users **cannot originate a meeting** themselves, and **nothing captures the recording or transcript**.
Additionally, the current standalone-onlineMeeting creation path cannot reliably surface
recordings/transcripts (Graph requires calendar-backed meetings for those APIs).
## Goals
- Online meetings appear as "Online Meeting" interactions on the contact/client timeline with the join
link, regardless of edition (CE shows join link only).
- MSP users can schedule a Teams meeting from the interactions feed and from the schedule-entry popup.
- After a meeting ends, recording(s) and transcript(s) are captured and surfaced on the interaction and
appointment detail views, via a manual "Refresh recordings" action (Phase 1) and Graph change
notifications (Phase 2).
- Appointment-approval meetings and MSP-initiated meetings share one data model and one interaction
type.
## Non-goals
- Importing externally-organized Teams meetings (per-user OAuth browse/import) — deferred.
- Per-user meeting organizer (everything uses one tenant service account in v1).
- Co-organizer / presenter role assignment for the creating MSP user.
- Providers other than Teams (data model is provider-agnostic but only `teams` is implemented).
- "Recording available" notification emails.
- Operational gold-plating (metrics, dashboards, retry frameworks) beyond the explicit capability
diagnostics and the bounded recording-fetch retry cap below.
## Users and Primary Flows
- **MSP user (internal):**
- Approves a client appointment request with "Generate Teams meeting" → meeting created, interaction +
`online_meetings` row created, capture registered.
- Creates a meeting ad-hoc from the interactions feed (Quick Add → type "Online Meeting" → "Create
Teams meeting") or from the schedule-entry popup.
- Views the interaction: Join, list of recording/transcript artifacts (newest first), status, and a
"Refresh recordings" button.
- **Client user (client portal):** sees Join, and recording/transcript links only when the tenant has
enabled client-portal visibility (default MSP-only).
- **Tenant admin:** configures the meeting organizer service account and the `download_recordings` /
`expose_recordings_in_portal` toggles in the Teams integration settings page.
## UX / UI Notes
- New system interaction type **"Online Meeting"**, icon `video` (provider-agnostic; distinct from the
manual "Meeting" type).
- `InteractionDetails`: "Online Meeting" section — Join + status ("Recording pending" / "No recording") +
"Refresh recordings" + an **artifacts list** (each transcript → "View transcript" opening the internal
document; each recording → "Download recording" hitting the internal proxy).
- `QuickAddInteraction`: when type = Online Meeting, a "Create Teams meeting" toggle (gated on
capability) with start/end.
- `EntryPopup`: "Generate Teams meeting" option when creating a schedule entry.
- Client-portal appointment views: recording/transcript links next to "Join Teams Meeting", gated on the
tenant visibility setting.
- All new interactive elements need stable kebab-case `id`s (UI reflection); all new copy via `t('...')`
i18n keys.
## Requirements
### Functional Requirements
1. Create meetings as **calendar-backed events** (`POST /users/{organizerUpn}/events`,
`isOnlineMeeting: true`, `onlineMeetingProvider: 'teamsForBusiness'`); resolve the onlineMeeting id
from the event `joinUrl`.
2. Persist a `online_meetings` row + an "Online Meeting" interaction for both creation paths
(appointment approval and MSP-initiated), linked by `appointment_request_id` or `schedule_entry_id`.
3. Capture recordings and transcripts as **`online_meeting_artifacts`** rows (collections, not single
values). Transcript content stored as an internal document (client+contact association,
`is_client_visible=false` unless opted in). Recording surfaced via internal proxy; optional blob
download.
4. Manual "Refresh recordings" action (Phase 1) and Graph change-notification subscriptions (Phase 2)
both drive the same idempotent fetch handler.
5. Keep appointment reschedule/cancel in sync: update/delete the calendar event via `provider_event_id`
for new rows; preserve legacy `online_meeting_id` handling for pre-existing standalone meetings;
set status `cancelled` on decline.
6. Tenant settings (Teams integration settings page): organizer service account
(`default_meeting_organizer_upn` + `default_meeting_organizer_object_id`), `download_recordings`,
`expose_recordings_in_portal`.
7. Capability diagnostics surface missing consent / Exchange-side mailbox scoping instead of failing
silently.
### Non-functional Requirements
- **Multi-tenant / Citus:** all new tables distributed on `tenant`, PK includes `tenant`,
application-level integrity (no cross-shard FKs). `online_meeting_artifacts` colocated with
`online_meetings`.
- **Transaction discipline:** all Graph create/update/delete calls run **outside** DB transactions; if
Graph create succeeds but the DB transaction fails, the orphaned calendar event is deleted
(compensation).
- **Edition:** `online_meetings`, `online_meeting_artifacts`, and the interaction type are
edition-agnostic; recording/transcript capture, subscriptions, and renewal jobs are EE-gated.
- **Bounded retry:** recording fetch capped via `recording_fetch_attempts`, terminal `no_recording`.
- **Security:** raw Graph content URLs never sent to any client; proxy enforces auth + tenant + (for
portal) the visibility setting.
## Data / API / Integrations
- **New core migration** — `online_meetings` (PK `(tenant, meeting_id)`; `provider`,
`provider_meeting_id`, `provider_event_id`, `organizer_upn`, `organizer_user_id`, `subject`,
`join_url`, `start_time`, `end_time`, `status`
{scheduled, ended, recording_pending, recording_ready, no_recording, cancelled, failed},
`recording_fetch_attempts`, `last_fetch_at`, `appointment_request_id`, `interaction_id`,
`schedule_entry_id`, `created_by`, timestamps) **and** `online_meeting_artifacts`
(PK `(tenant, artifact_id)`; `meeting_id`, `artifact_type` ∈ {recording, transcript},
`provider_artifact_id`, `content_url`, `document_id`, `file_id`, `created_date_time`, timestamps;
unique `(tenant, meeting_id, artifact_type, provider_artifact_id)`).
- **New core migration** — insert "Online Meeting" into `system_interaction_types` (icon `video`).
- **EE migration** — add `default_meeting_organizer_object_id`, `download_recordings`,
`expose_recordings_in_portal` (+ Phase 2 subscription id/expiry) to `teams_integrations`.
- **Types** — `IOnlineMeeting` + `IOnlineMeetingArtifact` (with `artifacts[]`); optional
`online_meeting` on `IInteraction`.
- **Graph (EE)** — change `createTeamsMeeting`/`update`/`delete` to the events endpoints; new
`fetchMeetingArtifacts` (recordings + transcripts collections, transcript content); reuse
`fetchMicrosoftGraphAppToken` / `resolveTeamsMeetingExecutionConfig`. URL-encode id path segments.
- **Permissions (document, admin-consent)** — `Calendars.ReadWrite` (application) **scoped via an
Exchange Application Access Policy / RBAC to the organizer mailbox**, plus protected/metered
`OnlineMeetingRecording.Read.All`, `OnlineMeetingTranscript.Read.All` on top of existing
`OnlineMeetings.ReadWrite.All` + Teams Application Access Policy.
## Security / Permissions
- `scheduleTeamsMeeting` requires an explicit `hasPermission` check (confirm resource/action in repo
catalog) in addition to `withAuth`.
- Recording proxy route: authenticated, tenant-scoped, portal-visibility-gated.
- Transcript documents default `is_client_visible=false`.
- `Calendars.ReadWrite` must be mailbox-scoped on the Exchange side (Teams Application Access Policy does
not scope calendar/mailbox access).
## Observability
Out of scope beyond capability diagnostics (consent/scoping warnings) — no metrics/dashboards.
## Rollout / Migration
- New tables/type ship in CE & EE; capture features are EE-only and no-op in CE.
- **No backfill** of appointments approved before launch (they keep `online_meeting_*` columns +
email/portal links; no timeline interaction).
- Phase 1 (manual refresh) ships first; Phase 2 (subscriptions + renewal job) follows.
- New Graph permissions + Exchange scoping documented in Teams setup docs; not auto-granted.
## Open Questions
- Exact `hasPermission` resource/action for `scheduleTeamsMeeting` (confirm against repo catalog).
- Whether MSP-initiated meetings should default to including the contact as an attendee or leave it
optional in the UI.
## Acceptance Criteria (Definition of Done)
- Approving an appointment with a Teams meeting creates an "Online Meeting" interaction + `online_meetings`
row (calendar-backed), with no external attendees, and the join link still appears in email/portal.
- An MSP user can create a meeting from the interactions feed and the schedule popup; both produce the
same interaction + row; a schedule entry (when created) has `work_item_id = interaction_id`.
- After recording a meeting, "Refresh recordings" populates transcript document(s) + recording
artifact(s); the interaction lists all artifacts; recording plays via the proxy; no raw Graph URL is
exposed.
- Reschedule updates, and cancel/decline cancels, the calendar event via `provider_event_id` (legacy
rows still handled); all Graph calls run outside DB transactions; orphaned events are cleaned up on DB
failure.
- Client portal shows recording/transcript only when `expose_recordings_in_portal` is on.
- CE build shows the interaction with join link only and no recording UI errors.
- Migrations apply/rollback cleanly on plain Postgres and Citus.