src/Service/Shop/SmartPricerService.php line 67

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 '31f0bf44-e6ce-49ca-a28c-366dc57a2d32';
  33.     private string $APIToken 'Hj0eU6wNOfE4QdMEObDJzCOQ03iqwcDBy+DLl4aAZTl2F+XDxykYw3D67YxPE3OQOXYVuZ07pOkr/Bvp1YYviuqY4qpsjSUOF0MrXST8NtuBaKZ079DHvAiii0vJ5MRGr1Ef/Q==';
  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' => 3,
  77.                     'timeout' => 3,
  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.                     'connect_timeout' => 3,
  144.                     'timeout' => 3,
  145.                 ]
  146.             );
  147.             if ($response->getStatusCode() == 200) {
  148.                 $data json_decode($response->getBody()->getContents());
  149.                 if (!empty($data->ranges)) {
  150.                     $this->logger->info('SmartPricer live-price request debug ID', ['ID' => $debugID]);
  151.                     if (isset($data->ranges)) {
  152.                         $currency = new Currency(Factory::getInstance()->getEnvironment()->getDefaultCurrency()->getShortName());
  153.                         $prices = [];
  154.                         foreach ($data->ranges as $price) {
  155.                             if (!empty($price->categories) && !empty($price->steerings)) {
  156.                                 if (isset($price->steerings[0]->error)) {
  157.                                     $this->logger->error('No Price found.', [
  158.                                         'productExternalId' => implode(', '$productExternalIdMatrix),
  159.                                         'customerExternalId' => implode(', '$consumerExternalIdMatrix),
  160.                                         'startDate' => $startDate->getTimestamp(),
  161.                                         'endDate' => $endDate->getTimestamp(),
  162.                                         'debugId' => $debugID,
  163.                                     ]);
  164.                                 } else {
  165.                                     $prices[$price->categories->cat01][$price->categories->cat03] = new Price(Decimal::fromRawValue($price->steerings[0]->result->price0)->withScale(4), $currencyfalse);
  166.                                 }
  167.                             }
  168.                         }
  169.                         $this->livePrices $prices;
  170.                     } else {
  171.                         $this->logger->error('No Price found.', [
  172.                             'productExternalId' => implode(', '$productExternalIdMatrix),
  173.                             'customerExternalId' => implode(', '$consumerExternalIdMatrix),
  174.                             'startDate' => $startDate->getTimestamp(),
  175.                             'endDate' => $endDate->getTimestamp(),
  176.                             'debugId' => $debugID,
  177.                         ]);
  178.                     }
  179.                 } else {
  180.                     $this->logger->error('SmartPricer response-error.', [
  181.                         'productExternalId' => implode(', '$productExternalIdMatrix),
  182.                         'customerExternalId' => implode(', '$consumerExternalIdMatrix),
  183.                         'startDate' => $startDate->getTimestamp(),
  184.                         'endDate' => $endDate->getTimestamp(),
  185.                         'debugId' => $debugID,
  186.                     ]);
  187.                 }
  188.             }
  189.         } catch (GuzzleException $e) {
  190.             $this->logger->error('Guzzle-Error while requesting price.', [
  191.                 'exception' => $e->getMessage(),
  192.                 'productExternalId' => implode(', '$productExternalIdMatrix),
  193.                 'customerExternalId' => implode(', '$consumerExternalIdMatrix),
  194.                 'startDate' => $startDate->getTimestamp(),
  195.                 'endDate' => $endDate->getTimestamp(),
  196.             ]);
  197.         }
  198.     }
  199.     /**
  200.      * @param array<mixed> $insurances
  201.      * @param array<mixed> $originalConsumerGroups
  202.      * @param array<mixed> $upgradeIds
  203.      */
  204.     public function setPriceMatrix(TicketCatalogAvailability $ticketCatalogAvailability, array $insurances, array $originalConsumerGroupsCarbon $startDateCarbon $endDateTicketProduct $ticket null, array $upgradeIds = []): void
  205.     {
  206.         $catalog $ticketCatalogAvailability->getTicketCatalog();
  207.         $isUpgrade = !empty($ticket);
  208.         foreach ($ticketCatalogAvailability->getTicketAvailability() as $ticketAvailability) {
  209.             $originalTicket $ticketAvailability->getTicketProduct();
  210.             if (!$isUpgrade) {
  211.                 $ticket $originalTicket;
  212.             } else {
  213.                 /** @var \App\Model\Shop\Ticket\TicketProduct $ticket */
  214.                 $metaProduct $ticket->getMetaProduct();
  215.                 if ($metaProduct && !in_array($originalTicket$metaProduct->getRelatedTo(), true)) {
  216.                     continue;
  217.                 }
  218.             }
  219.             foreach ($catalog->getUpgrades() as $upgrade) {
  220.                 $upgradeCatalog $upgrade->getObject();
  221.                 if ($upgradeCatalog instanceof TicketCatalog) {
  222.                     $upgradeFilter = new TicketFilter($startDate$endDate);
  223.                     $upgradeFilter->setExactDuration(true);
  224.                     $upgradeFilter->setIgnorePrice(true);
  225.                     $allowedConsumers = [];
  226.                     foreach ($upgradeCatalog->getTicketConsumerCategories() as $allowedConsumer) {
  227.                         $allowedConsumers[] = $allowedConsumer->getId();
  228.                     }
  229.                     if ($allowedConsumers) {
  230.                         $upgradeFilter->setConsumers($allowedConsumers);
  231.                     }
  232.                     $catalogAvailability $upgradeFilter->getTicketCatalogAvailability($upgradeCatalogtrue);
  233.                     if ($catalogAvailability->hasAvailability() && !$upgradeCatalog->getIsNotBookable()) {
  234.                         $this->setPriceMatrix($catalogAvailability$insurances$originalConsumerGroups$startDate$endDate$ticket$upgradeIds);
  235.                     }
  236.                 }
  237.             }
  238.             foreach ($ticketAvailability->getSortedConsumerAvailabilities() as $consumerAvailability) {
  239.                 $consumer $consumerAvailability->getTicketConsumer();
  240.                 $product $consumerAvailability->getTicketProductAvailability()->getTicketProduct();
  241.                 $this->cat1Array[] = $product->getSkidataProduct()?->getExternalId();
  242.                 $this->cat3Array[] = $consumer->getSkidataConsumerForSkidataProduct($product->getSkidataProduct())?->getExternalId();
  243.             }
  244.         }
  245.     }
  246.     /**
  247.      * @param string $productExternalId
  248.      * @param string $customerExternalId
  249.      * @param Carbon $validityDate
  250.      *
  251.      * @return float[]|null
  252.      *
  253.      * @throws \Doctrine\DBAL\Driver\Exception
  254.      */
  255.     public function getTimePressure(string $productExternalIdstring $customerExternalIdCarbon $validityDate): ?array
  256.     {
  257.         $scores = [];
  258.         try {
  259.             //see if score is already in DB
  260.             $connection $this->getConnection();
  261.             $qb $connection->createQueryBuilder();
  262.             $timePressureData $qb->select('score''timePressureTTL''textScore')
  263.                 ->from(self::TIME_PRESSURE_TABLE_NAME)
  264.                 ->where('
  265.                 product_external_id = :productExternalId
  266.                 AND consumer_external_id = :customerExternalId
  267.                 AND validityDate = :validityDate
  268.                 AND timePressureTTL > :now
  269.                 ')
  270.                 ->setParameters([
  271.                     'productExternalId' => $productExternalId,
  272.                     'customerExternalId' => $customerExternalId,
  273.                     'validityDate' => $validityDate->getTimestamp(),
  274.                     'now' => Carbon::now()->getTimestamp(),
  275.                 ])
  276.                 ->execute()
  277.                 ->fetchAssociative();
  278.             //if data is found, check if still valid
  279.             if ($timePressureData !== false) {
  280.                 $ttl Carbon::createFromTimestamp($timePressureData['timePressureTTL']);
  281.                 if ($ttl->greaterThan(Carbon::now())) {
  282.                     $scores = [
  283.                         'score' => $timePressureData['score'],
  284.                         'textScore' => $timePressureData['textScore'],
  285.                     ];
  286.                 }
  287.             }
  288.         } catch (\Exception $e) {
  289.             $this->logger->error('Error while getting time-pressure from DB.', ['exception' => $e->getMessage()]);
  290.         }
  291.         //if no valid score is found, send a request to smartpricer and save the data
  292.         if (empty($scores)) {
  293.             try {
  294.                 $debugID $this->guidv4();
  295.                 $response $this->client->request(
  296.                     'POST',
  297.                     $this->apiDomain '/steeringservice/v1/steering/' $this->customer '/steer',
  298.                     [
  299.                         'json' => [
  300.                             'categories' => [
  301.                                 'cat01' => $productExternalId,
  302.                                 'cat03' => $customerExternalId,
  303.                             ],
  304.                             'season' => $this->timePressureSeason,
  305.                             'validAt' => $validityDate->getTimestamp(),
  306.                             'temporary' => true,
  307.                             'priority' => 2,
  308.                         ],
  309.                         'headers' => [
  310.                             'Authorization' => 'Bearer ' $this->customer ':' $this->APIToken,
  311.                             'X-Correlation-ID' => $debugID,
  312.                         ],
  313.                         'connect_timeout' => 3,
  314.                         'timeout' => 3,
  315.                     ]
  316.                 );
  317.                 if ($response->getStatusCode() == 200) {
  318.                     try {
  319.                         $data json_decode($response->getBody()->getContents());
  320.                         if (empty($data->error)) {
  321.                             $metadata $data->result->metadata ?? null;
  322.                             $qb $connection->createQueryBuilder();
  323.                             $qb->insert(self::TIME_PRESSURE_TABLE_NAME)
  324.                                 ->values([
  325.                                     'product_external_id' => $productExternalId,
  326.                                     'consumer_external_id' => $customerExternalId,
  327.                                     'lastUpdated' => Carbon::now()->getTimestamp(),
  328.                                     'validityDate' => $validityDate->getTimestamp(),
  329.                                     'score' => $metadata $metadata->timePressureScore 0,
  330.                                     'textScore' => $metadata $metadata->textPressureScore 0,
  331.                                     'timePressureTTL' => $metadata $metadata->timePressureTTL Carbon::now()->addMinutes(5)->getTimestamp(),
  332.                                 ])
  333.                                 ->execute();
  334.                             $this->logger->info('SmartPricer time-pressure request debug ID', ['ID' => $debugID]);
  335.                             $scores = [
  336.                                 'score' => $metadata $metadata->timePressureScore 0,
  337.                                 'textScore' => $metadata $metadata->textPressureScore 0,
  338.                             ];
  339.                         } else {
  340.                             $this->logger->error('SmartPricer response-error.', ['error' => $data->error]);
  341.                         }
  342.                     } catch (\Exception $e) {
  343.                         $this->logger->error('SmartPricer database error.', ['exception' => $e->getMessage()]);
  344.                     }
  345.                 }
  346.             } catch (GuzzleException $e) {
  347.                 $this->logger->error('Guzzle-Error while requesting time-pressure.', ['exception' => $e->getMessage()]);
  348.             }
  349.         }
  350.         return $scores;
  351.     }
  352.     public function getDemandPressureBulkQuery(): void
  353.     {
  354.         try {
  355.             /** @var Pimcore\Model\DataObject\SiteConfig $config */
  356.             $config $this->config;
  357.             if ($startDate $config->getDemandPressureSeasonStart()) {
  358.                 $startDate $startDate->lessThan(Carbon::today()) ? Carbon::today() : $startDate;
  359.             } else {
  360.                 $startDate Carbon::today();
  361.             }
  362.             $response $this->client->request(
  363.                 'POST',
  364.                 $this->apiDomain '/steeringservice/v1/steering/' $this->customer '/steer-demand-pressure',
  365.                 [
  366.                     'headers' => [
  367.                         'Authorization' => 'Bearer ' $this->customer ':' $this->demandPressureAPIToken,
  368.                     ],
  369.                     'json' => [
  370.                         'season' => $this->demandPressureSeason,
  371.                         'startDate' => $startDate->startOfDay()->getTimestamp(),
  372.                         'endDate' => $config->getDemandPressureSeasonEnd()?->startOfDay()->getTimestamp(),
  373.                         'intervalMinutes' => 1440,
  374.                         'ignoreErrors' => true,
  375.                         'priority' => 0,
  376.                     ],
  377.                     'connect_timeout' => 3,
  378.                     'timeout' => 3,
  379.                 ]
  380.             );
  381.             if ($response->getStatusCode() == 200) {
  382.                 try {
  383.                     $data json_decode($response->getBody()->getContents());
  384.                     $connection $this->getConnection();
  385.                     foreach ($data->ranges as $item) {
  386.                         /** @phpstan-ignore-next-line  */
  387.                         $connection->query('INSERT INTO bundle_smartpricer_demand_pressure(pointInTime, score) VALUES(?, ?) ON DUPLICATE KEY UPDATE score = ?',
  388.                             [
  389.                                 $item->pointInTime,
  390.                                 $item->result->score,
  391.                                 $item->result->score,
  392.                             ])
  393.                             ->execute();
  394.                     }
  395.                 } catch (\Exception $e) {
  396.                     $this->logger->error('SmartPricer database error.', ['exception' => $e->getMessage()]);
  397.                 }
  398.             }
  399.         } catch (GuzzleException $e) {
  400.             $this->logger->error('Guzzle-Error while requesting demand-pressure.', ['exception' => $e->getMessage()]);
  401.         }
  402.     }
  403.     public function getPressureText(float $pressure): string
  404.     {
  405.         /** @var Pimcore\Model\DataObject\SiteConfig $config */
  406.         $config $this->config;
  407.         $lowTill $config->getLowTill() ?? 0.143;
  408.         $ratherLowTill $config->getRatherLowTill() ?? 0.285;
  409.         $normalTill $config->getNormalTill() ?? 0.43;
  410.         $ratherHighTill $config->getRatherHighTill() ?? 0.715;
  411.         $pressureText 'low';
  412.         switch ($pressure) {
  413.             case $pressure >= $lowTill && $pressure $ratherLowTill:
  414.                 $pressureText 'rather-low';
  415.                 break;
  416.             case $pressure >= $ratherLowTill && $pressure $normalTill:
  417.                 $pressureText 'normal';
  418.                 break;
  419.             case $pressure >= $normalTill && $pressure $ratherHighTill:
  420.                 $pressureText 'rather-high';
  421.                 break;
  422.             case $pressure >= $ratherHighTill:
  423.                 $pressureText 'high';
  424.                 break;
  425.         }
  426.         return $pressureText;
  427.     }
  428.     public function getPriceAvailabilityText(float $pressure): string
  429.     {
  430.         /** @var Pimcore\Model\DataObject\SiteConfig $config */
  431.         $config $this->config;
  432.         $fewTicketsTill $config->getFewTicketsTill() ?? 0.4;
  433.         $someTicketsTill $config->getSomeTicketsTill() ?? 0.715;
  434.         $pressureText 'many-tickets-available';
  435.         switch ($pressure) {
  436.             case $pressure $fewTicketsTill:
  437.                 $pressureText 'few-tickets-available';
  438.                 break;
  439.             case  $pressure >= $fewTicketsTill && $pressure $someTicketsTill:
  440.                 $pressureText 'some-tickets-available';
  441.                 break;
  442.         }
  443.         return $pressureText;
  444.     }
  445.     /**
  446.      * @param Carbon $startDate
  447.      * @param Carbon $endDate
  448.      *
  449.      * @return array<mixed>
  450.      */
  451.     public function getDemandPressureEntries(Carbon $startDateCarbon $endDate): array
  452.     {
  453.         $db $this->getConnection();
  454.         $queryBuilder $db->createQueryBuilder();
  455.         $pressureEntries $queryBuilder
  456.             ->select('score''pointInTime')
  457.             ->from(self::DEMAND_PRESSURE_TABLE_NAME)
  458.             ->where('pointInTime BETWEEN :startTimestamp AND :endTimestamp')
  459.             ->setParameters([
  460.                 'startTimestamp' => $startDate->getTimestamp(),
  461.                 'endTimestamp' => $endDate->getTimestamp(),
  462.             ])
  463.             ->execute()
  464.             ->fetchAllAssociative();
  465.         return $pressureEntries;
  466.     }
  467.     /**
  468.      * For debugging purposes
  469.      *
  470.      * @param string|null $data
  471.      *
  472.      * @return string
  473.      *
  474.      * @throws \Exception
  475.      */
  476.     private function guidv4(string $data null): string
  477.     {
  478.         // Generate 16 bytes (128 bits) of random data or use the data passed into the function.
  479.         $data $data ?? random_bytes(16);
  480.         assert(strlen($data) == 16);
  481.         // Set version to 0100
  482.         $data[6] = chr(ord($data[6]) & 0x0f 0x40);
  483.         // Set bits 6-7 to 10
  484.         $data[8] = chr(ord($data[8]) & 0x3f 0x80);
  485.         // Output the 36 character UUID.
  486.         return vsprintf('%s%s-%s-%s-%s-%s%s%s'str_split(bin2hex($data), 4));
  487.     }
  488. }