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