diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e665f5..1508545 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,63 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [0.18.1] - 2026-09-16 + +### Added +- `Modules\Core\Payment\Privacy\PaymentDataProvider` — `lunar_transactions` (`card_type`/ + `last_four`) and `stripe_payment_intents` were previously uncovered by any Privacy provider. + Pseudonymizes card metadata on erasure (same tax/accounting retention reasoning as + `OrderDataProvider`); deletes the Stripe correlation rows outright, since their only purpose + (resolving an async webhook callback) has already been served by the time an erasure request + runs. No Stripe Customer object exists anywhere in this app to also request deletion of — see + `docs/payments.md` "Reconciliation". +- `Modules\Core\Auth\Privacy\UserSessionDataProvider` — `user_sessions` (`ip_address`, + `user_agent`) was previously uncovered. User-scope only; deleted outright on erasure, no legal + retention argument applies to login-session metadata. +- `Modules\Core\Logging\Privacy\ActivityLogDataProvider` — Spatie's `activity_log` table + (`Modules\Core\Logging\ActivityLogService`, plus several Lunar models' native `LogsActivity`) + durably retained full PII snapshots in `properties` even after the real row was erased + elsewhere. Redacts `properties` by subject (`Customer`/`Address`/`CartAddress`/`OrderAddress`/ + `Transaction`) on erasure; deliberately never touches `causer_id`, which is an actor reference, + not PII content. Must run before `AddressDataProvider` in `config('core.privacy.providers')` — + see the class's own docblock. +- `ErasureOutcome::Failed` — a provider throwing an exception is now a genuine, distinct outcome + from `Skipped` (a deliberate no-op), surfaced in the erasure report rather than silently + aborting the request. + +### Fixed +- `PrivacyService::completeErasure()` and `ExportDataSubjectJob::handle()` ran every registered + provider through a plain `array_map()` with no per-provider error handling — one provider + throwing aborted the entire request, discarding every other provider's already-computed + result and leaving the request stuck `Pending`/`Failed` with no report at all. Both now catch + per-provider (`PrivacyService::safeErase()`, `ExportDataSubjectJob::safeExport()`), logging the + exception and recording `ErasureOutcome::Failed`/`ProviderExportResult::$error` for that one + provider while every other provider's result is still recorded normally. Verified live: + simulating a throwing provider mid-erasure now correctly completes the request with a mixed + `erased`/`failed`/`erased` report instead of leaving it `Pending` forever. +- `CartDataProvider`/`OrderDataProvider` never covered PII-adjacent keys living in `Cart.meta`/ + `Order.meta`/`OrderAddress.meta` — `recovery_consent*`, `payment_method`, `checkout_fingerprint` + (Cart), `terms_accepted*` (Order), and `box_now_locker` (OrderAddress) all survived an erasure + request untouched. Both providers now clear these keys alongside their existing address/ + free-text field erasure. +- `CustomerDataProvider::eraseForUser()` left `otp_code`/`otp_expires_at`/`otp_attempts` on an + otherwise-erased `User` row. Now cleared alongside name/email. +- `Modules\Core\Privacy\Filament\Resources\DataErasureRequestResource`'s "Outcome" section + referenced `docs/privacy.md` directly in staff-facing UI text (meaningless to a user with no + repo access) and rendered the per-provider report as raw JSON strings via a `KeyValueEntry` + (the wrong component for a list of structured rows). Replaced with a plain-language + description and a proper `RepeatableEntry` table (Data category / Outcome badge / Reason). + +### Changed +- The 5 existing Privacy providers (`CustomerDataProvider`, `AddressDataProvider`, + `OrderDataProvider`, `CartDataProvider`, `ReviewDataProvider`) moved out of + `Modules\Core\Privacy\Providers` into their owning domain module's own `Privacy/` subdirectory + (e.g. `Modules\Core\Order\Privacy\OrderDataProvider`) — `Modules\Core\Privacy` now owns only + the shared contract, request lifecycle, and DTOs/enums. Matters concretely if a module is ever + extracted into its own composer package: the provider that knows how to erase that module's + data now travels with it, rather than being stranded in `Privacy` depending on a package that + no longer ships in this repo. See `docs/privacy.md` for the full reasoning. + ## [0.18.0] - 2026-09-16 ### Added diff --git a/composer.json b/composer.json index d871508..4f67d15 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "boboko/core", "description": "Core module — authentication and shared panel behaviour", "type": "library", - "version": "0.18.0", + "version": "0.18.1", "autoload": { "psr-4": { "Modules\\Core\\": "src/" diff --git a/docs/payments.md b/docs/payments.md index d87f489..bfadf61 100644 --- a/docs/payments.md +++ b/docs/payments.md @@ -202,6 +202,19 @@ purely a recovery aid for the case where our own write never happened at all. --- +## GDPR erasure/export + +`Modules\Core\Payment\Privacy\PaymentDataProvider` covers `lunar_transactions` +(`card_type`/`last_four`) and `stripe_payment_intents` — see `docs/privacy.md` for the full +right-of-erasure/right-of-access design. Pseudonymizes card metadata on erasure (same +tax/accounting retention reasoning `Order`'s own provider uses) and deletes the Stripe +correlation rows outright, since their only purpose — resolving an async webhook callback, see +"Async resolution" above — has already been served by the time an erasure request runs. No +Stripe Customer object exists anywhere in this app (see "Reconciliation" above) for this +provider to also request deletion of. + +--- + ## Explicitly out of scope for this pass - **`Checkout`/`Order` wiring** — how `Checkout` calls into `Payment`, how `Order`/`Checkout`