From a097159897d26b61b55fd70983808fafe48dd521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Mon, 19 Jan 2026 18:55:35 +0100 Subject: [PATCH] feat: move profile completeness check from login to booking flow entry - Add profileComplete flag to User entity to avoid API calls - Set flag during login (BpnAuthenticator) and profile save - Check flag in IndexController when entering booking flow - Remove ProfileCompletionSubscriber (no longer needed) Users with incomplete profiles are only redirected when starting a new booking, not on every login. Eliminates extra API call by caching completeness status on the user entity. --- migrations/Version20260119174522.php | 29 +++++ .../Account/PersonalDataController.php | 17 ++- .../Booking/Create/IndexController.php | 13 ++- src/Entity/User.php | 15 +++ .../ProfileCompletionSubscriber.php | 105 ------------------ src/Security/BpnAuthenticator.php | 11 +- 6 files changed, 76 insertions(+), 114 deletions(-) create mode 100644 migrations/Version20260119174522.php delete mode 100644 src/EventSubscriber/ProfileCompletionSubscriber.php diff --git a/migrations/Version20260119174522.php b/migrations/Version20260119174522.php new file mode 100644 index 0000000..7765722 --- /dev/null +++ b/migrations/Version20260119174522.php @@ -0,0 +1,29 @@ +addSql('ALTER TABLE user ADD profile_complete TINYINT(1) NOT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE user DROP profile_complete'); + } +} diff --git a/src/Controller/Account/PersonalDataController.php b/src/Controller/Account/PersonalDataController.php index 0b3108d..9c6483e 100644 --- a/src/Controller/Account/PersonalDataController.php +++ b/src/Controller/Account/PersonalDataController.php @@ -9,11 +9,11 @@ use App\BusProNet\Exception\ApiClientException; use App\BusProNet\Model\Notification; use App\BusProNet\Model\PersonalData; use App\Entity\User; -use App\EventSubscriber\ProfileCompletionSubscriber; use App\Form\PersonalDataType; use App\Security\Crypt; use App\Service\BookingEditDataLoaderService; use App\Service\ProfileCompletenessChecker; +use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -30,11 +30,14 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; */ class PersonalDataController extends AbstractController { + public const SESSION_REDIRECT_KEY = '_profile_completion_redirect'; + /** * @param ApiClient $apiClient BusProNet API client for data operations * @param Crypt $crypt Encryption service for password handling * @param BookingEditDataLoaderService $dataLoader Data loader for cache invalidation * @param ProfileCompletenessChecker $completenessChecker Profile validation service + * @param EntityManagerInterface $entityManager Entity manager for persisting user changes * @param LoggerInterface $logger Logger for audit trails and debugging */ public function __construct( @@ -42,6 +45,7 @@ class PersonalDataController extends AbstractController private readonly Crypt $crypt, private readonly BookingEditDataLoaderService $dataLoader, private readonly ProfileCompletenessChecker $completenessChecker, + private readonly EntityManagerInterface $entityManager, private readonly LoggerInterface $logger, ) { } @@ -100,6 +104,11 @@ class PersonalDataController extends AbstractController // Invalidate cached bookings to ensure edit mode shows updated applicant data $this->dataLoader->invalidateUserBookingCaches($user); + // Update profile completeness flag on user entity + $isComplete = $this->completenessChecker->isComplete($personalData); + $user->setProfileComplete($isComplete); + $this->entityManager->flush(); + $this->addFlash('success', 'Deine persönlichen Daten wurden aktualisiert'); $this->logger->info('Updated personal data', [ 'email' => $user->getEmail(), @@ -107,10 +116,10 @@ class PersonalDataController extends AbstractController // Handle profile completion redirect $session = $request->getSession(); - $redirectUrl = $session->get(ProfileCompletionSubscriber::SESSION_REDIRECT_KEY); + $redirectUrl = $session->get(self::SESSION_REDIRECT_KEY); - if (null !== $redirectUrl && true === $this->completenessChecker->isComplete($personalData)) { - $session->remove(ProfileCompletionSubscriber::SESSION_REDIRECT_KEY); + if (null !== $redirectUrl && true === $isComplete) { + $session->remove(self::SESSION_REDIRECT_KEY); return $this->redirect($redirectUrl); } diff --git a/src/Controller/Booking/Create/IndexController.php b/src/Controller/Booking/Create/IndexController.php index 7be085d..0e2da41 100644 --- a/src/Controller/Booking/Create/IndexController.php +++ b/src/Controller/Booking/Create/IndexController.php @@ -5,6 +5,8 @@ declare(strict_types=1); namespace App\Controller\Booking\Create; use App\BusProNet\XmlLoader\AgencyLoader; +use App\Controller\Account\PersonalDataController; +use App\Entity\User; use App\Exception\HotelNotFoundException; use App\Exception\HotelNotInTravelException; use App\Exception\NoRoomsAvailableException; @@ -42,17 +44,26 @@ class IndexController extends AbstractController * * Renders a loading page that triggers the actual initialization via HTMX. * This provides immediate visual feedback while BusProNet API calls are made. + * Redirects to profile completion if logged-in user has incomplete profile data. */ #[Route( path: '/bookings/create', name: 'app_booking_create', )] - public function index(#[MapQueryString] ?BookingQueryParams $params): Response + public function index(Request $request, #[MapQueryString] ?BookingQueryParams $params): Response { if (null === $params) { throw $this->createNotFoundException('Invalid booking parameters provided'); } + $user = $this->getUser(); + + if ($user instanceof User && false === $user->isProfileComplete()) { + $request->getSession()->set(PersonalDataController::SESSION_REDIRECT_KEY, $request->getUri()); + + return $this->redirectToRoute('app_personal_data'); + } + return $this->render('booking/create/index.html.twig', [ 'params' => $params, ]); diff --git a/src/Entity/User.php b/src/Entity/User.php index c2fc179..9d830a9 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -35,6 +35,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\Column(type: 'datetime_immutable', nullable: true)] private ?\DateTimeImmutable $lastLoginAt = null; + #[ORM\Column(type: 'boolean')] + private bool $profileComplete = false; + public function __construct(string $email) { $this->email = $email; @@ -129,6 +132,18 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + public function isProfileComplete(): bool + { + return $this->profileComplete; + } + + public function setProfileComplete(bool $profileComplete): static + { + $this->profileComplete = $profileComplete; + + return $this; + } + public function eraseCredentials(): void { } diff --git a/src/EventSubscriber/ProfileCompletionSubscriber.php b/src/EventSubscriber/ProfileCompletionSubscriber.php deleted file mode 100644 index 514fcec..0000000 --- a/src/EventSubscriber/ProfileCompletionSubscriber.php +++ /dev/null @@ -1,105 +0,0 @@ - ['onLoginSuccess', -10], - ]; - } - - public function onLoginSuccess(LoginSuccessEvent $event): void - { - $user = $event->getUser(); - - if (false === $user instanceof User) { - return; - } - - $email = $user->getEmail(); - $password = $this->crypt->decrypt($user->getPassword()); - - try { - $personalData = $this->apiClient->getPersonalData($email, $password); - } catch (ApiClientException $e) { - $this->logger->warning('Failed to fetch personal data for profile completeness check', [ - 'email' => $email, - 'error' => $e->getMessage(), - ]); - - return; - } - - if (false === $personalData instanceof PersonalData) { - $this->logger->warning('Invalid response when fetching personal data for profile completeness check', [ - 'email' => $email, - ]); - - return; - } - - if (true === $this->completenessChecker->isComplete($personalData)) { - return; - } - - $this->logger->info('Incomplete profile detected, redirecting to profile completion', [ - 'email' => $email, - ]); - - $request = $event->getRequest(); - $session = $request->getSession(); - - // Store the original target URL (from authenticator's response or target path) - $originalResponse = $event->getResponse(); - $targetUrl = null; - - if ($originalResponse instanceof RedirectResponse) { - $targetUrl = $originalResponse->getTargetUrl(); - } - - // Don't redirect back to the personal data page itself - $personalDataUrl = $this->urlGenerator->generate('app_personal_data'); - if (null !== $targetUrl && $targetUrl !== $personalDataUrl) { - $session->set(self::SESSION_REDIRECT_KEY, $targetUrl); - } - - // Override the response to redirect to personal data page - $event->setResponse(new RedirectResponse($personalDataUrl)); - } -} diff --git a/src/Security/BpnAuthenticator.php b/src/Security/BpnAuthenticator.php index e7cba36..e2eeaa4 100644 --- a/src/Security/BpnAuthenticator.php +++ b/src/Security/BpnAuthenticator.php @@ -9,6 +9,7 @@ use App\BusProNet\Exception\ApiClientException; use App\BusProNet\Model\PersonalData; use App\Entity\User; use App\Htmx\HxRedirectResponse; +use App\Service\ProfileCompletenessChecker; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -41,6 +42,7 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent private readonly ApiClient $apiClient, private readonly EntityManagerInterface $entityManager, private readonly Crypt $crypt, + private readonly ProfileCompletenessChecker $completenessChecker, private readonly LoggerInterface $authLogger, ) { } @@ -72,7 +74,7 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent return new SelfValidatingPassport( new UserBadge($email, function () use ($email, $password, $response) { - return $this->createOrUpdateLocalUser($email, $password, $response->personId, $response->addressId); + return $this->createOrUpdateLocalUser($email, $password, $response); }), [ new CsrfTokenBadge('authenticate', $csrfToken), @@ -80,7 +82,7 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent ); } - private function createOrUpdateLocalUser(string $email, string $password, ?int $personId, ?int $addressId): User + private function createOrUpdateLocalUser(string $email, string $password, PersonalData $personalData): User { try { $crmAttributes = $this->apiClient->getCrmAttributes($email, $password); @@ -102,11 +104,12 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent $user ->setPassword($encryptedPassword) - ->setPersonId($personId) - ->setAddressId($addressId) + ->setPersonId($personalData->personId) + ->setAddressId($personalData->addressId) ->setRoles($roles) ->setHotelCodes($hotelCodes) ->setLastLoginAt(new \DateTimeImmutable()) + ->setProfileComplete($this->completenessChecker->isComplete($personalData)) ; $this->entityManager->flush();