Files
myep/src/Controller/Account/PersonalDataController.php
T
2026-09-11 19:37:00 +02:00

248 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Controller\Account;
use App\BusProNet\ApiClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\Notification;
use App\BusProNet\Model\PersonalData;
use App\Entity\User;
use App\Exception\NewsletterProviderException;
use App\Form\PersonalDataType;
use App\Htmx\HxTrait;
use App\Model\NewsletterSubscriptionRequestResult;
use App\Security\Crypt;
use App\Service\BookingEditDataLoader;
use App\Service\NewsletterManager;
use App\Service\ProfileCompletenessChecker;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/**
* Controller for managing customer personal data operations.
*
* Provides functionality for viewing and updating customer profile information
* through integration with the BusProNet API system. Handles personal data
* management for authenticated users.
*/
class PersonalDataController extends AbstractController
{
use HxTrait;
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 BookingEditDataLoader $dataLoader Data loader for cache invalidation
* @param ProfileCompletenessChecker $completenessChecker Profile validation service
* @param EntityManagerInterface $entityManager Entity manager for persisting user changes
* @param NewsletterManager $newsletterManager Newsletter confirmation service
* @param LoggerInterface $logger Logger for audit trails and debugging
*/
public function __construct(
private readonly ApiClient $apiClient,
private readonly Crypt $crypt,
private readonly BookingEditDataLoader $dataLoader,
private readonly ProfileCompletenessChecker $completenessChecker,
private readonly EntityManagerInterface $entityManager,
private readonly NewsletterManager $newsletterManager,
private readonly LoggerInterface $logger,
) {
}
/**
* Display and handle updates to customer personal data.
*
* Fetches current personal data from BusProNet API and displays an editable form.
* Handles form submission to update personal information including address and
* communication details. Uses the Post-Redirect-Get pattern for form processing.
*
* @param Request $request The HTTP request containing form data
*
* @return Response The rendered personal data page or redirect response
*
* @throws ApiClientException When BusProNet API communication fails
*/
#[Route('/personal-data', name: 'app_personal_data')]
#[IsGranted('ROLE_USER')]
public function index(Request $request): Response
{
/** @var User $user */
$user = $this->getUser();
$email = $user->getEmail();
$password = $this->crypt->decrypt($user->getPassword());
$personalData = $this->loadPersonalData($user);
$newsletterSubscribed = $this->newsletterManager->hasConfirmedOptIn($email);
$newsletterPendingConfirmation = $this->newsletterManager->hasPendingConfirmation($email);
// The e-mail is deliberately locked here: BPN stores it on the address, where it doubles as
// the login identity of any person on that address. Accepting a submitted value would let
// one household member overwrite another's login. It is only ever echoed back to BPN as read.
$personalDataForm = $this->createForm(PersonalDataType::class, $personalData, [
'attr' => ['novalidate' => 'novalidate'],
'validation_groups' => ['personal_data'],
'email_editable' => false,
]);
// A failed load yields an empty PersonalData. Updating from it would send blank values for
// fields we never read - including the e-mail - so the form must not accept a submission.
if (null !== $personalData->addressId) {
$personalDataForm->handleRequest($request);
}
if ($personalDataForm->isSubmitted() && $personalDataForm->isValid()) {
try {
$updateResult = $this->apiClient->updatePersonalData($email, $password, $personalData);
if ($updateResult instanceof Notification) {
$this->logger->error('Personal data update rejected by BusProNet', [
'email' => $user->getEmail(),
'code' => $updateResult->code,
'error' => $updateResult->message,
]);
$this->addFlash('error', 'Deine persönlichen Daten konnten nicht aktualisiert werden. Bitte versuche es erneut.');
return $this->redirectToRoute('app_personal_data');
}
if (false === $updateResult->changed) {
$this->logger->error('BPN did not apply personal data update', [
'email' => $user->getEmail(),
'addressId' => $updateResult->addressId,
]);
$this->addFlash('error', 'Deine persönlichen Daten konnten nicht aktualisiert werden. Bitte versuche es erneut.');
return $this->redirectToRoute('app_personal_data');
}
// 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)
// Otherwise a rename here would stay invisible in the backend until the next
// login, which is the only other place the name is synced from BPN.
->setFirstName($personalData->firstName)
->setLastName($personalData->name)
;
$this->entityManager->flush();
$this->addFlash('success', 'Deine persönlichen Daten wurden aktualisiert');
$this->logger->info('Updated personal data', [
'email' => $user->getEmail(),
]);
// Handle profile completion redirect
$session = $request->getSession();
$redirectUrl = $session->get(self::SESSION_REDIRECT_KEY);
if (null !== $redirectUrl && true === $isComplete) {
$session->remove(self::SESSION_REDIRECT_KEY);
return $this->redirect($redirectUrl);
}
} catch (ApiClientException $e) {
$this->addFlash('error', $e->getMessage());
}
return $this->redirectToRoute('app_personal_data');
}
return $this->render('account/personal_data.html.twig', [
'personalData' => $personalData,
'personalDataForm' => $personalDataForm,
'newsletterSubscribed' => $newsletterSubscribed,
'newsletterPendingConfirmation' => $newsletterPendingConfirmation,
]);
}
#[Route('/personal-data/newsletter', name: 'app_personal_data_newsletter', methods: ['POST'])]
#[IsGranted('ROLE_USER')]
public function newsletter(Request $request): Response
{
/** @var User $user */
$user = $this->getUser();
$email = $user->getEmail();
$personalData = $this->loadPersonalData($user);
$hasPendingConfirmation = $this->newsletterManager->hasPendingConfirmation($email);
try {
if (true === $hasPendingConfirmation) {
$this->newsletterManager->requestConfirmation($email, $personalData->firstName, $personalData->name);
$this->addFlash('success', 'Wir haben dir eine neue Bestätigungs-E-Mail gesendet.');
} else {
$result = $this->newsletterManager->requestDefaultListSubscription($email, $personalData->firstName, $personalData->name);
$this->addFlash(...$this->newsletterRequestFlash($result));
}
} catch (NewsletterProviderException|\InvalidArgumentException $e) {
$this->addFlash('error', 'Die Newsletter-Aktion konnte gerade nicht verarbeitet werden. Bitte versuche es erneut.');
$this->logger->warning('Newsletter action failed', [
'email' => $email,
'error' => $e->getMessage(),
]);
}
return $this->htmxRedirect($request, $this->generateUrl('app_personal_data'));
}
/**
* @return array{string, string}
*/
private function newsletterRequestFlash(NewsletterSubscriptionRequestResult $result): array
{
$states = array_unique($result->listStates);
if (
NewsletterSubscriptionRequestResult::STATE_SUBSCRIBED === $result->state
&& [NewsletterSubscriptionRequestResult::LIST_STATE_ALREADY_REGISTERED] === array_values($states)
) {
return ['info', 'Du bist bereits zum Newsletter angemeldet.'];
}
return match ($result->state) {
NewsletterSubscriptionRequestResult::STATE_SUBSCRIBED => ['success', 'Deine Newsletter-Anmeldung wurde aktualisiert.'],
NewsletterSubscriptionRequestResult::STATE_PENDING_CONFIRMATION => ['info', 'Deine Anmeldung muss noch bestätigt werden. Bitte nutze den Link aus der Bestätigungs-E-Mail.'],
default => ['success', 'Bitte bestätige deine Newsletter-Anmeldung über den Link in der E-Mail.'],
};
}
private function loadPersonalData(User $user): PersonalData
{
$email = $user->getEmail();
$password = $this->crypt->decrypt($user->getPassword());
try {
$personalData = $this
->apiClient
->getPersonalData($email, $password);
} catch (ApiClientException $e) {
$this->addFlash('error', 'Deine persönlichen Daten konnten nicht abgerufen werden');
return new PersonalData();
}
if ($personalData instanceof Notification) {
$this->logger->error('Unable to fetch personal data', [
'code' => $personalData->code,
'error' => $personalData->message,
]);
$this->addFlash('error', 'Deine persönlichen Daten konnten nicht abgerufen werden');
return new PersonalData();
}
return $personalData;
}
}