<?php 
 
namespace Slivki\Repository; 
 
use DateTimeImmutable; 
use Doctrine\ORM\AbstractQuery; 
use Doctrine\ORM\EntityRepository; 
use Doctrine\ORM\Query; 
use Doctrine\ORM\QueryBuilder; 
use League\Period\Period; 
use Slivki\Dao\Statistic\PurchasesByOfferDao; 
use Slivki\Entity\Banner; 
use Slivki\Entity\CacheReloadScheduler; 
use Slivki\Entity\Category; 
use Slivki\Entity\City; 
use Slivki\Entity\Director; 
use Slivki\Entity\EntityDescription; 
use Slivki\Entity\GeoLocation; 
use Slivki\Entity\Media; 
use Slivki\Entity\Offer; 
use Slivki\Entity\OfferExtension; 
use Slivki\Entity\OfferPayedCategory; 
use Slivki\Entity\PurchaseCount; 
use Slivki\Entity\OfferOrder; 
use Slivki\Entity\Seo; 
use Slivki\Entity\SiteSettings; 
use Slivki\Entity\UserGroup; 
use Slivki\Entity\Visit; 
use Slivki\Exception\OfferNotFoundException; 
use Slivki\Services\ImageService; 
use Slivki\Util\Logger; 
use Slivki\Util\SoftCache; 
use Slivki\Entity\User; 
use Slivki\Entity\EntityOption; 
use Symfony\Component\Config\Definition\Exception\Exception; 
 
class OfferRepository extends EntityRepository { 
    const CACHE_NAME = "offers"; 
    const CACHE_NAME_TEASERS = 'offers-teasers'; 
    const CACHE_NAME_TEASERS_MOBILE = 'teasers-mobile'; 
    const LOCKED_KEY = "lock"; 
    const SORTED_OFFERS_CACHE_KEY = "sorted-offers-list-"; 
    const SORTED_OFFERS_CACHE_NAME = "sortedOffersByPosition"; 
    const SORT_MODE_DEFAULT = 'po-promokodam'; 
    const SORT_MODE_BY_VISIT = 'po-prosmotram'; 
    const SORT_MODE_BY_RATING = 'po-ocenkam'; 
    const COMPANIES_RATING_DATA_CACHE_KEY = 'company-rating-data-'; 
    const OFFER_POSITIONS_CACHE_KEY = 'positions'; 
    const OFFER_LOCATIONS_CACHE_KEY = 'locations'; 
    const FILTER_DATA_CACHE_KEY = 'filterData'; 
    const NEW_SUBCATEGORY_CACHE_KEY = '-new'; 
    const RESIZED_IMAGE_PREFIX = '_resized_'; 
    const CACHE_NAME_GEO_LOCATION_DATA = 'offer-geolocation-data'; 
    const OFFER_CITIES_CACHE_NAME = 'offer-cities'; 
 
    private static $purchaseCountListByCategory; 
    private static $purchaseCountList; 
    private static $allOffersList; 
    private static $offerRatingList; 
    private static $offersOnlyPayedMonthlyPurchaseCount; 
 
    /** 
     * @throws OfferNotFoundException 
     */ 
    public function getById(int $offerId): Offer 
    { 
        $queryBuilder = $this->createQueryBuilder('offer'); 
        $expr = $queryBuilder->expr(); 
 
        $offer = $queryBuilder 
            ->andWhere($expr->eq('offer.ID', ':offerId')) 
            ->setParameter('offerId', $offerId) 
            ->getQuery() 
            ->getOneOrNullResult(); 
 
        if (!$offer instanceof Offer) { 
            throw OfferNotFoundException::missingId($offerId); 
        } 
 
        return $offer; 
    } 
 
    public function getActiveOffersByCategoryID($categoryID) { 
        ini_set('memory_limit', '4g'); 
        $softCache = new SoftCache(self::CACHE_NAME); 
        $category = $this->getEntityManager()->getRepository(Category::class)->findCached($categoryID); 
        $offerList = []; 
        if(isset($category['entityList'])) { 
            $offerList = $softCache->getMulti($category['entityList']); 
        } 
        if (!$offerList) { 
            return []; 
        } 
        $offerListCached = []; 
        foreach ($offerList as $key => $offer) { 
            if ($offer) { 
                $offerListCached[$key] = $offer; 
            } 
        } 
 
        return $offerListCached; 
    } 
 
    public function getAllActiveOffersCached() { 
        $entityList = []; 
        $categoryList = $this->getEntityManager()->getRepository(Category::class)->getAllActiveCategoriesFromCache(); 
        foreach ($categoryList as $category) { 
            if ($category['category']->getDomainObjectID() == Category::OFFER_CATEGORY_ID) { 
                if (isset($category['entityList']) && is_array($category['entityList'])) { 
                    $entityList = array_merge($entityList, $category['entityList']); 
                } else { 
                    Logger::instance('getAllActiveOffersCached')->info('Category ' . $category['category']->getID() . ' entityList not array'); 
                } 
            } 
        } 
        $entityList = array_unique($entityList); 
        $softCache = new SoftCache(self::CACHE_NAME); 
        $offerList = $softCache->getMulti($entityList); 
 
        return $offerList; 
    } 
 
    public function setAllOffersList() { 
        self::$allOffersList = $this->getAllActiveOffersNoCache(); 
    } 
 
    public function getAllActiveOffersNoCache(){ 
        $dql = "select offer from Slivki:Offer offer 
                where offer.active = true 
                and offer.activeSince < CURRENT_TIMESTAMP()  
                and offer.activeTill > CURRENT_TIMESTAMP() 
                order by offer.active desc"; 
        $offers = $this->getEntityManager()->createQuery($dql)->getResult(); 
        return $offers; 
    } 
 
    public function getActiveOffersByCategoryIDNoCache($categoryID) { 
        $dql = "select offer from Slivki:Offer offer 
            join offer.categories category 
            where category.ID = :categoryID 
              and offer.active = true 
              and offer.activeSince < CURRENT_TIMESTAMP() 
              and offer.activeTill > CURRENT_TIMESTAMP() 
            order by offer.active desc"; 
        $offerList = $this->getEntityManager()->createQuery($dql)->setParameter("categoryID", $categoryID)->getResult(); 
        return $offerList; 
    } 
 
    public function getAdvOfferList() { 
        return $this->getAllActiveOffersNoCache(); 
    } 
 
