Feature: Handling Cases for User to Customer Relationships
This commit handles a case where customer data are "dead-data" menaing there is no way of erasure for them, which makes the app non-compliant
This commit is contained in:
@@ -4,9 +4,11 @@ namespace Modules\Core\Privacy;
|
||||
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Lunar\Base\LunarUser;
|
||||
use Lunar\Models\Customer;
|
||||
use Modules\Core\Auth\Models\Staff;
|
||||
use Modules\Core\Privacy\Events\UserErasureRequested;
|
||||
use Modules\Core\Privacy\Jobs\ExportDataSubjectJob;
|
||||
use Modules\Core\Privacy\Models\DataErasureRequest;
|
||||
use Modules\Core\Privacy\Models\DataExportRequest;
|
||||
@@ -80,11 +82,21 @@ class PrivacyService
|
||||
* privacy:process-erasure-requests picks this up once scheduled_for has
|
||||
* passed, unless cancelErasure() is called first.
|
||||
*
|
||||
* $requestedBy is either the Customer themselves (self-service deletion) or a
|
||||
* Staff member acting on their behalf.
|
||||
* $requestedBy is the Customer themselves (self-service deletion), a Staff
|
||||
* member acting on their behalf, or a User — the User case is for
|
||||
* Modules\Core\Privacy\Listeners\CascadeCustomerErasureListener, where erasing
|
||||
* a User leaves a Customer with no remaining user: the User is a real,
|
||||
* meaningful "who caused this," even though they didn't directly request the
|
||||
* Customer's own erasure. $causedByRequestId links a cascade-created request
|
||||
* back to the User erasure request that triggered it, so
|
||||
* CancelErasureOnLoginListener can revert exactly that cascade on login,
|
||||
* without touching an unrelated, independently-requested Customer erasure.
|
||||
*/
|
||||
public function requestErasureForCustomer(Customer $customer, Customer|Staff $requestedBy): DataErasureRequest
|
||||
{
|
||||
public function requestErasureForCustomer(
|
||||
Customer $customer,
|
||||
Customer|Staff|(Authenticatable&LunarUser) $requestedBy,
|
||||
?int $causedByRequestId = null,
|
||||
): DataErasureRequest {
|
||||
return DataErasureRequest::create([
|
||||
'subject_type' => $customer->getMorphClass(),
|
||||
'subject_id' => $customer->id,
|
||||
@@ -93,6 +105,7 @@ class PrivacyService
|
||||
'requested_by_id' => $requestedBy->getKey(),
|
||||
'status' => ErasureRequestStatus::Pending,
|
||||
'scheduled_for' => now()->addDays(config('core.privacy.grace_period_days', 30)),
|
||||
'caused_by_request_id' => $causedByRequestId,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -117,6 +130,13 @@ class PrivacyService
|
||||
|
||||
$this->setUserDeactivated($user->id, true);
|
||||
|
||||
// CascadeCustomerErasureListener implements ShouldQueue, so this just
|
||||
// enqueues a job rather than running inline — no transaction wrapping
|
||||
// needed here, since the cascade check happens as an independent,
|
||||
// separately-retryable unit of work after this request is already
|
||||
// committed, not as part of this same call.
|
||||
Event::dispatch(new UserErasureRequested($request));
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
@@ -148,6 +168,15 @@ class PrivacyService
|
||||
/**
|
||||
* Erases a User's data right now, bypassing the grace period entirely.
|
||||
* Staff-only by construction — see requestImmediateErasureForCustomer().
|
||||
*
|
||||
* Still fires UserErasureRequested — and deliberately BEFORE completeErasure()
|
||||
* runs, not after — so Modules\Core\Privacy\Listeners\
|
||||
* CascadeCustomerErasureListener sees the User still linked to their Customers
|
||||
* (completeErasure() -> CustomerDataProvider::eraseForUser() is what detaches
|
||||
* the pivot). The User's own erasure is immediate, but any Customer left
|
||||
* orphaned by it still gets a normal grace-period erasure request, not an
|
||||
* immediate one — an orphaned Customer isn't itself the subject of the
|
||||
* original urgent request.
|
||||
*/
|
||||
public function requestImmediateErasureForUser(Authenticatable&LunarUser $user, Staff $requestedBy): ErasureReport
|
||||
{
|
||||
@@ -163,6 +192,16 @@ class PrivacyService
|
||||
|
||||
$this->setUserDeactivated($user->id, true);
|
||||
|
||||
// Queued (see requestErasureForUser()) — the cascade job may run before
|
||||
// or after completeErasure() below detaches the pivot. Either is fine:
|
||||
// CascadeCustomerErasureListener re-reads $user->customers fresh when it
|
||||
// runs, so it only cascades if this User is still linked at that point.
|
||||
// If completeErasure() detaches first, the queued job simply finds no
|
||||
// Customers left to check and no-ops — never a wrong cascade, at worst a
|
||||
// missed one on a race that immediate (staff-triggered, rare) erasure
|
||||
// doesn't need to guard against as tightly as the grace-period path.
|
||||
Event::dispatch(new UserErasureRequested($request));
|
||||
|
||||
return $this->completeErasure($request);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user