# API Contracts: 111 — Findings Workflow V2 + SLA

**Date**: 2026-02-24  
**Branch**: `111-findings-workflow-sla`

---

## Overview

This feature does not introduce new external REST endpoints. User interaction is through Filament/Livewire actions on the Findings Resource and Alert Rules configuration. The only new cross-module “contract” is the `sla_due` alert event produced during scheduled alert evaluation and consumed by the Alerts dispatch pipeline.

---

## 1. Alert Event Contract: `sla_due`

**Producer:** `App\Jobs\Alerts\EvaluateAlertsJob`  
**Event Type:** `AlertRule::EVENT_SLA_DUE` (`sla_due`)  
**Event Cardinality:** At most 1 event per tenant per evaluation window (when newly-overdue findings exist)

### Eligibility (Per Tenant)

An event is produced when a tenant has one or more **newly-overdue** open findings since the previous evaluation window:
- `status IN (new, triaged, in_progress, reopened)`
- `due_at <= now()`
- `due_at > windowStart`

Terminal statuses (`resolved`, `closed`, `risk_accepted`) never contribute to overdue evaluation.

### Event Payload Shape

```json
{
  "event_type": "sla_due",
  "tenant_id": 123,
  "severity": "high",
  "fingerprint_key": "sla_due:tenant:123",
  "title": "SLA overdue findings detected",
  "body": "Tenant Contoso has 5 overdue open findings (critical: 1, high: 2, medium: 2, low: 0).",
  "metadata": {
    "overdue_total": 5,
    "overdue_by_severity": {
      "critical": 1,
      "high": 2,
      "medium": 2,
      "low": 0
    }
  }
}
```

### Severity Semantics

`severity` is the maximum severity among overdue open findings for that tenant at evaluation time.

Rationale: alert rules can use `minimum_severity`; a critical-overdue case can bypass a “high-only” rule.

---

## 2. Filament Findings Resource: Workflow Actions Contract

All workflow actions:
- enforce tenant membership as deny-as-not-found (404) for non-members
- enforce capability checks (403 for members lacking capability)
- write an audit log entry with before/after and any reason fields

### List Defaults

Default list shows:
- all finding types (no drift-only default)
- open statuses only: `new`, `triaged`, `in_progress`, `reopened`

Quick filters:
- Open
- Overdue (`due_at < now()` and open statuses)
- High severity (high + critical)
- My assigned (`assignee_user_id = current user`)

### Row Actions (More menu)

| Action | Allowed From Status | To Status | Capability | Confirmation | Notes |
|--------|---------------------|----------|------------|--------------|------|
| Triage | `new`, `reopened` | `triaged` | `TENANT_FINDINGS_TRIAGE` (or legacy `TENANT_FINDINGS_ACKNOWLEDGE` alias) | No | Sets `triaged_at` |
| Start progress | `triaged` | `in_progress` | `TENANT_FINDINGS_TRIAGE` (or legacy alias) | No | Sets `in_progress_at` |
| Assign | open statuses | unchanged | `TENANT_FINDINGS_ASSIGN` | No | Sets `assignee_user_id` and optional `owner_user_id` (picker limited to tenant members) |
| Resolve | open statuses | `resolved` | `TENANT_FINDINGS_RESOLVE` | Yes | Requires `resolved_reason`; sets `resolved_at` |
| Close | any status | `closed` | `TENANT_FINDINGS_CLOSE` | Yes | Requires `closed_reason`; sets `closed_at` + `closed_by_user_id` |
| Risk accept | any status | `risk_accepted` | `TENANT_FINDINGS_RISK_ACCEPT` | Yes | Requires reason (stored as `closed_reason`); sets `closed_at` + `closed_by_user_id` |
| Reopen | `resolved`, `closed`, `risk_accepted` | `reopened` | `TENANT_FINDINGS_TRIAGE` (or legacy alias) | Yes | Manual reopen only. Automatic reopen is allowed only from `resolved` during detection. |

### Bulk Actions

Bulk actions are all-or-nothing (if any record is unauthorized for the current tenant context, the action is disabled):

| Action | Capability | Confirmation | Notes |
|--------|------------|--------------|------|
| Bulk triage | `TENANT_FINDINGS_TRIAGE` (or legacy alias) | Yes (typed confirm for large selections) | Moves `new|reopened → triaged` |
| Bulk assign | `TENANT_FINDINGS_ASSIGN` | Yes (typed confirm for large selections) | Assignee/owner pickers limited to tenant members |
| Bulk resolve | `TENANT_FINDINGS_RESOLVE` | Yes | Reason required |
| Bulk close | `TENANT_FINDINGS_CLOSE` | Yes | Reason required |
| Bulk risk accept | `TENANT_FINDINGS_RISK_ACCEPT` | Yes | Reason required |

---

## 3. Backfill/Consolidation Operation Contract

Backfill is a tenant-context operation that upgrades legacy findings to v2 lifecycle fields and consolidates drift duplicates. It MUST be OperationRun-backed and use OPS-UX feedback surfaces (queued toast, progress surfaces, initiator-only completion notification).

**OperationRun type:** `findings.lifecycle.backfill` (label registered in OperationCatalog)  
**Idempotency:** One active run per tenant (deduped by OperationRun identity)

Summary counts use canonical numeric keys only (e.g., `total`, `processed`, `updated`, `failed`, `skipped`).

---

## 4. Audit Contract (Tenant Scope)

Every workflow mutation writes a tenant audit record (via `App\Services\Intune\AuditLogger`) with:
- `action` string (e.g., `finding.triaged`, `finding.resolved`, `finding.closed`, `finding.risk_accepted`, `finding.reopened`, `findings.lifecycle.backfill.started`)
- `metadata` including: `finding_id`, `before_status`, `after_status`, and any reason fields or assignment deltas

Audit payloads must remain sanitized and must not include secrets/tokens.

