feat: automatic saving of drafts when editing bookings
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260106091117 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Create booking_edit_draft table for persisting user edits during booking edit flow';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE booking_edit_draft (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, booking_id INT NOT NULL, form_data JSON NOT NULL COMMENT \'(DC2Type:json)\', created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_E6548098A76ED395 (user_id), UNIQUE INDEX user_booking_unique (user_id, booking_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('ALTER TABLE booking_edit_draft ADD CONSTRAINT FK_E6548098A76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE booking_edit_draft DROP FOREIGN KEY FK_E6548098A76ED395');
|
||||||
|
$this->addSql('DROP TABLE booking_edit_draft');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,8 @@ use App\BusProNet\Exception\TimeoutException;
|
|||||||
use App\BusProNet\Model\Notification;
|
use App\BusProNet\Model\Notification;
|
||||||
use App\Controller\Booking\Traits;
|
use App\Controller\Booking\Traits;
|
||||||
use App\Controller\Booking\Traits\BookingExceptionHandlerTrait;
|
use App\Controller\Booking\Traits\BookingExceptionHandlerTrait;
|
||||||
use App\Exception\TravelNotFoundException;
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Exception\TravelNotFoundException;
|
||||||
use App\Form\BookingEditType;
|
use App\Form\BookingEditType;
|
||||||
use App\Form\BookingParticipantType;
|
use App\Form\BookingParticipantType;
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
@@ -19,6 +19,7 @@ use App\Form\Model\ParticipantEditDto;
|
|||||||
use App\Htmx\HxTrait;
|
use App\Htmx\HxTrait;
|
||||||
use App\Security\Crypt;
|
use App\Security\Crypt;
|
||||||
use App\Service\BookingEditDataLoaderService;
|
use App\Service\BookingEditDataLoaderService;
|
||||||
|
use App\Service\BookingEditDraftService;
|
||||||
use App\Service\BookingFingerprintService;
|
use App\Service\BookingFingerprintService;
|
||||||
use App\Service\BookingService;
|
use App\Service\BookingService;
|
||||||
use App\Service\BookingSummaryDataService;
|
use App\Service\BookingSummaryDataService;
|
||||||
@@ -49,6 +50,7 @@ class IndexController extends AbstractController
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ApiClient $apiClient,
|
private readonly ApiClient $apiClient,
|
||||||
private readonly BookingEditDataLoaderService $dataLoader,
|
private readonly BookingEditDataLoaderService $dataLoader,
|
||||||
|
private readonly BookingEditDraftService $draftService,
|
||||||
private readonly TravelDataService $travelDataService,
|
private readonly TravelDataService $travelDataService,
|
||||||
private readonly BookingService $bookingService,
|
private readonly BookingService $bookingService,
|
||||||
private readonly BookingFingerprintService $fingerprintService,
|
private readonly BookingFingerprintService $fingerprintService,
|
||||||
@@ -73,7 +75,7 @@ class IndexController extends AbstractController
|
|||||||
|
|
||||||
// Load form data from session (or API on first load)
|
// Load form data from session (or API on first load)
|
||||||
try {
|
try {
|
||||||
$bookingDto = $this->dataLoader->loadFormData($request, $id, $email, $password);
|
$bookingDto = $this->dataLoader->loadFormData($request, $id, $email, $password, $user);
|
||||||
} catch (TravelNotFoundException) {
|
} catch (TravelNotFoundException) {
|
||||||
$this->addFlash('error', 'Reisedaten sind nicht (mehr) verfügbar');
|
$this->addFlash('error', 'Reisedaten sind nicht (mehr) verfügbar');
|
||||||
|
|
||||||
@@ -86,6 +88,11 @@ class IndexController extends AbstractController
|
|||||||
return $this->redirectToRoute('app_bookings');
|
return $this->redirectToRoute('app_bookings');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show flash message if draft was restored
|
||||||
|
if (true === $this->dataLoader->wasDraftRestored()) {
|
||||||
|
$this->addFlash('info', 'Dein zuvor gespeicherter Entwurf wurde wiederhergestellt.');
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch booking data for display (surcharges, canceled status, etc.)
|
// Fetch booking data for display (surcharges, canceled status, etc.)
|
||||||
$bookingData = $this->dataLoader->fetchBookingData($email, $password, $id);
|
$bookingData = $this->dataLoader->fetchBookingData($email, $password, $id);
|
||||||
if (null === $bookingData || $bookingData instanceof Notification) {
|
if (null === $bookingData || $bookingData instanceof Notification) {
|
||||||
@@ -103,7 +110,7 @@ class IndexController extends AbstractController
|
|||||||
|
|
||||||
// Handle form submission (clicking "Buchung aktualisieren")
|
// Handle form submission (clicking "Buchung aktualisieren")
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
return $this->handleFormSubmission($request, $bookingDto, $id, $email);
|
return $this->handleFormSubmission($request, $bookingDto, $id, $email, $user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always generate card data with validation state to show completeness
|
// Always generate card data with validation state to show completeness
|
||||||
@@ -202,6 +209,9 @@ class IndexController extends AbstractController
|
|||||||
// Save updated booking data to session
|
// Save updated booking data to session
|
||||||
$this->bookingService->saveBookingDto($request, $bookingDto, BookingDto::MODE_EDIT);
|
$this->bookingService->saveBookingDto($request, $bookingDto, BookingDto::MODE_EDIT);
|
||||||
|
|
||||||
|
// Auto-save draft to database for data persistence
|
||||||
|
$this->draftService->saveDraft($user, $id, $bookingDto);
|
||||||
|
|
||||||
// Add notifications as flash messages (redirects destroy HTMX-triggered toasts)
|
// Add notifications as flash messages (redirects destroy HTMX-triggered toasts)
|
||||||
$this->addNotificationsAsFlashMessages($notifications);
|
$this->addNotificationsAsFlashMessages($notifications);
|
||||||
|
|
||||||
@@ -376,7 +386,7 @@ class IndexController extends AbstractController
|
|||||||
/**
|
/**
|
||||||
* Handles form submission for booking update.
|
* Handles form submission for booking update.
|
||||||
*/
|
*/
|
||||||
private function handleFormSubmission(Request $request, BookingDto $bookingDto, int $id, string $email): Response
|
private function handleFormSubmission(Request $request, BookingDto $bookingDto, int $id, string $email, User $user): Response
|
||||||
{
|
{
|
||||||
$this->logger->info('Initiated booking update', [
|
$this->logger->info('Initiated booking update', [
|
||||||
'email' => $email,
|
'email' => $email,
|
||||||
@@ -401,6 +411,9 @@ class IndexController extends AbstractController
|
|||||||
$this->dataLoader->invalidateBookingCache($id);
|
$this->dataLoader->invalidateBookingCache($id);
|
||||||
$this->bookingService->clearBookingDto($request, BookingDto::MODE_EDIT);
|
$this->bookingService->clearBookingDto($request, BookingDto::MODE_EDIT);
|
||||||
|
|
||||||
|
// Delete draft on successful submission
|
||||||
|
$this->draftService->deleteDraft($user, $id);
|
||||||
|
|
||||||
$this->addFlash('success', 'Buchung erfolgreich aktualisiert');
|
$this->addFlash('success', 'Buchung erfolgreich aktualisiert');
|
||||||
$this->logger->info('Booking update successful', [
|
$this->logger->info('Booking update successful', [
|
||||||
'email' => $email,
|
'email' => $email,
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\BookingEditDraftRepository;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: BookingEditDraftRepository::class)]
|
||||||
|
#[ORM\Table(name: 'booking_edit_draft')]
|
||||||
|
#[ORM\UniqueConstraint(name: 'user_booking_unique', columns: ['user_id', 'booking_id'])]
|
||||||
|
class BookingEditDraft
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||||
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
|
private User $user;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
private int $bookingId;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'json')]
|
||||||
|
private array $formData = [];
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'datetime_immutable')]
|
||||||
|
private \DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'datetime_immutable')]
|
||||||
|
private \DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
|
public function __construct(User $user, int $bookingId, array $formData)
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
$this->bookingId = $bookingId;
|
||||||
|
$this->formData = $formData;
|
||||||
|
$this->createdAt = new \DateTimeImmutable();
|
||||||
|
$this->updatedAt = new \DateTimeImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUser(): User
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBookingId(): int
|
||||||
|
{
|
||||||
|
return $this->bookingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormData(): array
|
||||||
|
{
|
||||||
|
return $this->formData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFormData(array $formData): static
|
||||||
|
{
|
||||||
|
$this->formData = $formData;
|
||||||
|
$this->updatedAt = new \DateTimeImmutable();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): \DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): \DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\BookingEditDraft;
|
||||||
|
use App\Entity\User;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<BookingEditDraft>
|
||||||
|
*/
|
||||||
|
class BookingEditDraftRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, BookingEditDraft::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a draft for a specific user and booking combination.
|
||||||
|
*
|
||||||
|
* @param User $user The user who owns the draft
|
||||||
|
* @param int $bookingId The BusProNet booking ID
|
||||||
|
*
|
||||||
|
* @return BookingEditDraft|null The draft if found, null otherwise
|
||||||
|
*/
|
||||||
|
public function findByUserAndBooking(User $user, int $bookingId): ?BookingEditDraft
|
||||||
|
{
|
||||||
|
return $this->findOneBy([
|
||||||
|
'user' => $user,
|
||||||
|
'bookingId' => $bookingId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a draft for a specific user and booking combination.
|
||||||
|
*
|
||||||
|
* @param User $user The user who owns the draft
|
||||||
|
* @param int $bookingId The BusProNet booking ID
|
||||||
|
*/
|
||||||
|
public function deleteByUserAndBooking(User $user, int $bookingId): void
|
||||||
|
{
|
||||||
|
$this->createQueryBuilder('d')
|
||||||
|
->delete()
|
||||||
|
->where('d.user = :user')
|
||||||
|
->andWhere('d.bookingId = :bookingId')
|
||||||
|
->setParameter('user', $user)
|
||||||
|
->setParameter('bookingId', $bookingId)
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ use App\BusProNet\ApiClient;
|
|||||||
use App\BusProNet\DataProcessor\BookingDataProcessor;
|
use App\BusProNet\DataProcessor\BookingDataProcessor;
|
||||||
use App\BusProNet\Model\Booking;
|
use App\BusProNet\Model\Booking;
|
||||||
use App\BusProNet\Model\Notification;
|
use App\BusProNet\Model\Notification;
|
||||||
|
use App\Entity\User;
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use Psr\Cache\InvalidArgumentException;
|
use Psr\Cache\InvalidArgumentException;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -17,29 +18,54 @@ use Symfony\Contracts\Cache\ItemInterface;
|
|||||||
/**
|
/**
|
||||||
* Handles loading and initializing booking data for edit mode.
|
* Handles loading and initializing booking data for edit mode.
|
||||||
*
|
*
|
||||||
* Encapsulates the logic for loading booking data from session or API
|
* Encapsulates the logic for loading booking data from session or API,
|
||||||
* and refreshing availability data.
|
* refreshing availability data, and restoring drafts when available.
|
||||||
*/
|
*/
|
||||||
class BookingEditDataLoaderService
|
class BookingEditDataLoaderService
|
||||||
{
|
{
|
||||||
|
private bool $draftWasRestored = false;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ApiClient $apiClient,
|
private readonly ApiClient $apiClient,
|
||||||
private readonly BookingDataProcessor $bookingDataProcessor,
|
private readonly BookingDataProcessor $bookingDataProcessor,
|
||||||
private readonly BookingService $bookingService,
|
private readonly BookingService $bookingService,
|
||||||
private readonly BookingFingerprintService $fingerprintService,
|
private readonly BookingFingerprintService $fingerprintService,
|
||||||
private readonly TravelDataService $travelDataService,
|
private readonly TravelDataService $travelDataService,
|
||||||
|
private readonly BookingEditDraftService $draftService,
|
||||||
private readonly CacheInterface $cache,
|
private readonly CacheInterface $cache,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether a draft was restored during the last load operation.
|
||||||
|
*
|
||||||
|
* This flag is reset on each call to loadFormData() and can be used
|
||||||
|
* by the controller to show a flash message to the user.
|
||||||
|
*/
|
||||||
|
public function wasDraftRestored(): bool
|
||||||
|
{
|
||||||
|
return $this->draftWasRestored;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads booking data from session or initializes from API on first load.
|
* Loads booking data from session or initializes from API on first load.
|
||||||
*
|
*
|
||||||
* Validates that any cached session data matches the requested booking ID.
|
* Validates that any cached session data matches the requested booking ID.
|
||||||
* If a different booking is in session, it is cleared and fresh data is loaded.
|
* If a different booking is in session, it is cleared and fresh data is loaded.
|
||||||
|
* If a draft exists for this user and booking, it is automatically applied.
|
||||||
|
*
|
||||||
|
* @param Request $request The HTTP request
|
||||||
|
* @param int $bookingId The booking ID to load
|
||||||
|
* @param string $email User email for API authentication
|
||||||
|
* @param string $password User password for API authentication
|
||||||
|
* @param User $user The authenticated user (for draft lookup)
|
||||||
|
*
|
||||||
|
* @return BookingDto|null The loaded booking DTO, or null if loading failed
|
||||||
*/
|
*/
|
||||||
public function loadFormData(Request $request, int $bookingId, string $email, string $password): ?BookingDto
|
public function loadFormData(Request $request, int $bookingId, string $email, string $password, User $user): ?BookingDto
|
||||||
{
|
{
|
||||||
|
$this->draftWasRestored = false;
|
||||||
|
|
||||||
$formData = $this->bookingService->getBookingDto($request, BookingDto::MODE_EDIT);
|
$formData = $this->bookingService->getBookingDto($request, BookingDto::MODE_EDIT);
|
||||||
|
|
||||||
// Validate session data matches requested booking - clear stale data if mismatched
|
// Validate session data matches requested booking - clear stale data if mismatched
|
||||||
@@ -49,7 +75,7 @@ class BookingEditDataLoaderService
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (null === $formData) {
|
if (null === $formData) {
|
||||||
return $this->initializeFromApi($request, $bookingId, $email, $password);
|
return $this->initializeFromApi($request, $bookingId, $email, $password, $user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh availability data
|
// Refresh availability data
|
||||||
@@ -60,8 +86,20 @@ class BookingEditDataLoaderService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes booking data from API on first load and stores in session.
|
* Initializes booking data from API on first load and stores in session.
|
||||||
|
*
|
||||||
|
* If a draft exists for this user and booking, it is automatically applied
|
||||||
|
* on top of the fresh API data. This ensures user input is preserved while
|
||||||
|
* structural data (services, prices) remains current.
|
||||||
|
*
|
||||||
|
* @param Request $request The HTTP request
|
||||||
|
* @param int $bookingId The booking ID to load
|
||||||
|
* @param string $email User email for API authentication
|
||||||
|
* @param string $password User password for API authentication
|
||||||
|
* @param User $user The authenticated user (for draft lookup)
|
||||||
|
*
|
||||||
|
* @return BookingDto|null The loaded booking DTO, or null if loading failed
|
||||||
*/
|
*/
|
||||||
public function initializeFromApi(Request $request, int $bookingId, string $email, string $password): ?BookingDto
|
public function initializeFromApi(Request $request, int $bookingId, string $email, string $password, User $user): ?BookingDto
|
||||||
{
|
{
|
||||||
$bookingData = $this->fetchBookingData($email, $password, $bookingId);
|
$bookingData = $this->fetchBookingData($email, $password, $bookingId);
|
||||||
|
|
||||||
@@ -87,9 +125,19 @@ class BookingEditDataLoaderService
|
|||||||
|
|
||||||
$formData = $this->bookingDataProcessor->createBookingDtoFromBooking($bookingData, $travelData);
|
$formData = $this->bookingDataProcessor->createBookingDtoFromBooking($bookingData, $travelData);
|
||||||
|
|
||||||
// Set original fingerprint for dirty state detection
|
// Set original fingerprint BEFORE applying draft, so dirty detection
|
||||||
|
// compares against the original API data (not the draft-modified data)
|
||||||
$formData->originalFingerprint = $this->fingerprintService->generateFingerprint($formData, true);
|
$formData->originalFingerprint = $this->fingerprintService->generateFingerprint($formData, true);
|
||||||
|
|
||||||
|
// Check for existing draft and apply if found
|
||||||
|
$draft = $this->draftService->findDraft($user, $bookingId);
|
||||||
|
if (null !== $draft) {
|
||||||
|
$applied = $this->draftService->applyDraftToDto($draft, $formData, $travelData);
|
||||||
|
if ($applied) {
|
||||||
|
$this->draftWasRestored = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$this->bookingService->saveBookingDto($request, $formData, BookingDto::MODE_EDIT);
|
$this->bookingService->saveBookingDto($request, $formData, BookingDto::MODE_EDIT);
|
||||||
|
|
||||||
return $formData;
|
return $formData;
|
||||||
|
|||||||
@@ -0,0 +1,442 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Address;
|
||||||
|
use App\BusProNet\Model\Travel;
|
||||||
|
use App\Entity\BookingEditDraft;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Form\Model\BankAccountDto;
|
||||||
|
use App\Form\Model\BookingDto;
|
||||||
|
use App\Form\Model\ParticipantDto;
|
||||||
|
use App\Repository\BookingEditDraftRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages draft persistence for the booking edit flow.
|
||||||
|
*
|
||||||
|
* This service handles saving, loading, and applying draft data to prevent
|
||||||
|
* data loss when BusProNet API rejects booking update submissions. Drafts
|
||||||
|
* are automatically applied on top of fresh API data when a user returns
|
||||||
|
* to edit a booking.
|
||||||
|
*/
|
||||||
|
class BookingEditDraftService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly BookingEditDraftRepository $draftRepository,
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly BookingFingerprintService $fingerprintService,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds an existing draft for a user and booking combination.
|
||||||
|
*
|
||||||
|
* @param User $user The user who owns the draft
|
||||||
|
* @param int $bookingId The BusProNet booking ID
|
||||||
|
*
|
||||||
|
* @return BookingEditDraft|null The draft if found, null otherwise
|
||||||
|
*/
|
||||||
|
public function findDraft(User $user, int $bookingId): ?BookingEditDraft
|
||||||
|
{
|
||||||
|
return $this->draftRepository->findByUserAndBooking($user, $bookingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves or updates a draft for the given booking.
|
||||||
|
*
|
||||||
|
* Uses an upsert pattern: creates a new draft if none exists,
|
||||||
|
* or updates the existing draft with new form data.
|
||||||
|
*
|
||||||
|
* @param User $user The user who owns the draft
|
||||||
|
* @param int $bookingId The BusProNet booking ID
|
||||||
|
* @param BookingDto $bookingDto The booking DTO containing user edits
|
||||||
|
*/
|
||||||
|
public function saveDraft(User $user, int $bookingId, BookingDto $bookingDto): void
|
||||||
|
{
|
||||||
|
$formData = $this->fingerprintService->extractUserData($bookingDto);
|
||||||
|
$existingDraft = $this->findDraft($user, $bookingId);
|
||||||
|
|
||||||
|
if (null !== $existingDraft) {
|
||||||
|
$existingDraft->setFormData($formData);
|
||||||
|
} else {
|
||||||
|
$draft = new BookingEditDraft($user, $bookingId, $formData);
|
||||||
|
$this->entityManager->persist($draft);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->logger->debug('Saved booking edit draft', [
|
||||||
|
'user_id' => $user->getId(),
|
||||||
|
'booking_id' => $bookingId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a draft after successful booking submission.
|
||||||
|
*
|
||||||
|
* @param User $user The user who owns the draft
|
||||||
|
* @param int $bookingId The BusProNet booking ID
|
||||||
|
*/
|
||||||
|
public function deleteDraft(User $user, int $bookingId): void
|
||||||
|
{
|
||||||
|
$this->draftRepository->deleteByUserAndBooking($user, $bookingId);
|
||||||
|
|
||||||
|
$this->logger->debug('Deleted booking edit draft', [
|
||||||
|
'user_id' => $user->getId(),
|
||||||
|
'booking_id' => $bookingId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies draft data to a fresh BookingDto loaded from the API.
|
||||||
|
*
|
||||||
|
* This method merges user input from the draft onto fresh API data.
|
||||||
|
* The draft contains personal data, service selections, and payment info
|
||||||
|
* that the user previously entered. Services are resolved by ID against
|
||||||
|
* the current Travel data to ensure prices and availability are current.
|
||||||
|
*
|
||||||
|
* @param BookingEditDraft $draft The draft containing saved user data
|
||||||
|
* @param BookingDto $dto The fresh BookingDto from API (modified in place)
|
||||||
|
* @param Travel $travel The current travel data for service resolution
|
||||||
|
*
|
||||||
|
* @return bool True if draft was applied successfully, false if draft data was invalid
|
||||||
|
*/
|
||||||
|
public function applyDraftToDto(BookingEditDraft $draft, BookingDto $dto, Travel $travel): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$formData = $draft->getFormData();
|
||||||
|
|
||||||
|
// Apply payment data
|
||||||
|
if (isset($formData['paymentMethod'])) {
|
||||||
|
$dto->paymentMethod = $formData['paymentMethod'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($formData['bankAccount']) && true === is_array($formData['bankAccount'])) {
|
||||||
|
$this->applyBankAccountData($dto, $formData['bankAccount']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply participant data
|
||||||
|
if (isset($formData['participants']) && true === is_array($formData['participants'])) {
|
||||||
|
foreach ($formData['participants'] as $index => $participantData) {
|
||||||
|
// Only apply to participants that exist in fresh DTO
|
||||||
|
if (false === isset($dto->participants[$index])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->applyParticipantData($dto->participants[$index], $participantData, $travel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->info('Applied draft to booking DTO', [
|
||||||
|
'booking_id' => $draft->getBookingId(),
|
||||||
|
'draft_created_at' => $draft->getCreatedAt()->format('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->logger->error('Failed to apply draft to booking DTO', [
|
||||||
|
'booking_id' => $draft->getBookingId(),
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies bank account data from draft to BookingDto.
|
||||||
|
*/
|
||||||
|
private function applyBankAccountData(BookingDto $dto, array $bankAccountData): void
|
||||||
|
{
|
||||||
|
$iban = $bankAccountData['iban'] ?? null;
|
||||||
|
$accountHolder = $bankAccountData['accountHolder'] ?? null;
|
||||||
|
|
||||||
|
if (null === $iban && null === $accountHolder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $dto->bankAccount) {
|
||||||
|
$dto->bankAccount = new BankAccountDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
$dto->bankAccount->iban = $iban;
|
||||||
|
$dto->bankAccount->accountHolder = $accountHolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies participant data from draft to a ParticipantDto.
|
||||||
|
*/
|
||||||
|
private function applyParticipantData(ParticipantDto $participant, array $data, Travel $travel): void
|
||||||
|
{
|
||||||
|
// Personal data
|
||||||
|
if (isset($data['personalData']) && true === is_array($data['personalData'])) {
|
||||||
|
$this->applyPersonalData($participant, $data['personalData']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address
|
||||||
|
if (isset($data['address']) && true === is_array($data['address'])) {
|
||||||
|
$this->applyAddressData($participant, $data['address']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body dimensions
|
||||||
|
if (isset($data['bodyDimensions']) && true === is_array($data['bodyDimensions'])) {
|
||||||
|
$this->applyBodyDimensions($participant, $data['bodyDimensions']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Room assignment
|
||||||
|
if (isset($data['roomAssignment']) && true === is_array($data['roomAssignment'])) {
|
||||||
|
$this->applyRoomAssignment($participant, $data['roomAssignment']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// License plate
|
||||||
|
if (true === array_key_exists('licensePlate', $data)) {
|
||||||
|
$participant->licensePlate = $data['licensePlate'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Services
|
||||||
|
if (isset($data['services']) && true === is_array($data['services'])) {
|
||||||
|
$this->applyServiceSelections($participant, $data['services'], $travel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vouchers
|
||||||
|
if (isset($data['vouchers']) && true === is_array($data['vouchers'])) {
|
||||||
|
$this->applyVoucherCodes($participant, $data['vouchers']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies personal data fields to participant.
|
||||||
|
*/
|
||||||
|
private function applyPersonalData(ParticipantDto $participant, array $data): void
|
||||||
|
{
|
||||||
|
if (true === array_key_exists('firstName', $data)) {
|
||||||
|
$participant->firstName = $data['firstName'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('lastName', $data)) {
|
||||||
|
$participant->lastName = $data['lastName'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('dateOfBirth', $data) && null !== $data['dateOfBirth']) {
|
||||||
|
$participant->dateOfBirth = new \DateTimeImmutable($data['dateOfBirth']);
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('email', $data)) {
|
||||||
|
$participant->email = $data['email'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('mobile', $data)) {
|
||||||
|
$participant->mobile = $data['mobile'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('gender', $data)) {
|
||||||
|
$participant->gender = $data['gender'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('nationality', $data)) {
|
||||||
|
$participant->nationality = $data['nationality'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies address data to participant.
|
||||||
|
*/
|
||||||
|
private function applyAddressData(ParticipantDto $participant, array $data): void
|
||||||
|
{
|
||||||
|
$hasAddressData = null !== ($data['street'] ?? null)
|
||||||
|
|| null !== ($data['postCode'] ?? null)
|
||||||
|
|| null !== ($data['city'] ?? null)
|
||||||
|
|| null !== ($data['country'] ?? null);
|
||||||
|
|
||||||
|
if (false === $hasAddressData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $participant->address) {
|
||||||
|
$participant->address = new Address();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (true === array_key_exists('street', $data)) {
|
||||||
|
$participant->address->street = $data['street'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('postCode', $data)) {
|
||||||
|
$participant->address->postCode = $data['postCode'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('city', $data)) {
|
||||||
|
$participant->address->city = $data['city'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('country', $data)) {
|
||||||
|
$participant->address->country = $data['country'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies body dimension data to participant.
|
||||||
|
*/
|
||||||
|
private function applyBodyDimensions(ParticipantDto $participant, array $data): void
|
||||||
|
{
|
||||||
|
if (true === array_key_exists('height', $data)) {
|
||||||
|
$participant->height = $data['height'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('weight', $data)) {
|
||||||
|
$participant->weight = $data['weight'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('shoeSize', $data)) {
|
||||||
|
$participant->shoeSize = $data['shoeSize'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies room assignment data to participant.
|
||||||
|
*/
|
||||||
|
private function applyRoomAssignment(ParticipantDto $participant, array $data): void
|
||||||
|
{
|
||||||
|
if (true === array_key_exists('assignedRoomId', $data)) {
|
||||||
|
$participant->assignedRoomId = $data['assignedRoomId'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('remarksRoom', $data)) {
|
||||||
|
$participant->remarksRoom = $data['remarksRoom'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies service selections to participant, resolving IDs against Travel data.
|
||||||
|
*/
|
||||||
|
private function applyServiceSelections(ParticipantDto $participant, array $data, Travel $travel): void
|
||||||
|
{
|
||||||
|
// Ski pass (single service)
|
||||||
|
if (true === array_key_exists('skiPass', $data)) {
|
||||||
|
$participant->skiPass = $this->resolveService($data['skiPass'], $travel->additionalServices);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Courses (array)
|
||||||
|
if (true === array_key_exists('courses', $data) && true === is_array($data['courses'])) {
|
||||||
|
$participant->courses = $this->resolveServiceArray($data['courses'], $travel->additionalServices);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Board (array)
|
||||||
|
if (true === array_key_exists('board', $data) && true === is_array($data['board'])) {
|
||||||
|
$participant->board = $this->resolveServiceArray($data['board'], $travel->additionalServices);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rentals (array)
|
||||||
|
if (true === array_key_exists('rentals', $data) && true === is_array($data['rentals'])) {
|
||||||
|
$participant->rentals = $this->resolveServiceArray($data['rentals'], $travel->additionalServices);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rental insurance (single)
|
||||||
|
if (true === array_key_exists('rentalInsurance', $data)) {
|
||||||
|
$participant->rentalInsurance = $this->resolveService($data['rentalInsurance'], $travel->additionalServices);
|
||||||
|
$participant->rentalInsuranceSelected = null !== $participant->rentalInsurance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional services (array)
|
||||||
|
if (true === array_key_exists('additionalServices', $data) && true === is_array($data['additionalServices'])) {
|
||||||
|
$participant->additionalServices = $this->resolveServiceArray($data['additionalServices'], $travel->additionalServices);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transportation outbound (single)
|
||||||
|
if (true === array_key_exists('transportationOutbound', $data)) {
|
||||||
|
$participant->transportationOutbound = $this->resolveService($data['transportationOutbound'], $travel->transportationServices);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transportation inbound (single)
|
||||||
|
if (true === array_key_exists('transportationInbound', $data)) {
|
||||||
|
$participant->transportationInbound = $this->resolveService($data['transportationInbound'], $travel->transportationServices);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pickup (single)
|
||||||
|
if (true === array_key_exists('pickup', $data)) {
|
||||||
|
$participant->pickup = $this->resolvePickup($data['pickup'], $travel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parking (boolean)
|
||||||
|
if (true === array_key_exists('parking', $data)) {
|
||||||
|
$participant->parking = (bool) $data['parking'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insurance (single)
|
||||||
|
if (true === array_key_exists('insurance', $data)) {
|
||||||
|
$participant->insurance = $this->resolveInsurance($data['insurance'], $travel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulk insurance booking (boolean)
|
||||||
|
if (true === array_key_exists('bulkInsuranceBooking', $data)) {
|
||||||
|
$participant->bulkInsuranceBooking = (bool) $data['bulkInsuranceBooking'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies voucher codes to participant.
|
||||||
|
*/
|
||||||
|
private function applyVoucherCodes(ParticipantDto $participant, array $data): void
|
||||||
|
{
|
||||||
|
if (true === array_key_exists('purchaseVoucherCode', $data)) {
|
||||||
|
$participant->purchaseVoucherCode = $data['purchaseVoucherCode'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('promoVoucherCode', $data)) {
|
||||||
|
$participant->promoVoucherCode = $data['promoVoucherCode'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a single service ID to a Service object from the available services.
|
||||||
|
*
|
||||||
|
* @param int|null $serviceId The service ID to resolve
|
||||||
|
* @param array $services Available services indexed by ID
|
||||||
|
*
|
||||||
|
* @return object|null The service object if found, null otherwise
|
||||||
|
*/
|
||||||
|
private function resolveService(?int $serviceId, array $services): ?object
|
||||||
|
{
|
||||||
|
if (null === $serviceId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $services[$serviceId] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves an array of service IDs to Service objects.
|
||||||
|
*
|
||||||
|
* @param array $serviceIds Array of service IDs
|
||||||
|
* @param array $services Available services indexed by ID
|
||||||
|
*
|
||||||
|
* @return array Array of resolved Service objects (missing services are skipped)
|
||||||
|
*/
|
||||||
|
private function resolveServiceArray(array $serviceIds, array $services): array
|
||||||
|
{
|
||||||
|
$resolved = [];
|
||||||
|
|
||||||
|
foreach ($serviceIds as $serviceId) {
|
||||||
|
if (isset($services[$serviceId])) {
|
||||||
|
$resolved[] = $services[$serviceId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a pickup ID to a Pickup object.
|
||||||
|
*/
|
||||||
|
private function resolvePickup(?int $pickupId, Travel $travel): ?object
|
||||||
|
{
|
||||||
|
if (null === $pickupId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $travel->pickupsOutbound[$pickupId] ?? $travel->pickupsInbound[$pickupId] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves an insurance ID to an Insurance object.
|
||||||
|
*/
|
||||||
|
private function resolveInsurance(?int $insuranceId, Travel $travel): ?object
|
||||||
|
{
|
||||||
|
if (null === $insuranceId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $travel->insurances[$insuranceId] ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,14 @@ declare(strict_types=1);
|
|||||||
namespace App\Service;
|
namespace App\Service;
|
||||||
|
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
|
use App\Form\Model\ParticipantDto;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates fingerprints of booking state for change detection in edit mode.
|
* Generates fingerprints of booking state for change detection in edit mode.
|
||||||
*
|
*
|
||||||
* Creates SHA-256 hashes of all mutable booking data to detect unsaved changes.
|
* Creates SHA-256 hashes of all mutable booking data to detect unsaved changes.
|
||||||
* Used by EditController to determine if user modifications need to be saved.
|
* Used by EditController to determine if user modifications need to be saved.
|
||||||
|
* Also provides data extraction for draft persistence.
|
||||||
*/
|
*/
|
||||||
class BookingFingerprintService
|
class BookingFingerprintService
|
||||||
{
|
{
|
||||||
@@ -21,6 +23,22 @@ class BookingFingerprintService
|
|||||||
* personal information, addresses, body dimensions, room assignments, and service selections.
|
* personal information, addresses, body dimensions, room assignments, and service selections.
|
||||||
*/
|
*/
|
||||||
public function generateFingerprint(BookingDto $bookingDto, bool $logData = false): string
|
public function generateFingerprint(BookingDto $bookingDto, bool $logData = false): string
|
||||||
|
{
|
||||||
|
return hash('sha256', serialize($this->extractUserData($bookingDto)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts all user-editable data from a BookingDto as a serializable array.
|
||||||
|
*
|
||||||
|
* This method captures all data that represents user input: personal information,
|
||||||
|
* addresses, body dimensions, service selections, voucher codes, and payment details.
|
||||||
|
* The resulting array can be used for fingerprint generation or draft persistence.
|
||||||
|
*
|
||||||
|
* @param BookingDto $bookingDto The booking DTO to extract data from
|
||||||
|
*
|
||||||
|
* @return array Serializable array of all user-editable data
|
||||||
|
*/
|
||||||
|
public function extractUserData(BookingDto $bookingDto): array
|
||||||
{
|
{
|
||||||
$data = [
|
$data = [
|
||||||
'paymentMethod' => $bookingDto->paymentMethod,
|
'paymentMethod' => $bookingDto->paymentMethod,
|
||||||
@@ -32,50 +50,66 @@ class BookingFingerprintService
|
|||||||
];
|
];
|
||||||
|
|
||||||
foreach ($bookingDto->participants as $index => $participant) {
|
foreach ($bookingDto->participants as $index => $participant) {
|
||||||
$data['participants'][$index] = [
|
$data['participants'][$index] = $this->extractParticipantData($participant);
|
||||||
'personalData' => [
|
|
||||||
'firstName' => $participant->firstName,
|
|
||||||
'lastName' => $participant->lastName,
|
|
||||||
'dateOfBirth' => $participant->dateOfBirth?->format('Y-m-d'),
|
|
||||||
'email' => $participant->email,
|
|
||||||
'mobile' => $participant->mobile,
|
|
||||||
'gender' => $participant->gender,
|
|
||||||
'nationality' => $participant->nationality,
|
|
||||||
],
|
|
||||||
'address' => [
|
|
||||||
'street' => $participant->address?->street,
|
|
||||||
'postCode' => $participant->address?->postCode,
|
|
||||||
'city' => $participant->address?->city,
|
|
||||||
'country' => $participant->address?->country,
|
|
||||||
],
|
|
||||||
'bodyDimensions' => [
|
|
||||||
'height' => $participant->height,
|
|
||||||
'weight' => $participant->weight,
|
|
||||||
'shoeSize' => $participant->shoeSize,
|
|
||||||
],
|
|
||||||
'roomAssignment' => [
|
|
||||||
'assignedRoomId' => $participant->assignedRoomId,
|
|
||||||
'remarksRoom' => $participant->remarksRoom,
|
|
||||||
],
|
|
||||||
'licensePlate' => $participant->licensePlate,
|
|
||||||
'services' => [
|
|
||||||
'skiPass' => $participant->skiPass?->id,
|
|
||||||
'courses' => $this->normalizeServiceArray($participant->courses),
|
|
||||||
'board' => $this->normalizeServiceArray($participant->board),
|
|
||||||
'rentals' => $this->normalizeServiceArray($participant->rentals),
|
|
||||||
'rentalInsurance' => $participant->rentalInsurance?->id,
|
|
||||||
'additionalServices' => $this->normalizeServiceArray($participant->additionalServices),
|
|
||||||
'transportationOutbound' => $participant->transportationOutbound?->id,
|
|
||||||
'transportationInbound' => $participant->transportationInbound?->id,
|
|
||||||
'pickup' => $participant->pickup?->id,
|
|
||||||
'parking' => $participant->parking,
|
|
||||||
'insurance' => $participant->insurance?->id,
|
|
||||||
'bulkInsuranceBooking' => $participant->bulkInsuranceBooking,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return hash('sha256', serialize($data));
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts user-editable data from a single participant.
|
||||||
|
*
|
||||||
|
* @param ParticipantDto $participant The participant to extract data from
|
||||||
|
*
|
||||||
|
* @return array Serializable array of participant data
|
||||||
|
*/
|
||||||
|
private function extractParticipantData(ParticipantDto $participant): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'personalData' => [
|
||||||
|
'firstName' => $participant->firstName,
|
||||||
|
'lastName' => $participant->lastName,
|
||||||
|
'dateOfBirth' => $participant->dateOfBirth?->format('Y-m-d'),
|
||||||
|
'email' => $participant->email,
|
||||||
|
'mobile' => $participant->mobile,
|
||||||
|
'gender' => $participant->gender,
|
||||||
|
'nationality' => $participant->nationality,
|
||||||
|
],
|
||||||
|
'address' => [
|
||||||
|
'street' => $participant->address?->street,
|
||||||
|
'postCode' => $participant->address?->postCode,
|
||||||
|
'city' => $participant->address?->city,
|
||||||
|
'country' => $participant->address?->country,
|
||||||
|
],
|
||||||
|
'bodyDimensions' => [
|
||||||
|
'height' => $participant->height,
|
||||||
|
'weight' => $participant->weight,
|
||||||
|
'shoeSize' => $participant->shoeSize,
|
||||||
|
],
|
||||||
|
'roomAssignment' => [
|
||||||
|
'assignedRoomId' => $participant->assignedRoomId,
|
||||||
|
'remarksRoom' => $participant->remarksRoom,
|
||||||
|
],
|
||||||
|
'licensePlate' => $participant->licensePlate,
|
||||||
|
'services' => [
|
||||||
|
'skiPass' => $participant->skiPass?->id,
|
||||||
|
'courses' => $this->normalizeServiceArray($participant->courses),
|
||||||
|
'board' => $this->normalizeServiceArray($participant->board),
|
||||||
|
'rentals' => $this->normalizeServiceArray($participant->rentals),
|
||||||
|
'rentalInsurance' => $participant->rentalInsurance?->id,
|
||||||
|
'additionalServices' => $this->normalizeServiceArray($participant->additionalServices),
|
||||||
|
'transportationOutbound' => $participant->transportationOutbound?->id,
|
||||||
|
'transportationInbound' => $participant->transportationInbound?->id,
|
||||||
|
'pickup' => $participant->pickup?->id,
|
||||||
|
'parking' => $participant->parking,
|
||||||
|
'insurance' => $participant->insurance?->id,
|
||||||
|
'bulkInsuranceBooking' => $participant->bulkInsuranceBooking,
|
||||||
|
],
|
||||||
|
'vouchers' => [
|
||||||
|
'purchaseVoucherCode' => $participant->purchaseVoucherCode,
|
||||||
|
'promoVoucherCode' => $participant->promoVoucherCode,
|
||||||
|
],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user