src/MDS/ApiBundle/Controller/ApiProposalsClientController.php line 424

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\MDS\ApiBundle\Controller;
  4. use App\Entity\ClientContact;
  5. use App\MDS\VenuesBundle\Entity\Reservation;
  6. use App\MDS\VenuesBundle\Entity\ReservationLoungeDetails;
  7. use App\MDS\VenuesBundle\Entity\ReservationLoungeDescription;
  8. use App\MDS\VenuesBundle\Entity\ReservationLoungeSimple;
  9. use App\MDS\VenuesBundle\Entity\ReservationLoungePicture;
  10. use App\MDS\VenuesBundle\Entity\ReservationService;
  11. use App\MDS\AvexpressBundle\Entity\AveProduct;
  12. use App\MDS\AvexpressBundle\Entity\AvePackageTemplate;
  13. use App\MDS\AvexpressBundle\Entity\AvePackageTemplateItems;
  14. use Doctrine\ORM\EntityManagerInterface;
  15. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  16. use Symfony\Component\HttpFoundation\JsonResponse;
  17. use Symfony\Component\HttpFoundation\Request;
  18. use Symfony\Component\HttpFoundation\Response;
  19. use Symfony\Component\Routing\Annotation\Route;
  20. class ApiProposalsClientController extends AbstractController
  21. {
  22.     public function __construct(
  23.         private readonly EntityManagerInterface $em
  24.     ) {}
  25.     
  26. /**
  27.      * @Route("/api/client/proposal/{token}/recommended/avproducts", name="api_proposal_recommended_products", methods={"GET", "POST"})
  28.      * Recoger la selección de productos y packs enviados por el cliente desde la web y asociarlos a la reserva general por Opción
  29.      */
  30.     public function processRecommendedProductsAction(string $tokenRequest $request): JsonResponse
  31.     {
  32.         /** @var Reservation|null $reservation */
  33.         $reservation $this->em->getRepository(Reservation::class)->findOneBy(['token' => $token]);
  34.         
  35.         if (!$reservation) {
  36.             return new JsonResponse([
  37.                 'success' => false
  38.                 'error' => 'No se encontró ninguna reserva asociada al token proporcionado.'
  39.             ], Response::HTTP_NOT_FOUND);
  40.         }
  41.         $content $request->getContent();
  42.         if (empty($content)) {
  43.             return new JsonResponse(['success' => false'error' => 'El cuerpo de la petición está vacío.'], Response::HTTP_BAD_REQUEST);
  44.         }
  45.         $payload json_decode($contenttrue);
  46.         if (json_last_error() !== JSON_ERROR_NONE) {
  47.             return new JsonResponse(['success' => false'error' => 'Formato JSON inválido.'], Response::HTTP_BAD_REQUEST);
  48.         }
  49.         // Aceptamos tanto la clave "products" como "avProducts" enviada desde la web
  50.         $rawProducts $payload['products'] ?? $payload['avProducts'] ?? null;
  51.         if (!is_array($rawProducts)) {
  52.             return new JsonResponse(['success' => false'error' => 'Estructura JSON incorrecta (se esperaba array de productos).'], Response::HTTP_BAD_REQUEST);
  53.         }
  54.         $processedItems = [];
  55.         $errors = [];
  56.         $ahora = new \DateTime("now");
  57.         
  58.         $userId $this->getUser() && method_exists($this->getUser(), 'getId') ? $this->getUser()->getId() : 1;
  59.         $this->em->beginTransaction();
  60.         try {
  61.             // Actualización de ajustes de Lounges si se envían
  62.             if (isset($payload['loungeSettings']) && is_array($payload['loungeSettings'])) {
  63.                 foreach ($payload['loungeSettings'] as $setting) {
  64.                     $loungeId filter_var($setting['loungeId'] ?? nullFILTER_VALIDATE_INT);
  65.                     if ($loungeId) {
  66.                         $loungeEntity $this->em->getRepository(ReservationLoungeSimple::class)->find($loungeId);
  67.                         if ($loungeEntity && (int)$loungeEntity->getIdReservation() === (int)$reservation->getId()) {
  68.                             if (isset($setting['showAudiovisualPrice'])) {
  69.                                 $loungeEntity->setShowAudiovisualPrice((bool)$setting['showAudiovisualPrice']);
  70.                             }
  71.                             if (isset($setting['showAudiovisualPackage'])) {
  72.                                 $loungeEntity->setShowAudiovisualPackage((bool)$setting['showAudiovisualPackage']);
  73.                             }
  74.                             $this->em->persist($loungeEntity);
  75.                         }
  76.                     }
  77.                 }
  78.             }
  79.             $individualProducts = [];
  80.             $packagesToConsolidate = [];
  81.             $receivedKeys = [];
  82.             $activeRanksInPayload = [];
  83.             
  84.             // MAPA PARA CONTROLAR Y DESDUPLICAR ELEMENTOS REPETIDOS EN EL MISMO PAYLOAD
  85.             $seenProductByOptionAndDate = [];
  86.             foreach ($rawProducts as $index => $item) {
  87.                 $sourceType filter_var($item['sourceType'] ?? nullFILTER_UNSAFE_RAW);
  88.                 $packId     filter_var($item['packId'] ?? nullFILTER_VALIDATE_INT);
  89.                 $productId  filter_var($item['productId'] ?? nullFILTER_VALIDATE_INT);
  90.                 $dateStr    = !empty($item['date']) ? trim((string)$item['date']) : null;
  91.                 $quantity   = isset($item['quantity']) ? (float)$item['quantity'] : 0.0;
  92.                 
  93.                 $optionKey  filter_var($item['optionKey'] ?? nullFILTER_VALIDATE_INT);
  94.                 $rankQuote  null;
  95.                 if ($optionKey) {
  96.                     /** @var ReservationLoungeSimple|null $loungeRef */
  97.                     $loungeRef $this->em->getRepository(ReservationLoungeSimple::class)->find($optionKey);
  98.                     if ($loungeRef) {
  99.                         $rankQuote $loungeRef->getRankQuote();
  100.                     }
  101.                 }
  102.                 if ($rankQuote !== null) {
  103.                     $activeRanksInPayload[$rankQuote] = true;
  104.                 }
  105.                 if ($sourceType === 'avPackage' && $packId && !$productId) {
  106.                     $key sprintf('%d-%s-%s'$packId$rankQuote ?? 'no-rank'$dateStr ?? 'no-date');
  107.                     if (!isset($packagesToConsolidate[$key])) {
  108.                         $packagesToConsolidate[$key] = [
  109.                             'packId' => $packId,
  110.                             'rankQuote' => $rankQuote,
  111.                             'date' => $dateStr,
  112.                             'index' => $index
  113.                         ];
  114.                     }
  115.                 } 
  116.                 elseif ($productId && $quantity 0) {
  117.                     // Prevenimos la duplicación si el frontend envía el mismo productId para la misma Opción y Fecha
  118.                     $dedupKey sprintf('%d_%s_%s'$productId$rankQuote ?? 'norank'$dateStr ?? 'nodate');
  119.                     if (isset($seenProductByOptionAndDate[$dedupKey])) {
  120.                         continue;
  121.                     }
  122.                     $seenProductByOptionAndDate[$dedupKey] = true;
  123.                     $individualProducts[] = [
  124.                         'item' => $item,
  125.                         'index' => $index
  126.                     ];
  127.                     $key sprintf('%d_%s'$productId$rankQuote ?? 'norank');
  128.                     $receivedKeys[$key] = true;
  129.                 }
  130.             }
  131.             // --- RECONCILIACIÓN POR AUSENCIA (Acotada a los ranks recibidos) ---
  132.             $ranksToProcess array_keys($activeRanksInPayload);
  133.             if (!empty($ranksToProcess)) {
  134.                 $qbExisting $this->em->createQueryBuilder()
  135.                     ->select('s')
  136.                     ->from(ReservationService::class, 's')
  137.                     ->where('s.reservationId = :resId')
  138.                     ->andWhere('s.toinvoice = true')
  139.                     ->andWhere('s.rank IN (:ranks)')
  140.                     ->setParameter('resId'$reservation->getId())
  141.                     ->setParameter('ranks'$ranksToProcess);
  142.                 /** @var ReservationService[] $existingServicesForRanks */
  143.                 $existingServicesForRanks $qbExisting->getQuery()->getResult();
  144.                 foreach ($existingServicesForRanks as $dbService) {
  145.                     $dbServiceName $dbService->getName() ?? '';
  146.                     if (str_contains($dbServiceName'[Recomendado]')) {
  147.                         $dbProductId = (int) $dbService->getServiceId();
  148.                         $dbRank      $dbService->getRank() ?? 'norank';
  149.                         $dbKey sprintf('%d_%s'$dbProductId$dbRank);
  150.                         if (!isset($receivedKeys[$dbKey])) {
  151.                             $this->em->remove($dbService);
  152.                         }
  153.                     }
  154.                 }
  155.             }
  156.             // --- PROCESAR PRODUCTOS INDIVIDUALES (SOLO FECHA, SIN HORA) ---
  157.             foreach ($individualProducts as $wrapped) {
  158.                 $item  $wrapped['item'];
  159.                 $index $wrapped['index'];
  160.                 $productId filter_var($item['productId'] ?? nullFILTER_VALIDATE_INT);
  161.                 $quantity  = (float) $item['quantity'];
  162.                 $dateStr   = !empty($item['date']) ? trim((string)$item['date']) : null;
  163.                 
  164.                 $optionKey filter_var($item['optionKey'] ?? nullFILTER_VALIDATE_INT);
  165.                 $rankQuote null;
  166.                 if ($optionKey) {
  167.                     /** @var ReservationLoungeSimple|null $loungeRef */
  168.                     $loungeRef $this->em->getRepository(ReservationLoungeSimple::class)->find($optionKey);
  169.                     if ($loungeRef) {
  170.                         $rankQuote $loungeRef->getRankQuote();
  171.                     }
  172.                 }
  173.                 /** @var AveProduct|null $product */
  174.                 $product $this->em->getRepository(AveProduct::class)->find($productId);
  175.                 if (!$product) {
  176.                     continue;
  177.                 }
  178.                 $targetDate $dateStr \DateTime::createFromFormat('d/m/Y'$dateStr) : null;
  179.                 if (!$targetDate) {
  180.                     $targetDate $reservation->getDateStart();
  181.                 }
  182.                 
  183.                 // Normalizamos siempre a las 00:00:00
  184.                 $dateIn   = (clone $targetDate)->setTime(000);
  185.                 $dateOut  = (clone $targetDate)->setTime(235959);
  186.                 $dayStart = (clone $targetDate)->setTime(000);
  187.                 $dayEnd   = (clone $targetDate)->setTime(235959);
  188.                 $cleanProductName trim($product->getName());
  189.                 $expectedName     $cleanProductName " [Recomendado]";
  190.                 // BÚSQUEDA DQL DENTRO DEL RANGO DEL DÍA DE LA OPCIÓN
  191.                 $qb $this->em->createQueryBuilder()
  192.                     ->select('s')
  193.                     ->from(ReservationService::class, 's')
  194.                     ->where('s.reservationId = :resId')
  195.                     ->andWhere('s.serviceId = :prodId')
  196.                     ->andWhere('s.name = :expectedName')
  197.                     ->andWhere('s.dateInAt >= :dayStart AND s.dateInAt <= :dayEnd')
  198.                     ->setParameter('resId'$reservation->getId())
  199.                     ->setParameter('prodId'$product->getId())
  200.                     ->setParameter('expectedName'$expectedName)
  201.                     ->setParameter('dayStart'$dayStart)
  202.                     ->setParameter('dayEnd'$dayEnd);
  203.                 if ($rankQuote !== null) {
  204.                     $qb->andWhere('s.rank = :rankQuote')
  205.                        ->setParameter('rankQuote'$rankQuote);
  206.                 } else {
  207.                     $qb->andWhere('s.rank IS NULL');
  208.                 }
  209.                 /** @var ReservationService|null $reservationService */
  210.                 $reservationService $qb->setMaxResults(1)->getQuery()->getOneOrNullResult();
  211.                 if ($reservationService && (float)$reservationService->getUnits() === $quantity) {
  212.                     $processedItems[] = [
  213.                         'productId' => $productId,
  214.                         'name' => $expectedName,
  215.                         'quantity' => $quantity,
  216.                         'rank' => $rankQuote,
  217.                         'date' => $dateIn->format('d/m/Y')
  218.                     ];
  219.                     continue;
  220.                 }
  221.                 if (!$reservationService) {
  222.                     $reservationService = new ReservationService();
  223.                     $reservationService->setReservationId($reservation->getId());
  224.                     $reservationService->setServiceId($product->getId());
  225.                     $reservationService->setSupplierId(0);
  226.                     $reservationService->setServiceCatId(0);
  227.                     $reservationService->setCommission('0');
  228.                     $reservationService->setContcolor('#32aaf0');
  229.                     $reservationService->setViewInfo(true);
  230.                     $reservationService->setCreatedAt($ahora);
  231.                     $reservationService->setCreatedId($userId);
  232.                     $reservationService->setToinvoice(true); 
  233.                 }
  234.                 $reservationService->setRank($rankQuote);
  235.                 $reservationService->setDateInAt($dateIn);
  236.                 $reservationService->setDateOutAt($dateOut);
  237.                 $reservationService->setPrice((string) $product->getPrice());
  238.                 $reservationService->setUnits((string) $quantity);
  239.                 $reservationService->setServiceCatName(!empty($product->getType()) ? $product->getType() : 'Audiovisuales');
  240.                 $reservationService->setName($expectedName);
  241.                 $reservationService->setUpdatedAt($ahora);
  242.                 $reservationService->setUpdatedId($userId);
  243.                 $this->em->persist($reservationService);
  244.                 $processedItems[] = [
  245.                     'productId' => $productId,
  246.                     'name' => $expectedName,
  247.                     'quantity' => $quantity,
  248.                     'rank' => $rankQuote,
  249.                     'date' => $dateIn->format('d/m/Y')
  250.                 ];
  251.             }
  252.             // --- PROCESAR PAQUETES CONSOLIDADOS ---
  253.             foreach ($packagesToConsolidate as $packData) {
  254.                 $packId    $packData['packId'];
  255.                 $rankQuote $packData['rankQuote'];
  256.                 $dateStr   $packData['date'];
  257.                 $index     $packData['index'];
  258.                 /** @var AvePackageTemplate|null $package */
  259.                 $package $this->em->getRepository(AvePackageTemplate::class)->find($packId);
  260.                 if (!$package) {
  261.                     $errors[] = "Fila {$index}: El paquete con ID {$packId} no existe.";
  262.                     continue;
  263.                 }
  264.                 $expectedPackName sprintf("[Pack: %s]"trim($package->getName()));
  265.                 $packagePrice $package->getPricePackage() !== null 
  266.                     ? (float) $package->getPricePackage() 
  267.                     : (float) $package->getTotalNetPrice();
  268.                 $targetDate $dateStr \DateTime::createFromFormat('d/m/Y'$dateStr) : null;
  269.                 if (!$targetDate) {
  270.                     $targetDate $reservation->getDateStart();
  271.                 }
  272.                 
  273.                 $dateIn   = (clone $targetDate)->setTime(000);
  274.                 $dateOut  = (clone $targetDate)->setTime(235959);
  275.                 $dayStart = (clone $targetDate)->setTime(000);
  276.                 $dayEnd   = (clone $targetDate)->setTime(235959);
  277.                 $qb $this->em->createQueryBuilder()
  278.                     ->select('s')
  279.                     ->from(ReservationService::class, 's')
  280.                     ->where('s.reservationId = :resId')
  281.                     ->andWhere('s.name = :packName')
  282.                     ->andWhere('s.toinvoice = true')
  283.                     ->andWhere('s.dateInAt >= :dayStart AND s.dateInAt <= :dayEnd')
  284.                     ->setParameter('resId'$reservation->getId())
  285.                     ->setParameter('packName'$expectedPackName)
  286.                     ->setParameter('dayStart'$dayStart)
  287.                     ->setParameter('dayEnd'$dayEnd);
  288.                 if ($rankQuote !== null) {
  289.                     $qb->andWhere('s.rank = :rankQuote')
  290.                        ->setParameter('rankQuote'$rankQuote);
  291.                 } else {
  292.                     $qb->andWhere('s.rank IS NULL');
  293.                 }
  294.                 /** @var ReservationService|null $reservationService */
  295.                 $reservationService $qb->setMaxResults(1)->getQuery()->getOneOrNullResult();
  296.                 if ($reservationService && (float)$reservationService->getUnits() === 1.0) {
  297.                     $processedItems[] = [
  298.                         'packageId' => $packId,
  299.                         'name' => $expectedPackName,
  300.                         'quantity' => 1.0,
  301.                         'rank' => $rankQuote,
  302.                         'date' => $dateIn->format('d/m/Y')
  303.                     ];
  304.                     continue;
  305.                 }
  306.                 if (!$reservationService) {
  307.                     $reservationService = new ReservationService();
  308.                     $reservationService->setReservationId($reservation->getId());
  309.                     $reservationService->setServiceId(0);
  310.                     $reservationService->setSupplierId(0);
  311.                     $reservationService->setServiceCatId(0);
  312.                     $reservationService->setCommission('0');
  313.                     $reservationService->setContcolor('#32aaf0');
  314.                     $reservationService->setViewInfo(true);
  315.                     $reservationService->setCreatedAt($ahora);
  316.                     $reservationService->setCreatedId($userId);
  317.                     $reservationService->setToinvoice(true);
  318.                 }
  319.                 $reservationService->setRank($rankQuote);
  320.                 $reservationService->setDateInAt($dateIn);
  321.                 $reservationService->setDateOutAt($dateOut);
  322.                 $reservationService->setPrice((string) $packagePrice);
  323.                 $reservationService->setUnits('1.0');
  324.                 $reservationService->setServiceCatName('Audiovisuales');
  325.                 $reservationService->setName($expectedPackName);
  326.                 $reservationService->setUpdatedAt($ahora);
  327.                 $reservationService->setUpdatedId($userId);
  328.                 $this->em->persist($reservationService);
  329.                 $processedItems[] = [
  330.                     'packageId' => $packId,
  331.                     'name' => $expectedPackName,
  332.                     'quantity' => 1.0,
  333.                     'rank' => $rankQuote,
  334.                     'date' => $dateIn->format('d/m/Y')
  335.                 ];
  336.             }
  337.             if (!empty($errors)) {
  338.                 $this->em->rollback();
  339.                 return new JsonResponse(['success' => false'errors' => $errors], Response::HTTP_UNPROCESSABLE_ENTITY);
  340.             }
  341.             $this->em->flush();
  342.             $this->em->commit();
  343.             return new JsonResponse([
  344.                 'success' => true,
  345.                 'message' => 'Selección guardada y asociada a la reserva correctamente.',
  346.                 'processedItems' => $processedItems
  347.             ], Response::HTTP_OK);
  348.         } catch (\Exception $e) {
  349.             $this->em->rollback();
  350.             return new JsonResponse([
  351.                 'success' => false,
  352.                 'error' => 'Error interno al procesar e insertar la solicitud.',
  353.                 'details' => $e->getMessage()
  354.             ], Response::HTTP_INTERNAL_SERVER_ERROR);
  355.         }
  356.     }
  357.     /**
  358.      * @Route("/api/client/proposal/{token}", name="generate_token", methods={"GET"})
  359.      */
  360.     public function generateToken(Request $requeststring $token): JsonResponse
  361.     {
  362.         /** @var Reservation|null $reservation */
  363.         $reservation $this->em->getRepository(Reservation::class)->findOneBy(['token' => $token]);
  364.         if (!$reservation) {
  365.             throw $this->createNotFoundException('No se encontró ninguna reserva para el token proporcionado.');
  366.         }
  367.         $formattedReservation $this->formatReservation($reservation);
  368.         $formattedReservation['breakdownPackages'] = $reservation->isBreakdownPackages();
  369.         $data = [
  370.             'reservation' => $formattedReservation,
  371.             'options' => [],
  372.             'reservationService' => [], 
  373.             'filteredServices' => [],
  374.         ];
  375.         // 1. Obtener plantillas de paquetes e ítems maestros
  376.         $avPackages $this->em->getRepository(AvePackageTemplate::class)->findAll();
  377.         $allPackageItems $this->em->getRepository(AvePackageTemplateItems::class)->findBy([], ['rankAvPack' => 'ASC']);
  378.         
  379.         $itemsByPackageId = [];
  380.         $packageItemPricesMap = [];
  381.         foreach ($allPackageItems as $item) {
  382.             $itemsByPackageId[(int) $item->getPackId()][] = $item;
  383.             $packageItemPricesMap[(int) $item->getPackId()][(int) $item->getProductId()] = (float) $item->getServicePrice();
  384.         }
  385.         $packagesByLoungeDetail = [];
  386.         $packNamesToDetailIdsMap = []; 
  387.         $packNameToIdMap = [];
  388.         foreach ($avPackages as $pkg) {
  389.             if (!$pkg->getLounges()->isInitialized()) {
  390.                 $pkg->getLounges()->initialize();
  391.             }
  392.             $packageItems $itemsByPackageId[(int) $pkg->getId()] ?? [];
  393.             $formattedPackage $this->formatPackageTemplate($pkg$packageItems);
  394.             $packNameTrimmed trim($pkg->getName());
  395.             $packNameToIdMap[$packNameTrimmed] = $pkg->getId();
  396.             foreach ($pkg->getLounges() as $lounge) {
  397.                 $packagesByLoungeDetail[$lounge->getId()][] = $formattedPackage;
  398.                 $packNamesToDetailIdsMap[$packNameTrimmed][] = $lounge->getId();
  399.             }
  400.         }
  401.         // Obtener las salas contratadas
  402.         $loungeItems $this->em->getRepository(ReservationLoungeSimple::class)->findBy(
  403.             ['idReservation' => $reservation->getId()],
  404.             ['dateStart' => 'ASC']
  405.         );
  406.         // Obtener todas las líneas de servicio reales de la reserva
  407.         $allReservationServices $this->em->getRepository(ReservationService::class)->findBy([
  408.             'reservationId' => $reservation->getId(),
  409.             'toinvoice' => true 
  410.         ]);
  411.         // MAPEAR LOS SERVICIOS FILTRANDO STRICTAMENTE POR FECHA Y OPCIÓN (rankQuote)
  412.         $selectionsByLoungeId = [];        
  413.         $packageSelectionsByLoungeId = []; 
  414.         foreach ($allReservationServices as $service) {
  415.             $serviceDate $service->getDateInAt();
  416.             if (!$serviceDate) {
  417.                 continue;
  418.             }
  419.             
  420.             $dateKey     $serviceDate->format('Y-m-d');
  421.             $serviceId   = (int)$service->getServiceId();
  422.             $units       = (float)$service->getUnits();
  423.             $serviceName $service->getName() ?? '';
  424.             $serviceRank $service->getRank();
  425.             foreach ($loungeItems as $loungeItem) {
  426.                 $loungeDate $loungeItem->getDateStart();
  427.                 $loungeRank $loungeItem->getRankQuote();
  428.                 if ($loungeDate && $loungeDate->format('Y-m-d') === $dateKey) {
  429.                     
  430.                     if ($serviceRank !== null && (int)$serviceRank !== (int)$loungeRank) {
  431.                         continue;
  432.                     }
  433.                     $loungeSimpleId $loungeItem->getId();
  434.                     if ($serviceId && str_contains($serviceName'[Recomendado]')) {
  435.                         $selectionsByLoungeId[$loungeSimpleId][$serviceId] = $units;
  436.                     }
  437.                     if (str_contains($serviceName'[Pack:')) {
  438.                         if (preg_match('/\[Pack:\s*(.*?)\]/'$serviceName$matches)) {
  439.                             $extractedPackName trim($matches[1]);
  440.                             $packageSelectionsByLoungeId[$loungeSimpleId][$extractedPackName] = $units;
  441.                         }
  442.                     }
  443.                 }
  444.             }
  445.         }
  446.         $servicesGroupedByLoungeDetailId = [];
  447.         $globalRecommendedServices = [];
  448.         $specialServices = [];
  449.         $allowedCategories = ['limpieza''seguridad''parking''mobiliario'];
  450.         foreach ($allReservationServices as $service) {
  451.             $units = ($service->getUnits() > 0) ? (float) $service->getUnits() : 1.0;
  452.             $pax = ($service->getPax() > 0) ? (float) $service->getPax() : 1.0;
  453.             $priceOver = (float) $service->getOver();
  454.             $price = (float) $service->getPrice();
  455.             $commission = (float) $service->getCommission();
  456.             $iva $service->getSageIva()?->getIva() ?? 0;
  457.             $totalPrice = ($price $pax $units);
  458.             $priceCommission $totalPrice * ($commission 100);
  459.             $totalConComision $totalPrice $priceCommission;
  460.             $dateInAt $service->getDateInAt() ?? $reservation->getDateStart();
  461.             $dateOutAt $service->getDateOutAt() ?? $reservation->getDateEnd();
  462.             $days 1;
  463.             if ($dateInAt && $dateOutAt) {
  464.                 $interval $dateInAt->diff($dateOutAt);
  465.                 $days max(1, (int) $interval->days 1);
  466.             }
  467.             
  468.             $totalConDays $totalConComision $days;
  469.             $totalConOver $totalConDays $priceOver;
  470.             $priceIva $totalConOver * ($iva 100);
  471.             $totalPriceCalculated $totalConOver $priceIva;
  472.             $pricePackage null;
  473.             $serviceName $service->getName() ?? '';
  474.             $assigned false;
  475.             $formattedService = [
  476.                 'id' => $service->getId(),
  477.                 'reservationId' => $service->getReservationId(),
  478.                 'supplierId' => $service->getSupplierId(),
  479.                 'serviceId' => $service->getServiceId(),
  480.                 'serviceCatId' => $service->getServiceCatId(),
  481.                 'serviceCatName' => $service->getServiceCatName(),
  482.                 'name' => $service->getName(),
  483.                 'price' => $service->getPrice(),
  484.                 'pricePackage' => $pricePackage,
  485.                 'currency' => $service->getCurrency(),
  486.                 'units' => $units,
  487.                 'opCommission' => $service->getOpCommission(),
  488.                 'commission' => $service->getCommission(),
  489.                 'opOver' => $service->getOpOver(),
  490.                 'priceOver' => $priceOver,
  491.                 'opIva' => $service->getOpIva(),
  492.                 'iva' => $service->getSageIva(),
  493.                 'pax' => $pax,
  494.                 'hour' => $service->getHour(),
  495.                 'dateInAt' => $dateInAt?->format('Y-m-d H:i:s'),
  496.                 'dateOutAt' => $dateOutAt?->format('Y-m-d H:i:s'),
  497.                 'contcolor' => $service->getContcolor(),
  498.                 'rank' => $service->getRank(),
  499.                 'assistantId' => $service->getAssistantId(),
  500.                 'activityId' => $service->getActivityId(),
  501.                 'pay' => $service->getPay(),
  502.                 'createdAt' => $service->getCreatedAt()?->format('Y-m-d H:i:s'),
  503.                 'createdId' => $service->getCreatedId(),
  504.                 'updatedAt' => $service->getUpdatedAt()?->format('Y-m-d H:i:s'),
  505.                 'updatedId' => $service->getUpdatedId(),
  506.                 'toInvoice' => $service->getToinvoice(),
  507.                 'totalSinIva' => $totalConOver,
  508.                 'totalIva' => $priceIva,
  509.                 'totalPrice' => $totalPriceCalculated,
  510.                 'viewInfo' => $service->getViewInfo()
  511.             ];
  512.             if (str_contains($serviceName'[Pack:')) {
  513.                 if (preg_match('/\[Pack:\s*(.*?)\]/'$serviceName$matches)) {
  514.                     $extractedPackName trim($matches[1]);
  515.                     
  516.                     if (isset($packNamesToDetailIdsMap[$extractedPackName])) {
  517.                         $associatedPackId $packNameToIdMap[$extractedPackName] ?? null;
  518.                         $productId = (int) $service->getServiceId();
  519.                         if ($associatedPackId !== null && isset($packageItemPricesMap[$associatedPackId][$productId])) {
  520.                             $formattedService['pricePackage'] = $packageItemPricesMap[$associatedPackId][$productId];
  521.                         }
  522.                         $servicesGroupedByLoungeDetailId[$extractedPackName][] = $formattedService;
  523.                         $assigned true;
  524.                     }
  525.                 }
  526.             }
  527.             if (!$assigned) {
  528.                 $globalRecommendedServices[] = $formattedService;
  529.             }
  530.             $catNameNormalized mb_strtolower(trim($service->getServiceCatName() ?? ''));
  531.             if (in_array($catNameNormalized$allowedCategoriestrue)) {
  532.                 $specialServices[] = $formattedService;
  533.             }
  534.         }
  535.         // CONSTRUCCIÓN DE OPCIONES CON FECHAS E ÍTEMS INYECTADOS
  536.         foreach ($loungeItems as $item) {
  537.             $data['reservation']['idWebLanguage'] = $item->getLanguage() ?: 1;
  538.             $data['reservation']['showPackagePrice'] = $reservation->getShowPackagePrice() ?: 1;
  539.             $uniqueId $item->getId();
  540.             $rankQuote $item->getRankQuote();
  541.             $loungeIva $item->getSageIva() ?? 21;
  542.             $loungeDateFormatted $item->getDateStart() ? $item->getDateStart()->format('d/m/Y') : '';
  543.             $loungeData $this->formatLoungeData($item$uniqueId$loungeIva);
  544.             $loungeDetailId $loungeData['loungeId'] ? (int) $loungeData['loungeId'] : null;
  545.             $rawPackages = ($loungeDetailId && isset($packagesByLoungeDetail[$loungeDetailId])) 
  546.                 ? $packagesByLoungeDetail[$loungeDetailId
  547.                 : [];
  548.             $formattedPackagesForThisLounge = [];
  549.             foreach ($rawPackages as $pkg) {
  550.                 $packNameTrimmed trim($pkg['name']);
  551.                 
  552.                 $pkg['date'] = $loungeDateFormatted;
  553.                 if (isset($packageSelectionsByLoungeId[$uniqueId][$packNameTrimmed])) {
  554.                     $pkg['isChecked'] = true;
  555.                     $pkg['quantity'] = $packageSelectionsByLoungeId[$uniqueId][$packNameTrimmed];
  556.                 } else {
  557.                     $pkg['isChecked'] = false;
  558.                     $pkg['quantity'] = 0;
  559.                 }
  560.                 foreach ($pkg['items'] as &$subItem) {
  561.                     $productId = (int)$subItem['productId'];
  562.                     
  563.                     $subItem['date'] = $loungeDateFormatted;
  564.                     if (isset($selectionsByLoungeId[$uniqueId][$productId])) {
  565.                         $subItem['isChecked'] = true;
  566.                         $subItem['quantity'] = $selectionsByLoungeId[$uniqueId][$productId];
  567.                     } else {
  568.                         $subItem['isChecked'] = false;
  569.                         $subItem['quantity'] = 0;
  570.                     }
  571.                 }
  572.                 unset($subItem);
  573.                 
  574.                 $formattedPackagesForThisLounge[] = $pkg;
  575.             }
  576.             $loungeData['avPackages'] = $formattedPackagesForThisLounge;
  577.             $loungeData['loungeServices'] = [];
  578.             if ($loungeDetailId && isset($packagesByLoungeDetail[$loungeDetailId])) {
  579.                 foreach ($packagesByLoungeDetail[$loungeDetailId] as $allowedPack) {
  580.                     $allowedPackName trim($allowedPack['name']);
  581.                     
  582.                     if (isset($servicesGroupedByLoungeDetailId[$allowedPackName])) {
  583.                         foreach ($servicesGroupedByLoungeDetailId[$allowedPackName] as $purchasedService) {
  584.                             $loungeData['loungeServices'][] = $purchasedService;
  585.                         }
  586.                     }
  587.                 }
  588.             }
  589.             if (!isset($data['options'][$rankQuote])) {
  590.                 $data['options'][$rankQuote] = [];
  591.             }
  592.             $data['options'][$rankQuote][] = $loungeData;
  593.         }
  594.         $data['reservationService'] = $globalRecommendedServices;
  595.         $data['filteredServices'] = $specialServices
  596.         return new JsonResponse($data);
  597.     }
  598.     private function formatPackageTemplate(AvePackageTemplate $template, array $items): array
  599.     {
  600.         $totalPrice $template->getPricePackage() !== null 
  601.             ? (float) $template->getPricePackage() 
  602.             : (float) $template->getTotalNetPrice();
  603.         return [
  604.             'id'          => $template->getId(),
  605.             'name'        => $template->getName(),
  606.             'description' => $template->getDescription(),
  607.             'totalPrice'  => $totalPrice
  608.             'isFeatured'  => $template->isFeatured(), 
  609.             'lounges'     => array_map(fn($lounge) => [
  610.                 'id'   => $lounge->getId(),
  611.                 'name' => $lounge->getName()
  612.             ], $template->getLounges()->toArray()),
  613.             'items'       => array_map(fn(AvePackageTemplateItems $item) => [
  614.                 'id' => $item->getId(),
  615.                 'productName' => $item->getProductName(),
  616.                 'productId' => $item->getProductId(),
  617.                 'percProductPrice' => $item->getPercProductPrice(),
  618.                 'servicePrice' => $item->getServicePrice(),
  619.                 'priceWithoutPack' => $item->getPriceWithoutPack(),
  620.                 'rankAvPack' => $item->getRankAvPack(),
  621.                 'description' => $item->getDescription(),
  622.             ], $items),
  623.         ];
  624.     }
  625.     private function formatReservation(Reservation $reservation): array
  626.     {
  627.         $contactEmail null;
  628.         $clientContact $reservation->getClientContact();
  629.         if ($clientContact instanceof ClientContact) {
  630.             $contactEmail $clientContact->getEmail();
  631.             $clientContactId $clientContact->getId();
  632.         } elseif (is_numeric($clientContact)) {
  633.             $contact $this->em->getRepository(ClientContact::class)->find((int) $clientContact);
  634.             $contactEmail $contact $contact->getEmail() : null;
  635.             $clientContactId $contact $contact->getId() : null;
  636.         } else {
  637.             $contactEmail $reservation->getContactUnregistered();
  638.             $clientContactId $clientContact;
  639.         }
  640.         $client $reservation->getClient();
  641.         $clientBlock $client ? [
  642.             'id'   => method_exists($client'getId') ? $client->getId() : null,
  643.             'name' => method_exists($client'getName') ? $client->getName() : null,
  644.         ] : null;
  645.         $idProposal $this->resolveProposalId($reservation) ?? $reservation->getId();
  646.         return [
  647.             'id' => $reservation->getId(),
  648.             'title' => $reservation->getTitle(),
  649.             'client' => $clientBlock,
  650.             'createdAt' => $reservation->getCreatedAt()?->format('Y-m-d H:i:s'),
  651.             'priority' => $reservation->getPriority(),
  652.             'dateStart' => $reservation->getDateStart()?->format('Y-m-d H:i:s'),
  653.             'dateEnd' => $reservation->getDateEnd()?->format('Y-m-d H:i:s'),
  654.             'createdBy' => $reservation->getCreatedBy(),
  655.             'supplier' => $reservation->getSupplier(),
  656.             'status' => $reservation->getStatus(),
  657.             'updatedAt' => $reservation->getUpdatedAt()?->format('Y-m-d H:i:s'),
  658.             'updatedBy' => $reservation->getUpdatedBy(),
  659.             'daysBlock' => $reservation->getDaysBlock(),
  660.             'idProposal' => $idProposal,
  661.             'pax' => $reservation->getPax(),
  662.             'accessKey' => $reservation->getAccessKey(),
  663.             'description' => $reservation->getDescription(),
  664.             'clientContact' => $clientContactId,
  665.             'contactUnregistered' => $reservation->getContactUnregistered(),
  666.             'days' => $reservation->getDays(),
  667.             'contract' => $reservation->getContract(),
  668.             'nameContactUnregistered' => $reservation->getNameContactUnregistered(),
  669.             'phoneContactUnregistered' => $reservation->getPhoneContactUnregistered(),
  670.             'token' => $reservation->getToken(),
  671.             'contactEmail' => $contactEmail,
  672.         ];
  673.     }
  674.     private function formatLoungeData(ReservationLoungeSimple $item$uniqueId$loungeIva): array
  675.     {
  676.         $loungeName $item->getLoungeName();
  677.         /** @var ReservationLoungeDetails|null $detail */
  678.         $detail $this->em->getRepository(ReservationLoungeDetails::class)->findOneBy(['name' => $loungeName]);
  679.         
  680.         if (!$detail) {
  681.             $dateEnd $item->getDateEnd();
  682.             $xdateEnd $dateEnd
  683.                 ? ($dateEnd->format('H:i') === '23:59' $dateEnd->format('Y-m-d 00:00:00') : $dateEnd->format('Y-m-d H:i:s'))
  684.                 : null;
  685.             return [
  686.                 'id' => $uniqueId,
  687.                 'loungeId' => null,
  688.                 'loungeName' => $loungeName,
  689.                 'type' => $item->getType(),
  690.                 'rankQuote' => $item->getRankQuote(),
  691.                 'dateStart' => $item->getDateStart()?->format('Y-m-d H:i:s'),
  692.                 'dateEnd' => $xdateEnd,
  693.                 'pax' => $item->getPax(),
  694.                 'price' => $item->getServicePrice(),
  695.                 'loungeIva' => 0,
  696.                 'combo' => null,
  697.                 'isCombo' => 0,
  698.                 'descriptions' => [],
  699.                 'loungeDescription' => $item->getLoungeDescription(),
  700.                 'importantDescription' => empty($item->getType()) ? $item->getImportantDescription() : '',
  701.                 'importantDescGeneralText' => $item->getImportantDescGeneralText(),
  702.                 'importantDescSchedules' => $item->getImportantDescSchedules(),
  703.                 'importantDescParking' => $item->getImportantDescParking(),
  704.                 'showAudiovisualPrice' => $item->isShowAudiovisualPrice(),
  705.                 'showAudiovisualPackage' => $item->isShowAudiovisualPackage(),
  706.                 'pictures' => [],
  707.                 'comboDetails' => [],
  708.                 'loungeDetails' => null,
  709.                 'totalPrice' => $item->getServicePrice()
  710.             ];
  711.         }
  712.         $ivaPct is_numeric($loungeIva) ? (float) $loungeIva 21.0;
  713.         $ivaImporte $item->getServicePrice() * ($ivaPct 100);
  714.         $comboIds $detail->getCombo();
  715.         return [
  716.             'id' => $uniqueId,
  717.             'loungeId' => $detail->getId(),
  718.             'loungeName' => $detail->getName(),
  719.             'type' => $item->getType(),
  720.             'rankQuote' => $item->getRankQuote(),
  721.             'dateStart' => $item->getDateStart()?->format('Y-m-d H:i:s'),
  722.             'dateEnd' => $item->getDateEnd()?->format('Y-m-d H:i:s'),
  723.             'pax' => $item->getPax(),
  724.             'price' => $item->getServicePrice(),
  725.             'loungeIva' => $ivaImporte,
  726.             'combo' => $comboIds,
  727.             'isCombo' => $comboIds 0,
  728.             'descriptions' => $this->getDescriptions($detail),
  729.             'loungeDescription' => $item->getLoungeDescription(),
  730.             'importantDescription' => empty($item->getType()) ? $item->getImportantDescription() : '',
  731.             'importantDescGeneralText' => $item->getImportantDescGeneralText(),
  732.             'importantDescSchedules' => $item->getImportantDescSchedules(),
  733.             'importantDescParking' => $item->getImportantDescParking(),
  734.             'showAudiovisualPrice' => $item->isShowAudiovisualPrice(),
  735.             'showAudiovisualPackage' => $item->isShowAudiovisualPackage(),
  736.             'pictures' => $this->getPictures($detail),
  737.             'comboDetails' => $comboIds $this->getComboDetails($comboIds) : [],
  738.             'loungeDetails' => $this->getLoungeDetails($detail->getId()),
  739.             'totalPrice' => $item->getServicePrice() + $ivaImporte
  740.         ];
  741.     }
  742.     private function getDescriptions(ReservationLoungeDetails $loungeDetail): array
  743.     {
  744.         $descriptions = [];
  745.         $result $this->em->getRepository(ReservationLoungeDescription::class)->findBy(['loungeId' => $loungeDetail->getId()]);
  746.         foreach ($result as $description) {
  747.             $descriptions[] = [
  748.                 'language' => $description->getLanguage(),
  749.                 'description' => $description->getDescription(),
  750.                 'createdAt' => $description->getCreatedAt()?->format('Y-m-d H:i:s'),
  751.                 'updatedAt' => $description->getUpdatedAt()?->format('Y-m-d H:i:s'),
  752.             ];
  753.         }
  754.         return $descriptions;
  755.     }
  756.     private function getPictures(ReservationLoungeDetails $loungeDetail): array
  757.     {
  758.         $pictures = [];
  759.         $result $this->em->getRepository(ReservationLoungePicture::class)->findBy(['loungeId' => $loungeDetail->getId()]);
  760.         foreach ($result as $picture) {
  761.             $pictures[] = [
  762.                 'id' => $picture->getId(),
  763.                 'title' => $picture->getTitle(),
  764.                 'imageLarge' => $picture->getImageLarge(),
  765.                 'imageMedium' => $picture->getImageMedium(),
  766.                 'imageSmall' => $picture->getImageSmall(),
  767.                 'createdAt' => $picture->getCreatedAt()?->format('Y-m-d H:i:s'),
  768.                 'updatedAt' => $picture->getUpdatedAt()?->format('Y-m-d H:i:s'),
  769.             ];
  770.         }
  771.         return $pictures;
  772.     }
  773.     private function getComboDetails(string $comboIds): array
  774.     {
  775.         $comboDetails = [];
  776.         $comboIdArray explode(','$comboIds);
  777.         foreach ($comboIdArray as $comboId) {
  778.             $comboDetail $this->em->getRepository(ReservationLoungeDetails::class)->find($comboId);
  779.             if ($comboDetail) {
  780.                 $comboDetails[] = [
  781.                     'id' => $comboDetail->getId(),
  782.                     'loungeName' => $comboDetail->getName(),
  783.                     'meters' => $comboDetail->getMeters(),
  784.                     'length' => $comboDetail->getLength(),
  785.                     'width' => $comboDetail->getWidth(),
  786.                     'height' => $comboDetail->getHeight(),
  787.                     'capSchool' => $comboDetail->getCapSchool(),
  788.                     'capTheater' => $comboDetail->getCapTheater(),
  789.                     'capCocktail' => $comboDetail->getCapCocktail(),
  790.                     'capBanquet' => $comboDetail->getCapBanquet(),
  791.                     'capImperial' => $comboDetail->getCapImperial(),
  792.                     'rankLounge' => $comboDetail->getRankLounge(),
  793.                     'descriptions' => $this->getDescriptions($comboDetail),
  794.                     'pictures' => $this->getPictures($comboDetail),
  795.                 ];
  796.             }
  797.         }
  798.         return $comboDetails;
  799.     }
  800.     private function getLoungeDetails(int $id): ?array
  801.     {
  802.         $loungeDetail $this->em->getRepository(ReservationLoungeDetails::class)->findOneBy(['id' => $id]);
  803.         if (!$loungeDetail) {
  804.             return null;
  805.         }
  806.         return [
  807.             'id' => $loungeDetail->getId(),
  808.             'name' => $loungeDetail->getName(),
  809.             'meters' => $loungeDetail->getMeters(),
  810.             'length' => $loungeDetail->getLength(),
  811.             'width' => $loungeDetail->getWidth(),
  812.             'height' => $loungeDetail->getHeight(),
  813.             'capSchool' => $loungeDetail->getCapSchool(),
  814.             'capTheater' => $loungeDetail->getCapTheater(),
  815.             'capCocktail' => $loungeDetail->getCapCocktail(),
  816.             'capBanquet' => $loungeDetail->getCapBanquet(),
  817.             'capImperial' => $loungeDetail->getCapImperial(),
  818.             'rankLounge' => $loungeDetail->getRankLounge(),
  819.             'createdAt' => $loungeDetail->getCreatedAt()?->format('Y-m-d H:i:s'),
  820.             'updatedAt' => $loungeDetail->getUpdatedAt()?->format('Y-m-d H:i:s'),
  821.         ];
  822.     }
  823.     private function resolveProposalId(Reservation $reservation): ?int
  824.     {
  825.         if (method_exists($reservation'getProposal')) {
  826.             $proposal $reservation->getProposal();
  827.             if (is_object($proposal) && method_exists($proposal'getId')) {
  828.                 return $proposal->getId();
  829.             }
  830.         }
  831.         foreach (['getIdProposal''getProposalId'] as $m) {
  832.             if (method_exists($reservation$m)) {
  833.                 $val $reservation->$m();
  834.                 return is_numeric($val) ? (int) $val null;
  835.             }
  836.         }
  837.         return null;
  838.     }
  839. }