TenantAtlas/apps/platform/app/Models/Policy.php
ahmido feeaadd5ad feat: add provider-missing policy visibility and restore continuity (#316)
## Summary
- separate provider-missing policy presence from local ignore semantics by introducing `missing_from_provider_at`
- update policy, backup, and restore surfaces so current-state capture stays honest while historical restore continuity remains available
- add focused sync, Filament, backup, restore, localization, and badge coverage for the new provider-missing behavior

## Scope
- policy sync and model truth
- policy resource visibility, badges, labels, and action gating
- backup/export eligibility and restore continuity messaging
- spec 261 artifacts and focused tests

## Validation
- feature-specific Pest coverage is included in the branch
- validation was not re-run as part of this commit/push/PR handoff

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #316
2026-05-01 20:18:27 +00:00

132 lines
3.6 KiB
PHP

<?php
namespace App\Models;
use App\Support\Concerns\DerivesWorkspaceIdFromTenant;
use App\Support\Concerns\InteractsWithODataTypes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Policy extends Model
{
use DerivesWorkspaceIdFromTenant;
use HasFactory;
use InteractsWithODataTypes;
public const VISIBILITY_ACTIVE = 'active';
public const VISIBILITY_IGNORED_LOCALLY = 'ignored_locally';
public const VISIBILITY_PROVIDER_MISSING = 'provider_missing';
public const VISIBILITY_IGNORED_LOCALLY_PROVIDER_MISSING = 'ignored_locally_provider_missing';
protected $guarded = [];
protected $casts = [
'metadata' => 'array',
'last_synced_at' => 'datetime',
'ignored_at' => 'datetime',
'missing_from_provider_at' => 'datetime',
];
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function versions(): HasMany
{
return $this->hasMany(PolicyVersion::class);
}
public function backupItems(): HasMany
{
return $this->hasMany(BackupItem::class);
}
public function scopeActive(Builder $query): Builder
{
return $query
->whereNull('ignored_at')
->whereNull('missing_from_provider_at');
}
public function scopeIgnored(Builder $query): Builder
{
return $query->whereNotNull('ignored_at');
}
public function scopeProviderMissing(Builder $query): Builder
{
return $query->whereNotNull('missing_from_provider_at');
}
public function scopeCurrentBackupEligible(Builder $query): Builder
{
return $query
->whereNull('ignored_at')
->whereNull('missing_from_provider_at');
}
public function isIgnoredLocally(): bool
{
return $this->ignored_at !== null;
}
public function isProviderMissing(): bool
{
return $this->missing_from_provider_at !== null;
}
public function visibilityState(): string
{
return match (true) {
$this->isIgnoredLocally() && $this->isProviderMissing() => self::VISIBILITY_IGNORED_LOCALLY_PROVIDER_MISSING,
$this->isIgnoredLocally() => self::VISIBILITY_IGNORED_LOCALLY,
$this->isProviderMissing() => self::VISIBILITY_PROVIDER_MISSING,
default => self::VISIBILITY_ACTIVE,
};
}
public function isCurrentBackupEligible(): bool
{
return ! $this->isIgnoredLocally() && ! $this->isProviderMissing();
}
public function currentBackupBlockedReason(): ?string
{
if ($this->isProviderMissing()) {
return self::VISIBILITY_PROVIDER_MISSING;
}
if ($this->isIgnoredLocally()) {
return self::VISIBILITY_IGNORED_LOCALLY;
}
return null;
}
public function currentBackupBlockedReasonLabel(): ?string
{
return match ($this->currentBackupBlockedReason()) {
self::VISIBILITY_PROVIDER_MISSING => 'Provider missing - current provider-backed capture is unavailable.',
self::VISIBILITY_IGNORED_LOCALLY => 'Ignored locally - restore local visibility before fresh capture.',
default => null,
};
}
public function ignore(): void
{
$this->update(['ignored_at' => now()]);
}
public function unignore(): void
{
$this->update(['ignored_at' => null]);
}
}