Compare commits
1 Commits
dev
...
092-legacy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90c4727add |
@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Clusters\Inventory\InventoryCluster;
|
|
||||||
use App\Filament\Resources\InventoryItemResource;
|
|
||||||
use App\Filament\Widgets\Inventory\InventoryKpiHeader;
|
|
||||||
use App\Models\Tenant;
|
|
||||||
use BackedEnum;
|
|
||||||
use Filament\Pages\Page;
|
|
||||||
use UnitEnum;
|
|
||||||
|
|
||||||
class InventoryLanding extends Page
|
|
||||||
{
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
|
||||||
|
|
||||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-squares-2x2';
|
|
||||||
|
|
||||||
protected static string|UnitEnum|null $navigationGroup = 'Inventory';
|
|
||||||
|
|
||||||
protected static ?string $navigationLabel = 'Overview';
|
|
||||||
|
|
||||||
protected static ?string $cluster = InventoryCluster::class;
|
|
||||||
|
|
||||||
protected string $view = 'filament.pages.inventory-landing';
|
|
||||||
|
|
||||||
public function mount(): void
|
|
||||||
{
|
|
||||||
$this->redirect(InventoryItemResource::getUrl('index', tenant: Tenant::current()));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getHeaderWidgets(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
InventoryKpiHeader::class,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -449,7 +449,7 @@ public static function table(Table $table): Table
|
|||||||
);
|
);
|
||||||
|
|
||||||
$operationRunService->dispatchOrFail($operationRun, function () use ($record, $operationRun): void {
|
$operationRunService->dispatchOrFail($operationRun, function () use ($record, $operationRun): void {
|
||||||
Bus::dispatch(new RunBackupScheduleJob(0, $operationRun, (int) $record->getKey()));
|
Bus::dispatch(new RunBackupScheduleJob(operationRun: $operationRun, backupScheduleId: (int) $record->getKey()));
|
||||||
});
|
});
|
||||||
|
|
||||||
OpsUxBrowserEvents::dispatchRunEnqueued($livewire);
|
OpsUxBrowserEvents::dispatchRunEnqueued($livewire);
|
||||||
@ -520,7 +520,7 @@ public static function table(Table $table): Table
|
|||||||
);
|
);
|
||||||
|
|
||||||
$operationRunService->dispatchOrFail($operationRun, function () use ($record, $operationRun): void {
|
$operationRunService->dispatchOrFail($operationRun, function () use ($record, $operationRun): void {
|
||||||
Bus::dispatch(new RunBackupScheduleJob(0, $operationRun, (int) $record->getKey()));
|
Bus::dispatch(new RunBackupScheduleJob(operationRun: $operationRun, backupScheduleId: (int) $record->getKey()));
|
||||||
});
|
});
|
||||||
|
|
||||||
OpsUxBrowserEvents::dispatchRunEnqueued($livewire);
|
OpsUxBrowserEvents::dispatchRunEnqueued($livewire);
|
||||||
@ -746,7 +746,7 @@ public static function table(Table $table): Table
|
|||||||
);
|
);
|
||||||
|
|
||||||
$operationRunService->dispatchOrFail($operationRun, function () use ($record, $operationRun): void {
|
$operationRunService->dispatchOrFail($operationRun, function () use ($record, $operationRun): void {
|
||||||
Bus::dispatch(new RunBackupScheduleJob(0, $operationRun, (int) $record->getKey()));
|
Bus::dispatch(new RunBackupScheduleJob(operationRun: $operationRun, backupScheduleId: (int) $record->getKey()));
|
||||||
}, emitQueuedNotification: false);
|
}, emitQueuedNotification: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -843,7 +843,7 @@ public static function table(Table $table): Table
|
|||||||
);
|
);
|
||||||
|
|
||||||
$operationRunService->dispatchOrFail($operationRun, function () use ($record, $operationRun): void {
|
$operationRunService->dispatchOrFail($operationRun, function () use ($record, $operationRun): void {
|
||||||
Bus::dispatch(new RunBackupScheduleJob(0, $operationRun, (int) $record->getKey()));
|
Bus::dispatch(new RunBackupScheduleJob(operationRun: $operationRun, backupScheduleId: (int) $record->getKey()));
|
||||||
}, emitQueuedNotification: false);
|
}, emitQueuedNotification: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -43,10 +43,17 @@ class RunBackupScheduleJob implements ShouldQueue
|
|||||||
|
|
||||||
public int $tries = 3;
|
public int $tries = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compatibility-only legacy field.
|
||||||
|
*
|
||||||
|
* Kept as an uninitialized typed property so old queued payloads can still
|
||||||
|
* deserialize safely, while new payloads omit the field.
|
||||||
|
*/
|
||||||
|
public int $backupScheduleRunId;
|
||||||
|
|
||||||
public ?OperationRun $operationRun = null;
|
public ?OperationRun $operationRun = null;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public int $backupScheduleRunId,
|
|
||||||
?OperationRun $operationRun = null,
|
?OperationRun $operationRun = null,
|
||||||
public ?int $backupScheduleId = null,
|
public ?int $backupScheduleId = null,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -1,55 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Notifications;
|
|
||||||
|
|
||||||
use Illuminate\Notifications\Notification;
|
|
||||||
|
|
||||||
class BackupScheduleRunDispatchedNotification extends Notification
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param array{
|
|
||||||
* tenant_id:int,
|
|
||||||
* trigger:string,
|
|
||||||
* scheduled_for:string,
|
|
||||||
* backup_schedule_id?:int,
|
|
||||||
* backup_schedule_run_id?:int,
|
|
||||||
* schedule_ids?:array<int, int>,
|
|
||||||
* backup_schedule_run_ids?:array<int, int>
|
|
||||||
* } $metadata
|
|
||||||
*/
|
|
||||||
public function __construct(public array $metadata) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<int, string>
|
|
||||||
*/
|
|
||||||
public function via(object $notifiable): array
|
|
||||||
{
|
|
||||||
return ['database'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function toDatabase(object $notifiable): array
|
|
||||||
{
|
|
||||||
$trigger = (string) ($this->metadata['trigger'] ?? 'run_now');
|
|
||||||
|
|
||||||
$title = match ($trigger) {
|
|
||||||
'retry' => 'Retry dispatched',
|
|
||||||
'bulk_retry' => 'Retries dispatched',
|
|
||||||
'bulk_run_now' => 'Runs dispatched',
|
|
||||||
default => 'Run dispatched',
|
|
||||||
};
|
|
||||||
|
|
||||||
$body = match ($trigger) {
|
|
||||||
'bulk_retry', 'bulk_run_now' => 'Backup runs have been queued.',
|
|
||||||
default => 'A backup run has been queued.',
|
|
||||||
};
|
|
||||||
|
|
||||||
return [
|
|
||||||
'title' => $title,
|
|
||||||
'body' => $body,
|
|
||||||
'metadata' => $this->metadata,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -117,7 +117,7 @@ public function dispatchDue(?array $tenantIdentifiers = null): array
|
|||||||
'next_run_at' => $this->scheduleTimeService->nextRunFor($schedule, $nowUtc),
|
'next_run_at' => $this->scheduleTimeService->nextRunFor($schedule, $nowUtc),
|
||||||
])->saveQuietly();
|
])->saveQuietly();
|
||||||
|
|
||||||
Bus::dispatch(new RunBackupScheduleJob(0, $operationRun, (int) $schedule->id));
|
Bus::dispatch(new RunBackupScheduleJob(operationRun: $operationRun, backupScheduleId: (int) $schedule->id));
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@ -3,10 +3,10 @@
|
|||||||
namespace App\Support;
|
namespace App\Support;
|
||||||
|
|
||||||
use App\Filament\Pages\DriftLanding;
|
use App\Filament\Pages\DriftLanding;
|
||||||
use App\Filament\Pages\InventoryLanding;
|
|
||||||
use App\Filament\Resources\BackupScheduleResource;
|
use App\Filament\Resources\BackupScheduleResource;
|
||||||
use App\Filament\Resources\BackupSetResource;
|
use App\Filament\Resources\BackupSetResource;
|
||||||
use App\Filament\Resources\EntraGroupResource;
|
use App\Filament\Resources\EntraGroupResource;
|
||||||
|
use App\Filament\Resources\InventoryItemResource;
|
||||||
use App\Filament\Resources\PolicyResource;
|
use App\Filament\Resources\PolicyResource;
|
||||||
use App\Filament\Resources\ProviderConnectionResource;
|
use App\Filament\Resources\ProviderConnectionResource;
|
||||||
use App\Filament\Resources\RestoreRunResource;
|
use App\Filament\Resources\RestoreRunResource;
|
||||||
@ -55,7 +55,7 @@ public static function related(OperationRun $run, ?Tenant $tenant): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($run->type === 'inventory_sync') {
|
if ($run->type === 'inventory_sync') {
|
||||||
$links['Inventory'] = InventoryLanding::getUrl(panel: 'tenant', tenant: $tenant);
|
$links['Inventory'] = InventoryItemResource::getUrl('index', panel: 'tenant', tenant: $tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (in_array($run->type, ['policy.sync', 'policy.sync_one'], true)) {
|
if (in_array($run->type, ['policy.sync', 'policy.sync_one'], true)) {
|
||||||
|
|||||||
@ -23,7 +23,6 @@ public static function baseline(): self
|
|||||||
'App\\Filament\\Pages\\ChooseWorkspace' => 'Workspace chooser has no contract-style table action surface.',
|
'App\\Filament\\Pages\\ChooseWorkspace' => 'Workspace chooser has no contract-style table action surface.',
|
||||||
'App\\Filament\\Pages\\DriftLanding' => 'Drift landing retrofit deferred to drift-focused UI spec.',
|
'App\\Filament\\Pages\\DriftLanding' => 'Drift landing retrofit deferred to drift-focused UI spec.',
|
||||||
'App\\Filament\\Pages\\InventoryCoverage' => 'Inventory coverage page retrofit deferred; no action-surface declaration yet.',
|
'App\\Filament\\Pages\\InventoryCoverage' => 'Inventory coverage page retrofit deferred; no action-surface declaration yet.',
|
||||||
'App\\Filament\\Pages\\InventoryLanding' => 'Inventory landing page retrofit deferred; no action-surface declaration yet.',
|
|
||||||
'App\\Filament\\Pages\\Monitoring\\Alerts' => 'Monitoring alerts page retrofit deferred; no action-surface declaration yet.',
|
'App\\Filament\\Pages\\Monitoring\\Alerts' => 'Monitoring alerts page retrofit deferred; no action-surface declaration yet.',
|
||||||
'App\\Filament\\Pages\\Monitoring\\AuditLog' => 'Monitoring audit-log page retrofit deferred; no action-surface declaration yet.',
|
'App\\Filament\\Pages\\Monitoring\\AuditLog' => 'Monitoring audit-log page retrofit deferred; no action-surface declaration yet.',
|
||||||
'App\\Filament\\Pages\\Monitoring\\Operations' => 'Monitoring operations page retrofit deferred; canonical route behavior already covered elsewhere.',
|
'App\\Filament\\Pages\\Monitoring\\Operations' => 'Monitoring operations page retrofit deferred; canonical route behavior already covered elsewhere.',
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<x-filament::section>
|
<x-filament::section>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
Review new drift findings between the last two inventory sync runs for the current scope.
|
Review new drift findings between the last two operation runs for the current scope.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (filled($scopeKey))
|
@if (filled($scopeKey))
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
<x-filament::page>
|
|
||||||
<x-filament::section>
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
|
||||||
Browse inventory items, inspect sync runs, and review coverage/capabilities.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-3">
|
|
||||||
<x-filament::button tag="a" :href="$this->getInventoryItemsUrl()">
|
|
||||||
Inventory Items
|
|
||||||
</x-filament::button>
|
|
||||||
|
|
||||||
<x-filament::button tag="a" color="gray" :href="$this->getSyncRunsUrl()">
|
|
||||||
Sync Runs
|
|
||||||
</x-filament::button>
|
|
||||||
|
|
||||||
<x-filament::button tag="a" color="gray" :href="$this->getCoverageUrl()">
|
|
||||||
Coverage
|
|
||||||
</x-filament::button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</x-filament::section>
|
|
||||||
</x-filament::page>
|
|
||||||
@ -47,7 +47,6 @@
|
|||||||
|
|
||||||
$hasAnyFilamentTenantContext = Filament::getTenant() instanceof Tenant;
|
$hasAnyFilamentTenantContext = Filament::getTenant() instanceof Tenant;
|
||||||
|
|
||||||
$path = '/'.ltrim(request()->path(), '/');
|
|
||||||
$route = request()->route();
|
$route = request()->route();
|
||||||
$routeName = (string) ($route?->getName() ?? '');
|
$routeName = (string) ($route?->getName() ?? '');
|
||||||
$tenantQuery = request()->query('tenant');
|
$tenantQuery = request()->query('tenant');
|
||||||
@ -65,7 +64,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$isTenantScopedRoute = $route?->hasParameter('tenant')
|
$isTenantScopedRoute = $route?->hasParameter('tenant')
|
||||||
|| str_starts_with($path, '/admin/t/')
|
|
||||||
|| ($hasTenantQuery && str_starts_with($routeName, 'filament.admin.'));
|
|| ($hasTenantQuery && str_starts_with($routeName, 'filament.admin.'));
|
||||||
|
|
||||||
$lastTenantId = $workspaceContext->lastTenantId(request());
|
$lastTenantId = $workspaceContext->lastTenantId(request());
|
||||||
|
|||||||
@ -150,19 +150,6 @@
|
|||||||
->get('/admin/operations', \App\Filament\Pages\Monitoring\Operations::class)
|
->get('/admin/operations', \App\Filament\Pages\Monitoring\Operations::class)
|
||||||
->name('admin.operations.index');
|
->name('admin.operations.index');
|
||||||
|
|
||||||
Route::middleware([
|
|
||||||
'web',
|
|
||||||
'panel:admin',
|
|
||||||
'ensure-correct-guard:web',
|
|
||||||
DisableBladeIconComponents::class,
|
|
||||||
DispatchServingFilamentEvent::class,
|
|
||||||
FilamentAuthenticate::class,
|
|
||||||
'ensure-workspace-selected',
|
|
||||||
'ensure-filament-tenant-selected',
|
|
||||||
])
|
|
||||||
->get('/admin/t/{tenant:external_id}/operations', fn () => redirect()->route('admin.operations.index'))
|
|
||||||
->name('admin.operations.legacy-tenant-index');
|
|
||||||
|
|
||||||
Route::middleware([
|
Route::middleware([
|
||||||
'web',
|
'web',
|
||||||
'panel:admin',
|
'panel:admin',
|
||||||
|
|||||||
35
specs/092-legacy-purge-final/checklists/requirements.md
Normal file
35
specs/092-legacy-purge-final/checklists/requirements.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Specification Quality Checklist: Legacy Purge (Runs / Routes / UI / Test Shims)
|
||||||
|
|
||||||
|
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||||
|
**Created**: 2026-02-14
|
||||||
|
**Feature**: [specs/092-legacy-purge-final/spec.md](../spec.md)
|
||||||
|
|
||||||
|
## Content Quality
|
||||||
|
|
||||||
|
- [x] No implementation details (languages, frameworks, APIs)
|
||||||
|
- [x] Focused on user value and business needs
|
||||||
|
- [x] Written for non-technical stakeholders
|
||||||
|
- [x] All mandatory sections completed
|
||||||
|
|
||||||
|
## Requirement Completeness
|
||||||
|
|
||||||
|
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||||
|
- [x] Requirements are testable and unambiguous
|
||||||
|
- [x] Success criteria are measurable
|
||||||
|
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||||
|
- [x] All acceptance scenarios are defined
|
||||||
|
- [x] Edge cases are identified
|
||||||
|
- [x] Scope is clearly bounded
|
||||||
|
- [x] Dependencies and assumptions identified
|
||||||
|
|
||||||
|
## Feature Readiness
|
||||||
|
|
||||||
|
- [x] All functional requirements have clear acceptance criteria
|
||||||
|
- [x] User scenarios cover primary flows
|
||||||
|
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||||
|
- [x] No implementation details leak into specification
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`
|
||||||
|
- This spec includes a small “verification-only glossary” of legacy identifiers to keep the scope unambiguous and to support guard tests; it is intentionally isolated from the user stories.
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: TenantPilot Legacy Routes (Spec 092)
|
||||||
|
version: 1.0.0
|
||||||
|
description: |
|
||||||
|
This contract documents deterministic HTTP semantics for removed legacy endpoints.
|
||||||
|
All listed endpoints MUST return 404 Not Found and MUST NOT redirect.
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- url: /
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/admin/t/{tenantExternalId}/operations:
|
||||||
|
get:
|
||||||
|
summary: Legacy tenant-scoped Operations list (removed)
|
||||||
|
parameters:
|
||||||
|
- name: tenantExternalId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'404':
|
||||||
|
description: Not Found
|
||||||
|
|
||||||
|
/admin/t/{tenantExternalId}/inventory-sync-runs:
|
||||||
|
get:
|
||||||
|
summary: Legacy inventory sync runs list (removed)
|
||||||
|
parameters:
|
||||||
|
- name: tenantExternalId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'404':
|
||||||
|
description: Not Found
|
||||||
|
|
||||||
|
/admin/t/{tenantExternalId}/inventory-sync-runs/{id}:
|
||||||
|
get:
|
||||||
|
summary: Legacy inventory sync run detail (removed)
|
||||||
|
parameters:
|
||||||
|
- name: tenantExternalId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
responses:
|
||||||
|
'404':
|
||||||
|
description: Not Found
|
||||||
|
|
||||||
|
/admin/t/{tenantExternalId}/entra-group-sync-runs:
|
||||||
|
get:
|
||||||
|
summary: Legacy Entra group sync runs list (removed)
|
||||||
|
parameters:
|
||||||
|
- name: tenantExternalId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'404':
|
||||||
|
description: Not Found
|
||||||
|
|
||||||
|
/admin/t/{tenantExternalId}/entra-group-sync-runs/{id}:
|
||||||
|
get:
|
||||||
|
summary: Legacy Entra group sync run detail (removed)
|
||||||
|
parameters:
|
||||||
|
- name: tenantExternalId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
responses:
|
||||||
|
'404':
|
||||||
|
description: Not Found
|
||||||
|
|
||||||
|
/admin/t/{tenantExternalId}/backup-schedules/{scheduleId}/runs/{runId}:
|
||||||
|
get:
|
||||||
|
summary: Legacy backup schedule run detail (removed)
|
||||||
|
parameters:
|
||||||
|
- name: tenantExternalId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: scheduleId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
- name: runId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
responses:
|
||||||
|
'404':
|
||||||
|
description: Not Found
|
||||||
31
specs/092-legacy-purge-final/data-model.md
Normal file
31
specs/092-legacy-purge-final/data-model.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Data Model — Legacy Purge (Runs / Routes / UI / Test Shims)
|
||||||
|
|
||||||
|
Date: 2026-02-14
|
||||||
|
|
||||||
|
This feature does not add or change canonical domain entities. It removes legacy artifacts and a legacy queued-job payload field.
|
||||||
|
|
||||||
|
## Entities (impacted)
|
||||||
|
|
||||||
|
### OperationRun
|
||||||
|
|
||||||
|
- Role: canonical run observability record for operational actions.
|
||||||
|
- Change: none.
|
||||||
|
|
||||||
|
### Backup scheduling job payload
|
||||||
|
|
||||||
|
- Role: queued execution of backup schedule work.
|
||||||
|
- Legacy field to purge: `backupScheduleRunId` (job payload / dispatch parameter).
|
||||||
|
|
||||||
|
#### Compatibility constraints
|
||||||
|
|
||||||
|
- Deploy A: The job MUST still deserialize when older payloads include `backupScheduleRunId`.
|
||||||
|
- Deploy B: Remove the field/property entirely after the compatibility window.
|
||||||
|
|
||||||
|
## UI artifacts (non-data)
|
||||||
|
|
||||||
|
- Redirect-only Inventory landing page: must be removed so Inventory entry points route directly to Inventory Items.
|
||||||
|
- Drift landing copy: change wording only.
|
||||||
|
|
||||||
|
## State transitions
|
||||||
|
|
||||||
|
- No new state transitions introduced.
|
||||||
185
specs/092-legacy-purge-final/plan.md
Normal file
185
specs/092-legacy-purge-final/plan.md
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
# Implementation Plan: Legacy Purge (Runs / Routes / UI / Test Shims)
|
||||||
|
|
||||||
|
**Branch**: `092-legacy-purge-final` | **Date**: 2026-02-14 | **Spec**: `/specs/092-legacy-purge-final/spec.md`
|
||||||
|
**Input**: Feature specification from `/specs/092-legacy-purge-final/spec.md`
|
||||||
|
|
||||||
|
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Remove remaining legacy “run world” artifacts across routes, UI copy, redirect-only pages, and tests. Enforce permanence with guard tests and deliver the queued-job payload shape change via a staged rollout (compatibility release → final purge).
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
**Language/Version**: PHP 8.4.x (Laravel 12)
|
||||||
|
**Primary Dependencies**: Filament v5, Livewire v4, Pest v4, Laravel Sail
|
||||||
|
**Storage**: PostgreSQL
|
||||||
|
**Testing**: Pest (run via `vendor/bin/sail artisan test`)
|
||||||
|
**Target Platform**: Web application (Docker/Sail locally; Dokploy containers in staging/prod)
|
||||||
|
**Project Type**: Laravel monolith
|
||||||
|
**Performance Goals**: No new performance goals (cleanup-only change).
|
||||||
|
**Constraints**: Must not change historical migrations; must preserve RBAC semantics; queued-job payload change must be staged to avoid unserialize failures.
|
||||||
|
**Scale/Scope**: Repo-wide purge + guard rails; small number of routes/views touched; staged deploy A/B for the job payload.
|
||||||
|
|
||||||
|
## Constitution Check
|
||||||
|
|
||||||
|
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||||
|
|
||||||
|
- Inventory-first: clarify what is “last observed” vs snapshots/backups
|
||||||
|
- Read/write separation: any writes require preview + confirmation + audit + tests
|
||||||
|
- Graph contract path: Graph calls only via `GraphClientInterface` + `config/graph_contracts.php`
|
||||||
|
- Deterministic capabilities: capability derivation is testable (snapshot/golden tests)
|
||||||
|
- RBAC-UX: two planes (/admin vs /system) remain separated; cross-plane is 404; non-member tenant access is 404; member-but-missing-capability is 403; authorization checks use Gates/Policies + capability registries (no raw strings, no role-string checks)
|
||||||
|
- Workspace isolation: non-member workspace access is 404; tenant-plane routes require an established workspace context; workspace context switching is separate from Filament Tenancy
|
||||||
|
- RBAC-UX: destructive-like actions require `->requiresConfirmation()` and clear warning text
|
||||||
|
- RBAC-UX: global search is tenant-scoped; non-members get no hints; inaccessible results are treated as not found (404 semantics)
|
||||||
|
- Tenant isolation: all reads/writes tenant-scoped; cross-tenant views are explicit and access-checked
|
||||||
|
- Run observability: long-running/remote/queued work creates/reuses `OperationRun`; start surfaces enqueue-only; Monitoring is DB-only; DB-only <2s actions may skip runs but security-relevant ones still audit-log; auth handshake exception OPS-EX-AUTH-001 allows synchronous outbound HTTP on `/auth/*` without `OperationRun`
|
||||||
|
- Automation: queued/scheduled ops use locks + idempotency; handle 429/503 with backoff+jitter
|
||||||
|
- Data minimization: Inventory stores metadata + whitelisted meta; logs contain no secrets/tokens
|
||||||
|
- Badge semantics (BADGE-001): status-like badges use `BadgeCatalog` / `BadgeRenderer`; no ad-hoc mappings; new values include tests
|
||||||
|
- Filament UI Action Surface Contract: for any new/modified Filament Resource/RelationManager/Page, define Header/Row/Bulk/Empty-State actions, ensure every List/Table has a record inspection affordance (prefer `recordUrl()` clickable rows; do not render a lone View row action), keep max 2 visible row actions with the rest in “More”, group bulk actions, require confirmations for destructive actions (typed confirmation for large/bulk where applicable), write audit logs for mutations, enforce RBAC via central helpers (non-member 404, member missing capability 403), and ensure CI blocks merges if the contract is violated or not explicitly exempted
|
||||||
|
|
||||||
|
**Gate status (pre-Phase 0)**: PASS
|
||||||
|
|
||||||
|
- Inventory-first: no changes to inventory modeling; only removes a redirect-only landing page and ensures canonical entry points.
|
||||||
|
- Read/write separation: no new writes introduced.
|
||||||
|
- Graph contract path: no new Graph calls.
|
||||||
|
- Deterministic capabilities: unchanged.
|
||||||
|
- RBAC-UX + workspace/tenant isolation: routes removed are legacy shims; behavior for unauthorized access remains deny-as-not-found (404) where applicable.
|
||||||
|
- Run observability: unchanged.
|
||||||
|
- Filament Action Surface Contract: only copy/navigation/heuristic removal; no new resources/pages or actions.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Documentation (this feature)
|
||||||
|
|
||||||
|
```text
|
||||||
|
specs/092-legacy-purge-final/
|
||||||
|
├── plan.md # This file (/speckit.plan command output)
|
||||||
|
├── research.md # Phase 0 output (/speckit.plan command)
|
||||||
|
├── data-model.md # Phase 1 output (/speckit.plan command)
|
||||||
|
├── quickstart.md # Phase 1 output (/speckit.plan command)
|
||||||
|
├── contracts/ # Phase 1 output (/speckit.plan command)
|
||||||
|
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Code (repository root)
|
||||||
|
```text
|
||||||
|
app/
|
||||||
|
├── Filament/
|
||||||
|
├── Http/
|
||||||
|
├── Jobs/
|
||||||
|
├── Models/
|
||||||
|
├── Providers/
|
||||||
|
├── Support/
|
||||||
|
└── ...
|
||||||
|
|
||||||
|
routes/
|
||||||
|
├── web.php
|
||||||
|
└── console.php
|
||||||
|
|
||||||
|
resources/
|
||||||
|
├── views/
|
||||||
|
└── ...
|
||||||
|
|
||||||
|
database/
|
||||||
|
├── migrations/ # immutable
|
||||||
|
└── factories/
|
||||||
|
|
||||||
|
tests/
|
||||||
|
├── Feature/
|
||||||
|
└── Unit/
|
||||||
|
|
||||||
|
specs/092-legacy-purge-final/
|
||||||
|
├── spec.md
|
||||||
|
├── plan.md
|
||||||
|
├── research.md
|
||||||
|
├── data-model.md
|
||||||
|
├── quickstart.md
|
||||||
|
└── contracts/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure Decision**: Laravel monolith. Changes are localized to routing (`routes/web.php`), UI views (Blade), Filament pages/helpers, and Pest feature tests.
|
||||||
|
|
||||||
|
## Complexity Tracking
|
||||||
|
|
||||||
|
No constitution violations are required for this feature.
|
||||||
|
|
||||||
|
## Phase 0 — Outline & Research
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
|
||||||
|
- Translate spec requirements into concrete code locations and guard patterns.
|
||||||
|
- Decide how to deliver the queued-job payload change safely (Deploy A/B).
|
||||||
|
|
||||||
|
### Research tasks (from spec + repo reality)
|
||||||
|
|
||||||
|
- Identify all legacy redirect shims and legacy endpoints still present (routes + UI links + tests).
|
||||||
|
- Identify all legacy identifiers that must be purged (class names, field names, copy).
|
||||||
|
- Confirm which routes should return `404 Not Found` after removal (per spec Clarifications).
|
||||||
|
|
||||||
|
### Output
|
||||||
|
|
||||||
|
- `specs/092-legacy-purge-final/research.md`
|
||||||
|
|
||||||
|
## Phase 1 — Design & Contracts
|
||||||
|
|
||||||
|
### Data model
|
||||||
|
|
||||||
|
No new entities. Document impacted payload/fields and removal timeline:
|
||||||
|
|
||||||
|
- Background job payload: remove legacy `backupScheduleRunId` using staged rollout.
|
||||||
|
- UI/route artifacts: remove redirect-only landing page and legacy route shims.
|
||||||
|
|
||||||
|
### Contracts
|
||||||
|
|
||||||
|
This feature is not an external API feature, but it does define deterministic HTTP semantics for legacy endpoints.
|
||||||
|
|
||||||
|
- Provide a minimal OpenAPI contract that documents legacy URLs returning `404` (deny-by-absence).
|
||||||
|
|
||||||
|
### Output
|
||||||
|
|
||||||
|
- `specs/092-legacy-purge-final/data-model.md`
|
||||||
|
- `specs/092-legacy-purge-final/contracts/*`
|
||||||
|
- `specs/092-legacy-purge-final/quickstart.md`
|
||||||
|
|
||||||
|
### Agent context update
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
- `.specify/scripts/bash/update-agent-context.sh copilot`
|
||||||
|
|
||||||
|
## Constitution Check (post-Phase 1)
|
||||||
|
|
||||||
|
**Gate status (post-Phase 1)**: PASS
|
||||||
|
|
||||||
|
- No new Graph calls.
|
||||||
|
- No new mutations/actions.
|
||||||
|
- Route removals preserve deny-as-not-found semantics for legacy deep links.
|
||||||
|
- Staged rollout plan prevents queued-payload failures.
|
||||||
|
|
||||||
|
## Phase 2 — Implementation Planning (Deploy/PR sequence)
|
||||||
|
|
||||||
|
### Deploy A (compatibility release)
|
||||||
|
|
||||||
|
- Make the legacy job field optional / ignorable so older queued payloads still deserialize.
|
||||||
|
- Stop passing/creating dummy legacy run IDs at dispatch sites.
|
||||||
|
- Add/adjust tests ensuring both payload shapes are accepted.
|
||||||
|
|
||||||
|
### Deploy B (final purge)
|
||||||
|
|
||||||
|
- Remove the legacy job field entirely once the compatibility window has passed and queues are drained.
|
||||||
|
- Remove any remaining references + guard against reintroduction.
|
||||||
|
|
||||||
|
### PR breakdown (recommended)
|
||||||
|
|
||||||
|
1. **Routing + tests**: remove legacy redirect routes (e.g., `/admin/t/{tenant}/operations` shim) and update tests to assert `404`.
|
||||||
|
2. **UI cleanup**: remove redirect-only Inventory landing + dead view; update Drift copy to “operation runs”; remove context-bar URL heuristic.
|
||||||
|
3. **Job staged rollout**: implement Deploy A compatibility changes with tests.
|
||||||
|
4. **Final purge**: implement Deploy B removal + tighten guards.
|
||||||
|
|
||||||
|
### Test strategy (minimum)
|
||||||
|
|
||||||
|
- Focused: run the feature test files that assert legacy URLs return 404 and any job serialization tests added for Deploy A/B.
|
||||||
|
- Repo hygiene: run `vendor/bin/sail bin pint --dirty`.
|
||||||
38
specs/092-legacy-purge-final/quickstart.md
Normal file
38
specs/092-legacy-purge-final/quickstart.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Quickstart — Legacy Purge (Runs / Routes / UI / Test Shims)
|
||||||
|
|
||||||
|
Date: 2026-02-14
|
||||||
|
|
||||||
|
## Local dev
|
||||||
|
|
||||||
|
- Start containers: `vendor/bin/sail up -d`
|
||||||
|
- Run formatting (required): `vendor/bin/sail bin pint --dirty`
|
||||||
|
|
||||||
|
## Targeted verification (suggested)
|
||||||
|
|
||||||
|
### Legacy endpoints
|
||||||
|
|
||||||
|
- Run the legacy endpoints tests:
|
||||||
|
- `vendor/bin/sail artisan test --compact tests/Feature/Operations/LegacyRunRoutesNotFoundTest.php`
|
||||||
|
- If updated as part of this spec: `vendor/bin/sail artisan test --compact tests/Feature/078/TenantListRedirectTest.php`
|
||||||
|
|
||||||
|
### UI copy and navigation
|
||||||
|
|
||||||
|
- Verify Drift landing copy contains “operation runs” and does not contain “inventory sync runs”.
|
||||||
|
- Verify Inventory navigation lands on Inventory Items without passing through a redirect-only landing page.
|
||||||
|
|
||||||
|
### Guard suite
|
||||||
|
|
||||||
|
- Run the guard tests added/updated for this spec to ensure legacy identifiers cannot be reintroduced.
|
||||||
|
|
||||||
|
## Deployment notes (staged rollout)
|
||||||
|
|
||||||
|
### Deploy A (compat)
|
||||||
|
|
||||||
|
- Goal: stop emitting the legacy job field while keeping deserialization compatible.
|
||||||
|
- Run migrations if any (this feature should not include migrations).
|
||||||
|
|
||||||
|
### Deploy B (final purge)
|
||||||
|
|
||||||
|
- Goal: remove the legacy field/property entirely once queues are drained.
|
||||||
|
|
||||||
|
Operational note: coordinate the compatibility window with queue retention / worker restarts to reduce the chance of old payloads persisting.
|
||||||
71
specs/092-legacy-purge-final/research.md
Normal file
71
specs/092-legacy-purge-final/research.md
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# Research — Legacy Purge (Runs / Routes / UI / Test Shims)
|
||||||
|
|
||||||
|
Date: 2026-02-14
|
||||||
|
|
||||||
|
This document resolves planning unknowns by grounding the spec in current repository reality (routes, views, tests, and job payload constraints).
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### 1) Legacy deep links return `404 Not Found`
|
||||||
|
|
||||||
|
- Decision: Legacy run endpoints and legacy tenant-scoped redirect shims MUST return `404 Not Found` (no redirect).
|
||||||
|
- Rationale: The spec’s clarifications explicitly choose fail-fast behavior for old bookmarks, and removing redirects eliminates long-term maintenance of compatibility shims.
|
||||||
|
- Alternatives considered:
|
||||||
|
- Keep redirects indefinitely → rejected (ongoing maintenance + encourages legacy URLs).
|
||||||
|
- Redirect with warnings → rejected (still maintains shim surface).
|
||||||
|
|
||||||
|
Evidence in repo:
|
||||||
|
- Legacy run endpoints are already asserted as `404` in `tests/Feature/Operations/LegacyRunRoutesNotFoundTest.php`.
|
||||||
|
- A legacy operations list redirect shim exists in `routes/web.php` (`/admin/t/{tenant:external_id}/operations`) and is currently tested as `302` in `tests/Feature/078/TenantListRedirectTest.php`.
|
||||||
|
|
||||||
|
### 2) Context chrome must not infer tenant scope from URL prefixes
|
||||||
|
|
||||||
|
- Decision: Remove URL-prefix heuristics (e.g., `str_starts_with($path, '/admin/t/')`) and infer tenant scope from route parameters + workspace/tenant context helpers.
|
||||||
|
- Rationale: URL heuristics are brittle and conflict with the “single canonical mental model” objective.
|
||||||
|
- Alternatives considered:
|
||||||
|
- Keep URL-prefix checks as a fallback → rejected (per spec FR-006).
|
||||||
|
|
||||||
|
Evidence in repo:
|
||||||
|
- URL-prefix detection is currently present in `resources/views/filament/partials/context-bar.blade.php`.
|
||||||
|
|
||||||
|
### 3) Inventory entry point is the Inventory Items index (canonical)
|
||||||
|
|
||||||
|
- Decision: Remove redirect-only Inventory landing entry points and route Inventory navigation directly to Inventory Items.
|
||||||
|
- Rationale: Redirect-only pages create dead views and increase cognitive overhead; the spec clarifies Inventory Items as the canonical target.
|
||||||
|
- Alternatives considered:
|
||||||
|
- Keep landing page as a “hub” → rejected (not needed; creates dead/duplicate UI).
|
||||||
|
|
||||||
|
### 4) Drift copy uses canonical terminology “operation runs”
|
||||||
|
|
||||||
|
- Decision: Replace the legacy phrase “inventory sync runs” with “operation runs”.
|
||||||
|
- Rationale: Consolidates terminology around the canonical Operations mental model.
|
||||||
|
- Alternatives considered:
|
||||||
|
- “inventory sync operations” / “sync operations” → rejected (still perpetuates legacy specificity).
|
||||||
|
|
||||||
|
### 5) Guard scope and exclusions
|
||||||
|
|
||||||
|
- Decision: Add/extend guard tests scanning for removed legacy identifiers/patterns. Exclude `database/migrations/**`, `references/**`, and `docs/**`.
|
||||||
|
- Rationale: Migrations are immutable; references/docs are allowed to mention legacy for historical context.
|
||||||
|
- Alternatives considered:
|
||||||
|
- Scan entire repo including migrations → rejected (would fail on immutable history).
|
||||||
|
|
||||||
|
## Staged rollout rationale (queued job payload)
|
||||||
|
|
||||||
|
- Decision: Deliver the queued job payload shape change in two deployments:
|
||||||
|
- Deploy A: accept/ignore the legacy field while new dispatches stop sending it.
|
||||||
|
- Deploy B: remove the legacy field/property entirely once the queue is drained.
|
||||||
|
- Rationale: Laravel queue payloads are serialized; removing a property/constructor argument prematurely can break deserialization of previously queued jobs.
|
||||||
|
|
||||||
|
## Legacy hotspot inventory (confirmed current matches)
|
||||||
|
|
||||||
|
- `app/Jobs/RunBackupScheduleJob.php` (legacy queued payload field compatibility/purge target)
|
||||||
|
- `app/Notifications/BackupScheduleRunDispatchedNotification.php` (dead legacy notification target)
|
||||||
|
- `app/Filament/Pages/InventoryLanding.php` (redirect-only landing removal target)
|
||||||
|
- `resources/views/filament/partials/context-bar.blade.php` (URL-prefix heuristic removal target)
|
||||||
|
- `routes/web.php` (legacy redirect shim endpoint removal target)
|
||||||
|
- `tests/Pest.php` (legacy test shim bootstrap require removal target)
|
||||||
|
- `tests/Support/LegacyModels/InventorySyncRun.php` (legacy test shim file removal target)
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
None identified that block Phase 1 design. Any newly discovered legacy identifiers during implementation should be added to the guard list only if they are truly “must be purged” (and should respect the exclusions).
|
||||||
163
specs/092-legacy-purge-final/spec.md
Normal file
163
specs/092-legacy-purge-final/spec.md
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
# Feature Specification: Legacy Purge (Runs / Routes / UI / Test Shims)
|
||||||
|
|
||||||
|
**Feature Branch**: `092-legacy-purge-final`
|
||||||
|
**Created**: 2026-02-14
|
||||||
|
**Status**: Draft
|
||||||
|
**Input**: User description: "Spec 092 — Legacy Purge: Runs / Routes / UI / Test-Shims vollständig entfernen"
|
||||||
|
|
||||||
|
## Clarifications
|
||||||
|
|
||||||
|
### Session 2026-02-14
|
||||||
|
|
||||||
|
- Q: For legacy tenant-scoped deep links that were previously served via redirect shims under `/admin/t/{tenant:external_id}/...` (e.g. `/admin/t/{tenant}/operations`), what response semantics do you want? → A: Always `404 Not Found`.
|
||||||
|
- Q: For guard tests scanning for legacy patterns, which paths should be excluded? → A: Exclude `database/migrations/**` + `references/**` + `docs/**`.
|
||||||
|
- Q: For Inventory entry after removing the redirect-only landing page, what should be the canonical target? → A: Inventory Items.
|
||||||
|
- Q: For Drift landing copy, what exact phrase should replace the legacy text “inventory sync runs”? → A: Use “operation runs”.
|
||||||
|
|
||||||
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|
||||||
|
### User Story 1 - Operate without legacy concepts (Priority: P1)
|
||||||
|
|
||||||
|
As a platform maintainer, I can use the application without encountering legacy run concepts (URLs, labels, redirect-only pages, or test shims), so the system has a single canonical mental model.
|
||||||
|
|
||||||
|
**Why this priority**: Removing legacy remnants reduces refactor risk, prevents onboarding confusion, and eliminates hidden compatibility logic.
|
||||||
|
|
||||||
|
**Independent Test**: Navigate through Inventory, Drift, Operations/run history, and Provider Connections using the primary UI entry points; verify no legacy URLs or legacy wording appear and no compatibility shims are required.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a user navigates via the canonical UI navigation, **When** they open Inventory and Drift entry points, **Then** they land on canonical pages (not redirect-only landing pages) and see canonical operations terminology.
|
||||||
|
2. **Given** a user is in the admin UI, **When** the context/navigation chrome renders, **Then** it derives its state from the current workspace/tenant context only (not from legacy URL heuristics).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 2 - Legacy deep links fail fast (Priority: P2)
|
||||||
|
|
||||||
|
As a tenant admin with old bookmarks, I get a clear failure (not a redirect) when accessing legacy tenant-scoped admin paths, so the system enforces a single set of canonical URLs.
|
||||||
|
|
||||||
|
**Why this priority**: Removing redirects is an intentional breaking change; we want predictable behavior and to avoid ongoing maintenance of compatibility shims.
|
||||||
|
|
||||||
|
**Independent Test**: Attempt to access representative legacy tenant-scoped admin URLs and confirm they are not handled by the application.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** an old bookmark to a legacy tenant-scoped admin path, **When** it is requested, **Then** the request returns `404 Not Found` and is not redirected by the application.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 3 - Prevent reintroduction (Priority: P3)
|
||||||
|
|
||||||
|
As a developer, I get fast feedback if I accidentally reintroduce removed legacy patterns (identifiers, parameters, route prefixes, or UI copy), so the cleanup remains permanent.
|
||||||
|
|
||||||
|
**Why this priority**: Cleanup work regresses easily during refactors; guard tests turn “tribal knowledge” into enforceable rules.
|
||||||
|
|
||||||
|
**Independent Test**: Introduce a known legacy identifier in a non-migration file and confirm automated guards fail reliably; remove it and confirm the suite is green again.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a change introduces a removed legacy identifier in non-migration code, **When** the automated guard suite runs, **Then** the build fails with an actionable message.
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- Legacy tenant-scoped paths may still exist in browser history or external references; the application must not rely on redirects to function.
|
||||||
|
- Previously enqueued background work must continue to process safely during the compatibility window; removal of legacy job fields must not cause noisy runtime behavior.
|
||||||
|
- Automated legacy scans must explicitly exclude database migration history (migrations are immutable).
|
||||||
|
- Authorization semantics must not change as a side-effect of cleanup.
|
||||||
|
|
||||||
|
## Requirements *(mandatory)*
|
||||||
|
|
||||||
|
**Constitution alignment (required):** This feature does not introduce new external calls. It does change long-running/queued work APIs (background job payload shape), so it MUST be delivered using a staged rollout to avoid compatibility issues with previously queued payloads.
|
||||||
|
|
||||||
|
**Constitution alignment (RBAC-UX):** This feature does not change authorization rules. It removes legacy routes and UI heuristics; access semantics must remain consistent and must not introduce new information leaks.
|
||||||
|
|
||||||
|
**Constitution alignment (OPS-EX-AUTH-001):** Not applicable.
|
||||||
|
|
||||||
|
**Constitution alignment (BADGE-001):** Not applicable.
|
||||||
|
|
||||||
|
**Constitution alignment (Filament Action Surfaces):** This feature changes admin UI content and navigation/landing behavior but does not introduce new mutation actions. The Action Surface Contract remains satisfied by existing surfaces.
|
||||||
|
|
||||||
|
### Scope (bounded)
|
||||||
|
|
||||||
|
- In scope: removal of legacy run artifacts in code, routes, UI copy/heuristics, and tests; addition/extension of guard tests that prevent reintroduction.
|
||||||
|
- Out of scope: any changes to the canonical operations data model; any new features; any modifications to historical database migrations.
|
||||||
|
|
||||||
|
### Assumptions & Dependencies
|
||||||
|
|
||||||
|
- Release process supports two deployments/releases for the staged job payload change (compatibility window → final purge).
|
||||||
|
- Migration history is immutable and excluded from legacy-scanning guards.
|
||||||
|
- Release notes will communicate removal of legacy deep links.
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
- **FR-001 (Dead notification removal)**: The unused legacy notification for “backup schedule run dispatched” MUST not exist in active code.
|
||||||
|
- **Verification**: A repository-wide scan of non-migration code finds no references to the removed notification artifact.
|
||||||
|
|
||||||
|
- **FR-002 (Job API cleanup, staged)**: The backup schedule execution job MUST not require a legacy run identifier at dispatch sites.
|
||||||
|
- **FR-002.A (Compatibility release)**: During a compatibility window, older queued payloads MUST still deserialize safely while new dispatches no longer include a dummy legacy run id.
|
||||||
|
- **FR-002.B (Final purge)**: After the compatibility window, the legacy parameter/property MUST be removed entirely.
|
||||||
|
- **Compatibility window definition**: The compatibility window MUST be considered complete only after Deploy A has been in production for at least 7 calendar days, and the operations team confirms there is no remaining backlog of previously queued backup schedule execution jobs.
|
||||||
|
- **Verification**: After final purge, no non-migration code references the legacy run-id field, and dispatch sites do not include placeholder arguments.
|
||||||
|
|
||||||
|
- **FR-003 (Inventory landing removal)**: Inventory navigation MUST not rely on redirect-only landing pages or dead views.
|
||||||
|
- **Verification**: Inventory entry points route directly to Inventory Items (canonical index), without using a redirect-only landing page.
|
||||||
|
|
||||||
|
- **FR-004 (Drift landing copy)**: Drift UI copy MUST not use legacy terminology such as “inventory sync runs”; it MUST use “operation runs”.
|
||||||
|
- **Verification**: Drift landing content shows “operation runs” and no legacy phrase remains visible.
|
||||||
|
|
||||||
|
- **FR-005 (Legacy redirect routes removal)**: The application MUST not define redirect shims for removed legacy tenant-scoped admin endpoints (e.g. `/admin/t/{tenant:external_id}/operations`).
|
||||||
|
- **Verification**: Routing definitions contain no legacy tenant-scoped redirect shims, and requests to the removed legacy endpoints return `404 Not Found`.
|
||||||
|
- **Scope note**: At the time of writing, the only known legacy tenant-scoped redirect shim endpoint in `routes/**` is `/admin/t/{tenant:external_id}/operations`. If additional legacy redirect shim endpoints are discovered during implementation, they MUST be added to this spec and covered by tests.
|
||||||
|
|
||||||
|
- **FR-006 (Context chrome heuristic removal)**: Context/navigation chrome MUST not infer state from legacy URL patterns.
|
||||||
|
- **Verification**: Context chrome behavior is correct without any legacy URL detection logic.
|
||||||
|
|
||||||
|
- **FR-007 (Test shim removal)**: Automated tests MUST not rely on legacy model shims or bootstrap hacks that simulate removed legacy domain concepts.
|
||||||
|
- **Verification**: Test suite passes without any legacy shim files or bootstrapping.
|
||||||
|
|
||||||
|
- **FR-008 (Guard suite)**: Guard tests MUST fail if removed legacy identifiers/patterns are reintroduced outside migration history.
|
||||||
|
- **Verification**: Guard suite reliably fails on reintroduction and passes when removed. Scans MUST exclude `database/migrations/**`, `references/**`, and `docs/**`.
|
||||||
|
|
||||||
|
- **FR-009 (Migration immutability)**: Database migrations MUST remain unchanged.
|
||||||
|
- **Verification**: No migration files are modified as part of this feature.
|
||||||
|
|
||||||
|
### Legacy identifiers (verification-only glossary)
|
||||||
|
|
||||||
|
The following legacy identifiers/patterns are considered removed and are used only for verification/guarding (outside migrations):
|
||||||
|
|
||||||
|
- Notification artifact name: `BackupScheduleRunDispatchedNotification`
|
||||||
|
- Legacy shim model name: `InventorySyncRun`
|
||||||
|
- Legacy job field name: `backupScheduleRunId`
|
||||||
|
- Legacy tenant-scoped deep-link endpoint(s) served by redirect shims (current known set):
|
||||||
|
- `/admin/t/{tenant:external_id}/operations`
|
||||||
|
- Non-legacy note: the tenant-plane prefix `/admin/t/{tenant:external_id}/...` is canonical by itself; only explicitly listed legacy deep-link shim endpoints are treated as removed.
|
||||||
|
- Legacy UI copy phrase: `inventory sync runs`
|
||||||
|
|
||||||
|
### Acceptance Criteria (Definition of Done)
|
||||||
|
|
||||||
|
- No active code (excluding database migration history) contains the removed legacy identifiers/patterns.
|
||||||
|
- No legacy redirect routes exist; legacy tenant-scoped deep links are not supported and return `404 Not Found`.
|
||||||
|
- Inventory entry points are canonical (no redirect-only landing UI).
|
||||||
|
- Drift landing copy uses canonical operations terminology.
|
||||||
|
- Test suite is green; guard tests prevent reintroduction.
|
||||||
|
- Guard scans explicitly exclude `database/migrations/**`, `references/**`, and `docs/**`.
|
||||||
|
|
||||||
|
## UI Action Matrix *(mandatory when Filament is changed)*
|
||||||
|
|
||||||
|
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Drift landing content | Drift entry UI | No change | No change | No change | No change | No change | No change | No change | No | Copy-only change; no action surface impact |
|
||||||
|
| Inventory navigation entry | Inventory entry UI | No change | No change | No change | No change | No change | No change | No change | No | Remove redirect-only landing behavior |
|
||||||
|
| Context/navigation chrome | Admin UI chrome | No change | No change | No change | No change | No change | No change | No change | No | Remove legacy URL detection; no actions affected |
|
||||||
|
|
||||||
|
### Key Entities *(include if feature involves data)*
|
||||||
|
|
||||||
|
- **Background job payloads**: Existing queued payloads may contain legacy fields; staged rollout must maintain compatibility until payloads are drained.
|
||||||
|
|
||||||
|
## Success Criteria *(mandatory)*
|
||||||
|
|
||||||
|
### Measurable Outcomes
|
||||||
|
|
||||||
|
- **SC-001**: A canonical navigation walkthrough (Inventory → Drift → Operations/run history → Provider connections) shows no legacy deep-link endpoints/redirect shims and no legacy run terminology.
|
||||||
|
- **SC-002**: The application defines zero legacy tenant-scoped redirect shim routes, and removed legacy deep links return `404 Not Found`.
|
||||||
|
- **SC-003**: The automated test suite passes; guard tests fail deterministically when any removed legacy identifier is reintroduced outside migration history.
|
||||||
|
- **SC-004**: The staged rollout completes with no emergency rollback due to queued job payload incompatibilities.
|
||||||
157
specs/092-legacy-purge-final/tasks.md
Normal file
157
specs/092-legacy-purge-final/tasks.md
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
description: "Task list for Spec 092 implementation"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tasks: Legacy Purge (Runs / Routes / UI / Test Shims)
|
||||||
|
|
||||||
|
**Input**: Design documents from `/specs/092-legacy-purge-final/`
|
||||||
|
|
||||||
|
**Available docs**: plan.md (required), spec.md (required), research.md, data-model.md, quickstart.md, contracts/
|
||||||
|
|
||||||
|
**Tests**: Required (Pest) — this feature changes runtime behavior (routes, queue payload compatibility, and UI).
|
||||||
|
|
||||||
|
## Phase 1: Setup (Shared Infrastructure)
|
||||||
|
|
||||||
|
**Purpose**: Establish a safe baseline for a repo-wide purge.
|
||||||
|
|
||||||
|
- [X] T001 Capture baseline by running focused tests (no code changes): `tests/Feature/Operations/LegacyRunRoutesNotFoundTest.php`, `tests/Feature/078/TenantListRedirectTest.php`
|
||||||
|
- [X] T002 Capture baseline formatting state by running `vendor/bin/sail bin pint --dirty`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Foundational (Blocking Prerequisites)
|
||||||
|
|
||||||
|
**Purpose**: Shared guard rails and spec consistency that must be correct before story work.
|
||||||
|
|
||||||
|
- [X] T003 Confirm legacy identifier glossary in `specs/092-legacy-purge-final/spec.md` targets only removed legacy deep-link endpoints / redirect shims (and does not treat the canonical `/admin/t/{tenant:external_id}/...` tenant-plane prefix as legacy by itself)
|
||||||
|
- [X] T004 [P] Inventory legacy hotspots and annotate in `specs/092-legacy-purge-final/research.md` (confirm current matches: `app/Jobs/RunBackupScheduleJob.php`, `app/Notifications/BackupScheduleRunDispatchedNotification.php`, `app/Filament/Pages/InventoryLanding.php`, `resources/views/filament/partials/context-bar.blade.php`, `routes/web.php`, `tests/Pest.php`, `tests/Support/LegacyModels/InventorySyncRun.php`)
|
||||||
|
- [X] T005 Update guard scan exclusions in `tests/Feature/Guards/NoLegacyRunsTest.php` to explicitly exclude `docs/**` (even if not scanned today) and confirm exclusions include `database/migrations/**`, `references/**`
|
||||||
|
|
||||||
|
**Checkpoint**: Foundation ready — user story implementation can begin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: User Story 1 — Operate without legacy concepts (Priority: P1) 🎯 MVP
|
||||||
|
|
||||||
|
**Goal**: Remove legacy artifacts from primary navigation/UX and staged-purge the legacy job field.
|
||||||
|
|
||||||
|
**Independent Test**: Navigate Inventory and Drift entry points; verify no redirect-only landing page, no legacy “inventory sync runs” copy, and no legacy URL heuristics are required. Run the focused tests listed under this story.
|
||||||
|
|
||||||
|
### Tests for User Story 1
|
||||||
|
|
||||||
|
- [X] T006 [P] [US1] Add/adjust UI copy assertion test for Drift landing in `tests/Feature/Monitoring/` or `tests/Feature/Drift/` (create if absent) to ensure “operation runs” is present and “inventory sync runs” is absent (view: `resources/views/filament/pages/drift-landing.blade.php`)
|
||||||
|
- [X] T007 [P] [US1] Add compatibility test for Deploy A job payload in `tests/Feature/BackupScheduling/RunBackupScheduleJobCompatibilityTest.php` (new) ensuring the job can be constructed without the legacy argument and older payloads still deserialize
|
||||||
|
|
||||||
|
### Implementation for User Story 1
|
||||||
|
|
||||||
|
- [X] T008 [P] [US1] Remove dead notification `app/Notifications/BackupScheduleRunDispatchedNotification.php` and update any references (if any) to avoid unused legacy artifacts
|
||||||
|
- [X] T009 [P] [US1] Remove redirect-only Inventory landing page `app/Filament/Pages/InventoryLanding.php` and delete `resources/views/filament/pages/inventory-landing.blade.php`
|
||||||
|
- [X] T010 [P] [US1] Update related links in `app/Support/OperationRunLinks.php` to link directly to the canonical Inventory Items index (remove `InventoryLanding::getUrl(...)` usage)
|
||||||
|
- [X] T011 [P] [US1] Remove InventoryLanding exemption entry from `app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php`
|
||||||
|
- [X] T012 [US1] Update Drift landing copy in `resources/views/filament/pages/drift-landing.blade.php` to replace “inventory sync runs” with “operation runs”
|
||||||
|
- [X] T013 [US1] Remove legacy URL heuristic from `resources/views/filament/partials/context-bar.blade.php` (remove `str_starts_with($path, '/admin/t/')`) and derive tenant-scoped behavior from route params + workspace/tenant context only
|
||||||
|
|
||||||
|
### Deploy A — job payload compatibility release (part of FR-002)
|
||||||
|
|
||||||
|
- [X] T014 [US1] Make `backupScheduleRunId` optional in `app/Jobs/RunBackupScheduleJob.php` (compat) and stop passing dummy values at dispatch sites (search dispatchers) so new dispatches don’t require a placeholder argument
|
||||||
|
- [X] T015 [US1] Update tests that assume `backupScheduleRunId === 0` (`tests/Feature/BackupScheduling/DispatchIdempotencyTest.php`, `tests/Feature/BackupScheduling/RunNowRetryActionsTest.php`) to match the compatibility behavior
|
||||||
|
|
||||||
|
### Deploy B — final purge (scheduled after compatibility window)
|
||||||
|
|
||||||
|
- [X] T016 [US1] Remove `backupScheduleRunId` from `app/Jobs/RunBackupScheduleJob.php` entirely and delete/adjust any remaining references in app + tests
|
||||||
|
|
||||||
|
**Checkpoint**: US1 complete — UI entry points are canonical, and the staged job cleanup is delivered (Deploy A ready; Deploy B planned/executable).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: User Story 2 — Legacy deep links fail fast (Priority: P2)
|
||||||
|
|
||||||
|
**Goal**: Remove redirect shims and ensure legacy endpoints return `404 Not Found`.
|
||||||
|
|
||||||
|
**Independent Test**: Requests to legacy tenant-scoped endpoints return 404 and do not redirect.
|
||||||
|
|
||||||
|
### Tests for User Story 2
|
||||||
|
|
||||||
|
- [X] T017 [P] [US2] Extend `tests/Feature/Operations/LegacyRunRoutesNotFoundTest.php` to include `/admin/t/{tenant}/operations` and confirm it returns 404
|
||||||
|
|
||||||
|
### Implementation for User Story 2
|
||||||
|
|
||||||
|
- [X] T018 [US2] Remove legacy redirect shim route `/admin/t/{tenant:external_id}/operations` from `routes/web.php`
|
||||||
|
- [X] T019 [US2] Update `tests/Feature/078/TenantListRedirectTest.php` to assert `404 Not Found` (no redirect) for the legacy operations list URL
|
||||||
|
- [X] T020 [US2] Ensure the legacy-routes contract stays accurate by updating `specs/092-legacy-purge-final/contracts/legacy-routes.openapi.yaml` if the covered legacy endpoints list changes
|
||||||
|
|
||||||
|
**Checkpoint**: US2 complete — old bookmarks fail fast and deterministically (404).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: User Story 3 — Prevent reintroduction (Priority: P3)
|
||||||
|
|
||||||
|
**Goal**: Remove test shims and strengthen guards so legacy patterns cannot reappear.
|
||||||
|
|
||||||
|
**Independent Test**: Reintroduce a forbidden identifier in a non-excluded path and confirm guard tests fail; remove it and confirm suite passes.
|
||||||
|
|
||||||
|
### Tests for User Story 3
|
||||||
|
|
||||||
|
- [X] T021 [P] [US3] Update guard test patterns in `tests/Feature/Guards/NoLegacyRunsTest.php` to include `backupScheduleRunId`, `BackupScheduleRunDispatchedNotification`, and the legacy UI phrase “inventory sync runs”, while keeping exclusions aligned with the spec
|
||||||
|
|
||||||
|
### Implementation for User Story 3
|
||||||
|
|
||||||
|
- [X] T022 [US3] Remove legacy test shim require in `tests/Pest.php` (`require_once __DIR__.'/Support/LegacyModels/InventorySyncRun.php';`)
|
||||||
|
- [X] T023 [US3] Delete `tests/Support/LegacyModels/InventorySyncRun.php` and update all tests that import/use `App\Models\InventorySyncRun` to use canonical `App\Models\OperationRun` patterns instead (e.g., `tests/Feature/Inventory/InventorySyncButtonTest.php`, `tests/Feature/RunStartAuthorizationTest.php`)
|
||||||
|
- [X] T024 [US3] Update `tests/Feature/Guards/NoLegacyRunBackfillTest.php` to remove references to legacy shim types (InventorySyncRun/EntraGroupSyncRun/BackupScheduleRun) if they’re no longer meaningful after the purge
|
||||||
|
|
||||||
|
**Checkpoint**: US3 complete — reintroduction is blocked automatically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: Polish & Cross-Cutting Concerns
|
||||||
|
|
||||||
|
**Purpose**: Finish the purge with verification and repo hygiene.
|
||||||
|
|
||||||
|
- [X] T025 Run `vendor/bin/sail bin pint --dirty` and ensure formatting is clean
|
||||||
|
- [X] T026 Run the minimal full targeted suite for this change:
|
||||||
|
- `vendor/bin/sail artisan test --compact tests/Feature/Guards/NoLegacyRunsTest.php`
|
||||||
|
- `vendor/bin/sail artisan test --compact tests/Feature/Operations/LegacyRunRoutesNotFoundTest.php`
|
||||||
|
- `vendor/bin/sail artisan test --compact tests/Feature/078/TenantListRedirectTest.php`
|
||||||
|
- `vendor/bin/sail artisan test --compact tests/Feature/BackupScheduling`
|
||||||
|
- [X] T027 Confirm no migration files were modified (guardrail for FR-009)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies & Execution Order
|
||||||
|
|
||||||
|
- Phase 1 → Phase 2 is required.
|
||||||
|
- US1 (Phase 3) can start after Phase 2.
|
||||||
|
- US2 (Phase 4) can start after Phase 2 and is mostly independent of US1.
|
||||||
|
- US3 (Phase 5) can start after Phase 2, but some changes may be easier after US1 removes the primary app-level legacy artifacts.
|
||||||
|
- Deploy B task (T016) must be scheduled after the compatibility window for queued jobs (operational constraint), even if code is ready.
|
||||||
|
|
||||||
|
## Dependency Graph (User Stories)
|
||||||
|
|
||||||
|
```text
|
||||||
|
Foundation (Phase 2)
|
||||||
|
├─> US1 (P1): UI + staged job cleanup (Deploy A)
|
||||||
|
├─> US2 (P2): Remove redirects; legacy deep links 404
|
||||||
|
└─> US3 (P3): Remove test shims; strengthen guards
|
||||||
|
|
||||||
|
US1 (Deploy A) ──(compat window elapsed)──> US1 (Deploy B final purge)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parallel Execution Examples
|
||||||
|
|
||||||
|
### US1 parallelizable tasks
|
||||||
|
|
||||||
|
- T008 (notification removal), T009 (InventoryLanding removal), T010 (OperationRunLinks update), and T011 (exemption removal) can be done in parallel.
|
||||||
|
- T006 (Drift copy test) can be authored in parallel with T012.
|
||||||
|
|
||||||
|
### US2 parallelizable tasks
|
||||||
|
|
||||||
|
- T017 (extend 404 tests) can be done in parallel with T018/T019.
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
- MVP scope: US1 (Phase 3) + Deploy A (T014–T015) only.
|
||||||
|
- Then: US2 (Phase 4) to enforce 404 behavior.
|
||||||
|
- Then: US3 (Phase 5) to remove shims and lock in guards.
|
||||||
|
- Finally: Deploy B (T016) after the compatibility window.
|
||||||
@ -11,7 +11,7 @@ final class TenantListRedirectTest extends TestCase
|
|||||||
{
|
{
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
public function test_redirects_legacy_tenant_scoped_operations_list_url_to_canonical_operations_index(): void
|
public function test_returns_not_found_for_legacy_tenant_scoped_operations_list_url(): void
|
||||||
{
|
{
|
||||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||||
|
|
||||||
@ -20,7 +20,6 @@ public function test_redirects_legacy_tenant_scoped_operations_list_url_to_canon
|
|||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||||
->get('/admin/t/'.$tenant->external_id.'/operations')
|
->get('/admin/t/'.$tenant->external_id.'/operations')
|
||||||
->assertStatus(302)
|
->assertNotFound();
|
||||||
->assertRedirect(route('admin.operations.index'));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,7 +44,6 @@
|
|||||||
|
|
||||||
Bus::assertDispatched(RunBackupScheduleJob::class, function (RunBackupScheduleJob $job) use ($tenant): bool {
|
Bus::assertDispatched(RunBackupScheduleJob::class, function (RunBackupScheduleJob $job) use ($tenant): bool {
|
||||||
return $job->backupScheduleId !== null
|
return $job->backupScheduleId !== null
|
||||||
&& $job->backupScheduleRunId === 0
|
|
||||||
&& $job->operationRun?->tenant_id === $tenant->getKey()
|
&& $job->operationRun?->tenant_id === $tenant->getKey()
|
||||||
&& $job->operationRun?->type === 'backup_schedule_run';
|
&& $job->operationRun?->type === 'backup_schedule_run';
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Jobs\RunBackupScheduleJob;
|
||||||
|
|
||||||
|
it('serializes without the legacy backup schedule run id field', function (): void {
|
||||||
|
$job = new RunBackupScheduleJob(operationRun: null, backupScheduleId: 42);
|
||||||
|
|
||||||
|
expect(serialize($job))->not->toContain('backupScheduleRunId');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can deserialize legacy payloads that still contain backupScheduleRunId', function (): void {
|
||||||
|
$class = RunBackupScheduleJob::class;
|
||||||
|
$payload = sprintf(
|
||||||
|
'O:%d:"%s":3:{s:19:"backupScheduleRunId";i:0;s:12:"operationRun";N;s:16:"backupScheduleId";i:42;}',
|
||||||
|
strlen($class),
|
||||||
|
$class
|
||||||
|
);
|
||||||
|
|
||||||
|
$job = unserialize($payload, ['allowed_classes' => [RunBackupScheduleJob::class]]);
|
||||||
|
|
||||||
|
expect($job)->toBeInstanceOf(RunBackupScheduleJob::class)
|
||||||
|
->and($job->backupScheduleId)->toBe(42);
|
||||||
|
});
|
||||||
@ -75,7 +75,7 @@ public function createBackupSet($tenant, $policyIds, ?string $actorEmail = null,
|
|||||||
|
|
||||||
Cache::flush();
|
Cache::flush();
|
||||||
|
|
||||||
(new RunBackupScheduleJob(0, $operationRun, (int) $schedule->id))->handle(
|
(new RunBackupScheduleJob(operationRun: $operationRun, backupScheduleId: (int) $schedule->id))->handle(
|
||||||
app(PolicySyncService::class),
|
app(PolicySyncService::class),
|
||||||
app(BackupService::class),
|
app(BackupService::class),
|
||||||
app(PolicyTypeResolver::class),
|
app(PolicyTypeResolver::class),
|
||||||
@ -134,7 +134,7 @@ public function createBackupSet($tenant, $policyIds, ?string $actorEmail = null,
|
|||||||
|
|
||||||
Cache::flush();
|
Cache::flush();
|
||||||
|
|
||||||
(new RunBackupScheduleJob(0, $operationRun, (int) $schedule->id))->handle(
|
(new RunBackupScheduleJob(operationRun: $operationRun, backupScheduleId: (int) $schedule->id))->handle(
|
||||||
app(PolicySyncService::class),
|
app(PolicySyncService::class),
|
||||||
app(BackupService::class),
|
app(BackupService::class),
|
||||||
app(PolicyTypeResolver::class),
|
app(PolicyTypeResolver::class),
|
||||||
@ -195,7 +195,7 @@ public function createBackupSet($tenant, $policyIds, ?string $actorEmail = null,
|
|||||||
|
|
||||||
Cache::flush();
|
Cache::flush();
|
||||||
|
|
||||||
(new RunBackupScheduleJob(0, $operationRun, (int) $schedule->id))->handle(
|
(new RunBackupScheduleJob(operationRun: $operationRun, backupScheduleId: (int) $schedule->id))->handle(
|
||||||
app(PolicySyncService::class),
|
app(PolicySyncService::class),
|
||||||
app(BackupService::class),
|
app(BackupService::class),
|
||||||
app(PolicyTypeResolver::class),
|
app(PolicyTypeResolver::class),
|
||||||
@ -255,7 +255,7 @@ public function createBackupSet($tenant, $policyIds, ?string $actorEmail = null,
|
|||||||
$queueJob = \Mockery::mock(Job::class);
|
$queueJob = \Mockery::mock(Job::class);
|
||||||
$queueJob->shouldReceive('fail')->once();
|
$queueJob->shouldReceive('fail')->once();
|
||||||
|
|
||||||
$job = new RunBackupScheduleJob(0, null, (int) $schedule->id);
|
$job = new RunBackupScheduleJob(operationRun: null, backupScheduleId: (int) $schedule->id);
|
||||||
$job->setJob($queueJob);
|
$job->setJob($queueJob);
|
||||||
|
|
||||||
$job->handle(
|
$job->handle(
|
||||||
|
|||||||
@ -62,8 +62,7 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
Queue::assertPushed(RunBackupScheduleJob::class, function (RunBackupScheduleJob $job) use ($schedule, $operationRun): bool {
|
Queue::assertPushed(RunBackupScheduleJob::class, function (RunBackupScheduleJob $job) use ($schedule, $operationRun): bool {
|
||||||
return $job->backupScheduleRunId === 0
|
return $job->backupScheduleId === (int) $schedule->getKey()
|
||||||
&& $job->backupScheduleId === (int) $schedule->getKey()
|
|
||||||
&& $job->operationRun instanceof OperationRun
|
&& $job->operationRun instanceof OperationRun
|
||||||
&& $job->operationRun->is($operationRun);
|
&& $job->operationRun->is($operationRun);
|
||||||
});
|
});
|
||||||
@ -160,8 +159,7 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
Queue::assertPushed(RunBackupScheduleJob::class, function (RunBackupScheduleJob $job) use ($schedule, $operationRun): bool {
|
Queue::assertPushed(RunBackupScheduleJob::class, function (RunBackupScheduleJob $job) use ($schedule, $operationRun): bool {
|
||||||
return $job->backupScheduleRunId === 0
|
return $job->backupScheduleId === (int) $schedule->getKey()
|
||||||
&& $job->backupScheduleId === (int) $schedule->getKey()
|
|
||||||
&& $job->operationRun instanceof OperationRun
|
&& $job->operationRun instanceof OperationRun
|
||||||
&& $job->operationRun->is($operationRun);
|
&& $job->operationRun->is($operationRun);
|
||||||
});
|
});
|
||||||
|
|||||||
16
tests/Feature/Drift/DriftLandingCopyTest.php
Normal file
16
tests/Feature/Drift/DriftLandingCopyTest.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Filament\Pages\DriftLanding;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
|
||||||
|
it('uses operation runs wording on the drift landing page', function (): void {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'manager');
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
|
Livewire::test(DriftLanding::class)
|
||||||
|
->assertSee('operation runs')
|
||||||
|
->assertDontSee('inventory sync runs');
|
||||||
|
});
|
||||||
@ -12,7 +12,6 @@
|
|||||||
|
|
||||||
$forbiddenPatterns = [
|
$forbiddenPatterns = [
|
||||||
'/insert\s+into\s+["`]?operation_runs["`]?.{0,1200}\b(?:inventory_sync_runs|entra_group_sync_runs|backup_schedule_runs)\b/is',
|
'/insert\s+into\s+["`]?operation_runs["`]?.{0,1200}\b(?:inventory_sync_runs|entra_group_sync_runs|backup_schedule_runs)\b/is',
|
||||||
'/\bOperationRun::query\(\)->(?:create|insert|upsert|updateOrCreate)\(.{0,1200}\b(?:InventorySyncRun|EntraGroupSyncRun|BackupScheduleRun)\b/is',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/** @var Collection<int, string> $files */
|
/** @var Collection<int, string> $files */
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
$root.'/storage',
|
$root.'/storage',
|
||||||
$root.'/specs',
|
$root.'/specs',
|
||||||
$root.'/spechistory',
|
$root.'/spechistory',
|
||||||
|
$root.'/docs',
|
||||||
$root.'/references',
|
$root.'/references',
|
||||||
$root.'/bootstrap/cache',
|
$root.'/bootstrap/cache',
|
||||||
$root.'/database/migrations',
|
$root.'/database/migrations',
|
||||||
@ -33,8 +34,17 @@
|
|||||||
'/\\bBackupScheduleRun\\b/',
|
'/\\bBackupScheduleRun\\b/',
|
||||||
'/\\bbackup_schedule_runs\\b/',
|
'/\\bbackup_schedule_runs\\b/',
|
||||||
'/\\bBackupScheduleRunsRelationManager\\b/',
|
'/\\bBackupScheduleRunsRelationManager\\b/',
|
||||||
|
'/\\bbackupScheduleRunId\\b/',
|
||||||
|
'/\\bBackupScheduleRunDispatchedNotification\\b/',
|
||||||
|
'/inventory sync runs/i',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$allowedExceptions = [
|
||||||
|
'/\\bbackupScheduleRunId\\b/' => [
|
||||||
|
'app/Jobs/RunBackupScheduleJob.php',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
/** @var Collection<int, string> $files */
|
/** @var Collection<int, string> $files */
|
||||||
$files = collect($directories)
|
$files = collect($directories)
|
||||||
->filter(fn (string $dir): bool => is_dir($dir))
|
->filter(fn (string $dir): bool => is_dir($dir))
|
||||||
@ -89,6 +99,11 @@
|
|||||||
foreach ($lines as $index => $line) {
|
foreach ($lines as $index => $line) {
|
||||||
if (preg_match($pattern, $line)) {
|
if (preg_match($pattern, $line)) {
|
||||||
$relative = str_replace($root.'/', '', $path);
|
$relative = str_replace($root.'/', '', $path);
|
||||||
|
|
||||||
|
if (array_key_exists($pattern, $allowedExceptions) && in_array($relative, $allowedExceptions[$pattern], true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$hits[] = $relative.':'.($index + 1).' -> '.trim($line);
|
$hits[] = $relative.':'.($index + 1).' -> '.trim($line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
use App\Filament\Resources\InventoryItemResource\Pages\ListInventoryItems;
|
use App\Filament\Resources\InventoryItemResource\Pages\ListInventoryItems;
|
||||||
use App\Jobs\RunInventorySyncJob;
|
use App\Jobs\RunInventorySyncJob;
|
||||||
use App\Livewire\BulkOperationProgress;
|
use App\Livewire\BulkOperationProgress;
|
||||||
use App\Models\InventorySyncRun;
|
|
||||||
use App\Models\OperationRun;
|
use App\Models\OperationRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Services\Inventory\InventorySyncService;
|
use App\Services\Inventory\InventorySyncService;
|
||||||
@ -31,8 +30,6 @@
|
|||||||
|
|
||||||
Queue::assertPushed(RunInventorySyncJob::class);
|
Queue::assertPushed(RunInventorySyncJob::class);
|
||||||
|
|
||||||
expect(InventorySyncRun::query()->where('tenant_id', $tenant->id)->exists())->toBeFalse();
|
|
||||||
|
|
||||||
$opRun = OperationRun::query()
|
$opRun = OperationRun::query()
|
||||||
->where('tenant_id', $tenant->id)
|
->where('tenant_id', $tenant->id)
|
||||||
->where('user_id', $user->id)
|
->where('user_id', $user->id)
|
||||||
@ -69,8 +66,6 @@
|
|||||||
|
|
||||||
Queue::assertPushed(RunInventorySyncJob::class);
|
Queue::assertPushed(RunInventorySyncJob::class);
|
||||||
|
|
||||||
expect(InventorySyncRun::query()->where('tenant_id', $tenant->id)->exists())->toBeFalse();
|
|
||||||
|
|
||||||
$opRun = OperationRun::query()
|
$opRun = OperationRun::query()
|
||||||
->where('tenant_id', $tenant->id)
|
->where('tenant_id', $tenant->id)
|
||||||
->where('type', 'inventory_sync')
|
->where('type', 'inventory_sync')
|
||||||
@ -102,8 +97,6 @@
|
|||||||
])
|
])
|
||||||
->assertHasNoActionErrors();
|
->assertHasNoActionErrors();
|
||||||
|
|
||||||
expect(InventorySyncRun::query()->where('tenant_id', $tenant->id)->exists())->toBeFalse();
|
|
||||||
|
|
||||||
$opRun = OperationRun::query()
|
$opRun = OperationRun::query()
|
||||||
->where('tenant_id', $tenant->id)
|
->where('tenant_id', $tenant->id)
|
||||||
->where('type', 'inventory_sync')
|
->where('type', 'inventory_sync')
|
||||||
@ -135,8 +128,6 @@
|
|||||||
->callMountedAction()
|
->callMountedAction()
|
||||||
->assertHasNoActionErrors();
|
->assertHasNoActionErrors();
|
||||||
|
|
||||||
expect(InventorySyncRun::query()->where('tenant_id', $tenant->id)->exists())->toBeFalse();
|
|
||||||
|
|
||||||
$opRun = OperationRun::query()
|
$opRun = OperationRun::query()
|
||||||
->where('tenant_id', $tenant->id)
|
->where('tenant_id', $tenant->id)
|
||||||
->where('type', 'inventory_sync')
|
->where('type', 'inventory_sync')
|
||||||
@ -168,8 +159,6 @@
|
|||||||
])
|
])
|
||||||
->assertHasNoActionErrors();
|
->assertHasNoActionErrors();
|
||||||
|
|
||||||
expect(InventorySyncRun::query()->where('tenant_id', $tenant->id)->exists())->toBeFalse();
|
|
||||||
|
|
||||||
$opRun = OperationRun::query()
|
$opRun = OperationRun::query()
|
||||||
->where('tenant_id', $tenant->id)
|
->where('tenant_id', $tenant->id)
|
||||||
->where('type', 'inventory_sync')
|
->where('type', 'inventory_sync')
|
||||||
@ -199,7 +188,7 @@
|
|||||||
|
|
||||||
Queue::assertNothingPushed();
|
Queue::assertNothingPushed();
|
||||||
|
|
||||||
expect(InventorySyncRun::query()->where('tenant_id', $tenantB->id)->exists())->toBeFalse();
|
expect(OperationRun::query()->where('tenant_id', $tenantB->id)->where('type', 'inventory_sync')->exists())->toBeFalse();
|
||||||
expect(OperationRun::query()->where('tenant_id', $tenantB->id)->exists())->toBeFalse();
|
expect(OperationRun::query()->where('tenant_id', $tenantB->id)->exists())->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -236,7 +225,6 @@
|
|||||||
->callAction('run_inventory_sync', data: ['policy_types' => $computed['selection']['policy_types']]);
|
->callAction('run_inventory_sync', data: ['policy_types' => $computed['selection']['policy_types']]);
|
||||||
|
|
||||||
Queue::assertNothingPushed();
|
Queue::assertNothingPushed();
|
||||||
expect(InventorySyncRun::query()->where('tenant_id', $tenant->id)->count())->toBe(0);
|
|
||||||
expect(OperationRun::query()->where('tenant_id', $tenant->id)->where('type', 'inventory_sync')->count())->toBe(1);
|
expect(OperationRun::query()->where('tenant_id', $tenant->id)->where('type', 'inventory_sync')->count())->toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -252,5 +240,5 @@
|
|||||||
->assertActionDisabled('run_inventory_sync');
|
->assertActionDisabled('run_inventory_sync');
|
||||||
|
|
||||||
Queue::assertNothingPushed();
|
Queue::assertNothingPushed();
|
||||||
expect(InventorySyncRun::query()->where('tenant_id', $tenant->id)->exists())->toBeFalse();
|
expect(OperationRun::query()->where('tenant_id', $tenant->id)->where('type', 'inventory_sync')->exists())->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||||
|
|
||||||
$legacyUrls = [
|
$legacyUrls = [
|
||||||
|
"/admin/t/{$tenant->external_id}/operations",
|
||||||
"/admin/t/{$tenant->external_id}/inventory-sync-runs",
|
"/admin/t/{$tenant->external_id}/inventory-sync-runs",
|
||||||
"/admin/t/{$tenant->external_id}/inventory-sync-runs/123",
|
"/admin/t/{$tenant->external_id}/inventory-sync-runs/123",
|
||||||
"/admin/t/{$tenant->external_id}/entra-group-sync-runs",
|
"/admin/t/{$tenant->external_id}/entra-group-sync-runs",
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
use App\Jobs\EntraGroupSyncJob;
|
use App\Jobs\EntraGroupSyncJob;
|
||||||
use App\Jobs\RunBackupScheduleJob;
|
use App\Jobs\RunBackupScheduleJob;
|
||||||
use App\Models\BackupSchedule;
|
use App\Models\BackupSchedule;
|
||||||
use App\Models\InventorySyncRun;
|
|
||||||
use App\Models\OperationRun;
|
use App\Models\OperationRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Services\Inventory\InventorySyncService;
|
use App\Services\Inventory\InventorySyncService;
|
||||||
@ -34,7 +33,7 @@
|
|||||||
|
|
||||||
Queue::assertNothingPushed();
|
Queue::assertNothingPushed();
|
||||||
|
|
||||||
expect(InventorySyncRun::query()->where('tenant_id', $tenantB->id)->exists())->toBeFalse();
|
expect(OperationRun::query()->where('tenant_id', $tenantB->id)->where('type', 'inventory_sync')->exists())->toBeFalse();
|
||||||
expect(OperationRun::query()->where('tenant_id', $tenantB->id)->exists())->toBeFalse();
|
expect(OperationRun::query()->where('tenant_id', $tenantB->id)->exists())->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -54,7 +53,7 @@
|
|||||||
->callAction('run_inventory_sync', data: ['tenant_id' => $tenant->getKey(), 'policy_types' => $allTypes]);
|
->callAction('run_inventory_sync', data: ['tenant_id' => $tenant->getKey(), 'policy_types' => $allTypes]);
|
||||||
|
|
||||||
Queue::assertNothingPushed();
|
Queue::assertNothingPushed();
|
||||||
expect(InventorySyncRun::query()->where('tenant_id', $tenant->id)->exists())->toBeFalse();
|
expect(OperationRun::query()->where('tenant_id', $tenant->id)->where('type', 'inventory_sync')->exists())->toBeFalse();
|
||||||
expect(OperationRun::query()->where('tenant_id', $tenant->id)->exists())->toBeFalse();
|
expect(OperationRun::query()->where('tenant_id', $tenant->id)->exists())->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
require_once __DIR__.'/Support/LegacyModels/InventorySyncRun.php';
|
|
||||||
|
|
||||||
use App\Models\ProviderConnection;
|
use App\Models\ProviderConnection;
|
||||||
use App\Models\ProviderCredential;
|
use App\Models\ProviderCredential;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
|
|||||||
@ -1,167 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Database\Factories\OperationRunFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
|
||||||
use Illuminate\Support\Arr;
|
|
||||||
|
|
||||||
class InventorySyncRun extends OperationRun
|
|
||||||
{
|
|
||||||
protected $table = 'operation_runs';
|
|
||||||
|
|
||||||
public const STATUS_PENDING = 'pending';
|
|
||||||
|
|
||||||
public const STATUS_RUNNING = 'running';
|
|
||||||
|
|
||||||
public const STATUS_SUCCESS = 'success';
|
|
||||||
|
|
||||||
public const STATUS_PARTIAL = 'partial';
|
|
||||||
|
|
||||||
public const STATUS_FAILED = 'failed';
|
|
||||||
|
|
||||||
public const STATUS_SKIPPED = 'skipped';
|
|
||||||
|
|
||||||
protected static function newFactory(): Factory
|
|
||||||
{
|
|
||||||
return (new class extends OperationRunFactory
|
|
||||||
{
|
|
||||||
protected $model = InventorySyncRun::class;
|
|
||||||
})->state([
|
|
||||||
'type' => 'inventory_sync',
|
|
||||||
'status' => self::STATUS_SUCCESS,
|
|
||||||
'outcome' => 'succeeded',
|
|
||||||
'context' => [
|
|
||||||
'selection_hash' => hash('sha256', 'inventory-sync-selection-default'),
|
|
||||||
'policy_types' => ['deviceConfiguration'],
|
|
||||||
'categories' => [],
|
|
||||||
'include_foundations' => false,
|
|
||||||
'include_dependencies' => false,
|
|
||||||
],
|
|
||||||
'started_at' => now()->subMinute(),
|
|
||||||
'completed_at' => now(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static function booted(): void
|
|
||||||
{
|
|
||||||
static::addGlobalScope('inventory_sync_type', function (Builder $builder): void {
|
|
||||||
$builder->where('type', 'inventory_sync');
|
|
||||||
});
|
|
||||||
|
|
||||||
static::addGlobalScope('legacy_statuses_only', function (Builder $builder): void {
|
|
||||||
$builder->whereIn('status', [
|
|
||||||
self::STATUS_PENDING,
|
|
||||||
self::STATUS_SUCCESS,
|
|
||||||
self::STATUS_PARTIAL,
|
|
||||||
self::STATUS_FAILED,
|
|
||||||
self::STATUS_SKIPPED,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
static::saving(function (self $run): void {
|
|
||||||
if (! filled($run->type)) {
|
|
||||||
$run->type = 'inventory_sync';
|
|
||||||
}
|
|
||||||
|
|
||||||
$legacyStatus = (string) $run->status;
|
|
||||||
$normalizedStatus = match ($legacyStatus) {
|
|
||||||
self::STATUS_PENDING => 'queued',
|
|
||||||
self::STATUS_RUNNING => 'running',
|
|
||||||
self::STATUS_SUCCESS,
|
|
||||||
self::STATUS_PARTIAL,
|
|
||||||
self::STATUS_FAILED,
|
|
||||||
self::STATUS_SKIPPED => 'completed',
|
|
||||||
default => $legacyStatus,
|
|
||||||
};
|
|
||||||
|
|
||||||
if ($normalizedStatus !== '') {
|
|
||||||
$run->status = $normalizedStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
$context = is_array($run->context) ? $run->context : [];
|
|
||||||
|
|
||||||
if (! array_key_exists('selection_hash', $context) || ! is_string($context['selection_hash']) || $context['selection_hash'] === '') {
|
|
||||||
$context['selection_hash'] = hash('sha256', (string) $run->getKey().':selection');
|
|
||||||
}
|
|
||||||
|
|
||||||
$run->context = $context;
|
|
||||||
|
|
||||||
$run->outcome = match ($legacyStatus) {
|
|
||||||
self::STATUS_SUCCESS => 'succeeded',
|
|
||||||
self::STATUS_PARTIAL => 'partially_succeeded',
|
|
||||||
self::STATUS_SKIPPED => 'blocked',
|
|
||||||
self::STATUS_RUNNING, self::STATUS_PENDING => 'pending',
|
|
||||||
default => 'failed',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (in_array($legacyStatus, [self::STATUS_SUCCESS, self::STATUS_PARTIAL, self::STATUS_FAILED, self::STATUS_SKIPPED], true)
|
|
||||||
&& $run->completed_at === null
|
|
||||||
) {
|
|
||||||
$run->completed_at = now();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSelectionHashAttribute(): ?string
|
|
||||||
{
|
|
||||||
$context = is_array($this->context) ? $this->context : [];
|
|
||||||
|
|
||||||
return isset($context['selection_hash']) && is_string($context['selection_hash'])
|
|
||||||
? $context['selection_hash']
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setSelectionHashAttribute(?string $value): void
|
|
||||||
{
|
|
||||||
$context = is_array($this->context) ? $this->context : [];
|
|
||||||
$context['selection_hash'] = $value;
|
|
||||||
|
|
||||||
$this->context = $context;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function getSelectionPayloadAttribute(): array
|
|
||||||
{
|
|
||||||
$context = is_array($this->context) ? $this->context : [];
|
|
||||||
|
|
||||||
return Arr::only($context, [
|
|
||||||
'policy_types',
|
|
||||||
'categories',
|
|
||||||
'include_foundations',
|
|
||||||
'include_dependencies',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed>|null $value
|
|
||||||
*/
|
|
||||||
public function setSelectionPayloadAttribute(?array $value): void
|
|
||||||
{
|
|
||||||
$context = is_array($this->context) ? $this->context : [];
|
|
||||||
|
|
||||||
if (is_array($value)) {
|
|
||||||
$context = array_merge($context, Arr::only($value, [
|
|
||||||
'policy_types',
|
|
||||||
'categories',
|
|
||||||
'include_foundations',
|
|
||||||
'include_dependencies',
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->context = $context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getFinishedAtAttribute(): mixed
|
|
||||||
{
|
|
||||||
return $this->completed_at;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setFinishedAtAttribute(mixed $value): void
|
|
||||||
{
|
|
||||||
$this->completed_at = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user