2026-08-24 21:06:11 +03:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Modules\Core\Privacy\Models;
|
|
|
|
|
|
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
2026-08-24 21:41:23 +03:00
|
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
|
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
2026-08-24 21:06:11 +03:00
|
|
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|
|
|
|
use Lunar\Models\Customer;
|
2026-09-16 01:21:22 +03:00
|
|
|
use Modules\Core\Privacy\Enums\ErasureRequestStatus;
|
2026-08-24 21:06:11 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A pending, cancelled, or completed right-of-erasure request — the grace-period
|
|
|
|
|
* record between "subject/staff asked for this" and "providers actually erased
|
2026-09-16 01:21:22 +03:00
|
|
|
* their data" (see Modules\Core\Privacy\Services\PrivacyService, which creates/processes
|
2026-08-24 21:06:11 +03:00
|
|
|
* these).
|
|
|
|
|
*
|
|
|
|
|
* `subject` is polymorphic — either a Lunar Customer (business account) or a User
|
|
|
|
|
* (individual), never both. See docs/privacy.md "User-scope vs Customer-scope" for
|
|
|
|
|
* why these are two genuinely different operations with different blast radii,
|
|
|
|
|
* not one "erase this customer and cascade to their users" flow.
|
|
|
|
|
*
|
|
|
|
|
* `requestedBy` is separately polymorphic (the subject themselves, self-service,
|
|
|
|
|
* or Staff acting on their behalf), stored as plain type+id columns rather than
|
|
|
|
|
* morphs() since it's always exactly one of those two concrete actor types.
|
|
|
|
|
*/
|
|
|
|
|
class DataErasureRequest extends Model
|
|
|
|
|
{
|
|
|
|
|
protected $guarded = [];
|
|
|
|
|
|
|
|
|
|
protected $casts = [
|
|
|
|
|
'status' => ErasureRequestStatus::class,
|
|
|
|
|
'scheduled_for' => 'datetime',
|
|
|
|
|
'cancelled_at' => 'datetime',
|
|
|
|
|
'completed_at' => 'datetime',
|
|
|
|
|
'report' => 'array',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
public function subject(): MorphTo
|
|
|
|
|
{
|
|
|
|
|
return $this->morphTo(__FUNCTION__, 'subject_type', 'subject_id');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function requestedBy(): MorphTo
|
|
|
|
|
{
|
|
|
|
|
return $this->morphTo(__FUNCTION__, 'requested_by_type', 'requested_by_id');
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-24 21:41:23 +03:00
|
|
|
/**
|
|
|
|
|
* The User erasure request that caused this one to be auto-created, if any —
|
|
|
|
|
* see Modules\Core\Privacy\Listeners\CascadeCustomerErasureListener.
|
|
|
|
|
*/
|
|
|
|
|
public function causedBy(): BelongsTo
|
|
|
|
|
{
|
|
|
|
|
return $this->belongsTo(self::class, 'caused_by_request_id');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Every Customer erasure request THIS request caused (see causedBy()) —
|
|
|
|
|
* used by CancelErasureOnLoginListener to revert exactly the cascade this
|
|
|
|
|
* User's own cancellation should undo.
|
|
|
|
|
*/
|
|
|
|
|
public function caused(): HasMany
|
|
|
|
|
{
|
|
|
|
|
return $this->hasMany(self::class, 'caused_by_request_id');
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-24 21:06:11 +03:00
|
|
|
public function isForCustomer(): bool
|
|
|
|
|
{
|
|
|
|
|
return $this->subject_type === (new Customer)->getMorphClass();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function isPending(): bool
|
|
|
|
|
{
|
|
|
|
|
return $this->status === ErasureRequestStatus::Pending;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function isDue(): bool
|
|
|
|
|
{
|
|
|
|
|
return $this->isPending() && $this->scheduled_for->isPast();
|
|
|
|
|
}
|
2026-08-25 09:33:03 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A human-readable label for whichever record `$morphable` resolves to
|
|
|
|
|
* (Customer, User, or Staff — the three concrete types that appear across
|
|
|
|
|
* subject/requestedBy), since each uses a different name field and there's
|
|
|
|
|
* no shared interface for it. Falls back to "#id" if the record is gone
|
|
|
|
|
* (e.g. a Customer erased since this request completed) or the relation is
|
|
|
|
|
* simply empty.
|
|
|
|
|
*/
|
|
|
|
|
public static function displayNameFor(mixed $morphable): string
|
|
|
|
|
{
|
|
|
|
|
if (! $morphable) {
|
|
|
|
|
return '—';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return match (true) {
|
|
|
|
|
isset($morphable->full_name) => $morphable->full_name,
|
|
|
|
|
isset($morphable->name) => $morphable->name,
|
|
|
|
|
isset($morphable->first_name) => trim("{$morphable->first_name} {$morphable->last_name}"),
|
|
|
|
|
default => "#{$morphable->getKey()}",
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-08-24 21:06:11 +03:00
|
|
|
}
|