    public function getAllOffersByCategoryID($categoryID) { 
        $dql = "select offer from Slivki:Offer offer join offer.categories category where category.ID = :categoryID order by offer.active desc"; 
        $offerList = $this->getEntityManager()->createQuery($dql)->setParameter("categoryID", $categoryID)->getResult(); 
        return $offerList; 
    } 
 
    public function getCountActiveOffersByCategory(Category $category) { 
        $offerList = $this->getActiveOffersByCategoryID($category->getID()); 
        if (!$offerList) { 
            return 0; 
        } 
        return count($offerList); 
    } 
 
    /** 
     * @return boolean 
     */ 
    public function isOfferFreeForUser(Offer $offer, User $user = null) { 
        if (!$offer->isInFreeCodesCategory()) { 
            return false; 
        } 
        if ($offer->isFree() || !$user || $user->getID() == User::FAKE_USER_ID) { 
            return true; 
        } 
        $orderStatus = OfferOrder::STATUS_CONFIRM_FREE_IMPLICITLY; 
        $dql = "select count(orderDetails) as amount from Slivki:OfferOrder offerOrder join offerOrder.offerOrderDetails orderDetails 
        where offerOrder.userID = :userID and offerOrder.offerID = :offerID and offerOrder.status = :status and offerOrder.createdOn between :from and :to"; 
        $query = $this->getEntityManager()->createQuery($dql); 
        $query->setParameter('userID', $user->getID()); 
        $query->setParameter('offerID', $offer->getID()); 
        $query->setParameter('status', $orderStatus); 
        $query->setParameter("from", date_format(new \DateTime(), 'Y-m-d 00:00:00')); 
        $query->setParameter("to", date_format(new \DateTime(), 'Y-m-d 23:59:59')); 
        $result = $query->getOneOrNullResult(Query::HYDRATE_SCALAR); 
        if ($offer->getID() == Offer::PETROL_OFFER_ID) { 
            return $result['amount'] < 2; 
        } else { 
            return $result['amount'] == 0; 
        } 
    } 
 
    /** 
     * @return \Slivki\Entity\Offer 
     * @deprecated 
     */ 
    public function findCached($offerID) { 
        $softCache = new SoftCache(self::CACHE_NAME); 
 
        return $softCache->get($offerID); 
    } 
 
    /** 
     * @return \Slivki\Entity\Offer|null 
     * @deprecated 
     */ 
    public function getAnyWay($offerID) { 
        $offer = $this->findCached($offerID); 
        if (!$offer) { 
            $offer = $this->find($offerID); 
        } 
 
        return $offer; 
    } 
 
    public function getUsedCodesCount($offerID) { 
        $dql = "select count(details) from  Slivki:OfferOrderDetails details where details.offerOrderID=:offerID"; 
        $query = $this->getEntityManager()->createQuery($dql)->setParameter("offerID", $offerID); 
        $usedCodesCount = $query->getSingleScalarResult(); 
        return $usedCodesCount; 
    } 
 
    public function getOfferMonthlyPurchaseCount($offerID, $days = 30) { 
        if ($days == 30) { 
            $sql = "select purchase_count_last_month_with_correction from purchase_count where entity_id = " . $offerID; 
            return $this->getEntityManager()->getConnection()->executeQuery($sql)->fetchColumn(); 
        } 
        $sql = "select count(*) from offer_code inner join offer_order on offer_code.offer_order_id = offer_order.id 
            where offer_order.offer_id = $offerID and offer_code.created_on > (now() + '- $days days')"; 
        return $this->getEntityManager()->getConnection()->executeQuery($sql)->fetchColumn(); 
    } 
 
    public function getOfferList($ids) { 
        $softCache = new SoftCache(self::CACHE_NAME); 
        $offerList =  $softCache->getMulti($ids); 
        if (!$offerList) { 
            return []; 
        } 
 
        return $offerList; 
    } 
 
    public function getCodeCost(Offer $offer = null, $codesCount = null) 
    { 
        if ($offer) { 
            $offerId = $offer->getID(); 
 
            if ($offerId === 283793) { 
                return 0; 
            } 
        } 
 
        $siteSettingsRepository = $this->getEntityManager()->getRepository(SiteSettings::class); 
        if (!$offer) { 
            return $siteSettingsRepository->getSiteSettingsCached()->getCodeCost(); 
        } 
 
        if ($offer->getCodeCostByCount()) { 
            $codeCost = null; 
            foreach ($offer->getCodeCostByCount() as $count => $cost) { 
                if ($count > $codesCount) { 
                    break; 
                } 
                $codeCost = $cost; 
            } 
 
            return $codeCost; 
        } 
 
        return $offer->getCodeCost() ?? $siteSettingsRepository->getSiteSettingsCached()->getCodeCost(); 
    } 
 
    public function getOfferExtensionCodeCost(OfferExtension $extension) { 
        if ($extension->getCodePrice() != null) { 
            return $extension->getCodePrice(); 
        } 
        return $this->getCodeCost($extension->getOffer()); 
    } 
 
    /** @deprecated  */ 
    public function putOfferToCache(Offer $offer) { 
        $softCache = new SoftCache(self::CACHE_NAME); 
        $cacheKey = $offer->getID() . self::LOCKED_KEY; 
        while (!$softCache->add($cacheKey, self::LOCKED_KEY, 60)); 
        $softCache->set($offer->getID(), $offer, 0); 
        $softCache->set($cacheKey, null, 1); 
    } 
 
