'Rabat procentowy na produkty z kategorii 1 jeżeli w koszyku jest produkt z kategorii 2', 2 => 'Rabat procentowy na produkty z kategorii 1 i 2', 3 => 'Najtańszy produkt w koszyku (z wybranych kategorii) za X zł', 4 => 'Rabat procentowy na cały koszyk', 5 => 'Rabat procentowy na produkty z kategorii 1 lub 2', ]; public static $discount_type = [ 1 => 'Rabat procentowy' ]; public function __construct($db) { $this->db = $db; } /** * @return array{items: array>, total: int} */ public function listForAdmin( array $filters, string $sortColumn = 'id', string $sortDir = 'DESC', int $page = 1, int $perPage = 15 ): array { $allowedSortColumns = [ 'id' => 'sp.id', 'name' => 'sp.name', 'status' => 'sp.status', 'condition_type' => 'sp.condition_type', 'date_from' => 'sp.date_from', 'date_to' => 'sp.date_to', ]; $sortSql = $allowedSortColumns[$sortColumn] ?? 'sp.id'; $sortDir = strtoupper(trim($sortDir)) === 'ASC' ? 'ASC' : 'DESC'; $page = max(1, $page); $perPage = min(self::MAX_PER_PAGE, max(1, $perPage)); $offset = ($page - 1) * $perPage; $where = ['1 = 1']; $params = []; $name = trim((string)($filters['name'] ?? '')); if ($name !== '') { if (strlen($name) > 255) { $name = substr($name, 0, 255); } $where[] = 'sp.name LIKE :name'; $params[':name'] = '%' . $name . '%'; } $status = trim((string)($filters['status'] ?? '')); if ($status === '0' || $status === '1') { $where[] = 'sp.status = :status'; $params[':status'] = (int)$status; } $whereSql = implode(' AND ', $where); $sqlCount = " SELECT COUNT(0) FROM pp_shop_promotion AS sp WHERE {$whereSql} "; $stmtCount = $this->db->query($sqlCount, $params); $countRows = $stmtCount ? $stmtCount->fetchAll() : []; $total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0; $sql = " SELECT sp.id, sp.name, sp.status, sp.condition_type, sp.discount_type, sp.amount, sp.date_from, sp.date_to, sp.include_coupon, sp.include_product_promo, sp.min_product_count, sp.price_cheapest_product FROM pp_shop_promotion AS sp WHERE {$whereSql} ORDER BY {$sortSql} {$sortDir}, sp.id DESC LIMIT {$perPage} OFFSET {$offset} "; $stmt = $this->db->query($sql, $params); $items = $stmt ? $stmt->fetchAll() : []; return [ 'items' => is_array($items) ? $items : [], 'total' => $total, ]; } public function find(int $promotionId): array { if ($promotionId <= 0) { return $this->defaultPromotion(); } $promotion = $this->db->get('pp_shop_promotion', '*', ['id' => $promotionId]); if (!is_array($promotion)) { return $this->defaultPromotion(); } $promotion['id'] = (int)($promotion['id'] ?? 0); $promotion['status'] = $this->toSwitchValue($promotion['status'] ?? 0); $promotion['include_coupon'] = $this->toSwitchValue($promotion['include_coupon'] ?? 0); $promotion['include_product_promo'] = $this->toSwitchValue($promotion['include_product_promo'] ?? 0); $promotion['condition_type'] = (int)($promotion['condition_type'] ?? 1); $promotion['discount_type'] = (int)($promotion['discount_type'] ?? 1); $promotion['categories'] = $this->decodeIdList($promotion['categories'] ?? null); $promotion['condition_categories'] = $this->decodeIdList($promotion['condition_categories'] ?? null); return $promotion; } public function save(array $data): ?int { $promotionId = (int)($data['id'] ?? 0); $row = [ 'name' => trim((string)($data['name'] ?? '')), 'status' => $this->toSwitchValue($data['status'] ?? 0), 'condition_type' => (int)($data['condition_type'] ?? 1), 'discount_type' => (int)($data['discount_type'] ?? 1), 'amount' => $this->toNullableNumeric($data['amount'] ?? null), 'date_from' => $this->toNullableDate($data['date_from'] ?? null), 'date_to' => $this->toNullableDate($data['date_to'] ?? null), 'categories' => $this->encodeIdList($data['categories'] ?? null), 'condition_categories' => $this->encodeIdList($data['condition_categories'] ?? null), 'include_coupon' => $this->toSwitchValue($data['include_coupon'] ?? 0), 'include_product_promo' => $this->toSwitchValue($data['include_product_promo'] ?? 0), 'min_product_count' => $this->toNullableInt($data['min_product_count'] ?? null), 'price_cheapest_product' => $this->toNullableNumeric($data['price_cheapest_product'] ?? null), ]; if ($promotionId <= 0) { $this->db->insert('pp_shop_promotion', $row); $id = (int)$this->db->id(); if ($id <= 0) { return null; } $this->invalidateActivePromotionsCache(); \Shared\Helpers\Helpers::delete_dir('../temp/'); return $id; } $this->db->update('pp_shop_promotion', $row, ['id' => $promotionId]); $this->invalidateActivePromotionsCache(); \Shared\Helpers\Helpers::delete_dir('../temp/'); return $promotionId; } public function delete(int $promotionId): bool { if ($promotionId <= 0) { return false; } $deleted = $this->db->delete('pp_shop_promotion', ['id' => $promotionId]); $ok = (bool)$deleted; if ($ok) { $this->invalidateActivePromotionsCache(); } return $ok; } /** * @return array> */ public function categoriesTree($parentId = null): array { $rows = $this->db->select('pp_shop_categories', ['id'], [ 'parent_id' => $parentId, 'ORDER' => ['o' => 'ASC'], ]); if (!is_array($rows)) { return []; } $categories = []; foreach ($rows as $row) { $categoryId = (int)($row['id'] ?? 0); if ($categoryId <= 0) { continue; } $category = $this->db->get('pp_shop_categories', '*', ['id' => $categoryId]); if (!is_array($category)) { continue; } $translations = $this->db->select('pp_shop_categories_langs', '*', ['category_id' => $categoryId]); $category['languages'] = []; if (is_array($translations)) { foreach ($translations as $translation) { $langId = (string)($translation['lang_id'] ?? ''); if ($langId !== '') { $category['languages'][$langId] = $translation; } } } $category['title'] = $this->categoryTitle($category['languages']); $category['subcategories'] = $this->categoriesTree($categoryId); $categories[] = $category; } return $categories; } private function defaultPromotion(): array { return [ 'id' => 0, 'name' => '', 'status' => 1, 'condition_type' => 1, 'discount_type' => 1, 'amount' => null, 'date_from' => null, 'date_to' => null, 'categories' => [], 'condition_categories' => [], 'include_coupon' => 0, 'include_product_promo' => 0, 'min_product_count' => null, 'price_cheapest_product' => null, ]; } private function toSwitchValue($value): int { if (is_bool($value)) { return $value ? 1 : 0; } if (is_numeric($value)) { return ((int)$value) === 1 ? 1 : 0; } if (is_string($value)) { $normalized = strtolower(trim($value)); return in_array($normalized, ['1', 'on', 'true', 'yes'], true) ? 1 : 0; } return 0; } private function toNullableInt($value): ?int { if ($value === null) { return null; } if (is_string($value)) { $value = trim($value); if ($value === '') { return null; } } $intValue = (int)$value; return $intValue > 0 ? $intValue : null; } private function toNullableNumeric($value): ?string { if ($value === null) { return null; } $stringValue = trim((string)$value); if ($stringValue === '') { return null; } return str_replace(',', '.', $stringValue); } private function toNullableDate($value): ?string { $date = trim((string)$value); if ($date === '') { return null; } return $date; } private function encodeIdList($values): ?string { $ids = $this->normalizeIdList($values); if (empty($ids)) { return null; } return json_encode($ids); } /** * @return int[] */ private function decodeIdList($raw): array { if (is_array($raw)) { return $this->normalizeIdList($raw); } $text = trim((string)$raw); if ($text === '') { return []; } $decoded = json_decode($text, true); if (!is_array($decoded)) { return []; } return $this->normalizeIdList($decoded); } /** * @return int[] */ private function normalizeIdList($values): array { if ($values === null) { return []; } if (!is_array($values)) { $text = trim((string)$values); if ($text === '') { return []; } if (strpos($text, ',') !== false) { $values = explode(',', $text); } else { $values = [$text]; } } $ids = []; foreach ($values as $value) { $id = (int)$value; if ($id > 0) { $ids[$id] = $id; } } return array_values($ids); } private function categoryTitle(array $languages): string { $defaultLang = $this->defaultLanguageId(); if ($defaultLang !== '' && isset($languages[$defaultLang]['title'])) { $title = trim((string)$languages[$defaultLang]['title']); if ($title !== '') { return $title; } } foreach ($languages as $language) { $title = trim((string)($language['title'] ?? '')); if ($title !== '') { return $title; } } return ''; } private function defaultLanguageId(): string { if ($this->defaultLangId !== null) { return $this->defaultLangId; } $rows = $this->db->select('pp_langs', ['id', 'start', 'o'], [ 'status' => 1, 'ORDER' => ['start' => 'DESC', 'o' => 'ASC'], ]); if (is_array($rows) && !empty($rows)) { $this->defaultLangId = (string)($rows[0]['id'] ?? ''); } else { $this->defaultLangId = ''; } return $this->defaultLangId; } private function invalidateActivePromotionsCache(): void { if (!class_exists('\Shared\Cache\CacheHandler')) { return; } try { $cache = new \Shared\Cache\CacheHandler(); if (method_exists($cache, 'delete')) { $cache->delete('PromotionRepository::getActivePromotions'); } } catch (\Throwable $e) { // Cache invalidation should not block save/delete. } } public function getActivePromotions() { $cacheHandler = new \Shared\Cache\CacheHandler(); $cacheKey = "PromotionRepository::getActivePromotions"; $objectData = $cacheHandler->get( $cacheKey ); if ( !$objectData ) { $results = $this->db->select( 'pp_shop_promotion', 'id', [ 'AND' => [ 'status' => 1, 'OR #date_from' => [ 'date_from' => null, 'date_from[<=]' => date( 'Y-m-d' ) ], 'OR #date_to' => [ 'date_to' => null, 'date_to[>=]' => date( 'Y-m-d' ) ] ], 'ORDER' => [ 'id' => 'DESC' ] ] ); $cacheHandler->set( $cacheKey, $results ); } else { return unserialize( $objectData ); } return $results; } public function findPromotion( $basket ) { if ( !is_array( $basket ) || empty( $basket ) ) return is_array( $basket ) ? $basket : []; foreach ( $basket as $key => $val ) { unset( $basket[$key]['discount_type'] ); unset( $basket[$key]['discount_amount'] ); unset( $basket[$key]['discount_include_coupon'] ); unset( $basket[$key]['include_product_promo'] ); } $basket_tmp = $basket; $results = $this->getActivePromotions(); if ( is_array( $results ) and count( $results ) ) { foreach ( $results as $row ) { $promotion = $this->find( (int)$row ); if ( $promotion['id'] <= 0 ) continue; if ( $promotion['condition_type'] == 4 ) return $this->applyTypeWholeBasket( $basket_tmp, $promotion ); if ( $promotion['condition_type'] == 3 ) return $this->applyTypeCheapestProduct( $basket_tmp, $promotion ); if ( $promotion['condition_type'] == 5 ) return $this->applyTypeCategoriesOr( $basket_tmp, $promotion ); if ( $promotion['condition_type'] == 2 ) return $this->applyTypeCategoriesAnd( $basket_tmp, $promotion ); if ( $promotion['condition_type'] == 1 ) return $this->applyTypeCategoryCondition( $basket_tmp, $promotion ); } } return $basket; } // ========================================================================= // Frontend: basket promotion logic (migrated from front\factory\ShopPromotion) // ========================================================================= /** * Promocja na cały koszyk (condition_type=4) */ public function applyTypeWholeBasket(array $basket, $promotion): array { $productRepo = new \Domain\Product\ProductRepository( $this->db ); foreach ( $basket as $key => $val ) { $product_promotion = $productRepo->isProductOnPromotion( $val['product-id'] ); if ( !$product_promotion or $product_promotion and $promotion['include_product_promo'] ) { $product_categories = $productRepo->productCategoriesFront( (int) $val['product-id'] ); foreach ( $product_categories as $category_tmp ) { $basket[$key]['discount_type'] = $promotion['discount_type']; $basket[$key]['discount_amount'] = $promotion['amount']; $basket[$key]['discount_include_coupon'] = $promotion['include_coupon']; $basket[$key]['include_product_promo'] = $promotion['include_product_promo']; } } } return $basket; } /** * Promocja na najtańszy produkt z kategorii 1 lub 2 (condition_type=3) */ public function applyTypeCheapestProduct(array $basket, $promotion): array { $productRepo = new \Domain\Product\ProductRepository( $this->db ); $condition_1 = false; $categories = $promotion['categories']; if ( is_array( $categories ) and is_array( $categories ) ) { foreach ( $basket as $key => $val ) { $product_promotion = $productRepo->isProductOnPromotion( $val['product-id'] ); if ( !$product_promotion or $product_promotion and $promotion['include_product_promo'] ) { $product_categories = $productRepo->productCategoriesFront( (int) $val['product-id'] ); foreach ( $product_categories as $category_tmp ) { if ( !$condition_1[$key] and in_array( $category_tmp['category_id'], $categories ) ) $condition_1[$key] = true; } } } } if ( count( $condition_1 ) >= $promotion['min_product_count'] ) { $cheapest_position = false; foreach ( $basket as $key => $val ) { $price = $productRepo->getPrice( $val['product-id'] ); if ( !$cheapest_position or $cheapest_position['price'] > $price ) { $cheapest_position['price'] = $price; $cheapest_position['key'] = $key; } } $basket[$cheapest_position['key']]['quantity'] = 1; $basket[$cheapest_position['key']]['discount_type'] = 3; $basket[$cheapest_position['key']]['discount_amount'] = $promotion['price_cheapest_product']; $basket[$cheapest_position['key']]['discount_include_coupon'] = $promotion['include_coupon']; $basket[$cheapest_position['key']]['include_product_promo'] = $promotion['include_product_promo']; } return $basket; } /** * Promocja na wszystkie produkty z kategorii 1 lub 2 (condition_type=5) */ public function applyTypeCategoriesOr(array $basket, $promotion): array { $productRepo = new \Domain\Product\ProductRepository( $this->db ); $categories = $promotion['categories']; $condition_categories = $promotion['condition_categories']; foreach ( $basket as $key => $val ) { $product_promotion = $productRepo->isProductOnPromotion( $val['product-id'] ); if ( !$product_promotion or $product_promotion and $promotion['include_product_promo'] ) { $product_categories = $productRepo->productCategoriesFront( (int) $val['product-id'] ); foreach ( $product_categories as $category_tmp ) { if ( in_array( $category_tmp['category_id'], $condition_categories ) or in_array( $category_tmp['category_id'], $categories ) ) { $basket[$key]['discount_type'] = $promotion['discount_type']; $basket[$key]['discount_amount'] = $promotion['amount']; $basket[$key]['discount_include_coupon'] = $promotion['include_coupon']; $basket[$key]['include_product_promo'] = $promotion['include_product_promo']; } } } } return $basket; } /** * Promocja na produkty z kategorii 1 i 2 (condition_type=2) */ public function applyTypeCategoriesAnd(array $basket, $promotion): array { $productRepo = new \Domain\Product\ProductRepository( $this->db ); $condition_1 = false; $condition_2 = false; $categories = $promotion['categories']; $condition_categories = $promotion['condition_categories']; if ( is_array( $condition_categories ) and is_array( $categories ) ) { foreach ( $basket as $key => $val ) { $product_categories = $productRepo->productCategoriesFront( (int) $val['product-id'] ); foreach ( $product_categories as $category_tmp ) { if ( !$condition_1 and in_array( $category_tmp['category_id'], $condition_categories ) ) { $condition_1 = true; } } } foreach ( $basket as $key => $val ) { $product_categories = $productRepo->productCategoriesFront( (int) $val['product-id'] ); foreach ( $product_categories as $category_tmp ) { if ( !$condition_2 and in_array( $category_tmp['category_id'], $categories ) ) $condition_2 = true; } } } if ( $condition_1 and $condition_2 ) { foreach ( $basket as $key => $val ) { $product_categories = $productRepo->productCategoriesFront( (int) $val['product-id'] ); foreach ( $product_categories as $category_tmp ) { if ( in_array( $category_tmp['category_id'], $categories ) or in_array( $category_tmp['category_id'], $condition_categories ) ) { $basket[$key]['discount_type'] = $promotion['discount_type']; $basket[$key]['discount_amount'] = $promotion['amount']; $basket[$key]['discount_include_coupon'] = $promotion['include_coupon']; $basket[$key]['include_product_promo'] = $promotion['include_product_promo']; } } } } return $basket; } /** * Rabat procentowy na produkty z kategorii I jeżeli w koszyku jest produkt z kategorii II (condition_type=1) */ public function applyTypeCategoryCondition(array $basket, $promotion): array { $productRepo = new \Domain\Product\ProductRepository( $this->db ); $condition = false; $categories = $promotion['categories']; $condition_categories = $promotion['condition_categories']; if ( is_array( $condition_categories ) and is_array( $categories ) ) { foreach ( $basket as $key => $val ) { $product_categories = $productRepo->productCategoriesFront( (int) $val['product-id'] ); foreach ( $product_categories as $category_tmp ) { if ( in_array( $category_tmp['category_id'], $condition_categories ) ) { $condition = true; } } } } if ( $condition ) { foreach ( $basket as $key => $val ) { $product_categories = $productRepo->productCategoriesFront( (int) $val['product-id'] ); foreach ( $product_categories as $category_tmp ) { if ( in_array( $category_tmp['category_id'], $categories ) ) { $basket[$key]['discount_type'] = $promotion['discount_type']; $basket[$key]['discount_amount'] = $promotion['amount']; $basket[$key]['discount_include_coupon'] = $promotion['include_coupon']; $basket[$key]['include_product_promo'] = $promotion['include_product_promo']; } } } } return $basket; } }