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

221 lines
12 KiB
Markdown

# PRD: Ticket Close Rules
- **Status:** Draft (scope approved via design review)
- **Owner:** Robert Isaacs
- **Created:** 2026-06-10
- **Design doc:** `docs/plans/2026-06-10-ticket-close-rules-design.md`
- **Branch:** `feature/ticket-close-rules`
## 1. Problem statement & user value
Nothing governs how a ticket gets closed today. Any status update landing on a
status with `is_closed = true` closes the ticket — no required resolution, no
time-entry check, no completion checklist — and tickets abandoned in "Waiting
for Customer" sit open forever unless a human remembers them. MSPs lose billing
(closed tickets with no time logged), lose accountability (no record of who
verified completion steps), and carry stale open-ticket noise.
This feature gives boards three new capabilities:
1. **Pre-close validation gates** — per-board conditions a ticket must satisfy
before a human can close it, hard-blocked with a permissioned override.
2. **Ticket checklists** — first-class checklist items on tickets (ad-hoc,
template, auto-applied, or workflow-applied) where checking an item
permanently records and conspicuously displays who checked it and when.
Required-item completion is one of the close gates.
3. **Auto-close rules** — tickets sitting in a configured status with no
activity for N days close automatically, with an optional advance warning
to the customer.
## 2. Goals
- Per-board close gates: resolution comment, ≥1 time entry, required checklist
items complete, no open bundled children, admin-chosen required fields.
- Hard block with structured failure reporting in UI (dialog) and API (422);
override via new `ticket:close_override` permission, audit-logged.
- Checklists with permanent `completed_by`/`completed_at` accountability,
template management UI, auto-apply matchers, and a workflow action.
- Auto-close via a single recurring scan job on the existing `IJobRunner`
abstraction (Temporal on EE/appliance/on-prem, pg-boss otherwise).
- Auto-close rides the normal close path so emails, SLA recording, surveys,
webhooks, and search reindexing fire exactly as for a manual close.
- Fix the client portal closure-recording gap (`is_closed`/`closed_at`/
`closed_by` + `TICKET_CLOSED` publication) as part of wiring.
## 3. Non-goals
- No reopen rules (who may reopen, reopen windows) — future work.
- No per-rule block-vs-warn configuration; all enabled gates hard-block.
- No attestation ceremony (typed initials/notes) on checklist check-off.
- No generic transition-policy engine; gates are ticket-closure-specific.
- No per-ticket Temporal workflow engine for auto-close (see design §2 for the
rejection rationale); the scan is the single engine for all editions.
- No REST API endpoints for checklist CRUD in v1 (UI uses server actions);
the API surface is limited to close-validation behavior on existing
ticket update endpoints.
- No backfill of accountability fields onto project task checklists.
- Client portal users are **exempt** from close gates (they cannot satisfy
internal-hygiene conditions); portal closures are audit-logged as such.
## 4. Users & primary flows
- **Technician:** sees a checklist on the ticket with a progress chip near the
status control; checks items (name + timestamp recorded inline); attempts to
close; if gates fail, a dialog lists each unmet condition with a quick
action (jump to checklist, add time).
- **Dispatcher/Manager (with `ticket:close_override`):** same dialog plus
"Close anyway" with optional reason; override and failure list land in the
ticket audit log.
- **Admin:** configures gates and auto-close rules per board in board
settings; manages checklist templates and their auto-apply matchers in a
new Ticketing settings tab.
- **Customer (portal):** can still close/resolve their own tickets unimpeded;
receives an optional warning email before auto-close; replying resets the
inactivity timer and cancels the pending close.
- **Automation:** workflows can apply a checklist template to a ticket;
workflow `tickets.close`, CSV import, and the auto-close engine bypass
gates with the bypass audit-logged.
## 5. Functional requirements
### 5.1 Close validation gates
- One `board_close_rules` row per board: `require_resolution_comment`,
`require_time_entry`, `require_checklist_complete`,
`require_no_open_children`, `required_fields` (jsonb array from the allowed
set: category, subcategory, priority, assignee), `is_enabled`.
- Shared chokepoint `validateTicketClosure(trx, tenant, ticketId, user, opts)`
in `packages/tickets`, evaluated inside the caller's transaction at the
exact open→closed flip detection points in: `updateTicket`
(`ticketActions.ts`), `updateTicketInTransaction`
(`optimizedTicketActions.ts`), `TicketService.update` (REST v1).
- Gate queries: resolution comment → `comments.is_resolution = true` or
`metadata->>'closes_ticket' = 'true'`; time entry → any `time_entries` row
with `work_item_id = ticket_id AND work_item_type = 'ticket'`; checklist →
no incomplete `is_required` items in `ticket_checklist_items`; children →
no `tickets` rows with `master_ticket_id = ticket_id AND closed_at IS NULL`;
required fields → null checks on the ticket row.
- Failure → typed `TicketCloseValidationError` with
`failures: [{ rule, message, meta }]`; UI renders the blocked-close dialog,
API returns 422 with structured details via the existing `ValidationError`
pattern.
- `overrideCloseRules: true` honored only when the server confirms
`ticket:close_override`; writes `TICKET_CLOSE_RULES_OVERRIDDEN` (with the
failure list and optional reason) to `ticket_audit_logs`.
- Bulk close validates per ticket and reports per-ticket results.
- Bypass flag for workflow `tickets.close`, CSV import, auto-close engine,
and client portal; bypassed closures audit-logged.
### 5.2 Ticket checklists
- `ticket_checklist_items`: name, description, order, assignee, `is_required`
(default true), `completed`, `completed_by`, `completed_at`, provenance
(`source`: manual | template | workflow; nullable `template_id`).
- Check sets `completed_by`/`completed_at` permanently displayed inline;
uncheck clears them and writes an audit entry recording the prior signoff.
- Templates (`checklist_templates` + `checklist_template_items`) are copied
onto tickets, never referenced; later template edits do not mutate history.
- Auto-apply matchers (`checklist_template_apply_rules`): nullable board /
category / subcategory / priority (null = any), evaluated on ticket
creation and on board/category change; additive and idempotent (a template
never applies twice to one ticket).
- New workflow action `tickets.apply_checklist` registered in
`registerTicketActions()` (`shared/workflow/runtime/actions/businessOperations/tickets.ts`).
### 5.3 Auto-close rules
- `board_auto_close_rules` (multiple per board): `trigger_status_id`,
`inactivity_days`, nullable `warning_days_before`, `close_to_status_id`
(must be `is_closed`), `is_enabled`.
- Recurring job `auto-close-tickets` every 15 minutes via
`IJobRunner.scheduleRecurringJob`, registered in `registerAllHandlers.ts` /
`initializeScheduledJobs.ts` following the `reconcile-bucket-usage` model.
- Per run: (1) match eligible tickets, compute `last_activity_at` (latest of
comments, status changes, customer replies) and upsert
`ticket_auto_close_state` (`rule_id`, `scheduled_close_at`,
`warning_sent_at`) — newer activity recomputes and resets; (2) send
`ticket-auto-close-warning` notification when inside the warning window,
stamp `warning_sent_at`; (3) close due tickets via
`updateTicketInTransaction` with system actor, bypass flag, and an
automatic comment, re-validating inactivity inside the closing transaction.
- Per-tenant, per-ticket try/catch; idempotent re-runs.
- `closed_by` is null for auto-closed tickets; audit entry uses
`actor_type: 'system'`, `source: 'system'`.
## 6. Data model
Six new tables (all tenant-scoped, composite `(tenant, id)` PKs per Citus
conventions, with RLS policies): `board_close_rules`,
`ticket_checklist_items`, `checklist_templates`, `checklist_template_items`,
`checklist_template_apply_rules`, `board_auto_close_rules`,
`ticket_auto_close_state`. Full column detail in design doc §3.
New permission row: resource `ticket`, action `close_override`, granted to
Admin by default. New notification subtype + system email template
`ticket-auto-close-warning`.
## 7. UI / UX notes
- **Board settings (`BoardsSettings.tsx` dialog):** "Close Rules" bordered
section (following the inbound-reply-reopen section pattern) with gate
toggles + required-fields multi-select; "Auto-Close Rules" list editor
(trigger status, inactivity days, warning lead, target closed status).
- **Ticketing settings:** new `checklist-templates` sub-tab in
`TicketingSettings.tsx` (`TICKETING_TAB_IDS`) hosting
`ChecklistTemplatesSettings` — template CRUD, item ordering via the
existing up/down button pattern, per-item `is_required`, apply-rule
matchers editor.
- **Ticket details:** new `TicketChecklistSection.tsx` composed into
`TicketDetails.tsx` like sibling sections; checked items show checker
avatar/name + timestamp inline; progress chip ("3 of 5 required done")
near the status control; blocked-close dialog with per-failure quick
actions and permissioned "Close anyway".
- **Auto-close banner** on tickets with pending `ticket_auto_close_state`:
"Will close automatically on <date> unless there's new activity."
- Checklist activity (check/uncheck, template applied, auto-close warning/
close) renders in `TicketActivityTimeline.tsx`.
## 8. Security / permissions
- Gate override requires `ticket:close_override` (checked server-side via
`hasPermission(user, 'ticket', 'close_override')`).
- Template management rides existing settings-admin permission; checking
items requires ordinary ticket update permission.
- Portal exemption is server-enforced by path, not client-asserted.
## 9. Rollout / migration
- All new tables and the permission ship in standard knex migrations; no
backfill required. Boards without a `board_close_rules` row (or with
`is_enabled = false`) behave exactly as today.
- Auto-close affects only boards where an admin creates an enabled rule.
## 10. Open questions
- Required-fields allowed set: start with category, subcategory, priority,
assignee — extend later if asked.
- Whether checklist REST endpoints are needed for the mobile app (deferred,
see non-goals).
## 11. Acceptance criteria (definition of done)
- A board with all gates enabled blocks closure from MSP UI (single + bulk)
and REST API when any gate fails, listing every unmet condition; closure
succeeds once conditions are met.
- A user with `ticket:close_override` can close anyway; the override with
failure list appears in the ticket audit log. A user without it cannot.
- Checking a checklist item records and displays who/when; unchecking clears
it and leaves an audit trail. Required items gate closure; optional don't.
- Applying a template copies its items once (idempotent); auto-apply rules
attach templates on creation and board/category change.
- A ticket in a rule's trigger status with no activity for `inactivity_days`
is closed by the scan with a system comment, and all normal closure side
effects fire (email, SLA resolution, surveys, webhooks). A customer reply
before the deadline cancels the pending close; the warning email goes out
once at the configured lead time.
- Portal status changes to a closed status set `is_closed`/`closed_at`/
`closed_by` and publish `TICKET_CLOSED`, without gate enforcement.
- All features in `features.json` implemented; all tests in `tests.json`
passing; the manual smoke pass in `SMOKE_TESTS.md` executed clean against
the running app.