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
98 lines
5.0 KiB
Markdown
98 lines
5.0 KiB
Markdown
# Ticket Activity Conventions
|
|
|
|
Author: Robert Isaacs · Date: 2026-05-25
|
|
|
|
This document captures the conventions any new ticket mutation path must follow to participate in the unified ticket activity timeline. It complements `PRD.md`.
|
|
|
|
## Where to write activity rows
|
|
|
|
Use the shared helper:
|
|
|
|
```ts
|
|
import {
|
|
TICKET_ACTIVITY_ACTOR,
|
|
TICKET_ACTIVITY_ENTITY,
|
|
TICKET_ACTIVITY_EVENT,
|
|
TICKET_ACTIVITY_SOURCE,
|
|
writeTicketActivity,
|
|
} from '@alga-psa/shared/lib/ticketActivity';
|
|
```
|
|
|
|
Write inside the same transaction as the underlying ticket/comment/document mutation. This keeps activity rows atomically consistent with the data they describe.
|
|
|
|
```ts
|
|
await writeTicketActivity(trx, {
|
|
tenant,
|
|
ticketId,
|
|
eventType: TICKET_ACTIVITY_EVENT.STATUS_CHANGED,
|
|
entityType: TICKET_ACTIVITY_ENTITY.TICKET,
|
|
entityId: ticketId,
|
|
actor: { actorType: TICKET_ACTIVITY_ACTOR.USER, userId: user.user_id },
|
|
source: TICKET_ACTIVITY_SOURCE.UI,
|
|
changes: curatedDiff,
|
|
details: { ...optionalMetadata },
|
|
});
|
|
```
|
|
|
|
## Explicit tenant — required
|
|
|
|
`writeTicketActivity` never reads `app.current_tenant`. This is intentional so it works inside `withAdminTransaction()` paths (inbound email, workflow runner) which do not set the GUC.
|
|
|
|
If you are tempted to "let the helper figure out tenant," stop and pass it explicitly.
|
|
|
|
## Field diffs — use the curated helper
|
|
|
|
For ticket updates, use `buildCuratedTicketDiffWithLabels` (or `buildCuratedTicketDiff` if you already have labels resolved). The helper:
|
|
|
|
- Returns only the user-meaningful fields listed in `CURATED_TICKET_FIELDS`.
|
|
- Skips no-op updates (deep-equal old and new).
|
|
- Optionally attaches `oldLabel`/`newLabel` resolved against the relevant lookup table.
|
|
|
|
If no curated fields changed, the helper returns `{}`. Use `hasCuratedChanges()` and skip the activity write — the UI does not want noise from updates that only touched `updated_at` or internal denormalized flags.
|
|
|
|
If you need to log a new field, add it to `CURATED_TICKET_FIELDS` (in `shared/lib/ticketActivity/types.ts`). Do not bypass the helper.
|
|
|
|
## Safe metadata rules
|
|
|
|
`details` and `changes` are JSONB. They are read by the timeline UI and may be returned through internal APIs. They must NOT contain:
|
|
|
|
- Raw inbound email bodies (text/html). Store only `messageId`, `threadId`, `from`, `subject`, `provider`, `receivedAt`.
|
|
- Full old/new comment body content for edits. Store only edit-metadata (e.g., `{ edited: true, is_internal: false }`).
|
|
- Secrets, API tokens, or PII not already visible on the ticket.
|
|
|
|
Comment edits are metadata-only by design. If a future feature needs body history, it should live in a separate body-snapshot table, not in `ticket_audit_logs`.
|
|
|
|
## Event-type vocabulary
|
|
|
|
Use the constants in `TICKET_ACTIVITY_EVENT`. Names mirror the existing domain events in `packages/event-schemas/src/schemas/domain/ticketEventSchemas.ts` where one exists. If you need a new event, add it to the constant set rather than inventing strings at call sites; the UI formatter dispatches on these names.
|
|
|
|
When picking the event for a curated-diff update, prefer the most specific match:
|
|
|
|
- single status_id change → `TICKET_STATUS_CHANGED` (or `TICKET_CLOSED`/`TICKET_REOPENED` when transitioning the `is_closed` boundary)
|
|
- single priority_id → `TICKET_PRIORITY_CHANGED`
|
|
- single assigned_to → `TICKET_ASSIGNED` / `TICKET_UNASSIGNED`
|
|
- single board_id → `TICKET_BOARD_MOVED`
|
|
- single response_state → `TICKET_RESPONSE_STATE_CHANGED`
|
|
- multiple curated fields → `TICKET_UPDATED`
|
|
|
|
## Actor and source
|
|
|
|
- `actor.actorType` should be the actor classification, not the channel. A request originating from the REST API is `API`; an inbound email parsed to a contact is `EMAIL_SENDER`; a bundle-master reopen triggered by a child reply is `SYSTEM`.
|
|
- `source` is the origin channel and is independent of actor type. For example, an inbound-email reply uses `actor=EMAIL_SENDER` and `source=INBOUND_EMAIL`.
|
|
|
|
## Failure semantics
|
|
|
|
Activity writes are intended to fail fast inside the surrounding transaction (NFR-03). If you need a path to tolerate write failure as best-effort, wrap your call in `try/catch` and document the reason at the call site. Do not change the helper.
|
|
|
|
Display-name enrichment failures are best-effort: the helper logs and falls back to the actor's IDs without throwing.
|
|
|
|
## Read paths
|
|
|
|
- `readTicketActivity(knex, tenant, ticketId)` — activity rows only, ordered newest-first.
|
|
- `buildUnifiedTicketTimeline(knex, tenant, ticketId)` — activity + comments merged chronologically (internal-only in v1; do NOT call from the client portal).
|
|
- `getTicketTimelineEntries` (server action) — production entry point used by the MSP UI. Enforces internal `ticket:read` permission and blocks client portal users.
|
|
|
|
## Generic `audit_logs` is untouched
|
|
|
|
The legacy `packages/db/src/lib/auditLog.ts` (and its `server/src/lib/logging/auditLog.ts` copy) remain in place and continue to serve RBAC-style logging. Do not consolidate the two in v1. They have different scope, schema, and tenant-context contract than `ticket_audit_logs`.
|