feat: include accommodation bookings in db anonymization

This commit is contained in:
2026-09-08 15:41:28 +02:00
parent 8443dae5ad
commit cfe901b9c2
4 changed files with 142 additions and 12 deletions
+2 -1
View File
@@ -50,11 +50,12 @@ final class DbAnonymizeCommand extends Command
}
$io->success(sprintf(
'Anonymized %d users, %d newsletter consents, %d newsletter opt-in requests, and %d booking drafts.',
'Anonymized %d users, %d newsletter consents, %d newsletter opt-in requests, %d booking drafts, and %d accommodation bookings.',
$report['users'],
$report['newsletterConsents'],
$report['newsletterOptInRequests'],
$report['bookingEditDrafts'],
$report['accommodationBookings'],
));
return Command::SUCCESS;
+71 -10
View File
@@ -5,12 +5,13 @@ declare(strict_types=1);
namespace App\Service;
use App\Entity\BookingEditDraft;
use App\Entity\Groups\AccommodationBooking;
use App\Entity\NewsletterConsent;
use App\Entity\NewsletterOptInRequest;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Faker\Factory;
use Faker\Generator;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
/**
@@ -49,7 +50,7 @@ class DatabaseAnonymizer
}
/**
* @return array{users: int, newsletterConsents: int, newsletterOptInRequests: int, bookingEditDrafts: int}
* @return array{users: int, newsletterConsents: int, newsletterOptInRequests: int, bookingEditDrafts: int, accommodationBookings: int}
*/
public function anonymizeAll(int $batchSize = self::DEFAULT_BATCH_SIZE): array
{
@@ -80,6 +81,13 @@ class DatabaseAnonymizer
fn (BookingEditDraft $draft) => $this->anonymizeBookingEditDraft($draft),
$batchSize,
),
// Last, so that a booking whose email matches a mirrored BusPro account resolves to
// the synthetic identity the user pass has already minted for that person.
'accommodationBookings' => $this->anonymizeQuery(
'SELECT b FROM App\Entity\Groups\AccommodationBooking b ORDER BY b.id ASC',
fn (AccommodationBooking $booking) => $this->anonymizeAccommodationBooking($booking),
$batchSize,
),
];
$this->entityManager->flush();
@@ -108,8 +116,8 @@ class DatabaseAnonymizer
$consent->setEmail($identity['email']);
$consent->setNames(
$this->shouldReplaceName($consent->getFirstName()) ? $identity['firstName'] : $consent->getFirstName(),
$this->shouldReplaceName($consent->getLastName()) ? $identity['lastName'] : $consent->getLastName(),
$this->hasReplaceableValue($consent->getFirstName()) ? $identity['firstName'] : $consent->getFirstName(),
$this->hasReplaceableValue($consent->getLastName()) ? $identity['lastName'] : $consent->getLastName(),
);
}
@@ -123,8 +131,8 @@ class DatabaseAnonymizer
$request->setEmail($identity['email']);
$request->setNames(
$this->shouldReplaceName($request->getFirstName()) ? $identity['firstName'] : $request->getFirstName(),
$this->shouldReplaceName($request->getLastName()) ? $identity['lastName'] : $request->getLastName(),
$this->hasReplaceableValue($request->getFirstName()) ? $identity['firstName'] : $request->getFirstName(),
$this->hasReplaceableValue($request->getLastName()) ? $identity['lastName'] : $request->getLastName(),
);
}
@@ -136,6 +144,58 @@ class DatabaseAnonymizer
$draft->replaceFormData($formData);
}
/**
* The uuid is deliberately left alone: it is what the signed customer access link resolves
* against, so regenerating it would silently invalidate every link in an anonymized dump.
* The salutation stays too, being no more identifying than the gender it stands for.
*/
public function anonymizeAccommodationBooking(AccommodationBooking $booking): void
{
$identity = $this->resolveIdentity(
email: $booking->getEmail(),
firstName: $booking->getFirstName(),
lastName: $booking->getLastName(),
);
// Only fields that actually hold something are replaced, so a half-prepared draft stays
// half-prepared instead of becoming a record the `edit` group would suddenly accept.
if ($this->hasReplaceableValue($booking->getGroupName())) {
$booking->setGroupName($identity['groupName']);
}
if ($this->hasReplaceableValue($booking->getFirstName())) {
$booking->setFirstName($identity['firstName']);
}
if ($this->hasReplaceableValue($booking->getLastName())) {
$booking->setLastName($identity['lastName']);
}
if ($this->hasReplaceableValue($booking->getEmail())) {
$booking->setEmail($identity['email']);
}
if ($this->hasReplaceableValue($booking->getPhone())) {
$booking->setPhone($identity['phone']);
}
if ($this->hasReplaceableValue($booking->getStreet())) {
$booking->setStreet($identity['street']);
}
if ($this->hasReplaceableValue($booking->getZip())) {
$booking->setZip($identity['postCode']);
}
if ($this->hasReplaceableValue($booking->getCity())) {
$booking->setCity($identity['city']);
}
if ($this->hasReplaceableValue($booking->getRemarks())) {
$booking->setRemarks($this->placeholderText($identity, 'remarks'));
}
}
/**
* @param callable(object):void $anonymize
*/
@@ -230,7 +290,7 @@ class DatabaseAnonymizer
}
/**
* @param array<string, mixed> $personalData
* @param array<string, mixed> $personalData
* @param array<string, string> $identity
*
* @return array<string, mixed>
@@ -265,7 +325,7 @@ class DatabaseAnonymizer
}
/**
* @param array<string, mixed> $addressData
* @param array<string, mixed> $addressData
* @param array<string, string> $identity
*
* @return array<string, mixed>
@@ -296,7 +356,7 @@ class DatabaseAnonymizer
}
/**
* @param array<string, mixed> $bankAccountData
* @param array<string, mixed> $bankAccountData
* @param array<string, string>|null $identity
*
* @return array<string, mixed>
@@ -385,6 +445,7 @@ class DatabaseAnonymizer
'city' => $this->faker->city(),
'country' => self::SYNTHETIC_COUNTRY,
'district' => $this->faker->citySuffix(),
'groupName' => $this->faker->company(),
'accountHolder' => $firstName.' '.$lastName,
'bankName' => $this->faker->company(),
'iban' => self::SYNTHETIC_IBAN,
@@ -403,7 +464,7 @@ class DatabaseAnonymizer
return $type.':'.mb_strtolower($normalized);
}
private function shouldReplaceName(?string $value): bool
private function hasReplaceableValue(?string $value): bool
{
return null !== $this->stringOrNull($value);
}