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
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_requestsrow:external_handoff_mode,external_ticket_reference,external_ticket_url, andexternal_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_requestsis the only persisted truth for this slice. Nosupport_ticketstable, no queue artifact, and no new support page model is justified.- The plan adds these columns to
support_requests:external_handoff_modeas a non-null string with defaultinternal_onlyexternal_ticket_referenceas a nullable stringexternal_ticket_urlas a nullable text fieldexternal_handoff_failure_summaryas 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_requestsindexes 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_requestsrow 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:
- authorize and validate the existing request
- persist the internal support request and
support_request.createdaudit event - perform link or create handoff work after the internal row exists
- 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 supportslide-over onTenantDashboard - the same latest-handoff summary placeholder inside the grouped
Request supportslide-over onTenantlessOperationRunViewer
- Tenant context summary scopes to the latest support request where
primary_context_type = tenantfor the current entitled tenant. - Run context summary scopes to the latest support request where
primary_context_type = operation_runandoperation_run_idmatches the currently opened run.
Minimal application config contract is in scope; support settings UI is not
- The repo has no existing
supportsettings 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.phpbacked 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
Moreand 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.phpapps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.phpapps/platform/app/Models/SupportRequest.phpapps/platform/app/Support/SupportRequests/SupportRequestSubmissionService.phpapps/platform/app/Support/SupportRequests/SupportRequestContextBuilder.phpapps/platform/app/Support/SupportRequests/SupportRequestReferenceGenerator.phpapps/platform/app/Services/Audit/WorkspaceAuditLogger.phpapps/platform/app/Support/Audit/AuditActionId.phpapps/platform/database/factories/SupportRequestFactory.phpapps/platform/config/support_desk.phpapps/platform/lang/en/localization.phpapps/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
UiEnforcementcapability 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, andTenantPilot onlyversusTenantPilot + 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 remain403; 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
OperationRunusage. - 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:assetsremains 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.phpexport 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
OperationRunbehavior 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 supportactions, keeps the run support action grouped underMore, and does not add a support-request resource, support queue, global-search surface, target-management UI, provider registry, or newOperationRunbehavior. - Finalization exception outcome: PASS. The only post-create mutation on
support_requestsis the Spec 256 bounded finalization write toexternal_handoff_mode,external_ticket_reference,external_ticket_url, andexternal_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 runlink_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
SupportRequestrow 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
OperationRundelivery 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_modetointernal_onlyso 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
SupportRequestSubmissionServicepath. - 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 populatedlink_existing_ticket: validate and normalize the provided reference or URL locally, persist linkage, and auditlinkedcreate_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_requestsrow for the current tenant whereprimary_context_type = tenant. - Run summary queries the latest
support_requestsrow for the current run whereprimary_context_type = operation_runandoperation_run_idmatches the viewed run. - The visible summary always includes the internal support reference it belongs to.
4. Filament surface extension
- Extend the existing
Request supportaction on both pages with:- mutation-scope guidance (
TenantPilot onlyversusTenantPilot + 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
- mutation-scope guidance (
- Keep
Open support diagnosticsunchanged 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
- 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.phpcontract. - Submission flow: refactor
SupportRequestSubmissionServiceso internal creation commits first, then link or create outcome persists back to the same row. - Surface wiring: extend the tenant dashboard and run viewer forms with handoff mode, latest-summary placeholder, and outcome-sensitive notification copy.
- 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:assetsstays part of the general deployment path but does not change for this feature.