# Phase J — Case Workflow Management

**Branch**: `feat/fraud-case-workflows`
**Parent**: `feat/fraud-rules-jube-parity`
**Jube reference**: `CaseWorkflow*` POCO family (11 tables) + Image-5/6/7 "Case Workflow" tabs.

---

## 1. Goal

The Activation rule already references `case_workflow_key` and
`case_workflow_status_key`. What's missing is the **operator UI** to define
workflows, statuses, actions, forms, filters, macros, xpaths and roles —
plus the **runtime case opener** that consumes `SideEffect{:case}` (already
emitted by Phase E) and inserts a `risk_case` row mapped to the selected
workflow.

---

## 2. Schema inventory (already exist)

| Table | Purpose |
|---|---|
| `risk_case_workflows`          | top-level workflow definition (active, locked, visualisation) |
| `risk_case_workflow_statuses`  | named stages with colours + priority |
| `risk_case_workflow_actions`   | buttons available on a case (transition + side-effects) |
| `risk_case_workflow_forms`     | structured-data captures attached to actions |
| `risk_case_workflow_filters`   | predefined case-list filters |
| `risk_case_workflow_macros`    | reusable HTTP/AMQP outbound actions |
| `risk_case_workflow_xpaths`    | which payload fields to surface in case viewer |
| `risk_case_workflow_roles`     | RBAC — which roles can transition / edit |
| `risk_cases`                   | the case rows themselves |
| `risk_case_events`             | audit trail per case |
| `risk_case_diary` / `risk_case_form_entry` / `risk_case_file` | attachments & journal |

No migrations needed except small `version` + `created_by` additions for parity.

---

## 3. Work breakdown

### 3.1 Migrations

For each of the 8 `risk_case_workflow_*` tables:
- `add :version, :integer, default: 1, null: false`
- `add :created_by, :string`

Plus on `risk_cases`:
- Index `(tenant_id, workflow_id, status_id, opened_at desc)`.
- Index `(tenant_id, activation_rule_id, opened_at desc)`.

### 3.2 Context expansion

Centralise under `InfraRepo.Risk.CaseWorkflows`:

```elixir
defmodule InfraRepo.Risk.CaseWorkflows do
  # workflow CRUD
  list_workflows/1   get_workflow/1   upsert_workflow/1   delete_workflow/1
  # nested children — passed parent_id
  list_statuses/1    upsert_status/1    delete_status/1
  list_actions/1     upsert_action/1    delete_action/1
  list_forms/1       upsert_form/1      delete_form/1
  list_filters/1     upsert_filter/1    delete_filter/1
  list_macros/1      upsert_macro/1     delete_macro/1
  list_xpaths/1      upsert_xpath/1     delete_xpath/1
  list_roles/1       grant_role/1       revoke_role/1
end
```

And a runtime helper:

```elixir
def open_case(%{tenant_id: t, workflow_key: wk, status_key: sk,
                rule_id: r, payload: pl, activation_score: s}) do
  # resolve keys → ids, create RiskCase + initial RiskCaseEvent
end
```

### 3.3 Runtime opener (NEW)

Hook into `SideEffectDispatcher`:

```elixir
defp dispatch_one(%{kind: :case, rule_id: rid, rule_name: rn,
                    details: %{workflow_key: wk, status_key: sk,
                               case_key: ck, entity_value: ev, score: s,
                               payload: pl}}, tenant_id) do
  case CaseWorkflows.open_case(%{...}) do
    {:ok, case_row} -> {:ok, %{kind: :case, case_id: case_row.id, ...}}
    {:error, e}     -> Logger.warning("case open failed: #{inspect(e)}")
                       {:error, %{kind: :case, reason: e}}
  end
end
```

The returned `case_id` propagates back into the pipeline result so the
fraud_score Message and Activation Watcher row both carry it.

### 3.4 LiveViews (5 pages)

| Path | LiveView | Notes |
|---|---|---|
| `/admin/fraud/case-workflows`         | `CaseWorkflowsLive`        | Index + drawer for workflow row |
| `/admin/fraud/case-workflows/:id`     | `CaseWorkflowDetailLive`   | Tabs: Statuses / Actions / Forms / Filters / Macros / XPaths / Roles — each tab a stream-rendered table with inline drawer |
| `/admin/fraud/cases`                  | `CasesLive`                | Filterable case list (workflow + status + assigned-to + age) |
| `/admin/fraud/cases/:id`              | `CaseDetailLive`           | Header + status chips + payload XPaths + diary + form entries + files + action buttons |
| `/admin/fraud/cases/:id/diary`        | (modal in CaseDetailLive)  | inline |

All slide-overs follow the established `max-w-lg` (drawers) / `max-w-3xl`
(case detail) pattern.

### 3.5 Action execution

When an operator clicks a workflow action button on a case:
1. Validate RBAC against `risk_case_workflow_roles`.
2. Transition `status_id`.
3. Append `risk_case_event` (actor, from_status, to_status, comment).
4. Trigger attached macros (HTTP/AMQP calls — wraps existing
   `MwRouter.WebhookDispatcher.deliver/3`).
5. Broadcast `Phoenix.PubSub` `"case:#{id}"` so any open `CaseDetailLive`
   refreshes.

### 3.6 Sidebar

New "Cases" section above "Fraud":
- `Cases` → `/admin/fraud/cases`
- `Case Workflows` → `/admin/fraud/case-workflows`

### 3.7 Tests

- Context tests for each of the 8 child resources.
- `open_case_test.exs` — full SideEffect→RiskCase round trip.
- LiveView smoke tests: workflow CRUD, status transition with RBAC denial path, macro execution.

### 3.8 Seeds

One demo workflow `Card Fraud Triage`:
- Statuses: New (blue) · In Review (yellow) · Escalated (orange) · Resolved (green) · False Positive (grey)
- Actions: Assign · Escalate · Resolve · Mark False Positive
- 1 form (Decision Rationale, free text + outcome enum)
- 1 macro (POST notification to slack webhook URL placeholder)
- 1 role grant: `fraud_analyst` → can transition all statuses.

Wire one seeded Activation rule's `case_workflow_key` to this workflow's key.

---

## 4. Acceptance criteria

- [ ] CRUD on workflows, statuses, actions, forms, filters, macros, xpaths, roles.
- [ ] Firing an Activation rule with `enable_case_workflow = true` inserts a `risk_case` row and an initial `risk_case_event`.
- [ ] CasesLive list streams updates when a new case is opened.
- [ ] Status transitions enforce RBAC and append events.
- [ ] All 30+ tests pass.

---

## 5. Out of scope

- Visualisation widgets embedded in case viewer (depends on Visualisation Registry UI — separate effort).
- Case bulk assignment.
- Case SLAs / escalation timers (could be a follow-up via Oban).
- Case search across structured form entries (depends on `risk_case_search_sessions` — schema exists but is currently broken on test DB; address separately).
