2026-08-24 21:06:11 +03:00
|
|
|
<?php
|
|
|
|
|
|
2026-09-16 13:44:14 +03:00
|
|
|
namespace Modules\Core\Order\Privacy;
|
2026-08-24 21:06:11 +03:00
|
|
|
|
|
|
|
|
use Lunar\Models\Order;
|
|
|
|
|
use Lunar\Models\OrderAddress;
|
|
|
|
|
use Modules\Core\Privacy\Contracts\PersonalDataProvider;
|
2026-09-16 01:21:22 +03:00
|
|
|
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;
|
2026-08-24 21:06:11 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Orders and order addresses (lunar_orders, lunar_order_addresses) belong to the
|
|
|
|
|
* Customer (business account) via customer_id, not to an individual User, so this
|
|
|
|
|
* is Customer-scope only. They're also subject to legal retention (tax/accounting
|
|
|
|
|
* law generally requires invoices be kept for several years — GDPR Art. 17(3)(b)
|
|
|
|
|
* explicitly allows this to override an erasure request). eraseForCustomer()
|
|
|
|
|
* therefore pseudonymizes the PII-bearing free-text fields in place rather than
|
|
|
|
|
* deleting the order: totals, line items, tax data, and the order itself all
|
|
|
|
|
* remain intact and auditable.
|
2026-09-16 18:51:20 +03:00
|
|
|
*
|
|
|
|
|
* Also covers PII-adjacent keys living in Order.meta and OrderAddress.meta —
|
|
|
|
|
* Modules\Core\Checkout\Services\CheckoutService::initiatePayment() writes
|
|
|
|
|
* terms_accepted/terms_accepted_at/terms_accepted_policy_version/payment_method
|
|
|
|
|
* onto Order.meta, and Modules\Core\Shipping\Carriers\BoxNow\
|
|
|
|
|
* BoxNowFulfillmentService writes the shopper's chosen box_now_locker onto
|
|
|
|
|
* OrderAddress.meta — neither of which the free-text column erase above ever
|
|
|
|
|
* touched. Kept Customer-scope, consistent with Order/OrderAddress themselves.
|
2026-08-24 21:06:11 +03:00
|
|
|
*/
|
|
|
|
|
class OrderDataProvider implements PersonalDataProvider
|
|
|
|
|
{
|
2026-09-16 18:51:20 +03:00
|
|
|
private const ORDER_META_KEYS = [
|
|
|
|
|
'terms_accepted',
|
|
|
|
|
'terms_accepted_at',
|
|
|
|
|
'terms_accepted_policy_version',
|
|
|
|
|
'payment_method',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
private const ADDRESS_META_KEYS = [
|
|
|
|
|
'box_now_locker',
|
|
|
|
|
];
|
|
|
|
|
|
2026-08-24 21:06:11 +03:00
|
|
|
public function name(): string
|
|
|
|
|
{
|
|
|
|
|
return 'orders';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function exportForCustomer(CustomerSubject $subject): ProviderExportResult
|
|
|
|
|
{
|
|
|
|
|
$orders = Order::where('customer_id', $subject->customerId)->with('addresses')->get();
|
|
|
|
|
|
|
|
|
|
return new ProviderExportResult('orders', $orders->map(fn (Order $order) => [
|
|
|
|
|
'id' => $order->id,
|
|
|
|
|
'reference' => $order->reference,
|
|
|
|
|
'status' => $order->status,
|
|
|
|
|
'total' => $order->total?->decimal(),
|
|
|
|
|
'placed_at' => $order->placed_at?->toIso8601String(),
|
2026-09-16 18:51:20 +03:00
|
|
|
'meta' => $this->onlyKeys((array) $order->meta, self::ORDER_META_KEYS),
|
2026-08-24 21:06:11 +03:00
|
|
|
'addresses' => $order->addresses->map(fn (OrderAddress $address) => [
|
|
|
|
|
'type' => $address->type,
|
|
|
|
|
'first_name' => $address->first_name,
|
|
|
|
|
'last_name' => $address->last_name,
|
|
|
|
|
'line_one' => $address->line_one,
|
|
|
|
|
'city' => $address->city,
|
|
|
|
|
'postcode' => $address->postcode,
|
|
|
|
|
'contact_email' => $address->contact_email,
|
|
|
|
|
'contact_phone' => $address->contact_phone,
|
2026-09-16 18:51:20 +03:00
|
|
|
'meta' => $this->onlyKeys((array) $address->meta, self::ADDRESS_META_KEYS),
|
2026-08-24 21:06:11 +03:00
|
|
|
])->all(),
|
|
|
|
|
])->all());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function exportForUser(UserSubject $subject): ProviderExportResult
|
|
|
|
|
{
|
|
|
|
|
return new ProviderExportResult('orders', []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function eraseForCustomer(CustomerSubject $subject): ProviderErasureResult
|
|
|
|
|
{
|
2026-09-16 18:51:20 +03:00
|
|
|
$orders = Order::where('customer_id', $subject->customerId)->with('addresses')->get();
|
2026-08-24 21:06:11 +03:00
|
|
|
|
2026-09-16 18:51:20 +03:00
|
|
|
if ($orders->isEmpty()) {
|
2026-08-24 21:06:11 +03:00
|
|
|
return new ProviderErasureResult('orders', ErasureOutcome::Skipped, 'No orders for this customer.');
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-16 18:51:20 +03:00
|
|
|
foreach ($orders as $order) {
|
|
|
|
|
$order->update([
|
|
|
|
|
'customer_reference' => null,
|
|
|
|
|
'notes' => null,
|
|
|
|
|
'meta' => $this->withoutKeys((array) $order->meta, self::ORDER_META_KEYS),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
foreach ($order->addresses as $address) {
|
|
|
|
|
$address->update([
|
|
|
|
|
'title' => null,
|
|
|
|
|
'first_name' => 'Erased',
|
|
|
|
|
'last_name' => 'Customer',
|
|
|
|
|
'company_name' => null,
|
|
|
|
|
'tax_identifier' => null,
|
|
|
|
|
'line_one' => null,
|
|
|
|
|
'line_two' => null,
|
|
|
|
|
'line_three' => null,
|
|
|
|
|
'delivery_instructions' => null,
|
|
|
|
|
'contact_email' => null,
|
|
|
|
|
'contact_phone' => null,
|
|
|
|
|
'meta' => $this->withoutKeys((array) $address->meta, self::ADDRESS_META_KEYS),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-24 21:06:11 +03:00
|
|
|
|
|
|
|
|
return new ProviderErasureResult(
|
|
|
|
|
'orders',
|
|
|
|
|
ErasureOutcome::Pseudonymized,
|
2026-09-16 18:51:20 +03:00
|
|
|
'Order and address free-text fields and PII-bearing meta keys cleared; order records, totals, and line items retained for legal/tax record-keeping.'
|
2026-08-24 21:06:11 +03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function eraseForUser(UserSubject $subject): ProviderErasureResult
|
|
|
|
|
{
|
|
|
|
|
return new ProviderErasureResult('orders', ErasureOutcome::Skipped, 'Orders belong to Customer accounts, not individual users.');
|
|
|
|
|
}
|
2026-09-16 18:51:20 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param array<string, mixed> $meta
|
|
|
|
|
* @param array<int, string> $keys
|
|
|
|
|
* @return array<string, mixed>
|
|
|
|
|
*/
|
|
|
|
|
private function onlyKeys(array $meta, array $keys): array
|
|
|
|
|
{
|
|
|
|
|
return array_intersect_key($meta, array_flip($keys));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param array<string, mixed> $meta
|
|
|
|
|
* @param array<int, string> $keys
|
|
|
|
|
* @return array<string, mixed>
|
|
|
|
|
*/
|
|
|
|
|
private function withoutKeys(array $meta, array $keys): array
|
|
|
|
|
{
|
|
|
|
|
foreach ($keys as $key) {
|
|
|
|
|
unset($meta[$key]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $meta;
|
|
|
|
|
}
|
2026-08-24 21:06:11 +03:00
|
|
|
}
|