# SCRATCHPAD — Workflow Event Catalog v2 Domain Events

Rolling notes for this plan: discoveries, decisions, links, commands, and gotchas.

## Source Material

- Event proposals inventory: `ee/docs/plans/2025-12-28-workflow-event-catalog/event-proposals.md`
- Existing Events Catalog v2 plan: `ee/docs/plans/2025-12-28-workflow-event-catalog/PRD.md`
- This plan: `ee/docs/plans/2026-01-23-workflow-event-catalog-domain-events/PRD.md`
- Features/tests: `ee/docs/plans/2026-01-23-workflow-event-catalog-domain-events/features.json` and `ee/docs/plans/2026-01-23-workflow-event-catalog-domain-events/tests.json`

## Key Discovery: Event Bus Already Has a Large Event Surface

`shared/workflow/streams/eventBusSchema.ts` already defines many event types beyond what exists in `system_event_catalog` migrations, including:

- Ticket comment events (`TICKET_COMMENT_ADDED`, `TICKET_COMMENT_UPDATED`)
- Scheduling events (`SCHEDULE_ENTRY_CREATED`, `SCHEDULE_ENTRY_UPDATED`, `SCHEDULE_ENTRY_DELETED`)
- Accounting export and calendar sync events
- Document mention event (`USER_MENTIONED_IN_DOCUMENT`)
- RMM events (`RMM_*`)

Implication: adding events is not only “add catalog rows”; we must keep **catalog**, **event bus schema/types**, and **workflow schema registry refs** aligned.

## Key Discovery: Schema Registry (Workflow Runtime v2) Is Separate From Event Bus Zod Schemas

Workflow runtime v2 registers payload schemas in `shared/workflow/runtime/init.ts` (schema registry keys like `payload.TicketCreated.v1`).

Event bus has its own Zod payload schemas in `shared/workflow/streams/eventBusSchema.ts`.

Implication: for each new catalog entry we want form-mode simulation and trigger mapping, we likely need:

- `payload_schema_ref` → schema registry registration (`payload.*.v1`)
- and (if the event is published through the event bus) inclusion in `EventTypeEnum` + `EventPayloadSchemas` mapping.

## Current Event Publishing Path (observed)

- Workflow v2 simulation / submission path uses: `packages/workflows/src/actions/workflow-event-actions.ts` (`submitWorkflowEventAction`) which publishes to the event bus and validates `event_type` exists in catalog tables.
- There are multiple non-workflow publishers already in the repo (examples found via ripgrep):
  - `packages/billing/src/services/accountingExportService.ts`
  - `packages/integrations/src/services/calendar/CalendarSyncService.ts`
  - `server/src/services/email/EmailProcessor.ts` (publishes `INBOUND_EMAIL_RECEIVED`)

Implication: we should standardize on `@alga-psa/event-bus/publishers` helpers for new emissions, and ensure payload shapes match both schemas.

## Decisions Needed (Blocking)

1. **Ticket messages vs ticket comments**
   - Keep existing `TICKET_COMMENT_*` as canonical, and map proposed `TICKET_MESSAGE_*` onto those semantics?
   - Or introduce `TICKET_MESSAGE_*` and deprecate `TICKET_COMMENT_*`?
   - **Decision (2026-01-23):** Introduce `TICKET_MESSAGE_*` as canonical workflow v2 domain events; keep `TICKET_COMMENT_*` as legacy/back-compat (no automatic aliasing due to payload shape differences).
2. **Scheduling appointments vs schedule entries**
   - Reuse existing `SCHEDULE_ENTRY_*` as canonical?
   - Or introduce `APPOINTMENT_*` (and decide whether a schedule entry is an appointment)?
   - **Decision (2026-01-23):** Introduce `APPOINTMENT_*` as canonical workflow triggers; keep `SCHEDULE_ENTRY_*` for lower-level schedule entry changes and existing integrations.
3. **Client/company identifiers**
   - Standardize payloads on `clientId`, `companyId`, or include both?
4. **Payload casing**
   - **Decision (2026-01-23):** use camelCase payload keys for all new `payload.*.v1` schemas and event bus payloads (documented in `ee/docs/plans/2026-01-23-workflow-event-catalog-domain-events/PRD.md` §6.2).
5. **Catalog-only events**
   - Which of SLA/approvals/dispatch/signatures are expected to be emitted now vs later?

## Work Log

- 2026-01-23: Completed `F001` (payload conventions) by expanding `PRD.md` §6.2 with:
  - camelCase key requirement
  - required `tenantId` + `occurredAt`
  - actor field guidance (`actorUserId` / `actorContactId` / optional `actorType`)
  - transition and mutation conventions (`previousX`/`newX`, `updatedFields`, `changes`)
- 2026-01-23: Completed `F002` (shared publishing helpers + hooks):
  - Added payload enrichment helper `shared/workflow/streams/workflowEventPublishHelpers.ts`:
    - ensures `tenantId` and `occurredAt`
    - enriches actor fields (`actorType`, `actorUserId`, `actorContactId`)
    - carries optional `idempotencyKey`
  - Added publish-time hooks for workflow stream conversion:
    - `shared/workflow/streams/eventBusSchema.ts` now supports `WorkflowPublishHooks` and `convertToWorkflowEvent(event, hooks)`
    - workflow `user_id` now prefers `payload.actorUserId` (falls back to `payload.userId`)
  - Wired hooks through both event bus implementations:
    - `packages/event-bus/src/eventBus.ts` and `server/src/lib/eventBus/index.ts` accept `{ workflow }` publish options
  - Added convenience publisher wrappers:
    - `packages/event-bus/src/publishers/index.ts` and `server/src/lib/eventBus/publishers/index.ts` export `publishWorkflowEvent(...)`
- 2026-01-23: Completed `F003` (schema registry payload schemas + registrations):
  - Added shared payload building blocks in `shared/workflow/runtime/schemas/commonEventPayloadSchemas.ts`:
    - `tenantId` + `occurredAt` required on all new payload schemas
    - optional actor fields (`actorUserId`, `actorContactId`, `actorType`)
    - shared `updatedFields[]` and `changes{path:{previous,new}}` helpers
  - Added `payload.*.v1` Zod payload schemas for every event in `event-proposals.md`:
    - tickets: `shared/workflow/runtime/schemas/ticketEventSchemas.ts`
    - scheduling: `shared/workflow/runtime/schemas/schedulingEventSchemas.ts`
    - projects: `shared/workflow/runtime/schemas/projectEventSchemas.ts`
    - billing: `shared/workflow/runtime/schemas/billingEventSchemas.ts`
    - CRM/tags: `shared/workflow/runtime/schemas/crmEventSchemas.ts`
    - documents: `shared/workflow/runtime/schemas/documentEventSchemas.ts`
    - email/notifications/surveys: `shared/workflow/runtime/schemas/communicationsEventSchemas.ts`
    - integrations: `shared/workflow/runtime/schemas/integrationEventSchemas.ts`
    - assets/media: `shared/workflow/runtime/schemas/assetMediaEventSchemas.ts`
    - company: `shared/workflow/runtime/schemas/companyEventSchemas.ts`
  - Centralized schema registrations in `shared/workflow/runtime/schemas/workflowEventPayloadSchemas.ts` and updated `shared/workflow/runtime/init.ts` to register the full map at init.
  - Verified registry coverage: all `payload.<PascalCase>.v1` refs implied by `event-proposals.md` are present in `workflowEventPayloadSchemas`.
- 2026-01-23: Completed `F004` (event bus schema/types + overlap resolution):
  - Expanded `shared/workflow/streams/eventBusSchema.ts`:
    - added all proposed domain `EventTypeEnum` values from `event-proposals.md`
    - mapped new domain event types to workflow runtime payload schemas (camelCase + `occurredAt`)
    - kept legacy event payloads working via Zod unions for overlapping “already present” events
    - added a runtime guard to ensure every `EVENT_TYPES` entry has a payload schema
  - Removed duplicate schema/type definitions by re-exporting shared schema from:
    - `packages/event-bus/src/events.ts`
    - `server/src/lib/eventBus/events.ts`
  - Aligned catalog typing with the shared event bus enum:
    - `shared/workflow/types/eventCatalog.ts`
  - Added focused test coverage:
    - `shared/workflow/streams/__tests__/eventBusSchema.expandedEvents.test.ts`
  - Documented overlap decisions in `ee/docs/plans/2026-01-23-workflow-event-catalog-domain-events/PRD.md` §10.
