TenantAtlas/specs/224-findings-notifications-escalation/contracts/findings-notifications-escalation.logical.openapi.yaml
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

349 lines
10 KiB
YAML

openapi: 3.1.0
info:
title: Findings Notifications & Escalation Surface Contract
version: 1.0.0
summary: Logical internal contract for Spec 224 delivery, alert-rule exposure, and finding deep links.
description: |
This contract documents the structured payloads and UI-facing surfaces that Spec 224 must satisfy.
It is intentionally logical rather than public-API only: the feature reuses existing Filament resources,
database notifications, and background jobs instead of introducing a new public controller namespace.
servers:
- url: https://logical.internal
description: Non-routable placeholder used to describe internal repository contracts.
paths:
/internal/findings/notification-events:
post:
summary: Dispatch one finding event to direct personal delivery and optional external alert copies.
description: |
Logical internal contract implemented by the bounded finding-notification delivery seam.
It normalizes one finding event, resolves at most one entitled direct recipient, writes one
Filament-compatible database notification when appropriate, and forwards the same event to the
existing workspace alert dispatch pipeline.
operationId: dispatchFindingNotificationEvent
x-not-public-http: true
requestBody:
required: true
content:
application/vnd.tenantpilot.finding-notification-event+json:
schema:
$ref: '#/components/schemas/FindingNotificationEventDispatch'
responses:
'202':
description: Event accepted and evaluated for direct and optional external delivery.
content:
application/vnd.tenantpilot.finding-notification-dispatch-result+json:
schema:
$ref: '#/components/schemas/FindingNotificationDispatchResult'
/admin/alert-rules:
get:
summary: Existing alert-rule surfaces expose the four new finding event types.
description: |
Existing Filament resource pages continue to render HTML, while this logical media type documents
the event-type options that must appear in create, edit, and filter flows.
operationId: viewFindingAlertRuleOptions
responses:
'200':
description: Alert-rule surfaces show the finding event options and reuse the existing delivery families.
content:
text/html:
schema:
type: string
application/vnd.tenantpilot.finding-alert-rule-options+json:
schema:
$ref: '#/components/schemas/FindingAlertRuleOptionsSurface'
'403':
description: Workspace operator lacks permission to view alert-rule configuration.
'404':
description: Workspace operator is not a member of the active workspace or the alert surface is outside their visible scope.
/admin/alert-deliveries:
get:
summary: Existing alert-delivery history can filter and label finding event copies.
operationId: viewFindingAlertDeliveries
parameters:
- name: event_type
in: query
required: false
schema:
$ref: '#/components/schemas/FindingEventType'
responses:
'200':
description: Alert-delivery viewer surfaces show finding-event labels and safe summaries.
content:
text/html:
schema:
type: string
application/vnd.tenantpilot.finding-alert-deliveries+json:
schema:
$ref: '#/components/schemas/FindingAlertDeliveriesSurface'
'403':
description: Workspace operator lacks permission to inspect alert deliveries.
'404':
description: Workspace operator is not a member of the active workspace or the alert-delivery surface is outside their visible scope.
/admin/t/{tenant}/findings/{finding}:
get:
summary: The direct notification action opens the existing tenant finding detail route.
operationId: openFindingFromNotification
parameters:
- name: tenant
in: path
required: true
schema:
type: integer
- name: finding
in: path
required: true
schema:
type: integer
responses:
'200':
description: Existing finding detail renders for an entitled tenant operator.
content:
text/html:
schema:
type: string
application/vnd.tenantpilot.finding-notification-context+json:
schema:
$ref: '#/components/schemas/FindingNotificationContext'
'403':
description: Recipient still has tenant visibility but lacks current capability to inspect the finding.
'404':
description: Recipient no longer has tenant or record visibility.
components:
schemas:
FindingEventType:
type: string
enum:
- findings.assigned
- findings.reopened
- findings.due_soon
- findings.overdue
FindingSeverity:
type: string
enum:
- low
- medium
- high
- critical
RecipientReason:
type: string
enum:
- new_assignee
- current_assignee
- current_owner
DirectRecipient:
type: object
required:
- userId
- reason
properties:
userId:
type: integer
displayName:
type: string
reason:
$ref: '#/components/schemas/RecipientReason'
FindingNotificationEventDispatch:
type: object
required:
- eventType
- workspaceId
- tenantId
- findingId
- severity
- title
- body
- fingerprintKey
- metadata
properties:
eventType:
$ref: '#/components/schemas/FindingEventType'
workspaceId:
type: integer
tenantId:
type: integer
findingId:
type: integer
severity:
$ref: '#/components/schemas/FindingSeverity'
title:
type: string
body:
type: string
directRecipient:
oneOf:
- $ref: '#/components/schemas/DirectRecipient'
- type: 'null'
fingerprintKey:
type: string
dueCycleKey:
type:
- string
- 'null'
metadata:
type: object
required:
- tenantName
- recipientReason
properties:
tenantName:
type: string
recipientReason:
$ref: '#/components/schemas/RecipientReason'
ownerUserId:
type:
- integer
- 'null'
assigneeUserId:
type:
- integer
- 'null'
dueAt:
type:
- string
- 'null'
format: date-time
reopenedAt:
type:
- string
- 'null'
format: date-time
summary:
type: string
allOf:
- if:
properties:
eventType:
enum:
- findings.due_soon
- findings.overdue
then:
required:
- dueCycleKey
properties:
dueCycleKey:
type: string
- if:
properties:
eventType:
enum:
- findings.assigned
- findings.reopened
then:
properties:
dueCycleKey:
type: 'null'
FindingNotificationDispatchResult:
type: object
required:
- eventType
- fingerprintKey
- directDeliveryStatus
- externalDeliveryCount
properties:
eventType:
$ref: '#/components/schemas/FindingEventType'
fingerprintKey:
type: string
directDeliveryStatus:
type: string
enum:
- sent
- suppressed
- deduped
- no_recipient
externalDeliveryCount:
type: integer
minimum: 0
externalDeliveryStatuses:
type: array
items:
type: string
enum:
- queued
- deferred
- suppressed
- none
EventTypeOption:
type: object
required:
- value
- label
properties:
value:
$ref: '#/components/schemas/FindingEventType'
label:
type: string
FindingAlertRuleOptionsSurface:
type: object
required:
- eventTypes
- existingDeliveryFamiliesReused
properties:
eventTypes:
type: array
items:
$ref: '#/components/schemas/EventTypeOption'
existingDeliveryFamiliesReused:
type: boolean
notes:
type: array
items:
type: string
FindingAlertDeliveryRow:
type: object
required:
- deliveryId
- eventType
- label
- status
properties:
deliveryId:
type: integer
eventType:
$ref: '#/components/schemas/FindingEventType'
label:
type: string
tenantId:
type:
- integer
- 'null'
status:
type: string
destinationName:
type: string
summary:
type: string
FindingAlertDeliveriesSurface:
type: object
required:
- filterEventTypes
- rows
properties:
filterEventTypes:
type: array
items:
$ref: '#/components/schemas/EventTypeOption'
rows:
type: array
items:
$ref: '#/components/schemas/FindingAlertDeliveryRow'
FindingNotificationContext:
type: object
required:
- findingId
- tenantId
- eventType
- recipientReason
properties:
findingId:
type: integer
tenantId:
type: integer
eventType:
$ref: '#/components/schemas/FindingEventType'
recipientReason:
$ref: '#/components/schemas/RecipientReason'
title:
type: string
body:
type: string