    public function reloadCache() { 
        $dql = "select offer, descriptions, offerCodePools, geoLocations, category from Slivki:Offer offer index by offer.ID inner join offer.categories category 
          left join offer.descriptions as descriptions left join offer.offerCodePools offerCodePools left join offer.geoLocations as geoLocations   
          where category.active = true and offer.active = true and CURRENT_TIMESTAMP() between offer.activeSince and offer.activeTill 
          and offer.hidden = false"; 
        $offerList = $query = $this->getEntityManager()->createQuery($dql)->getResult(); 
        $offerListByCategory = []; 
        $tagList = []; 
        $offerListCached = []; 
        $cityList = []; 
        $mediaRepository = $this->getEntityManager()->getRepository(Media::class); 
        foreach ($offerList as $offer) { 
            $this->loadOfferData($offer, $mediaRepository, $tagList, $offerListByCategory); 
            $offerListCached[$offer->getID()] = $offer; 
        } 
        $categoryListCached = []; 
        $categoryList = $this->getEntityManager()->getRepository(Category::class)->getActiveCategoriesNotCached(Category::OFFER_CATEGORY_ID); 
        foreach ($categoryList as $category) { 
            if (!isset($offerListByCategory[$category->getID()])) { 
                continue; 
            } 
            $offerList = $offerListByCategory[$category->getID()]; 
            if (count($offerList) == 0) { 
                continue; 
            } 
            $category->getParentCategories()->toArray(); 
            $category->setEntityCount(count($offerList)); 
            $category->setHotFeedIconMedia($mediaRepository->getСategoryHotFeedIconMedia($category->getID())); 
            $category->getCity()->getID(); 
            $city = $category->getCity(); 
            if (!isset($cityList[$city->getID()])) { 
                $cityList[$city->getID()] = $city; 
            } 
            $categoryListCached[$category->getID()] = ['category' => $category, 'entityList' => $offerList]; 
        } 
        $this->getEntityManager()->clear(); 
        $softCache = new SoftCache(self::CACHE_NAME); 
        $softCache->setMulti($offerListCached, 0); 
        $softCache = new SoftCache(CategoryRepository::CACHE_NAME); 
        $softCache->set(CategoryRepository::ALL_CATEGORIES_CACHE_KEY, $categoryListCached, 0); 
        $softCache->setMulti($categoryListCached, 0); 
        $softCache = new SoftCache(CityRepository::CACHE_NAME); 
        $softCache->set(CityRepository::CACHE_KEY_ACTIVE, $cityList, 0); 
    } 
 
    /** @deprecated  */ 
    public function reloadCacheForOneOffer($offerID, $source = '') { 
        $offerOrderRepository = $this->getEntityManager()->getRepository(OfferOrder::class); 
        $logger = Logger::instance('reloadCacheForOneOffer'); 
        try { 
            $logger->info('Lock code pool for offer ' . $offerID); 
            $offerOrderRepository->lockCodePool($offerID); 
            /** @var \Slivki\Entity\Offer $offer */ 
            $offer = $this->find($offerID); 
            if (!$offer->isActive() || !$offer->isInActivePeriod() || $offer->isHidden()) { 
                $this->removeOfferFromCache($offerID); 
                $logger->info('Unlock code pool for offer ' . $offerID . '. Offer is not active'); 
                $offerOrderRepository->unlockCodePool($offerID); 
                return; 
            } 
            if ($offer->getDescriptionByType(EntityDescription::TYPE_OFFER_CONDITIONS_ID) == '') { 
                Logger::instance('DESCRIPTION-DEBUG')->info("$offerID empty description. $source"); 
            } 
            $mediaRepository = $this->getEntityManager()->getRepository(Media::class); 
            $tagList = []; 
            $offerListByCategory = []; 
            $this->loadOfferData($offer, $mediaRepository, $tagList, $offerListByCategory); 
            foreach ($offer->getGeoLocations() as $geoLocation) { 
                $geoLocation->getPhoneNumbers()->toArray(); 
            } 
            $softCache = new SoftCache(self::CACHE_NAME); 
            $softCache->set($offerID, $offer, 0); 
            $logger->info('Unlock code pool for offer ' . $offerID); 
            $offerOrderRepository->unlockCodePool($offerID); 
            $categoryList = $this->getEntityManager()->getRepository(Category::class)->getAllActiveCategoriesFromCache(); 
            $categoryListChanged = false; 
            $categoryRepository = $this->getEntityManager()->getRepository(Category::class); 
            $cityList = $this->getEntityManager()->getRepository(City::class)->getActiveCitiesCached(); 
            foreach ($offer->getCategories() as $category) { 
                if (!isset($categoryList[$category->getID()])) { 
                    $category->getBanners()->toArray(); 
                    $category->setEntityCount(1); 
                    $categoryForCache['category'] = $category; 
                    $categoryForCache['entityList'][] = $offerID; 
                    $categoryList[$category->getID()] = $categoryForCache; 
                    $categoryListChanged = true; 
                    //$categoryRepository->putCategoryToCache($categoryForCache); 
                } 
                $city = $category->getCity(); 
                if ($city && !isset($cityList[$city->getID()])) { 
                    $cityList[$city->getID()] = $city; 
                } 
            } 
            foreach ($offerListByCategory as $categoryID => $category) { 
                if (!isset($categoryList[$categoryID]['entityList'])) { 
                    $categoryList[$categoryID]['entityList'] = []; 
                } 
                if (!in_array($offerID, $categoryList[$categoryID]['entityList'])) { 
                    $categoryList[$categoryID]['entityList'][] = $offerID; 
                    $categoryList[$categoryID]['category']->setEntityCount(count($categoryList[$categoryID]['entityList'])); 
                    $categoryListChanged = true; 
                    //$categoryRepository->putCategoryToCache($categoryList[$categoryID]); 
                } 
            } 
            foreach ($categoryList as $categoryID => $category) { 
                if (!isset($category['entityList']) || !$category['entityList']) { 
                    continue; 
                } 
                $index = array_search($offerID, $category['entityList']); 
                if (false !== $index && !isset($offerListByCategory[$categoryID])) { 
                    unset($categoryList[$categoryID]['entityList'][$index]); 
                    $categoryList[$categoryID]['category']->setEntityCount(count($categoryList[$categoryID]['entityList'])); 
                    $categoryListChanged = true; 
                    //$categoryRepository->putCategoryToCache($categoryList[$categoryID]); 
                } 
            } 
            if ($categoryListChanged) { 
                $categoryRepository->cacheAllActiveCategories($categoryList); 
            } 
 
            $softCache = new SoftCache(CityRepository::CACHE_NAME); 
            $softCache->set(CityRepository::CACHE_KEY_ACTIVE, $cityList, 0); 
        } catch (Exception $e) { 
            $logger->info('Unlock code pool for offer ' . $offerID . '. ' . $e->getMessage()); 
            $offerOrderRepository->unlockCodePool($offerID); 
        } 
        return $offer; 
    } 
 
