## 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
204 lines
7.8 KiB
Markdown
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. |