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.
103 lines
9 KiB
Markdown
103 lines
9 KiB
Markdown
# 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 PM–5 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 PM–5 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 PM–5 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?
|