Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f620cae4f9 | ||
|
|
6c4ea07e6e | ||
|
|
b95944701e | ||
|
|
2f8701c2e0 | ||
|
|
f47142c465 | ||
|
|
3105f2e7a0 | ||
|
|
0ea43d0c6b | ||
|
|
99b6876ce2 | ||
|
|
4e26b8acc0 | ||
|
|
0b2a16d117 |
No files matched your search
Generated
+26
-26
@@ -3684,16 +3684,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpspreadsheet",
|
||||
"version": "5.8.0",
|
||||
"version": "5.9.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||
"reference": "01964d92536edf1a3a874b9580a52824bebf6fbb"
|
||||
"reference": "05e99ebf61238a70227b4d9cc02d0030d34f6339"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/01964d92536edf1a3a874b9580a52824bebf6fbb",
|
||||
"reference": "01964d92536edf1a3a874b9580a52824bebf6fbb",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/05e99ebf61238a70227b4d9cc02d0030d34f6339",
|
||||
"reference": "05e99ebf61238a70227b4d9cc02d0030d34f6339",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3715,7 +3715,7 @@
|
||||
"maennchen/zipstream-php": "^2.1 || ^3.0",
|
||||
"markbaker/complex": "^3.0",
|
||||
"markbaker/matrix": "^3.0",
|
||||
"php": "^8.1",
|
||||
"php": "^8.2",
|
||||
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
@@ -3729,7 +3729,7 @@
|
||||
"phpstan/phpstan": "^1.1 || ^2.0",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0",
|
||||
"phpstan/phpstan-phpunit": "^1.0 || ^2.0",
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"phpunit/phpunit": "^10.5 || ^11.0",
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"tecnickcom/tcpdf": "^6.5"
|
||||
},
|
||||
@@ -3787,9 +3787,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.8.0"
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.9.0"
|
||||
},
|
||||
"time": "2026-06-07T03:51:10+00:00"
|
||||
"time": "2026-07-12T19:17:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpseclib/phpseclib",
|
||||
@@ -11533,16 +11533,16 @@
|
||||
},
|
||||
{
|
||||
"name": "friendsofphp/php-cs-fixer",
|
||||
"version": "v3.95.12",
|
||||
"version": "v3.95.13",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
|
||||
"reference": "b1b9055997a98dce3c2338e884626e718a25a923"
|
||||
"reference": "ea941114a002eb5e5876f190223deec1066c482c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/b1b9055997a98dce3c2338e884626e718a25a923",
|
||||
"reference": "b1b9055997a98dce3c2338e884626e718a25a923",
|
||||
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/ea941114a002eb5e5876f190223deec1066c482c",
|
||||
"reference": "ea941114a002eb5e5876f190223deec1066c482c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -11626,7 +11626,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
|
||||
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.12"
|
||||
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.13"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -11634,7 +11634,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-07T13:29:36+00:00"
|
||||
"time": "2026-07-10T09:23:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "myclabs/deep-copy",
|
||||
@@ -11937,16 +11937,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-doctrine",
|
||||
"version": "2.0.27",
|
||||
"version": "2.0.28",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-doctrine.git",
|
||||
"reference": "39b4ca45a07cdd6366eeefa2f7a993cddf3b9f9f"
|
||||
"reference": "b4623954d5ffee6311e4ddbaef65c074ae0f781a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/39b4ca45a07cdd6366eeefa2f7a993cddf3b9f9f",
|
||||
"reference": "39b4ca45a07cdd6366eeefa2f7a993cddf3b9f9f",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/b4623954d5ffee6311e4ddbaef65c074ae0f781a",
|
||||
"reference": "b4623954d5ffee6311e4ddbaef65c074ae0f781a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -12008,9 +12008,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan-doctrine/issues",
|
||||
"source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.27"
|
||||
"source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.28"
|
||||
},
|
||||
"time": "2026-06-10T10:39:35+00:00"
|
||||
"time": "2026-07-13T08:43:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-symfony",
|
||||
@@ -13028,16 +13028,16 @@
|
||||
},
|
||||
{
|
||||
"name": "rector/rector",
|
||||
"version": "2.5.5",
|
||||
"version": "2.5.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/rectorphp/rector.git",
|
||||
"reference": "9718a72e7f1aacacbdcb6eeed07a47147bce802e"
|
||||
"reference": "ba22f8c087848278fed6b4910d4cf1108096d8d3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/rectorphp/rector/zipball/9718a72e7f1aacacbdcb6eeed07a47147bce802e",
|
||||
"reference": "9718a72e7f1aacacbdcb6eeed07a47147bce802e",
|
||||
"url": "https://api.github.com/repos/rectorphp/rector/zipball/ba22f8c087848278fed6b4910d4cf1108096d8d3",
|
||||
"reference": "ba22f8c087848278fed6b4910d4cf1108096d8d3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -13076,7 +13076,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/rectorphp/rector/issues",
|
||||
"source": "https://github.com/rectorphp/rector/tree/2.5.5"
|
||||
"source": "https://github.com/rectorphp/rector/tree/2.5.7"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -13084,7 +13084,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-09T09:48:44+00:00"
|
||||
"time": "2026-07-13T15:24:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
|
||||
@@ -105,7 +105,7 @@ class PersonalDataController extends AbstractController
|
||||
return $this->redirectToRoute('app_personal_data');
|
||||
}
|
||||
|
||||
if ($updateResult instanceof PersonalDataUpdateResponse && false === $updateResult->changed) {
|
||||
if (false === $updateResult->changed) {
|
||||
$this->logger->error('BPN did not apply personal data update', [
|
||||
'email' => $user->getEmail(),
|
||||
'addressId' => $updateResult->addressId,
|
||||
|
||||
@@ -12,6 +12,7 @@ use App\Form\Model\BookingDto;
|
||||
use App\Htmx\HxTrait;
|
||||
use App\Model\BookingEditSubmissionResult;
|
||||
use App\Service\BookingChangeTracker;
|
||||
use App\Service\BookingConfigurator;
|
||||
use App\Service\BookingEditContextFactory;
|
||||
use App\Service\BookingEditDataLoader;
|
||||
use App\Service\BookingEditDraftManager;
|
||||
@@ -42,6 +43,7 @@ class IndexController extends AbstractController
|
||||
private readonly BookingEditDraftManager $draftService,
|
||||
private readonly BookingSessionManager $bookingSessionService,
|
||||
private readonly BookingChangeTracker $fingerprintService,
|
||||
private readonly BookingConfigurator $bookingConfigurator,
|
||||
private readonly BookingEditContextFactory $editContextFactory,
|
||||
private readonly BookingEditPreFlightChecker $preFlightChecker,
|
||||
private readonly BookingEditSubmitter $formSubmitter,
|
||||
@@ -92,6 +94,15 @@ class IndexController extends AbstractController
|
||||
return $this->redirectToRoute('app_bookings');
|
||||
}
|
||||
|
||||
$wasDirtyBeforePreselection = $this->fingerprintService->isDirty($bookingDto);
|
||||
|
||||
$this->bookingConfigurator->preselectDefaultServices($bookingDto);
|
||||
if (false === $wasDirtyBeforePreselection) {
|
||||
$bookingDto->originalFingerprint = $this->fingerprintService->generateFingerprint($bookingDto);
|
||||
}
|
||||
|
||||
$this->bookingSessionService->saveBookingDto($request, $bookingDto, BookingDto::MODE_EDIT);
|
||||
|
||||
// Show flash message if draft was restored
|
||||
if (true === $this->dataLoader->isDraftRestored()) {
|
||||
$this->addFlash('info', 'Dein zuvor gespeicherter Entwurf wurde wiederhergestellt.');
|
||||
@@ -137,10 +148,13 @@ class IndexController extends AbstractController
|
||||
return $this->redirectToRoute('app_booking_edit', ['bookingId' => $bookingId]);
|
||||
}
|
||||
|
||||
$isDirty = $wasDirtyBeforePreselection
|
||||
|| (true === $isOverviewSubmitted && $this->fingerprintService->isDirty($bookingDto));
|
||||
|
||||
$bookingEditContext = $this->editContextFactory->createOverviewContext(
|
||||
$bookingDto,
|
||||
$bookingData,
|
||||
$this->fingerprintService->isDirty($bookingDto),
|
||||
$isDirty,
|
||||
$isOverviewSubmitted,
|
||||
$hasBlockingValidationErrors,
|
||||
$missingValueLabelsByParticipantIndex,
|
||||
@@ -210,6 +224,7 @@ class IndexController extends AbstractController
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$this->draftService->deleteDraft($user, $bookingId);
|
||||
$this->dataLoader->invalidateBookingCache($bookingId, $user);
|
||||
|
||||
$this->addFlash('success', 'Änderungen verworfen, Daten neu geladen');
|
||||
|
||||
@@ -238,9 +253,14 @@ class IndexController extends AbstractController
|
||||
// Clear session state
|
||||
$this->bookingSessionService->clearBookingDto($request, BookingDto::MODE_EDIT);
|
||||
|
||||
// Check if a draft exists to show appropriate message
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
// Invalidate booking cache so a direct re-entry to /edit (bypassing /start)
|
||||
// still loads live data rather than the 5-minute cached response.
|
||||
$this->dataLoader->invalidateBookingCache($bookingId, $user);
|
||||
|
||||
// Check if a draft exists to show appropriate message
|
||||
$draft = $this->draftService->findDraft($user, $bookingId);
|
||||
|
||||
if (null !== $draft) {
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Htmx\HxTrait;
|
||||
use App\Service\BookingEditContextFactory;
|
||||
use App\Service\BookingEditDataLoader;
|
||||
use App\Service\BookingEditDraftManager;
|
||||
use App\Service\BookingConfigurator;
|
||||
use App\Service\ParticipantDataPrefiller;
|
||||
use App\Service\BookingSessionManager;
|
||||
use App\Service\ParticipantFormSupport;
|
||||
@@ -34,6 +35,7 @@ class ParticipantController extends AbstractController
|
||||
private readonly BookingEditDataLoader $dataLoader,
|
||||
private readonly BookingEditDraftManager $draftService,
|
||||
private readonly BookingEditContextFactory $editContextFactory,
|
||||
private readonly BookingConfigurator $bookingConfigurator,
|
||||
private readonly BookingSessionManager $bookingSessionService,
|
||||
private readonly ParticipantDataPrefiller $prepopulationService,
|
||||
private readonly ParticipantFormSupport $participantFormSupportService,
|
||||
@@ -148,6 +150,15 @@ class ParticipantController extends AbstractController
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
$this->bookingConfigurator->preselectDefaultServices($bookingDto);
|
||||
$this->bookingSessionService->saveBookingDto($request, $bookingDto, BookingDto::MODE_EDIT);
|
||||
|
||||
$form = $this->createForm(
|
||||
BookingParticipantType::class,
|
||||
$this->participantFormSupportService->createParticipantEditDto($bookingDto, $index),
|
||||
$this->participantFormSupportService->getParticipantFormOptions($bookingDto, true)
|
||||
);
|
||||
|
||||
$notifications = $this->participantFormSupportService->collectAndClearNotifications($bookingDto);
|
||||
$context = $this->editContextFactory->createParticipantContext(
|
||||
$bookingDto,
|
||||
|
||||
@@ -6,6 +6,9 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
trait ReturnUrlTrait
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $parameters
|
||||
*/
|
||||
public function getReturnUrl(Request $request, string $defaultRoute, array $parameters = []): string
|
||||
{
|
||||
$defaultUrl = $this->generateUrl($defaultRoute, $parameters);
|
||||
|
||||
@@ -52,11 +52,13 @@ class PersonalDataType extends AbstractType
|
||||
])
|
||||
->add('country', CountryType::class, [
|
||||
'label' => 'Land',
|
||||
'placeholder' => 'Bitte auswählen...',
|
||||
'property_path' => 'address.country',
|
||||
])
|
||||
->add('nationality', CountryType::class, [
|
||||
'label' => 'Nationalität',
|
||||
'property' => 'nationality',
|
||||
'placeholder' => 'Bitte auswählen...',
|
||||
'preferred_choices' => ['D', 'A', 'CH'],
|
||||
])
|
||||
->add('email', EmailType::class, [
|
||||
|
||||
@@ -24,6 +24,9 @@ abstract class AbstractMenuBuilder
|
||||
return $this->factory->createItem('root');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function getDefaultRouteParameters(string $parameter = 'id', string $default = '0'): array
|
||||
{
|
||||
$request = $this->requestStack->getMainRequest();
|
||||
|
||||
@@ -6,6 +6,9 @@ use Knp\Menu\ItemInterface;
|
||||
|
||||
class AdminMenuBuilder extends AbstractMenuBuilder
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function createMainMenu(array $options): ItemInterface
|
||||
{
|
||||
$menu = $this->createRootElement();
|
||||
|
||||
@@ -38,4 +38,31 @@ class NewsletterConsentRepository extends ServiceEntityRepository
|
||||
'mailjetListId' => $mailjetListId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $mailjetListIds
|
||||
*
|
||||
* @return array<int, NewsletterConsent> existing consents indexed by mailjetListId
|
||||
*/
|
||||
public function findByEmailAndListIds(string $email, array $mailjetListIds): array
|
||||
{
|
||||
if ([] === $mailjetListIds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$consents = $this->createQueryBuilder('c')
|
||||
->where('c.email = :email')
|
||||
->andWhere('c.mailjetListId IN (:listIds)')
|
||||
->setParameter('email', mb_strtolower(trim($email)))
|
||||
->setParameter('listIds', $mailjetListIds)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
$byListId = [];
|
||||
foreach ($consents as $consent) {
|
||||
$byListId[$consent->getMailjetListId()] = $consent;
|
||||
}
|
||||
|
||||
return $byListId;
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,17 @@ class BookingEditDataLoader
|
||||
return $this->initializeFromApi($request, $bookingId, $user);
|
||||
}
|
||||
|
||||
// No user edits pending — discard the session and reload live from API so that
|
||||
// BusPro-side changes (participant data, status, surcharges) are immediately
|
||||
// visible. isDirty() also returns false when originalFingerprint is null
|
||||
// (inconsistent session state), which is equally safe to reload.
|
||||
if (!$this->fingerprintService->isDirty($formData)) {
|
||||
$this->bookingSessionService->clearBookingDto($request, BookingDto::MODE_EDIT);
|
||||
$this->invalidateBookingCache($bookingId, $user);
|
||||
|
||||
return $this->initializeFromApi($request, $bookingId, $user);
|
||||
}
|
||||
|
||||
// Refresh availability and mutability data to ensure current state.
|
||||
// Availability is refreshed on every load; mutability is patched from cache.
|
||||
$this->travelDataService->enrichWithFreshAvailabilities($formData->travel);
|
||||
|
||||
@@ -10,41 +10,11 @@ use Faker\Generator;
|
||||
class BpnXmlAnonymizer
|
||||
{
|
||||
private const SAFE_EMAIL_DOMAIN = 'example.com';
|
||||
private const SALUTATIONS = ['Herr', 'Frau', 'Divers'];
|
||||
private const GENDERS = ['M', 'W', 'D'];
|
||||
private const COUNTRY_CODES = ['AT', 'BE', 'CH', 'DE', 'DK', 'ES', 'FR', 'IT', 'NL', 'NO', 'PL', 'SE'];
|
||||
|
||||
private Generator $faker;
|
||||
|
||||
/**
|
||||
* @var array<string, array{
|
||||
* firstName: string,
|
||||
* lastName: string,
|
||||
* salutation: string,
|
||||
* title: string,
|
||||
* gender: string,
|
||||
* nationality: string,
|
||||
* birthDate: string,
|
||||
* street: string,
|
||||
* postalCode: string,
|
||||
* city: string,
|
||||
* district: string,
|
||||
* country: string,
|
||||
* email: string,
|
||||
* mobilePhone: string,
|
||||
* phone: string,
|
||||
* newsletter: string,
|
||||
* iban: string,
|
||||
* userName: string,
|
||||
* requestKey: string,
|
||||
* requestPassword: string,
|
||||
* personId: string,
|
||||
* addressId: string,
|
||||
* customerId: string,
|
||||
* height: string,
|
||||
* weight: string,
|
||||
* shoeSize: string
|
||||
* }>
|
||||
* @var array<string, array<string, string>>
|
||||
*/
|
||||
private array $identities = [];
|
||||
|
||||
@@ -141,24 +111,9 @@ class BpnXmlAnonymizer
|
||||
}
|
||||
}
|
||||
|
||||
$currentNode = $personNode;
|
||||
while ($currentNode instanceof \DOMElement) {
|
||||
foreach (['personid', 'idperson', 'idadresseperson'] as $tagName) {
|
||||
$child = $this->getDirectChild($currentNode, $tagName);
|
||||
if (null !== $child) {
|
||||
$value = trim($child->textContent);
|
||||
if ('' !== $value) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$parentNode = $currentNode->parentNode;
|
||||
if (false === $parentNode instanceof \DOMElement) {
|
||||
break;
|
||||
}
|
||||
|
||||
$currentNode = $parentNode;
|
||||
$personIdValue = $this->findPersonIdInAncestors($personNode);
|
||||
if (null !== $personIdValue) {
|
||||
return $personIdValue;
|
||||
}
|
||||
|
||||
$fingerprintKey = $this->buildFingerprintKey($personNode);
|
||||
@@ -232,34 +187,7 @@ class BpnXmlAnonymizer
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* firstName: string,
|
||||
* lastName: string,
|
||||
* salutation: string,
|
||||
* title: string,
|
||||
* gender: string,
|
||||
* nationality: string,
|
||||
* birthDate: string,
|
||||
* street: string,
|
||||
* postalCode: string,
|
||||
* city: string,
|
||||
* district: string,
|
||||
* country: string,
|
||||
* email: string,
|
||||
* mobilePhone: string,
|
||||
* phone: string,
|
||||
* newsletter: string,
|
||||
* iban: string,
|
||||
* userName: string,
|
||||
* requestKey: string,
|
||||
* requestPassword: string,
|
||||
* personId: string,
|
||||
* addressId: string,
|
||||
* customerId: string,
|
||||
* height: string,
|
||||
* weight: string,
|
||||
* shoeSize: string
|
||||
* }
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function getOrCreateIdentity(string $personId): array
|
||||
{
|
||||
@@ -330,7 +258,7 @@ class BpnXmlAnonymizer
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{firstName: string, lastName: string, street: string, postalCode: string, city: string, email: string, mobilePhone: string} $identity
|
||||
* @param array<string, string> $identity
|
||||
*/
|
||||
private function replaceNameFields(\DOMElement $personNode, array $identity): void
|
||||
{
|
||||
@@ -358,7 +286,7 @@ class BpnXmlAnonymizer
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{firstName: string, lastName: string, street: string, postalCode: string, city: string, email: string, mobilePhone: string} $identity
|
||||
* @param array<string, string> $identity
|
||||
*/
|
||||
private function replaceEmailFields(\DOMElement $personNode, array $identity): void
|
||||
{
|
||||
@@ -396,7 +324,7 @@ class BpnXmlAnonymizer
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{firstName: string, lastName: string, street: string, postalCode: string, city: string, email: string, mobilePhone: string} $identity
|
||||
* @param array<string, string> $identity
|
||||
*/
|
||||
private function replaceAddressFields(\DOMElement $personNode, array $identity): void
|
||||
{
|
||||
@@ -436,6 +364,9 @@ class BpnXmlAnonymizer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $identity
|
||||
*/
|
||||
private function replaceDemographicFields(\DOMElement $personNode, array $identity): void
|
||||
{
|
||||
$salutationNode = $this->getDirectChild($personNode, 'anrede');
|
||||
@@ -479,6 +410,9 @@ class BpnXmlAnonymizer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $identity
|
||||
*/
|
||||
private function replaceFinancialFields(\DOMElement $personNode, array $identity): void
|
||||
{
|
||||
$bankNode = $this->getDirectChild($personNode, 'bankverbindung');
|
||||
@@ -492,6 +426,9 @@ class BpnXmlAnonymizer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $identity
|
||||
*/
|
||||
private function replaceMetadataFields(\DOMElement $personNode, array $identity): void
|
||||
{
|
||||
if (true === $personNode->hasAttribute('id')) {
|
||||
@@ -503,6 +440,9 @@ class BpnXmlAnonymizer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $identity
|
||||
*/
|
||||
private function replaceRequestFields(\DOMElement $personNode, array $identity): void
|
||||
{
|
||||
$userNode = $this->getDirectChild($personNode, 'user');
|
||||
@@ -544,23 +484,24 @@ class BpnXmlAnonymizer
|
||||
$node->setAttribute($attributeName, $replacement);
|
||||
}
|
||||
|
||||
private function findAncestorDirectChild(\DOMElement $personNode, string $ancestorTagName, string $childTagName): ?\DOMElement
|
||||
private function findPersonIdInAncestors(\DOMElement $node): ?string
|
||||
{
|
||||
$currentNode = $personNode;
|
||||
while ($currentNode instanceof \DOMElement) {
|
||||
if ($ancestorTagName === $currentNode->tagName) {
|
||||
return $this->getDirectChild($currentNode, $childTagName);
|
||||
foreach (['personid', 'idperson', 'idadresseperson'] as $tagName) {
|
||||
$child = $this->getDirectChild($node, $tagName);
|
||||
if (null !== $child) {
|
||||
$value = trim($child->textContent);
|
||||
if ('' !== $value) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
$parentNode = $currentNode->parentNode;
|
||||
if (false === $parentNode instanceof \DOMElement) {
|
||||
break;
|
||||
}
|
||||
|
||||
$currentNode = $parentNode;
|
||||
}
|
||||
|
||||
return null;
|
||||
$parent = $node->parentNode;
|
||||
if (false === $parent instanceof \DOMElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->findPersonIdInAncestors($parent);
|
||||
}
|
||||
|
||||
private function getDirectChild(\DOMElement $parent, string $name): ?\DOMElement
|
||||
@@ -578,21 +519,4 @@ class BpnXmlAnonymizer
|
||||
return null;
|
||||
}
|
||||
|
||||
private function slug(string $value): string
|
||||
{
|
||||
$normalizedValue = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
|
||||
if (false === $normalizedValue) {
|
||||
$normalizedValue = $value;
|
||||
}
|
||||
|
||||
$normalizedValue = strtolower($normalizedValue);
|
||||
$normalizedValue = preg_replace('/[^a-z0-9]+/', '.', $normalizedValue);
|
||||
$normalizedValue = trim((string) $normalizedValue, '.');
|
||||
|
||||
if ('' === $normalizedValue) {
|
||||
return 'user';
|
||||
}
|
||||
|
||||
return $normalizedValue;
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,9 @@ class DatabaseAnonymizer
|
||||
$this->identitySequence = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{users: int, newsletterConsents: int, newsletterOptInRequests: int, bookingEditDrafts: int}
|
||||
*/
|
||||
public function anonymizeAll(int $batchSize = self::DEFAULT_BATCH_SIZE): array
|
||||
{
|
||||
if ($batchSize < 1) {
|
||||
@@ -405,6 +408,9 @@ class DatabaseAnonymizer
|
||||
return null !== $this->stringOrNull($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $identity
|
||||
*/
|
||||
private function placeholderText(array $identity, string $suffix): string
|
||||
{
|
||||
return sprintf('%s-%s', $identity['lastName'], $suffix);
|
||||
|
||||
@@ -327,8 +327,7 @@ class NewsletterManager
|
||||
$this->newsletterService->ensureSubscribed($confirmation->getEmail(), $mailjetListId);
|
||||
}
|
||||
|
||||
$confirmation->markConfirmed();
|
||||
$this->upsertConfirmedConsents($confirmation->getEmail(), $mailjetListIds, $confirmation->getFirstName(), $confirmation->getLastName(), false, false);
|
||||
$this->upsertConfirmedConsents($confirmation->getEmail(), $mailjetListIds, $confirmation->getFirstName(), $confirmation->getLastName(), flush: false, deletePending: false);
|
||||
$this->entityManager->remove($confirmation);
|
||||
$this->entityManager->flush();
|
||||
|
||||
@@ -468,8 +467,10 @@ class NewsletterManager
|
||||
*/
|
||||
private function upsertConfirmedConsents(string $email, array $mailjetListIds, ?string $firstName, ?string $lastName, bool $flush = true, bool $deletePending = true): void
|
||||
{
|
||||
$existingByListId = $this->consentRepository->findByEmailAndListIds($email, $mailjetListIds);
|
||||
|
||||
foreach ($mailjetListIds as $mailjetListId) {
|
||||
$consent = $this->consentRepository->findOneByEmailAndListId($email, $mailjetListId);
|
||||
$consent = $existingByListId[$mailjetListId] ?? null;
|
||||
if (null === $consent) {
|
||||
$consent = new NewsletterConsent($email, $mailjetListId, $firstName, $lastName);
|
||||
$this->entityManager->persist($consent);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="loading-indicator"
|
||||
class="invisible fixed top-0 left-0 inset-0 z-50 backdrop-blur-sm flex items-center justify-center transition-all duration-100">
|
||||
class="invisible fixed top-0 left-0 inset-0 z-[60] backdrop-blur-sm flex items-center justify-center transition-all duration-100">
|
||||
{% include '_partials/_spinner.html.twig' %}
|
||||
</div>
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Tests\Controller\Booking\Edit;
|
||||
|
||||
use App\BusProNet\Model\Booking;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\BusProNet\XmlLoader\AgencyLoader;
|
||||
use App\Controller\Booking\Edit\IndexController;
|
||||
@@ -16,6 +17,7 @@ use App\Form\Model\ParticipantCardDataDto;
|
||||
use App\Form\Model\ParticipantCardPriceDto;
|
||||
use App\Model\BookingEditSubmissionResult;
|
||||
use App\Service\BookingChangeTracker;
|
||||
use App\Service\BookingConfigurator;
|
||||
use App\Service\BookingEditContextFactory;
|
||||
use App\Service\BookingEditDataLoader;
|
||||
use App\Service\BookingEditDraftManager;
|
||||
@@ -67,6 +69,8 @@ class IndexControllerTest extends TestCase
|
||||
->method('isDirty')
|
||||
->with($bookingDto)
|
||||
->willReturn(true);
|
||||
$fingerprintService->expects($this->never())
|
||||
->method('generateFingerprint');
|
||||
|
||||
$dataLoader = $this->createMock(BookingEditDataLoader::class);
|
||||
$dataLoader->expects($this->once())
|
||||
@@ -81,6 +85,16 @@ class IndexControllerTest extends TestCase
|
||||
->method('isDraftRestored')
|
||||
->willReturn(false);
|
||||
|
||||
$bookingSessionService = $this->createMock(BookingSessionManager::class);
|
||||
$bookingSessionService->expects($this->once())
|
||||
->method('saveBookingDto')
|
||||
->with($request, $bookingDto, BookingDto::MODE_EDIT);
|
||||
|
||||
$bookingConfigurator = $this->createMock(BookingConfigurator::class);
|
||||
$bookingConfigurator->expects($this->once())
|
||||
->method('preselectDefaultServices')
|
||||
->with($bookingDto);
|
||||
|
||||
$preflightChecker = $this->createMock(BookingEditPreFlightChecker::class);
|
||||
$preflightChecker->expects($this->once())
|
||||
->method('findMissingValueLabelsByParticipantIndex')
|
||||
@@ -96,8 +110,9 @@ class IndexControllerTest extends TestCase
|
||||
$controller = new TestableIndexController(
|
||||
$dataLoader,
|
||||
$this->createMock(BookingEditDraftManager::class),
|
||||
$this->createMock(BookingSessionManager::class),
|
||||
$bookingSessionService,
|
||||
$fingerprintService,
|
||||
$bookingConfigurator,
|
||||
$contextFactory,
|
||||
$preflightChecker,
|
||||
$this->createMock(BookingEditSubmitter::class),
|
||||
@@ -139,6 +154,10 @@ class IndexControllerTest extends TestCase
|
||||
->method('isDirty')
|
||||
->with($bookingDto)
|
||||
->willReturn(false);
|
||||
$fingerprintService->expects($this->once())
|
||||
->method('generateFingerprint')
|
||||
->with($bookingDto)
|
||||
->willReturn('normalized-fingerprint');
|
||||
|
||||
$dataLoader = $this->createMock(BookingEditDataLoader::class);
|
||||
$dataLoader->expects($this->once())
|
||||
@@ -170,6 +189,7 @@ class IndexControllerTest extends TestCase
|
||||
$this->createMock(BookingEditDraftManager::class),
|
||||
$this->createMock(BookingSessionManager::class),
|
||||
$fingerprintService,
|
||||
$this->createMock(BookingConfigurator::class),
|
||||
$contextFactory,
|
||||
$preflightChecker,
|
||||
$this->createMock(BookingEditSubmitter::class),
|
||||
@@ -212,8 +232,12 @@ class IndexControllerTest extends TestCase
|
||||
->willReturn(false);
|
||||
|
||||
$fingerprintService = $this->createMock(BookingChangeTracker::class);
|
||||
$fingerprintService->expects($this->once())
|
||||
->method('isDirty')
|
||||
->with($bookingDto)
|
||||
->willReturn(true);
|
||||
$fingerprintService->expects($this->never())
|
||||
->method('isDirty');
|
||||
->method('generateFingerprint');
|
||||
|
||||
$dataLoader = $this->createMock(BookingEditDataLoader::class);
|
||||
$dataLoader->expects($this->once())
|
||||
@@ -253,6 +277,7 @@ class IndexControllerTest extends TestCase
|
||||
$this->createMock(BookingEditDraftManager::class),
|
||||
$this->createMock(BookingSessionManager::class),
|
||||
$fingerprintService,
|
||||
$this->createMock(BookingConfigurator::class),
|
||||
$contextFactory,
|
||||
$preflightChecker,
|
||||
$submitter,
|
||||
@@ -296,10 +321,14 @@ class IndexControllerTest extends TestCase
|
||||
->willReturn(false);
|
||||
|
||||
$fingerprintService = $this->createMock(BookingChangeTracker::class);
|
||||
$fingerprintService->expects($this->once())
|
||||
$fingerprintService->expects($this->exactly(2))
|
||||
->method('isDirty')
|
||||
->with($bookingDto)
|
||||
->willReturn(false);
|
||||
->willReturnOnConsecutiveCalls(false, false);
|
||||
$fingerprintService->expects($this->once())
|
||||
->method('generateFingerprint')
|
||||
->with($bookingDto)
|
||||
->willReturn('normalized-fingerprint');
|
||||
|
||||
$dataLoader = $this->createMock(BookingEditDataLoader::class);
|
||||
$dataLoader->expects($this->once())
|
||||
@@ -335,6 +364,7 @@ class IndexControllerTest extends TestCase
|
||||
$this->createMock(BookingEditDraftManager::class),
|
||||
$this->createMock(BookingSessionManager::class),
|
||||
$fingerprintService,
|
||||
$this->createMock(BookingConfigurator::class),
|
||||
$contextFactory,
|
||||
$preflightChecker,
|
||||
$submitter,
|
||||
@@ -350,6 +380,184 @@ class IndexControllerTest extends TestCase
|
||||
$this->assertSame([0 => ['E-Mail']], $controller->renderParameters['bookingEditContext']->missingValueLabelsByParticipantIndex);
|
||||
}
|
||||
|
||||
public function testIndexTreatsDefaultPreselectionOnCleanDtoAsBaselineState(): void
|
||||
{
|
||||
$request = Request::create('/bookings/42/edit', 'GET');
|
||||
$user = $this->createUser();
|
||||
$bookingDto = $this->createBookingDto();
|
||||
$bookingDto->originalFingerprint = 'api-fingerprint';
|
||||
$bookingDto->participants = [$this->createParticipant()];
|
||||
$bookingData = $this->createBooking();
|
||||
|
||||
$form = $this->createMock(FormInterface::class);
|
||||
$form->method('isSubmitted')->willReturn(false);
|
||||
|
||||
$fingerprintService = $this->createMock(BookingChangeTracker::class);
|
||||
$fingerprintService->expects($this->once())
|
||||
->method('isDirty')
|
||||
->with($bookingDto)
|
||||
->willReturn(false);
|
||||
$fingerprintService->expects($this->once())
|
||||
->method('generateFingerprint')
|
||||
->with($bookingDto)
|
||||
->willReturn('normalized-fingerprint');
|
||||
|
||||
$dataLoader = $this->createMock(BookingEditDataLoader::class);
|
||||
$dataLoader->expects($this->once())
|
||||
->method('loadFormData')
|
||||
->with($request, 42, $user)
|
||||
->willReturn($bookingDto);
|
||||
$dataLoader->expects($this->once())
|
||||
->method('fetchBookingData')
|
||||
->with(42, $user)
|
||||
->willReturn($bookingData);
|
||||
$dataLoader->expects($this->once())
|
||||
->method('isDraftRestored')
|
||||
->willReturn(false);
|
||||
|
||||
$preselectedService = $this->createService(1001);
|
||||
$bookingConfigurator = $this->createMock(BookingConfigurator::class);
|
||||
$bookingConfigurator->expects($this->once())
|
||||
->method('preselectDefaultServices')
|
||||
->with($bookingDto)
|
||||
->willReturnCallback(static function (BookingDto $dto) use ($preselectedService): void {
|
||||
$dto->participants[0]->additionalServices[] = $preselectedService;
|
||||
});
|
||||
|
||||
$contextFactory = $this->createMock(BookingEditContextFactory::class);
|
||||
$contextFactory->expects($this->once())
|
||||
->method('createOverviewContext')
|
||||
->with($bookingDto, $bookingData, false, false, false, [])
|
||||
->willReturn($this->createOverviewContext($bookingDto, $bookingData, false, false, false));
|
||||
|
||||
$controller = new TestableIndexController(
|
||||
$dataLoader,
|
||||
$this->createMock(BookingEditDraftManager::class),
|
||||
$this->createMock(BookingSessionManager::class),
|
||||
$fingerprintService,
|
||||
$bookingConfigurator,
|
||||
$contextFactory,
|
||||
$this->createPreflightChecker($bookingDto),
|
||||
$this->createMock(BookingEditSubmitter::class),
|
||||
$user,
|
||||
$form,
|
||||
);
|
||||
|
||||
$response = $controller->index(42, $request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame('normalized-fingerprint', $bookingDto->originalFingerprint);
|
||||
$this->assertFalse($controller->renderParameters['bookingEditContext']->isDirty);
|
||||
}
|
||||
|
||||
public function testIndexKeepsDirtyStateWhenDtoWasDirtyBeforeDefaultPreselection(): void
|
||||
{
|
||||
$request = Request::create('/bookings/42/edit', 'GET');
|
||||
$user = $this->createUser();
|
||||
$bookingDto = $this->createBookingDto();
|
||||
$bookingDto->originalFingerprint = 'api-fingerprint';
|
||||
$bookingDto->participants = [$this->createParticipant()];
|
||||
$bookingData = $this->createBooking();
|
||||
|
||||
$form = $this->createMock(FormInterface::class);
|
||||
$form->method('isSubmitted')->willReturn(false);
|
||||
|
||||
$fingerprintService = $this->createMock(BookingChangeTracker::class);
|
||||
$fingerprintService->expects($this->once())
|
||||
->method('isDirty')
|
||||
->with($bookingDto)
|
||||
->willReturn(true);
|
||||
$fingerprintService->expects($this->never())
|
||||
->method('generateFingerprint');
|
||||
|
||||
$dataLoader = $this->createMock(BookingEditDataLoader::class);
|
||||
$dataLoader->expects($this->once())
|
||||
->method('loadFormData')
|
||||
->with($request, 42, $user)
|
||||
->willReturn($bookingDto);
|
||||
$dataLoader->expects($this->once())
|
||||
->method('fetchBookingData')
|
||||
->with(42, $user)
|
||||
->willReturn($bookingData);
|
||||
$dataLoader->expects($this->once())
|
||||
->method('isDraftRestored')
|
||||
->willReturn(false);
|
||||
|
||||
$preselectedService = $this->createService(1001);
|
||||
$bookingConfigurator = $this->createMock(BookingConfigurator::class);
|
||||
$bookingConfigurator->expects($this->once())
|
||||
->method('preselectDefaultServices')
|
||||
->with($bookingDto)
|
||||
->willReturnCallback(static function (BookingDto $dto) use ($preselectedService): void {
|
||||
$dto->participants[0]->additionalServices[] = $preselectedService;
|
||||
});
|
||||
|
||||
$contextFactory = $this->createMock(BookingEditContextFactory::class);
|
||||
$contextFactory->expects($this->once())
|
||||
->method('createOverviewContext')
|
||||
->with($bookingDto, $bookingData, true, false, false, [])
|
||||
->willReturn($this->createOverviewContext($bookingDto, $bookingData, true, false, false));
|
||||
|
||||
$controller = new TestableIndexController(
|
||||
$dataLoader,
|
||||
$this->createMock(BookingEditDraftManager::class),
|
||||
$this->createMock(BookingSessionManager::class),
|
||||
$fingerprintService,
|
||||
$bookingConfigurator,
|
||||
$contextFactory,
|
||||
$this->createPreflightChecker($bookingDto),
|
||||
$this->createMock(BookingEditSubmitter::class),
|
||||
$user,
|
||||
$form,
|
||||
);
|
||||
|
||||
$response = $controller->index(42, $request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame('api-fingerprint', $bookingDto->originalFingerprint);
|
||||
$this->assertTrue($controller->renderParameters['bookingEditContext']->isDirty);
|
||||
}
|
||||
|
||||
public function testReloadFromApiClearsSessionDeletesDraftInvalidatesCacheAndRedirectsToEdit(): void
|
||||
{
|
||||
$request = Request::create('/bookings/42/edit/reload', 'POST');
|
||||
$user = $this->createUser();
|
||||
|
||||
$bookingSessionService = $this->createMock(BookingSessionManager::class);
|
||||
$bookingSessionService->expects($this->once())
|
||||
->method('clearBookingDto')
|
||||
->with($request, BookingDto::MODE_EDIT);
|
||||
|
||||
$draftService = $this->createMock(BookingEditDraftManager::class);
|
||||
$draftService->expects($this->once())
|
||||
->method('deleteDraft')
|
||||
->with($user, 42);
|
||||
|
||||
$dataLoader = $this->createMock(BookingEditDataLoader::class);
|
||||
$dataLoader->expects($this->once())
|
||||
->method('invalidateBookingCache')
|
||||
->with(42, $user);
|
||||
|
||||
$controller = new TestableIndexController(
|
||||
$dataLoader,
|
||||
$draftService,
|
||||
$bookingSessionService,
|
||||
$this->createMock(BookingChangeTracker::class),
|
||||
$this->createMock(BookingConfigurator::class),
|
||||
$this->createMock(BookingEditContextFactory::class),
|
||||
$this->createMock(BookingEditPreFlightChecker::class),
|
||||
$this->createMock(BookingEditSubmitter::class),
|
||||
$user,
|
||||
$this->createMock(FormInterface::class),
|
||||
);
|
||||
|
||||
$response = $controller->reloadFromApi(42, $request);
|
||||
|
||||
$this->assertInstanceOf(RedirectResponse::class, $response);
|
||||
$this->assertSame('/bookings/42/edit', $response->headers->get('Location'));
|
||||
$this->assertSame([['success', 'Änderungen verworfen, Daten neu geladen']], $controller->flashes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider submissionResultProvider
|
||||
*
|
||||
@@ -433,6 +641,25 @@ class IndexControllerTest extends TestCase
|
||||
return new \App\Form\Model\ParticipantDto();
|
||||
}
|
||||
|
||||
private function createService(int $id): Service
|
||||
{
|
||||
$service = new Service();
|
||||
$service->id = $id;
|
||||
|
||||
return $service;
|
||||
}
|
||||
|
||||
private function createPreflightChecker(BookingDto $bookingDto): BookingEditPreFlightChecker
|
||||
{
|
||||
$preflightChecker = $this->createMock(BookingEditPreFlightChecker::class);
|
||||
$preflightChecker->expects($this->once())
|
||||
->method('findMissingValueLabelsByParticipantIndex')
|
||||
->with($bookingDto)
|
||||
->willReturn([]);
|
||||
|
||||
return $preflightChecker;
|
||||
}
|
||||
|
||||
private function createOverviewContext(
|
||||
BookingDto $bookingDto,
|
||||
Booking $bookingData,
|
||||
@@ -484,8 +711,12 @@ class IndexControllerTest extends TestCase
|
||||
$bookingData = $this->createBooking();
|
||||
$form = $this->createMock(FormInterface::class);
|
||||
$fingerprintService = $this->createMock(BookingChangeTracker::class);
|
||||
$fingerprintService->expects($this->once())
|
||||
->method('isDirty')
|
||||
->with($bookingDto)
|
||||
->willReturn(true);
|
||||
$fingerprintService->expects($this->never())
|
||||
->method('isDirty');
|
||||
->method('generateFingerprint');
|
||||
|
||||
$form->expects($this->once())
|
||||
->method('handleRequest')
|
||||
@@ -526,6 +757,7 @@ class IndexControllerTest extends TestCase
|
||||
$this->createMock(BookingEditDraftManager::class),
|
||||
$this->createMock(BookingSessionManager::class),
|
||||
$fingerprintService,
|
||||
$this->createMock(BookingConfigurator::class),
|
||||
$this->createMock(BookingEditContextFactory::class),
|
||||
$preflightChecker,
|
||||
$submitter,
|
||||
@@ -566,6 +798,7 @@ final class TestableIndexController extends IndexController
|
||||
BookingEditDraftManager $draftService,
|
||||
BookingSessionManager $bookingSessionService,
|
||||
BookingChangeTracker $fingerprintService,
|
||||
BookingConfigurator $bookingService,
|
||||
BookingEditContextFactory $editContextFactory,
|
||||
BookingEditPreFlightChecker $preFlightChecker,
|
||||
BookingEditSubmitter $formSubmitter,
|
||||
@@ -579,6 +812,7 @@ final class TestableIndexController extends IndexController
|
||||
$draftService,
|
||||
$bookingSessionService,
|
||||
$fingerprintService,
|
||||
$bookingService,
|
||||
$editContextFactory,
|
||||
$preFlightChecker,
|
||||
$formSubmitter,
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Form\Model\BookingEditContext;
|
||||
use App\Form\Model\BookingSummaryDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Model\ParticipantEditDto;
|
||||
use App\Service\BookingConfigurator;
|
||||
use App\Service\BookingEditContextFactory;
|
||||
use App\Service\BookingEditDataLoader;
|
||||
use App\Service\BookingEditDraftManager;
|
||||
@@ -26,6 +27,7 @@ use Carbon\CarbonImmutable;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -126,6 +128,7 @@ class ParticipantControllerTest extends TestCase
|
||||
$dataLoader,
|
||||
$draftService,
|
||||
$contextFactory,
|
||||
$this->createMock(BookingConfigurator::class),
|
||||
$bookingSessionService,
|
||||
$prepopulationService,
|
||||
$participantFormSupportService,
|
||||
@@ -145,6 +148,109 @@ class ParticipantControllerTest extends TestCase
|
||||
$this->assertSame(BookingParticipantType::class, $controller->createdFormTypes[1]);
|
||||
}
|
||||
|
||||
public function testRefreshPreselectsDefaultServicesAndRerendersParticipantForm(): void
|
||||
{
|
||||
$request = Request::create('/bookings/42/edit/participants/0/refresh', 'POST');
|
||||
$user = $this->createUser();
|
||||
$bookingDto = $this->createBookingDto();
|
||||
$bookingData = $this->createBooking();
|
||||
$participant = $bookingDto->participants[0];
|
||||
|
||||
$form = $this->createMock(FormInterface::class);
|
||||
$form->expects($this->once())
|
||||
->method('handleRequest')
|
||||
->with($request)
|
||||
->willReturnSelf();
|
||||
$form->expects($this->once())
|
||||
->method('createView')
|
||||
->willReturn(new FormView());
|
||||
|
||||
$dataLoader = $this->createMock(BookingEditDataLoader::class);
|
||||
$dataLoader->expects($this->once())
|
||||
->method('fetchBookingData')
|
||||
->with(42, $user)
|
||||
->willReturn($bookingData);
|
||||
|
||||
$bookingSessionService = $this->createMock(BookingSessionManager::class);
|
||||
$bookingSessionService->expects($this->once())
|
||||
->method('getBookingDto')
|
||||
->with($request, BookingDto::MODE_EDIT)
|
||||
->willReturn($bookingDto);
|
||||
$bookingSessionService->expects($this->once())
|
||||
->method('saveBookingDto')
|
||||
->with($request, $bookingDto, BookingDto::MODE_EDIT);
|
||||
|
||||
$bookingConfigurator = $this->createMock(BookingConfigurator::class);
|
||||
$bookingConfigurator->expects($this->once())
|
||||
->method('preselectDefaultServices')
|
||||
->with($bookingDto);
|
||||
|
||||
$participantFormSupportService = $this->createMock(ParticipantFormSupport::class);
|
||||
$participantFormSupportService->expects($this->once())
|
||||
->method('ensureParticipantExists')
|
||||
->with($bookingDto, 0)
|
||||
->willReturn($participant);
|
||||
$participantFormSupportService->expects($this->exactly(2))
|
||||
->method('createParticipantEditDto')
|
||||
->with($bookingDto, 0)
|
||||
->willReturn(new ParticipantEditDto($participant, $bookingDto));
|
||||
$participantFormSupportService->expects($this->exactly(2))
|
||||
->method('getParticipantFormOptions')
|
||||
->with($bookingDto, true)
|
||||
->willReturn([
|
||||
'booking_context' => $bookingDto,
|
||||
'body_dimension_ranges' => [
|
||||
'height_min' => 100,
|
||||
'height_max' => 250,
|
||||
'weight_min' => 20,
|
||||
'weight_max' => 200,
|
||||
'shoe_size_min' => 20,
|
||||
'shoe_size_max' => 55,
|
||||
],
|
||||
'validation_groups' => false,
|
||||
]);
|
||||
$participantFormSupportService->expects($this->once())
|
||||
->method('collectAndClearNotifications')
|
||||
->with($bookingDto)
|
||||
->willReturn([]);
|
||||
|
||||
$summaryData = $this->createMock(BookingSummaryDto::class);
|
||||
$context = new BookingEditContext($bookingDto, $bookingData, null, $summaryData);
|
||||
|
||||
$contextFactory = $this->createMock(BookingEditContextFactory::class);
|
||||
$contextFactory->expects($this->once())
|
||||
->method('prepareBookingDto')
|
||||
->with($bookingDto);
|
||||
$contextFactory->expects($this->once())
|
||||
->method('createParticipantContext')
|
||||
->with($bookingDto, $bookingData)
|
||||
->willReturn($context);
|
||||
|
||||
$prepopulationService = new ParticipantDataPrefiller(
|
||||
$this->createMock(ApiClient::class),
|
||||
$this->createMock(Crypt::class),
|
||||
$this->createMock(LoggerInterface::class),
|
||||
);
|
||||
|
||||
$controller = new TestableParticipantController(
|
||||
$dataLoader,
|
||||
$this->createMock(BookingEditDraftManager::class),
|
||||
$contextFactory,
|
||||
$bookingConfigurator,
|
||||
$bookingSessionService,
|
||||
$prepopulationService,
|
||||
$participantFormSupportService,
|
||||
$user,
|
||||
$form,
|
||||
);
|
||||
|
||||
$response = $controller->refreshParticipantForm(42, 0, $request);
|
||||
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertSame([BookingParticipantType::class, BookingParticipantType::class], $controller->createdFormTypes);
|
||||
$this->assertSame(['participant_form', 'booking_summary'], $controller->renderedBlocks);
|
||||
}
|
||||
|
||||
private function createUser(): User
|
||||
{
|
||||
return new User('[email protected]');
|
||||
@@ -184,6 +290,11 @@ final class TestableParticipantController extends ParticipantController
|
||||
|
||||
public ?string $renderTemplate = null;
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
public array $renderedBlocks = [];
|
||||
|
||||
/**
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
@@ -201,6 +312,7 @@ final class TestableParticipantController extends ParticipantController
|
||||
BookingEditDataLoader $dataLoader,
|
||||
BookingEditDraftManager $draftService,
|
||||
BookingEditContextFactory $editContextFactory,
|
||||
BookingConfigurator $bookingService,
|
||||
BookingSessionManager $bookingSessionService,
|
||||
ParticipantDataPrefiller $prepopulationService,
|
||||
ParticipantFormSupport $participantFormSupportService,
|
||||
@@ -213,6 +325,7 @@ final class TestableParticipantController extends ParticipantController
|
||||
$dataLoader,
|
||||
$draftService,
|
||||
$editContextFactory,
|
||||
$bookingService,
|
||||
$bookingSessionService,
|
||||
$prepopulationService,
|
||||
$participantFormSupportService,
|
||||
@@ -256,4 +369,14 @@ final class TestableParticipantController extends ParticipantController
|
||||
|
||||
return new Response('');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $parameters
|
||||
*/
|
||||
protected function renderBlockView(string $view, string $block, array $parameters = []): string
|
||||
{
|
||||
$this->renderedBlocks[] = $block;
|
||||
|
||||
return '<div></div>';
|
||||
}
|
||||
}
|
||||
@@ -317,6 +317,107 @@ class BookingEditPreFlightCheckerTest extends TestCase
|
||||
return $bookingDto;
|
||||
}
|
||||
|
||||
public function testScanAttentionReturnsEmptyArrayForCreateMode(): void
|
||||
{
|
||||
$travel = new Travel();
|
||||
$travel->dateFrom = CarbonImmutable::now()->addMonth()->toDateTimeImmutable();
|
||||
|
||||
$bookingDto = new BookingDto($travel, 1);
|
||||
// No $bookingDto->booking set → getMode() returns MODE_CREATE
|
||||
|
||||
$participant = $this->createCompleteParticipant(0);
|
||||
$participant->firstName = null;
|
||||
$participant->lastName = null;
|
||||
|
||||
$bookingDto->participants = [$participant];
|
||||
|
||||
$checker = new BookingEditPreFlightChecker($this->createValidator());
|
||||
|
||||
$this->assertSame([], $checker->findMissingValueLabelsByParticipantIndex($bookingDto));
|
||||
}
|
||||
|
||||
public function testScanAttentionSkipsCanceledParticipants(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto(true);
|
||||
|
||||
$canceledParticipant = $this->createCompleteParticipant(1);
|
||||
$canceledParticipant->status = 'S';
|
||||
$canceledParticipant->firstName = null;
|
||||
$canceledParticipant->lastName = null;
|
||||
$canceledParticipant->email = null;
|
||||
|
||||
$bookingDto->participants = [
|
||||
$this->createCompleteParticipant(0),
|
||||
$canceledParticipant,
|
||||
];
|
||||
|
||||
$checker = new BookingEditPreFlightChecker($this->createValidator());
|
||||
|
||||
$this->assertSame([], $checker->findMissingValueLabelsByParticipantIndex($bookingDto));
|
||||
}
|
||||
|
||||
public function testScanAttentionFlagsEmailUniquenessViolationForDuplicateAdultEmail(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto(false);
|
||||
|
||||
// All three participants share the same email address (from createCompleteParticipant).
|
||||
// Index 0 (applicant) is exempt; indices 1 and 2 each see a duplicate and are flagged.
|
||||
$bookingDto->participants = [
|
||||
$this->createCompleteParticipant(0),
|
||||
$this->createCompleteParticipant(1),
|
||||
$this->createCompleteParticipant(2),
|
||||
];
|
||||
|
||||
$checker = new BookingEditPreFlightChecker($this->createValidator());
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
1 => ['E-Mail'],
|
||||
2 => ['E-Mail'],
|
||||
],
|
||||
$checker->findMissingValueLabelsByParticipantIndex($bookingDto)
|
||||
);
|
||||
}
|
||||
|
||||
public function testScanAttentionCoversRemainingPropertyLabels(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto(true);
|
||||
|
||||
$participant = $this->createCompleteParticipant(0);
|
||||
$participant->gender = null;
|
||||
$participant->nationality = null;
|
||||
$participant->dateOfBirth = null;
|
||||
$participant->assignedRoomId = null;
|
||||
$participant->transportationOutbound = null;
|
||||
$participant->transportationInbound = null;
|
||||
|
||||
$bookingDto->participants = [$participant];
|
||||
|
||||
$checker = new BookingEditPreFlightChecker($this->createValidator());
|
||||
|
||||
$this->assertSame(
|
||||
[0 => ['Anrede', 'Nationalität', 'Geburtsdatum', 'Zimmer', 'Anreise', 'Abreise']],
|
||||
$checker->findMissingValueLabelsByParticipantIndex($bookingDto)
|
||||
);
|
||||
}
|
||||
|
||||
public function testScanAttentionFlagsAddressFieldLabelsForApplicantInNonInternalAgencyBooking(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto(false);
|
||||
|
||||
$participant = $this->createCompleteParticipant(0);
|
||||
$participant->address = new Address();
|
||||
|
||||
$bookingDto->participants = [$participant];
|
||||
|
||||
$checker = new BookingEditPreFlightChecker($this->createValidator());
|
||||
|
||||
$this->assertSame(
|
||||
[0 => ['Straße', 'PLZ', 'Ort', 'Land']],
|
||||
$checker->findMissingValueLabelsByParticipantIndex($bookingDto)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $selectionIds
|
||||
*/
|
||||
|
||||
@@ -166,6 +166,10 @@ class NewsletterManagerTest extends TestCase
|
||||
default => null,
|
||||
};
|
||||
});
|
||||
$consents
|
||||
->method('findByEmailAndListIds')
|
||||
->with('[email protected]', [1, 2])
|
||||
->willReturn([1 => $existingConsent, 2 => $existingOtherConsent]);
|
||||
$repository->expects(self::once())->method('deletePendingByEmail')->with('[email protected]')->willReturn(0);
|
||||
$entityManager->expects(self::never())->method('persist');
|
||||
$entityManager->expects(self::once())->method('flush');
|
||||
@@ -223,6 +227,7 @@ class NewsletterManagerTest extends TestCase
|
||||
$mailjet->expects(self::once())->method('ensureSubscribed')->with('[email protected]', 2);
|
||||
$consents->method('findActiveByEmail')->with('[email protected]')->willReturn($existingConsent);
|
||||
$consents->method('findOneByEmailAndListId')->willReturn(null);
|
||||
$consents->method('findByEmailAndListIds')->willReturn([]);
|
||||
$repository->expects(self::once())->method('deletePendingByEmail')->with('[email protected]')->willReturn(0);
|
||||
$repository->expects(self::never())->method('findPendingByEmail');
|
||||
$entityManager->expects(self::once())->method('persist')->with(self::isInstanceOf(NewsletterConsent::class));
|
||||
@@ -254,6 +259,7 @@ class NewsletterManagerTest extends TestCase
|
||||
$mailjet->expects(self::once())->method('ensureSubscribed')->with('[email protected]', 2);
|
||||
$consents->method('findActiveByEmail')->with('[email protected]')->willReturn($existingConsent);
|
||||
$consents->method('findOneByEmailAndListId')->with('[email protected]', 2)->willReturn(null);
|
||||
$consents->method('findByEmailAndListIds')->willReturn([]);
|
||||
$repository->expects(self::once())->method('deletePendingByEmail')->with('[email protected]')->willReturn(0);
|
||||
$entityManager->expects(self::once())->method('persist')->with(self::isInstanceOf(NewsletterConsent::class));
|
||||
$entityManager->expects(self::once())->method('flush');
|
||||
@@ -338,7 +344,7 @@ class NewsletterManagerTest extends TestCase
|
||||
|
||||
$repository->expects(self::once())->method('findByTokenHash')->with(hash('sha256', $token))->willReturn($confirmation);
|
||||
$repository->expects(self::never())->method('deletePendingByEmail');
|
||||
$consents->expects(self::once())->method('findOneByEmailAndListId')->with('[email protected]', 10321569)->willReturn(null);
|
||||
$consents->expects(self::once())->method('findByEmailAndListIds')->with('[email protected]', [10321569])->willReturn([]);
|
||||
$mailjet->expects(self::once())->method('upsertContact')->with('[email protected]', 'Mia', 'Muster');
|
||||
$mailjet->expects(self::once())->method('ensureSubscribed')->with('[email protected]', 10321569);
|
||||
$entityManager->expects(self::once())->method('persist')->with(self::isInstanceOf(NewsletterConsent::class));
|
||||
@@ -370,6 +376,7 @@ class NewsletterManagerTest extends TestCase
|
||||
$repository->expects(self::once())->method('findByTokenHash')->with(hash('sha256', $token))->willReturn($confirmation);
|
||||
$repository->expects(self::never())->method('deletePendingByEmail');
|
||||
$consents->method('findOneByEmailAndListId')->willReturn(null);
|
||||
$consents->method('findByEmailAndListIds')->with('[email protected]', [1, 2])->willReturn([]);
|
||||
$mailjet
|
||||
->expects(self::exactly(2))
|
||||
->method('ensureSubscribed')
|
||||
|
||||
Reference in new issue
Block a user