## 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
7.8 KiB
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:
idworkspace_idtenant_idseveritystatusdue_atsla_daysowner_user_idassignee_user_idreopened_atresolved_atclosed_atfinding_typesubject_typesubject_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_atremains explanatory lifecycle context and only matters when the existing lifecycle recalculatesdue_at. No extra reminder-state field is added. - Existing aggregate
sla_duealerts 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_idevent_typemin_severitydestination_idscooldown_minutesquiet_hoursenabled
Rules relevant to notifications:
- The feature adds four new
event_typevalues 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_idtenant_idevent_typestatusdestination_snapshotpayloadfingerprintsuppressed_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:
idtypenotifiable_typenotifiable_iddataread_atcreated_at
Rules relevant to notifications:
- The feature stores direct-delivery metadata and the deterministic
fingerprint_keyinsidedata; 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_idtenant_memberships.user_idUser::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 offindings.assigned,findings.reopened,findings.due_soon,findings.overdueworkspace_idtenant_idfinding_idseveritytitlebodyrecipient_reason: one ofnew_assignee,current_assignee,current_ownerresolved_recipient_user_id: nullablefingerprint_keydue_cycle_key: nullable, derived from currentdue_atmetadata: 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_reasonmust match the event-specific precedence rule.fingerprint_keymust deterministically distinguish the specific assignment change, reopen occurrence, or due cycle.due_cycle_keyis required forfindings.due_soonandfindings.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: nullablereason: one ofnew_assignee,current_assignee,current_owneris_entitled: booleansuppression_reason: nullable string
Rules:
findings.assignedresolves to the new assignee only.findings.reopenedresolves to current assignee, else current owner.findings.due_soonresolves to current assignee, else current owner.findings.overdueresolves 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 = filamenttitlebodyactions[0].label = Open findingactions[0].url = /admin/t/{tenant}/findings/{finding}finding_event.event_typefinding_event.recipient_reasonfinding_event.fingerprint_keyfinding_event.tenant_namefinding_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.datastores direct-delivery fingerprint metadata only as a delivery artifact.alert_deliveriesstores external-copy artifacts only as it already does today.Findingremains the sole business-truth model for ownership, lifecycle, and due-cycle resets.