248 lines
11 KiB
PHP
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;
|
|
}
|
|
}
|