334 lines
12 KiB
PHP
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 ?? [];
|
|
}
|
|
}
|