    private function loadOfferData(Offer &$offer, MediaRepository &$mediaRepository, &$tagList, &$offerListByCategory) { 
        $offerID = $offer->getID(); 
        if ($offer->getTeaserMedia()) { 
            $offer->getTeaserMedia()->getID(); 
        } 
        $offer->setHotFeedIconMedia($mediaRepository->getOfferHotFeedIconMedia($offerID)); 
        $offer->setDetailMeidas($mediaRepository->getOfferDetailMedias($offerID)); 
        $offer->setShopMedias($mediaRepository->getOfferShopMedias($offerID)); 
        $offer->getPhoneNumbers()->toArray(); 
        $offer->getGiftCertificates()->toArray(); 
        $offer->getExtensions()->toArray(); 
        $offer->getOfferCodePools()->toArray(); 
        $offer->getGeoLocations()->toArray(); 
        $offer->getPhoneNumbers()->toArray(); 
        $offer->getExtensions()->toArray(); 
        $offer->getGiftCertificates()->toArray(); 
        $offer->getSupplierPhotoMedias()->toArray(); 
        foreach ($offer->getCategories() as $category) { 
            $offerListByCategory[$category->getID()][] = $offer->getID(); 
        } 
        if($offer->isActiveCurrencyCalculator()) { 
            $offer->getBankCurrency()->getRate(); 
        } 
        foreach ($offer->getDescriptions()->toArray() as $description) { 
            $description->getEntityDescriptionSliderImages()->toArray(); 
        } 
    } 
 
    public function removeOfferFromCache($offerID) { 
        $softCache = new SoftCache(self::CACHE_NAME); 
        $offer = $softCache->get($offerID); 
        if (!$offer) { 
            return; 
        } 
        $softCache->delete($offerID); 
        $categoryRepository = $this->getEntityManager()->getRepository(Category::class); 
        $offersByCategory = $categoryRepository->getAllActiveCategoriesFromCache(); 
        $arrayChanged = false; 
        foreach ($offersByCategory as $key => $category) { 
            if ($category['category']->getDomainObjectID() == Category::OFFER_CATEGORY_ID) { 
                if (!isset($category['entityList']) || !is_array($category['entityList'])) { 
                    continue; 
                } 
                $entityCount = count($category['entityList']); 
                $offersByCategory[$key]['entityList'] = array_diff($category['entityList'], [$offerID]); 
                if (count($offersByCategory[$key]['entityList']) != $entityCount) { 
                    $arrayChanged = true; 
                    if (count($offersByCategory[$key]['entityList']) == 0) { 
                        unset($offersByCategory[$key]); 
                    } 
                } 
            } 
        } 
        if ($arrayChanged) { 
            $categoryRepository->cacheAllActiveCategories($offersByCategory); 
        } 
    } 
 
    public function getOffersOnlyPayedMonthlyPurchaseCount($offerID) { 
        if (!self::$offersOnlyPayedMonthlyPurchaseCount) { 
            $dbConnection = $this->getEntityManager()->getConnection(); 
            $payedStatuses = [ 
                OfferOrder::STATUS_CONFIRM, 
                OfferOrder::STATUS_EXTENSION_ORDER_CONFIRM, 
                OfferOrder::STATUS_HALF_CONFIRM, 
                OfferOrder::STATUS_BALANCE_CONFIRM 
            ]; 
            $sql = "select offer_order.offer_id, count(*) as amount from offer_code inner join offer_order on offer_code.offer_order_id = offer_order.id  
                where offer_order.status in (" . join(',', $payedStatuses). ") and offer_code.created_on > date_trunc('day', now() + '-30 days')::timestamp without time zone group by 1"; 
            $offersOnlyPayedMonthlyPurchaseCount = $dbConnection->executeQuery($sql)->fetchAll(); 
            foreach ($offersOnlyPayedMonthlyPurchaseCount as $item) { 
                self::$offersOnlyPayedMonthlyPurchaseCount[$item['offer_id']] = $item['amount']; 
            } 
        } 
        return isset(self::$offersOnlyPayedMonthlyPurchaseCount[$offerID]) ? self::$offersOnlyPayedMonthlyPurchaseCount[$offerID] : 0; 
    } 
 
    private function getPurchaseByCategory($categoryID, $dateFrom, $dateTo) { 
        $sql = "select count(*) as amount from offer_order_details inner join offer_code_pool on offer_order_details.offer_code_pool_id = offer_code_pool.id   
          inner join category2entity on offer_code_pool.offer_id = category2entity.entity_id where category2entity.category_id = $categoryID    
          and offer_order_details.created_on between '$dateFrom' and '$dateTo'"; 
        $result = $this->getEntityManager()->getConnection()->executeQuery($sql)->fetch(\PDO::FETCH_COLUMN); 
 
        return (int)$result; 
    } 
 
    private function compareBannersByPosition(Banner $banner, Banner $banner1) { 
        if ($banner->getPositionRow() < $banner1->getPositionRow()) { 
            return -1; 
        } 
        if ($banner->getPositionRow() == $banner1->getPositionRow()) { 
            return $banner->getPositionColumn() > $banner1->getPositionColumn() ? 1 : -1; 
        } 
 
        return 1; 
    } 
 
    public function getSortedOffersByPosition($categoryID) { 
        $dql = "select position.entityID from Slivki:OfferCategoryPosition as position where position.categoryID = :categoryID order by position.position asc"; 
        $offersListSorted = $this->getEntityManager()->createQuery($dql)->setParameter('categoryID', $categoryID)->getResult(); 
        return $offersListSorted; 
    } 
 
    public function getTeaserBanners(Category $category, $teaserInARowCount = 3 ) { 
        $category = $this->getEntityManager()->merge($category); 
        $teaserBanners = $category->getActiveBannersByType(Banner::TYPE_TEASER_BANNER); 
        $teaserVerticalBanners = $category->getActiveBannersByType(Banner::TYPE_TEASER_VERTICAL_BANNER); 
        $teaserBanners = array_merge($teaserBanners, $teaserVerticalBanners); 
        $teaserBannersPosition = array(); 
        $withVerticalBannersRowList = array(); 
        foreach ($teaserBanners as $banner) { 
            $position = ($banner->getPositionRow() - 1) * 3 + $banner->getPositionColumn(); 
            if($banner->isActive()) { 
                $teaserBannersPosition[$position] = $banner; 
                if($banner->getTypeID() == Banner::TYPE_TEASER_VERTICAL_BANNER) { 
                    $withVerticalBannersRowList[] = (int)ceil($position / $teaserInARowCount); 
                } 
            }; 
        } 
        return array( 
            'teaserBanners' => $teaserBannersPosition, 
            'withVerticalBannersRowList' => $withVerticalBannersRowList 
        ); 
 
    } 
 
