100 lines
3.8 KiB
PHP
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);
|
|
}
|
|
}
|