feat: include accommodation bookings in db anonymization
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ class DbAnonymizeCommandTest extends TestCase
|
||||
'newsletterConsents' => 2,
|
||||
'newsletterOptInRequests' => 3,
|
||||
'bookingEditDrafts' => 4,
|
||||
'accommodationBookings' => 5,
|
||||
]);
|
||||
|
||||
$tester = new CommandTester($this->createCommand($anonymizer, 'dev'));
|
||||
@@ -42,7 +43,7 @@ class DbAnonymizeCommandTest extends TestCase
|
||||
|
||||
self::assertSame(Command::SUCCESS, $tester->getStatusCode());
|
||||
self::assertStringContainsString(
|
||||
'Anonymized 1 users, 2 newsletter consents, 3 newsletter opt-in requests, and 4 booking drafts.',
|
||||
'Anonymized 1 users, 2 newsletter consents, 3 newsletter opt-in requests, 4 booking drafts, and 5 accommodation bookings.',
|
||||
$this->normalizeConsoleOutput($tester->getDisplay()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,9 +5,12 @@ declare(strict_types=1);
|
||||
namespace App\Tests\Service;
|
||||
|
||||
use App\Entity\BookingEditDraft;
|
||||
use App\Entity\Groups\AccommodationBooking;
|
||||
use App\Entity\NewsletterConsent;
|
||||
use App\Entity\NewsletterOptInRequest;
|
||||
use App\Entity\User;
|
||||
use App\Enum\Groups\AccommodationBookingOrigin;
|
||||
use App\Enum\Groups\AccommodationBookingStatus;
|
||||
use App\Service\DatabaseAnonymizer;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -127,6 +130,25 @@ class DatabaseAnonymizerTest extends TestCase
|
||||
$draft->setDateId(55);
|
||||
$draft->setHotelId(77);
|
||||
|
||||
$booking = new AccommodationBooking();
|
||||
$booking->setGroupName('Gymnasium Musterstadt, Klasse 10b');
|
||||
$booking->setSalutation('Frau');
|
||||
$booking->setFirstName('Mia');
|
||||
$booking->setLastName('Muster');
|
||||
$booking->setEmail('[email protected]');
|
||||
$booking->setPhone('+49123456789');
|
||||
$booking->setStreet('Main Street 1');
|
||||
$booking->setZip('12345');
|
||||
$booking->setCity('Berlin');
|
||||
$booking->setRemarks('Ruft immer donnerstags an.');
|
||||
$booking->setDateFrom(new \DateTimeImmutable('2027-01-10'));
|
||||
$booking->setDateTo(new \DateTimeImmutable('2027-01-17'));
|
||||
$booking->setPaxCount(42);
|
||||
$booking->setPriceSnapshot(['accommodation' => 1000], 1000, 'EUR', 2);
|
||||
$booking->setStatus(AccommodationBookingStatus::Confirmed);
|
||||
$booking->setOrigin(AccommodationBookingOrigin::Direct);
|
||||
$originalBookingUuid = $booking->getUuid();
|
||||
|
||||
$originalConsentCreatedAt = $consent->getCreatedAt();
|
||||
$originalConsentUpdatedAt = $consent->getUpdatedAt();
|
||||
$originalRequestCreatedAt = $request->getCreatedAt();
|
||||
@@ -137,6 +159,7 @@ class DatabaseAnonymizerTest extends TestCase
|
||||
$service->anonymizeNewsletterConsent($consent);
|
||||
$service->anonymizeNewsletterOptInRequest($request);
|
||||
$service->anonymizeBookingEditDraft($draft);
|
||||
$service->anonymizeAccommodationBooking($booking);
|
||||
|
||||
self::assertStringEndsWith('@example.test', $user->getEmail());
|
||||
self::assertSame('plain-text-password', $user->getPassword());
|
||||
@@ -186,5 +209,49 @@ class DatabaseAnonymizerTest extends TestCase
|
||||
self::assertSame(77, $draft->getHotelId());
|
||||
self::assertSame($originalDraftCreatedAt, $draft->getCreatedAt());
|
||||
self::assertSame($originalDraftUpdatedAt, $draft->getUpdatedAt());
|
||||
|
||||
self::assertStringEndsWith('@example.test', (string) $booking->getEmail());
|
||||
self::assertSame($user->getEmail(), $booking->getEmail());
|
||||
self::assertSame($consent->getFirstName(), $booking->getFirstName());
|
||||
self::assertSame($consent->getLastName(), $booking->getLastName());
|
||||
self::assertNotSame('Gymnasium Musterstadt, Klasse 10b', $booking->getGroupName());
|
||||
self::assertNotSame('Ruft immer donnerstags an.', $booking->getRemarks());
|
||||
self::assertNotSame('+49123456789', $booking->getPhone());
|
||||
self::assertNotSame('Main Street 1', $booking->getStreet());
|
||||
self::assertNotSame('12345', $booking->getZip());
|
||||
self::assertNotSame('Berlin', $booking->getCity());
|
||||
self::assertSame($originalBookingUuid, $booking->getUuid());
|
||||
self::assertSame('Frau', $booking->getSalutation());
|
||||
self::assertSame('2027-01-10', $booking->getDateFrom()?->format('Y-m-d'));
|
||||
self::assertSame('2027-01-17', $booking->getDateTo()?->format('Y-m-d'));
|
||||
self::assertSame(42, $booking->getPaxCount());
|
||||
self::assertSame(1000, $booking->getTotalPrice());
|
||||
self::assertSame(['accommodation' => 1000], $booking->getPriceBreakdown());
|
||||
self::assertSame('EUR', $booking->getPricingCurrency());
|
||||
self::assertSame(AccommodationBookingStatus::Confirmed, $booking->getStatus());
|
||||
self::assertSame(AccommodationBookingOrigin::Direct, $booking->getOrigin());
|
||||
}
|
||||
|
||||
public function testAnonymizeAccommodationBookingLeavesUnsetContactFieldsNull(): void
|
||||
{
|
||||
$service = new DatabaseAnonymizer(
|
||||
$this->createStub(EntityManagerInterface::class),
|
||||
new NullLogger(),
|
||||
);
|
||||
|
||||
$booking = new AccommodationBooking();
|
||||
$booking->setEmail('[email protected]');
|
||||
|
||||
$service->anonymizeAccommodationBooking($booking);
|
||||
|
||||
self::assertStringEndsWith('@example.test', (string) $booking->getEmail());
|
||||
self::assertNull($booking->getGroupName());
|
||||
self::assertNull($booking->getFirstName());
|
||||
self::assertNull($booking->getLastName());
|
||||
self::assertNull($booking->getPhone());
|
||||
self::assertNull($booking->getStreet());
|
||||
self::assertNull($booking->getZip());
|
||||
self::assertNull($booking->getCity());
|
||||
self::assertNull($booking->getRemarks());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user