- 2026-01-23: Completed `F005` (catalog migration upsert for expanded domain events):
  - Added idempotent system catalog upsert migration: `server/migrations/20260123150000_upsert_domain_workflow_event_catalog_v2.cjs`
    - Upserts **134** total events into `system_event_catalog`:
      - **122** proposed events parsed from `ee/docs/plans/2025-12-28-workflow-event-catalog/event-proposals.md`
      - **12** “already present” core events (Company/Ticket/Project/Invoice/Email provider)
    - Normalizes `name`, `description`, `category`, and `payload_schema_ref` for each row.
    - Sets `payload_schema_ref` as `payload.<PascalCase(event_type)>.v1` (e.g. `INBOUND_EMAIL_RECEIVED` → `payload.InboundEmailReceived.v1`).
    - Intentionally does **not** manage legacy `payload_schema` JSON in catalog rows (workflow v2 uses schema registry via `payload_schema_ref`).
- 2026-01-23: Completed `F006` (simulation uses `payload_schema_ref` + schema validation):
  - Server-side event ingestion now enriches simulator payloads and validates them against the schema registry:
    - `server/src/lib/actions/workflow-runtime-v2-actions.ts` now:
      - derives `sourcePayloadSchemaRef` from submission override or catalog `payload_schema_ref`
      - enriches payload via `buildWorkflowPayload(...)` (adds `tenantId`, `occurredAt`, `actorUserId`, `actorType`)
      - rejects invalid payloads with `400` including Zod `issues`, and records an error `workflow_runtime_event`
  - Added schema registry coverage tests (no DB required):
    - `shared/workflow/runtime/__tests__/workflowEventSimulatorSchemas.test.ts`
  - Attempted DB-backed integration test run, but this sandbox cannot connect to localhost Postgres (EPERM); kept verification to schema-level tests.

- 2026-01-23: Completed `F010` (ticket transition emission):
  - Emitted workflow v2 domain events from real ticket update paths:
    - `packages/tickets/src/actions/ticketActions.ts` now publishes: `TICKET_STATUS_CHANGED`, `TICKET_PRIORITY_CHANGED`, `TICKET_UNASSIGNED`, `TICKET_REOPENED`, `TICKET_ESCALATED`, `TICKET_QUEUE_CHANGED` via `publishWorkflowEvent(...)` and shared transition detection.
    - `packages/tickets/src/actions/optimizedTicketActions.ts` updated similarly (this is the main cached ticket update path).
  - Upgraded legacy ticket lifecycle emissions to include workflow v2 context:
    - `TICKET_UPDATED`, `TICKET_ASSIGNED`, `TICKET_CLOSED` now emit via `publishWorkflowEvent(...)` (adds `occurredAt` + `actorUserId`), and `TICKET_ASSIGNED` includes `previousAssignee*`/`newAssignee*` fields for domain-style payloads while keeping legacy `userId`.
  - Added a pure transition builder for testability: `packages/tickets/src/lib/workflowTicketTransitionEvents.ts`.
  - Extended ticket zod schemas to include ITIL escalation fields (present in DB): `packages/tickets/src/schemas/ticket.schema.ts`.
  - Added unit coverage for transition detection: `packages/tickets/src/lib/__tests__/workflowTicketTransitionEvents.test.ts` (runs without DB).

- 2026-01-23: Completed `F011` (ticket relationship/aggregation emission):
  - Implemented `TICKET_MERGED` / `TICKET_SPLIT` emissions via ticket bundling actions:
    - `packages/tickets/src/actions/ticketBundleActions.ts` now publishes:
      - `TICKET_MERGED` when child tickets are attached to a master bundle (bundle/add children/promote master).
      - `TICKET_SPLIT` when child tickets are detached (remove child/unbundle master).
    - Semantics note: these events represent **bundle attach/detach** in today’s product (there is no true destructive “merge tickets” feature yet); payload still follows proposed schema (`sourceTicketId`/`targetTicketId`, `originalTicketId`/`newTicketIds`) and includes `reason` for workflow authors.
  - Updated bundling integration test harness to mock `@alga-psa/event-bus/publishers` to keep DB-backed tests isolated from the event bus.

- 2026-01-23: Completed `F012` (ticket communication emission):
  - Added a shared builder for ticket communication domain events:
    - `packages/tickets/src/lib/workflowTicketCommunicationEvents.ts` builds additive `TICKET_MESSAGE_ADDED` plus:
      - `TICKET_INTERNAL_NOTE_ADDED` for internal visibility
      - `TICKET_CUSTOMER_REPLIED` when a contact id is available
  - Emitted these events from real comment creation paths (while keeping legacy `TICKET_COMMENT_*` unchanged):
    - `packages/tickets/src/actions/comment-actions/commentActions.ts` (primary comment creation flow)
    - `packages/tickets/src/actions/ticketActions.ts` and `packages/tickets/src/actions/optimizedTicketActions.ts` (legacy/server action flows)
  - Added unit coverage with schema validation via `buildWorkflowPayload(...)`:
    - `packages/tickets/src/lib/__tests__/workflowTicketCommunicationEvents.test.ts`

- 2026-01-23: Completed `F013` (ticket work tracking emission):
  - Emitted `TICKET_TIME_ENTRY_ADDED` when time entries are created for `work_item_type='ticket'`:
    - `server/src/lib/api/services/TimeEntryService.ts` publishes on both direct create (`create`) and time-tracking completion (`stopTimeTracking`).
  - Added a small pure builder for publish-time payload shaping:
    - `server/src/lib/api/services/timeEntryWorkflowEvents.ts`
  - Added unit coverage that validates the built payload against `payload.TicketTimeEntryAdded.v1` via `buildWorkflowPayload(...)`:
    - `server/src/test/unit/timeEntryWorkflowEvents.test.ts`

- 2026-01-23: Completed `F014` (ticket SLA stage emission):
  - Implemented ITIL-backed “resolution SLA” stage events when `itil_priority_level` is present:
    - `TICKET_SLA_STAGE_ENTERED` emitted on ticket create in:
      - `packages/tickets/src/actions/ticketActions.ts` (`addTicket`, `createTicketFromAsset`)
    - `TICKET_SLA_STAGE_MET` / `TICKET_SLA_STAGE_BREACHED` emitted on ticket close in:
      - `packages/tickets/src/actions/ticketActions.ts` (`updateTicket`)
      - `packages/tickets/src/actions/optimizedTicketActions.ts` (`updateTicketWithCache`)
  - Added pure builders and idempotency keys:
    - `packages/tickets/src/lib/workflowTicketSlaStageEvents.ts`
  - Notes/constraints:
    - Stage emitted is currently `resolution` only (no first-class response/custom stage model in product yet).
    - Uses `tenantId` as a stable `slaPolicyId` placeholder until a real SLA policy model exists.

