TenantAtlas/specs/256-external-support-desk-handoff/plan.md
ahmido 52ebf63af1
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 2m6s
feat(specs/256): external support desk handoff (#301)
Implement external support desk handoff (spec 256). Created and pushed branch `256-external-support-desk-handoff`.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #301
2026-04-29 20:16:40 +00:00

28 KiB

Implementation Plan: External Support Desk / PSA Handoff

Branch: 256-external-support-desk-handoff | Date: 2026-04-29 | Spec: /Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/256-external-support-desk-handoff/spec.md Input: Feature specification from /Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/256-external-support-desk-handoff/spec.md

Summary

  • Extend the existing support-request submission flow so the two current support-aware surfaces can keep a request internal-only, create one external desk ticket, or link one existing external ticket without adding a new support product surface.
  • Persist only the minimal neutral linkage truth on the existing support_requests row: external_handoff_mode, external_ticket_reference, external_ticket_url, and external_handoff_failure_summary.
  • Keep the flow synchronous and auditable inside the existing support-request path: create the internal SR-... request first, allow exactly one bounded synchronous finalization write for external create, link, or failure fields on the same row, enforce a five-second outbound timeout on the create path, and surface the latest linkage summary only in the current tenant or run support context.

Technical Context

Language/Version: PHP 8.4 on Laravel 12
Primary Dependencies: Filament v5, Livewire v4, Pest v4, existing SupportRequestSubmissionService, SupportRequestContextBuilder, SupportRequestReferenceGenerator, WorkspaceAuditLogger, UiEnforcement, and CapabilityResolver
Storage: PostgreSQL; extend the tenant-owned support_requests table, keep workspace_id and tenant_id required, and do not add a second support-ticket table
Testing: Pest unit + feature tests
Validation Lanes: fast-feedback, confidence
Target Platform: Sail-backed Laravel admin panel under /admin and /admin/t/{tenant}
Project Type: web
Performance Goals: keep the submit path synchronous, apply a maximum five-second outbound timeout on the create path, and avoid queue or OperationRun overhead
Constraints: one application-configured external target only, no new support-request resource/list/detail page, no global-search surface, no bidirectional sync, no retry scheduler, no raw provider payload persistence, no provider registration changes, and no runtime asset changes
Scale/Scope: one additive migration on support_requests, one concrete provider-owned handoff service, one small derived latest-summary helper or equivalent shared read path, two Filament action-form extensions, audit additions, and focused unit plus feature coverage only

Key Design Decisions

Persistence and source of truth

  • support_requests is the only persisted truth for this slice. No support_tickets table, no queue artifact, and no new support page model is justified.
  • The plan adds these columns to support_requests:
    • external_handoff_mode as a non-null string with default internal_only
    • external_ticket_reference as a nullable string
    • external_ticket_url as a nullable text field
    • external_handoff_failure_summary as a nullable text field
  • The plan does not add external_handoff_status, external_target_type, external_target_id, raw payload JSON, or a dedicated failure timestamp. Those are not needed for the current operator contract because:
    • the handoff mode already captures operator intent
    • success is derivable from external_ticket_reference
    • failure visibility only needs a bounded summary on revisit
    • audit timestamps already provide exact event timing
  • Spec 256 explicitly narrows Spec 246 immutability in one bounded way: after the internal request row exists, the same row may receive exactly one synchronous finalization write limited to the external handoff fields above. After that finalization step, the row is immutable again.
  • Existing support_requests indexes on (tenant_id, created_at) and (operation_run_id, created_at) are sufficient for latest-summary lookups. No external-reference index is planned because cross-scope lookup by external ticket reference is explicitly out of scope.

Failure truth and auditable outcomes

  • External create failure is not audit-only. A bounded failure summary must be persisted back on the same support_requests row so the current support context can show the last failure on revisit.
  • Timeout is treated as the same failure family as any other create failure. The provider-owned service must enforce the five-second outbound timeout budget and return a normalized bounded failure summary rather than raw transport details.
  • Detailed provider-specific error payloads remain out of persisted product truth. They stay confined to the provider-owned handoff service, log redaction rules, and audit metadata where appropriate.
  • The internal support request remains durable even when external create fails. The implementation must therefore split the flow into:
    1. authorize and validate the existing request
    2. persist the internal support request and support_request.created audit event
    3. perform link or create handoff work after the internal row exists
    4. perform the one allowed synchronous finalization write back to the same row and emit the corresponding audit event

Visible linkage stays inside existing support contexts only

  • External ticket references do not become a new dashboard card, run section, support history block, global search result, or Filament resource.
  • The narrowest correct visibility path is:
    • success or partial-success notification immediately after submit
    • a latest-handoff summary placeholder inside the existing Request support slide-over on TenantDashboard
    • the same latest-handoff summary placeholder inside the grouped Request support slide-over on TenantlessOperationRunViewer
  • Tenant context summary scopes to the latest support request where primary_context_type = tenant for the current entitled tenant.
  • Run context summary scopes to the latest support request where primary_context_type = operation_run and operation_run_id matches the currently opened run.

Minimal application config contract is in scope; support settings UI is not

  • The repo has no existing support settings domain, so leaving target resolution as an external prerequisite would create hidden implementation drift.
  • This plan therefore brings one minimal application config contract into scope: apps/platform/config/support_desk.php backed by environment values for the single supported target.
  • The implementation may resolve availability, create endpoint configuration, and timeout settings from that config file only.
  • This spec still forbids workspace settings UI, a new settings domain, per-workspace target management, provider-connection product work, or multi-target support.

Timeout and latency rule

  • The one application-configured create path must use a maximum five-second outbound timeout.
  • A timeout is normalized into the same bounded failure-summary and audit path as any other external create failure.
  • The timeout budget is part of the feature contract and must be covered by the handoff-service unit tests.

UI / Surface Guardrail Plan

  • Guardrail scope: changed surfaces
  • Native vs custom classification summary: native Filament actions plus shared support primitives
  • Shared-family relevance: header actions, grouped detail actions, support-request slide-overs, success or warning notifications, latest-handoff summaries, and external-link navigation
  • State layers in scope: page, detail, action form
  • Audience modes in scope: operator-MSP, support-platform
  • Decision/diagnostic/raw hierarchy plan: decision-first support form, diagnostics-second through the existing neighboring diagnostics action, provider/raw evidence third and hidden
  • Raw/support gating plan: provider-specific payloads, secrets, and raw responses stay provider-owned and hidden; only bounded human-readable linkage or failure summary becomes default-visible
  • One-primary-action / duplicate-truth control: the dominant action remains Submit support request; handoff choice is a form field, not a second primary action, and the visible summary names one internal support reference so the surface does not become a history register
  • Handling modes by drift class or surface: review-mandatory
  • Repository-signal treatment: review-mandatory
  • Special surface test profiles: standard-native-filament, monitoring-state-page
  • Required tests or manual smoke: functional-core, state-contract, manual smoke after implementation
  • Exception path and spread control: the tenant dashboard keeps its existing bounded action-surface exception; the run viewer keeps both support actions grouped under More and does not add a new top-level support action family
  • Active feature PR close-out entry: Guardrail / Exception / Smoke Coverage

Shared Pattern & System Fit

  • Cross-cutting feature marker: yes
  • Systems touched:
    • apps/platform/app/Filament/Pages/TenantDashboard.php
    • apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php
    • apps/platform/app/Models/SupportRequest.php
    • apps/platform/app/Support/SupportRequests/SupportRequestSubmissionService.php
    • apps/platform/app/Support/SupportRequests/SupportRequestContextBuilder.php
    • apps/platform/app/Support/SupportRequests/SupportRequestReferenceGenerator.php
    • apps/platform/app/Services/Audit/WorkspaceAuditLogger.php
    • apps/platform/app/Support/Audit/AuditActionId.php
    • apps/platform/database/factories/SupportRequestFactory.php
    • apps/platform/config/support_desk.php
    • apps/platform/lang/en/localization.php
    • apps/platform/lang/de/localization.php
  • Shared abstractions reused: existing support-request submission path, existing redacted context builder, existing internal reference generator, existing audit logger, and existing UiEnforcement capability gating
  • New abstraction introduced? why?: one concrete provider-owned external handoff service is justified because both existing surfaces must call or normalize one real external target without page-local HTTP logic; one tiny shared latest-summary read helper is allowed if needed to avoid duplicating the same context-scoped query and copy twice
  • Why the existing abstraction was sufficient or insufficient: the existing abstractions already solve context capture, internal request creation, and audit logging, but they stop at internal persistence and cannot yet persist external linkage or explicit handoff failure truth
  • Bounded deviation / spread control: no interface registry, no adapter catalog, no support-desk framework, no second persistence model, and no new support history vocabulary

OperationRun UX Impact

  • Touches OperationRun start/completion/link UX?: no
  • Central contract reused: N/A
  • Delegated UX behaviors: N/A
  • Surface-owned behavior kept local: the run viewer uses the current run only as support context and as the scoping key for its latest-handoff summary; it does not create, resume, or link an OperationRun
  • Queued DB-notification policy: N/A
  • Terminal notification path: N/A
  • Exception path: none

Provider Boundary & Portability Fit

  • Shared provider/platform boundary touched?: yes
  • Provider-owned seams: outbound create payload, authentication, target-specific reference normalization, URL normalization, and remote error parsing
  • Platform-core seams: SupportRequest, internal support reference, external ticket reference and URL, handoff mode, latest-handoff summary, and bounded failure summary
  • Neutral platform terms / contracts preserved: Request support, Support reference, External ticket, Create external ticket, Link existing ticket, and TenantPilot only versus TenantPilot + external support desk
  • Retained provider-specific semantics and why: provider-specific ticket identifiers, auth requirements, and request payload shape remain inside one concrete provider-owned service because the current release has exactly one real external target
  • Bounded extraction or follow-up path: multi-provider support, target-management UI, and broader ITSM modeling remain follow-up-spec work only

Constitution Check

GATE: Passed against repo truth before artifact write. Re-checked after Phase 1 design artifacts were drafted.

  • Inventory-first / snapshots-second: PASS. The slice does not alter inventory or snapshot truth.
  • Read/write separation: PASS. The mutation remains an explicit operator submit action with auditable outcomes and planned tests.
  • Graph contract path: PASS. No Microsoft Graph calls are introduced.
  • Deterministic capabilities: PASS. Capability checks stay on Capabilities::SUPPORT_REQUESTS_CREATE; no raw capability strings or role-string checks are planned.
  • RBAC-UX / workspace isolation / tenant isolation: PASS. Non-members or actors outside workspace or tenant scope remain 404; in-scope members missing the capability remain 403; latest-handoff visibility uses the same boundary as submit.
  • Global search safety: PASS. No new Filament resource or globally searchable surface is introduced.
  • Run observability / Ops UX: PASS. The slice is intentionally synchronous and does not add queue work or OperationRun usage.
  • Proportionality / PROP-001, ABSTR-001, PERSIST-001, STATE-001, BLOAT-001: PASS. The only new persisted truth is four bounded columns on an existing canonical row, one small handoff mode family, and one concrete provider-owned service for one real target.
  • Shared pattern reuse / XCUT-001: PASS. The plan extends the existing support-request service and existing support-aware action surfaces instead of creating page-local handoff logic.
  • Provider boundary / PROV-001: PASS. Provider semantics stay confined to the concrete handoff service; platform truth remains neutral.
  • Filament-native UI / UI-FIL-001: PASS. The flow stays inside native Filament action forms and notifications.
  • Livewire v4 / Filament v5 compliance: PASS. The plan stays on the current Filament v5 and Livewire v4 stack.
  • Provider registration location: PASS. No provider registration changes are needed; Laravel 11+ provider registration remains in bootstrap/providers.php.
  • Destructive action confirmation: PASS. No destructive action is added, so no new ->requiresConfirmation() path is introduced.
  • Asset strategy: PASS. No new panel or shared assets are required; deployment behavior for cd apps/platform && php artisan filament:assets remains unchanged.
  • Test governance / TEST-GOV-001: PASS. Proof remains in focused unit plus feature lanes, with manual smoke only as implementation close-out.

Test Governance Check

  • Test purpose / classification by changed surface: Unit for handoff branching, target-unavailable fallback, provider normalization, and latest-summary derivation; Feature for tenant and run action behavior, authorization boundaries, persisted linkage truth, partial-success feedback, and audit events
  • Affected validation lanes: fast-feedback, confidence
  • Why this lane mix is the narrowest sufficient proof: the feature is server-driven and synchronous; business truth lives in the submission service, persistence, and authorization boundaries, so browser automation would mostly duplicate what Pest can already prove through Livewire and domain tests
  • Narrowest proving command(s):
    • export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/SupportRequests/ExternalSupportDeskHandoffServiceTest.php tests/Unit/Support/SupportRequests/SupportRequestLatestHandoffSummaryTest.php
    • export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/SupportRequests/TenantSupportRequestExternalHandoffTest.php tests/Feature/SupportRequests/OperationRunSupportRequestExternalHandoffTest.php tests/Feature/SupportRequests/SupportRequestExternalHandoffAuthorizationTest.php tests/Feature/SupportRequests/SupportRequestExternalHandoffAuditTest.php
  • Fixture / helper / factory / seed / context cost risks: reuse existing workspace, tenant, operation run, user membership, and support-request fixtures; add only a small fake for the one external target and a narrow latest-summary assertion helper if needed
  • Expensive defaults or shared helper growth introduced?: no
  • Heavy-family additions, promotions, or visibility changes: none
  • Surface-class relief / special coverage rule: standard-native-filament relief applies on the tenant dashboard action; the run viewer remains under its monitoring-state-page contract and needs the same tenant-entitlement checks as the current support action
  • Closing validation and reviewer handoff: re-run the exact unit and feature commands above, then manually smoke create, link, and failure handling from both existing surfaces; reviewers should explicitly verify that no support-request resource, queue, settings UI, or global-search behavior was added
  • Budget / baseline / trend follow-up: none expected beyond ordinary feature-local upkeep
  • Review-stop questions: did implementation add a new support table, a support-request resource, a support settings UI, a multi-provider registry, or queue or OperationRun behavior that the spec forbids?
  • Escalation path: reject-or-split if target-configuration management, multi-provider support, or retry orchestration appears during implementation
  • Active feature PR close-out entry: Guardrail / Exception / Smoke Coverage
  • Why no dedicated follow-up spec is needed: the delivery cost stays local to the existing support-request path; broader configuration or multi-provider expansion is separate work, not latent scope inside this slice

Implementation Close-Out — Guardrail / Exception / Smoke Coverage

  • Guardrail outcome: PASS. The implementation extends only the existing tenant-dashboard and operation-run Request support actions, keeps the run support action grouped under More, and does not add a support-request resource, support queue, global-search surface, target-management UI, provider registry, or new OperationRun behavior.
  • Finalization exception outcome: PASS. The only post-create mutation on support_requests is the Spec 256 bounded finalization write to external_handoff_mode, external_ticket_reference, external_ticket_url, and external_handoff_failure_summary; invalid linked-ticket input is rejected before the internal support request is created.
  • Smoke coverage outcome: PASS. A temporary Pest Browser smoke harness loaded the tenant dashboard and run detail, submitted tenant create_external_ticket, submitted run link_existing_ticket, forced run create failure, reopened the run support action to verify the latest failure summary, and asserted no browser console or JavaScript errors. The temporary browser test was removed after execution so the permanent coverage remains the planned unit plus feature lanes.
  • Follow-up decision: No in-scope follow-up spec is required. Target-management UI, retry/relink workflows, and multi-provider support remain explicit future-spec candidates only if product pressure proves them necessary.

Project Structure

Documentation (this feature)

specs/256-external-support-desk-handoff/
├── checklists/
│   └── requirements.md
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│   └── external-support-desk-handoff.logical.openapi.yaml
└── tasks.md

Source Code (repository root)

apps/platform/
├── app/
│   ├── Filament/
│   │   └── Pages/
│   │       ├── Operations/
│   │       │   └── TenantlessOperationRunViewer.php
│   │       └── TenantDashboard.php
│   ├── Models/
│   │   └── SupportRequest.php
│   ├── Services/
│   │   └── Audit/
│   │       └── WorkspaceAuditLogger.php
│   └── Support/
│       ├── Audit/
│       │   └── AuditActionId.php
│       └── SupportRequests/
│           ├── SupportRequestContextBuilder.php
│           ├── SupportRequestReferenceGenerator.php
│           ├── SupportRequestSubmissionService.php
│           └── ExternalSupportDeskHandoffService.php
├── config/
│   └── support_desk.php
├── database/
│   ├── factories/
│   │   └── SupportRequestFactory.php
│   └── migrations/
│       └── *_add_external_handoff_fields_to_support_requests_table.php
├── lang/
│   ├── de/
│   │   └── localization.php
│   └── en/
│       └── localization.php
└── tests/
    ├── Feature/SupportRequests/
    │   ├── OperationRunSupportRequestExternalHandoffTest.php
    │   ├── SupportRequestExternalHandoffAuditTest.php
    │   ├── SupportRequestExternalHandoffAuthorizationTest.php
    │   └── TenantSupportRequestExternalHandoffTest.php
    └── Unit/Support/SupportRequests/
        ├── ExternalSupportDeskHandoffServiceTest.php
        └── SupportRequestLatestHandoffSummaryTest.php

Structure Decision: Single Laravel application. The slice extends the existing support-request domain and two existing Filament pages only. One minimal application config contract in config/support_desk.php is in scope so target resolution is explicit, while workspace settings UI and a support settings domain remain out of scope. The constitution-mandated checklist in checklists/requirements.md stays part of the implementation handoff set.

Complexity Tracking

Violation / review item Why Needed Simpler Alternative Rejected Because
Extend support_requests with four external-handoff columns The operator must be able to revisit the current support context and still see the same external linkage or failure truth on the canonical support request A separate support_tickets table would create a second lifecycle and a new surface the current slice does not need
Add one concrete provider-owned handoff service One real external target must be called or normalized from both existing support-aware surfaces without page-local HTTP logic A generic interface, registry, or multi-provider adapter catalog would be premature because the repo has exactly one current-release target case

Proportionality Review

  • Current operator problem: the product can already create an internal support request with redacted context, but operators still have to create or paste an external desk ticket manually outside TenantPilot and then remember that linkage separately
  • Existing structure is insufficient because: the current service ends at internal persistence and cannot carry durable external linkage or explicit failure truth back into the current support context
  • Narrowest correct implementation: extend the existing SupportRequest row with minimal neutral linkage fields, route create or link decisions through the existing submission service, and render the latest linkage only inside the same two support-aware actions
  • Ownership cost created: one additive migration, one concrete provider-owned service, a few audit IDs and audit-logger methods, modest action-form growth on two pages, and focused tests
  • Alternative intentionally rejected: a new support-ticket model, support-request resource or detail page, target-management UI, provider registry, background retry path, or OperationRun delivery orchestration were all rejected as broader than current-release truth
  • Release truth: current-release support follow-through and commercialization gap, not future ITSM platform preparation

Implementation Outline

1. Support request persistence extension

  • Add the four external-handoff columns to support_requests.
  • Default external_handoff_mode to internal_only so existing rows remain truthful without compatibility shims.
  • Keep the internal SR-... reference canonical for every request.

2. Submission service orchestration

  • Continue to authorize and validate through the current SupportRequestSubmissionService path.
  • Persist the internal support request first and keep WorkspaceAuditLogger::logSupportRequestCreated(...) unchanged for that stage.
  • Branch by handoff mode after the internal row exists:
    • internal_only: return immediately with no external fields populated
    • link_existing_ticket: validate and normalize the provided reference or URL locally, persist linkage, and audit linked
    • create_external_ticket: call one concrete provider-owned handoff service outside the initial DB transaction with the five-second timeout budget, then perform the one allowed synchronous finalization write back to the same row and audit the outcome

3. Latest-summary derivation

  • Add one shared read path for the latest handoff summary per primary context.
  • Tenant summary queries the latest support_requests row for the current tenant where primary_context_type = tenant.
  • Run summary queries the latest support_requests row for the current run where primary_context_type = operation_run and operation_run_id matches the viewed run.
  • The visible summary always includes the internal support reference it belongs to.

4. Filament surface extension

  • Extend the existing Request support action on both pages with:
    • mutation-scope guidance (TenantPilot only versus TenantPilot + external support desk)
    • handoff mode choice
    • conditional external reference and URL inputs for link_existing_ticket
    • a read-only latest-handoff summary placeholder scoped to the current context
  • Keep Open support diagnostics unchanged as the diagnostics-secondary affordance.
  • Success notifications include the internal reference and, when present, the external reference.
  • External create failure uses explicit partial-success or warning feedback: internal request created, external handoff failed.

5. Audit and copy consistency

  • Add stable audit action IDs for:
    • external ticket created
    • external ticket linked
    • external handoff failed
  • Keep audit context bounded to workspace, tenant, internal support reference, primary context, handoff mode, and external ticket reference when present.
  • Preserve neutral UI copy and do not surface provider product names as the primary operator vocabulary.

Implementation Phases

  1. Foundation: add the migration shape, model casts and constants, audit IDs, the concrete handoff service contract for one target, and the minimal config/support_desk.php contract.
  2. Submission flow: refactor SupportRequestSubmissionService so internal creation commits first, then link or create outcome persists back to the same row.
  3. Surface wiring: extend the tenant dashboard and run viewer forms with handoff mode, latest-summary placeholder, and outcome-sensitive notification copy.
  4. Hardening: add latest-summary derivation, target-unavailable fallback to internal_only, authorization proof, and audit proof.

Guardrail Close-Out Expectations

  • Livewire v4 compatibility remains unchanged because the flow stays inside existing Filament v5 page actions.
  • Laravel 12 provider registration facts remain unchanged: panel providers stay in bootstrap/providers.php.
  • No globally searchable resource is added, so there is no new global-search contract to satisfy.
  • No destructive action is introduced, so there is no new confirmation flow requirement.
  • No new assets are required; cd apps/platform && php artisan filament:assets stays part of the general deployment path but does not change for this feature.