seubert-claims/ondeck/docs/task-automation-alignment-plan.md
lorentz 835267c333 feat(automation): move task auto-generate to post-sync + admin config UI
Extracts the cron-only auto-generate logic into shared modules
(lib/sync/auto-generate.ts, automation-config.ts) and adds
group-auto-assign.ts, which attaches ungrouped policies to an existing
renewal group when their renewal dates are close. Both now also run
automatically after every AMS sync (sync-engine.ts), guarded by a
Postgres advisory lock so overlapping syncs cannot double-generate.
The external /api/cron/auto-generate endpoint remains for manual/
out-of-band triggering, delegating to the same shared engine.

Adds an admin settings UI + /api/admin/automation-settings route to
configure grace periods and toggles without a redeploy, and surfaces
the new audit actions (AUTO_GENERATE_TASKS_CLIENT, AUTO_ASSIGN_POLICY_GROUP)
in the audit log filters.

Includes a fixed AUTOMATION_GO_LIVE_AT floor (2026-07-07) in
automation-config.ts, applied to every candidate query in
auto-generate.ts and group-auto-assign.ts. Without it, the first run
after enabling this treated every historical policy/client/group that
happened to be old enough as newly eligible, generating tasks anchored
to old renewal/expiration dates in one burst (incident: 6,552 tasks
created 2026-07-07 02:01, most already 1-2 years overdue for Jeanne
Strong and Luke Billman; cleaned up same day after a verified backup).
The floor ensures automation only ever processes records created from
its go-live date forward.
2026-07-07 18:26:12 +00:00