- 2026-01-23: Completed `F015` (ticket approvals — catalog-only):
  - Marked `TICKET_APPROVAL_REQUESTED` / `TICKET_APPROVAL_GRANTED` / `TICKET_APPROVAL_REJECTED` as **catalog-only** for now (no ticket approval subsystem exists to emit these events from).
  - Documented the decision in `ee/docs/plans/2026-01-23-workflow-event-catalog-domain-events/PRD.md` (Open Questions #4).

- 2026-01-23: Completed `F020` (appointment lifecycle/assignment emission):
  - Emitted workflow v2 appointment domain events from real scheduling/appointment flows:
    - `packages/scheduling/src/actions/scheduleActions.ts` emits `APPOINTMENT_CREATED`, `APPOINTMENT_RESCHEDULED`, `APPOINTMENT_CANCELED`, `APPOINTMENT_COMPLETED`, `APPOINTMENT_NO_SHOW`, `APPOINTMENT_ASSIGNED` when schedule entries represent appointments (`work_item_type='appointment_request'` or `ticket`).
    - `packages/client-portal/src/actions/client-portal-actions/appointmentRequestActions.ts` emits `APPOINTMENT_CREATED`, `APPOINTMENT_RESCHEDULED`, `APPOINTMENT_CANCELED`, `APPOINTMENT_ASSIGNED` for client portal appointment requests (actor is `CONTACT`).
    - `packages/scheduling/src/actions/appointmentRequestManagementActions.ts` emits `APPOINTMENT_CREATED`/`APPOINTMENT_ASSIGNED` for the legacy “approve request creates schedule entry” fallback; emits `APPOINTMENT_ASSIGNED` on reassignment during approval.
  - Added shared payload builders:
    - `shared/workflow/streams/domainEventBuilders/appointmentEventBuilders.ts`
  - Added schema-compat unit test coverage:
    - `shared/workflow/streams/domainEventBuilders/__tests__/appointmentEventBuilders.test.ts`
  - Notes/constraints:
    - `timezone` is emitted as `UTC` (schedule UI and storage treat times as UTC today).
    - `APPOINTMENT_NO_SHOW` is emitted only when a schedule entry status is set to a no-show variant; `party` defaults to `customer` until the product captures who no-showed explicitly.

- 2026-01-23: Completed `F021` (schedule block create/delete emission):
  - Implemented `SCHEDULE_BLOCK_CREATED` / `SCHEDULE_BLOCK_DELETED` emission for “availability blocks” represented today as **private ad-hoc schedule entries**:
    - `is_private=true`, `work_item_type='ad_hoc'`, `work_item_id=null`, exactly one `assigned_user_ids` owner.
  - Added shared payload builders:
    - `shared/workflow/streams/domainEventBuilders/scheduleBlockEventBuilders.ts`
  - Emitted events from scheduling actions:
    - `packages/scheduling/src/actions/scheduleActions.ts` publishes on create/delete, and on update when an entry becomes/ceases a private ad-hoc block.
  - Added schema-compat unit test coverage:
    - `shared/workflow/streams/domainEventBuilders/__tests__/scheduleBlockEventBuilders.test.ts`
  - Notes/constraints:
    - `timezone` is emitted as `UTC`.
    - No “block updated” domain event exists yet; time changes to an existing block do not emit a domain event unless the entry crosses the block/non-block boundary.

- 2026-01-23: Completed `F022` (capacity threshold emission):
  - Emitted `CAPACITY_THRESHOLD_REACHED` from real scheduling mutations:
    - `packages/scheduling/src/actions/scheduleActions.ts` publishes after create/update/delete via `maybePublishCapacityThresholdReached(...)`.
  - Implemented team/day capacity math (UTC date) based on existing data model:
    - Capacity limit = sum of `resources.max_daily_capacity` for active team members.
    - Current booked = sum of schedule-entry overlap hours per assignee for that team/date.
    - Event emits only on a threshold **crossing** (previousBooked < limit && currentBooked >= limit).
  - Added shared payload builder + unit coverage:
    - `shared/workflow/streams/domainEventBuilders/capacityThresholdEventBuilders.ts`
    - `shared/workflow/streams/domainEventBuilders/__tests__/capacityThresholdEventBuilders.test.ts`
  - Added scheduling unit coverage for threshold/date math:
    - `packages/scheduling/src/lib/__tests__/capacityThresholdWorkflowEvents.test.ts`

- 2026-01-23: Completed `F023` (technician dispatch lifecycle emission):
  - Added shared payload/status helpers:
    - `shared/workflow/streams/domainEventBuilders/technicianDispatchEventBuilders.ts`
  - Emitted dispatch lifecycle workflow events from real scheduling updates:
    - `packages/scheduling/src/actions/scheduleActions.ts` now publishes:
      - `TECHNICIAN_DISPATCHED` when technicians are newly assigned to a ticket/appointment schedule entry
      - `TECHNICIAN_EN_ROUTE` / `TECHNICIAN_ARRIVED` when schedule entry `status` transitions to an en-route / arrived value (case-insensitive, supports common variants)
      - `TECHNICIAN_CHECKED_OUT` when schedule entry `status` transitions to a checked-out value **or** when the appointment is marked completed
  - Added schema-compat unit test coverage:
    - `shared/workflow/streams/domainEventBuilders/__tests__/technicianDispatchEventBuilders.test.ts`

- 2026-01-23: Completed `F030` (project lifecycle emission):
  - Added shared payload builders:
    - `shared/workflow/streams/domainEventBuilders/projectLifecycleEventBuilders.ts`
  - Emitted domain events from real project update paths:
    - `packages/projects/src/actions/projectActions.ts` publishes `PROJECT_UPDATED` (with `updatedFields` + `{previous,new}` `changes`) and `PROJECT_STATUS_CHANGED` on status transitions via `publishWorkflowEvent(...)`.
    - `server/src/lib/api/services/ProjectService.ts` publishes the same domain events for REST API updates.
  - Kept legacy email notifications working with the new payload shape:
    - `server/src/lib/eventBus/subscribers/projectEmailSubscriber.ts` now supports both legacy `{ changes: Record<string, unknown> }` and domain `{ changes: Record<string, {previous,new}> }` shapes and uses `actorUserId` when `userId` is absent.
  - Added schema-compat unit test coverage:
    - `shared/workflow/streams/domainEventBuilders/__tests__/projectLifecycleEventBuilders.test.ts`
  - Cleanup: removed forbidden feature-to-feature import (projects → clients) by querying contacts directly in `packages/projects/src/actions/projectActions.ts`.

- 2026-01-23: Completed `F031` (project task lifecycle emission):
  - Added shared payload builders + schema-compat unit tests:
    - `shared/workflow/streams/domainEventBuilders/projectTaskEventBuilders.ts`
    - `shared/workflow/streams/domainEventBuilders/__tests__/projectTaskEventBuilders.test.ts`
  - Emitted workflow v2 domain events from real task creation/update paths:
    - `packages/projects/src/actions/projectTaskActions.ts` publishes:
      - `PROJECT_TASK_CREATED` on task create/duplicate
      - `PROJECT_TASK_ASSIGNED` on primary assignee changes (domain payload with `assignedToId/assignedToType`)
      - `PROJECT_TASK_STATUS_CHANGED` + `PROJECT_TASK_COMPLETED` when status mapping transitions (completed = enters a closed status)
    - `packages/projects/src/actions/phaseTaskImportActions.ts` publishes `PROJECT_TASK_CREATED` (+ `PROJECT_TASK_ASSIGNED` when assigned) for imported tasks.
    - `server/src/lib/api/services/ProjectService.ts` publishes the same task lifecycle events for REST API create/update paths.
    - `server/src/lib/models/scheduleEntry.ts` now publishes domain-shaped `PROJECT_TASK_ASSIGNED` when schedule-driven dispatch assigns a task.
  - Updated legacy notification/email subscribers to tolerate domain payloads:
    - `server/src/lib/eventBus/subscribers/internalNotificationSubscriber.ts` (PROJECT_TASK_ASSIGNED)
    - `server/src/lib/eventBus/subscribers/projectEmailSubscriber.ts` (PROJECT_TASK_ASSIGNED)
  - Verification:
    - `npx vitest run shared/workflow/streams/domainEventBuilders/__tests__/projectTaskEventBuilders.test.ts`

- 2026-01-23: Completed `F032` (project dependency events emission):
  - Added shared payload builders + schema-compat unit tests:
    - `shared/workflow/streams/domainEventBuilders/projectTaskEventBuilders.ts` (dependency blocked/unblocked payload builders)
    - `shared/workflow/streams/domainEventBuilders/__tests__/projectTaskEventBuilders.test.ts`
  - Emitted dependency lifecycle workflow events from real dependency CRUD paths:
    - `packages/projects/src/actions/projectTaskActions.ts` publishes:
      - `PROJECT_TASK_DEPENDENCY_BLOCKED` when a blocking relationship is created (`dependency_type` in {`blocks`,`blocked_by`})
      - `PROJECT_TASK_DEPENDENCY_UNBLOCKED` when a blocking relationship is removed
  - Notes/constraints:
    - Only blocking relationships emit events; `related_to` dependencies remain non-workflow.

- 2026-01-23: Completed `F033` (project approvals — catalog-only):
  - Discovery: there is no authoritative “project approval request/decision” subsystem today (no project approval model/service/actions to hook).
  - Decision: treat `PROJECT_APPROVAL_*` events as **catalog-only** (mirrors `TICKET_APPROVAL_*` rationale) until a real project approval feature exists.
  - Updated `ee/docs/plans/2026-01-23-workflow-event-catalog-domain-events/PRD.md` Open Questions #4 to reflect `PROJECT_APPROVAL_*` catalog-only status.

- 2026-01-23: Completed `F040` (invoice lifecycle emission):
  - Added billing workflow payload builders:
    - `server/src/lib/api/services/invoiceWorkflowEvents.ts`
  - Emitted invoice lifecycle workflow v2 domain events from real invoice operations:
    - `server/src/lib/api/services/InvoiceService.ts` now publishes:
      - `INVOICE_SENT` on `sendInvoice` (delivery method inferred; includes `clientId`, `sentByUserId`, `sentAt`)
      - `INVOICE_STATUS_CHANGED` on all invoice status transitions across `update`, `finalizeInvoice`, `sendInvoice`, `recordPayment`, `applyCredit`, `recordRefund`, `bulkUpdateStatus`, and soft-cancel in `delete`
      - `INVOICE_DUE_DATE_CHANGED` from `update` when due date changes
      - `INVOICE_OVERDUE` when status transitions into `overdue` (amount due computed from payments + credits)
      - `INVOICE_WRITTEN_OFF` when transitioning `overdue` → `cancelled` with a remaining balance (maps “write off” onto today’s available status model)
  - Added schema-compat unit coverage:
    - `server/src/test/unit/invoiceWorkflowEvents.test.ts`

- 2026-01-23: Completed `F041` (payment events emission):
  - Added billing workflow payload builders:
    - `server/src/lib/api/services/paymentWorkflowEvents.ts`
  - Emitted workflow v2 payment domain events from real payment paths:
    - Manual invoice payments/refunds in `server/src/lib/api/services/InvoiceService.ts` now publish:
      - `PAYMENT_RECORDED` + `PAYMENT_APPLIED` on `recordPayment`
      - `PAYMENT_REFUNDED` on `recordRefund`
    - Stripe webhook processing in `ee/server/src/lib/payments/PaymentService.ts` now publishes:
      - `PAYMENT_RECORDED` + `PAYMENT_APPLIED` on successful payment recording
      - `PAYMENT_REFUNDED` on refund recording
      - `PAYMENT_FAILED` on `payment_intent.payment_failed` events (when amount is present)
  - Added schema-compat unit coverage:
    - `server/src/test/unit/paymentWorkflowEvents.test.ts`
  - Verification:
    - `npx vitest run server/src/test/unit/paymentWorkflowEvents.test.ts`

- 2026-01-23: Completed `F042` (credit events emission):
  - Added shared billing payload builders + schema-compat tests:
    - `shared/workflow/streams/domainEventBuilders/creditNoteEventBuilders.ts`
    - `shared/workflow/streams/domainEventBuilders/__tests__/creditNoteEventBuilders.test.ts`
  - Emitted workflow v2 credit note domain events from real credit flows:
    - `packages/billing/src/actions/creditActions.ts` now publishes:
      - `CREDIT_NOTE_CREATED` when prepayment credit tracking entries are created
      - `CREDIT_NOTE_APPLIED` per credit source used when applying credits to an invoice (supports multi-credit allocation)
    - `packages/billing/src/actions/invoiceModification.ts` now publishes:
      - `CREDIT_NOTE_CREATED` when finalizing a negative invoice that issues credit (`credit_issuance_from_negative_invoice`)
      - `CREDIT_NOTE_VOIDED` when hard-deleting a negative invoice that issued unused credit (maps delete → void)
  - Verification:
    - `npx vitest run shared/workflow/streams/domainEventBuilders/__tests__/creditNoteEventBuilders.test.ts`

- 2026-01-23: Completed `F043` (contract events emission):
  - Added shared contract payload builders + renewal timing helper:
    - `shared/workflow/streams/domainEventBuilders/contractEventBuilders.ts`
    - Renewal “upcoming” window default: **30 days** (`computeContractRenewalUpcoming`)
  - Emitted workflow v2 contract domain events from real client-contract paths:
    - `packages/clients/src/actions/clientContractActions.ts` publishes:
      - `CONTRACT_CREATED` on `assignContractToClient` and `createClientContract`
      - `CONTRACT_UPDATED` + `CONTRACT_STATUS_CHANGED` + `CONTRACT_RENEWAL_UPCOMING` (on threshold crossing) on `updateClientContract`
      - `CONTRACT_STATUS_CHANGED` on `deactivateClientContract`
    - `packages/billing/src/actions/contractWizardActions.ts` publishes:
      - `CONTRACT_CREATED` (+ optional `CONTRACT_RENEWAL_UPCOMING`) on `createClientContractFromWizard`
  - Added schema-compat unit test coverage:
    - `shared/workflow/streams/domainEventBuilders/__tests__/contractEventBuilders.test.ts`

- 2026-01-23: Completed `F044` (recurring billing run events emission):
  - Added shared payload builders + schema-compat unit tests:
    - `shared/workflow/streams/domainEventBuilders/recurringBillingRunEventBuilders.ts`
    - `shared/workflow/streams/domainEventBuilders/__tests__/recurringBillingRunEventBuilders.test.ts`
  - Implemented a billing “run” action that publishes workflow v2 events around invoice generation:
    - `packages/billing/src/actions/recurringBillingRunActions.ts` publishes `RECURRING_BILLING_RUN_STARTED` / `COMPLETED` (with `invoicesCreated`, `failedCount`) and `FAILED` for unexpected fatal failures.
  - Updated billing dashboard invoice generation flows to use the run action (batch + preview generation):
    - `packages/billing/src/components/billing-dashboard/AutomaticInvoices.tsx`
  - Verification:
    - `npx vitest run shared/workflow/streams/domainEventBuilders/__tests__/recurringBillingRunEventBuilders.test.ts`
    - Note: `npx tsc -p packages/billing/tsconfig.json --noEmit` currently fails due to existing TS errors in `packages/billing/src/actions/contractWizardActions.ts` and `packages/billing/src/actions/creditActions.ts` (pre-existing; not addressed in this item).

- 2026-01-23: Completed `F050` (CRM client events emission):
  - Added shared payload builders:
    - `shared/workflow/streams/domainEventBuilders/clientEventBuilders.ts`
  - Emitted workflow v2 domain events from real client create/update/archive paths:
    - `packages/clients/src/actions/clientActions.ts` now publishes:
      - `CLIENT_CREATED` on create
      - `CLIENT_UPDATED` on update (with `updatedFields` + `{previous,new}` `changes`)
      - `CLIENT_STATUS_CHANGED` when `properties.status` transitions
      - `CLIENT_OWNER_ASSIGNED` when `account_manager_id` changes to a user
      - `CLIENT_ARCHIVED` when `is_inactive` transitions to true (including `markClientInactiveWithContacts`)
    - `server/src/lib/api/services/ClientService.ts` now publishes the same domain events for REST API create/update paths.
  - Added schema-compat unit test coverage:
    - `shared/workflow/streams/domainEventBuilders/__tests__/clientEventBuilders.test.ts`
  - Notes/constraints:
    - `CLIENT_MERGED` remains catalog-only for now (no authoritative client merge/consolidate path exists to emit from today).

- 2026-01-23: Completed `F051` (CRM contact events emission):
  - Added shared payload builders + schema-compat unit tests:
    - `shared/workflow/streams/domainEventBuilders/contactEventBuilders.ts`
    - `shared/workflow/streams/domainEventBuilders/__tests__/contactEventBuilders.test.ts`
  - Emitted workflow v2 contact domain events from real contact create/update/archive paths:
    - `packages/clients/src/actions/contact-actions/contactActions.tsx` publishes `CONTACT_CREATED`, `CONTACT_UPDATED`, `CONTACT_ARCHIVED` (when `is_inactive` transitions `false → true`) via `publishWorkflowEvent(...)`.
    - `server/src/lib/api/services/ContactService.ts` publishes the same domain events for REST API create/update paths.
  - Implemented `CONTACT_PRIMARY_SET` emission from the authoritative client billing contact change:
    - `packages/clients/src/actions/clientActions.ts` publishes when `billing_contact_id` changes to a non-empty contact id.
    - `server/src/lib/api/services/ClientService.ts` publishes the same event for REST API updates.
  - Notes/constraints:
    - `CONTACT_MERGED` remains catalog-only for now (no authoritative “merge contacts” path exists in product code today).

- 2026-01-23: Completed `F052` (CRM interaction/note events emission):
  - Added shared payload builders + schema-compat unit tests:
    - `shared/workflow/streams/domainEventBuilders/crmInteractionNoteEventBuilders.ts`
    - `shared/workflow/streams/domainEventBuilders/__tests__/crmInteractionNoteEventBuilders.test.ts`
  - Emitted workflow v2 domain events from real CRM activity paths:
    - `packages/clients/src/actions/interactionActions.ts` publishes `INTERACTION_LOGGED` on interaction creation (derives `channel` from interaction type; ensures interactions are linked to a `clientId`).
    - `packages/clients/src/actions/clientNoteActions.ts` publishes `NOTE_CREATED` when a notes document is first created/linked to a client.
    - `packages/clients/src/actions/contact-actions/contactNoteActions.ts` publishes `NOTE_CREATED` when a notes document is first created/linked to a contact.

- 2026-01-23: Completed `F053` (tags events emission):
  - Added shared payload builders + schema-compat unit tests:
    - `shared/workflow/streams/domainEventBuilders/tagEventBuilders.ts`
    - `shared/workflow/streams/domainEventBuilders/__tests__/tagEventBuilders.test.ts`
  - Emitted workflow v2 tag domain events from tag actions:
    - `packages/tags/src/actions/tagActions.ts` publishes `TAG_DEFINITION_CREATED`, `TAG_DEFINITION_UPDATED`, `TAG_APPLIED`, `TAG_REMOVED` (uses definition id as `tagId`, and `entityType/entityId` from `tagged_type/tagged_id`).
  - Added `getOrCreateWithStatus` to detect definition creates reliably:
    - `packages/tags/src/models/tagDefinition.ts`

- 2026-01-23: Completed `F060` (document storage lifecycle emission):
  - Added shared payload builders:
    - `shared/workflow/streams/domainEventBuilders/documentStorageEventBuilders.ts`
  - Emitted workflow v2 document storage domain events from the authoritative storage service:
    - `packages/documents/src/storage/StorageService.ts` publishes:
      - `DOCUMENT_UPLOADED` after file record creation (includes `documentId`, `fileName`, `contentType`, `sizeBytes`, `storageKey`)
      - `DOCUMENT_DELETED` after storage delete + soft delete (includes `documentId`, `deletedAt`)
  - Verification:
    - `npx tsc -p packages/documents/tsconfig.json --noEmit`
    - `npx vitest run packages/documents/tests/storageConfig.test.ts`
  - Notes/constraints:
    - `documentId` maps to the file storage record id (`external_files.file_id`) today.
    - Publishing is best-effort (errors are logged but do not fail the upload/delete).

- 2026-01-24: Completed `F061` (document association/detach emission):
  - Added shared payload builders + schema-compat unit tests:
    - `shared/workflow/streams/domainEventBuilders/documentAssociationEventBuilders.ts`
    - `shared/workflow/streams/domainEventBuilders/__tests__/documentAssociationEventBuilders.test.ts`
  - Emitted workflow v2 document association events from real document flows:
    - `packages/documents/src/actions/documentActions.ts` publishes:
      - `DOCUMENT_ASSOCIATED` on association creation (bulk associate and upload-time associations; also client/contract-specific helpers)
      - `DOCUMENT_DETACHED` on manual detach and on document deletion (reason=`document_deleted`)
    - `packages/documents/src/actions/documentBlockContentActions.ts` publishes `DOCUMENT_ASSOCIATED` when a block-document is created and linked to an entity
  - Verification:
    - `npx vitest run shared/workflow/streams/domainEventBuilders/__tests__/documentAssociationEventBuilders.test.ts`

- 2026-01-24: Completed `F062` (document generation emission):
  - Added shared payload builder + schema-compat unit test:
    - `shared/workflow/streams/domainEventBuilders/documentGeneratedEventBuilders.ts`
    - `shared/workflow/streams/domainEventBuilders/__tests__/documentGeneratedEventBuilders.test.ts`
  - Emitted `DOCUMENT_GENERATED` from the authoritative PDF generation path (covers invoice PDFs and document-to-PDF conversion):
    - `server/src/services/pdf-generation.service.ts` publishes `DOCUMENT_GENERATED` after storing the generated PDF in `external_files`
    - `sourceType` is `invoice` when generating an invoice PDF, otherwise `document`
    - `sourceId` is the invoice id / source document id
  - Notes/constraints:
    - Publishing is best-effort (errors are logged but do not fail PDF generation/storage).

- 2026-01-24: Completed `F063` (document signatures — catalog-only):
  - Discovery: there is no authoritative “document signature request/fulfillment” subsystem today (no signature request model/service/actions and no provider callback/webhook ingestion path to hook).
  - Decision: treat `DOCUMENT_SIGNATURE_REQUESTED`, `DOCUMENT_SIGNED`, and `DOCUMENT_SIGNATURE_EXPIRED` as **catalog-only** until an e-sign feature is implemented.
  - Updated `ee/docs/plans/2026-01-23-workflow-event-catalog-domain-events/PRD.md` Open Questions #4 to record the decision.

- 2026-01-24: Completed `F070` (inbound email reply emission):
  - Emitted `INBOUND_EMAIL_REPLY_RECEIVED` when the system email processing workflow matches an inbound email to an existing ticket and successfully creates the ticket comment.
  - Implemented best-effort publishing from `shared/workflow/actions/emailWorkflowActions.ts` (`createCommentFromEmail`) behind an optional `inboundReplyEvent` input.
  - Updated both workflow definitions to pass `matchedBy` (`reply_token` vs `thread_headers`) and normalized `receivedAt` for action schema validation:
    - `shared/workflow/workflows/system-email-processing-workflow.ts` (+ `.generated.js`)
    - `services/workflow-worker/src/workflows/system-email-processing-workflow.ts` (+ `.generated.js`)
  - Added a domain payload builder + schema-compat unit test:
    - `shared/workflow/streams/domainEventBuilders/inboundEmailReplyEventBuilders.ts`
    - `shared/workflow/streams/domainEventBuilders/__tests__/inboundEmailReplyEventBuilders.test.ts`

- 2026-01-24: Completed `F071` (outbound email lifecycle emission):
  - Emitted workflow v2 outbound lifecycle events from the authoritative outbound send abstraction:
    - `server/src/lib/email/BaseEmailService.ts` now publishes best-effort:
      - `OUTBOUND_EMAIL_QUEUED` immediately before provider send attempt
      - `OUTBOUND_EMAIL_SENT` or `OUTBOUND_EMAIL_FAILED` after provider result
  - Notes/constraints:
    - Uses a generated workflow `messageId` (UUID) per send attempt, and includes provider message id when available.
    - Publishing failures do not block email delivery (events are best-effort).
  - Added unit coverage:
    - `server/src/test/unit/outboundEmailWorkflowEvents.test.ts`

- 2026-01-24: Completed `F072` (email delivery/feedback emission):
  - Implemented Resend delivery/feedback webhook ingestion (Svix signature verification):
    - `server/src/app/api/email/webhooks/resend/route.ts`
    - `server/src/services/email/webhooks/resendWebhookEvents.ts` (signature verify + mapping)
  - Added correlation tags/headers so provider callbacks can map back to `tenantId` + workflow `messageId`:
    - `server/src/lib/email/BaseEmailService.ts` now sets `X-Alga-Workflow-Message-Id`/`X-Alga-Tenant-Id` headers and `alga_*` tags on provider messages.
  - Emits workflow v2 domain events (best-effort) when Resend provider support exists:
    - `EMAIL_DELIVERED`, `EMAIL_BOUNCED`, `EMAIL_COMPLAINT_RECEIVED`, `EMAIL_UNSUBSCRIBED`
  - Added schema-compat unit coverage:
    - `shared/workflow/streams/domainEventBuilders/emailFeedbackEventBuilders.ts`
    - `shared/workflow/streams/domainEventBuilders/__tests__/emailFeedbackEventBuilders.test.ts`
    - `server/src/test/unit/resendWebhookEvents.test.ts`

- 2026-01-24: Completed `F073` (notification delivery lifecycle emission):
  - Implemented workflow v2 notification lifecycle events for **in-app internal notifications**:
    - `NOTIFICATION_SENT` when an internal notification row is created
    - `NOTIFICATION_DELIVERED` / `NOTIFICATION_FAILED` based on Redis Pub/Sub broadcast success/failure
    - `NOTIFICATION_READ` when a user marks a notification as read
  - Wired emissions into the authoritative internal notification paths:
    - `packages/notifications/src/actions/internal-notification-actions/internalNotificationActions.ts`
    - `packages/notifications/src/realtime/internalNotificationBroadcaster.ts`
  - Added shared payload builders + schema-compat unit test coverage:
    - `shared/workflow/streams/domainEventBuilders/notificationEventBuilders.ts`
    - `shared/workflow/streams/domainEventBuilders/__tests__/notificationEventBuilders.test.ts`

- 2026-01-24: Completed `F074` (survey/CSAT lifecycle emission):
  - Added shared payload builders + schema-compat unit tests:
    - `shared/workflow/streams/domainEventBuilders/surveyEventBuilders.ts`
    - `shared/workflow/streams/domainEventBuilders/__tests__/surveyEventBuilders.test.ts`
  - Emitted workflow v2 survey lifecycle events from the authoritative survey flows:
    - `server/src/services/surveyService.ts` now publishes `SURVEY_SENT` when an invitation is sent, and publishes `SURVEY_REMINDER_SENT` when a subsequent invitation is sent for the same `(ticketId, templateId, contactId)` tuple (treated as reminder #N).
    - `packages/surveys/src/actions/surveyResponseActions.ts` now publishes `SURVEY_RESPONSE_RECEIVED` when a response is persisted, and publishes `CSAT_ALERT_TRIGGERED` when the rating is <= `NEGATIVE_RATING_THRESHOLD` (scope = assigned agent when available, otherwise org).
    - `packages/surveys/src/actions/surveyTokenService.ts` now publishes `SURVEY_EXPIRED` (best-effort) when an expired token is resolved (idempotencyKey = `survey_expired:<tenant>:<invitationId>`).

- 2026-01-24: Completed `F080` (integration sync lifecycle emission):
  - Added workflow v2 integration sync domain payload builders + schema-compat unit tests:
    - `shared/workflow/streams/domainEventBuilders/integrationSyncEventBuilders.ts`
    - `shared/workflow/streams/domainEventBuilders/__tests__/integrationSyncEventBuilders.test.ts`
  - Emitted `INTEGRATION_SYNC_STARTED` / `INTEGRATION_SYNC_COMPLETED` / `INTEGRATION_SYNC_FAILED` from real RMM sync paths (NinjaOne):
    - `ee/server/src/lib/integrations/ninjaone/sync/syncEngine.ts` publishes lifecycle events for:
      - full sync (`runFullSync`)
      - incremental sync (`runIncrementalSync`)
      - single-device sync (`syncDevice`)
    - Uses `SyncOptions.performedBy` (when provided) to attribute `INTEGRATION_SYNC_STARTED.initiatedByUserId`.

- 2026-01-24: Completed `F081` (integration webhook receipt emission):
  - Added safe webhook payload builders:
    - `shared/workflow/streams/domainEventBuilders/integrationWebhookEventBuilders.ts`
      - `sanitizeIntegrationWebhookRawPayload(...)` redacts sensitive keys (token/secret/password/apiKey/authorization/signature) and enforces a size cap
      - `buildIntegrationWebhookReceivedPayload(...)` builds schema-aligned payloads
  - Emitted `INTEGRATION_WEBHOOK_RECEIVED` from the authoritative NinjaOne webhook handler:
    - `ee/server/src/lib/integrations/ninjaone/webhooks/webhookHandler.ts` publishes the domain event with:
      - `webhookId` derived from `payload.id`/`payload.activityId` (falls back to sha256-based id)
      - `rawPayloadRef` set to a generated `integration-webhook:<uuid>` reference
      - a structured log entry containing a **redacted** payload snapshot keyed by `rawPayloadRef`
  - Added schema-compat unit tests:
    - `shared/workflow/streams/domainEventBuilders/__tests__/integrationWebhookEventBuilders.test.ts`

- 2026-01-24: Completed `F082` (integration connection health emission):
  - Added workflow v2 integration connection payload builders + schema-compat unit tests:
    - `shared/workflow/streams/domainEventBuilders/integrationConnectionEventBuilders.ts`
    - `shared/workflow/streams/domainEventBuilders/__tests__/integrationConnectionEventBuilders.test.ts`
  - Emitted `INTEGRATION_CONNECTED` on successful NinjaOne OAuth connect:
    - `ee/server/src/app/api/integrations/ninjaone/callback/route.ts`
  - Emitted `INTEGRATION_DISCONNECTED` on manual NinjaOne disconnect:
    - `ee/server/src/lib/actions/integrations/ninjaoneActions.ts`
  - Note: for RMM, `connectionId` is set to the tenant integration UUID (`integrationId`) since the current data model has a single connection row per provider/tenant.

- 2026-01-24: Completed `F083` (integration token lifecycle emission):
  - Added shared payload builders + schema-compat unit tests:
    - `shared/workflow/streams/domainEventBuilders/integrationTokenEventBuilders.ts`
    - `shared/workflow/streams/domainEventBuilders/__tests__/integrationTokenEventBuilders.test.ts`
  - Implemented best-effort emission from the NinjaOne OAuth client:
    - `ee/server/src/lib/integrations/ninjaone/ninjaOneClient.ts` now publishes `INTEGRATION_TOKEN_REFRESH_FAILED` when refresh attempts fail (deduped in 5-minute buckets).
    - `ee/server/src/lib/integrations/ninjaone/ninjaOneClient.ts` now publishes `INTEGRATION_TOKEN_EXPIRING` when a token expiry timestamp indicates a long-lived token nearing expiration (default window 7 days; suppresses noisy <24h expiries).
  - Propagated integration context into NinjaOne client factories where available:
    - `ee/server/src/lib/integrations/ninjaone/sync/*` passes `integrationId` into `createNinjaOneClient(...)` so token events can include `integrationId`/`connectionId`.

- 2026-01-24: Completed `F084` (external mapping change emission):
  - Added shared payload builder:
    - `shared/workflow/streams/domainEventBuilders/externalMappingEventBuilders.ts`
  - Emitted `EXTERNAL_MAPPING_CHANGED` from the authoritative accounting mapping CRUD actions:
    - `packages/integrations/src/actions/externalMappingActions.ts` publishes on create/update/delete for `tenant_external_entity_mappings`
    - Payload uses `provider=integration_type`, `mappingType=alga_entity_type`, and includes `previousValue`/`newValue` snapshots (camelCase keys).
  - Added unit coverage for payload shaping:
    - `packages/integrations/src/lib/externalMappingWorkflowEvents.ts`
    - `packages/integrations/src/lib/__tests__/externalMappingWorkflowEvents.test.ts`

- 2026-01-24: Completed `F090` (asset lifecycle emission):
  - Added shared payload builders + schema-compat unit tests:
    - `shared/workflow/streams/domainEventBuilders/assetEventBuilders.ts`
    - `shared/workflow/streams/domainEventBuilders/__tests__/assetEventBuilders.test.ts`
  - Emitted workflow v2 asset domain events from authoritative asset and association paths:
    - `packages/assets/src/actions/assetActions.ts` publishes:
      - `ASSET_CREATED` on asset creation
      - `ASSET_UPDATED` on asset updates (with `updatedFields` + `{previous,new}` `changes` derived from the update payload)
      - `ASSET_ASSIGNED` / `ASSET_UNASSIGNED` on asset association create/remove (ownerType = `ticket`/`project`)
      - `ASSET_ASSIGNED` on client reassignment when `client_id` changes (ownerType = `client`)
      - `ASSET_WARRANTY_EXPIRING` on 30-day threshold crossings (create/update) via `computeAssetWarrantyExpiring(...)`
  - Verification:
    - `npx vitest run shared/workflow/streams/domainEventBuilders/__tests__/assetEventBuilders.test.ts`
    - `npx tsc -p packages/assets/tsconfig.json --noEmit`

- 2026-01-24: Completed `F091` (media upload + processing emission):
   - Added shared payload builders + schema-compat unit tests:
     - `shared/workflow/streams/domainEventBuilders/mediaEventBuilders.ts`
     - `shared/workflow/streams/domainEventBuilders/__tests__/mediaEventBuilders.test.ts`
   - Emitted `FILE_UPLOADED` from the authoritative file storage services:
     - `packages/documents/src/storage/StorageService.ts` (alongside existing `DOCUMENT_UPLOADED`)
     - `server/src/lib/storage/StorageService.ts`
   - Emitted media processing lifecycle events for document preview generation:
     - `MEDIA_PROCESSING_SUCCEEDED` / `MEDIA_PROCESSING_FAILED` from:
       - `packages/documents/src/lib/documentPreviewGenerator.ts`
       - `server/src/lib/utils/documentPreviewGenerator.ts`
     - Success outputs include `thumbnail`/`preview` file ids when generated; failures include a stable `errorCode`.
   - Added focused unit coverage:
     - `server/src/test/unit/documentPreviewGenerator.test.ts` asserts `MEDIA_PROCESSING_SUCCEEDED` is published for supported file types.
     - `packages/documents/tests/storageService.workflowEvents.test.ts` asserts `FILE_UPLOADED` is published on upload.
   - Gotcha: `packages/documents/src/storage/StorageService.ts` imports `@alga-psa/auth/getCurrentUser` (which depends on `@alga-psa/media`); tests that import this module must mock `@alga-psa/auth/getCurrentUser` to avoid Vite import-analysis failures.

## Next Up

- All features in `features.json` are now implemented; proceed to `tests.json` checklist.

- 2026-01-24: Completed `T001` (schema conventions coverage):
  - Added a schema-level regression test that parses `event-proposals.md` and asserts payload schema conventions:
    - `shared/workflow/runtime/__tests__/payloadSchemaConventions.test.ts`
    - Ensures `tenantId` + `occurredAt` are required, top-level keys are camelCase (no `_`), and `updatedFields`/`changes` are paired.
    - Ensures `previous*` transition fields have corresponding `new*` fields, with explicit exceptions for documented unassignment patterns and `CONTACT_PRIMARY_SET`.
  - Tweaked `payload.TicketUnassigned.v1` schema to include optional `newAssigneeId/newAssigneeType` fields for consistency:
    - `shared/workflow/runtime/schemas/ticketEventSchemas.ts`

- 2026-01-24: Completed `T002` (schema registry resolves + validates examples):
  - Added an auto-example generator test that:
    - Parses `event-proposals.md`, derives `payload.*.v1` refs, asserts `schemaRegistry.has(ref)`, and validates an auto-generated example payload per ref.
    - `shared/workflow/runtime/__tests__/payloadSchemaExamples.test.ts`

- 2026-01-24: Completed `T003` (event bus schema coverage):
  - Existing test asserts `EVENT_TYPES` ↔ `EventTypeEnum` alignment and that every event type has a payload schema mapping:
    - `shared/workflow/streams/__tests__/eventBusSchema.expandedEvents.test.ts`

- 2026-01-24: Completed `T004` (catalog migration coverage):
  - Added a migration-level coverage test that parses `event-proposals.md` and asserts the system catalog upsert migration includes every proposed `event_type`:
    - `server/src/test/unit/workflowEventCatalogMigrationCoverage.test.ts`

- 2026-01-24: Completed `T005` (simulator form-mode schema + validation coverage):
  - Added a schema-driven test that asserts every proposed `payload.*.v1` ref can be converted to JSON schema (form builder input) and that representative schemas reject invalid payloads:
    - `shared/workflow/runtime/__tests__/workflowEventFormModeBuilder.test.ts`

- 2026-01-24: Completed `T006` (overlap handling coverage):
  - Existing event bus schema regression test asserts overlap decisions are encoded explicitly:
    - `shared/workflow/streams/__tests__/eventBusSchema.expandedEvents.test.ts`

- 2026-01-24: Completed `T010` (ticket status/priority transition coverage):
  - Existing transition builder unit test asserts `TICKET_STATUS_CHANGED` + `TICKET_PRIORITY_CHANGED` include `previous*`/`new*` values:
    - `packages/tickets/src/lib/__tests__/workflowTicketTransitionEvents.test.ts`

- 2026-01-24: Completed `T011` (ticket assignment change coverage):
  - Added schema-level regression coverage to ensure the event bus accepts domain-shaped assignment payloads:
    - `shared/workflow/streams/__tests__/ticketAssignmentEvents.test.ts`

- 2026-01-24: Completed `T012` (ticket reopened coverage):
  - Existing transition builder unit test asserts `TICKET_REOPENED` includes `previousStatusId/newStatusId`:
    - `packages/tickets/src/lib/__tests__/workflowTicketTransitionEvents.test.ts`

- 2026-01-24: Completed `T013` (ticket escalation/queue change coverage):
  - Existing transition builder unit test asserts `TICKET_ESCALATED` + `TICKET_QUEUE_CHANGED` payloads:
    - `packages/tickets/src/lib/__tests__/workflowTicketTransitionEvents.test.ts`

- 2026-01-24: Completed `T014` (ticket merge/split coverage):
  - Added schema-level regression coverage for relationship event payloads:
    - `shared/workflow/streams/__tests__/ticketMergeSplitEvents.test.ts`

- 2026-01-24: Completed `T015` (ticket tag change coverage):
  - Existing tag action builder tests cover tag add/remove events (applies to tickets via `entityType/entityId`):
    - `shared/workflow/streams/domainEventBuilders/__tests__/tagEventBuilders.test.ts`

- 2026-01-24: Completed `T016` (ticket communication coverage):
  - Existing unit coverage validates `TICKET_MESSAGE_ADDED`, `TICKET_CUSTOMER_REPLIED`, and `TICKET_INTERNAL_NOTE_ADDED` payload construction and schema compatibility:
    - `packages/tickets/src/lib/__tests__/workflowTicketCommunicationEvents.test.ts`

- 2026-01-24: Completed `T017` (ticket time entry coverage):
  - Existing unit coverage validates `TICKET_TIME_ENTRY_ADDED` payload building against `payload.TicketTimeEntryAdded.v1`:
    - `server/src/test/unit/timeEntryWorkflowEvents.test.ts`

- 2026-01-24: Completed `T018` (ticket SLA stage coverage):
  - Existing unit coverage validates stage enter/met/breached builders and idempotency behavior:
    - `packages/tickets/src/lib/__tests__/workflowTicketSlaStageEvents.test.ts`

- 2026-01-24: Completed `T019` (ticket approvals are catalog-only):
  - Per PRD decision, `TICKET_APPROVAL_*` events are catalog-only; coverage relies on:
    - Schema registry example validation (`T002`): `shared/workflow/runtime/__tests__/payloadSchemaExamples.test.ts`
    - Migration coverage (`T004`): `server/src/test/unit/workflowEventCatalogMigrationCoverage.test.ts`

- 2026-01-24: Completed `T020` (appointment lifecycle coverage):
  - Existing builder unit test validates appointment lifecycle payloads (start/end time + timezone) against schemas:
    - `shared/workflow/streams/domainEventBuilders/__tests__/appointmentEventBuilders.test.ts`

- 2026-01-24: Completed `T021` (appointment assignment coverage):
  - Existing builder unit test validates `APPOINTMENT_ASSIGNED` payloads (previous/new assignee) against schemas:
    - `shared/workflow/streams/domainEventBuilders/__tests__/appointmentEventBuilders.test.ts`

- 2026-01-24: Completed `T022` (schedule block coverage):
  - Existing builder unit test validates `SCHEDULE_BLOCK_CREATED` and `SCHEDULE_BLOCK_DELETED` payloads against schemas:
    - `shared/workflow/streams/domainEventBuilders/__tests__/scheduleBlockEventBuilders.test.ts`

## Suggested Phasing (to reduce risk)

Phase 1 (authoritative CRUD/state transitions; low provider dependency):
- Tickets transitions + assignment
- Projects task/status
- Billing invoice lifecycle + payments
- CRM client/contact changes
- Tags generic apply/remove

Phase 2 (integration/provider dependent):
- Email delivery/bounce/complaint/unsubscribe
- Notifications delivered/read
- Integrations token lifecycle + webhook receipt

Phase 3 (feature dependent / optional modules):
- SLA stages, approvals, dispatch/field ops, signatures

## Commands / Runbooks

- Validate plan JSON shape: `python3 scripts/validate_plan.py ee/docs/plans/2026-01-23-workflow-event-catalog-domain-events`
- Local verification for this plan (until NX worker issue is resolved):
  - Lint changed files: `npx eslint shared/workflow/streams/eventBusSchema.ts shared/workflow/streams/workflowEventPublishHelpers.ts packages/event-bus/src/eventBus.ts packages/event-bus/src/publishers/index.ts server/src/lib/eventBus/index.ts server/src/lib/eventBus/publishers/index.ts`
  - Note: `npx nx build @alga-psa/shared` fails in this sandbox with `NX   Failed to start plugin worker.`

## Gotchas

- (Resolved in this worktree) Earlier sandbox runs had `git add` / `git commit` failing with `.../.git/worktrees/.../index.lock` permission errors. As of 2026-01-23 in `/Users/roberisaacs/alga-psa.worktrees/feature/workflow-events-catalog`, commits succeed normally.
- DB-backed vitest integration tests (e.g. `server/src/test/integration/ticketBundling.integration.test.ts`) require a local Postgres instance matching `.env.localtest` (observed `ECONNREFUSED` on `localhost:5438` when not running).
- `npm run test:nx` currently fails on `tools/nx-tests/editionSwapping.test.ts` (CE alias `ee` resolves to `packages/ee/src` instead of `server/src/empty`). This appears unrelated to workflow event changes; use targeted vitest runs for verification until fixed.
- Some older unit tests import deep, non-exported subpaths (e.g. `@alga-psa/projects/actions/projectActions`) and fail under Vite import-analysis with `Missing "./actions/projectActions" specifier...`; use source-relative imports in tests or prefer shared-schema-level tests until package exports are updated.
- `npx tsc -p ee/server/tsconfig.json --noEmit` currently fails with a pre-existing type error in `shared/workflow/runtime/actions/registerEmailWorkflowActions.ts` (`messageId` required vs optional mismatch).

## Work Log (continued)

- 2026-01-24: Completed `T023` (capacity thresholds coverage):
  - Added unit coverage that asserts `CAPACITY_THRESHOLD_REACHED` publishes only on a threshold crossing and does nothing when capacity isn’t configured:
    - `packages/scheduling/src/lib/__tests__/capacityThresholdWorkflowEvents.publisher.test.ts`
  - Refactored `maybePublishCapacityThresholdReached(...)` to accept optional `__deps` (publisher + data access functions + clock) so it can be tested without a DB:
    - `packages/scheduling/src/lib/capacityThresholdWorkflowEvents.ts`

- 2026-01-24: Completed `T024` (dispatch lifecycle “supported only” coverage):
  - Expanded unit coverage to assert dispatch lifecycle gating and technician id normalization:
    - `shared/workflow/streams/domainEventBuilders/__tests__/technicianDispatchEventBuilders.test.ts`

- 2026-01-24: Completed `T030` (project updated coverage):
  - Coverage already exists via schema-compat builder test asserting `updatedFields` + `{previous,new}` `changes` shape:
    - `shared/workflow/streams/domainEventBuilders/__tests__/projectLifecycleEventBuilders.test.ts`

- 2026-01-24: Completed `T031` (project status changed coverage):
  - Coverage already exists via schema-compat builder test asserting `previousStatus`/`newStatus` transition shape:
    - `shared/workflow/streams/domainEventBuilders/__tests__/projectLifecycleEventBuilders.test.ts`

- 2026-01-24: Completed `T032` (project task lifecycle coverage):
  - Coverage already exists via schema-compat builder test for task create/assign/status/complete payload shapes:
    - `shared/workflow/streams/domainEventBuilders/__tests__/projectTaskEventBuilders.test.ts`

- 2026-01-24: Completed `T033` (project dependency coverage):
  - Coverage already exists via schema-compat builder test for dependency blocked/unblocked payload shapes:
    - `shared/workflow/streams/domainEventBuilders/__tests__/projectTaskEventBuilders.test.ts`

- 2026-01-24: Completed `T034` (project approvals are catalog-only):
  - Per PRD decision, `PROJECT_APPROVAL_*` events are catalog-only; coverage relies on:
    - Schema registry example validation (`T002`): `shared/workflow/runtime/__tests__/payloadSchemaExamples.test.ts`
    - Migration coverage (`T004`): `server/src/test/unit/workflowEventCatalogMigrationCoverage.test.ts`

- 2026-01-24: Completed `T040` (invoice send/status/due-date coverage):
  - Coverage exists via billing payload builder unit tests:
    - `server/src/test/unit/invoiceWorkflowEvents.test.ts`

- 2026-01-24: Completed `T041` (invoice overdue/write-off coverage):
  - Coverage exists via billing payload builder unit tests (days overdue + monetary context):
    - `server/src/test/unit/invoiceWorkflowEvents.test.ts`

- 2026-01-24: Completed `T042` (payments coverage):
  - Coverage exists via billing payload builder unit tests (recorded/applied/failed/refunded + invoice applications):
    - `server/src/test/unit/paymentWorkflowEvents.test.ts`

- 2026-01-24: Completed `T043` (credits coverage):
  - Coverage exists via schema-compat builder tests for credit note created/applied/voided payload shapes:
    - `shared/workflow/streams/domainEventBuilders/__tests__/creditNoteEventBuilders.test.ts`

- 2026-01-24: Completed `T044` (contracts coverage):
  - Coverage exists via schema-compat builder tests for contract lifecycle + renewal-upcoming timing:
    - `shared/workflow/streams/domainEventBuilders/__tests__/contractEventBuilders.test.ts`

- 2026-01-24: Completed `T045` (recurring billing run coverage):
  - Coverage exists via schema-compat builder tests for run started/completed/failed payload shapes:
    - `shared/workflow/streams/domainEventBuilders/__tests__/recurringBillingRunEventBuilders.test.ts`

- 2026-01-24: Completed `T050` (CRM client coverage):
  - Coverage exists via schema-compat builder tests for client lifecycle payload shapes (including catalog-only `CLIENT_MERGED`):
    - `shared/workflow/streams/domainEventBuilders/__tests__/clientEventBuilders.test.ts`

- 2026-01-24: Completed `T051` (CRM contact coverage):
  - Coverage exists via schema-compat builder tests for contact lifecycle payload shapes (including catalog-only `CONTACT_MERGED`):
    - `shared/workflow/streams/domainEventBuilders/__tests__/contactEventBuilders.test.ts`

- 2026-01-24: Completed `T052` (CRM interaction/note coverage):
  - Coverage exists via schema-compat builder tests for interaction logged + note created payload shapes:
    - `shared/workflow/streams/domainEventBuilders/__tests__/crmInteractionNoteEventBuilders.test.ts`

- 2026-01-24: Completed `T053` (tags coverage):
  - Coverage exists via schema-compat builder tests for definition create/update and tag apply/remove payload shapes:
    - `shared/workflow/streams/domainEventBuilders/__tests__/tagEventBuilders.test.ts`

- 2026-01-24: Completed `T060` (document upload/delete coverage):
  - Added a unit test that asserts `StorageService` publishes both `DOCUMENT_UPLOADED` and `DOCUMENT_DELETED` with storage metadata:
    - `packages/documents/tests/storageService.workflowEvents.test.ts`

- 2026-01-24: Completed `T061` (document association/detach coverage):
  - Coverage exists via schema-compat builder tests for document associated/detached payload shapes:
    - `shared/workflow/streams/domainEventBuilders/__tests__/documentAssociationEventBuilders.test.ts`

- 2026-01-24: Completed `T062` (document generation coverage):
  - Coverage exists via schema-compat builder tests for `DOCUMENT_GENERATED` payload shape (sourceType/sourceId):
    - `shared/workflow/streams/domainEventBuilders/__tests__/documentGeneratedEventBuilders.test.ts`

- 2026-01-24: Completed `T063` (document signatures are catalog-only):
  - Per PRD decision, `DOCUMENT_SIGNATURE_*` events are catalog-only; coverage relies on:
    - Schema registry example validation (`T002`): `shared/workflow/runtime/__tests__/payloadSchemaExamples.test.ts`
    - Migration coverage (`T004`): `server/src/test/unit/workflowEventCatalogMigrationCoverage.test.ts`

- 2026-01-24: Completed `T070` (inbound email reply coverage):
  - Coverage exists via schema-compat builder tests for `INBOUND_EMAIL_REPLY_RECEIVED` payload shape:
    - `shared/workflow/streams/domainEventBuilders/__tests__/inboundEmailReplyEventBuilders.test.ts`

- 2026-01-24: Completed `T071` (outbound email lifecycle coverage):
  - Coverage exists via unit tests for the outbound send pipeline publishing queued/sent/failed events:
    - `server/src/test/unit/outboundEmailWorkflowEvents.test.ts`

- 2026-01-24: Completed `T072` (email delivery/feedback coverage):
  - Coverage exists via schema-compat builder tests and provider webhook mapping tests:
    - `shared/workflow/streams/domainEventBuilders/__tests__/emailFeedbackEventBuilders.test.ts`
    - `server/src/test/unit/resendWebhookEvents.test.ts`

- 2026-01-24: Completed `T073` (notification lifecycle coverage):
  - Coverage exists via schema-compat builder tests for sent/delivered/failed/read payload shapes:
    - `shared/workflow/streams/domainEventBuilders/__tests__/notificationEventBuilders.test.ts`

- 2026-01-24: Completed `T074` (survey/CSAT lifecycle coverage):
  - Coverage exists via schema-compat builder tests for survey sent/response/reminder/expired and CSAT alert payload shapes:
    - `shared/workflow/streams/domainEventBuilders/__tests__/surveyEventBuilders.test.ts`

- 2026-01-24: Completed `T080` (integration sync lifecycle coverage):
  - Coverage exists via schema-compat builder tests for sync started/completed/failed payload shapes:
    - `shared/workflow/streams/domainEventBuilders/__tests__/integrationSyncEventBuilders.test.ts`

- 2026-01-24: Completed `T081` (integration webhook receipt coverage):
  - Coverage exists via schema-compat builder tests for safe redaction and `INTEGRATION_WEBHOOK_RECEIVED` payload shape:
    - `shared/workflow/streams/domainEventBuilders/__tests__/integrationWebhookEventBuilders.test.ts`

- 2026-01-24: Completed `T082` (integration connect/disconnect coverage):
  - Coverage exists via schema-compat builder tests for connected/disconnected payload shapes:
    - `shared/workflow/streams/domainEventBuilders/__tests__/integrationConnectionEventBuilders.test.ts`

- 2026-01-24: Completed `T083` (integration token lifecycle coverage):
  - Coverage exists via schema-compat builder tests for token expiring and refresh-failed payload shapes:
    - `shared/workflow/streams/domainEventBuilders/__tests__/integrationTokenEventBuilders.test.ts`

- 2026-01-24: Completed `T084` (external mapping changed coverage):
  - Coverage exists via unit tests for mapping payload shaping (previous/new snapshots + mappingType):
    - `packages/integrations/src/lib/__tests__/externalMappingWorkflowEvents.test.ts`

- 2026-01-24: Completed `T090` (asset lifecycle coverage):
  - Coverage exists via schema-compat builder tests for asset lifecycle + warranty-expiring payload shapes:
    - `shared/workflow/streams/domainEventBuilders/__tests__/assetEventBuilders.test.ts`

- 2026-01-24: Completed `T091` (media upload + processing coverage):
  - Coverage exists via schema-compat builder tests and preview-generation publish tests:
    - `shared/workflow/streams/domainEventBuilders/__tests__/mediaEventBuilders.test.ts`
    - `packages/documents/tests/storageService.workflowEvents.test.ts` (FILE_UPLOADED)
    - `server/src/test/unit/documentPreviewGenerator.test.ts` (MEDIA_PROCESSING_SUCCEEDED)
