TenantAtlas/specs/221-findings-operator-inbox/data-model.md
ahmido 81bb5f42c7
Some checks failed
Main Confidence / confidence (push) Failing after 55s
feat: add findings operator inbox (#258)
## Summary
- add the canonical admin-plane `My Findings` inbox at `/admin/findings/my-work`
- add the workspace overview `Assigned to me` signal and inbox-to-detail continuity
- add focused Pest coverage plus the full Spec 221 artifact bundle

## Validation
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings/MyWorkInboxTest.php tests/Feature/Authorization/MyWorkInboxAuthorizationTest.php tests/Feature/Dashboard/MyFindingsSignalTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/WorkspaceOverviewNavigationTest.php`
- integrated-browser smoke completed against the browser-facing `tenantatlas` runtime, including seeded positive-path and negative-path checks plus fixture cleanup

## Filament v5 Guardrails
- Livewire v4.0+ compliant
- panel provider registration remains in `apps/platform/bootstrap/providers.php`
- global search behavior is unchanged; `FindingResource` already has a View page and the new inbox is a custom page, not a searchable resource
- no destructive actions were introduced on the inbox or overview signal
- no new assets were added; the existing deploy step for `cd apps/platform && php artisan filament:assets` remains unchanged
- coverage includes the new inbox page, authorization boundaries, the workspace overview signal, and the overview CTA regression

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #258
2026-04-21 09:19:54 +00:00

169 lines
6.7 KiB
Markdown

# Data Model: Findings Operator Inbox V1
## Overview
This feature does not add or modify persisted entities. It introduces two derived read models:
- the canonical admin-plane `My Findings` inbox at `/admin/findings/my-work`
- one compact `Assigned to me` signal on `/admin`
Both remain projections over existing finding, tenant membership, and workspace context truth.
## Existing Persistent Inputs
### 1. Finding
- Purpose: Tenant-owned workflow record representing current governance or execution work.
- Key persisted fields used by this feature:
- `id`
- `workspace_id`
- `tenant_id`
- `status`
- `severity`
- `due_at`
- `subject_display_name`
- `owner_user_id`
- `assignee_user_id`
- `reopened_at`
- Relationships used by this feature:
- `tenant()`
- `ownerUser()`
- `assigneeUser()`
Relevant existing semantics:
- `Finding::openStatusesForQuery()` defines inbox inclusion for open work.
- `Finding::openStatuses()` and terminal statuses remain unchanged.
- Spec 219 defines assignee inclusion and owner-only exclusion.
### 2. Tenant
- Purpose: Tenant boundary for findings ownership and tenant-plane detail navigation.
- Key persisted fields used by this feature:
- `id`
- `workspace_id`
- `name`
- `external_id`
- `status`
### 3. TenantMembership
- Purpose: Per-tenant entitlement boundary for visibility.
- Key persisted fields used by this feature:
- `tenant_id`
- `user_id`
- `role`
The inbox and overview signal must only materialize findings from tenants where the current user still has membership and findings-view entitlement.
### 4. Workspace Context
- Purpose: Active workspace selection in the admin plane.
- Source: Existing workspace session context, not a new persisted model for this feature.
- Effect on this feature:
- gates entry into the admin inbox
- constrains visible tenants to the current workspace
- feeds the workspace overview signal
## Derived Presentation Entities
### 1. AssignedFindingInboxRow
Logical row model for `/admin/findings/my-work`.
| Field | Meaning | Source |
|---|---|---|
| `findingId` | Target finding identifier | `Finding.id` |
| `tenantId` | Tenant route scope for detail drilldown | `Finding.tenant_id` |
| `tenantLabel` | Tenant name visible in the queue | `Tenant.name` |
| `summary` | Operator-facing finding summary | `Finding.subject_display_name` plus existing fallback logic |
| `severity` | Severity badge value | `Finding.severity` |
| `status` | Workflow/lifecycle badge value | `Finding.status` |
| `dueAt` | Due date if present | `Finding.due_at` |
| `dueState` | Derived urgency label such as overdue or due soon | existing finding due-state logic |
| `reopened` | Whether reopened context should be emphasized | `Finding.status === reopened` or existing lifecycle cues |
| `ownerLabel` | Accountable owner when different from assignee | `ownerUser.name` |
| `detailUrl` | Tenant finding detail route | derived from tenant finding view route |
| `navigationContext` | Return-path payload back to the inbox | derived from `CanonicalNavigationContext` |
Validation rules:
- Row inclusion requires all of the following:
- finding belongs to the current workspace
- finding belongs to a tenant the current user may inspect
- finding status is in `Finding::openStatusesForQuery()`
- `assignee_user_id` equals the current user ID
- Owner-only findings are excluded.
- Hidden-tenant findings produce no row, no count, and no filter option.
### 2. MyFindingsInboxState
Logical state model for the inbox page.
| Field | Meaning |
|---|---|
| `workspaceId` | Current admin workspace scope |
| `assigneeUserId` | Fixed personal-work scope |
| `tenantFilter` | Optional active-tenant prefilter, defaulted from canonical admin tenant context |
| `overdueOnly` | Optional urgency narrowing |
| `reopenedOnly` | Optional reopened-work narrowing |
| `highSeverityOnly` | Optional severity narrowing |
Rules:
- `assigneeUserId` is fixed and cannot be cleared in v1.
- `tenantFilter` is clearable.
- `tenantFilter` values may only reference entitled tenants.
- Invalid or stale tenant filter state is discarded rather than widening visibility.
- Inbox summary counts reflect the currently visible queue after the fixed assignee scope and any active tenant, overdue, reopened, or high-severity filters are applied.
### 3. MyFindingsSignal
Logical summary model for the workspace overview.
| Field | Meaning | Source |
|---|---|---|
| `openAssignedCount` | Count of visible open assigned findings | derived `Finding` count |
| `overdueAssignedCount` | Count of visible overdue assigned findings | derived `Finding` count with `due_at < now()` |
| `isCalm` | Whether the signal should render calm wording | derived from the two counts |
| `headline` | Operator-facing signal summary | derived presentation copy |
| `description` | Supporting copy clarifying visible-scope truth | derived presentation copy |
| `ctaLabel` | Explicit drill-in label | fixed vocabulary `Open my findings` |
| `ctaUrl` | Canonical inbox route | derived from the new admin page URL |
Validation rules:
- Counts are workspace-scoped and tenant-entitlement scoped.
- The signal never implies hidden or inaccessible work.
- Calm wording is only allowed when both visible counts are zero.
## State And Ordering Rules
### Inbox inclusion order
1. Restrict to the current workspace.
2. Restrict to visible tenant IDs.
3. Restrict to `assignee_user_id = current user`.
4. Restrict to `Finding::openStatusesForQuery()`.
5. Apply optional tenant/overdue/reopened/high-severity filters.
6. Sort overdue work first, reopened non-overdue work next, then remaining work.
7. Within each urgency bucket, rows with due dates sort by `dueAt` ascending, rows without due dates sort last, and remaining ties sort by `findingId` descending.
### Urgency semantics
- Overdue work is the highest-priority urgency bucket.
- Reopened non-overdue work is the next urgency bucket.
- High-severity remains a filter and emphasis cue rather than a separate mandatory sort bucket.
- Terminal findings are never part of the inbox or signal.
### Empty-state semantics
- If no visible assigned open findings exist anywhere in scope, the inbox shows a calm empty state.
- If the active tenant prefilter causes the empty state while other visible tenants still have work, the empty state must explain the tenant boundary and provide a clear fallback CTA.
## Authorization-sensitive Output
- Tenant labels, filter values, rows, and counts are only derived from entitled tenants.
- The inbox itself is workspace-context dependent.
- Detail navigation remains tenant-scoped and must preserve existing `404`/`403` semantics on the destination.
- The derived signal and row model remain useful without ever revealing hidden tenant names or quantities.