src/Controller/BookingApi/ApiTicketControllerApi.php line 49

Open in your IDE?
  1. <?php
  2. /**
  3.  * Created by Elements.at New Media Solutions GmbH
  4.  *
  5.  */
  6. namespace App\Controller\BookingApi;
  7. use App\Model\BookingApi\CartItemTicket;
  8. use App\Model\BookingApi\Exception\DataMappingException;
  9. use App\Model\BookingApi\Exception\DataMissingException;
  10. use App\Model\BookingApi\LocalizedText;
  11. use App\Model\BookingApi\TicketAcquisitionType;
  12. use App\Model\BookingApi\TicketAvailability;
  13. use App\Model\BookingApi\TicketAvailabilityIncludingUpgrades;
  14. use App\Model\BookingApi\TicketCatalogUpgradeRelation;
  15. use App\Model\BookingApi\TicketInsurancePrice;
  16. use App\Model\BookingApi\TicketUpgradePrice;
  17. use App\Model\Shop\Ticket\TicketProduct;
  18. use App\Model\Type\TenantType;
  19. use App\Service\BookingApi\ApiService;
  20. use App\Service\Shop\JsonDataService;
  21. use App\Service\Shop\ShopService;
  22. use App\Service\TicketShopFrameworkBundle\TicketFilter;
  23. use Carbon\Carbon;
  24. use Elements\Bundle\SkidataTicketingSwebBundle\Service\OrderService;
  25. use Elements\Bundle\TicketShopFrameworkBundle\Model\Constant\AcquisitionType;
  26. use Elements\Bundle\TicketShopFrameworkBundle\Model\DataObject\TicketCatalog;
  27. use Elements\Bundle\TicketShopFrameworkBundle\Model\Shop\Ticketing\TicketCatalogAvailability;
  28. use League\Csv\Exception;
  29. use OpenApi\Annotations as OA;
  30. use Pimcore\Model\DataObject\KeycardNumber;
  31. use Pimcore\Model\DataObject\ShopTicketCatalog;
  32. use Pimcore\Model\DataObject\TicketshopTicketAdditional;
  33. use Pimcore\Translation\Translator;
  34. use Symfony\Component\HttpFoundation\JsonResponse;
  35. use Symfony\Component\HttpFoundation\Request;
  36. use Symfony\Component\Routing\Annotation\Route;
  37. use Symfony\Component\Validator\Validator\ValidatorInterface;
  38. /**
  39.  * Class ApiTicketControllerApi
  40.  *
  41.  * @Route("/bookingAPI/ticket")
  42.  *
  43.  * @OA\Tag(name="Ticket")
  44.  */
  45. class ApiTicketControllerApi extends ApiAbstractController
  46. {
  47.     /**
  48.      * Get availability for ShopTicketCatalog and specific date
  49.      *
  50.      * @Route("/availability", name="ticket_availability")
  51.      *
  52.      * @param Request $request
  53.      *
  54.      * @return JsonResponse
  55.      *
  56.      * @OA\Post(
  57.      *      path="/bookingAPI/ticket/availability",
  58.      *     description="Get availability for ShopTicketCatalog and specific date",
  59.      *     tags={"Ticket"},
  60.      *
  61.      *     @OA\RequestBody(
  62.      *      required=true,
  63.      *
  64.      *           @OA\JsonContent(
  65.      *              required={"startDate","catalogId"},
  66.      *
  67.      *              @OA\Property( property="startDate", type="string", description="YYYY-MM-DD"),
  68.      *              @OA\Property( property="endDate", type="string", description="YYYY-MM-DD"),
  69.      *              @OA\Property( property="catalogId", type="int"),
  70.      *              @OA\Property( property="countryCode", type="string", description="required if acquisitionTypesPerConsumerCategory should be included in response (stop sale for delivery is set per country)"),
  71.      *
  72.      *              @OA\Examples(example="sandboxTicketExample", value="{
  73.     ""catalogId"": ""3000188"",
  74.     ""startDate"": ""2024-04-11"",
  75.     ""endDate"": ""2024-04-11"",
  76.     ""countryCode"": ""CH""
  77.     }", summary="SANDBOX ticket availability"),
  78.     @OA\Examples(example="liveTicketExample", value="{
  79.     ""catalogId"": ""1501805"",
  80.     ""startDate"": ""2024-03-30"",
  81.     ""endDate"": ""2024-03-30"",
  82.     ""countryCode"": ""CH""
  83.     }", summary="LIVE ticket availability")
  84.      *           )
  85.      *      ),
  86.      *     @OA\Response(
  87.      *          response=200,
  88.      *          description="Successful operation",
  89.      *
  90.      *          @OA\MediaType(
  91.      *              mediaType="application/json",
  92.      *
  93.      *              @OA\Schema(
  94.      *                  type = "object",
  95.      *
  96.      *                   @OA\Property(property="success", type="boolean"),
  97.      *                   @OA\Property(property="availabilities", type="array",
  98.      *
  99.      *                      @OA\Items(ref="#/components/schemas/TicketAvailability")
  100.      *                   )
  101.      *              )
  102.      *          )
  103.      *      ),
  104.      *
  105.      *     @OA\Response(
  106.      *          response=500,
  107.      *          description="Error",
  108.      *      ),
  109.      *     )
  110.      *
  111.      *
  112.      */
  113.     public function getTicketAvailability(Request $requestApiService $apiServiceShopService $shopHelperJsonDataService $jsonDataService): JsonResponse
  114.     {
  115.         try {
  116.             $body $request->getContent();
  117.             $requestData json_decode($bodytrue);
  118.             $cart $this->getCart();
  119.             $this->clearCartData($cart);
  120.             // only for testing purposes
  121.             if (!$requestData && $request->get('debug')) {
  122.                 $requestData = [
  123.                     'startDate' => '2024-04-20',
  124.                     'endDate' => '2024-04-25',
  125.                     'catalogId' => 1501817,
  126.                     'countryCode' => 'CH',
  127.                 ];
  128.             }
  129.             //check for required params
  130.             if (!is_array($requestData) || !array_key_exists('startDate'$requestData) || empty($requestData['startDate'])) {
  131.                 $this->bookingapiLogger->warning('required startDate for ticket/availability endpoint is missing');
  132.                 return $this->sendErrors(['startDate missing'], 400);
  133.             }
  134.             if (!is_array($requestData) || !array_key_exists('catalogId'$requestData) || empty($requestData['catalogId'])) {
  135.                 $this->bookingapiLogger->warning('required catalogId for ticket/availability endpoint is missing');
  136.                 return $this->sendErrors(['catalogId missing'], 400);
  137.             }
  138.             $from Carbon::createFromFormat('Y-m-d'$requestData['startDate']);
  139.             $from $from->startOfDay();
  140.             if (isset($requestData['endDate']) && $requestData['endDate']) {
  141.                 $to Carbon::createFromFormat('Y-m-d'$requestData['endDate']);
  142.             } else {
  143.                 $to $from->copy();
  144.             }
  145.             $to $to->endOfDay();
  146.             $countryCode = isset($requestData['countryCode']) ? $requestData['countryCode'] : null;
  147.             $catalog TicketCatalog::getById($requestData['catalogId']);
  148.             if (!$catalog) {
  149.                 throw new \Exception('ticketCatalog with id ' $requestData['catalogId'] . 'not found');
  150.             } elseif (!in_array(TenantType::MARKETPLACE$catalog->getTenant())) {
  151.                 $this->bookingapiLogger->warning('call to ticket/availability with catalogId' $requestData['catalogId'] . ' but catalog does not have marketplace tenant');
  152.                 return $this->sendErrors(['ticketCatalog with id ' $requestData['catalogId'] . ' not available for tenant ' TenantType::MARKETPLACE], 400);
  153.             }
  154.             $filter = new TicketFilter($from$to);
  155.             $filter->setExactDuration(true);
  156.             $filter->setUseTicketValidity(true);
  157.             $allowedConsumers = [];
  158.             foreach ($catalog->getTicketConsumerCategories() as $allowedConsumer) {
  159.                 $allowedConsumers[] = $allowedConsumer->getId();
  160.             }
  161.             if ($allowedConsumers) {
  162.                 $filter->setConsumers($allowedConsumers);
  163.             }
  164.             /**
  165.              * @var TicketCatalogAvailability $ticketCatalogAvailability
  166.              */
  167.             $ticketCatalogAvailability $filter->getTicketCatalogAvailability($catalogtrue);
  168.             //insurances
  169.             $insuranceData $shopHelper->getInsurancesForProduct($ticketCatalogAvailability);
  170.             $insurances $this->getInsuranceData($catalog$ticketCatalogAvailability$insuranceData);
  171.             $availabilityData = [];
  172.             foreach ($ticketCatalogAvailability->getTicketAvailability() as $productAvailability) {
  173.                 $ticketProduct $productAvailability->getTicketProduct();
  174.                 //upgrades
  175.                 /** @phpstan-ignore-next-line */
  176.                 $upgrades $this->getUpgradeData($catalog$ticketProduct$from$to);
  177.                 $consumerCategories = [];
  178.                 $consumerCategoryData = [];
  179.                 $acquisitionTypesByConsumerCategory = [];
  180.                 foreach ($productAvailability->getConsumerAvailabilities() as $consumerAvailability) {
  181.                     $priceInfo $consumerAvailability->getPriceInfo();
  182.                     if ($priceInfo) {
  183.                         $consumerCategories[$consumerAvailability->getTicketConsumer()->getId()] = $priceInfo->getPrice()->getGrossAmount()->asNumeric();
  184.                         $consumerCategoryData[] = $consumerAvailability->getTicketConsumer();
  185.                         if ($countryCode) {
  186.                             $possibleAquisitionTypes $apiService->calculatePossibleAcquisitionTypes($countryCode$productAvailability->getValidFrom(), $consumerAvailability->getTicketConsumer(), $productAvailability->getTicketProduct());
  187.                             $acquisitionTypesByConsumerCategory[$consumerAvailability->getTicketConsumer()->getId()] = array_values($possibleAquisitionTypes);
  188.                         }
  189.                     }
  190.                 }
  191.                 $localizedName = new LocalizedText();
  192.                 $localizedInfoBubbleText = new LocalizedText();
  193.                 $localizedCatalogInfoText = new LocalizedText();
  194.                 foreach ($this->languages as $language) {
  195.                     $localizedName->$language $ticketProduct->getName($language);
  196.                     $localizedCatalogInfoText->$language $catalog->getInfoBubbleDescription($language) ?: '';
  197.                     $localizedInfoBubbleText->$language $ticketProduct->getDescription($language) ?: '';
  198.                 }
  199.                 $apiTicketAvailability = new TicketAvailability();
  200.                 $apiTicketAvailability->setTicketId($productAvailability->getTicketProduct()->getId());
  201.                 $apiTicketAvailability->setName($localizedName);
  202.                 $apiTicketAvailability->setInfoText($localizedInfoBubbleText);
  203.                 $apiTicketAvailability->setInfoTextCatalog($localizedCatalogInfoText);
  204.                 $apiTicketAvailability->setConsumerCategories($this->getConsumerCategories($consumerCategoryData));
  205.                 $apiTicketAvailability->setPriceByConsumerCategory($consumerCategories);
  206.                 $apiTicketAvailability->setAcquisitionTypesByConsumerCategory($acquisitionTypesByConsumerCategory);
  207.                 $apiTicketAvailability->setDefaultAcquisitionType($ticketProduct->getDefaultAcquisitionType());
  208.                 $apiTicketAvailability->setPersonalization($productAvailability->getTicketProduct()->getRequirements() ?: []);
  209.                 $apiInsurances = isset($insurances[$productAvailability->getTicketProduct()->getId()]) ? $insurances[$productAvailability->getTicketProduct()->getId()] : [];
  210.                 $apiTicketAvailability->setInsurances(array_values($apiInsurances));
  211.                 $apiTicketAvailability->setUpgrades($upgrades);
  212.                 //delivery prices
  213.                 $deliveryPrices = [];
  214.                 if ($countryCode) {
  215.                     $deliveryPrices[$countryCode] = $apiService->getDeliveryPriceForCountry($countryCode);
  216.                 } else {
  217.                     $deliveryPrices $apiService->getDeliveryPricePerCountry();
  218.                 }
  219.                 $apiTicketAvailability->setDeliveryPricePerCountry($deliveryPrices);
  220.                 $availabilityData[] = $apiTicketAvailability->jsonSerialize();
  221.             }
  222.             if ($request->get('debug')) {
  223.                 p_r($availabilityData);
  224.                 die();
  225.             }
  226.             return new JsonResponse(
  227.                 [
  228.                     'success' => true,
  229.                     'availabilities' => $availabilityData,
  230.                 ]
  231.             );
  232.         } catch (\Throwable $throwable) {
  233.             $this->bookingapiLogger->error($throwable->getMessage() . 'Stack Trace: ' $throwable->getTraceAsString());
  234.             return $this->sendErrors([$throwable->getMessage()], 500);
  235.         }
  236.     }
  237.     /**
  238.      * Get all  ticket acquisitionTypes with description
  239.      *
  240.      * @Route("/acquisitionTypes")
  241.      *
  242.      * @param Request $request
  243.      *
  244.      * @return JsonResponse
  245.      *
  246.      * @OA\Post(
  247.      *     path="/bookingAPI/ticket/acquisitionTypes",
  248.      *     description="Get all  ticket acquisitionTypes with description",
  249.      *     tags={"Ticket"},
  250.      *
  251.      *     @OA\RequestBody(
  252.      *      required=false
  253.      *      ),
  254.      *
  255.      *  @OA\Response(
  256.      *          response=200,
  257.      *          description="Successful operation",
  258.      *
  259.      *          @OA\MediaType(
  260.      *              mediaType="application/json",
  261.      *
  262.      *              @OA\Schema(
  263.      *                  type = "object",
  264.      *
  265.      *                      @OA\Property(property="success", type="boolean"),
  266.      *                      @OA\Property(property="acquisitionTypes", type="array",
  267.      *
  268.      *                          @OA\Items(type="object", ref="#/components/schemas/TicketAcquisitionType")
  269.      *                      ),
  270.      *              )
  271.      *          )
  272.      *      ),
  273.      *
  274.      *     @OA\Response(
  275.      *          response=500,
  276.      *          description="Error",
  277.      *      ),
  278.      *
  279.      * )
  280.      */
  281.     public function getAcquisitionTypes(Request $requestShopService $shopHelperTranslator $translator): JsonResponse
  282.     {
  283.         $acquisitionTypes = [AcquisitionType::SWISSSPASSAcquisitionType::BARCODEAcquisitionType::RELOADAcquisitionType::REQUESTAcquisitionType::TICKETPICKUP];
  284.         $returnData = [];
  285.         $acquisitionTypes array_diff($acquisitionTypes, ($this->siteConfigService->getSiteConfig()->getHideAcquisitionTypes() ?? []));
  286.         foreach ($acquisitionTypes as $acquisitionType) {
  287.             $descriptions = new LocalizedText();
  288.             foreach ($this->languages as $language) {
  289.                 $tk "shop.acquisition-type." $acquisitionType ".info-text";
  290.                 $translated $translator->trans($tk, [], null$language);
  291.                 $descriptions->$language $translated != $tk $translated '';
  292.             }
  293.             $apiAcquisitionType = new TicketAcquisitionType();
  294.             $apiAcquisitionType->setAcquisitionType($acquisitionType);
  295.             $apiAcquisitionType->setTexts($descriptions);
  296.             $returnData[] = $apiAcquisitionType;
  297.         }
  298.         return new JsonResponse(
  299.             [
  300.                 'success' => true,
  301.                 'acquisitionTypes' => $returnData,
  302.             ]
  303.         );
  304.     }
  305.     /**
  306.      * Get all possible insurances for a specific ShopTicketCatalog
  307.      *
  308.      * @Route("/insurances")
  309.      *
  310.      * @param Request $request
  311.      *
  312.      * @return JsonResponse
  313.      *
  314.      * @OA\Post(
  315.      *     path="/bookingAPI/ticket/insurances",
  316.      *     description="Get all possible insurances for a specific ShopTicketCatalog",
  317.      *     tags={"Ticket"},
  318.      *
  319.      *     @OA\RequestBody(
  320.      *      required=true,
  321.      *
  322.      *           @OA\JsonContent(
  323.      *              required={"catalogId","ticketId"},
  324.      *
  325.      *              @OA\Property( property="catalogId", type="int", example=1501805),
  326.      *              @OA\Property( property="ticketId", type="int", example=518928)
  327.      *           )
  328.      *      ),
  329.      *
  330.      *  @OA\Response(
  331.      *          response=200,
  332.      *          description="Successful operation",
  333.      *
  334.      *          @OA\MediaType(
  335.      *              mediaType="application/json",
  336.      *
  337.      *              @OA\Schema(
  338.      *                  type = "object",
  339.      *
  340.      *                      @OA\Property(property="success", type="boolean"),
  341.      *                      @OA\Property(property="insurances", type="array",
  342.      *
  343.      *                          @OA\Items(type="object", ref="#/components/schemas/TicketInsurancePrice")
  344.      *                      ),
  345.      *              )
  346.      *          )
  347.      *      ),
  348.      *
  349.      *     @OA\Response(
  350.      *          response=500,
  351.      *          description="Error",
  352.      *      ),
  353.      *
  354.      * )
  355.      */
  356.     public function getInsurances(Request $requestShopService $shopHelper): JsonResponse
  357.     {
  358.         try {
  359.             $body $request->getContent();
  360.             $requestData json_decode($bodytrue);
  361.             // only for testing purposes
  362.             if (!$requestData && $request->get('debug')) {
  363.                 $requestData['catalogId'] = 1501805;
  364.                 $requestData['ticketId'] = 518928;
  365.             }
  366.             //check for required params
  367.             $required = ['ticketId''catalogId'];
  368.             foreach ($required as $key) {
  369.                 if (!$requestData || !array_key_exists($key$requestData) || !$requestData[$key]) {
  370.                     $this->bookingapiLogger->warning('required data for ticket/insurances endpoint ist missing. missing ' $key);
  371.                     return $this->sendErrors([$key ' missing'], 400);
  372.                 }
  373.             }
  374.             $catalog TicketCatalog::getById($requestData['catalogId']);
  375.             if (!$catalog) {
  376.                 throw new \Exception('ticketCatalog with id ' $request['catalogId'] . 'not found');
  377.             }
  378.             $filter = new TicketFilter(Carbon::now());
  379.             $filter->setExactDuration(false);
  380.             $filter->setUseTicketValidity(true);
  381.             $allowedConsumers = [];
  382.             foreach ($catalog->getTicketConsumerCategories() as $allowedConsumer) {
  383.                 $allowedConsumers[] = $allowedConsumer->getId();
  384.             }
  385.             if ($allowedConsumers) {
  386.                 $filter->setConsumers($allowedConsumers);
  387.             }
  388.             $ticketCatalogAvailability $filter->getTicketCatalogAvailability($catalogtrue);
  389.             //insurances
  390.             $insuranceData $shopHelper->getInsurancesForProduct($ticketCatalogAvailability);
  391.             $insurances $this->getInsuranceData($catalog$ticketCatalogAvailability$insuranceData);
  392.             $returnData = [];
  393.             if (isset($insurances[$requestData['ticketId']])) {
  394.                 foreach ($insurances[$requestData['ticketId']] as $ticketInsurancePrice) {
  395.                     $returnData[] = $ticketInsurancePrice->jsonSerialize();
  396.                 }
  397.             }
  398.             if ($request->get('debug')) {
  399.                 p_r($returnData);
  400.                 die();
  401.             }
  402.             return new JsonResponse(
  403.                 [
  404.                     'success' => true,
  405.                     'insurances' => $returnData,
  406.                 ]
  407.             );
  408.         } catch (\Throwable $throwable) {
  409.             $this->bookingapiLogger->error($throwable->getMessage() . 'Stack Trace: ' $throwable->getTraceAsString());
  410.             return $this->sendErrors([$throwable->getMessage()], 500);
  411.         }
  412.     }
  413.     /**
  414.      * Keycard validation
  415.      *
  416.      * @Route("/validateKeycard")
  417.      *
  418.      * @param Request $request
  419.      *
  420.      * @return JsonResponse
  421.      *
  422.      * @OA\Post(
  423.      *      path="/bookingAPI/ticket/validateKeycard",
  424.      *     description="validates key card number; if a short number is given, mapping to a skidata key card number is attempted",
  425.      *     tags={"Ticket"},
  426.      *
  427.      *      @OA\RequestBody(
  428.      *      required=true,
  429.      *
  430.      *           @OA\JsonContent(
  431.      *              required={"keycardNumber"},
  432.      *
  433.      *              @OA\Property( property="keycardNumber", type="string", example="01-11111111111111111111-1 or U111111"),
  434.      *           )
  435.      *      ),
  436.      *
  437.      *     @OA\Response(
  438.      *          response=200,
  439.      *          description="Successful operation",
  440.      *
  441.      *          @OA\MediaType(
  442.      *              mediaType="application/json",
  443.      *
  444.      *              @OA\Schema(
  445.      *                  type = "object",
  446.      *
  447.      *                  @OA\Property(property="success", type="boolean"),
  448.      *                  @OA\Property(property="keycardNumber", type="string"),
  449.      *                  @OA\Property(property="keycardShortNumber", type="string"),
  450.      *                  @OA\Property(property="valid", type="boolean"),
  451.      *              )
  452.      *          )
  453.      *
  454.      *     ),
  455.      *
  456.      *     @OA\Response(
  457.      *          response=500,
  458.      *          description="Error"
  459.      *     ),
  460.      *      @OA\Response(
  461.      *          response=400,
  462.      *          description="Invalid request - data is missing."
  463.      *     )
  464.      * )
  465.      */
  466.     public function validateKeycard(Request $requestApiService $apiServiceOrderService $orderServiceValidatorInterface $validator): JsonResponse
  467.     {
  468.         try {
  469.             $body $request->getContent();
  470.             $requestData json_decode($bodytrue);
  471.             // only for testing purposes
  472.             if (!$requestData && $request->get('debug')) {
  473.                 $requestData = [
  474.                     'keycardNumber' => '01-16147133534995632168-4',
  475.                 ];
  476.             }
  477.             // check for required params
  478.             if (!$requestData || !array_key_exists('keycardNumber'$requestData) || !$requestData['keycardNumber']) {
  479.                 $this->bookingapiLogger->warning('required keycardNumber for ticket/validateKeycard endpoint is missing');
  480.                 return $this->sendErrors(['keycardNumber missing'], 400);
  481.             }
  482.             $keycardNr $requestData['keycardNumber'];
  483.             $keycardNumberObj KeycardNumber::getByShortNumber($keycardNr1);
  484.             if (strpos($keycardNr'-') === false) {
  485.                 //this is a shortNumber - try to map
  486.                 if ($keycardNumberObj && $keycardNumberObj->getNumber()) {
  487.                     $keycardNr $keycardNumberObj->getNumber();
  488.                 }
  489.             }
  490.             $valid $apiService->validateKeycard($orderService$validator$keycardNr);
  491.             return new JsonResponse(
  492.                 [
  493.                     'success' => true,
  494.                     'keycardNumber' => $keycardNr,
  495.                     'keycardShortNumber' => $keycardNumberObj strval($keycardNumberObj->getShortNumber()) : '',
  496.                     'valid' => $valid,
  497.                 ]
  498.             );
  499.         } catch (\Throwable $throwable) {
  500.             $this->bookingapiLogger->error($throwable->getMessage() . 'Stack Trace: ' $throwable->getTraceAsString());
  501.             return $this->sendErrors([$throwable->getMessage()], 500);
  502.         }
  503.     }
  504.     /**
  505.      * Swisspass validation
  506.      *
  507.      * @Route("/validateSwisspass")
  508.      *
  509.      * @param Request $request
  510.      *
  511.      * @return JsonResponse
  512.      *
  513.      * @OA\Post(
  514.      *      path="/bookingAPI/ticket/validateSwisspass",
  515.      *     description="validates Swisspass number",
  516.      *     tags={"Ticket"},
  517.      *
  518.      *      @OA\RequestBody(
  519.      *      required=true,
  520.      *
  521.      *           @OA\JsonContent(
  522.      *              required={"swisspassNumber","zip"},
  523.      *
  524.      *              @OA\Property( property="swisspassNumber", type="string", example="S42682117607"),
  525.      *              @OA\Property( property="zip", type="string", example="3048")
  526.      *           )
  527.      *      ),
  528.      *
  529.      *     @OA\Response(
  530.      *          response=200,
  531.      *          description="Successful operation",
  532.      *
  533.      *          @OA\MediaType(
  534.      *              mediaType="application/json",
  535.      *
  536.      *              @OA\Schema(
  537.      *                  type = "object",
  538.      *
  539.      *                  @OA\Property(property="success", type="boolean"),
  540.      *                  @OA\Property(property="valid", type="boolean"),
  541.      *              )
  542.      *          )
  543.      *
  544.      *     ),
  545.      *
  546.      *     @OA\Response(
  547.      *          response=500,
  548.      *          description="Error"
  549.      *     ),
  550.      *      @OA\Response(
  551.      *          response=400,
  552.      *          description="Invalid request - data is missing."
  553.      *     )
  554.      * )
  555.      */
  556.     public function validateSwisspass(Request $requestApiService $apiServiceOrderService $orderServiceValidatorInterface $validator): JsonResponse
  557.     {
  558.         try {
  559.             $body $request->getContent();
  560.             $requestData json_decode($bodytrue);
  561.             // only for testing purposes
  562.             if (!$requestData && $request->get('debug')) {
  563.                 $requestData = [
  564.                     'swisspassNumber' => 'S42682117607',
  565.                     'zip' => '3048',
  566.                 ];
  567.             }
  568.             // check for required params
  569.             if (!is_array($requestData) || !array_key_exists('swisspassNumber'$requestData) || empty($requestData['swisspassNumber'])) {
  570.                 $this->bookingapiLogger->warning('required swisspassNumber for ticket/validateSwisspass endpoint is missing');
  571.                 return $this->sendErrors(['swisspassNumber missing'], 400);
  572.             }
  573.             if (!is_array($requestData) || !array_key_exists('zip'$requestData) || empty($requestData['zip'])) {
  574.                 $this->bookingapiLogger->warning('required zip for ticket/validateSwisspass endpoint is missing');
  575.                 return $this->sendErrors(['zip missing'], 400);
  576.             }
  577.             $swisspassNumber $requestData['swisspassNumber'];
  578.             $zip $requestData['zip'];
  579.             try {
  580.                 $valid $apiService->validateSwisspass($swisspassNumber$zip);
  581.                 $success true;
  582.             } catch (\Throwable $throwable) {
  583.                 $valid false;
  584.                 $success false;
  585.             }
  586.             return new JsonResponse(
  587.                 [
  588.                     'success' => $success,
  589.                     'valid' => $valid,
  590.                 ]
  591.             );
  592.         } catch (\Throwable $throwable) {
  593.             $this->bookingapiLogger->error($throwable->getMessage() . 'Stack Trace: ' $throwable->getTraceAsString());
  594.             return $this->sendErrors([$throwable->getMessage()], 500);
  595.         }
  596.     }
  597.     /**
  598.      * Validate ticket booking data before submitting the order (optional step)
  599.      *
  600.      * @Route("/validateData")
  601.      *
  602.      * @OA\Post(
  603.      *     path="/bookingAPI/ticket/validateData",
  604.      *     description="Validate ticket item booking data before submitting the order (optional step)",
  605.      *     tags={"Ticket"},
  606.      *
  607.      *      @OA\RequestBody(
  608.      *          required=true,
  609.      *
  610.      *           @OA\JsonContent(
  611.      *            required={"ticket"},
  612.      *
  613.      *            @OA\Property(property="tickets", type="array", @OA\Items(type="object", ref="#/components/schemas/CartItemTicket")),
  614.      *
  615.      *            @OA\Examples(example="ticketExample", value="{""tickets"": [{
  616.     ""price"": 10,
  617.     ""ticketId"": 518928,
  618.     ""catalogId"": 1501805,
  619.     ""consumerCategoryId"": 502338,
  620.     ""date"": ""2024-11-30"",
  621.     ""deliveryType"": ""reload"",
  622.     ""deliveryCountry"": """",
  623.     ""firstName"": ""John"",
  624.     ""lastName"": ""Doe"",
  625.     ""birthday"": ""2001-01-01"",
  626.     ""keycardNumber"": ""01-16147133534995632168-4"",
  627.     ""keycardShortnumber"": """",
  628.     ""swisspassNumber"": """",
  629.     ""swisspassZip"": """",
  630.     ""insuranceId"": 518668
  631.     }]}", summary="a valid example for ticket validation")
  632.      *           )
  633.      *      ),
  634.      *     @OA\Response(
  635.      *          response=200,
  636.      *          description="Successful operation",
  637.      *
  638.      *          @OA\MediaType(
  639.      *              mediaType="application/json",
  640.      *
  641.      *              @OA\Schema(
  642.      *                  type = "object",
  643.      *
  644.      *                  @OA\Property(property="success", type="boolean"),
  645.      *                  @OA\Property(property="valid", type="boolean"),
  646.      *                  @OA\Property(property="errors", type="array",
  647.      *
  648.      *                       @OA\Items(type="string", example="keycardNumber missing")
  649.      *                  ),
  650.      *              )
  651.      *          )
  652.      *
  653.      *     ),
  654.      *
  655.      *      @OA\Response(
  656.      *          response=500,
  657.      *          description="Error"
  658.      *     ),
  659.      *      @OA\Response(
  660.      *          response=400,
  661.      *          description="Invalid request - data is missing."
  662.      *     )
  663.      * )
  664.      *
  665.      * @param Request $request
  666.      *
  667.      * @return JsonResponse
  668.      */
  669.     public function validateTicketData(Request $requestApiService $apiServiceOrderService $orderServiceShopService $shopHelperValidatorInterface $validator): JsonResponse
  670.     {
  671.         try {
  672.             $body $request->getContent();
  673.             $requestData json_decode($bodytrue);
  674.             // only for testing purposes
  675.             if (!$requestData && $request->get('debug')) {
  676.                 $requestData = ['tickets' => [[
  677.                     'ticketId' => 518928,
  678.                     'catalogId' => 1501805,
  679.                     'consumerCategoryId' => 502338,
  680.                     'date' => '2024-11-04',
  681. //                    "profilePicture" =>  "string", // TODO TBD
  682.                     'deliveryType' => 'reload',
  683.                     'firstName' => 'Michaela',
  684.                     'lastName' => 'Steyrer',
  685.                     'birthday' => '2001-08-16',
  686.                     //'keycardNumber' =>  '01-16147133534995632168-4',
  687.                     "keycardShortnumber" => "U123123",
  688.                     //'insuranceId' => 518668,
  689.                     'deliveryCountry' => 'CH',
  690.                 ]]];
  691.             }
  692.             if (!isset($requestData['tickets'])) {
  693.                 $this->bookingapiLogger->warning('required array tickets for ticket/validateData endpoint is missing');
  694.                 return $this->sendErrors(['ticket data missing'], 400);
  695.             }
  696.             $errors = [];
  697.             try {
  698.                 foreach($requestData['tickets'] as $index => $ticketData) {
  699.                     $cartItemTicket = new CartItemTicket();
  700.                     $cartItemTicket->unmarshall($ticketData);
  701.                     $keycardCheck $this->keyCardNumberShortToLongCheck($cartItemTicket);
  702.                     if (!$keycardCheck) {
  703.                         return $this->sendErrors(['could not map keycard short number ' $cartItemTicket->getKeycardShortnumber()], 500);
  704.                     }
  705.                     $errors array_merge($errors$apiService->validateTicketData($shopHelper$orderService$validator$cartItemTicket'ticket '.($index+1).": "));
  706.                 }
  707.             } catch (DataMappingException $e) {
  708.                 $errors[] = $e->getMessage();
  709.             } catch (DataMissingException $e) {
  710.                 $this->bookingapiLogger->warning('required data for ticket/validateData endpoint is missing:' $e->getMessage());
  711.                 return $this->sendErrors([$e->getMessage()], 400);
  712.             }
  713.             return new JsonResponse(
  714.                 [
  715.                     'success' => true,
  716.                     'valid' => empty($errors),
  717.                     'errors' => $errors,
  718.                 ]
  719.             );
  720.         } catch (\Throwable $throwable) {
  721.             $this->bookingapiLogger->error($throwable->getMessage() . 'Stack Trace: ' $throwable->getTraceAsString());
  722.             return $this->sendErrors([$throwable->getMessage()], 500);
  723.         }
  724.     }
  725.     /**
  726.      * @param TicketCatalog $catalog
  727.      * @param TicketCatalogAvailability $ticketCatalogAvailability
  728.      * @param array<mixed> $insuranceData
  729.      *
  730.      * @return array<int,mixed>
  731.      *
  732.      * @throws \Exception
  733.      */
  734.     protected function getInsuranceData(TicketCatalog $catalogTicketCatalogAvailability $ticketCatalogAvailability, array $insuranceData): array
  735.     {
  736.         $insurances = [];
  737.         if (isset($insuranceData['insurances'])) {
  738.             foreach ($insuranceData['insurances'] as $ticketId => $insuranceProducts) {
  739.                 foreach ($insuranceProducts as $insuranceProduct) {
  740.                     /**
  741.                      * @var TicketProduct $insuranceProduct
  742.                      */
  743.                     $insuranceMetaListing = new TicketshopTicketAdditional\Listing();
  744.                     $insuranceMetaListing->setCondition("o_id in (select src_id from object_relations_TSF_Additional where dest_id = " $insuranceProduct->getId() . ")");
  745.                     $metaListings $insuranceMetaListing->load();
  746.                     $descriptionLocalized = new LocalizedText();
  747.                     $nameLocalized = new LocalizedText();
  748.                     $groupNameLocalized = new LocalizedText();
  749.                     $groupId 0;
  750.                     foreach ($this->languages as $language) {
  751.                         if (count($metaListings) > 0) {
  752.                             $descriptionLocalized->$language $metaListings[0]->getDescription($language) ?: '';
  753.                             $groupNameLocalized->$language $metaListings[0]->getName($language) ?: '';
  754.                             $groupId $metaListings[0]->getId();
  755.                         } else {
  756.                             $descriptionLocalized->$language '';
  757.                             $groupNameLocalized->$language '';
  758.                         }
  759.                         $nameLocalized->$language $insuranceProduct->getName($language) ?: '';
  760.                     }
  761.                     foreach ($ticketCatalogAvailability->getTicketAvailability() as $ticketAvailability) {
  762.                         if ($ticketAvailability->getTicketProduct()->getId() == $ticketId) {
  763.                             $insurancePricesPerConsumer = [];
  764.                             $ticketArticleId null;
  765.                             foreach ($ticketAvailability->getSortedConsumerAvailabilities() as $consumerAvailability) {
  766.                                 $insurancePriceInfo $insuranceProduct->getOSPriceInfo(1, [],
  767.                                     $ticketAvailability->getValidFrom(),
  768.                                     $consumerAvailability->getTicketConsumer(), catalog$catalog);
  769.                                 $insurancePrice $insurancePriceInfo->getTotalPrice()->getGrossAmount()->asNumeric();
  770.                                 $insurancePricesPerConsumer[$consumerAvailability->getTicketConsumer()->getId()] = $insurancePrice;
  771.                                 if (!$ticketArticleId || $ticketArticleId === $consumerAvailability->getId()) {
  772.                                     $ticketArticleId $consumerAvailability->getId();
  773.                                 } else {
  774.                                     continue 2;
  775.                                     //throw new \Exception('Insurance TicketArticle '.$ticketArticleId.' ID is not the same across all consumer availabilities for ticket ' . $ticketAvailability->getTicketProduct()->getId());
  776.                                 }
  777.                             }
  778.                             $apiInsurancePrice = new TicketInsurancePrice();
  779.                             $apiInsurancePrice->insuranceId $insuranceProduct->getId();
  780.                             $apiInsurancePrice->ticketArticleId $ticketArticleId;
  781.                             $apiInsurancePrice->priceByConsumerCategory $insurancePricesPerConsumer;
  782.                             $apiInsurancePrice->name $nameLocalized;
  783.                             $apiInsurancePrice->description $descriptionLocalized;
  784.                             $apiInsurancePrice->groupName $groupNameLocalized;
  785.                             $apiInsurancePrice->groupId $groupId;
  786.                             $insurances[$ticketAvailability->getTicketProduct()->getId()][$insuranceProduct->getId()] = $apiInsurancePrice;
  787.                         }
  788.                     }
  789.                 }
  790.             }
  791.         }
  792.         return $insurances;
  793.     }
  794.     /**
  795.      * @param ShopTicketCatalog $catalog
  796.      * @param TicketProduct $ticketProduct
  797.      * @param Carbon $from
  798.      * @param Carbon $to
  799.      *
  800.      * @return TicketUpgradePrice[]
  801.      *
  802.      * @throws \Exception
  803.      */
  804.     protected function getUpgradeData(ShopTicketCatalog $catalogTicketProduct $ticketProductCarbon $fromCarbon $to): array
  805.     {
  806.         $metaProduct $ticketProduct->getMetaProduct();
  807.         $relatedProductIds = [];
  808.         if($metaProduct){
  809.             foreach($metaProduct->getRelatedTo() as $relatedProduct){
  810.                 $relatedProductIds[]=$relatedProduct->getId();
  811.             }
  812.         }
  813.         $upgradeCatalogKeys = [];
  814.         foreach($catalog->getUpgrades() as $objectMetadata){
  815.             $upgradeCatalogKeys[$objectMetadata->getObject()->getId()] = $objectMetadata->getData()['name'];
  816.         }
  817.         $upgrades = [];
  818.         foreach ($catalog->getUpgrades() as $upgrade) {
  819.             /** @var TicketCatalog $upgradeObject */
  820.             $upgradeObject $upgrade->getObject();
  821.             $upgradeFilter = new TicketFilter($from$to);
  822.             $upgradeFilter->setExactDuration(true);
  823.             $upgradeFilter->setIgnorePrice(true);
  824.             $upgradeFilter->setUseTicketValidity(true);
  825.             $upgradeCatalogAvailability $upgradeFilter->getTicketCatalogAvailability($upgradeObjectfalse);
  826.             if ($upgradeCatalogAvailability->hasAvailability() && !$upgradeObject->getIsNotBookable()) {
  827.                 //special logic for flex passes where there is no shuttle
  828.                 $useUpgrade false;
  829.                 if ($metaProduct) {
  830.                     if (array_intersect($metaProduct->getRelatedTo(), $upgradeObject->getTicketProducts())) {
  831.                         $useUpgrade true;
  832.                     }
  833.                 } else {
  834.                     $useUpgrade true;
  835.                 }
  836.                 if ($useUpgrade) {
  837.                     $nameLocalized = new LocalizedText();
  838.                     $descriptionLocalized = new LocalizedText();
  839.                     $infoBubbleTextLocalized = new LocalizedText();
  840.                     foreach ($this->languages as $language) {
  841.                         $nameLocalized->$language $upgradeObject->getName($language) ?: '';
  842.                         $descriptionLocalized->$language $upgradeObject->getMarketplaceDescription() ?: '';
  843.                         $infoBubbleTextLocalized->$language $upgradeObject->getInfoBubbleDescription($language) ?: '';
  844.                     }
  845.                     $upgradePricesPerConsumerCategory = [];
  846.                     $ticketProductId null;
  847.                     foreach ($upgradeCatalogAvailability->getTicketAvailability() as $ticketAvailability) {
  848.                         foreach ($ticketAvailability->getConsumerAvailabilities() as $consumerAvailability) {
  849.                             $ticketProduct $consumerAvailability->getTicketProductAvailability()->getTicketProduct();
  850.                             if ($metaProduct) {
  851.                                 if ($ticketProduct != null && !in_array($ticketProduct->getId(), $relatedProductIds)) {
  852.                                     //this is not a directly related product, ignore
  853.                                     continue;
  854.                                 }
  855.                             }
  856.                             foreach ($this->languages as $language) {
  857.                                 if ($ticketProduct->getDescription($language)) {
  858.                                     $infoBubbleTextLocalized->$language $ticketProduct->getDescription($language);
  859.                                 }
  860.                             }
  861.                             if (!$ticketProductId || $ticketProductId === $ticketProduct->getId()) {
  862.                                 $ticketProductId $ticketProduct->getId();
  863.                             } else {
  864.                                 continue 2;
  865.                                 //throw new \Exception('Upgrade TicketArticle ID '.$ticketProductId.' is not the same across all consumer availabilities for ticket ' . $ticketAvailability->getTicketProduct()->getId());
  866.                             }
  867.                             /** @phpstan-ignore-next-line */
  868.                             $priceInfo $ticketProduct->getOSPriceInfo(1, [], $ticketAvailability->getValidFrom(), $consumerAvailability->getTicketConsumer(), $catalog);
  869.                             $price $priceInfo->getTotalPrice()->getGrossAmount()->asNumeric();
  870.                             $upgradePricesPerConsumerCategory[$consumerAvailability->getTicketConsumer()->getId()] = $price;
  871.                         }
  872.                     }
  873.                     $apiUpgradePrice = new TicketUpgradePrice();
  874.                     $apiUpgradePrice->upgradeId $upgradeObject->getId();
  875.                     if(isset($upgradeCatalogKeys[$upgradeObject->getId()])){
  876.                         $apiUpgradePrice->setUpgradeKey($upgradeCatalogKeys[$upgradeObject->getId()]);
  877.                     }
  878.                     $apiUpgradePrice->ticketArticleId $ticketProductId;
  879.                     $apiUpgradePrice->priceByConsumerCategory $upgradePricesPerConsumerCategory;
  880.                     $apiUpgradePrice->name $nameLocalized;
  881.                     $apiUpgradePrice->description $descriptionLocalized;
  882.                     $apiUpgradePrice->infoText $infoBubbleTextLocalized;
  883.                     $upgrades[] = $apiUpgradePrice;
  884.                 }
  885.             }
  886.         }
  887.         return $upgrades;
  888.     }
  889. }