Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
7.6 KiB
Scratchpad — Ticket Close Rules
- Plan slug:
2026-06-10-ticket-close-rules - Created: 2026-06-10
- Design doc:
docs/plans/2026-06-10-ticket-close-rules-design.md
Decisions
- (2026-06-10) Per-board scope for all rules — matches statuses/SLA/email settings scoping.
- (2026-06-10) Hard block +
ticket:close_overridepermission; overrides audit-logged with failure list. - (2026-06-10) Client portal exempt from gates (customers can't satisfy internal-hygiene conditions) but gets the closure-recording fix; portal bypass audit-logged.
- (2026-06-10) Auto-close engine = single 15-min recurring scan via
IJobRunner(Temporal EE / pg-boss CE). Per-ticket Temporal workflows rejected: CE would still need the scan, inactivity resets need signal plumbing across every comment path (SLA Temporal workflow already grew a self-healing poll for this drift), config changes invalidate in-flight timers.ticket_auto_close_stateis engine-agnostic if we ever swap. - (2026-06-10) Template items are copied onto tickets (provenance via
template_id), never referenced — template edits must not mutate history. Idempotency key = template_id present among ticket's items. - (2026-06-10)
closed_by = nullfor auto-closes; attribution via audit row (actor_type: 'system',source: 'system'). No dedicated system user UUID exists in the codebase. - (2026-06-10) Checklist accountability = permanent
completed_by/completed_atdisplayed inline, no confirm dialog; uncheck clears but audit preserves prior signoff. - (2026-06-10) Test strategy 80/20: automated tests own logic/data correctness (units, integration, races, tenant isolation, regressions — 37 tests); screen flows, job wiring, and i18n moved to the risk-framed manual pass in
SMOKE_TESTS.md. Former T034 folded into T032; T036/T038–T045/T048 replaced by smoke flows.
Discoveries / Constraints
-
No pre-close validation exists anywhere today; only idempotency guards in workflow
tickets.closeand the "closed status can't be board default" config constraint (packages/reference-data/.../statusActions.ts:161). -
Four open→closed flip detection sites must be wired:
ticketActions.tsupdateTicket (~724–1064),optimizedTicketActions.tsupdateTicketInTransaction (~2386–2442),TicketService.tsupdate (1188–1221), portalclient-tickets.tsupdateTicketStatus (712, currently doesn't even set closure fields — known gap, fixed by F029). -
Resolution comment markers:
comments.is_resolutionboolean ANDmetadata->>'closes_ticket'(set by the TicketDetails close-with-comment flow atoptimizedTicketActions.ts:2734). Gate accepts either. The close-flow inserts the comment before the status update, so gate ordering works naturally. -
Time entry linkage:
time_entries.work_item_id+work_item_type = 'ticket'. -
Bundles: inline
tickets.master_ticket_id, no join table. Open child =closed_at IS NULL. -
Board settings dialog pattern to copy: inbound-reply-reopen bordered section in
BoardsSettings.tsx(~1139–1218); status list uses up/down buttons, not DnD — reuse for template item ordering. -
New settings tab goes in
TicketingSettings.tsxTICKETING_TAB_IDS, not the top-levelSettingsPage.tsx. -
Job model:
reconcile-bucket-usageregistration inregisterAllHandlers.ts(~266–275) + per-tenant cron loop ininitializeScheduledJobs.ts. -
Notification template model migration:
20250226090000_add_credit_expiration_notification.cjs. Subtypenamestring must exactly match what's passed tosendNotificationIfEnabled. -
Audit:
writeTicketActivity(shared/lib/ticketActivity/), event constants intypes.ts, rendering inTicketActivityTimeline.tsx. Actor types includesystem; sources includesystemandclient_portal. -
RBAC:
hasPermission(user, resource, action)frompackages/authorization/src/rbac.ts; permission seed model20250619120000_add_comprehensive_permissions.cjs. -
422 pattern: throw
ValidationError('...', details)→ApiBaseControllerserializes with HTTP 422. -
Workflow action registry:
getActionRegistryV2().registerinregisterTicketActions(); next free action ID is A09. -
(2026-06-10) RLS is retired for new tables (see 20251111120000_disable_rls_on_survey_tables.cjs); the close-rules tables follow ticket_audit_logs: composite (tenant, id) PKs + Citus distribution + guarded raw FKs, no RLS.
-
(2026-06-10)
comments.author_typeDB enum has no 'system' value — TicketModel.createComment maps system→'internal'; auto-close comment provenance lives inmetadata.source = 'auto_close'. -
(2026-06-10) The blocked-close dialog uses a non-throwing
checkTicketClosureserver action instead of catching TicketCloseValidationError client-side: custom Error fields don't survive the Next server-action boundary. -
(2026-06-10) Auto-apply hooks live at TicketModel.createTicket (all creation paths) and the two update paths; the apply helpers moved to shared/lib/ticketChecklists to avoid a packages/tickets→shared cycle. Bypass-audit helpers live in shared/lib/ticketCloseRules for the same reason (workflow runtime can't import packages/tickets).
-
(2026-06-10) updateTicketInTransaction gained
options.systemActorfor the auto-close engine: closed_by stays null, events publish with a SYSTEM actor (v2 ticketClosedEventPayloadSchema allows omitted closedByUserId), live updates are skipped, and the audit row is system-sourced. -
(2026-06-10)
updateTicket's catch-all used to flatten every error into 'Failed to update ticket'; TicketCloseValidationError is now re-thrown so the fallback UI path surfaces the unmet conditions.
Commands / Runbooks
- Migrations (CE+EE combined):
cd server && DB_HOST=localhost DB_PORT=5472 DB_NAME_SERVER=server DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=$(cat ../secrets/postgres_password) node scripts/run-ee-migrations.js latest. - Integration tests:
cd server && DB_HOST=localhost DB_PORT=5472 DB_PASSWORD_ADMIN=$(cat ../secrets/postgres_password) DB_PASSWORD_SERVER=$(cat ../secrets/db_password_server) npx vitest run src/test/integration/ticketCloseRules.integration.test.ts src/test/integration/ticketChecklists.integration.test.ts src/test/integration/autoCloseTickets.integration.test.ts --coverage.enabled=false. The explicit password env overrides are required:.env.localtestsets DB_PASSWORD_* to /run/secrets paths that don't exist on the host, and the secret provider then falls back to those literal strings. - Server type-check needs a big heap:
NODE_OPTIONS=--max-old-space-size=16384 npx tsc --noEmit -p tsconfig.json(default heap OOMs). - Dev stack for this worktree: alga-dev-env-manager / alga-env-manager skills.
Links / References
- Design doc:
docs/plans/2026-06-10-ticket-close-rules-design.md(commit 1a64dccb0d) - Prior art for plan format:
docs/plans/2026-05-15-outbound-webhooks-for-projects/
Known issues (not from this feature)
commentActionsThreading.integration.test.tsT078 fails on this branch AND on the pre-implementation commit (cade9ab5a5): its cleanup deletes users that ticket_audit_logs rows reference (ticket_audit_logs_actor_user_fkey). Pre-existing; not introduced by close rules.
Open Questions
- Required-fields allowed set frozen at category/subcategory/priority/assignee for v1 — revisit if customers ask for custom fields.
- Mobile/REST checklist CRUD endpoints deferred (non-goal §3) — revisit when mobile wants checklists.
- Email-created tickets: auto-apply hooks TicketModel.createTicket, the single chokepoint all creation paths (UI, API, CSV, inbound email) funnel through — resolved.
- The progress chip renders in the banner row above TicketInfo rather than literally beside the status dropdown (which lives deep in TicketInfo); revisit if the placement doesn't land.