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

5.0 KiB

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:

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.

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.