# Data Model: Findings Notifications & Escalation v1

## Overview

This feature introduces no new persisted business entity. Existing finding truth, alert rules, alert deliveries, database notifications, and tenant-membership or capability truth remain canonical. The new work is a bounded derived-event layer over those existing records.

## Existing Persistent Entities

### Finding

**Purpose**: Canonical tenant-scoped finding truth for ownership, lifecycle, severity, and due-date evaluation.

**Key fields used by this feature**:

- `id`
- `workspace_id`
- `tenant_id`
- `severity`
- `status`
- `due_at`
- `sla_days`
- `owner_user_id`
- `assignee_user_id`
- `reopened_at`
- `resolved_at`
- `closed_at`
- `finding_type`
- `subject_type`
- `subject_external_id`

**Relationships**:

- belongs to one tenant
- belongs to one workspace through tenant ownership
- may reference one current owner user
- may reference one current assignee user

**Rules relevant to notifications**:

- Only open findings participate in assignment, due-soon, and overdue notification evaluation.
- Terminal findings suppress due-soon and overdue delivery even if they previously entered a reminder window.
- The current due cycle is keyed by `due_at`; `reopened_at` remains explanatory lifecycle context and only matters when the existing lifecycle recalculates `due_at`. No extra reminder-state field is added.
- Existing aggregate `sla_due` alerts remain separate and are not replaced by finding-level delivery.

### AlertRule

**Purpose**: Workspace-scoped configuration for optional external delivery copies.

**Key fields used by this feature**:

- `workspace_id`
- `event_type`
- `min_severity`
- `destination_ids`
- `cooldown_minutes`
- `quiet_hours`
- `enabled`

**Rules relevant to notifications**:

- The feature adds four new `event_type` values only.
- A direct personal notification does not depend on an alert rule.
- External copies still require an enabled matching rule and destination.

### AlertDelivery

**Purpose**: Existing persisted artifact for external-copy dispatch outcomes.

**Key fields used by this feature**:

- `workspace_id`
- `tenant_id`
- `event_type`
- `status`
- `destination_snapshot`
- `payload`
- `fingerprint`
- `suppressed_reason`

**Rules relevant to notifications**:

- Finding-level external copies reuse the same delivery pipeline, cooldown, suppression, and quiet-hours semantics as other alerts.
- Delivery-history viewing remains read-only and only gains the new event labels and safe summaries.

### Database Notification (`notifications` table)

**Purpose**: Existing persisted artifact for direct in-app notification delivery.

**Key fields used by this feature**:

- `id`
- `type`
- `notifiable_type`
- `notifiable_id`
- `data`
- `read_at`
- `created_at`

**Rules relevant to notifications**:

- The feature stores direct-delivery metadata and the deterministic `fingerprint_key` inside `data`; no new table is introduced.
- The persisted payload remains Filament-compatible so the existing notification drawer can render it unchanged.

### Tenant Membership and User Entitlement Context

**Purpose**: Current authorization truth for whether a resolved direct recipient may still inspect the tenant and finding at send time.

**Key inputs used by this feature**:

- `tenant_memberships.tenant_id`
- `tenant_memberships.user_id`
- `User::canAccessTenant($tenant)`
- `CapabilityResolver::can($user, $tenant, Capabilities::TENANT_FINDINGS_VIEW)` or the existing findings-view equivalent used by the implementation seam

**Rules relevant to notifications**:

- Direct delivery is suppressed when the resolved recipient is no longer entitled.
- Open-time route authorization remains authoritative even after send-time validation.

## Derived Models

### FindingNotificationEvent

**Purpose**: Canonical derived event envelope used by both direct personal delivery and optional external alert copies.

**Fields**:

- `event_type`: one of `findings.assigned`, `findings.reopened`, `findings.due_soon`, `findings.overdue`
- `workspace_id`
- `tenant_id`
- `finding_id`
- `severity`
- `title`
- `body`
- `recipient_reason`: one of `new_assignee`, `current_assignee`, `current_owner`
- `resolved_recipient_user_id`: nullable
- `fingerprint_key`
- `due_cycle_key`: nullable, derived from current `due_at`
- `metadata`: object with finding summary, owner and assignee ids, due date, reopen timestamp, and deep-link-safe context

**Validation rules**:

- Event type must be one of the four new finding events.
- `recipient_reason` must match the event-specific precedence rule.
- `fingerprint_key` must deterministically distinguish the specific assignment change, reopen occurrence, or due cycle.
- `due_cycle_key` is required for `findings.due_soon` and `findings.overdue`, and omitted or null for assignment and reopen.

### RecipientResolutionResult

**Purpose**: Bounded contract that picks at most one direct recipient from existing owner and assignee truth without creating a second ownership model.

**Fields**:

- `user_id`: nullable
- `reason`: one of `new_assignee`, `current_assignee`, `current_owner`
- `is_entitled`: boolean
- `suppression_reason`: nullable string

**Rules**:

- `findings.assigned` resolves to the new assignee only.
- `findings.reopened` resolves to current assignee, else current owner.
- `findings.due_soon` resolves to current assignee, else current owner.
- `findings.overdue` resolves to current owner, else current assignee.
- A recipient who is not currently entitled becomes a suppression result, not a broadened-delivery fallback.

### DirectFindingNotificationMessage

**Purpose**: Filament database-notification payload rendered in the existing notification drawer.

**Fields**:

- `format = filament`
- `title`
- `body`
- `actions[0].label = Open finding`
- `actions[0].url = /admin/t/{tenant}/findings/{finding}`
- `finding_event.event_type`
- `finding_event.recipient_reason`
- `finding_event.fingerprint_key`
- `finding_event.tenant_name`
- `finding_event.severity`

**Rules**:

- One notification row represents one direct delivery to one entitled user.
- The payload must explain why the operator received the notification.
- The payload must not include hidden-tenant data beyond what the recipient is entitled to inspect.

## Event Matrix

| Event type | Trigger | Recipient precedence | Fingerprint components | Suppression rules |
|------------|---------|----------------------|------------------------|-------------------|
| `findings.assigned` | An open finding is assigned to a new assignee | new assignee | finding id + target assignee id + assignment change marker | suppress for owner-only changes, assignee clears, no-op saves, terminal findings, or non-entitled recipient |
| `findings.reopened` | A terminal finding is reopened by system detection | current assignee, else current owner | finding id + reopened occurrence marker | suppress for manual reopen, missing recipient, or non-entitled recipient |
| `findings.due_soon` | An open finding first enters the 24-hour pre-due window for the current due cycle | current assignee, else current owner | finding id + current `due_at` + event type | suppress for terminal findings, missing `due_at`, no entitled recipient, or duplicate within the same due cycle |
| `findings.overdue` | An open finding first becomes overdue for the current due cycle | current owner, else current assignee | finding id + current `due_at` + event type | suppress for terminal findings, no entitled recipient, or duplicate within the same due cycle |

## Persistence Boundaries

- No new table, enum-backed persistence, or reminder-state model is introduced.
- `notifications.data` stores direct-delivery fingerprint metadata only as a delivery artifact.
- `alert_deliveries` stores external-copy artifacts only as it already does today.
- `Finding` remains the sole business-truth model for ownership, lifecycle, and due-cycle resets.