103 lines
9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Task Automation — Alignment Plan
**Date:** 2026-06-25
**Source:** Code review of `/opt/projects/OnDeck/ondeck` against the Seubert task-automation meeting (Lorentz Hinrichsen, Luke Billman, Dawn Boland).
**Status:** Planning — to be executed at a later date.
---
## Executive Summary
The meeting reaffirmed the design for SHAPE task automation: tasks should only generate for **active SHAPE / SHAPE 2 clients with active policies**, the manager setup queue should be drivable **to zero**, and client active/inactive status changes should be **logged and surfaced in a daily briefing**.
The Horizon data model and the core task mechanics already match the agreed design. **Tasks are policy-specific (not carrier-specific), loss-run tasks follow the policy renewal date, and clients are never auto-deactivated** — the advocate stays on the profile so returning clients resume cleanly. These need no change.
Three decisions from the call are **not yet built**, and one **likely defect** can mask half the SHAPE population. In priority order:
1. **Generation is not gated on active policies.** Tasks generate for any client that *has* policies, regardless of whether those policies are Cancelled/Expired/Non-Renewed. The dead-policy filter only hides tasks at display time — they are still created. This contradicts the meeting's central rule: *"no active policies → no tasks of any kind."*
2. **No way to clear non-P&C-SHAPE accounts from the manager queue.** Client setup still *requires* a claims advocate before it can be completed, with no "NA / no advocate / other department" option. Accounts like surety/bond-shape (in-transit example) therefore cannot be cleared, so the queue can never reach zero.
3. **No status-change logging or daily briefing.** Client active↔inactive transitions are not recorded anywhere, and no briefing/digest feature exists.
4. **(Likely bug) `Shape2` vs `Shape 2` name mismatch.** The secondary designation is created/synced as `Shape2` (no space) but nearly every read query filters for `Shape 2` (with a space). If the stored name is `Shape2`, all SHAPE-2 clients are silently excluded from the manager queue, dashboard, and metrics.
None of the proposed work changes the database schema meaningfully — it is logic-only — so it is safe to deploy in the agreed 7 PM5 AM window.
---
## What already matches (no action needed)
| Meeting decision | Code reality |
|---|---|
| Tasks are policy-specific, not carrier-specific | `Task.policyId` / `policyGroupId`; carrier only on `Policy.carrierName`; generation filters by `policyTypeFilter`, never carrier (`src/lib/sync/auto-generate.ts:179`) |
| Loss-run tasks follow the policy renewal date | Negative `daysOffset` anchored to `policy.expirationDate + 1` (`auto-generate.ts:175-185`) |
| Don't auto-mark clients inactive; advocate persists | No `isActive` flag on `Client`; nothing auto-deactivates; `claimsAdvocateId` persists |
| Logic-only change, safe maintenance window | Auto-generation writes no schema; scheduled sync runs 02:00 (`0 2 * * *`) |
---
## Items to address (priority order)
### Priority 1 — Gate task generation on active policies
**Problem:** `src/lib/sync/auto-generate.ts` selects clients/policies/groups by *existence* of policies, not status. Dead-policy filtering (`['Cancelled','Expired','Non-Renewed','Rewritten','Not taken']`) only runs at display time (`api/clients/[id]/tasks/route.ts:29`), so tasks for hibernating/departed clients are still created and merely hidden.
**Target behavior:** A client with no active policy generates **no** client-, policy-, or group-level tasks.
### Priority 2 — Client-level "NA / no advocate" path
**Problem:** Setup completion requires an advocate (`src/components/renewal-groups/setup-wizard.tsx:422-426`, disabled button at `:436`). A client leaves the manager queue only when **both** `claimsAdvocateId` and `setupCompletedAt` are set (`manager/setup/page.tsx:40-43`). There is no client-level NA concept (the existing `NA` is a *task* status).
**Target behavior:** A manager can mark a client as NA / no-advocate / other-department, which clears it from the setup queue without assigning an advocate.
### Priority 3 — Status-change logging + daily briefing
**Problem:** No client active↔inactive transition is written to `AuditLog`; `Notification.create` is never called; no briefing/digest/summary feature exists.
**Target behavior:** When a client crosses the active/inactive line (e.g. a new policy reactivates them, or their last active policy lapses), log it; roll those events into a daily briefing for managers.
### Priority 4 (verify first) — `Shape2` vs `Shape 2` naming
**Problem:** Created/synced as `Shape2` (`api/admin/sync-designations/route.ts:22,84,92`; `scripts/import-shape-tasks.ts:68`) but read as `Shape 2` across manager queue, dashboard, metrics, workload. Import scripts hedge with both forms; runtime read queries do not.
**Target behavior:** One canonical name used consistently everywhere.
---
## Step-by-step plan (for later execution)
### Phase 0 — Verify the designation name (do this first; ~15 min)
1. Confirm the stored name with: `SELECT name FROM designations WHERE name ILIKE 'shape%';`
2. If it returns `Shape2`: either (a) rename the row to `Shape 2`, **or** (b) normalize every read filter to match the import scripts' `['Shape','Shape 2','Shape2']`. Pick one canonical form and apply it everywhere.
3. Re-check manager queue / dashboard / metrics counts before vs. after to confirm SHAPE-2 clients now appear.
### Phase 1 — Active-policy gate on task generation (Priority 1)
1. Define a single shared `ACTIVE_POLICY_STATUSES` (or reuse the existing `DEAD_STATUSES` exclusion) in one module so generation and display agree.
2. In `auto-generate.ts`:
- Client-level query (`~:228-232`): require at least one **active** policy (status not in dead set), not merely any policy/group.
- Policy-level generation (`~:149-221`): skip policies whose status is dead.
- Group-level generation (`~:80-146`): skip groups with no active policies.
3. Write tests covering: all-dead-policies client → 0 tasks; mixed active/dead → tasks only for active; reactivation (dead → active policy added) → tasks resume.
4. Decide and document the policy on **already-generated** tasks for now-inactive clients (leave hidden vs. mark NA/cancelled). Confirm with Luke/Lorentz.
### Phase 2 — Client NA / no-advocate path (Priority 2)
1. Decide the data representation (recommend a nullable `Client.setupStatus` enum or a `clearedReason` + boolean, avoiding a heavy schema change). Confirm naming with stakeholders.
2. Update the setup wizard so a manager can choose "NA / no advocate / other department" instead of an advocate, and allow completion in that case (relax `setup-wizard.tsx:422-426` / the `canFinish` guard).
3. Update the manager setup queue (`manager/setup/page.tsx:38-43`, `api/clients/setup-queue/route.ts`) so NA-marked clients drop off the queue.
4. Ensure NA-marked clients are excluded from (or clearly distinguished in) generation per the Phase 1 rules.
5. Tests: NA client clears the queue; NA client generates no advocate-assigned tasks; queue can reach zero.
### Phase 3 — Status-change logging + daily briefing (Priority 3)
1. Add active↔inactive transition detection at the point policies change (post-sync and/or on policy create/cancel). Derive status from active-policy presence (consistent with Phase 1).
2. Write transitions to `AuditLog` (and/or `Notification`) with old/new status and trigger.
3. Build the daily briefing aggregation (new clients needing setup, status changes, queue size) and choose delivery: in-app notification first; Teams/email later (explicitly deferred in the meeting).
4. Schedule within the 7 PM5 AM window; align with the existing 02:00 sync.
5. Tests: reactivation logs an event; lapse logs an event; briefing aggregates a day's events.
### Phase 4 — Non-SHAPE generation gate (follow-on to Priority 1/2)
1. Decide whether non-SHAPE clients should generate **zero** tasks (meeting implied yes) or only generic templates.
2. If zero: apply the SHAPE/SHAPE 2 designation gate (already used by the manager queue) to `auto-generate.ts` as well.
3. Tests: non-SHAPE client → 0 generated tasks.
### Cross-cutting
- All changes are logic-only (Phase 2/3 may add a small column/enum — coordinate a `prisma db push` per `CLAUDE.md`). Deploy in the 7 PM5 AM window.
- After each phase: rebuild and restart per `CLAUDE.md` (`docker compose build horizon-app && docker compose up -d horizon-app`).
- Validate against a sanitized/dev copy before production where possible.
---
## Open questions for Luke / Lorentz
1. For clients that go inactive, what happens to **tasks already generated** — leave hidden, auto-NA, or cancel?
2. Exact set of "NA" reasons for the setup queue (other department? surety/bond? no rep?) and whether each should still appear anywhere for reference.
3. Daily briefing delivery channel and recipients for v1 (in-app vs. Teams vs. email).
4. Should **non-SHAPE** clients generate *no* tasks at all, or just no SHAPE-specific tasks?