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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
36 KiB
36 KiB
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 standalonePOST /onlineMeetings. Reason: Graph recordings/transcripts APIs only work reliably for calendar-backed meetings. Cost: newCalendars.ReadWriteapp permission + onlineMeeting-id resolution from joinUrl + invite-behavior decision. - Provider-agnostic "Online Meeting" interaction type (icon
video), NOT "Teams Meeting" — theonline_meetings.providercolumn 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_idvsschedule_entry_id. No backfill.
- an interaction; differ only by
- 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_artifactschild 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
- Scope
Calendars.ReadWriteto the organizer mailbox via Exchange Application Access Policy / RBAC (Teams policy does NOT scope calendar). → F051, F052, T075. - Update/delete use
provider_event_idfor new rows; legacyonline_meeting_idhandling preserved. → F027, T038, T039. - Invite behavior locked: appointment approval = no external attendees; MSP-initiated = attendees allowed. → F018, T025, T026.
- No "co-organizer" claim unless a real Graph meeting-options step is added (deferred). → F018 (attendee only), §9 deferred.
- All Graph create/update/delete OUTSIDE DB transactions, with create→DB-fail compensation. → F028, F029, T040, T041.
Discoveries about existing code (verified)
approveAppointmentRequestwraps work inwithTransaction; existingif (createdMeeting)block ~appointmentRequestManagementActions.ts:790-793; writesonline_meeting_*columns.- Reschedule
updateAppointmentRequestDateTimecallsupdateTeamsMeetinginsidewithTransaction(~:1321) — MUST move out (F028). Cancel/delete paths (scheduleActions.ts:732, client-portalappointmentRequestActions.ts:1337) already call Graph outside the tx. - Facade
CreateTeamsMeetingResultreturns only{joinWebUrl, meetingId}(teamsMeetingService.ts), EE same (createTeamsMeeting.ts:107). Must add organizer UPN + AAD id + eventId (F020). InteractionModel.addInteraction/getByIdopen their owncreateTenantKnexconnection (interactions.ts:225), so the action'swithTransactiondoes 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).uploadDocumentiswithAuth+FormData(documentActions.ts:2642) — unusable from a job; need an internal helper (F038). Internal users inherit folderis_client_visible(documentActions.ts:2764) — set false explicitly.system_interaction_typesmodification trigger was REMOVED byserver/migrations/20250613000000_remove_system_interaction_types_trigger.cjs— no workaround needed.Meeting(icon users) already seeded by20241223015715_create_system_interaction_types.cjs; we add a separateOnline Meeting(icon video).ScheduleEntry.createstoreswork_item_idfor non-ad_hoc(scheduleEntry.ts:407) → set to interaction id (F033).- Calendar mapping uses
entry.notesas description in BOTHserver/src/utils/calendar/eventMapping.ts:82andpackages/integrations/src/utils/calendar/eventMapping.ts:80(F036). - EE calendar sync (
ee/packages/calendar/) auto-pushes schedule_entries to the user's connected calendar viacalendarSyncSubscriber— partially compensates for the service-account-organizer model (meeting still shows on the creating user's own calendar). Possible Phase-2 reuse:CalendarWebhookProcessor.tsfor the encrypted subscription machinery. scheduleRecurringJobexists (server/src/lib/jobs/jobScheduler.ts+index.ts); there is already aMicrosoftWebhookRenewalJobDatarecurring job to mirror for Phase 2 (F055).microsoft_profilesare tenant-level service accounts;usershas no AAD columns → no per-user organizer today.
Open questions
- Exact
hasPermissionresource/action forscheduleTeamsMeeting(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:falsefeature whose deps are met, implement + test, flipimplemented: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 inMspClientCrossFeatureProvider, and had QuickAdd callscheduleTeamsMeetingonly when the Online Meeting type is selected andgetTeamsMeetingCapabilityreports available. Rationale: keeps clients package reusable while MSP composition owns cross-feature wiring. - Implemented direct schedule-entry Teams generation by having
EntryPopupadd agenerate_teams_meetingsave payload for new entries andScheduleCalendarroute that payload toscheduleTeamsMeeting({ 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
scheduleTeamsMeetingnotes behavior: stored interaction/schedule notes now appendJoin Teams Meeting: <url>even when caller-supplied notes exist. - Updated both calendar mappers (
server/src/utils/calendar/eventMapping.tsandpackages/integrations/src/utils/calendar/eventMapping.ts) to append the join URL fromonline_meetingsbyschedule_entry_idas 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 typechecknpm -w @alga-psa/scheduling run typechecknpm -w @alga-psa/integrations run typechecknpx 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"fromserver/packages/msp-compositionhas 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 toOnlineMeetingModeland 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. fetchAndPersistMeetingArtifactsis session-agnostic and takes explicittenantId,meetingId, and optionalactorUserId. It resolves the meeting, skips cancelled rows, calls EEfetchMeetingArtifacts, 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: insertsdocuments,document_block_content, anddocument_associationsdirectly inside a transaction with explicit tenant/user metadata. Client/contact associations are resolved from the linked interaction becauseonline_meetingsintentionally storesinteraction_id, not duplicate client/contact columns. - Recording capture stores
content_urlby default. Whendownload_recordingsis enabled, it resolves the EE Teams Graph config, fetches an app token, downloads the Graph content URL server-side, stores the blob withStorageService.uploadFile, and setsfile_id. refreshMeetingRecordings(meetingId)was added topackages/clients/src/actions/onlineMeetingActions.tsunderwithAuth, passing the authenticated user id to the shared handler.- Exported
meetings/meetingConfigfrom@alga-psa/ee-microsoft-teams/libso the recording download helper can reuse the existing Graph config resolver. - Verification:
npm -w @alga-psa/clients run typechecknpm -w @alga-psa/scheduling run typechecknpm -w @alga-psa/ee-microsoft-teams run typechecknpx vitest run ../packages/clients/src/lib/onlineMeetingArtifactCapture.test.tsfromserver/
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 uponline_meeting_artifactsjoined toonline_meetingsby(tenant, meeting_id), and returns 404 for cross-tenant artifact ids because the query is tenant-scoped. - Proxy behavior: fetches the raw Graph
content_urlserver-side with the EE Teams Graph config + app token, forwardsRangewhen present, streamsgraphResponse.bodywith content headers, and never redirects or serializes the raw Graph URL to the caller. - Portal guard:
?portal=trueis denied unlessteams_integrations.expose_recordings_in_portalis true; missing column/table defaults to false until F048 lands. - Verification:
npx vitest run src/test/unit/onlineMeetingRecordingProxy.contract.test.tsfromserver/npm -w @alga-psa/ee-microsoft-teams run typecheck
2026-06-01 implementation notes
- Implemented F012/F013:
InteractionModel.addInteractionandgetByIdnow accept an optionalKnex/transaction and use it for both the insert and follow-up read; the default path still usescreateTenantKnex(tenantId).- Added
packages/clients/src/actions/interactionCreateHelper.tsas 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. addInteractionnow uses the helper inside its transaction but defers the helper-provided side effects until afterwithTransactionreturns, preserving post-commit event/revalidate behavior.getInteractionByIdnow passes its active transaction intoInteractionModel.getById.
- Implemented T017-T022 with focused unit tests:
packages/clients/src/models/interactions.transaction.test.tscovers trx-backed insertion and no-trx back-compat.packages/clients/src/actions/interactionCreateHelper.test.tscovers 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.tsfromserver/passed 6 tests.npm -w @alga-psa/clients run typecheckpassed.
- Implemented F014:
- Added
packages/clients/src/actions/onlineMeetingActions.tswithgetOnlineMeetingForInteractionunderwithAuth, delegating toOnlineMeetingModel.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.tscovering tenant-scoped lookup, absent rows, and missing id validation.
- Added
- Implemented F016-F022:
createTeamsMeetingnow creates calendar-backed Graph events throughPOST /users/{organizerUpn}/eventswithisOnlineMeeting: trueandonlineMeetingProvider: teamsForBusiness.- It resolves the
onlineMeeting.idfrom the event join URL via a URL-encodedJoinWebUrlfilter, then returnsjoinWebUrl,meetingId,organizerUpn,organizerUserId, andeventId. organizerUserIdis read fromteams_integrations.default_meeting_organizer_object_idwhen 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/deleteTeamsMeetingnow call/events/{eventId}(falling back tomeetingIdonly for temporary compatibility until F027 legacy branching is implemented).- Added EE
fetchMeetingArtifactsand facadefetchMeetingArtifactsno-op off enterprise. The EE implementation fetches recordings/transcripts collections, downloads transcript content withAccept: 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.tsfromserver/passed 23 tests.npm -w @alga-psa/scheduling run typecheckpassed.npm -w @alga-psa/ee-microsoft-teams run typecheckpassed.
2026-06-01 implementation notes
- Completed F001-F006 in
server/migrations/20260601120000_create_online_meetings.cjs: added CE/coreonline_meetingsandonline_meeting_artifactswith tenant-first PKs, PRD status/artifact type checks, provider-meeting uniqueness, tenant-leading lookup indexes, Citustransaction:falsedistribution ontenant, and artifact colocation withonline_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-onlyOnline Meetingsystem interaction type withvideoicon, following the existingadd_general_interaction_typeguard 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 laterOnlineMeetingModel.upsertArtifacthelper, not just the unique constraint. - Verification:
npx vitest run src/test/unit/migrations/onlineMeetingsMigration.test.tsfromserver/passed (8 tests). An earliernpm -w server run test:unit -- ...accidentally ran the entire unit suite due to the package script prependingsrc/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.tsandpackages/types/src/interfaces/interaction.interfaces.ts: addedIOnlineMeetingArtifact,IOnlineMeeting, status/artifact/provider type aliases,artifacts[], and optionalIInteraction.online_meeting. Exported viapackages/types/src/interfaces/index.tsand added@alga-psa/typessmoke imports topackages/types/src/exports.typecheck.test.ts. - Verification:
npm -w @alga-psa/types test -- src/exports.typecheck.test.ts src/interfaces/barrel.test.tspassed (2 tests). - Completed F010-F011/T008/T012-T016 in
packages/clients/src/models/onlineMeeting.ts: added session-agnosticOnlineMeetingModelusingcreateTenantKnex, 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 frompackages/clients/src/models/index.ts. - Verification:
npx vitest run ../packages/clients/src/models/onlineMeeting.test.tsfromserver/passed (6 tests);npm -w @alga-psa/clients run typecheckpassed. - Completed F023-F024 in
packages/scheduling/src/actions/appointmentRequestManagementActions.ts: approval now uses the shared interaction helper to create anOnline Meetinginteraction and inserts a linkedonline_meetingsrow 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 legacyappointment_requests.online_meeting_*columns remain populated for existing email/portal consumers. - Package wiring:
@alga-psa/schedulingnow depends on@alga-psa/clients, and@alga-psa/clientsexposes the narrow./actions/interactionCreateHelpersubpath used by scheduling.@alga-psa/licensingexports were pointed atsrcto 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 andonline_meetingsrow, the capability-unavailable path asserts no row is written, and the legacy appointment request columns remain asserted. The fixture now tolerates current schemas whereservice_types.billing_methodhas 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"fromserver/passed (3 tests, 38 skipped).npm -w @alga-psa/scheduling run typecheckpassed.- A full
npx vitest run src/test/integration/appointmentRequests.integration.test.tsrun was attempted before the fixture compatibility fix and failed on the pre-existingservice_types.billing_methodfixture/schema mismatch across many unrelated tests; targeted coverage now passes.
- Completed F025-F027:
updateAppointmentRequestDateTimenow syncs linkedonline_meetingsstart/end fields and the generated interaction's date/start/end/duration, then callsupdateTeamsMeetingafter the DB transaction withprovider_event_idfor modern rows. Legacy appointment-only meetings passeventId: nulland keep the standalone-compatible fallback path.- Decline, MSP schedule deletion, and client-portal cancellation now mark linked
online_meetingsrows ascancelled, which keeps them out of later capture/polling flows. - Delete paths prefer
online_meetings.provider_meeting_id+provider_event_id; legacy rows withoutprovider_event_idstill pass the appointment request's existingonline_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 assertseventId: null, and decline/cancel/delete tests assertonline_meetings.status='cancelled'. - Verification:
npm -w @alga-psa/scheduling run typecheckpassed.npm -w @alga-psa/client-portal run typecheckpassed.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"fromserver/passed (5 tests, 37 skipped).- The focused integration run still logs pre-existing non-fatal warnings in decline event publishing
(
requestedDateDate vs string) and client cancellation notifications (contact_idbinding).
- Completed F028-F029:
- Appointment approval now performs a read/validation preflight, calls
createTeamsMeetingoutside the write transaction, then consumes the prepared Graph result inside the DB transaction. - If the post-create DB transaction fails, the action calls
deleteTeamsMeetingwith the createdmeetingIdandeventId, 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.
- Appointment approval now performs a read/validation preflight, calls
- Completed T040-T041:
- Added
server/src/test/unit/scheduling/appointmentRequestTeamsTransaction.test.tsto guard that create/update/delete Graph calls are outside transaction bodies. - Added an approval integration test that hides the
Online Meetinginteraction 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-25to2026-08-25; the old date is now in the past relative to the 2026-06-02 runtime clock and fails request creation before approval.
- Added
- Verification:
npm -w @alga-psa/scheduling run typecheckpassed.npx vitest run src/test/unit/scheduling/appointmentRequestTeamsTransaction.test.tsfromserver/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"fromserver/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.jsonrace 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 noonline_meetingsrow and noOnline Meetingtimeline interaction are created just because the new schema exists. - Added the coverage to
server/src/test/integration/appointmentRequests.integration.test.ts.
- Confirmed the rollout remains no-backfill: a legacy approved appointment with existing
- Verification:
npx vitest run src/test/integration/appointmentRequests.integration.test.ts -t "does not backfill legacy approved Teams appointment links"fromserver/passed (1 test, 43 skipped).
- Completed F031-F033:
- Added
packages/scheduling/src/actions/onlineMeetingSchedulingActions.tsand exportedscheduleTeamsMeetingfrom@alga-psa/scheduling/actions. - The action is
withAuthand explicitly requiresuser_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
meetingIdandeventId. - The DB transaction creates the shared
Online Meetinginteraction, inserts theonline_meetingsrow with organizer UPN/object id and event id, and optionally creates aschedule_entriesrow withwork_item_type='interaction'andwork_item_id=<interaction_id>, linked fromonline_meetings.schedule_entry_id. - MSP-created meetings pass through provided attendees to the facade; appointment approval remains separate and still sends no attendees.
- Added
- 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_meetingspersistence. - Permission-denied and capability-unavailable paths assert no Graph create and no local interaction.
- Schedule-entry option asserts
work_item_type='interaction',work_item_idequals the created interaction id, assignee row creation, andonline_meetings.schedule_entry_idlinkage. - Test cleanup now tracks standalone online meeting and interaction ids because these rows are not appointment-request-linked.
- Happy path asserts Graph facade input, attendee pass-through, default organizer persistence from the
facade result, interaction creation, and
- Verification:
npm -w @alga-psa/scheduling run typecheckpassed.npx vitest run src/test/integration/appointmentRequests.integration.test.ts -t "Schedule Teams Meeting"fromserver/passed twice (4 tests, 44 skipped). The second run includes a package-level@alga-psa/event-bus/publishersmock so the new action tests do not touch Redis.
2026-06-01 - F043-F045 / T066-T068
- Implemented interaction read enrichment:
InteractionModel.getByIdandgetForEntityattachonline_meetingby callingOnlineMeetingModel.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
InteractionDetailsonline meeting section with Join, status, Refresh recordings, transcript document links, and recording proxy links. Artifact links use internal routes only:/api/documents/{documentId}/downloadfor transcripts and/api/online-meetings/recordings/{artifactId}for recordings. - Fixed
InteractionIconfallback soonline meetingmaps to thevideoicon before the generic meeting/users fallback. - Added source contract tests for model enrichment and UI wiring:
packages/clients/src/models/interactions.onlineMeeting.contract.test.tsandpackages/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.tsfromserver/;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_artifactsonly whenteams_integrations.expose_recordings_in_portal=true; missing settings/columns default to hidden. Returned rows intentionally omit raw Graphcontent_url. - Added MSP EntryPopup artifact metadata through
getAppointmentRequestByIdwithout portal gating, so internal users can see recordings/transcripts next to the existing Join Teams Meeting action. - Added artifact buttons to
AppointmentsPage,AppointmentRequestDetailsPage, andEntryPopup. Transcript buttons use/api/documents/{documentId}/download; recording buttons use/api/online-meetings/recordings/{artifactId}with?portal=truefor 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 uset(...)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.tsfromserver/;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_integrationsis absent. It addsdefault_meeting_organizer_object_idas nullable text, plusdownload_recordingsandexpose_recordings_in_portalas 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.tsfromserver/.
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 loadsgetTeamsMeetingsTabStateor exposes the olddefault-meeting-organizer-upn/ save / verify controls. - Added organizer + recording settings to the Teams integration settings page:
teams-default-meeting-organizer-upn,teams-download-recordings, andteams-expose-recordings-in-portal, all usingintegrations.teams.settings.*i18n keys. - Extended Teams integration contracts/actions in both shared and EE packages with
defaultMeetingOrganizerUpn,defaultMeetingOrganizerObjectId,downloadRecordings, andexposeRecordingsInPortal. - Save now resolves the organizer UPN to a Microsoft Entra object id via Graph
/users/{upn}using the selected Microsoft profile app credentials, then persistsdefault_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.tsfor organizer object-id resolution and recording toggle persistence; added source contractserver/src/test/unit/integrations/teamsIntegrationMeetingSettings.contract.test.tsfor 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, andnpx vitest run src/actions/integrations/teamsActions.test.tspassed. The server source-contract Vitest assertions passed, but one parallel run ended after coverage withENOENT ... server/coverage/.tmp; rerun before commit. - Rerun note: source-contract test passed cleanly when run alone; the earlier coverage
.tmperror was from concurrent Vitest runs.
2026-06-02 — F051/F052 recording diagnostics and setup docs
- Extended
getTeamsMeetingCapabilityto returnrecordingsAvailableseparately fromavailable. Meeting creation can be available while recording capture is not ready; missing organizer object id now reportsrecordingReason='missing_organizer_object_id'. - Added a Teams diagnostics step
recording_permissionsthat 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
TeamsIntegrationSettingsso warnings render with i18n-backed copy. - Updated
docs/integrations/teams-meetings-setup.mdand the browser-servedserver/public/docs/integrations/teams-meetings-setup.mdto 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.tsand 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 typecheckall passed.
2026-06-02 — F053 edition gating
- Made recording capture truly inert in CE:
fetchAndPersistMeetingArtifactsnow returns the existing online meeting before loading settings, fetching Graph artifacts, upserting artifacts, incrementingrecording_fetch_attempts, or changing status whenisEnterpriseis false. Added an injectableisEnterpriseEditiondependency so the boundary is runtime-tested. - Gated the internal recording proxy route with
isEnterprisebefore 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 unlessNEXT_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.tsfor CE UI/proxy/facade contracts and expandedpackages/clients/src/lib/onlineMeetingArtifactCapture.test.tswith the CE capture no-op assertion. - Verification:
npx vitest run src/lib/onlineMeetingArtifactCapture.test.tsfrompackages/clients/;npx vitest run src/test/unit/onlineMeetingEditionGating.contract.test.tsfromserver/;npm -w @alga-psa/clients run typecheck;npm -w server run typecheckall passed.npm -w @alga-psa/msp-composition run typecheckis unavailable because that workspace has notypecheckscript; 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.cjswithrecordings_subscription_id,recordings_subscription_expires_at,transcripts_subscription_id, andtranscripts_subscription_expires_atonteams_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/getAllRecordingsandcommunications/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.
- Creates Graph subscriptions for
- 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 sharedfetchAndPersistMeetingArtifactshandler so manual refresh and webhook refresh are idempotent. - Added job registration/scheduling:
renew-teams-meeting-artifact-subscriptionsrecurring job, scheduled per tenant only in Enterprise.process-teams-meeting-artifact-notificationimmediate job for webhook payloads.- Exported
@alga-psa/clients/lib/onlineMeetingArtifactCaptureso server jobs can reuse the capture helper without copying capture logic.
- Added route
server/src/app/api/teams/webhooks/recordings/route.ts: echoesvalidationTokenastext/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.tsfromserver/passed.npm -w @alga-psa/ee-microsoft-teams run typecheckpassed.npm -w @alga-psa/clients run typecheckpassed.npm -w server run typecheckpassed.