TenantAtlas/tests/Support/LegacyModels/InventorySyncRun.php
ahmido d6e7de597a feat(spec-087): remove legacy runs (#106)
Implements Spec 087: Legacy Runs Removal (rigorous).

### What changed
- Canonicalized run history: **`operation_runs` is the only run system** for inventory sync, Entra group sync, backup schedule execution/retention/purge.
- Removed legacy UI surfaces (Filament Resources / relation managers) for legacy run models.
- Legacy run URLs now return **404** (no redirects), with RBAC semantics preserved (404 vs 403 as specified).
- Canonicalized affected `operation_runs.type` values (dotted → underscore) via migration.
- Drift + inventory references now point to canonical operation runs; includes backfills and then drops legacy FK columns.
- Drops legacy run tables after cutover.
- Added regression guards to prevent reintroducing legacy run tokens or “backfilling” canonical runs from legacy tables.

### Migrations
- `2026_02_12_000001..000006_*` canonicalize types, add/backfill operation_run_id references, drop legacy columns, and drop legacy run tables.

### Tests
Focused pack for this spec passed:
- `tests/Feature/Guards/NoLegacyRunsTest.php`
- `tests/Feature/Guards/NoLegacyRunBackfillTest.php`
- `tests/Feature/Operations/LegacyRunRoutesNotFoundTest.php`
- `tests/Feature/Monitoring/MonitoringOperationsTest.php`
- `tests/Feature/Jobs/RunInventorySyncJobTest.php`

### Notes / impact
- Destructive cleanup is handled via migrations (drops legacy tables) after code cutover; deploy should run migrations in the same release.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #106
2026-02-12 12:40:51 +00:00

158 lines
4.7 KiB
PHP

<?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::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;
}
}