Files
kupshop/bundles/KupShop/SellerBundle/Utils/SellerUtil.php
2025-08-02 16:30:27 +02:00

334 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace KupShop\SellerBundle\Utils;
use KupShop\ComponentsBundle\Entity\Thumbnail;
use KupShop\KupShopBundle\Util\StringUtil;
use KupShop\OrderingBundle\Entity\Purchase\PurchaseState;
use KupShop\SellerBundle\Translations\SellersTranslation;
use Query\Operator;
use Query\QueryBuilder;
use Query\Translation;
class SellerUtil
{
public const AVAILABILITY_IN_STORE = 2;
public const AVAILABILITY_PARTIALLY_IN_STORE = 1;
public const AVAILABILITY_NOT_IN_STORE = 0;
protected SellerMultiFetch $multiFetch;
private ?array $sellers = null;
public function __construct(SellerMultiFetch $multiFetch)
{
$this->multiFetch = $multiFetch;
}
public function getSeller(int $sellerId): ?array
{
if (!($seller = $this->getSellers()[$sellerId] ?? false)) {
return null;
}
if (findModule(\Modules::COMPONENTS) && !findModule(\Modules::SELLERS, \Modules::SUB_SMARTY_SELLERS) && isset($seller['photos'])) {
$seller['thumbnails'] = array_map(function ($photo) {
return new Thumbnail((string) $photo['id'], $photo['description'], ''.$photo['dateUpdated']->getTimestamp());
}, $seller['photos']);
}
return $seller;
}
public function getSellers(bool $force = false): array
{
if ($this->sellers && $force === false) {
return $this->sellers;
}
$qb = $this->getBaseQueryBuilder();
$sellers = [];
foreach ($qb->execute() as $item) {
$sellers[$item['id']] = $this->prepareSeller($item);
}
$this->multiFetchSellersData($sellers);
return $this->sellers = $sellers;
}
public function isSellerOrderingAllowed(array $seller): bool
{
return ($seller['data']['ordering_disabled'] ?? 'N') === 'N';
}
public function sellersGroupBy(array $sellers, string $fieldPath): array
{
$result = [];
foreach ($sellers as $seller) {
$path = explode('/', $fieldPath);
$groupByField = $seller[array_shift($path)] ?? null;
foreach ($path as $field) {
if (empty($groupByField[$field])) {
$groupByField = null;
}
$groupByField = &$groupByField[$field];
}
$result[$groupByField ?: ''][$seller['id']] = $seller;
}
// seradim abecedne
uksort($result, fn ($a, $b) => StringUtil::slugify($a) <=> StringUtil::slugify($b));
return $result;
}
// Find closest seller by latitude and longitude
public function getClosestSeller(float $latitude, float $longitude): ?array
{
$seller = $this->getBaseQueryBuilder()
->addSelect('(
6371 *
acos(cos(radians(:latitude)) *
cos(radians(X(position))) *
cos(radians(Y(position)) -
radians(:longitude)) +
sin(radians(:latitude)) *
sin(radians(X(position))))
) AS distance')
->andWhere('position IS NOT NULL AND position != ""')
->addParameters(
[
'latitude' => $latitude,
'longitude' => $longitude,
]
)
->orderBy('distance', 'ASC')
->setMaxResults(1)
->execute()->fetch();
if (!$seller) {
return null;
}
return $this->prepareSeller($seller);
}
public function getSellersOrderedByClosestToPosition(float $latitude, float $longitude): ?array
{
$sellers = $this->getSellers();
foreach ($sellers as &$seller) {
if ($seller['x'] && $seller['y']) {
$theta = $longitude - $seller['y'];
$dist = sin(deg2rad($latitude)) * sin(deg2rad($seller['x'])) + cos(deg2rad($latitude)) * cos(deg2rad($seller['x'])) * cos(deg2rad($theta));
$dist = acos($dist);
$dist = rad2deg($dist);
$miles = $dist * 60 * 1.1515;
// vzdálenost v KM
$seller['distance'] = $miles * 1.609344;
} else {
$seller['distance'] = 0;
}
}
usort($sellers, fn ($a, $b) => $a['distance'] - $b['distance']);
return $sellers;
}
public function prepareSeller(array $seller): array
{
$seller['id'] = (int) $seller['id'];
$seller['blocks'] = [];
$seller['data'] = array_replace_recursive(
json_decode($seller['data'] ?: '', true) ?: [],
json_decode($seller['data_translation'] ?? '', true) ?: [],
);
$seller['is_ordering_disabled'] = !$this->isSellerOrderingAllowed($seller);
$seller['opening_hours'] = $this->getOpeningHours($seller, new \DateTime());
$seller['flags'] = array_filter(explodeFlags($seller['flags'] ?: ''), fn ($k) => !empty($k), ARRAY_FILTER_USE_KEY);
return $seller;
}
/** Vratí informaci, zda je prodejna už zavřená */
public function isSellerClosed(array $seller, int $hourReserve = 0, ?\DateTime $date = null): bool
{
// pripravim si datum
$date = $date ?: new \DateTime();
// zkontroluju, zda je vyplnena hodina zavreni pro konkretni den
if (!($closeTime = $this->getClosingTime($seller, $date, 1))) {
return true;
}
$closeTimeParts = explode(':', $closeTime);
$hour = (int) $closeTimeParts[0];
$minute = (int) ($closeTimeParts[1] ?? 0);
$closeDate = (clone $date)->setTime($hour - $hourReserve, $minute);
// zkontroluju, zda neni uz zavreno
if ($date >= $closeDate) {
return true;
}
return false;
}
/**
* Vrátí nejbližší datum, kdy je prodejna otevřená
*
* $returnNullOnNoOpenDate - pokud se mi v celem tydnu nepodari najit zadny datum, tak vrati `null` - znamena to, ze
* u ty prodejny pravdepodobne neni nastavena oteviraci doba
*/
public function getClosestOpenDate(array $seller, int $hourReserve = 0, bool $returnNullOnNoOpenDate = false): ?\DateTime
{
$loop = 0;
$date = new \DateTime();
$found = false;
do {
if ($isClosed = $this->isSellerClosed($seller, $hourReserve, $date)) {
$date->add(new \DateInterval('P1D'));
$date->setTime(0, 0);
}
if (!$isClosed) {
$found = true;
}
$loop++;
} while ($isClosed && $loop <= 7);
if ($returnNullOnNoOpenDate && !$found) {
return null;
}
return $date;
}
public function loadSellersDeliveryInfoByPurchaseState(PurchaseState $purchaseState, array &$sellers): void
{
$productPieces = [];
$products = [];
foreach ($purchaseState->getProducts() as $product) {
$products[$product->getIdProduct()] = $products[$product->getIdProduct()] ?? null;
if ($product->getIdVariation()) {
$products[$product->getIdProduct()][] = $product->getIdVariation();
}
$key = $product->getIdProduct().($product->getIdVariation() ? '/'.$product->getIdVariation() : '');
$productPieces[$key] = ($productPieces[$key] ?? 0) + $product->getPieces();
}
$this->multiFetch->fetchSellersInStore($sellers, $products);
$this->loadSellersDeliveryDate($sellers, $productPieces);
}
public function loadSellersDeliveryDate(array &$sellers, array $products = []): void
{
// pokus o nejaky obecny nacteni datumu doruceni pro prodejny
foreach ($sellers as &$seller) {
$deliveryDate = new \DateTime();
$availability = self::AVAILABILITY_IN_STORE;
$deliveryDateIncrement = 0;
foreach ($seller['products'] ?? [] as $key => $item) {
$pieces = $products[$key] ?? 1;
// pokud nejaky produkt neni v dostatecnem mnozstvi na prodejne, ale je skladem jinde
if ($item['in_store'] < $pieces) {
$missingPieces = $pieces - $item['in_store'];
// pokud je v dostatecnem mnozstvi skladem jinde
if (($item['in_store_main'] + $item['in_store_other']) >= $missingPieces) {
// mam dostatek kusu skladem, ale nejsou na aktualni prodejne, takze je budu muset zavest
$deliveryDateIncrement = 2;
$availability = min($availability, self::AVAILABILITY_PARTIALLY_IN_STORE);
} else {
// nemam dostatek kusu skladem
$availability = self::AVAILABILITY_NOT_IN_STORE;
}
}
}
// pokud je datum vyzvednuti dnes, ale prodejna za chvili zavira, tak bych mel dat datum az na dalsi den
if (!$deliveryDateIncrement) {
if ($this->isSellerClosed($seller, 1)) {
$deliveryDate = new \DateTime('tomorrow');
}
}
$seller['deliveryDate'] = $deliveryDate->add(new \DateInterval('P'.$deliveryDateIncrement.'D'));
$seller['availability'] = $availability;
}
}
/** Default fetches for sellers */
protected function multiFetchSellersData(array &$sellers): void
{
$this->multiFetch->fetchSellersBlocks($sellers);
$this->multiFetch->fetchSellersPhotos($sellers);
}
protected function getBaseQueryBuilder(): QueryBuilder
{
return sqlQueryBuilder()
->select('se.*, X(se.position) as x, Y(se.position) as y')
->from('sellers', 'se')
->andWhere(Operator::equals(['se.figure' => 'Y']))
->andWhere(Translation::coalesceTranslatedFields(
translationClass: SellersTranslation::class,
columns: function ($columns) {
$columns['data'] = 'data_translation';
return $columns;
}));
}
/**
* Fetches the specific times (opening hours, closing hours, break start, break end) of a seller for a particular date.
*
* @param int $valueIndex represents the type of time interval to retrieve (0 for opening time, 1 for closing time, 2 for break starting time, 3 for break ending time)
*/
public function getClosingTime(array $seller, \DateTime $date, int $valueIndex): ?string
{
$openingHours = $this->getOpeningHours($seller, $date);
return $openingHours[$date->format('N')][$valueIndex] ?? null;
}
/**
* Gets the opening hours of the seller, evaluates extra opening hours for a particular date.
*/
public function getOpeningHours(array $seller, \DateTime $date): array
{
if (($seller['data']['extra_opening_hours']['toggle'] ?? 'N') === 'N') {
return $seller['data']['opening_hours'] ?? [];
}
$startDateString = empty($seller['data']['extra_opening_hours']['date_from']) ? '01.01.1970 00:00:00' : $seller['data']['extra_opening_hours']['date_from'];
$endDateString = empty($seller['data']['extra_opening_hours']['date_to']) ? '01.01.2070 00:00:00' : $seller['data']['extra_opening_hours']['date_to'];
$startDate = new \DateTime($startDateString);
$endDate = new \DateTime($endDateString);
if ($date >= $startDate && $date <= $endDate) {
$openingHours = $seller['data']['extra_opening_hours'];
} else {
$openingHours = $seller['data']['opening_hours'];
}
return $openingHours ?? [];
}
}