TenantAtlas/specs/224-findings-notifications-escalation/data-model.md
ahmido e15d80cca5
Some checks failed
Main Confidence / confidence (push) Failing after 48s
Heavy Governance Lane / heavy-governance (push) Has been skipped
Browser Lane / browser (push) Has been skipped
feat: implement findings notifications escalation (#261)
## Summary
- implement Spec 224 findings notifications and escalation v1 on top of the existing alerts and Filament database notification infrastructure
- add finding assignment, reopen, due soon, and overdue event handling with direct recipient routing, dedupe, and optional external alert fan-out
- extend alert rule and alert delivery surfaces plus add the Spec 224 planning bundle and candidate-list promotion cleanup

## Validation
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings/FindingsNotificationEventTest.php tests/Feature/Findings/FindingsNotificationRoutingTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Alerts/FindingsAlertRuleIntegrationTest.php tests/Feature/Alerts/SlaDueAlertTest.php tests/Feature/Notifications/FindingNotificationLinkTest.php`

## Filament / Platform Notes
- Livewire v4.0+ compliance is preserved
- provider registration remains unchanged in `apps/platform/bootstrap/providers.php`
- no globally searchable resource behavior changed in this feature
- no new destructive action was introduced
- asset strategy is unchanged and the existing `cd apps/platform && php artisan filament:assets` deploy step remains sufficient

## Manual Smoke Note
- integrated-browser smoke testing confirmed the new alert rule event options, notification drawer entries, alert delivery history row, and tenant finding detail route on the active Sail host
- local notification deep links currently resolve from `APP_URL`, so a local `localhost` vs `127.0.0.1:8081` host mismatch can break the browser session if the app is opened on a different host/port combination

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #261
2026-04-22 00:54:38 +00:00

204 lines
7.8 KiB
Markdown

# 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.