TenantAtlas/specs/224-findings-notifications-escalation/plan.md
2026-04-22 02:51:50 +02:00

253 lines
24 KiB
Markdown

# Implementation Plan: Findings Notifications & Escalation v1
**Branch**: `224-findings-notifications-escalation` | **Date**: 2026-04-22 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/224-findings-notifications-escalation/spec.md`
**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/224-findings-notifications-escalation/spec.md`
**Note**: This plan keeps the work inside the existing findings workflow, workspace alerting, and Filament database-notification primitives. The intended implementation adds four finding event types, one narrow finding-notification service, one database notification class, and focused extensions to the existing alert evaluation and alert-management surfaces. It does not add a new table, a notification center, a preference system, a second findings queue, or a generic workflow engine.
## Summary
Extend the existing workspace Alerts event vocabulary with `findings.assigned`, `findings.reopened`, `findings.due_soon`, and `findings.overdue`, then add a narrow `FindingNotificationService` that sends one entitlement-safe direct database notification to the currently responsible operator and forwards the same event into `AlertDispatchService` for optional external copies. Emit assignment and system-reopen events from `FindingWorkflowService` after committed mutations, evaluate due-soon and overdue windows inside the existing `EvaluateAlertsJob` cadence, reuse the existing `notifications` table and Filament database-notification drawer for direct delivery, and keep finding follow-up on the existing tenant finding detail route.
## Technical Context
**Language/Version**: PHP 8.4.15, Laravel 12, Filament v5, Livewire v4, Blade
**Primary Dependencies**: Laravel notifications (`database` channel), Filament database notifications, `Finding`, `FindingWorkflowService`, `FindingSlaPolicy`, `AlertRule`, `AlertDelivery`, `AlertDispatchService`, `EvaluateAlertsJob`, `CapabilityResolver`, `WorkspaceContext`, `TenantMembership`, `FindingResource`
**Storage**: PostgreSQL via existing `findings`, `alert_rules`, `alert_deliveries`, `notifications`, `tenant_memberships`, and `audit_logs`; no schema changes planned
**Testing**: Pest v4 feature tests with Filament/Livewire assertions and notification-payload checks
**Validation Lanes**: fast-feedback, confidence
**Target Platform**: Dockerized Laravel web application via Sail locally and Linux containers in deployment
**Project Type**: Laravel monolith inside the `wt-plattform` monorepo
**Performance Goals**: Keep direct notification dispatch and external alert-copy generation DB-backed and queue-safe, avoid N+1 tenant or recipient lookups, and keep due-window scans bounded to workspace-scoped open findings with due-date indexes already present
**Constraints**: No new persisted notification-truth table, no notification-preference center, no new capability family, no hidden-tenant leakage, no manual-reopen notification in v1, no repeated overdue spam within the same due cycle, and no new frontend assets
**Scale/Scope**: Four new event types, one narrow finding-notification service, one new database notification class, extensions to two existing alert resources, one extension to the existing alerts evaluation job, and four focused feature suites
## UI / Surface Guardrail Plan
- **Guardrail scope**: changed surfaces
- **Native vs custom classification summary**: native Filament resources and existing database-notification primitives only
- **Shared-family relevance**: workspace alert configuration and delivery-history family, existing admin and tenant database-notification family, existing tenant finding detail family
- **State layers in scope**: shell, detail
- **Handling modes by drift class or surface**: review-mandatory
- **Repository-signal treatment**: review-mandatory
- **Special surface test profiles**: standard-native-filament, global-context-shell
- **Required tests or manual smoke**: functional-core, state-contract
- **Exception path and spread control**: none; the feature extends existing alert resources and notification primitives rather than creating a new page family
- **Active feature PR close-out entry**: Guardrail
## Constitution Check
*GATE: Passed before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Pre-Research | Post-Design | Notes |
|-----------|--------------|-------------|-------|
| Inventory-first / snapshots-second | PASS | PASS | All event production stays derived from existing `Finding` lifecycle, ownership, severity, and due-date truth; notifications and alert deliveries remain delivery artifacts only |
| Read/write separation | PASS | PASS | The feature adds no new operator mutation surface; assignment and reopen writes stay inside existing workflow actions, while due reminders remain scheduled evaluation side effects |
| Graph contract path | PASS | PASS | No Microsoft Graph call paths or contract-registry changes are introduced |
| Deterministic capabilities / RBAC-UX | PASS | PASS | Workspace-scoped alert configuration remains capability-gated, direct recipients are re-validated against current tenant membership plus findings-view capability at send time, non-members remain `404`, and in-scope capability failures remain `403` |
| Workspace / tenant isolation | PASS | PASS | Alert rules and alert deliveries stay workspace-scoped, while direct notifications always deep-link to tenant-scoped finding detail and must not expose hidden tenant data |
| Run observability / Ops-UX | PASS | PASS | Scheduled due-event evaluation stays inside the existing `alerts.evaluate` cadence; no `OperationRun` notification semantics are changed and no queued/running DB notifications are introduced |
| Proportionality / no premature abstraction | PASS | PASS | The plan allows one narrow `FindingNotificationService` because direct-recipient resolution, dedupe, payload composition, and external alert-copy forwarding would otherwise be duplicated across workflow mutations and scheduled due evaluation |
| Persisted truth / few layers | PASS | PASS | No new table or persisted workflow state is added; existing `notifications` JSONB and `alert_deliveries` rows remain the only delivery artifacts |
| Behavioral state discipline | PASS | PASS | The four new values are delivery event types, not a new finding lifecycle or responsibility taxonomy |
| Filament-native UI (UI-FIL-001) | PASS | PASS | Alert rules and alert deliveries remain native Filament resources; direct notifications use existing Filament database-notification payloads |
| Decision-first / action-surface contract | PASS | PASS | Notifications stay as secondary drill-in entry points, alert rules remain config-first, and alert deliveries remain read-only diagnostics |
| Test governance (TEST-GOV-001) | PASS | PASS | Proof stays in focused feature suites for event production, routing, alert-rule integration, and deep-link safety, with no browser or heavy-governance expansion |
| Filament v5 / Livewire v4 compliance | PASS | PASS | The feature uses existing Filament v5 resources and Livewire v4-compatible database notifications only |
| Provider registration / global search / assets | PASS | PASS | Panel providers already live in `apps/platform/bootstrap/providers.php`; no globally searchable resource is added or changed; no new assets are required, so the existing deploy `filament:assets` step remains unchanged |
## Test Governance Check
- **Test purpose / classification by changed surface**: `Feature` for finding-event production, direct-recipient routing, alert-rule UI integration, and deep-link authorization behavior
- **Affected validation lanes**: `fast-feedback`, `confidence`
- **Why this lane mix is the narrowest sufficient proof**: The main risk is integrated workflow behavior: which event fires, who gets it, whether direct delivery leaks scope, whether external copies remain optional, and whether alert-management surfaces expose the new event types coherently. Focused feature tests prove that without adding unit-only abstractions or browser cost.
- **Narrowest proving command(s)**:
- `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`
- **Fixture / helper / factory / seed / context cost risks**: Moderate. Tests need workspace and tenant context, current and removed memberships, owner-versus-assignee combinations, existing alert rules and destinations, notification-table assertions, and time-travel across due windows.
- **Expensive defaults or shared helper growth introduced?**: no; any notification-specific helper should stay local to the new tests and reuse existing `createUserWithTenant(...)`, `Finding::factory()`, and alert destination factories
- **Heavy-family additions, promotions, or visibility changes**: none
- **Surface-class relief / special coverage rule**: `standard-native-filament` for alert resources and `global-context-shell` for database notifications that bridge admin shell context to tenant finding detail
- **Closing validation and reviewer handoff**: Reviewers should rely on the exact commands above and verify that owner-only changes do not emit assignment notifications, manual reopen still emits nothing, due-soon and overdue stay one-per-due-cycle, same-user owner/assignee resolution creates one notification, direct delivery suppresses when entitlement is lost, and external alert copies only appear when a matching alert rule exists.
- **Budget / baseline / trend follow-up**: none
- **Review-stop questions**: Did the implementation introduce new persistence, a preference layer, or a generic workflow-notification engine? Did any path leak hidden tenant information in the notification title, body, or action URL? Did due evaluation widen beyond the existing alert-evaluation cadence without need? Did alert-rule resource changes stay native and read clearly?
- **Escalation path**: document-in-feature unless a second delivery abstraction, a preference center, or a new findings-specific notification surface is proposed, in which case split or follow up with a dedicated spec
- **Active feature PR close-out entry**: Guardrail
- **Why no dedicated follow-up spec is needed**: This feature remains bounded to four concrete event types and one direct-delivery contract built on existing infrastructure
## Project Structure
### Documentation (this feature)
```text
specs/224-findings-notifications-escalation/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│ └── findings-notifications-escalation.logical.openapi.yaml
├── checklists/
│ └── requirements.md
└── tasks.md
```
### Source Code (repository root)
```text
apps/platform/
├── app/
│ ├── Filament/
│ │ └── Resources/
│ │ ├── AlertDeliveryResource.php
│ │ └── AlertRuleResource.php
│ ├── Jobs/
│ │ └── Alerts/
│ │ └── EvaluateAlertsJob.php
│ ├── Models/
│ │ └── AlertRule.php
│ ├── Notifications/
│ │ └── Findings/
│ │ └── FindingEventNotification.php
│ └── Services/
│ ├── Alerts/
│ │ └── AlertDispatchService.php
│ └── Findings/
│ ├── FindingNotificationService.php
│ ├── FindingSlaPolicy.php
│ └── FindingWorkflowService.php
├── database/
│ └── factories/
│ └── FindingFactory.php
└── tests/
└── Feature/
├── Alerts/
│ └── FindingsAlertRuleIntegrationTest.php
├── Findings/
│ ├── FindingsNotificationEventTest.php
│ └── FindingsNotificationRoutingTest.php
└── Notifications/
└── FindingNotificationLinkTest.php
```
**Structure Decision**: Standard Laravel monolith. The feature stays inside existing finding workflow, alerting, and notification seams. No new base directory, panel, or persisted model is required.
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| none | — | — |
## Proportionality Review
- **Current operator problem**: Finding assignment, automatic reopen, and due-state changes remain silent unless operators keep polling findings pages.
- **Existing structure is insufficient because**: `FindingWorkflowService` knows about ownership changes but not delivery, `EvaluateAlertsJob` knows about workspace alert events but not direct responsible-user delivery, and the existing alert-rule UI cannot express these finding-specific workflow events yet.
- **Narrowest correct implementation**: Add four event types, extend the existing alert-rule and delivery viewer labels, create one narrow `FindingNotificationService` to unify recipient resolution plus direct and external delivery, and emit events only from existing workflow and alert-evaluation seams.
- **Ownership cost created**: One service, one notification class, incremental logic in `FindingWorkflowService` and `EvaluateAlertsJob`, and four focused feature suites.
- **Alternative intentionally rejected**: A new `FindingNotificationDelivery` table or generic workflow-notification engine. Both add persistence or framework complexity that current-release truth does not require because existing `notifications` and `alert_deliveries` already capture delivery artifacts.
- **Release truth**: Current-release truth. The feature closes an existing workflow loop now rather than preparing a later escalation framework.
## Phase 0 Research
Research outcomes are captured in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/224-findings-notifications-escalation/research.md`.
Key decisions:
- Add the four new finding event types to the existing `AlertRule` constant registry and `AlertRuleResource::eventTypeOptions()` instead of creating a second event catalog.
- Reuse existing Filament database notifications on the admin panel rather than building a findings-specific notification surface or page.
- Emit `findings.assigned` and `findings.reopened` from `FindingWorkflowService` after the write transaction commits so delivery does not race an uncommitted finding state.
- Evaluate `findings.due_soon` and `findings.overdue` inside the existing `EvaluateAlertsJob` workspace cadence rather than adding a second scheduler, command, or `OperationRun` family.
- Use the existing `notifications` table `data` payload to store a finding-event fingerprint for direct-delivery dedupe; do not add a new persistence model.
- Reuse `FindingResource::getUrl(..., panel: 'tenant', tenant: $tenant)` for notification deep links and re-check entitlement before sending any direct notification.
## Phase 1 Design
Design artifacts are created under `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/224-findings-notifications-escalation/`:
- `research.md`: event-registry, delivery, dedupe, and evaluation-seam decisions
- `data-model.md`: existing entities plus the derived finding-event and direct-notification payload models
- `contracts/findings-notifications-escalation.logical.openapi.yaml`: internal logical contract for finding-event dispatch, alert-rule event exposure, alert-delivery viewing, and finding deep-link payloads
- `quickstart.md`: focused validation workflow for implementation and review
Design decisions:
- No schema migration is required; direct notifications use the existing `notifications` table and external copies use existing `alert_deliveries` rows.
- The canonical new seam is one narrow `FindingNotificationService`, not a reusable workflow-notification framework.
- Direct-recipient dedupe uses a fingerprint stored in the existing database notification payload; due-cycle reset keys are derived from the current `due_at` value.
- Alert rules remain optional for external copies; direct responsible-user notifications do not depend on any matching alert rule.
- Existing tenant finding detail remains the only follow-up surface, and current `404` versus `403` route behavior remains authoritative at open time.
## Phase 1 Agent Context Update
Run:
- `.specify/scripts/bash/update-agent-context.sh copilot`
## Constitution Check — Post-Design Re-evaluation
- PASS — the design remains inside current findings, alerts, and database-notification seams with no new persistence, no Graph work, no new capability family, and no new frontend assets.
- PASS — Livewire v4.0+ and Filament v5 constraints remain satisfied, panel provider registration stays in `apps/platform/bootstrap/providers.php`, no globally searchable resource behavior changes, and no new destructive action path is introduced.
## Implementation Strategy
### Phase A — Extend the existing alert-event vocabulary and operator labels
**Goal**: Teach existing alert-management surfaces about the four new finding workflow events.
| Step | File | Change |
|------|------|--------|
| A.1 | `apps/platform/app/Models/AlertRule.php` | Add the four new `EVENT_FINDINGS_*` constants alongside the existing alert event types |
| A.2 | `apps/platform/app/Filament/Resources/AlertRuleResource.php` | Extend `eventTypeOptions()` and `eventTypeLabel()` with operator-facing labels for assignment, reopened, due soon, and overdue events |
| A.3 | `apps/platform/app/Filament/Resources/AlertDeliveryResource.php` | Ensure list, view, and filter surfaces continue to render the new event labels cleanly through the existing event-type label seam without adding a new delivery viewer |
### Phase B — Add one narrow finding-notification delivery seam on existing primitives
**Goal**: Send one entitlement-safe direct database notification and one optional external alert copy from the same event envelope.
| Step | File | Change |
|------|------|--------|
| B.1 | `apps/platform/app/Services/Findings/FindingNotificationService.php` | Add a focused service that builds finding-event payloads, resolves the direct recipient by the spec precedence rules, checks current tenant entitlement plus findings-view capability, computes a delivery fingerprint, suppresses duplicates by querying existing database notifications, and forwards the same event array to `AlertDispatchService` for external copies |
| B.2 | `apps/platform/app/Notifications/Findings/FindingEventNotification.php` | Add a database notification class that returns Filament notification payloads with tenant-safe title/body copy, recipient-reason copy, one finding-detail action URL, and embedded metadata including the event type and fingerprint |
| B.3 | `apps/platform/app/Services/Alerts/AlertDispatchService.php` | Keep the existing workspace alert-copy path intact and only absorb any payload-shape normalization needed for the new finding event titles, body copy, and metadata |
### Phase C — Emit assignment and automatic-reopen notifications from existing finding workflow mutations
**Goal**: Turn current write seams into finding-event producers without changing workflow truth.
| Step | File | Change |
|------|------|--------|
| C.1 | `apps/platform/app/Services/Findings/FindingWorkflowService.php` | After committed `assign(...)` mutations, compare before and after owner and assignee state, suppress no-op, owner-only, and assignee-clear transitions, and dispatch `findings.assigned` only when a new assignee is set on an open finding |
| C.2 | `apps/platform/app/Services/Findings/FindingWorkflowService.php` | After committed `reopenBySystem(...)` mutations, dispatch `findings.reopened` using the refreshed finding state; keep manual `reopen(...)` out of scope for v1 delivery |
| C.3 | `apps/platform/app/Services/Findings/FindingWorkflowService.php` and `apps/platform/database/factories/FindingFactory.php` | Preserve existing due-date reset and reopen semantics so notification due-cycle logic stays derived from the recalculated `due_at` value, with reopen metadata remaining explanatory rather than becoming a second cycle key |
### Phase D — Evaluate due-soon and overdue events inside the existing alerts evaluation cadence
**Goal**: Keep scheduled due reminders and escalations inside the already-established workspace alert window.
| Step | File | Change |
|------|------|--------|
| D.1 | `apps/platform/app/Jobs/Alerts/EvaluateAlertsJob.php` | Add finding-level due-soon and overdue candidate queries over workspace-scoped open findings with `due_at`, using the existing window semantics and a fixed v1 due-soon horizon of 24 hours |
| D.2 | `apps/platform/app/Jobs/Alerts/EvaluateAlertsJob.php` | For each candidate finding event, call `FindingNotificationService` so direct delivery and optional external alert copies stay consistent and no second dispatch pipeline appears |
| D.3 | `apps/platform/app/Services/Findings/FindingNotificationService.php` | Define due-cycle fingerprints from the current `due_at` value so due-soon and overdue notifications emit once per cycle and reset only when `due_at` is recalculated by existing lifecycle semantics |
### Phase E — Preserve tenant-safe deep links and existing notification shell behavior
**Goal**: Reuse the current notification drawer and finding detail route without widening visibility.
| Step | File | Change |
|------|------|--------|
| E.1 | `apps/platform/app/Notifications/Findings/FindingEventNotification.php` | Build finding action URLs with `FindingResource::getUrl('view', ['record' => $finding], panel: 'tenant', tenant: $tenant)` so links target the tenant panel explicitly |
| E.2 | Existing admin panel notification configuration | Rely on the already-configured `->databaseNotifications()` behavior in `AdminPanelProvider`; do not add polling, a new page, or a new notification surface |
| E.3 | Existing finding detail route behavior | Keep current route authorization authoritative so an operator who lost access after send time still receives the existing `404` or `403` outcome instead of leaked detail |
### Phase F — Protect event truth, routing, and link safety with focused regression coverage
**Goal**: Lock down event production, recipient precedence, alert-rule integration, and tenant-safe finding drilldown.
| Step | File | Change |
|------|------|--------|
| F.1 | `apps/platform/tests/Feature/Findings/FindingsNotificationEventTest.php` | Cover assignment-event production, system-reopen event production, rapid reassignment and repeated automatic-reopen dedupe, due-soon and overdue evaluation windows, terminal-finding suppression, and one-per-due-cycle behavior |
| F.2 | `apps/platform/tests/Feature/Findings/FindingsNotificationRoutingTest.php` | Cover recipient precedence, same-user owner and assignee dedupe, owner-only assignment suppression, entitlement-loss suppression, and no direct delivery without a current eligible recipient |
| F.3 | `apps/platform/tests/Feature/Alerts/FindingsAlertRuleIntegrationTest.php` and `apps/platform/tests/Feature/Alerts/SlaDueAlertTest.php` | Cover alert-rule event-type option exposure, `ALERTS_VIEW` read versus `ALERTS_MANAGE` mutation boundaries, inherited Alerts v1 external-copy behavior including minimum severity, tenant scoping, cooldown, quiet hours, and dedupe, delivery-history labels and filters, the rule-free case where direct personal delivery still occurs, and non-regression of existing aggregate `sla_due` behavior |
| F.4 | `apps/platform/tests/Feature/Notifications/FindingNotificationLinkTest.php` | Cover database notification payload shape, explicit tenant-panel URLs, and finding-detail open behavior with correct tenant-safe `404` and `403` semantics |
| F.5 | `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` plus the focused Pest commands above | Run formatting and the narrowest proving suites before closing implementation |