    public function getOffersByCategoryWithPositions($categoryID, $limit = null) { 
        $dql = "select offer, offerCategoryPosition.position from Slivki:Offer offer 
          left join Slivki:OfferCategoryPosition offerCategoryPosition with offerCategoryPosition.entityID = offer.ID and offerCategoryPosition.categoryID = :categoryID 
          inner join offer.categories category where category.ID = :categoryID 
          order by offerCategoryPosition.position"; 
        $query = $this->getEntityManager()->createQuery($dql); 
        $query->setParameter('categoryID', $categoryID); 
        if ($limit) { 
            $query->setMaxResults((int)$limit); 
        } 
        return $query->getResult(); 
    } 
 
    public function getActiveOffersByCategoryWithoutTags($domainObjectID, $cityID) { 
        $dql = "select offer.* from offer  
            inner join category2entity on offer.id = category2entity.entity_id  
            inner join category on category2entity.category_id = category.id  
            inner join entity_option on offer.id = entity_option.entity_id 
            where city_id = $cityID and category.domain_object_id = $domainObjectID and offer.active and entity_option.name = '".EntityOption::OPTION_OFFERS_WITHOUT_TAGS."' 
            and offer.active_till > now() 
            group by offer.id"; 
        return $this->getEntityManager()->getConnection()->executeQuery($dql)->fetchAll(Query::HYDRATE_ARRAY); 
    } 
 
    public function getActiveOffersByCategoryWithoutTagsAllCity() { 
        $dql = "select offer.* from offer  
            inner join category2entity on offer.id = category2entity.entity_id  
            inner join category on category2entity.category_id = category.id  
            inner join entity_option on offer.id = entity_option.entity_id 
            where offer.active and entity_option.name = '".EntityOption::OPTION_OFFERS_WITHOUT_TAGS."' 
            and offer.active_till > now() 
            group by offer.id"; 
        return $this->getEntityManager()->getConnection()->executeQuery($dql)->fetchAll(Query::HYDRATE_ARRAY); 
    } 
 
    public function getOffersOnReview($domainObjectID, $cityID) { 
        $dql = "select offer.* from offer  
            inner join category2entity on offer.id = category2entity.entity_id  
            inner join category on category2entity.category_id = category.id  
            where city_id = $cityID and category.domain_object_id = $domainObjectID and offer.review  
            and offer.on_review group by offer.id"; 
        return $this->getEntityManager()->getConnection()->executeQuery($dql)->fetchAll(Query::HYDRATE_ARRAY); 
    } 
 
    public function getOffersByCategorySortedByPopularity(Category $category) { 
        $categoryID = $category->getID(); 
        $offerList = $this->getActiveOffersByCategoryIDNoCache($categoryID); 
        if (!$offerList) { 
            return false; 
        } 
        if ($categoryID != Category::NEW_OFFER_CATEGORY_ID) { 
            $dql = "select purchaseCount from Slivki:PurchaseCount purchaseCount index by purchaseCount.entityID 
          inner join Slivki:Offer offer with purchaseCount.entityID = offer.ID 
          inner join offer.categories category where category.ID = :categoryID"; 
            $query = $this->getEntityManager()->createQuery($dql); 
            $query->setParameter('categoryID', $categoryID); 
            $query->execute(); 
            self::$purchaseCountListByCategory = $query->getResult(); 
            if (self::$purchaseCountListByCategory) { 
                usort($offerList, ['\Slivki\Repository\OfferRepository', 'compareOffersByPurchaseCount']); 
                self::$purchaseCountListByCategory = null; 
            } 
        } else { 
            usort($offerList, ['\Slivki\Repository\OfferRepository', 'compareOffersByRenewedOn']); 
        } 
        $payedPositions = $this->getEntityManager()->getRepository(OfferPayedCategory::class)->findBy( 
            ['categoryID' => $categoryID], ['position' => 'asc']); 
        if ($payedPositions) { 
            foreach ($payedPositions as $position) { 
                foreach ($offerList as $key => $offer) { 
                    if ($offer->getID() == $position->getEntityID()) { 
                        unset($offerList[$key]); 
                        array_splice($offerList, $position->getPosition() - 1, 0, [$offer]); 
                        break; 
                    } 
                } 
            } 
        } 
        return $offerList; 
    } 
 
    private static function compareOffersByID(Offer $offer, Offer $offer1) { 
        return $offer->getID() > $offer1->getID() ? -1 : 1; 
    } 
 
    private static function compareOffersByRenewedOn(Offer $offer, Offer $offer1) { 
        if ($offer->getRenewedOn() == $offer1->getRenewedOn()) { 
            return $offer->getID() > $offer1->getID() ? 1 : -1; 
        } 
        return $offer->getRenewedOn() > $offer1->getRenewedOn() ? -1 : 1; 
    } 
 
    private static function compareOffersByPurchaseCount(Offer $offer, Offer $offer1) { 
        /** @var \Slivki\Entity\PurchaseCount $purchaseCount */ 
        /** @var \Slivki\Entity\PurchaseCount $purchaseCount1 */ 
        $purchaseCount = isset(self::$purchaseCountListByCategory[$offer->getID()]) ? self::$purchaseCountListByCategory[$offer->getID()] : new PurchaseCount(); 
        $purchaseCount1 = isset(self::$purchaseCountListByCategory[$offer1->getID()]) ? self::$purchaseCountListByCategory[$offer1->getID()] : new PurchaseCount(); 
        $purchaseCountKoeff = $offer->getPurchaseKoeff() ?: ($offer->isInFreeCodesCategory() ? 3 : 1); 
        $purchaseCountKoeff1 = $offer1->getPurchaseKoeff() ?: ($offer->isInFreeCodesCategory() ? 3 : 1); 
        if ($purchaseCount->getPurchaseCountRecent()/$purchaseCountKoeff == $purchaseCount1->getPurchaseCountRecent()/$purchaseCountKoeff1) { 
            if ($purchaseCount->getPurchaseCountLastMonth()/$purchaseCountKoeff == $purchaseCount1->getPurchaseCountLastMonth()/$purchaseCountKoeff1) { 
                if ($purchaseCount->getPurchaseCount()/$purchaseCountKoeff == $purchaseCount1->getPurchaseCount()/$purchaseCountKoeff1) { 
                    return $offer->getID() > $offer1->getID() ? 1 : -1; 
                } 
                return $purchaseCount->getPurchaseCount()/$purchaseCountKoeff > $purchaseCount1->getPurchaseCount()/$purchaseCountKoeff1 ? -1 : 1; 
            } else { 
                return $purchaseCount->getPurchaseCountLastMonth()/$purchaseCountKoeff > $purchaseCount1->getPurchaseCountLastMonth()/$purchaseCountKoeff1 ? -1 : 1; 
            } 
        } else { 
            return $purchaseCount->getPurchaseCountRecent()/$purchaseCountKoeff > $purchaseCount1->getPurchaseCountRecent()/$purchaseCountKoeff1 ? -1 : 1; 
        } 
    } 
 
