feat(reservation/flow): Améliore le flux de réservation et ajoute des options.

Cette commit améliore le flux de réservation, ajoute une estimation des
frais de livraison et gère les options de produit et les paiements.
```
This commit is contained in:
Serreau Jovann
2026-02-05 08:18:29 +01:00
parent c837095cc3
commit 1896f83107
28 changed files with 1654 additions and 215 deletions

View File

@@ -50,13 +50,18 @@ class ReserverController extends AbstractController
string $sessionId,
OrderSessionRepository $repository,
ProductRepository $productRepository,
KernelInterface $kernel
KernelInterface $kernel,
\App\Repository\OptionsRepository $optionsRepository
): Response {
$session = $repository->findOneBy(['uuid' => $sessionId]);
if (!$session) {
return $this->redirectToRoute('reservation');
}
if ($session->getState() === 'send') {
return $this->redirectToRoute('reservation_flow_success', ['sessionId' => $sessionId]);
}
$sessionData = $session->getProducts();
$ids = $sessionData['ids'] ?? [];
$startStr = $sessionData['start'] ?? null;
@@ -106,15 +111,50 @@ class ReserverController extends AbstractController
$devis->setStartAt($start);
$devis->setEndAt($end);
$selectedOptionsMap = $sessionData['options'] ?? [];
if (!empty($ids)) {
$products = $productRepository->findBy(['id' => $ids]);
$processedProductIds = [];
foreach ($products as $product) {
$processedProductIds[] = $product->getId();
$line = new DevisLine();
$line->setProduct($product->getName());
$line->setPriceHt($product->getPriceDay());
$line->setPriceHtSup($product->getPriceSup());
$line->setDay($duration);
$devis->addDevisLine($line);
if (isset($selectedOptionsMap[$product->getId()])) {
$optionIds = $selectedOptionsMap[$product->getId()];
if (!empty($optionIds)) {
$options = $optionsRepository->findBy(['id' => $optionIds]);
foreach ($options as $option) {
$lineOpt = new DevisLine();
$lineOpt->setProduct("Option : " . $option->getName());
$lineOpt->setPriceHt($option->getPriceHt());
$lineOpt->setPriceHtSup(0);
$lineOpt->setDay($duration);
$devis->addDevisLine($lineOpt);
}
}
}
}
foreach ($selectedOptionsMap as $prodId => $optIds) {
if (!in_array($prodId, $processedProductIds) && !empty($optIds)) {
$options = $optionsRepository->findBy(['id' => $optIds]);
foreach ($options as $option) {
$lineOpt = new DevisLine();
$lineOpt->setProduct("Option : " . $option->getName());
$lineOpt->setPriceHt($option->getPriceHt());
$lineOpt->setPriceHtSup(0);
$lineOpt->setDay($duration);
$devis->addDevisLine($lineOpt);
}
}
}
}
@@ -494,13 +534,35 @@ class ReserverController extends AbstractController
UploaderHelper $uploaderHelper,
ProductReserveRepository $productReserveRepository,
EntityManagerInterface $em,
\App\Repository\OptionsRepository $optionsRepository
\App\Repository\OptionsRepository $optionsRepository,
HttpClientInterface $client,
Request $request,
Mailer $mailer
): Response {
$session = $repository->findOneBy(['uuid' => $sessionId]);
if (!$session) {
return $this->render('revervation/session_lost.twig');
}
if ($session->getState() === 'send') {
return $this->redirectToRoute('reservation_flow_success', ['sessionId' => $sessionId]);
}
if ($request->isMethod('POST')) {
$mailer->send(
'contact@ludikevent.fr',
"Ludikevent",
"[Ludikevent] - Nouvelle demande de réservation",
"mails/reserve/confirmation.twig",
['session' => $session]
);
$session->setState('send');
$em->flush();
$request->getSession()->remove('order_session_uuid');
return $this->redirectToRoute('reservation_flow_success', ['sessionId' => $sessionId]);
}
$sessionData = $session->getProducts();
$ids = $sessionData['ids'] ?? [];
$selectedOptionsMap = $sessionData['options'] ?? [];
@@ -620,6 +682,73 @@ class ReserverController extends AbstractController
$totalTva = $totalHT * $tvaRate;
$totalTTC = $totalHT + $totalTva;
// --- Calcul Frais de Livraison ---
$deliveryEstimation = null;
$deliveryDetails = null;
$deliveryGeometry = null;
if ($session->getAdressEvent() && $session->getZipCodeEvent() && $session->getTownEvent()) {
$query = sprintf('%s %s %s', $session->getAdressEvent(), $session->getZipCodeEvent(), $session->getTownEvent());
try {
$response = $client->request('GET', 'https://api-adresse.data.gouv.fr/search/', [
'query' => [
'q' => $query,
'limit' => 1
]
]);
$content = $response->toArray();
if (!empty($content['features'])) {
$coords = $content['features'][0]['geometry']['coordinates'];
$lon = $coords[0];
$lat = $coords[1];
// Point de départ (LudikEvent)
$startLat = 49.849;
$startLon = 3.286;
// Calcul itinéraire via API Geoplateforme
$itineraireResponse = $client->request('GET', 'https://data.geopf.fr/navigation/itineraire', [
'query' => [
'resource' => 'bdtopo-osrm',
'start' => $startLon . ',' . $startLat,
'end' => $lon . ',' . $lat,
'profile' => 'car',
'optimization' => 'fastest',
'distanceUnit' => 'kilometer',
'geometryFormat' => 'geojson'
]
]);
$itineraire = $itineraireResponse->toArray();
$distance = $itineraire['distance'];
$deliveryGeometry = $itineraire['geometry'] ?? null;
$rate = 0.50;
$trips = 4;
if ($distance <= 10) {
$deliveryEstimation = 0.0;
$chargedDistance = 0.0;
} else {
$chargedDistance = $distance - 10;
$deliveryEstimation = ($chargedDistance * $trips) * $rate;
}
$deliveryDetails = [
'distance' => $distance,
'chargedDistance' => $chargedDistance,
'trips' => $trips,
'rate' => $rate,
'isFree' => ($distance <= 10)
];
}
} catch (\Exception $e) {
// Silent fail for delivery calculation in flow
}
}
return $this->render('revervation/flow_confirmed.twig', [
'session' => $session,
'cart' => [
@@ -632,10 +761,31 @@ class ReserverController extends AbstractController
'totalTva' => $totalTva,
'totalTTC' => $totalTTC,
'tvaEnabled' => $tvaEnabled,
],
'delivery' => [
'estimation' => $deliveryEstimation,
'details' => $deliveryDetails,
'geometry' => $deliveryGeometry
]
]);
}
#[Route('/flow/{sessionId}/success', name: 'reservation_flow_success', methods: ['GET'])]
public function flowSuccess(string $sessionId, OrderSessionRepository $repository): Response
{
$session = $repository->findOneBy(['uuid' => $sessionId]);
if (!$session) {
return $this->redirectToRoute('reservation');
}
if ($session->getState() !== 'send') {
return $this->redirectToRoute('reservation_flow', ['sessionId' => $sessionId]);
}
return $this->render('revervation/success.twig');
}
#[Route('/flow/{sessionId}', name: 'reservation_flow', methods: ['GET', 'POST'])]
public function flowLogin(
string $sessionId,
@@ -643,8 +793,9 @@ class ReserverController extends AbstractController
OrderSessionRepository $repository,
ProductRepository $productRepository,
UploaderHelper $uploaderHelper,
ProductReserveRepository $productReserveRepository, // Added dependency
EntityManagerInterface $em
ProductReserveRepository $productReserveRepository,
EntityManagerInterface $em,
\App\Repository\OptionsRepository $optionsRepository
): Response {
// This is the POST target for the login form, but also the GET page.
// The authenticator handles the POST. For GET, we just render the page.
@@ -653,8 +804,13 @@ class ReserverController extends AbstractController
return $this->render('revervation/session_lost.twig');
}
if ($session->getState() === 'send') {
return $this->redirectToRoute('reservation_flow_success', ['sessionId' => $sessionId]);
}
$sessionData = $session->getProducts();
$ids = $sessionData['ids'] ?? [];
$selectedOptionsMap = $sessionData['options'] ?? [];
$startStr = $sessionData['start'] ?? null;
$endStr = $sessionData['end'] ?? null;
@@ -698,12 +854,45 @@ class ReserverController extends AbstractController
$tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
$tvaRate = $tvaEnabled ? 0.20 : 0;
$rootOptions = [];
$processedProductIds = [];
foreach ($products as $product) {
$processedProductIds[] = $product->getId();
$price1Day = $product->getPriceDay();
$priceSup = $product->getPriceSup() ?? 0.0;
// Calcul du coût total pour ce produit selon la durée
$productTotalHT = $price1Day + ($priceSup * max(0, $duration - 1));
// Traitement des options
$productOptions = [];
$optionsTotalHT = 0;
if (isset($selectedOptionsMap[$product->getId()])) {
$optionIds = $selectedOptionsMap[$product->getId()];
if (!empty($optionIds)) {
$optionsEntities = $optionsRepository->findBy(['id' => $optionIds]);
foreach ($optionsEntities as $option) {
$optPrice = $option->getPriceHt();
$optData = [
'id' => $option->getId(),
'name' => $option->getName(),
'price' => $optPrice
];
if ($product->getOptions()->contains($option)) {
$productOptions[] = $optData;
$optionsTotalHT += $optPrice;
} else {
$rootOptions[] = $optData;
$totalHT += $optPrice;
}
}
}
}
$productTotalHT += $optionsTotalHT;
$productTotalTTC = $productTotalHT * (1 + $tvaRate);
$items[] = [
@@ -713,11 +902,28 @@ class ReserverController extends AbstractController
'priceSup' => $priceSup,
'totalPriceHT' => $productTotalHT,
'totalPriceTTC' => $productTotalTTC,
'options' => $productOptions
];
$totalHT += $productTotalHT;
}
// Traiter les options orphelines
foreach ($selectedOptionsMap as $prodId => $optIds) {
if (!in_array($prodId, $processedProductIds) && !empty($optIds)) {
$optionsEntities = $optionsRepository->findBy(['id' => $optIds]);
foreach ($optionsEntities as $option) {
$optPrice = $option->getPriceHt();
$rootOptions[] = [
'id' => $option->getId(),
'name' => $option->getName(),
'price' => $optPrice
];
$totalHT += $optPrice;
}
}
}
$totalTva = $totalHT * $tvaRate;
$totalTTC = $totalHT + $totalTva;
@@ -727,6 +933,7 @@ class ReserverController extends AbstractController
'error' => $authenticationUtils->getLastAuthenticationError(),
'cart' => [
'items' => $items,
'options' => $rootOptions,
'startDate' => $startStr ? new \DateTimeImmutable($startStr) : null,
'endDate' => $endStr ? new \DateTimeImmutable($endStr) : null,
'duration' => $duration,
@@ -750,6 +957,10 @@ class ReserverController extends AbstractController
return $this->redirectToRoute('reservation');
}
if ($session->getState() === 'send') {
return $this->redirectToRoute('reservation_flow_success', ['sessionId' => $sessionId]);
}
$session->setBillingAddress($request->request->get('billingAddress'));
$session->setBillingZipCode($request->request->get('billingZipCode'));
$session->setBillingTown($request->request->get('billingTown'));
@@ -922,6 +1133,8 @@ class ReserverController extends AbstractController
if ($customer->getType() === 'buisness') {
$customer->setSiret($payload->getString('siret'));
$customer->setRaisonSocial($payload->getString('raisonSocial'));
$customer->setTypCompany($payload->getString('typCompany'));
}
$hashedPassword = $hasher->hashPassword($customer, $payload->getString('password'));