src/Service/Shop/SmartPricerService.php line 61

Open in your IDE?
  1. <?php
  2. /**
  3.  * Created by Elements.at New Media Solutions GmbH
  4.  *
  5.  */
  6. namespace App\Service\Shop;
  7. use App\Service\TicketShopFrameworkBundle\TicketFilter;
  8. use Carbon\Carbon;
  9. use Elements\Bundle\TicketShopFrameworkBundle\Model\DataObject\SiteConfigInterface;
  10. use Elements\Bundle\TicketShopFrameworkBundle\Model\DataObject\TicketCatalog;
  11. use Elements\Bundle\TicketShopFrameworkBundle\Model\DataObject\TicketProduct;
  12. use Elements\Bundle\TicketShopFrameworkBundle\Model\Shop\Ticketing\TicketCatalogAvailability;
  13. use Elements\Bundle\TicketShopFrameworkBundle\Service\SiteConfigService;
  14. use GuzzleHttp\Client;
  15. use GuzzleHttp\Exception\GuzzleException;
  16. use Pimcore;
  17. use Pimcore\Bundle\EcommerceFrameworkBundle\Factory;
  18. use Pimcore\Bundle\EcommerceFrameworkBundle\Model\Currency;
  19. use Pimcore\Bundle\EcommerceFrameworkBundle\PriceSystem\Price;
  20. use Pimcore\Bundle\EcommerceFrameworkBundle\Type\Decimal;
  21. use Pimcore\Db;
  22. use Psr\Log\LoggerInterface;
  23. class SmartPricerService
  24. {
  25.     const DEMAND_PRESSURE_TABLE_NAME 'bundle_smartpricer_demand_pressure';
  26.     const TIME_PRESSURE_TABLE_NAME 'bundle_smartpricer_time_pressure';
  27.     protected LoggerInterface $logger;
  28.     protected ?SiteConfigInterface $config;
  29.     private string $apiDomain 'https://api.app.smart-pricer.com';
  30.     private string $customer 'zermatt';
  31.     private string $demandPressureSeason 'ad508dcd-d652-4c60-9275-f1ccde3ace4c';
  32.     private string $timePressureSeason '1f17762e-290f-4b94-a0eb-6b8b57945b18';
  33.     private string $APIToken 'hHFmrIph8Dd2Ly+VZRN/CBvys89T/kYJaNEj3mu4fjuvB3SxSZpsOTfCXbhhX6NwHCMhtsXBZlNCyy/mnthZZK2XeHVS6FmITTdC/2lPf0eV4FcFjDDcgI4Pong307kl/5dApQ==';
  34.     private string $demandPressureAPIToken 'FHSHVyZCiyBwa5Aw4hkO6QvJu1ZlW5t3O6yzORgAD0uLtZjshqrez0J4J2gU1IMMOBD68a5f0WKEoBVGkL+Z2jWeyMclmSJmP0zODWc7Cqal8YXqgCcbmXK3gxTaeJ5a0jJV4w==';
  35.     /** @var array<string, array<string, Price>> $livePrices */
  36.     public array $livePrices = [];
  37.     /** @var array<string> $cat1Array */
  38.     public array $cat1Array = [];
  39.     /** @var array<string> $cat3Array */
  40.     public array $cat3Array = [];
  41.     /**
  42.      * SmartPricerService constructor.
  43.      */
  44.     public function __construct(
  45.         LoggerInterface $smartpricerLogger,
  46.         SiteConfigService $siteConfigService,
  47.         private Client $client
  48.     ) {
  49.         $this->logger $smartpricerLogger;
  50.         $this->config $siteConfigService->getSiteConfig();
  51.     }
  52.     protected function getConnection(): Db\ConnectionInterface
  53.     {
  54.         return Db::get();
  55.     }
  56.     public function getLivePrice(string $productExternalIdstring $consumerExternalIdCarbon $validityDate): ?Price
  57.     {
  58.         try {
  59.             $debugID $this->guidv4();
  60.             $response $this->client->request(
  61.                 'POST',
  62.                 $this->apiDomain '/steeringservice/v1/steering/' $this->customer '/steer',
  63.                 [
  64.                     'json' => [
  65.                         'categories' => [
  66.                             'cat01' => $productExternalId,
  67.                             'cat03' => $consumerExternalId,
  68.                         ],
  69.                         'season' => $this->timePressureSeason,
  70.                         'validAt' => $validityDate->getTimestamp(),
  71.                     ],
  72.                     'headers' => [
  73.                         'Authorization' => 'Bearer ' $this->customer ':' $this->APIToken,
  74.                         'X-Correlation-ID' => $debugID,
  75.                     ],
  76. //                        'connect_timeout' => 5,
  77. //                        'timeout' => 5,
  78.                 ]
  79.             );
  80.             if ($response->getStatusCode() == 200) {
  81.                 $data json_decode($response->getBody()->getContents());
  82.                 if (empty($data->error)) {
  83.                     $this->logger->info('SmartPricer live-price request debug ID', ['ID' => $debugID]);
  84.                     if (isset($data->result)) {
  85.                         $currency = new Currency(Factory::getInstance()->getEnvironment()->getDefaultCurrency()->getShortName());
  86.                         return new Price(Decimal::fromRawValue($data->result->price0)->withScale(4), $currencyfalse);
  87.                     } else {
  88.                         $this->logger->error('No Price found.', [
  89.                             'error' => $data->error,
  90.                             'productExternalId' => $productExternalId,
  91.                             'customerExternalId' => $consumerExternalId,
  92.                             'validityDate' => $validityDate->getTimestamp(),
  93.                             'debugId' => $debugID,
  94.                         ]);
  95.                     }
  96.                 } else {
  97.                     $this->logger->error('SmartPricer response-error.', [
  98.                         'error' => $data->error,
  99.                         'productExternalId' => $productExternalId,
  100.                         'customerExternalId' => $consumerExternalId,
  101.                         'validityDate' => $validityDate->getTimestamp(),
  102.                         'debugId' => $debugID,
  103.                     ]);
  104.                 }
  105.             }
  106.         } catch (GuzzleException $e) {
  107.             $this->logger->error('Guzzle-Error while requesting price.', [
  108.                 'exception' => $e->getMessage(),
  109.                 'productExternalId' => $productExternalId,
  110.                 'customerExternalId' => $consumerExternalId,
  111.                 'validityDate' => $validityDate->getTimestamp(),
  112.             ]);
  113.         }
  114.         return null;
  115.     }
  116.     /**
  117.      * @param array<string> $productExternalIdMatrix
  118.      * @param array<string> $consumerExternalIdMatrix
  119.      * @param Carbon $startDate
  120.      * @param Carbon $endDate
  121.      */
  122.     public function setLivePrices(array $productExternalIdMatrix, array $consumerExternalIdMatrixCarbon $startDateCarbon $endDate): void
  123.     {
  124.         try {
  125.             $debugID $this->guidv4();
  126.             $response $this->client->request(
  127.                 'POST',
  128.                 $this->apiDomain '/steeringservice/v1/steering/' $this->customer '/steer-matrix',
  129.                 [
  130.                     'json' => [
  131.                         'categories' => [
  132.                             'cat01' => $productExternalIdMatrix,
  133.                             'cat03' => $consumerExternalIdMatrix,
  134.                         ],
  135.                         'season' => $this->timePressureSeason,
  136.                         'startDate' => $startDate->getTimestamp(),
  137.                         'endDate' => $endDate->getTimestamp(),
  138.                     ],
  139.                     'headers' => [
  140.                         'Authorization' => 'Bearer ' $this->customer ':' $this->APIToken,
  141.                         'X-Correlation-ID' => $debugID,
  142.                     ],
  143.                 ]
  144.             );
  145.             if ($response->getStatusCode() == 200) {
  146.                 $data json_decode($response->getBody()->getContents());
  147.                 if (!empty($data->ranges)) {
  148.                     $this->logger->info('SmartPricer live-price request debug ID', ['ID' => $debugID]);
  149.                     if (isset($data->ranges)) {
  150.                         $currency = new Currency(Factory::getInstance()->getEnvironment()->getDefaultCurrency()->getShortName());
  151.                         $prices = [];
  152.                         foreach ($data->ranges as $price) {
  153.                             if (!empty($price->categories) && !empty($price->steerings)) {
  154.                                 $prices[$price->categories->cat01][$price->categories->cat03] = new Price(Decimal::fromRawValue($price->steerings[0]->result->price0)->withScale(4), $currencyfalse);
  155.                             }
  156.                         }
  157.                         $this->livePrices $prices;
  158.                     } else {
  159.                         $this->logger->error('No Price found.', [
  160.                             'productExternalId' => implode(', '$productExternalIdMatrix),
  161.                             'customerExternalId' => implode(', '$consumerExternalIdMatrix),
  162.                             'startDate' => $startDate->getTimestamp(),
  163.                             'endDate' => $endDate->getTimestamp(),
  164.                             'debugId' => $debugID,
  165.                         ]);
  166.                     }
  167.                 } else {
  168.                     $this->logger->error('SmartPricer response-error.', [
  169.                         'productExternalId' => implode(', '$productExternalIdMatrix),
  170.                         'customerExternalId' => implode(', '$consumerExternalIdMatrix),
  171.                         'startDate' => $startDate->getTimestamp(),
  172.                         'endDate' => $endDate->getTimestamp(),
  173.                         'debugId' => $debugID,
  174.                     ]);
  175.                 }
  176.             }
  177.         } catch (GuzzleException $e) {
  178.             $this->logger->error('Guzzle-Error while requesting price.', [
  179.                 'exception' => $e->getMessage(),
  180.                 'productExternalId' => implode(', '$productExternalIdMatrix),
  181.                 'customerExternalId' => implode(', '$consumerExternalIdMatrix),
  182.                 'startDate' => $startDate->getTimestamp(),
  183.                 'endDate' => $endDate->getTimestamp(),
  184.             ]);
  185.         }
  186.     }
  187.     /**
  188.      * @param array<mixed> $insurances
  189.      * @param array<mixed> $originalConsumerGroups
  190.      * @param array<mixed> $upgradeIds
  191.      */
  192.     public function setPriceMatrix(TicketCatalogAvailability $ticketCatalogAvailability, array $insurances, array $originalConsumerGroupsCarbon $startDateCarbon $endDateTicketProduct $ticket null, array $upgradeIds = []): void
  193.     {
  194.         $catalog $ticketCatalogAvailability->getTicketCatalog();
  195.         $isUpgrade = !empty($ticket);
  196.         foreach ($ticketCatalogAvailability->getTicketAvailability() as $ticketAvailability) {
  197.             $originalTicket $ticketAvailability->getTicketProduct();
  198.             if (!$isUpgrade) {
  199.                 $ticket $originalTicket;
  200.             } else {
  201.                 /** @var \App\Model\Shop\Ticket\TicketProduct $ticket */
  202.                 $metaProduct $ticket->getMetaProduct();
  203.                 if ($metaProduct && !in_array($originalTicket$metaProduct->getRelatedTo(), true)) {
  204.                     continue;
  205.                 }
  206.             }
  207.             foreach ($catalog->getUpgrades() as $upgrade) {
  208.                 $upgradeCatalog $upgrade->getObject();
  209.                 if ($upgradeCatalog instanceof TicketCatalog) {
  210.                     $upgradeFilter = new TicketFilter($startDate$endDate);
  211.                     $upgradeFilter->setExactDuration(true);
  212.                     $upgradeFilter->setIgnorePrice(true);
  213.                     $allowedConsumers = [];
  214.                     foreach ($upgradeCatalog->getTicketConsumerCategories() as $allowedConsumer) {
  215.                         $allowedConsumers[] = $allowedConsumer->getId();
  216.                     }
  217.                     if ($allowedConsumers) {
  218.                         $upgradeFilter->setConsumers($allowedConsumers);
  219.                     }
  220.                     $catalogAvailability $upgradeFilter->getTicketCatalogAvailability($upgradeCatalogtrue);
  221.                     if ($catalogAvailability->hasAvailability() && !$upgradeCatalog->getIsNotBookable()) {
  222.                         $this->setPriceMatrix($catalogAvailability$insurances$originalConsumerGroups$startDate$endDate$ticket$upgradeIds);
  223.                     }
  224.                 }
  225.             }
  226.             foreach ($ticketAvailability->getSortedConsumerAvailabilities() as $consumerAvailability) {
  227.                 $consumer $consumerAvailability->getTicketConsumer();
  228.                 $product $consumerAvailability->getTicketProductAvailability()->getTicketProduct();
  229.                 $this->cat1Array[] = $product->getSkidataProduct()?->getExternalId();
  230.                 $this->cat3Array[] = $consumer->getSkidataConsumerForSkidataProduct($product->getSkidataProduct())?->getExternalId();
  231.             }
  232.         }
  233.     }
  234.     /**
  235.      * @param string $productExternalId
  236.      * @param string $customerExternalId
  237.      * @param Carbon $validityDate
  238.      *
  239.      * @return float[]|null
  240.      *
  241.      * @throws \Doctrine\DBAL\Driver\Exception
  242.      */
  243.     public function getTimePressure(string $productExternalIdstring $customerExternalIdCarbon $validityDate): ?array
  244.     {
  245.         $scores = [];
  246.         try {
  247.             //see if score is already in DB
  248.             $connection $this->getConnection();
  249.             $qb $connection->createQueryBuilder();
  250.             $timePressureData $qb->select('score''timePressureTTL''textScore')
  251.                 ->from(self::TIME_PRESSURE_TABLE_NAME)
  252.                 ->where('
  253.                 product_external_id = :productExternalId
  254.                 AND consumer_external_id = :customerExternalId
  255.                 AND validityDate = :validityDate
  256.                 AND timePressureTTL > :now
  257.                 ')
  258.                 ->setParameters([
  259.                     'productExternalId' => $productExternalId,
  260.                     'customerExternalId' => $customerExternalId,
  261.                     'validityDate' => $validityDate->getTimestamp(),
  262.                     'now' => Carbon::now()->getTimestamp(),
  263.                 ])
  264.                 ->execute()
  265.                 ->fetchAssociative();
  266.             //if data is found, check if still valid
  267.             if ($timePressureData !== false) {
  268.                 $ttl Carbon::createFromTimestamp($timePressureData['timePressureTTL']);
  269.                 if ($ttl->greaterThan(Carbon::now())) {
  270.                     $scores = [
  271.                         'score' => $timePressureData['score'],
  272.                         'textScore' => $timePressureData['textScore'],
  273.                     ];
  274.                 }
  275.             }
  276.         } catch (\Exception $e) {
  277.             $this->logger->error('Error while getting time-pressure from DB.', ['exception' => $e->getMessage()]);
  278.         }
  279.         //if no valid score is found, send a request to smartpricer and save the data
  280.         if (empty($scores)) {
  281.             try {
  282.                 $debugID $this->guidv4();
  283.                 $response $this->client->request(
  284.                     'POST',
  285.                     $this->apiDomain '/steeringservice/v1/steering/' $this->customer '/steer',
  286.                     [
  287.                         'json' => [
  288.                             'categories' => [
  289.                                 'cat01' => $productExternalId,
  290.                                 'cat03' => $customerExternalId,
  291.                             ],
  292.                             'season' => $this->timePressureSeason,
  293.                             'validAt' => $validityDate->getTimestamp(),
  294.                             'temporary' => true,
  295.                             'priority' => 2,
  296.                         ],
  297.                         'headers' => [
  298.                             'Authorization' => 'Bearer ' $this->customer ':' $this->APIToken,
  299.                             'X-Correlation-ID' => $debugID,
  300.                         ],
  301. //                        'connect_timeout' => 5,
  302. //                        'timeout' => 5,
  303.                     ]
  304.                 );
  305.                 if ($response->getStatusCode() == 200) {
  306.                     try {
  307.                         $data json_decode($response->getBody()->getContents());
  308.                         if (empty($data->error)) {
  309.                             $metadata $data->result->metadata ?? null;
  310.                             $qb $connection->createQueryBuilder();
  311.                             $qb->insert(self::TIME_PRESSURE_TABLE_NAME)
  312.                                 ->values([
  313.                                     'product_external_id' => $productExternalId,
  314.                                     'consumer_external_id' => $customerExternalId,
  315.                                     'lastUpdated' => Carbon::now()->getTimestamp(),
  316.                                     'validityDate' => $validityDate->getTimestamp(),
  317.                                     'score' => $metadata $metadata->timePressureScore 0,
  318.                                     'textScore' => $metadata $metadata->textPressureScore 0,
  319.                                     'timePressureTTL' => $metadata $metadata->timePressureTTL Carbon::now()->addMinutes(5)->getTimestamp(),
  320.                                 ])
  321.                                 ->execute();
  322.                             $this->logger->info('SmartPricer time-pressure request debug ID', ['ID' => $debugID]);
  323.                             $scores = [
  324.                                 'score' => $metadata $metadata->timePressureScore 0,
  325.                                 'textScore' => $metadata $metadata->textPressureScore 0,
  326.                             ];
  327.                         } else {
  328.                             $this->logger->error('SmartPricer response-error.', ['error' => $data->error]);
  329.                         }
  330.                     } catch (\Exception $e) {
  331.                         $this->logger->error('SmartPricer database error.', ['exception' => $e->getMessage()]);
  332.                     }
  333.                 }
  334.             } catch (GuzzleException $e) {
  335.                 $this->logger->error('Guzzle-Error while requesting time-pressure.', ['exception' => $e->getMessage()]);
  336.             }
  337.         }
  338.         return $scores;
  339.     }
  340.     public function getDemandPressureBulkQuery(): void
  341.     {
  342.         try {
  343.             /** @var Pimcore\Model\DataObject\SiteConfig $config */
  344.             $config $this->config;
  345.             if ($startDate $config->getDemandPressureSeasonStart()) {
  346.                 $startDate $startDate->lessThan(Carbon::today()) ? Carbon::today() : $startDate;
  347.             } else {
  348.                 $startDate Carbon::today();
  349.             }
  350.             $response $this->client->request(
  351.                 'POST',
  352.                 $this->apiDomain '/steeringservice/v1/steering/' $this->customer '/steer-demand-pressure',
  353.                 [
  354.                     'headers' => [
  355.                         'Authorization' => 'Bearer ' $this->customer ':' $this->demandPressureAPIToken,
  356.                     ],
  357.                     'json' => [
  358.                         'season' => $this->demandPressureSeason,
  359.                         'startDate' => $startDate->startOfDay()->getTimestamp(),
  360.                         'endDate' => $config->getDemandPressureSeasonEnd()?->startOfDay()->getTimestamp(),
  361.                         'intervalMinutes' => 1440,
  362.                         'ignoreErrors' => true,
  363.                         'priority' => 0,
  364.                     ],
  365.                 ]
  366.             );
  367.             if ($response->getStatusCode() == 200) {
  368.                 try {
  369.                     $data json_decode($response->getBody()->getContents());
  370.                     $connection $this->getConnection();
  371.                     foreach ($data->ranges as $item) {
  372.                         /** @phpstan-ignore-next-line  */
  373.                         $connection->query('INSERT INTO bundle_smartpricer_demand_pressure(pointInTime, score) VALUES(?, ?) ON DUPLICATE KEY UPDATE score = ?',
  374.                             [
  375.                                 $item->pointInTime,
  376.                                 $item->result->score,
  377.                                 $item->result->score,
  378.                             ])
  379.                             ->execute();
  380.                     }
  381.                 } catch (\Exception $e) {
  382.                     $this->logger->error('SmartPricer database error.', ['exception' => $e->getMessage()]);
  383.                 }
  384.             }
  385.         } catch (GuzzleException $e) {
  386.             $this->logger->error('Guzzle-Error while requesting demand-pressure.', ['exception' => $e->getMessage()]);
  387.         }
  388.     }
  389.     public function getPressureText(float $pressure): string
  390.     {
  391.         /** @var Pimcore\Model\DataObject\SiteConfig $config */
  392.         $config $this->config;
  393.         $lowTill $config->getLowTill() ?? 0.143;
  394.         $ratherLowTill $config->getRatherLowTill() ?? 0.285;
  395.         $normalTill $config->getNormalTill() ?? 0.43;
  396.         $ratherHighTill $config->getRatherHighTill() ?? 0.715;
  397.         $pressureText 'low';
  398.         switch ($pressure) {
  399.             case $pressure >= $lowTill && $pressure $ratherLowTill:
  400.                 $pressureText 'rather-low';
  401.                 break;
  402.             case $pressure >= $ratherLowTill && $pressure $normalTill:
  403.                 $pressureText 'normal';
  404.                 break;
  405.             case $pressure >= $normalTill && $pressure $ratherHighTill:
  406.                 $pressureText 'rather-high';
  407.                 break;
  408.             case $pressure >= $ratherHighTill:
  409.                 $pressureText 'high';
  410.                 break;
  411.         }
  412.         return $pressureText;
  413.     }
  414.     public function getPriceAvailabilityText(float $pressure): string
  415.     {
  416.         /** @var Pimcore\Model\DataObject\SiteConfig $config */
  417.         $config $this->config;
  418.         $fewTicketsTill $config->getFewTicketsTill() ?? 0.4;
  419.         $someTicketsTill $config->getSomeTicketsTill() ?? 0.715;
  420.         $pressureText 'many-tickets-available';
  421.         switch ($pressure) {
  422.             case $pressure $fewTicketsTill:
  423.                 $pressureText 'few-tickets-available';
  424.                 break;
  425.             case  $pressure >= $fewTicketsTill && $pressure $someTicketsTill:
  426.                 $pressureText 'some-tickets-available';
  427.                 break;
  428.         }
  429.         return $pressureText;
  430.     }
  431.     /**
  432.      * @param Carbon $startDate
  433.      * @param Carbon $endDate
  434.      *
  435.      * @return array<mixed>
  436.      */
  437.     public function getDemandPressureEntries(Carbon $startDateCarbon $endDate): array
  438.     {
  439.         $db $this->getConnection();
  440.         $queryBuilder $db->createQueryBuilder();
  441.         $pressureEntries $queryBuilder
  442.             ->select('score''pointInTime')
  443.             ->from(self::DEMAND_PRESSURE_TABLE_NAME)
  444.             ->where('pointInTime BETWEEN :startTimestamp AND :endTimestamp')
  445.             ->setParameters([
  446.                 'startTimestamp' => $startDate->getTimestamp(),
  447.                 'endTimestamp' => $endDate->getTimestamp(),
  448.             ])
  449.             ->execute()
  450.             ->fetchAllAssociative();
  451.         return $pressureEntries;
  452.     }
  453.     /**
  454.      * For debugging purposes
  455.      *
  456.      * @param string|null $data
  457.      *
  458.      * @return string
  459.      *
  460.      * @throws \Exception
  461.      */
  462.     private function guidv4(string $data null): string
  463.     {
  464.         // Generate 16 bytes (128 bits) of random data or use the data passed into the function.
  465.         $data $data ?? random_bytes(16);
  466.         assert(strlen($data) == 16);
  467.         // Set version to 0100
  468.         $data[6] = chr(ord($data[6]) & 0x0f 0x40);
  469.         // Set bits 6-7 to 10
  470.         $data[8] = chr(ord($data[8]) & 0x3f 0x80);
  471.         // Output the 36 character UUID.
  472.         return vsprintf('%s%s-%s-%s-%s-%s%s%s'str_split(bin2hex($data), 4));
  473.     }
  474. }