    public function findPastOfferCached($offerID) { 
        $softCache = new SoftCache(self::CACHE_NAME); 
        $cacheKey = 'pastOffer-0-' . $offerID; 
        $offer = $softCache->get($cacheKey); 
        if ($offer) { 
            return $offer; 
        } 
        $offer = $this->find($offerID); 
        if (!$offer) { 
            return null; 
        } 
        $offer->getOfferCodePools()->toArray(); 
        $offer->getGeoLocations()->toArray(); 
        $softCache->set($cacheKey, $offer, 0); 
 
        return $offer; 
    } 
 
    public function getVisitCount($offer, $today = false) { 
        if ($today) { 
            $dateFrom = new \DateTime('-1 days'); 
        } else { 
            $dateFrom = new \DateTime('-30 days'); 
        } 
        $dql = "select count(visit.ID) from Slivki:Visit visit  
          where visit.entityID = :offerID and visit.entityTypeID = :entityTypeID and visit.createdOn > :dateFrom and visit.createdOn > :offerActiveSince"; 
        return $this->getEntityManager()->createQuery($dql) 
            ->setParameter('dateFrom', $dateFrom) 
            ->setParameter('offerID', $offer->getID()) 
            ->setParameter('offerActiveSince', $offer->getActiveSince()) 
            ->setParameter('entityTypeID', Category::OFFER_CATEGORY_ID) 
            ->getSingleScalarResult(); 
    } 
 
    public function getLastPurchaseTime($offerID) { 
        $sql = "select offer_order_details.created_on from offer_order_details inner join offer_order on offer_order_details.offer_order_id = offer_order.id 
          where offer_order.offer_id = $offerID and status > 0 and offer_order_details.created_on > now() + '-12 hours' order by offer_order_details.id desc limit 1"; 
        $lastPurchaseTime = $this->getEntityManager()->getConnection()->executeQuery($sql)->fetchColumn(); 
        if ($lastPurchaseTime) { 
            $lastPurchaseTime = \DateTime::createFromFormat('Y-m-d H:i:s', $lastPurchaseTime); 
        } 
        return $lastPurchaseTime; 
    } 
 
    public function getRecentPurchaseCount($offerID) { 
        $purchaseData = $this->getEntityManager()->getRepository(PurchaseCount::class)->findOneByEntityID($offerID); 
        if (!$purchaseData) { 
            return 0; 
        } 
        return $purchaseData->getPurchaseCountLastDay(); 
    } 
 
    public function getTeaserWatermark($offerID) { 
        $offer = $this->find($offerID); 
        if (!$offer) { 
            return []; 
        } 
        $mediaRepository = $this->getEntityManager()->getRepository(Media::class); 
        $watermark = $mediaRepository->getOfferTeaserLogo($offerID); 
        if ($watermark) { 
            return [ 
                'watermark' => $watermark, 
                'width' => $offer->getTeaserLogoWidth(), 
                'height' => $offer->getTeaserLogoHeight() 
            ]; 
        } 
        $directors = $offer->getDirectors(); 
        /** @var Director $director */ 
        foreach ($directors as $director) { 
            $watermark = $mediaRepository->getDirectorLogo($director->getID()); 
            if ($watermark) { 
                return [ 
                    'watermark' => $watermark, 
                    'width' => $director->getTeaserLogoWidth(), 
                    'height' => $director->getTeaserLogoHeight() 
                ]; 
            } 
        } 
        return []; 
    } 
 
    public function scheduleOfferReload($offerID) { 
        $scheduler = new CacheReloadScheduler(); 
        $scheduler->setType(CacheReloadScheduler::TYPE_OFFER); 
        $scheduler->setEntityID($offerID); 
        $entityManager = $this->getEntityManager(); 
        $entityManager->persist($scheduler); 
        $entityManager->flush($scheduler); 
    } 
 
    public function getActiveOffersCount($cityID = City::DEFAULT_CITY_ID) { 
        $sql = "select count(distinct offer.id) from offer inner join category2entity on offer.id = category2entity.entity_id 
          inner join category on category2entity.category_id = category.id  
          where offer.active and category.city_id = " . (int)$cityID . " and now() between active_since and active_till"; 
        return $this->getEntityManager()->getConnection()->executeQuery($sql)->fetchColumn(0); 
    } 
 
    public function getActiveOffersCountCached($cityID = City::DEFAULT_CITY_ID) { 
        $softCache = new SoftCache(self::CACHE_NAME); 
        $cacheKey = 'activeCount-' . $cityID; 
        $offersCount = $softCache->get($cacheKey); 
        if (!$offersCount) { 
            $offersCount = $this->getActiveOffersCount($cityID); 
            $softCache->set($cacheKey, $offersCount, 30 * 60); 
        } 
        return $offersCount; 
    } 
 
    public function getOfferGeoLocations(Offer $offer) { 
        $seoRepository = $this->getEntityManager()->getRepository(Seo::class); 
        $result = []; 
        /** @var GeoLocation $geoLocation */ 
        foreach($offer->getGeoLocations() as $geoLocation) { 
            $geoLocationStreet = $geoLocation->getStreet() ? $geoLocation->getStreet() . ', ' : ''; 
            $geoLocationHouse = $geoLocation->getHouse() ? $geoLocation->getHouse() : ''; 
            $geoLocationDescription = $geoLocation->getDescription() ? $geoLocation->getDescription() . ' - ' : ''; 
            $geoLocationInfos['markerAnnotation'] = $geoLocationDescription . $geoLocationStreet . $geoLocationHouse; 
            $geoLocationInfos['latitude'] = $geoLocation->getLatitude(); 
            $geoLocationInfos['longitude'] = $geoLocation->getLongitude(); 
            $geoLocationInfos['offerURI'] = $seoRepository->getOfferURL($offer->getID())->getMainAlias(); 
            $result[]['geoLocationInfos'][] = $geoLocationInfos; 
        } 
 
        return $result; 
    } 
 
