109 lines
4.7 KiB
PHP
109 lines
4.7 KiB
PHP
<?php
|
|
|
|
namespace Modules\Core\Payment\Privacy;
|
|
|
|
use Lunar\Models\Order;
|
|
use Lunar\Models\Transaction;
|
|
use Modules\Core\Payment\Models\StripePaymentIntent;
|
|
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;
|
|
|
|
/**
|
|
* Payment records (lunar_transactions, stripe_payment_intents) belong to the
|
|
* Customer (business account) via the Order they're attached to, not to an
|
|
* individual User, so this is Customer-scope only — same chain
|
|
* OrderDataProvider already uses (Order.customer_id).
|
|
*
|
|
* Like Order itself, payment/transaction records are subject to the same
|
|
* tax/accounting legal retention argument (GDPR Art. 17(3)(b)) — a payment
|
|
* record is part of the same financial audit trail as the order it settled,
|
|
* so this pseudonymizes the card-identifying fields in place rather than
|
|
* deleting the transaction: amount, status, and the transaction/order link
|
|
* all remain intact and auditable.
|
|
*
|
|
* No Stripe Customer object exists anywhere in this app (see docs/
|
|
* payments.md "Reconciliation") — there is nothing to request deletion of
|
|
* on Stripe's side. The only local, erasable PII is the card brand/last-4
|
|
* on Transaction and the cart_id/order_id/context correlation row on
|
|
* stripe_payment_intents, which is deleted outright once its Order is
|
|
* settled (its only purpose was resolving an async webhook callback — see
|
|
* docs/payments.md "Async resolution" — which has already happened by the
|
|
* time an erasure request would run).
|
|
*/
|
|
class PaymentDataProvider implements PersonalDataProvider
|
|
{
|
|
public function name(): string
|
|
{
|
|
return 'payments';
|
|
}
|
|
|
|
public function exportForCustomer(CustomerSubject $subject): ProviderExportResult
|
|
{
|
|
$orderIds = Order::where('customer_id', $subject->customerId)->pluck('id');
|
|
|
|
$transactions = Transaction::whereIn('order_id', $orderIds)->get();
|
|
$intents = StripePaymentIntent::whereIn('order_id', $orderIds)->get();
|
|
|
|
return new ProviderExportResult('payments', [
|
|
'transactions' => $transactions->map(fn (Transaction $transaction) => [
|
|
'id' => $transaction->id,
|
|
'order_id' => $transaction->order_id,
|
|
'type' => $transaction->type,
|
|
'status' => $transaction->status,
|
|
'amount' => $transaction->amount,
|
|
'card_type' => $transaction->card_type,
|
|
'last_four' => $transaction->last_four,
|
|
'reference' => $transaction->reference,
|
|
])->all(),
|
|
'stripe_payment_intents' => $intents->map(fn (StripePaymentIntent $intent) => [
|
|
'id' => $intent->id,
|
|
'order_id' => $intent->order_id,
|
|
'intent_id' => $intent->intent_id,
|
|
'status' => $intent->status,
|
|
])->all(),
|
|
]);
|
|
}
|
|
|
|
public function exportForUser(UserSubject $subject): ProviderExportResult
|
|
{
|
|
return new ProviderExportResult('payments', []);
|
|
}
|
|
|
|
public function eraseForCustomer(CustomerSubject $subject): ProviderErasureResult
|
|
{
|
|
$orderIds = Order::where('customer_id', $subject->customerId)->pluck('id');
|
|
|
|
if ($orderIds->isEmpty()) {
|
|
return new ProviderErasureResult('payments', ErasureOutcome::Skipped, 'No orders, and therefore no payment records, for this customer.');
|
|
}
|
|
|
|
Transaction::whereIn('order_id', $orderIds)->update([
|
|
'card_type' => null,
|
|
'last_four' => null,
|
|
]);
|
|
|
|
// stripe_payment_intents only ever existed to correlate a webhook
|
|
// callback back to a cart/order (see docs/payments.md "Async
|
|
// resolution") — that correlation has already served its purpose by
|
|
// the time an erasure request runs, so these rows are deleted
|
|
// outright rather than pseudonymized, unlike Transaction, which is
|
|
// the actual audit-trail record.
|
|
StripePaymentIntent::whereIn('order_id', $orderIds)->delete();
|
|
|
|
return new ProviderErasureResult(
|
|
'payments',
|
|
ErasureOutcome::Pseudonymized,
|
|
'Card brand/last-four cleared from transaction records; amounts, statuses, and references retained for legal/tax record-keeping. Stripe correlation rows (no longer needed post-settlement) deleted.'
|
|
);
|
|
}
|
|
|
|
public function eraseForUser(UserSubject $subject): ProviderErasureResult
|
|
{
|
|
return new ProviderErasureResult('payments', ErasureOutcome::Skipped, 'Payments belong to Customer-owned orders, not individual users.');
|
|
}
|
|
}
|