Files
core/src/Privacy/Providers/ReviewDataProvider.php
T

100 lines
3.8 KiB
PHP

<?php
namespace Modules\Core\Privacy\Providers;
use Illuminate\Database\Eloquent\Builder;
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 Modules\Core\Review\Models\ProductReview;
/**
* ProductReview (product_reviews) has no FK to Customer/User at all — it's
* deliberately anonymous, just free-text reviewer_name/reviewer_email (see
* docs/product-listing.md "Reviews"). A review is authored by an individual, not a
* business account, so this is User-scope only — matched best-effort by email
* against UserSubject::$email.
*
* NEEDS REVIEW: moved from Customer-scope to User-scope during the User/Customer
* split (see docs/privacy.md "User-scope vs Customer-scope") on the reasoning that
* authorship is a personal attribute — but this hasn't been fully validated against
* how reviews are actually attributed in this codebase; revisit before relying on
* it for a real erasure/export request.
*
* Matching by email is itself a real, documented limitation regardless of scope: a
* review submitted under a different email than the one on file won't be found.
* There's no stronger signal available without changing ProductReview's schema.
*/
class ReviewDataProvider implements PersonalDataProvider
{
public function name(): string
{
return 'reviews';
}
public function exportForCustomer(CustomerSubject $subject): ProviderExportResult
{
return new ProviderExportResult('reviews', []);
}
public function exportForUser(UserSubject $subject): ProviderExportResult
{
if (! $subject->email) {
return new ProviderExportResult('reviews', []);
}
$reviews = $this->matchingReviews($subject->email)->get();
return new ProviderExportResult('reviews', $reviews->map(fn (ProductReview $review) => [
'id' => $review->id,
'product_id' => $review->product_id,
'title' => $review->title,
'body' => $review->body,
'rating' => $review->rating,
'reviewer_name' => $review->reviewer_name,
'reviewer_email' => $review->reviewer_email,
'reviewed_at' => $review->reviewed_at?->toIso8601String(),
])->all());
}
public function eraseForCustomer(CustomerSubject $subject): ProviderErasureResult
{
return new ProviderErasureResult('reviews', ErasureOutcome::Skipped, 'Reviews are authored by individuals, not Customer accounts.');
}
public function eraseForUser(UserSubject $subject): ProviderErasureResult
{
if (! $subject->email) {
return new ProviderErasureResult('reviews', ErasureOutcome::Skipped, 'No email on this subject to match reviews by.');
}
$matched = $this->matchingReviews($subject->email)->count();
if ($matched === 0) {
return new ProviderErasureResult('reviews', ErasureOutcome::Skipped, 'No reviews matched this email.');
}
// The review content itself (rating/title/body) is kept — it's the
// reviewer's own product feedback, not identity data on its own — only
// the identifying fields are cleared.
$this->matchingReviews($subject->email)->update([
'reviewer_name' => 'Anonymous',
'reviewer_email' => null,
]);
return new ProviderErasureResult(
'reviews',
ErasureOutcome::Pseudonymized,
'Reviewer name/email cleared on reviews matched by email; rating/title/body text retained.'
);
}
private function matchingReviews(string $email): Builder
{
return ProductReview::where('reviewer_email', $email);
}
}