149 lines
6.4 KiB
PHP
149 lines
6.4 KiB
PHP
<?php
|
|
|
|
namespace Modules\Core\Logging\Privacy;
|
|
|
|
use Lunar\Models\Address;
|
|
use Lunar\Models\Cart;
|
|
use Lunar\Models\CartAddress;
|
|
use Lunar\Models\Customer;
|
|
use Lunar\Models\Order;
|
|
use Lunar\Models\OrderAddress;
|
|
use Lunar\Models\Transaction;
|
|
use Modules\Core\Privacy\Contracts\PersonalDataProvider;
|
|
use Modules\Core\Privacy\DTOs\CustomerSubject;
|
|
use Modules\Core\Privacy\Enums\ErasureOutcome;
|
|
use Modules\Core\Privacy\DTOs\ProviderErasureResult;
|
|
use Modules\Core\Privacy\DTOs\ProviderExportResult;
|
|
use Modules\Core\Privacy\DTOs\UserSubject;
|
|
use Spatie\Activitylog\Models\Activity;
|
|
|
|
/**
|
|
* Spatie's own activity_log table (Modules\Core\Logging\ActivityLogService,
|
|
* plus several Lunar models' native `use LogsActivity` — Customer,
|
|
* CartAddress, OrderAddress, Transaction) durably retains a full snapshot
|
|
* of whatever it logged in `properties` (created/updated/deleted
|
|
* attributes, including a before/after diff on update), completely
|
|
* independent of the real row it describes. Erasing/pseudonymizing
|
|
* Customer/Address/CartAddress/OrderAddress/Transaction elsewhere (see
|
|
* Customer\Privacy\CustomerDataProvider, Customer\Privacy\
|
|
* AddressDataProvider, Cart\Privacy\CartDataProvider, Order\Privacy\
|
|
* OrderDataProvider, Payment\Privacy\PaymentDataProvider) does nothing to
|
|
* this table — a full copy of the old PII survives here regardless.
|
|
*
|
|
* Redacts by SUBJECT only, never by `causer_id` — the causer is "who did
|
|
* this," not PII content, and erasing it would erode the audit trail's own
|
|
* purpose (see this provider's own eraseForUser(), which is a deliberate
|
|
* no-op). Genuinely Customer-scope only: every subject type here
|
|
* (Customer, Address, CartAddress, OrderAddress, Transaction) resolves to
|
|
* a business account via its own chain (Address/Customer directly;
|
|
* CartAddress via cart_id -> Cart.customer_id; OrderAddress/Transaction
|
|
* via order_id -> Order.customer_id) — none of it is a User's own data on
|
|
* its own.
|
|
*
|
|
* MUST run before Customer\Privacy\AddressDataProvider in
|
|
* config('core.privacy.providers') — that provider hard-deletes Address
|
|
* rows, and once gone there is no way to re-derive which activity_log
|
|
* rows (subject_type = Address) belonged to this customer. This provider
|
|
* resolves that address id list itself, before anything deletes it.
|
|
*/
|
|
class ActivityLogDataProvider implements PersonalDataProvider
|
|
{
|
|
private const REDACTED = '[redacted]';
|
|
|
|
public function name(): string
|
|
{
|
|
return 'activity_log';
|
|
}
|
|
|
|
public function exportForCustomer(CustomerSubject $subject): ProviderExportResult
|
|
{
|
|
$activities = Activity::query()
|
|
->where(fn ($query) => $this->scopeToCustomer($query, $subject->customerId))
|
|
->get();
|
|
|
|
return new ProviderExportResult('activity_log', $activities->map(fn (Activity $activity) => [
|
|
'id' => $activity->id,
|
|
'log_name' => $activity->log_name,
|
|
'description' => $activity->description,
|
|
'subject_type' => $activity->subject_type,
|
|
'subject_id' => $activity->subject_id,
|
|
'event' => $activity->event,
|
|
'properties' => $activity->properties?->toArray(),
|
|
'created_at' => $activity->created_at?->toIso8601String(),
|
|
])->all());
|
|
}
|
|
|
|
public function exportForUser(UserSubject $subject): ProviderExportResult
|
|
{
|
|
return new ProviderExportResult('activity_log', []);
|
|
}
|
|
|
|
public function eraseForCustomer(CustomerSubject $subject): ProviderErasureResult
|
|
{
|
|
$affected = Activity::query()
|
|
->where(fn ($query) => $this->scopeToCustomer($query, $subject->customerId))
|
|
->get();
|
|
|
|
if ($affected->isEmpty()) {
|
|
return new ProviderErasureResult('activity_log', ErasureOutcome::Skipped, 'No activity log entries for this customer.');
|
|
}
|
|
|
|
foreach ($affected as $activity) {
|
|
$activity->update(['properties' => $this->redact($activity->properties?->toArray() ?? [])]);
|
|
}
|
|
|
|
return new ProviderErasureResult(
|
|
'activity_log',
|
|
ErasureOutcome::Pseudonymized,
|
|
'PII-bearing properties redacted on matching audit log entries; who/what/when metadata (log_name, subject, event, timestamp, causer) retained for audit integrity.'
|
|
);
|
|
}
|
|
|
|
public function eraseForUser(UserSubject $subject): ProviderErasureResult
|
|
{
|
|
return new ProviderErasureResult(
|
|
'activity_log',
|
|
ErasureOutcome::Skipped,
|
|
'A User only ever appears here as causer_id (who performed an action), not as the PII content of a log entry — redacting that would erode the audit trail\'s own record of who acted.'
|
|
);
|
|
}
|
|
|
|
private function scopeToCustomer($query, int $customerId): void
|
|
{
|
|
$customerMorph = (new Customer)->getMorphClass();
|
|
$addressMorph = (new Address)->getMorphClass();
|
|
$cartAddressMorph = (new CartAddress)->getMorphClass();
|
|
$orderAddressMorph = (new OrderAddress)->getMorphClass();
|
|
$transactionMorph = (new Transaction)->getMorphClass();
|
|
|
|
$addressIds = Address::where('customer_id', $customerId)->pluck('id');
|
|
$cartIds = Cart::where('customer_id', $customerId)->pluck('id');
|
|
$cartAddressIds = CartAddress::whereIn('cart_id', $cartIds)->pluck('id');
|
|
$orderIds = Order::where('customer_id', $customerId)->pluck('id');
|
|
$orderAddressIds = OrderAddress::whereIn('order_id', $orderIds)->pluck('id');
|
|
$transactionIds = Transaction::whereIn('order_id', $orderIds)->pluck('id');
|
|
|
|
$query
|
|
->where(fn ($q) => $q->where('subject_type', $customerMorph)->where('subject_id', $customerId))
|
|
->orWhere(fn ($q) => $q->where('subject_type', $addressMorph)->whereIn('subject_id', $addressIds))
|
|
->orWhere(fn ($q) => $q->where('subject_type', $cartAddressMorph)->whereIn('subject_id', $cartAddressIds))
|
|
->orWhere(fn ($q) => $q->where('subject_type', $orderAddressMorph)->whereIn('subject_id', $orderAddressIds))
|
|
->orWhere(fn ($q) => $q->where('subject_type', $transactionMorph)->whereIn('subject_id', $transactionIds));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $properties
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function redact(array $properties): array
|
|
{
|
|
return array_map(function ($value) {
|
|
if (is_array($value)) {
|
|
return array_map(fn () => self::REDACTED, $value);
|
|
}
|
|
|
|
return self::REDACTED;
|
|
}, $properties);
|
|
}
|
|
}
|