    public function getOfferGeoLocationData(Offer $offer, $userGeoLocation, ImageService $imageService, $jsonEncode = true, $baseURL = '') { 
        $seoRepository = $this->getEntityManager()->getRepository(Seo::class); 
        $features = []; 
        $url = ''; 
        $seo = $seoRepository->getByEntity(SeoRepository::RESOURCE_URL_OFFER_DETAILS, $offer->getID()); 
        if ($seo) { 
            $url = $seo->getMainAlias(); 
        } 
        $closestLocationID = 0; 
        $offerGeoLocations = $offer->getGeoLocations(); 
        foreach ($offerGeoLocations as $geoLocation) { 
            $caption = $geoLocation->getStreet() . ', ' . $geoLocation->getHouse(); 
            $teaserURL = ''; 
            if ($offer->getTeaserMedia()) { 
                $teaserURL = $baseURL . $imageService->getImageURL($offer->getTeaserMedia(), 0, 0); 
            } 
            $features[] = [ 
                'type' => 'Feature', 
                'id' => $geoLocation->getID(), 
                'geometry' => [ 
                    'type' => 'Point', 
                    'coordinates' => [$geoLocation->getLatitude(), $geoLocation->getLongitude()] 
                ], 
                'properties' => [ 
                    'iconClass' => 'always-caption', 
                    'iconContent' => '', 
                    'locationID' => $geoLocation->getID(), 
                    'offerID' => $offer->getID(), 
                    'offerTitle' => $offer->getTitle(), 
                    'teaserURL' => $teaserURL, 
                    'offerType' => $offer->getOfferType(), 
                    'url' => $url, 
                    'hintContent' => $caption 
                ] 
            ]; 
        } 
        $getLocationData = ['type' => 'FeatureCollection', 'features' => $features]; 
 
        if ($jsonEncode) { 
            $data = json_encode($getLocationData); 
        } else { 
            $data = $getLocationData; 
        } 
 
        return $data; 
    } 
 
    public function getOfferIDListByCategory($categoryID) { 
        $sql = "select category2entity.entity_id from category2entity inner join offer on category2entity.entity_id = offer.id  
              where category2entity.category_id = " . (int)$categoryID; 
        return $this->getEntityManager()->getConnection()->executeQuery($sql)->fetchAll(\PDO::FETCH_COLUMN); 
    } 
 
    public function getLastPurchaseDate($entityID, $userID) { 
        $sql = "select offer_order_details.created_on::date from offer_order_details inner join offer_order on offer_order_details.offer_order_id = offer_order.id 
            where offer_order.offer_id = $entityID and offer_order.user_id = $userID order by offer_order_details.id desc limit 1"; 
        $lastPurchaseDate = $this->getEntityManager()->getConnection()->executeQuery($sql)->fetchColumn(); 
        return \DateTime::createFromFormat('Y-m-d', $lastPurchaseDate); 
    } 
 
    public function getOfferCityIDList(Offer $offer) { 
        $softCache = new SoftCache(self::OFFER_CITIES_CACHE_NAME); 
        $cityIDList = $softCache->get($offer->getID()); 
        if (!$cityIDList) { 
            $cityIDList = $this->reloadOfferCityIDListCache($offer->getID()); 
        } 
        return $cityIDList; 
    } 
 
    public function reloadOfferCityIDListCache($offerID) { 
        /** @var Offer $offer */ 
        $offer = $this->find($offerID); 
        if (!$offer) { 
            return false; 
        } 
        $cityIDList = []; 
        /** @var Category $category */ 
        foreach ($offer->getCategories() as $category) { 
            $categoryID = $category->getID(); 
            if ($categoryID == Category::COUNTRY_CATEGORY_ID || $category->isChildOfRecursive(Category::COUNTRY_CATEGORY_ID) 
                || $categoryID == Category::NEW_OFFER_CATEGORY_ID || $category->isChildOfRecursive(Category::NEW_OFFER_CATEGORY_ID)) { 
                continue; 
            } 
            if ($category->getCity() && !in_array($category->getCity()->getID(), $cityIDList)) { 
                $cityIDList[] = $category->getCity()->getID(); 
            } 
        } 
        if (empty($cityIDList)) { 
            $cityIDList = [City::DEFAULT_CITY_ID]; 
        } 
        $softCache = new SoftCache(self::OFFER_CITIES_CACHE_NAME); 
        $softCache->set($offerID, $cityIDList, 0); 
        return $cityIDList; 
    } 
 
    public function getOfferCity($offerID): ?City 
    { 
        $offer = $this->find($offerID); 
        /** @var Offer $offer */ 
        $categories = $offer->getCategories(); 
 
        if (null !== $offer->getDefaultCategoryID()) { 
            $defaultCategoryId = (int) $offer->getDefaultCategoryID(); 
            foreach ($categories as $category) { 
                /** @var Category $category */ 
                if ($category->getID() === $defaultCategoryId) { 
                    return $category->getCity(); 
                } 
            } 
        } 
 
        return $offer->getCityByFirstCategory(); 
    } 
 
    public function isOfferOwner(Offer $offer, User $user) { 
        if (!$user->hasRole(UserGroup::ROLE_SUPPLIER_ID)) { 
            return false; 
        } 
        foreach ($offer->getDirectors() as $director) { 
            if ($user->getEmail() == $director->getEmail()) { 
                return true; 
            } 
        } 
        return false; 
    } 
 
    public function getExtensions(Offer $offer) { 
        $dql = 'select extension from Slivki:FoodOfferExtension extension index by extension.partnerItemID where extension.offer = :offer and extension.active = true'; 
        $query = $this->getEntityManager()->createQuery($dql); 
        $query->setParameter('offer', $offer); 
        return $query->getResult(); 
    } 
 
    public function getExtensionsByShippingType(Offer $offer, string $shippingType): array 
    { 
        $dql = ' 
                SELECT  
                    e  
                FROM Slivki:FoodOfferExtension AS e INDEX BY e.partnerItemID  
                WHERE e.offer = :offer  
                    AND e.active = true 
                    AND e.is' . ucfirst($shippingType) . ' = true 
                '; 
        $query = $this->getEntityManager()->createQuery($dql); 
        $query->setParameter('offer', $offer); 
        return $query->getResult(); 
    } 
 
    public function getExtensionVariants(OfferExtension $offerExtension) { 
        $dql = 'select extensionVariant from Slivki:OfferExtensionVariant extensionVariant index by extensionVariant.partnerID where extensionVariant.offerExtension = :offerExtension'; 
        $query = $this->getEntityManager()->createQuery($dql); 
        $query->setParameter('offerExtension', $offerExtension); 
        return $query->getResult(); 
    } 
 
