requestStack = $requestStack; $this->contextManager = $contextManager; $this->emailService = $emailService; } protected function getEmailService(): WatchdogEmail { return $this->emailService; } /** * @internal * * @param $user User * @param $products ProductCollection */ public function sendUserNotification($user, $products) { $message = $this->getEmailService() ->setProducts($products) ->setUser($user) ->getEmail(['EMAIL_UZIVATELE' => $user->email]); $message['to'] = $user->email; $this->getEmailService()->sendEmail($message); } /** * @internal * * @throws Exception */ public function generateUserWatchdog($id_user) { $user = User::createFromId($id_user); $userAllowed = true; // check if user is blocked = has password, but is not active anymore if (!$user || (!$user->isActive() && !empty($user->passw))) { $userAllowed = false; } $productList = new ProductList(true); $productList->applyDefaultFilterParams(); $productList->andSpec(function (QueryBuilder $qb) use ($id_user) { $watchdogPriceField = $this->getWatchdogPriceField($qb); $qb->addSelect($watchdogPriceField.' as watchdog_price, pw.currency as watchdog_currency, pw.availability as watchdog_availability') ->joinVatsOnProducts() ->join('pv', 'products_watchdog', 'pw', 'p.id = pw.id_product AND (pv.id = pw.id_variation OR pw.id_variation IS NULL) AND pw.id_user = :id_user') ->join('pw', 'users', 'u', 'u.id = pw.id_user') ->setParameter('id_user', $id_user); $priceField = $this->getPriceField($qb); $inStoreField = $this->getInStoreField($qb); return Operator::andX( Operator::orX( "pw.availability = 1 AND (pw.last_in_store IS NULL OR pw.last_in_store < {$inStoreField})", "{$watchdogPriceField} IS NOT NULL AND {$watchdogPriceField} > ({$priceField})" ), "{$inStoreField} > {$this->inStoreLimit}" ); }); $productList->fetchProducers(); $productList->addResultModifiers(function (&$productCollection) use ($id_user) { $this->deleteUserWatchdog($id_user, $productCollection); }); $productList->addResultModifiers(function (ProductCollection $products, array $data) use ($id_user) { foreach ($products as $productId => $product) { $products[$productId]->hash = $this->getWatchdogHash($id_user, $product['id'], $product['variationId']); $products[$productId]->watchdogPrice = !empty($data[$productId]['watchdog_price']) ? toDecimal($data[$productId]['watchdog_price']) : null; if ($products[$productId]->watchdogPrice) { $watchdogPrice = PriceCalculator::convert( new Price( $products[$productId]->watchdogPrice, Contexts::get(CurrencyContext::class)->getDefault(), 0 ), Contexts::get(CurrencyContext::class)->getActive() ); $products[$productId]->watchdogPrice = $watchdogPrice->getPriceWithVat(); $products[$productId]->isWatchdogPriceLower = PriceCalculator::firstLower($product->getProductPrice(), $watchdogPrice); } $products[$productId]->watchdogAvailability = $data[$productId]['watchdog_availability'] ?? 1; } }); $products = $productList->getProducts(); if ($products->count() && $userAllowed) { $this->sendUserNotification($user, $products); } } public function getWatchdogHash($id_user, $id_product = '', $id_variant = '') { return md5($id_user.$id_product.$id_variant.'Ac~+4Q~,h=8Sh%[!'); } public function deleteUserWatchdog($id_user, $products) { foreach ($products as &$product) { $qb = sqlQueryBuilder() ->select('COUNT(pw.id_user) as id_users') ->from('products_watchdog', 'pw') ->join('pw', 'products', 'p', 'p.id = pw.id_product') ->joinVatsOnProducts() ->leftJoin('pw', 'products_variations', 'pvw', 'pvw.id = pw.id_variation') ->andWhere( Operator::orX( Operator::equalsNullable(['pw.id_product' => $product->id, 'pw.id_variation' => $product->variationId ?? null]), Operator::equalsNullable(['pw.id_product' => $product->id, 'pw.id_variation' => null]) ) ); $qb->addSelect($this->getInStoreField($qb, 'p', 'pvw').'as in_store'); $qb->addSelect($this->getWatchdogPriceField($qb).' as watchdog_price'); $watchdogSelect = $qb->execute()->fetchAssociative(); $qb = sqlQueryBuilder() ->select('pw.availability') ->from('products_watchdog', 'pw') ->join('pw', 'products', 'p', 'p.id = pw.id_product') ->joinVatsOnProducts() ->leftJoin('pw', 'products_variations', 'pvw', 'pvw.id = pw.id_variation') ->andWhere( Operator::orX( Operator::equalsNullable(['pw.id_user' => $id_user, 'pw.id_product' => $product->id, 'pw.id_variation' => $product->variationId ?? null]), Operator::equalsNullable(['pw.id_user' => $id_user, 'pw.id_product' => $product->id, 'pw.id_variation' => null]) ) ); $qb->addSelect('('.$this->getPriceField($qb).') as price'); $qb->addSelect($this->getWatchdogPriceField($qb).' as watchdog_price'); $currentWatchdog = $qb->execute()->fetchAssociative(); $filterSpec = Operator::andX( Operator::equals(['id_user' => $id_user]), Operator::orX( Operator::equals(['id_product' => $product->id, 'id_variation' => $product->variationId ?? null]), Operator::equalsNullable(['id_product' => $product->id, 'id_variation' => null]) ) ); $update = []; // prestat hlidat cenu pokud klesla // do defalutni meny prevadi uz DB, takze prevadime defaultni menu na aktivni a az tu porovnavame if ($currentWatchdog && $currentWatchdog['watchdog_price']) { $watchdogPrice = PriceCalculator::convert( new Price(toDecimal($currentWatchdog['watchdog_price']), Contexts::get(CurrencyContext::class)->getDefault(), 0), Contexts::get(CurrencyContext::class)->getActive() ); if (PriceCalculator::firstLower($product->getProductPrice(), $watchdogPrice)) { $update['price'] = null; } } // zkontrolovat, ze aktualni watchdog ma zapnuty hlidani ceny - pokud nema, tak neresim podminku s poctem naskladnenych kusu a poctem hlidani if ((($currentWatchdog['availability'] ?? 1) == 0) || ($watchdogSelect['in_store'] * $this->deleteMultiplier) >= $watchdogSelect['id_users']) { $update['availability'] = 0; } // aktualizovat stav watchdoga if (!empty($update)) { if (array_key_exists('price', $update) && array_key_exists('availability', $update)) { $product->removedWatchdog = true; } sqlQueryBuilder() ->update('products_watchdog') ->directValues($update) ->andWhere($filterSpec) ->execute(); } } // vymazat neaktivni watchdogy - musi byt neaktivni hlidani ceny i dostupnosti $this->deleteDisabledWatchdogs(); } public function generateWatchdogs(): void { $this->generateProductWatchdogs(); $this->updateLastInStore(); } private function generateProductWatchdogs(?callable $filterSpec = null): void { $qb = sqlQueryBuilder() ->select('pw.id_site as id') ->from('products_watchdog', 'pw') ->groupBy('pw.id_site'); foreach ($qb->execute() as $site) { $this->contextManager->activateSite( $site['id'], function () use ($filterSpec, $site) { $qb = sqlQueryBuilder() ->select('pw.id_user') ->from('products_watchdog', 'pw') ->join('pw', 'products', 'p', 'p.id = pw.id_product') ->joinVatsOnProducts() ->leftJoin('pw', 'products_variations', 'pvw', 'pvw.id = pw.id_variation') ->leftJoin('pw', 'users', 'u', 'u.id = pw.id_user') ->andWhere(\Query\Product::isVisible()) ->andWhere("(pvw.figure = 'Y' OR pvw.figure IS NULL)") // Query\Variation::isVisible() uses different alias ->andWhere(Operator::equals(['pw.id_site' => $site['id']])) ->groupBy('pw.id_user'); $watchdogPriceField = $this->getWatchdogPriceField($qb); $priceField = $this->getPriceField($qb); $inStoreField = $this->getInStoreField($qb, 'p', 'pvw'); $qb->addSelect('('.$priceField.') as price') ->andWhere( Operator::andX( Operator::orX( "pw.availability = 1 AND (pw.last_in_store IS NULL OR pw.last_in_store < {$inStoreField})", "{$watchdogPriceField} IS NOT NULL AND {$watchdogPriceField} > ({$priceField})" ), "{$inStoreField} > {$this->inStoreLimit}" ) ); if ($filterSpec) { $qb->andWhere($filterSpec); } foreach ($qb->execute() as $user) { $this->generateUserWatchdog($user['id_user']); } } ); } } /** * @param int $id_user */ public function updateLastInStore($id_user = null) { $qb = sqlQueryBuilder() ->update('products_watchdog', 'pw') ->join('pw', 'products', 'p', 'p.id = pw.id_product') ->leftJoin('pw', 'products_variations', 'pv', 'pv.id = pw.id_variation') ->leftJoin('pw', 'users', 'u', 'u.id = pw.id_user'); if ($id_user) { $qb->andWhere(Operator::equals(['pw.id_user' => $id_user])); } $qb->set('pw.last_in_store', $this->getInStoreField($qb)); $qb->execute(); } public function getInStoreField(QueryBuilder $qb, string $productAlias = 'p', string $variationAlias = 'pv'): string { $inStoreField = "COALESCE({$variationAlias}.in_store, {$productAlias}.in_store)"; if ($suppliers = findModule(\Modules::WATCHDOG, Modules::SUB_WATCHDOG_SUPPLIERS)) { $condition = "pos.in_store > 0 AND {$productAlias}.id = pos.id_product AND "; $condition .= "(({$variationAlias}.id IS NULL AND pos.id_variation IS NULL) OR ({$variationAlias}.id = pos.id_variation))"; if (is_array($suppliers)) { $qb->setParameter('inIntArray_suppliers', $suppliers, Connection::PARAM_INT_ARRAY); $condition .= ' AND pos.id_supplier IN (:inIntArray_suppliers)'; } $qb->leftJoin($productAlias, 'products_of_suppliers', 'pos', $condition); $inStoreSupplierField = 'COALESCE(pos.in_store, 0)'; $inStoreField = "IF({$inStoreField} > 0, {$inStoreField}, {$inStoreSupplierField})"; } return $inStoreField; } public function getPriceField(QueryBuilder $qb): string { return \Query\Product::withVatAndDiscount($qb, '(COALESCE(pv.price, p.price))'); } public function getWatchdogPriceField(QueryBuilder $qb): string { if (findModule(Modules::CURRENCIES)) { $qb->leftJoin('pw', 'currencies', 'pw_c', 'pw_c.id = pw.currency'); return '(pw.price*pw_c.rate)'; } return 'pw.price'; } public function addWatchdog(int $userId, int $productId, ?int $variationId = null, bool $availability = true, ?float $price = null): void { $id = sqlQueryBuilder() ->select('id') ->from($variationId ? 'products_variations' : 'products') ->where(Operator::equals(['id' => $variationId ?: $productId, 'figure' => 'Y'])) ->execute()->fetchOne(); if (!$id) { return; } $siteId = Contexts::get(SiteContext::class)->getActiveId(); // pokud watchdog neexistuje, tak ho insertnu QueryHint::routeToMaster(); if (!$this->isWatchdog($userId, $productId, $variationId)) { sqlQueryBuilder() ->insert('products_watchdog') ->directValues( [ 'id_site' => $siteId, 'id_user' => $userId, 'id_product' => $productId, 'id_variation' => $variationId, 'availability' => (int) $availability, 'price' => $price, 'currency' => $price ? Contexts::get(CurrencyContext::class)->getActiveId() : null, ] )->execute(); } else { // pokud watchdog uz existuje, tak ho aktualizuju podle zadanych parametru $update = [ 'id_site' => $siteId, 'availability' => (int) $availability, ]; if ($price) { $update['price'] = $price; $update['currency'] = Contexts::get(CurrencyContext::class)->getActiveId(); } sqlQueryBuilder() ->update('products_watchdog') ->directValues($update) ->andWhere( Operator::equalsNullable( [ 'id_user' => $userId, 'id_product' => $productId, 'id_variation' => $variationId, ] ) )->execute(); } QueryHint::routeToMaster(false); $this->setAddedToWatchdogSession( [ 'id_product' => $productId, 'id_variation' => $variationId, ] ); } /** * If $productId is not selected, all the user's watched products will be deleted. */ public function dropWatchdog(int $userId, ?int $productId = null, ?int $variationId = null, bool $availability = true, bool $price = true): void { $qb = sqlQueryBuilder() ->andWhere(Operator::equals(['id_user' => $userId])); if ($availability && $price) { $qb->delete('products_watchdog'); } else { $update = []; if ($availability) { $update['availability'] = 0; } if ($price) { $update['price'] = null; } $qb->update('products_watchdog') ->directValues($update); } // filter if ($productId) { $qb->andWhere(Operator::equals(['id_product' => $productId])); if ($variationId) { $qb->andWhere(Operator::equals(['id_variation' => $variationId])); } } $qb->execute(); // dropnu vsechny watchdogy, ktere maji vypnute jak hlidani dostupnosti, tak hlidani ceny $this->deleteDisabledWatchdogs(); } public function deleteDisabledWatchdogs(): void { // dropnu vsechny watchdogy, ktere maji vypnute jak hlidani dostupnosti, tak hlidani ceny sqlQueryBuilder() ->delete('products_watchdog') ->where( Operator::equalsNullable( [ 'availability' => 0, 'price' => null, ] ) )->execute(); } public function getWatchdog(int $userId, int $productId, ?int $variationId = null): ?array { $watchdog = sqlQueryBuilder() ->select('*') ->from('products_watchdog') ->where( Operator::equalsNullable( [ 'id_user' => $userId, 'id_product' => $productId, 'id_variation' => $variationId, ] ) )->execute()->fetchAssociative(); return $watchdog ?: null; } public function isWatchdog(int $userId, int $productId, ?int $variationId = null): bool { return (bool) $this->getWatchdog($userId, $productId, $variationId); } private function setAddedToWatchdogSession(array $data): void { if ($session = $this->requestStack->getSession()) { $session->set('addedToWatchdog', $data); } } public function deletePurchasedWatchdogProducts(): void { // STRAIGHT_JOIN použit proto, že maria vyhodnotila špatně indexy sqlQuery(' DELETE pw FROM products_watchdog as pw STRAIGHT_JOIN orders o ON o.id_user = pw.id_user INNER JOIN order_items oi ON pw.id_product = oi.id_product AND oi.id_order = o.id WHERE o.status_storno=0 AND (o.date_handle > pw.date_created and o.id_user = pw.id_user) AND (o.status IN(:status)) AND (o.date_handle > (CURDATE() - INTERVAL 1 DAY))', ['status' => implode(',', getStatuses('handled'))]); } } if (empty($subclass)) { class Watchdog extends WatchdogBase { } }