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

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

341 lines
18 KiB
Markdown

# PRD — CRM Workflow Actions
- Slug: `workflow-crm-actions`
- Date: `2026-04-25`
- Status: Draft
## Summary
Expand the Workflow Runtime V2 CRM action module beyond its single existing action, `crm.create_activity_note`, so workflow authors can query and update CRM activities, schedule follow-up activities, and automate quote sending from workflow logic.
This plan treats the user-proposed CRM action list as a phased roadmap. The first implementation pass focuses on the four highest-value actions:
1. `crm.find_activities`
2. `crm.update_activity`
3. `crm.schedule_activity`
4. `crm.send_quote`
The remaining recommended actions are documented as follow-on scope so their naming, dependencies, and overlap with newly merged Client workflow actions are explicit before implementation begins.
## Problem
The CRM workflow module is sparse. Today it registers exactly one action in `shared/workflow/runtime/actions/businessOperations/crm.ts`:
- `crm.create_activity_note`
That leaves workflow authors unable to automate common CRM loops:
- Look up recent sales/account interactions before choosing an onboarding path.
- Update an activity after a ticket, quote, or project milestone changes.
- Schedule a follow-up call or meeting after work is completed.
- Send a prepared quote when a workflow reaches a business-ready state.
By comparison, Tickets and Clients now have much richer workflow action coverage. Recent main-branch updates added extensive Client workflow actions and established implementation conventions that CRM should follow.
## Goals
- Add first-pass CRM workflow actions for activity lookup, activity update, follow-up scheduling, and quote sending.
- Keep actions grouped under the existing Workflow Designer CRM tile via `crm.*` action IDs.
- Preserve current shared workflow runtime architecture: Zod schemas, registry registration, `action.call`, tenant transaction helpers, permission checks, audit logs, and schema-derived designer forms.
- Reuse existing data models/services where safe, but do not import server-only `withAuth` actions into shared runtime code.
- Follow recent main-branch workflow-action conventions:
- picker metadata with `withWorkflowJsonSchemaMetadata`
- lazy workflow event publisher imports from shared runtime action handlers
- deterministic event idempotency keys
- DB-backed shared-root action tests
- Clearly separate CRM activity/interactions from Client-module notes/interactions that now exist in `clients.*` workflow actions.
## Non-goals
- Replacing `crm.create_activity_note` or changing its persisted contract.
- Implementing every recommended CRM action in the first pass.
- Building new Workflow Designer UI controls beyond existing schema metadata and picker conventions.
- Calling Next.js server actions or `withAuth` action wrappers directly from shared workflow runtime code.
- Reworking quote lifecycle, quote PDF rendering, email templates, or approval logic outside the workflow-action wrappers.
- Duplicating `clients.add_note` or `clients.add_interaction` semantics under CRM without a clear CRM-wide abstraction.
- Adding new event schemas unless implementation discovers a hard gap that cannot be solved with existing CRM/tag/quote events.
## Users and Primary Flows
### Users
- MSP admin building workflow automations.
- Account manager or dispatcher whose CRM follow-up tasks are triggered by tickets, projects, or quote state.
- Internal Alga PSA engineer extending Workflow Runtime V2 business operations.
### Primary flows
1. **Find recent CRM activity before branching**
- Workflow receives a client/contact/ticket event.
- Workflow runs `crm.find_activities` with client/contact/date/type/status filters.
- Workflow branches based on whether there were recent sales, QBR, onboarding, or support interactions.
2. **Update an existing CRM activity**
- Workflow has an interaction ID from a trigger or prior lookup.
- Workflow runs `crm.update_activity` to update status, notes, tags, visibility, or outcome-related fields.
- The action returns before/after summaries and changed fields.
3. **Schedule a follow-up activity**
- Workflow closes a ticket or completes onboarding.
- Workflow runs `crm.schedule_activity` to create a future-dated interaction linked to a client/contact/ticket.
- The activity appears as a CRM interaction/follow-up record and emits interaction logging events where appropriate.
4. **Send a quote from workflow**
- Workflow identifies an existing quote that is ready to send.
- Workflow runs `crm.send_quote` with optional recipients, subject, and message.
- Existing quote send logic publishes the quote to the portal, stores/generates PDF best-effort, and sends email best-effort.
## UX / UI Notes
- New actions should appear under the existing Workflow Designer CRM group. No catalog seed change should be needed because `crm.*` action IDs map to the built-in CRM group.
- First-pass labels:
- `crm.find_activities` → Find CRM Activities
- `crm.update_activity` → Update CRM Activity
- `crm.schedule_activity` → Schedule CRM Activity
- `crm.send_quote` → Send Quote
- Schema descriptions should use MSP language: activity, interaction, follow-up, client, contact, quote.
- Picker-backed fields should use current metadata conventions where supported:
- `client_id``client`
- `contact_id``contact` with `client_id` dependency when applicable
- `ticket_id``ticket`
- `user_id`/owner fields → `user`
- `quote_id` can remain UUID in v1 unless a quote picker already exists or is introduced separately
- interaction type/status can remain UUID fields in v1 unless a supported picker kind exists
- Output schemas should be rich enough for downstream branches: found counts, activity summaries, quote status, email sent flag where available, and changed fields.
## Requirements
### Functional Requirements — First Pass
#### `crm.find_activities`
- Register action ID `crm.find_activities`, version `1`.
- Side-effect-free.
- Inputs:
- optional `client_id`
- optional `contact_id`
- optional `ticket_id`
- optional `user_id`
- optional `type_id`
- optional `status_id`
- optional `date_from`
- optional `date_to`
- optional `limit` defaulted and capped
- optional `on_empty`: `return_empty` or `error`
- Require at least one meaningful filter or date range to avoid unbounded CRM scans.
- Enforce `client:read`, `contact:read`, or a CRM/read-equivalent permission decision documented before implementation. If no CRM-specific permission exists, use the safest existing resource permission based on supplied filters.
- Return:
- `activities`: array of normalized activity summaries
- `count`
- `matched_filters`
- Summaries should include interaction ID, type, status, client/contact/ticket IDs and names where available, title, notes preview, interaction date, start/end time, user ID/name, visibility, category, and tags where available.
#### `crm.update_activity`
- Register action ID `crm.update_activity`, version `1`.
- Inputs:
- `activity_id`
- `patch` object for editable fields:
- `title`
- `notes`
- `status_id`
- `visibility`
- `category`
- `tags`
- `interaction_date`
- `start_time`
- `end_time`
- `duration`
- optionally `type_id` if changing type is product-safe
- optional `reason`
- Reject empty patches.
- Validate activity exists in tenant.
- Validate status IDs are tenant statuses with `status_type = 'interaction'`.
- Validate interaction type IDs against tenant `interaction_types` or `system_interaction_types`.
- Preserve immutable fields such as `tenant`, `interaction_id`, and workflow actor attribution unless explicitly designed otherwise.
- Write run audit with before/after summary and changed fields.
- Return before/after summaries and changed fields.
#### `crm.schedule_activity`
- Register action ID `crm.schedule_activity`, version `1`.
- Inputs:
- `client_id` or `contact_id` (at least one required; resolve client from contact when omitted)
- optional `ticket_id`
- `type_id`
- `title`
- optional `notes`
- optional `status_id` (default to tenant default interaction status)
- `start_time`
- optional `end_time`
- optional `duration`
- optional `visibility`
- optional `category`
- optional `tags`
- optional `assigned_user_id`/`owner_user_id`; default workflow actor in v1
- optional `idempotency_key`
- Validate linked client/contact/ticket relationships.
- Validate `start_time <= end_time` when both are supplied.
- If duration is omitted and start/end are supplied, derive duration consistently with current interaction behavior.
- If status is omitted, use the tenant default `interaction` status or fail with a clear setup error if missing.
- Insert into `interactions` as a future-dated interaction/follow-up.
- Emit `INTERACTION_LOGGED` using existing payload builders through a lazy event publisher helper and deterministic idempotency key.
- Write run audit.
- Return created activity summary.
#### `crm.send_quote`
- Register action ID `crm.send_quote`, version `1`.
- Inputs:
- `quote_id`
- optional `email_addresses`
- optional `subject`
- optional `message`
- optional `no_op_if_already_sent` default `true`
- Validate quote exists in tenant and is not a template.
- Enforce billing update/read authorization equivalent to existing quote send behavior.
- Respect existing approval settings and quote status rules:
- if approval is required, only approved quotes can be sent
- otherwise only draft or approved quotes can be sent
- Prefer shared-safe reuse of underlying billing quote send services/models. Do not import `withAuth` server action wrappers directly into shared runtime. If direct package import is unsafe, extract a shared-safe quote send helper first.
- Preserve existing send behavior: transition quote to `sent`, set `sent_at`, store quote PDF best-effort, send quote email best-effort, record quote activity.
- Return quote summary plus send metadata such as previous status, new status, sent timestamp, recipients, email_sent, and message ID where available.
- Write run audit with quote ID, previous/new status, and email metadata.
### Functional Requirements — Roadmap / Follow-on Scope
These are explicitly useful but not required for the first implementation pass unless scope is expanded.
- `crm.create_interaction_type`: create tenant-specific CRM activity types such as QBR, Site Visit, or Upsell Call.
- `crm.update_activity_status`: dedicated status-transition wrapper around `crm.update_activity` for simple “mark completed/closed” workflows.
- `crm.create_quote`: create a quote from workflow, including template-based creation where safe.
- `crm.convert_quote`: convert quote to draft contract, invoice, or both using existing quote conversion services.
- `crm.find_quotes`: search quotes by client, status, date range, template flag, and pagination options.
- `crm.submit_quote_for_approval`: move eligible draft quotes into quote approval flow.
- `crm.tag_entity`: apply tags to client, contact, or interaction using existing tag definitions/mappings and TAG events.
- `crm.create_client_note`: only if a CRM-wide note action is still needed after the newly merged `clients.add_note` action; otherwise prefer Client/Contact module note actions.
### Cross-cutting Requirements
- Register all first-pass actions in `shared/workflow/runtime/actions/businessOperations/crm.ts` via existing `registerCrmActions()` and `registerBusinessOperationsActionsV2()` wiring.
- Use `withTenantTransaction`, `requirePermission`, `writeRunAudit`, `throwActionError`, and `rethrowAsStandardError` from `businessOperations/shared.ts`.
- Use `withWorkflowJsonSchemaMetadata` from `shared/workflow/runtime/jsonSchemaMetadata.ts` for supported picker fields.
- Use action-provided idempotency for create/send operations where retry duplicates are possible; use engine-provided idempotency for reads and deterministic updates.
- Use lazy dynamic import for workflow event publication from shared runtime action handlers, mirroring the new Client workflow actions.
- Use deterministic event idempotency keys for emitted workflow events.
- Keep implementation additive and backward compatible with existing workflows.
## Data / API / Integrations
### Current workflow files
- `shared/workflow/runtime/actions/businessOperations/crm.ts`
- `shared/workflow/runtime/actions/businessOperations/clients.ts`
- `shared/workflow/runtime/actions/businessOperations/tickets.ts`
- `shared/workflow/runtime/actions/registerBusinessOperationsActions.ts`
- `shared/workflow/runtime/designer/actionCatalog.ts`
- `shared/workflow/runtime/jsonSchemaMetadata.ts`
- `shared/workflow/runtime/actions/businessOperations/shared.ts`
### Existing CRM/client files and patterns
- `packages/clients/src/actions/interactionActions.ts`
- `getInteractionsForEntity`
- `getRecentInteractions`
- `updateInteraction`
- `addInteraction`
- `packages/clients/src/models/interactions.ts`
- `getForEntity`
- `getRecentInteractions`
- `updateInteraction`
- `getById`
- `packages/clients/src/actions/interactionTypeActions.ts`
- `createInteractionType`
- `shared/workflow/streams/domainEventBuilders/crmInteractionNoteEventBuilders.ts`
- `buildInteractionLoggedPayload`
- `buildNoteCreatedPayload`
### Existing quote files and patterns
- `packages/billing/src/actions/quoteActions.ts`
- `createQuote`
- `sendQuote`
- `submitQuoteForApproval`
- `convertQuoteToContract`
- `convertQuoteToInvoice`
- `convertQuoteToBoth`
- `packages/billing/src/models/quote.ts`
- `getById`
- `getByNumber`
- `listByTenant`
- `listByClient`
- `packages/billing/src/services/quoteConversionService.ts`
- `convertQuoteToDraftContract`
- `convertQuoteToDraftInvoice`
- `convertQuoteToDraftContractAndInvoice`
### Existing tag/event files
- `packages/tags/src/actions/tagActions.ts`
- `shared/workflow/streams/domainEventBuilders/tagEventBuilders.ts`
- `shared/workflow/runtime/schemas/crmEventSchemas.ts`
### Existing tables likely touched
- `interactions`
- `interaction_types`
- `system_interaction_types`
- `statuses` where `status_type = 'interaction'`
- `clients`
- `contacts`
- `tickets`
- `quotes`
- quote activity/document/email-related tables used by existing quote send behavior
- `audit_logs`
## Security / Permissions
- All queries and mutations must filter by tenant.
- Activity reads must require a safe read permission. Exact permission mapping must be documented before implementation if no CRM-specific permission exists.
- Activity updates and scheduling must require update/create permissions for the relevant CRM resource or safest existing client/contact permission.
- Quote send must require the same effective billing read/update authorization as existing quote send behavior, including authorization-kernel checks where applicable.
- Workflow actor should remain the actor for audit/event attribution unless a future action version explicitly supports permission-gated actor override.
- Do not allow workflow inputs to set `tenant`, `interaction_id`, quote ownership, or other system-managed fields.
## Observability
- Write `audit_logs` rows with `writeRunAudit` for all side-effectful actions.
- Include action ID/version, target IDs, changed fields, before/after status where relevant, and event/send metadata.
- Use existing workflow event builders and lazy publication helpers for `INTERACTION_LOGGED` where new interactions are created.
- Do not add new metrics in this plan.
## Rollout / Migration
- No database migration is expected for the first pass.
- Runtime/catalog additions are additive and should not break existing workflow definitions.
- Existing `crm.create_activity_note` remains available and unchanged.
- Designer catalog should expose the new CRM actions automatically after runtime initialization.
- If implementation discovers missing status/type constraints or missing quote-safe helper boundaries, update the plan before adding migrations or package refactors.
## Open Questions
1. What is the correct permission resource/action for CRM interactions? Candidate mappings: `client:read/update`, `contact:read/update`, or a CRM/activity-specific permission if one exists.
2. Should `crm.update_activity` emit a workflow event? There is `INTERACTION_LOGGED` for creation, but no obvious `INTERACTION_UPDATED` schema in current CRM event schemas.
3. Should `crm.schedule_activity` be considered a normal `INTERACTION_LOGGED` event even when the interaction date is in the future?
4. Is quote send safe to extract into a shared helper callable from `shared/workflow/runtime`, or should the workflow action perform equivalent persistence while avoiding server-only action imports?
5. Should `crm.send_quote` no-op for already sent quotes, or should it support resend semantics in a later `crm.resend_quote` action?
6. Should `crm.create_client_note` be dropped from the CRM roadmap because `clients.add_note` now exists, or retained as a cross-entity CRM note wrapper?
## Acceptance Criteria (Definition of Done)
- Runtime initialization registers `crm.find_activities`, `crm.update_activity`, `crm.schedule_activity`, and `crm.send_quote` at version `1`.
- Designer catalog shows the new actions under the CRM group with meaningful labels, descriptions, input schemas, output schemas, and supported picker metadata.
- `crm.find_activities` returns tenant-scoped filtered interaction summaries and rejects unsafe unbounded queries.
- `crm.update_activity` validates editable fields, status/type IDs, and tenant ownership, then returns before/after summaries and changed fields.
- `crm.schedule_activity` creates a future-dated interaction linked to valid client/contact/ticket records, audits the change, and emits `INTERACTION_LOGGED` through the shared-runtime-safe event publishing pattern.
- `crm.send_quote` sends/publishes an eligible quote using existing quote send semantics or a shared-safe extraction of that logic, audits the result, and returns send metadata.
- All side-effectful actions enforce permissions and write run audit rows.
- DB-backed tests cover representative success and guard/failure paths for activity update/schedule and quote send eligibility.
- Unit tests cover action registration, designer CRM grouping, picker metadata, and event payload compatibility.
- No existing workflow runtime, Client workflow action, Ticket workflow action, or CRM activity note tests regress.