    public function getDirector($offerID) { 
        $dql = 'select director from Slivki:Director director join director.offers offer where offer.ID = :offerID'; 
        $query = $this->getEntityManager()->createQuery($dql); 
        $query->setParameter('offerID', $offerID); 
        $result = $query->execute(); 
        if ($result) { 
            return $result[0]; 
        } 
 
        return false; 
    } 
 
    public function getOfferConversion($offerID) { 
        $entityManager = $this->getEntityManager(); 
        $visitRepository = $entityManager->getRepository(Visit::class); 
        $purchaseCountRepository = $entityManager->getRepository(PurchaseCount::class); 
 
        $visitCount = $visitRepository->getVisitCount($offerID, Visit::TYPE_OFFER, 30); 
        $purchaseCount = $purchaseCountRepository->findOneBy(['entityID' => $offerID]); 
 
        if (!$visitCount || !$purchaseCount || !$purchaseCount->getPurchaseCountLastMonth()) { 
            return 0; 
        } 
 
        return ceil(100 * ($purchaseCount->getPurchaseCountLastMonth() / $visitCount)); 
    } 
 
    public function getCityID(int $offerID) : int { 
        return $this->getEntityManager()->getConnection()->executeQuery( 
            "select coalesce(max(city_id), " . City::DEFAULT_CITY_ID . ") from category" 
            . " inner join category2entity on category.id = category2entity.category_id" 
            . " where category2entity.entity_id = " . $offerID 
        )->fetchColumn(); 
    } 
 
    private function getHasPurchaseOffersByCityIdQuery( 
        int $cityId, 
        Period $period 
    ) : QueryBuilder { 
        $qb = $this->createQueryBuilder('hasPurchaseOffer'); 
        $expr = $qb->expr(); 
 
        return  $qb->innerJoin('hasPurchaseOffer.offerOrders', 'offerOrder') 
            ->innerJoin('hasPurchaseOffer.categories', 'hasPurchaseOfferCategories') 
            ->andWhere($expr->eq('hasPurchaseOfferCategories.city', ':cityId')) 
            ->andWhere($expr->gt('offerOrder.status', ':status')) 
            ->andWhere($expr->between('offerOrder.createdOn', ':dateFrom', ':dateTo')) 
            ->distinct() 
            ->setParameters([ 
                'cityId' => $cityId, 
                'status' => OfferOrder::STATUS_INIT, 
                'dateFrom' => $period->getStartDate(), 
                'dateTo' => $period->getEndDate(), 
            ]); 
    } 
 
    /** 
     * @return Offer[] 
     */ 
    public function getHasNotPurchaseOffersByCityId( 
        int $cityId, 
        Period $period 
    ) : iterable { 
        $qb = $this->createQueryBuilder('offer'); 
        $expr = $qb->expr(); 
        $getHasPurchaseOffersByCityIdDql = $this->getHasPurchaseOffersByCityIdQuery($cityId, $period)->getDQL(); 
 
        return  $qb->innerJoin('offer.categories', 'categories') 
            ->andWhere($expr->eq('categories.city', ':cityId')) 
            ->andWhere($expr->eq('offer.active', ':active')) 
            ->andWhere($expr->between(':now', 'offer.activeSince', 'offer.activeTill')) 
            ->andWhere($expr->lt('offer.activeSince', ':dateTo')) 
            ->andWhere(sprintf('offer NOT IN (%s)', $getHasPurchaseOffersByCityIdDql)) 
            ->setParameters([ 
                'cityId' => $cityId, 
                'status' => OfferOrder::STATUS_INIT, 
                'active' => true, 
                'dateFrom' => $period->getStartDate(), 
                'dateTo' => $period->getEndDate(), 
                'now' => (new \DateTimeImmutable())->format('Y-m-d H:i:s'), 
            ]) 
            ->getQuery() 
            ->getResult(); 
    } 
 
    public function getHasNotPurchaseNewOffers(int $managerId, int $limit): iterable 
    { 
        $qb = $this->createQueryBuilder('offer'); 
        $expr = $qb->expr(); 
        $hasPurchaseOffersQuery = $this->getEntityManager()->createQueryBuilder(); 
 
        $purchasedOffers = $hasPurchaseOffersQuery 
            ->select('purchase.entityID') 
            ->from('Slivki:PurchaseCount', 'purchase') 
            ->andWhere($expr->gt('purchase.purchaseCount', '0')) 
            ->getQuery() 
            ->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN); 
 
        $qb 
            ->andWhere($expr->eq('offer.active', ':active')) 
            ->andWhere($expr->neq('offer.withoutCodes', ':codeless')) 
            ->andWhere($expr->between(':now', 'offer.activeSince', 'offer.activeTill')) 
            ->andWhere(sprintf('offer.ID NOT IN (%s)', implode(',', $purchasedOffers))) 
            ->andWhere($expr->gte('offer.activeSince', ':newOfferDays')) 
            ->setParameters([ 
                'codeless' => true, 
                'active' => true, 
                'now' => (new \DateTimeImmutable())->format('Y-m-d H:i:s'), 
                'newOfferDays' => (new DateTimeImmutable(sprintf('-%d days', PurchasesByOfferDao::NEW_OFFERS_DAYS)))->format('Y-m-d H:i:s'), 
            ]); 
 
        if ($managerId > 0) { 
            $qb 
                ->andWhere($expr->eq('offer.manager', ':managerId')) 
                ->setParameter('managerId', $managerId); 
        } 
 
        return $qb 
            ->orderBy('offer.title', 'ASC') 
            ->setMaxResults($limit) 
            ->getQuery() 
            ->getResult(); 
    } 
 
    /** 
     * @return Offer[] 
     */ 
    public function getActiveOffersByCityId(int $cityId): array 
    { 
        $queryBuilder = $this->createQueryBuilder('offer'); 
        $expr = $queryBuilder->expr(); 
 
        return $queryBuilder 
            ->innerJoin('offer.categories', 'category') 
            ->innerJoin('category.city', 'city') 
            ->andWhere($expr->eq('city.ID', ':cityId')) 
            ->andWhere($expr->eq('offer.active', ':active')) 
            ->andWhere($expr->neq('offer.hidden', ':hidden')) 
            ->andWhere($expr->between(':now', 'offer.activeSince', 'offer.activeTill')) 
            ->setParameters([ 
                'cityId' => $cityId, 
                'active' => true, 
                'hidden' => true, 
                'now' => (new \DateTimeImmutable())->format('Y-m-d H:i:s'), 
            ]) 
            ->getQuery() 
            ->getResult(); 
    } 
}