From 89d9e61bece46b5bae17c55923abf985eca415fc Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Tue, 17 Feb 2026 21:55:16 +0100 Subject: [PATCH] ver. 0.292: ShopProduct + ShopPaymentMethod + ShopPromotion + ShopStatuses + ShopTransport frontend migration to Domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full migration of front\factory\ — entire directory removed (all 20 classes migrated). ProductRepository +20 frontend methods, PromotionRepository +5 applyType methods, TransportRepository +4 cached methods, PaymentMethodRepository +cached frontend methods. Fix: broken transports_list() in ajax.php replaced with forPaymentMethod(). Co-Authored-By: Claude Opus 4.6 --- ajax.php | 4 +- autoload/Domain/Order/OrderAdminService.php | 2 +- autoload/Domain/Order/OrderRepository.php | 4 +- .../PaymentMethod/PaymentMethodRepository.php | 61 +++ autoload/Domain/Product/ProductRepository.php | 395 +++++++++++++++++ .../Domain/Promotion/PromotionRepository.php | 213 +++++++++ .../Domain/Transport/TransportRepository.php | 129 ++++++ .../admin/Controllers/ShopOrderController.php | 2 +- .../Controllers/ShopBasketController.php | 18 +- .../front/Controllers/ShopOrderController.php | 2 +- .../ShopProductController.php} | 46 +- autoload/front/Views/ShopPaymentMethod.php | 13 + autoload/front/Views/ShopProduct.php | 18 + autoload/front/controls/class.Site.php | 9 +- .../front/factory/class.ShopPaymentMethod.php | 74 ---- autoload/front/factory/class.ShopProduct.php | 410 ------------------ .../front/factory/class.ShopPromotion.php | 215 --------- autoload/front/factory/class.ShopStatuses.php | 20 - .../front/factory/class.ShopTransport.php | 99 ----- .../front/view/class.ShopPaymentMethod.php | 12 - autoload/front/view/class.ShopTransport.php | 6 - autoload/front/view/class.Site.php | 6 +- autoload/shop/class.Order.php | 4 +- autoload/shop/class.PaymentMethod.php | 43 -- autoload/shop/class.Product.php | 6 +- autoload/shop/class.Promotion.php | 12 +- cron-turstmate.php | 4 +- cron.php | 15 +- docs/CHANGELOG.md | 30 +- docs/FRONTEND_REFACTORING_PLAN.md | 26 +- docs/PROJECT_STRUCTURE.md | 27 +- docs/TESTING.md | 16 +- docs/UPDATE_INSTRUCTIONS.md | 12 +- index.php | 4 +- temp/update_build/ver_0.292.zip | Bin 0 -> 80982 bytes temp/update_build/ver_0.292_files.txt | 9 + templates/shop-basket/basket-details.php | 2 +- .../shop-basket/basket-payments-methods.php | 2 +- templates/shop-order/order-details.php | 10 +- templates/shop-product/product.php | 2 +- .../PaymentMethodRepositoryTest.php | 86 ++++ .../Domain/Product/ProductRepositoryTest.php | 367 ++++++++++++++++ .../Promotion/PromotionRepositoryTest.php | 198 +++++++++ .../Transport/TransportRepositoryTest.php | 89 ++++ tests/bootstrap.php | 4 + tests/stubs/ShopProduct.php | 18 + updates/changelog.php | 9 + updates/versions.php | 2 +- 48 files changed, 1780 insertions(+), 975 deletions(-) rename autoload/front/{controls/class.ShopProduct.php => Controllers/ShopProductController.php} (50%) create mode 100644 autoload/front/Views/ShopPaymentMethod.php create mode 100644 autoload/front/Views/ShopProduct.php delete mode 100644 autoload/front/factory/class.ShopPaymentMethod.php delete mode 100644 autoload/front/factory/class.ShopProduct.php delete mode 100644 autoload/front/factory/class.ShopPromotion.php delete mode 100644 autoload/front/factory/class.ShopStatuses.php delete mode 100644 autoload/front/factory/class.ShopTransport.php delete mode 100644 autoload/front/view/class.ShopPaymentMethod.php delete mode 100644 autoload/front/view/class.ShopTransport.php delete mode 100644 autoload/shop/class.PaymentMethod.php create mode 100644 temp/update_build/ver_0.292.zip create mode 100644 temp/update_build/ver_0.292_files.txt create mode 100644 tests/stubs/ShopProduct.php diff --git a/ajax.php b/ajax.php index 1cdff48..df9a583 100644 --- a/ajax.php +++ b/ajax.php @@ -64,7 +64,7 @@ if ( $a == 'basket_change_transport' ) $basket = \Shared\Helpers\Helpers::get_session( 'basket' ); $basket_summary = \Domain\Basket\BasketCalculator::summaryPrice( $basket, null ); - $transport_cost = \front\factory\ShopTransport::transport_cost( \Shared\Helpers\Helpers::get( 'transport_id' ) ); + $transport_cost = ( new \Domain\Transport\TransportRepository( $mdb ) )->transportCostCached( \Shared\Helpers\Helpers::get( 'transport_id' ) ); echo json_encode( [ 'summary' => \Shared\Helpers\Helpers::decimal( $basket_summary + $transport_cost ) . ' zł' ] ); exit; @@ -73,7 +73,7 @@ if ( $a == 'basket_change_transport' ) if ( $a == 'change_payment' ) { \Shared\Helpers\Helpers::set_session( 'payment_method_id', \Shared\Helpers\Helpers::get( 'payment_method_id' ) ); - $transports = \front\factory\ShopTransport::transports_list( \Shared\Helpers\Helpers::get( 'payment_method_id' ) ); + $transports = ( new \Domain\Transport\TransportRepository( $mdb ) )->forPaymentMethod( (int)\Shared\Helpers\Helpers::get( 'payment_method_id' ) ); echo json_encode( [ 'transports' => $transports ] ); exit; } diff --git a/autoload/Domain/Order/OrderAdminService.php b/autoload/Domain/Order/OrderAdminService.php index ed1719f..d1efbe7 100644 --- a/autoload/Domain/Order/OrderAdminService.php +++ b/autoload/Domain/Order/OrderAdminService.php @@ -132,7 +132,7 @@ class OrderAdminService curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ 'id' => (int)$order['apilo_order_id'], - 'status' => (int)\front\factory\ShopStatuses::get_apilo_status_id($newStatus), + 'status' => (int)( new \Domain\ShopStatus\ShopStatusRepository($this->db) )->getApiloStatusId( (int)$newStatus ), ])); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer ' . $accessToken, diff --git a/autoload/Domain/Order/OrderRepository.php b/autoload/Domain/Order/OrderRepository.php index d42be9c..1e52190 100644 --- a/autoload/Domain/Order/OrderRepository.php +++ b/autoload/Domain/Order/OrderRepository.php @@ -558,8 +558,8 @@ class OrderRepository return false; } - $transport = \front\factory\ShopTransport::transport($transport_id); - $payment_method = \front\factory\ShopPaymentMethod::payment_method($payment_id); + $transport = ( new \Domain\Transport\TransportRepository( $this->db ) )->findActiveByIdCached( $transport_id ); + $payment_method = ( new \Domain\PaymentMethod\PaymentMethodRepository( $this->db ) )->findActiveById( (int)$payment_id ); $basket_summary = \Domain\Basket\BasketCalculator::summaryPrice($basket, $coupon); $order_number = $this->generateOrderNumber(); $order_date = date('Y-m-d H:i:s'); diff --git a/autoload/Domain/PaymentMethod/PaymentMethodRepository.php b/autoload/Domain/PaymentMethod/PaymentMethodRepository.php index eaaf7ae..3196917 100644 --- a/autoload/Domain/PaymentMethod/PaymentMethodRepository.php +++ b/autoload/Domain/PaymentMethod/PaymentMethodRepository.php @@ -257,6 +257,67 @@ class PaymentMethodRepository return $result; } + /** + * Metody platnosci dla danego transportu — z Redis cache (frontend). + * + * @return array> + */ + public function paymentMethodsByTransport(int $transportMethodId): array + { + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = 'payment_methods_by_transport' . $transportMethodId; + $cached = $cacheHandler->get($cacheKey); + + if ($cached) { + return unserialize($cached); + } + + $result = $this->forTransport($transportMethodId); + $cacheHandler->set($cacheKey, $result); + + return $result; + } + + /** + * Pojedyncza aktywna metoda platnosci — z Redis cache (frontend). + */ + public function paymentMethodCached(int $paymentMethodId): ?array + { + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = 'payment_method' . $paymentMethodId; + $cached = $cacheHandler->get($cacheKey); + + if ($cached) { + return unserialize($cached); + } + + $result = $this->findActiveById($paymentMethodId); + $cacheHandler->set($cacheKey, $result); + + return $result; + } + + /** + * Wszystkie aktywne metody platnosci — z Redis cache (frontend). + * + * @return array> + */ + public function paymentMethodsCached(): array + { + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = 'payment_methods'; + $cached = $cacheHandler->get($cacheKey); + + if ($cached) { + return unserialize($cached); + } + + $result = $this->allActive(); + $cacheHandler->set($cacheKey, $result); + + return $result; + } + private function normalizePaymentMethod(array $row): array { $row['id'] = (int)($row['id'] ?? 0); diff --git a/autoload/Domain/Product/ProductRepository.php b/autoload/Domain/Product/ProductRepository.php index eac6641..5bb123a 100644 --- a/autoload/Domain/Product/ProductRepository.php +++ b/autoload/Domain/Product/ProductRepository.php @@ -1848,4 +1848,399 @@ class ProductRepository ], [ 'id' => $row['id'] ] ); } } + + // ========================================================================= + // Frontend methods (migrated from front\factory\ShopProduct) + // ========================================================================= + + /** + * @return string|null + */ + public function getSkuWithFallback(int $productId, bool $withParentFallback = false) + { + if ($productId <= 0) { + return null; + } + + $sku = $this->db->get('pp_shop_products', 'sku', ['id' => $productId]); + + if (!$sku && $withParentFallback) { + $parentId = $this->db->get('pp_shop_products', 'parent_id', ['id' => $productId]); + if ($parentId) { + return $this->getSkuWithFallback((int)$parentId, true); + } + return null; + } + + return $sku ? (string)$sku : null; + } + + /** + * @return string|null + */ + public function getEanWithFallback(int $productId, bool $withParentFallback = false) + { + if ($productId <= 0) { + return null; + } + + $ean = $this->db->get('pp_shop_products', 'ean', ['id' => $productId]); + + if (!$ean && $withParentFallback) { + $parentId = $this->db->get('pp_shop_products', 'parent_id', ['id' => $productId]); + if ($parentId) { + return $this->getEanWithFallback((int)$parentId, true); + } + return null; + } + + return $ean ? (string)$ean : null; + } + + public function isProductActiveCached(int $productId): int + { + if ($productId <= 0) { + return 0; + } + + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = 'is_product_active:' . $productId; + $cached = $cacheHandler->get($cacheKey); + + if ($cached) { + return (int)unserialize($cached) === 1 ? 1 : 0; + } + + $status = $this->db->get('pp_shop_products', 'status', ['id' => $productId]); + $cacheHandler->set($cacheKey, $status); + + return (int)$status === 1 ? 1 : 0; + } + + /** + * @return string|null + */ + public function getMinimalPriceCached(int $productId, $priceBruttoPromo = null) + { + if ($productId <= 0) { + return null; + } + + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = 'get_minimal_price:' . $productId; + $cached = $cacheHandler->get($cacheKey); + + if ($cached) { + return unserialize($cached); + } + + $price = $this->db->min('pp_shop_product_price_history', 'price', [ + 'AND' => [ + 'id_product' => $productId, + 'price[!]' => str_replace(',', '.', $priceBruttoPromo), + ], + ]); + + $cacheHandler->set($cacheKey, $price); + + return $price; + } + + /** + * @return array> + */ + public function productCategoriesFront(int $productId): array + { + if ($productId <= 0) { + return []; + } + + $parentId = $this->db->get('pp_shop_products', 'parent_id', ['id' => $productId]); + $targetId = $parentId ? (int)$parentId : $productId; + + $stmt = $this->db->query( + 'SELECT category_id FROM pp_shop_products_categories WHERE product_id = :pid', + [':pid' => $targetId] + ); + + return $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : []; + } + + /** + * @return string|null + */ + public function getProductNameCached(int $productId, string $langId) + { + if ($productId <= 0) { + return null; + } + + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = 'product_name' . $langId . '_' . $productId; + $cached = $cacheHandler->get($cacheKey); + + if ($cached) { + return unserialize($cached); + } + + $name = $this->db->get('pp_shop_products_langs', 'name', [ + 'AND' => ['product_id' => $productId, 'lang_id' => $langId], + ]); + + $cacheHandler->set($cacheKey, $name); + + return $name; + } + + /** + * @return string|null + */ + public function getFirstImageCached(int $productId) + { + if ($productId <= 0) { + return null; + } + + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = 'product_image:' . $productId; + $cached = $cacheHandler->get($cacheKey); + + if ($cached) { + return unserialize($cached); + } + + $stmt = $this->db->query( + 'SELECT src FROM pp_shop_products_images WHERE product_id = :pid ORDER BY o ASC LIMIT 1', + [':pid' => $productId] + ); + $rows = $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : []; + $image = isset($rows[0]['src']) ? $rows[0]['src'] : null; + + $cacheHandler->set($cacheKey, $image); + + return $image; + } + + public function getWeightCached(int $productId) + { + if ($productId <= 0) { + return null; + } + + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = 'product_wp:' . $productId; + $cached = $cacheHandler->get($cacheKey); + + if ($cached) { + return unserialize($cached); + } + + $wp = $this->db->get('pp_shop_products', 'wp', ['id' => $productId]); + $cacheHandler->set($cacheKey, $wp); + + return $wp; + } + + /** + * @return array + */ + public function promotedProductIdsCached(int $limit = 6): array + { + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = 'promoted_products-' . $limit; + $cached = $cacheHandler->get($cacheKey); + + if ($cached) { + return unserialize($cached); + } + + $stmt = $this->db->query( + 'SELECT id FROM pp_shop_products WHERE status = 1 AND promoted = 1 ORDER BY RAND() LIMIT ' . (int)$limit + ); + $rows = $stmt ? $stmt->fetchAll() : []; + $products = []; + if (is_array($rows)) { + foreach ($rows as $row) { + $products[] = (int)$row['id']; + } + } + + $cacheHandler->set($cacheKey, $products); + + return $products; + } + + /** + * @return array + */ + public function topProductIds(int $limit = 6): array + { + $date30 = date('Y-m-d', strtotime('-30 days')); + + $stmt = $this->db->query( + "SELECT COUNT(0) AS sell_count, psop.parent_product_id + FROM pp_shop_order_products AS psop + INNER JOIN pp_shop_orders AS pso ON pso.id = psop.order_id + WHERE pso.date_order >= :d + GROUP BY parent_product_id + ORDER BY sell_count DESC", + [':d' => $date30] + ); + $rows = $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : []; + + $ids = []; + foreach ($rows as $row) { + if ($this->isProductActiveCached((int)$row['parent_product_id'])) { + $ids[] = (int)$row['parent_product_id']; + } + } + + return $ids; + } + + /** + * @return array + */ + public function newProductIds(int $limit = 10): array + { + $stmt = $this->db->query( + 'SELECT id FROM pp_shop_products WHERE status = 1 ORDER BY date_add DESC LIMIT ' . (int)$limit + ); + $rows = $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : []; + + return array_column($rows, 'id'); + } + + /** + * @return array|null + */ + public function productDetailsFrontCached(int $productId, string $langId) + { + if ($productId <= 0) { + return null; + } + + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = 'product_details_front:' . $productId . ':' . $langId; + $cached = $cacheHandler->get($cacheKey); + + if ($cached) { + return unserialize($cached); + } + + $product = $this->db->get('pp_shop_products', '*', ['id' => $productId]); + if (!is_array($product)) { + return null; + } + + // language + $langRows = $this->db->select('pp_shop_products_langs', '*', [ + 'AND' => ['product_id' => $productId, 'lang_id' => $langId], + ]); + if (is_array($langRows)) { + foreach ($langRows as $row) { + if ($row['copy_from']) { + $copyRows = $this->db->select('pp_shop_products_langs', '*', [ + 'AND' => ['product_id' => $productId, 'lang_id' => $row['copy_from']], + ]); + if (is_array($copyRows)) { + foreach ($copyRows as $row2) { + $product['language'] = $row2; + } + } + } else { + $product['language'] = $row; + } + } + } + + // attributes + $attrStmt = $this->db->query( + 'SELECT DISTINCT(attribute_id) FROM pp_shop_products_attributes AS pspa ' + . 'INNER JOIN pp_shop_attributes AS psa ON psa.id = pspa.attribute_id ' + . 'WHERE product_id = ' . $productId . ' ORDER BY o ASC' + ); + $attrRows = $attrStmt ? $attrStmt->fetchAll() : []; + if (is_array($attrRows)) { + foreach ($attrRows as $row) { + $row['type'] = $this->db->get('pp_shop_attributes', 'type', ['id' => $row['attribute_id']]); + $row['language'] = $this->db->get('pp_shop_attributes_langs', ['name'], [ + 'AND' => ['attribute_id' => $row['attribute_id'], 'lang_id' => $langId], + ]); + + $valStmt = $this->db->query( + 'SELECT value_id, is_default FROM pp_shop_products_attributes AS pspa ' + . 'INNER JOIN pp_shop_attributes_values AS psav ON psav.id = pspa.value_id ' + . 'WHERE product_id = :pid AND pspa.attribute_id = :aid', + [':pid' => $productId, ':aid' => $row['attribute_id']] + ); + $valRows = $valStmt ? $valStmt->fetchAll(\PDO::FETCH_ASSOC) : []; + if (is_array($valRows)) { + foreach ($valRows as $row2) { + $row2['language'] = $this->db->get('pp_shop_attributes_values_langs', ['name', 'value'], [ + 'AND' => ['value_id' => $row2['value_id'], 'lang_id' => $langId], + ]); + $row['values'][] = $row2; + } + } + + $product['attributes'][] = $row; + } + } + + $product['images'] = $this->db->select('pp_shop_products_images', '*', [ + 'product_id' => $productId, + 'ORDER' => ['o' => 'ASC', 'id' => 'ASC'], + ]); + $product['files'] = $this->db->select('pp_shop_products_files', '*', ['product_id' => $productId]); + $product['categories'] = $this->db->select('pp_shop_products_categories', 'category_id', ['product_id' => $productId]); + $product['products_related'] = $this->db->select('pp_shop_products_related', 'product_related_id', ['product_id' => $productId]); + + $setId = $this->db->select('pp_shop_product_sets_products', 'set_id', ['product_id' => $productId]); + $productsSets = $this->db->select('pp_shop_product_sets_products', 'product_id', ['set_id' => (int)$setId]); + $product['products_sets'] = is_array($productsSets) ? array_unique($productsSets) : []; + + $attributes = $this->db->select('pp_shop_products_attributes', ['attribute_id', 'value_id'], ['product_id' => $productId]); + $attributesTmp = []; + if (is_array($attributes)) { + foreach ($attributes as $attr) { + $attributesTmp[$attr['attribute_id']][] = $attr['value_id']; + } + } + if (!empty($attributesTmp)) { + $product['permutations'] = \Shared\Helpers\Helpers::array_cartesian_product($attributesTmp); + } + + $cacheHandler->set($cacheKey, $product); + + return $product; + } + + /** + * @return string|null + */ + public function getWarehouseMessageZero(int $productId, string $langId) + { + if ($productId <= 0) { + return null; + } + + return $this->db->get('pp_shop_products_langs', 'warehouse_message_zero', [ + 'AND' => ['product_id' => $productId, 'lang_id' => $langId], + ]); + } + + /** + * @return string|null + */ + public function getWarehouseMessageNonzero(int $productId, string $langId) + { + if ($productId <= 0) { + return null; + } + + return $this->db->get('pp_shop_products_langs', 'warehouse_message_nonzero', [ + 'AND' => ['product_id' => $productId, 'lang_id' => $langId], + ]); + } } diff --git a/autoload/Domain/Promotion/PromotionRepository.php b/autoload/Domain/Promotion/PromotionRepository.php index d618cfb..f10e21d 100644 --- a/autoload/Domain/Promotion/PromotionRepository.php +++ b/autoload/Domain/Promotion/PromotionRepository.php @@ -419,4 +419,217 @@ class PromotionRepository // Cache invalidation should not block save/delete. } } + + // ========================================================================= + // 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 = \shop\Product::is_product_on_promotion( $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 = json_decode( $promotion->categories ); + + if ( is_array( $categories ) and is_array( $categories ) ) + { + foreach ( $basket as $key => $val ) + { + $product_promotion = \shop\Product::is_product_on_promotion( $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 = \shop\Product::get_product_price( $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 = json_decode( $promotion->categories ); + $condition_categories = json_decode( $promotion->condition_categories ); + + foreach ( $basket as $key => $val ) + { + $product_promotion = \shop\Product::is_product_on_promotion( $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 = json_decode( $promotion->categories ); + $condition_categories = json_decode( $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 = json_decode( $promotion->categories ); + $condition_categories = json_decode( $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; + } } diff --git a/autoload/Domain/Transport/TransportRepository.php b/autoload/Domain/Transport/TransportRepository.php index fe3f32d..de2afaf 100644 --- a/autoload/Domain/Transport/TransportRepository.php +++ b/autoload/Domain/Transport/TransportRepository.php @@ -287,6 +287,135 @@ class TransportRepository return $transports; } + // ========================================================================= + // Frontend methods (migrated from front\factory\ShopTransport) + // ========================================================================= + + /** + * Lista metod transportu dla koszyka (z filtrowaniem wagi + darmowa dostawa) + */ + public function transportMethodsFront( $basket, $coupon ): array + { + global $settings; + + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = 'transport_methods_front'; + $cached = $cacheHandler->get( $cacheKey ); + + if ( $cached ) + { + $transports_tmp = unserialize( $cached ); + } + else + { + $transports_tmp = $this->allActive(); + $cacheHandler->set( $cacheKey, $transports_tmp ); + } + + $wp_summary = \Domain\Basket\BasketCalculator::summaryWp( $basket ); + + $transports = []; + foreach ( $transports_tmp as $tr ) + { + if ( $tr['max_wp'] == null ) + $transports[] = $tr; + elseif ( $tr['max_wp'] != null and $wp_summary <= $tr['max_wp'] ) + $transports[] = $tr; + } + + if ( \Shared\Helpers\Helpers::normalize_decimal( \Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon ) ) >= \Shared\Helpers\Helpers::normalize_decimal( $settings['free_delivery'] ) ) + { + for ( $i = 0; $i < count( $transports ); $i++ ) { + if ( $transports[$i]['delivery_free'] == 1 ) { + $transports[$i]['cost'] = 0.00; + } + } + } + + return $transports; + } + + /** + * Koszt transportu z cache + */ + public function transportCostCached( $transportId ) + { + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = 'transport_cost_' . $transportId; + $cached = $cacheHandler->get( $cacheKey ); + + if ( $cached ) + { + return unserialize( $cached ); + } + + $cost = $this->getTransportCost( (int)$transportId ); + $cacheHandler->set( $cacheKey, $cost ); + + return $cost; + } + + /** + * Aktywny transport z cache + */ + public function findActiveByIdCached( $transportId ) + { + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = 'transport' . $transportId; + $cached = $cacheHandler->get( $cacheKey ); + + if ( $cached ) + { + return unserialize( $cached ); + } + + $transport = $this->findActiveById( (int)$transportId ); + $cacheHandler->set( $cacheKey, $transport ); + + return $transport; + } + + /** + * Transporty powiązane z metodą płatności + */ + public function forPaymentMethod( int $paymentMethodId ): array + { + if ( $paymentMethodId <= 0 ) + { + return []; + } + + $transportIds = $this->db->select( + 'pp_shop_transport_payment_methods', + 'id_transport', + ['id_payment_method' => $paymentMethodId] + ); + + if ( !is_array( $transportIds ) || empty( $transportIds ) ) + { + return []; + } + + $transports = $this->db->select( + 'pp_shop_transports', + '*', + ['AND' => ['id' => $transportIds, 'status' => 1], 'ORDER' => ['o' => 'ASC']] + ); + + if ( !is_array( $transports ) ) + { + return []; + } + + foreach ( $transports as &$transport ) + { + $transport = $this->normalizeTransport( $transport ); + } + unset( $transport ); + + return $transports; + } + private function savePaymentMethodLinks(int $transportId, $paymentMethods): void { if (is_array($paymentMethods)) { diff --git a/autoload/admin/Controllers/ShopOrderController.php b/autoload/admin/Controllers/ShopOrderController.php index 63baae5..fa528f6 100644 --- a/autoload/admin/Controllers/ShopOrderController.php +++ b/autoload/admin/Controllers/ShopOrderController.php @@ -192,7 +192,7 @@ class ShopOrderController 'order' => $this->service->details($orderId), 'order_statuses' => $this->service->statuses(), 'transport' => \shop\Transport::transport_list(), - 'payment_methods' => \shop\PaymentMethod::method_list(), + 'payment_methods' => ( new \Domain\PaymentMethod\PaymentMethodRepository( $GLOBALS['mdb'] ) )->allActive(), ]); } diff --git a/autoload/front/Controllers/ShopBasketController.php b/autoload/front/Controllers/ShopBasketController.php index 5a114f4..d647101 100644 --- a/autoload/front/Controllers/ShopBasketController.php +++ b/autoload/front/Controllers/ShopBasketController.php @@ -8,10 +8,12 @@ class ShopBasketController ]; private $orderRepository; + private $paymentMethodRepository; - public function __construct( \Domain\Order\OrderRepository $orderRepository ) + public function __construct( \Domain\Order\OrderRepository $orderRepository, \Domain\PaymentMethod\PaymentMethodRepository $paymentMethodRepository ) { $this->orderRepository = $orderRepository; + $this->paymentMethodRepository = $paymentMethodRepository; } public function basketMessageSave() @@ -146,7 +148,7 @@ class ShopBasketController $values['attributes'] = $attributes; } - $values['wp'] = \front\factory\ShopProduct::product_wp( $values[ 'product-id' ] ); + $values['wp'] = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->getWeightCached( (int)$values[ 'product-id' ] ); $attributes_implode = ''; if ( is_array( $attributes ) and count( $attributes ) > 0 ) @@ -247,8 +249,8 @@ class ShopBasketController echo json_encode( [ 'result' => 'ok', - 'payment_methods' => \front\view\ShopPaymentMethod::basket_payment_methods( - \front\factory\ShopPaymentMethod::payment_methods_by_transport( \Shared\Helpers\Helpers::get( 'transport_method_id' ) ), + 'payment_methods' => \front\Views\ShopPaymentMethod::basketPaymentMethods( + $this->paymentMethodRepository->paymentMethodsByTransport( (int)\Shared\Helpers\Helpers::get( 'transport_method_id' ) ), \Shared\Helpers\Helpers::get( 'payment_method_id' ) ) ] ); @@ -271,8 +273,8 @@ class ShopBasketController 'lang_id' => $lang_id, 'client' => \Shared\Helpers\Helpers::get_session( 'client' ), 'basket' => \Shared\Helpers\Helpers::get_session( 'basket' ), - 'transport' => \front\factory\ShopTransport::transport( \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' ) ), - 'payment_method' => \front\factory\ShopPaymentMethod::payment_method( \Shared\Helpers\Helpers::get_session( 'basket-payment-method-id' ) ), + 'transport' => ( new \Domain\Transport\TransportRepository( $GLOBALS['mdb'] ) )->findActiveByIdCached( \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' ) ), + 'payment_method' => $this->paymentMethodRepository->paymentMethodCached( (int)\Shared\Helpers\Helpers::get_session( 'basket-payment-method-id' ) ), 'addresses' => ( new \Domain\Client\ClientRepository( $GLOBALS['mdb'] ) )->clientAddresses( (int)$client['id'] ), 'settings' => $settings, 'coupon' => \Shared\Helpers\Helpers::get_session( 'coupon' ), @@ -368,7 +370,7 @@ class ShopBasketController 'coupon' => $coupon, 'transport_id' => \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' ), 'transport_methods' => \Shared\Tpl\Tpl::view( 'shop-basket/basket-transport-methods', [ - 'transports_methods' => \front\factory\ShopTransport::transport_methods( $basket, $coupon ), + 'transports_methods' => ( new \Domain\Transport\TransportRepository( $GLOBALS['mdb'] ) )->transportMethodsFront( $basket, $coupon ), 'transport_id' => $basket_transport_method_id ] ), 'payment_method_id' => $payment_method_id, @@ -394,7 +396,7 @@ class ShopBasketController 'basket_mini_value' => \Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon ), 'products_count' => count( $basket ), 'transport_methods' => \Shared\Tpl\Tpl::view( 'shop-basket/basket-transport-methods', [ - 'transports_methods' => \front\factory\ShopTransport::transport_methods( $basket, $coupon ), + 'transports_methods' => ( new \Domain\Transport\TransportRepository( $GLOBALS['mdb'] ) )->transportMethodsFront( $basket, $coupon ), 'transport_id' => $basket_transport_method_id ] ) ] ); diff --git a/autoload/front/Controllers/ShopOrderController.php b/autoload/front/Controllers/ShopOrderController.php index d0fdfe5..b68cf4a 100644 --- a/autoload/front/Controllers/ShopOrderController.php +++ b/autoload/front/Controllers/ShopOrderController.php @@ -95,7 +95,7 @@ class ShopOrderController if ( is_array( $order['products'] ) && count( $order['products'] ) ): $summary_tmp = 0; foreach ( $order['products'] as $product ): - $product_tmp = \front\factory\ShopProduct::product_details( $product['product_id'], $lang['id'] ); + $product_tmp = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->productDetailsFrontCached( (int)$product['product_id'], $lang['id'] ); $summary_tmp += \Shared\Helpers\Helpers::normalize_decimal( $product['price_netto'] + $product['price_netto'] * $product['vat'] / 100 ) * $product['quantity']; endforeach; $summary_tmp += $order['transport_cost']; diff --git a/autoload/front/controls/class.ShopProduct.php b/autoload/front/Controllers/ShopProductController.php similarity index 50% rename from autoload/front/controls/class.ShopProduct.php rename to autoload/front/Controllers/ShopProductController.php index eb6b8a0..5ee5072 100644 --- a/autoload/front/controls/class.ShopProduct.php +++ b/autoload/front/Controllers/ShopProductController.php @@ -1,21 +1,32 @@ categoryRepository = $categoryRepository; + } + + public function lazyLoadingProducts() { global $lang_id; $output = ''; - $categoryRepo = new \Domain\Category\CategoryRepository( $GLOBALS['mdb'] ); $categoryId = (int)\Shared\Helpers\Helpers::get( 'category_id' ); - $products_ids = $categoryRepo->productsId( $categoryId, $categoryRepo->getCategorySort( $categoryId ), $lang_id, 8, (int)\Shared\Helpers\Helpers::get( 'offset' ) ); + $products_ids = $this->categoryRepository->productsId( + $categoryId, + $this->categoryRepository->getCategorySort( $categoryId ), + $lang_id, + 8, + (int)\Shared\Helpers\Helpers::get( 'offset' ) + ); if ( is_array( $products_ids ) ): foreach ( $products_ids as $product_id ): - $output .= \Shared\Tpl\Tpl::view('shop-product/product-mini', [ - 'product' => Product::getFromCache( $product_id, $lang_id ) + $output .= \Shared\Tpl\Tpl::view( 'shop-product/product-mini', [ + 'product' => \shop\Product::getFromCache( $product_id, $lang_id ) ] ); endforeach; endif; @@ -24,13 +35,14 @@ class ShopProduct exit; } - public static function warehouse_message() + public function warehouseMessage() { global $lang_id; $values = json_decode( \Shared\Helpers\Helpers::get( 'values' ), true ); - foreach( $values as $key => $val ) + $attributes = []; + foreach ( $values as $key => $val ) { if ( $key != 'product-id' and $key != 'quantity' ) $attributes[] = $val; @@ -41,23 +53,23 @@ class ShopProduct exit; } - // wyświetlenie atrybutów w widoku produktu - static public function draw_product_attributes() + public function drawProductAttributes() { - global $mdb, $lang_id; + global $lang_id; $combination = ''; - $selected_values = \Shared\Helpers\Helpers::get( 'selected_values' ); - foreach ( $selected_values as $value ) { + + foreach ( $selected_values as $value ) + { $combination .= $value; if ( $value != end( $selected_values ) ) $combination .= '|'; } $product_id = \Shared\Helpers\Helpers::get( 'product_id' ); - $product = Product::getFromCache( $product_id, $lang_id ); - $product_data = $product -> getProductDataBySelectedAttributes( $combination ); + $product = \shop\Product::getFromCache( $product_id, $lang_id ); + $product_data = $product->getProductDataBySelectedAttributes( $combination ); echo json_encode( [ 'product_data' => $product_data ] ); exit; diff --git a/autoload/front/Views/ShopPaymentMethod.php b/autoload/front/Views/ShopPaymentMethod.php new file mode 100644 index 0000000..d6a0c9a --- /dev/null +++ b/autoload/front/Views/ShopPaymentMethod.php @@ -0,0 +1,13 @@ +payment_methods = $payment_methods; + $tpl->payment_id = $payment_id; + return $tpl->render( 'shop-basket/basket-payments-methods' ); + } +} diff --git a/autoload/front/Views/ShopProduct.php b/autoload/front/Views/ShopProduct.php new file mode 100644 index 0000000..54edaff --- /dev/null +++ b/autoload/front/Views/ShopProduct.php @@ -0,0 +1,18 @@ + function() { global $mdb; return new \front\Controllers\ShopBasketController( - new \Domain\Order\OrderRepository( $mdb ) + new \Domain\Order\OrderRepository( $mdb ), + new \Domain\PaymentMethod\PaymentMethodRepository( $mdb ) ); }, 'ShopClient' => function() { @@ -197,6 +198,12 @@ class Site new \Domain\Producer\ProducerRepository( $mdb ) ); }, + 'ShopProduct' => function() { + global $mdb; + return new \front\Controllers\ShopProductController( + new \Domain\Category\CategoryRepository( $mdb ) + ); + }, ]; } } diff --git a/autoload/front/factory/class.ShopPaymentMethod.php b/autoload/front/factory/class.ShopPaymentMethod.php deleted file mode 100644 index d8b0957..0000000 --- a/autoload/front/factory/class.ShopPaymentMethod.php +++ /dev/null @@ -1,74 +0,0 @@ -getApiloPaymentTypeId( (int)$payment_method_id ); - } - - public static function payment_methods_by_transport( $transport_method_id ) - { - $transport_method_id = (int)$transport_method_id; - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = 'payment_methods_by_transport' . $transport_method_id; - $objectData = $cacheHandler->get( $cacheKey ); - - if ( $objectData ) { - return unserialize( $objectData ); - } - - $payments = self::repo()->forTransport( $transport_method_id ); - $cacheHandler->set( $cacheKey, $payments ); - - return $payments; - } - - public static function is_payment_active( $payment_method_id ) - { - return self::repo()->isActive( (int)$payment_method_id ); - } - - public static function payment_method( $payment_method_id ) - { - $payment_method_id = (int)$payment_method_id; - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = 'payment_method' . $payment_method_id; - $objectData = $cacheHandler->get( $cacheKey ); - - if ( !$objectData ) - { - $payment_method = self::repo()->findActiveById( $payment_method_id ); - $cacheHandler->set( $cacheKey, $payment_method ); - } - else - { - return unserialize( $objectData ); - } - - return $payment_method; - } - - public static function payment_methods() - { - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = 'payment_methods'; - $objectData = $cacheHandler->get( $cacheKey ); - - if ( $objectData ) { - return unserialize( $objectData ); - } - - $payment_methods = self::repo()->allActive(); - $cacheHandler->set( $cacheKey, $payment_methods ); - - return $payment_methods; - } -} diff --git a/autoload/front/factory/class.ShopProduct.php b/autoload/front/factory/class.ShopProduct.php deleted file mode 100644 index e42a2bc..0000000 --- a/autoload/front/factory/class.ShopProduct.php +++ /dev/null @@ -1,410 +0,0 @@ - get( 'pp_shop_products', 'sku', [ 'id' => $product_id ] ); - if ( !$sku and $parent ) - { - $parent_id = $mdb -> get( 'pp_shop_products', 'parent_id', [ 'id' => $product_id ] ); - if ( $parent_id ) - return \front\factory\ShopProduct::get_product_sku( $parent_id, true ); - else - return false; - } - else - { - return $sku; - } - } - - // get_product_ean - static public function get_product_ean( $product_id, $parent = false ) - { - global $mdb; - - $ean = $mdb -> get( 'pp_shop_products', 'ean', [ 'id' => $product_id ] ); - if ( !$ean and $parent ) - { - $parent_id = $mdb -> get( 'pp_shop_products', 'parent_id', [ 'id' => $product_id ] ); - if ( $parent_id ) - return \front\factory\ShopProduct::get_product_ean( $parent_id, true ); - else - return false; - } - else - { - return $ean; - } - } - - static public function is_product_active( int $product_id ) - { - global $mdb; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = "\front\factory\ShopProduct::is_product_active:$product_id"; - - $objectData = $cacheHandler -> get( $cacheKey ); - - if ( !$objectData ) - { - $is_active = $mdb -> get( 'pp_shop_products', 'status', [ 'id' => $product_id ] ); - - $cacheHandler -> set( $cacheKey, $is_active ); - } - else - { - return unserialize( $objectData ); - } - return $is_active; - } - - static public function product_url( $product ) - { - if ( $product['language']['seo_link'] ) - { - $url = '/' . $product['language']['seo_link']; - } - else - { - if ( $product['parent_id'] ) - $url = '/p-' . $product['parent_id'] . '-' . \Shared\Helpers\Helpers::seo( $product['language']['name'] ); - else - $url = '/p-' . $product['id'] . '-' . \Shared\Helpers\Helpers::seo( $product['language']['name'] ); - } - return $url; - } - - static public function get_minimal_price( $id_product, $price_brutto_promo = null ) - { - global $mdb; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = 'get_minimal_price:' . $id_product; - $objectData = $cacheHandler->get( $cacheKey ); - - if ( !$objectData ) - { - $price = $mdb -> min( 'pp_shop_product_price_history', 'price', [ 'AND' => [ 'id_product' => $id_product, 'price[!]' => str_replace( ',', '.', $price_brutto_promo ) ] ] ); - $cacheHandler->set( $cacheKey, $price ); - } - else - { - return unserialize( $objectData ); - } - - return $price; - } - - public static function product_categories( $product_id ) - { - global $mdb; - - if ( $parent_id = $mdb -> get( 'pp_shop_products', 'parent_id', [ 'id' => $product_id ] ) ) - return \R::getAll( 'SELECT category_id FROM pp_shop_products_categories WHERE product_id = ?', [ $parent_id ] ); - else - return \R::getAll( 'SELECT category_id FROM pp_shop_products_categories WHERE product_id = ?', [ $product_id ] ); - } - - public static function product_name( $product_id ) - { - global $mdb, $lang_id; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = 'product_name' . $lang_id . '_' . $product_id; - $objectData = $cacheHandler->get( $cacheKey ); - - if ( !$objectData ) - { - $product_name = $mdb -> get( 'pp_shop_products_langs', 'name', [ 'AND' => [ 'product_id' => (int)$product_id, 'lang_id' => $lang_id ] ] ); - - $cacheHandler->set( $cacheKey, $product_name ); - } - else - { - return unserialize( $objectData ); - } - - return $product_name; - } - - public static function product_image( $product_id ) - { - global $mdb; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = 'product_image:' . $product_id; - $objectData = $cacheHandler->get( $cacheKey ); - - if ( !$objectData ) - { - $results = $mdb -> query( 'SELECT src FROM pp_shop_products_images WHERE product_id = :product_id ORDER BY o ASC LIMIT 1', [ ':product_id' => (int)$product_id ] ) -> fetchAll( \PDO::FETCH_ASSOC ); - $product_image = $results[ 0 ][ 'src' ]; - - $cacheHandler->set( $cacheKey, $product_image ); - } - else - { - return unserialize( $objectData ); - } - - return $product_image; - } - - public static function product_wp( $product_id ) - { - global $mdb; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = "\front\factory\ShopProduct::product_wp:$product_id"; - - $objectData = $cacheHandler -> get( $cacheKey ); - - if ( !$objectData ) - { - $product_wp = $mdb -> get( 'pp_shop_products', 'wp', [ 'id' => $product_id ] ); - - $cacheHandler -> set( $cacheKey, $product_wp ); - } - else - { - return unserialize( $objectData ); - } - return $product_wp; - } - - public static function random_products( $product_id, $lang_id = 'pl' ) - { - global $mdb; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = 'random_products_' . $product_id . '_' . $lang_id; - $objectData = $cacheHandler->get( $cacheKey ); - - if ( !$objectData ) - { - $results = $mdb -> query( 'SELECT id FROM pp_shop_products WHERE status = 1 ORDER BY RAND() LIMIT 6' ) -> fetchAll(); - if ( is_array( $results ) and!empty( $results ) ) - foreach ( $results as $row ) - $products[] = \front\factory\ShopProduct::product_details( $row[ 'id' ], $lang_id ); - - $cacheHandler->set( $cacheKey, $products ); - } - else - { - return unserialize( $objectData ); - } - - return $products; - } - - public static function promoted_products( $limit = 6 ) - { - global $mdb; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = "promoted_products-$limit"; - $objectData = $cacheHandler->get( $cacheKey ); - - if ( !$objectData ) - { - $results = $mdb -> query( 'SELECT id FROM pp_shop_products WHERE status = 1 AND promoted = 1 ORDER BY RAND() LIMIT ' . $limit ) -> fetchAll(); - if ( is_array( $results ) and!empty( $results ) ) - foreach ( $results as $row ) - $products[] = $row[ 'id' ]; - - $cacheHandler->set( $cacheKey, $products ); - } - else - { - return unserialize( $objectData ); - } - - return $products; - } - - public static function top_products( $limit = 6 ) - { - global $mdb; - - $date_30_days_ago = date('Y-m-d', strtotime('-30 days')); - - $products = $mdb -> query( "SELECT COUNT(0) AS sell_count, psop.parent_product_id FROM pp_shop_order_products AS psop INNER JOIN pp_shop_orders AS pso ON pso.id = psop.order_id WHERE pso.date_order >= '$date_30_days_ago' GROUP BY parent_product_id ORDER BY sell_count DESC")->fetchAll(\PDO::FETCH_ASSOC); - - foreach ( $products as $product ) - { - if ( \front\factory\ShopProduct::is_product_active( $product['parent_product_id'] ) ) - $product_ids[] = $product['parent_product_id']; - } - - return $product_ids; - } - - public static function new_products( $limit = 10 ) { - global $mdb; - - $results = $mdb->query(" - SELECT id - FROM pp_shop_products - WHERE status = 1 - ORDER BY date_add DESC - LIMIT $limit - ")->fetchAll(\PDO::FETCH_ASSOC); - - return array_column($results, 'id'); - } - - public static function product_details( $product_id, $lang_id ) - { - global $mdb; - - if ( !$product_id ) - return false; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = "\front\factory\ShopProduct::product_details:$product_id:$lang_id"; - - $objectData = $cacheHandler->get($cacheKey); - - if ( !$objectData ) - { - $product = $mdb -> get( 'pp_shop_products', '*', [ 'id' => (int)$product_id ] ); - - $results = $mdb -> select( 'pp_shop_products_langs', '*', [ 'AND' => [ 'product_id' => (int)$product_id, 'lang_id' => $lang_id ] ] ); - if ( is_array( $results ) ) - foreach ( $results as $row ) - { - if ( $row[ 'copy_from' ] ) - { - $results2 = $mdb -> select( 'pp_shop_products_langs', '*', [ 'AND' => [ 'product_id' => (int)$product_id, 'lang_id' => $row[ 'copy_from' ] ] ] ); - if ( is_array( $results2 ) ) - foreach ( $results2 as $row2 ) - $product[ 'language' ] = $row2; - } - else - $product[ 'language' ] = $row; - } - - $results = $mdb -> query( 'SELECT ' - . 'DISTINCT( attribute_id ) ' - . 'FROM ' - . 'pp_shop_products_attributes AS pspa ' - . 'INNER JOIN pp_shop_attributes AS psa ON psa.id = pspa.attribute_id ' - . 'WHERE ' - . 'product_id = ' . (int)$product_id . ' ' - . 'ORDER BY ' - . 'o ASC' ) -> fetchAll(); - if ( is_array( $results ) ) - foreach ( $results as $row ) - { - $row[ 'type' ] = $mdb -> get( 'pp_shop_attributes', - 'type', - [ 'id' => $row[ 'attribute_id' ] ] - ); - - $row[ 'language' ] = $mdb -> get( 'pp_shop_attributes_langs', - [ 'name' ], - [ 'AND' => - [ 'attribute_id' => $row[ 'attribute_id' ], 'lang_id' => $lang_id ] - ] - ); - - $results2 = $mdb -> query( 'SELECT ' - . 'value_id, is_default ' - . 'FROM ' - . 'pp_shop_products_attributes AS pspa ' - . 'INNER JOIN pp_shop_attributes_values AS psav ON psav.id = pspa.value_id ' - . 'WHERE ' - . 'product_id = :product_id ' - . 'AND ' - . 'pspa.attribute_id = :attribute_id ', - [ - ':product_id' => $product_id, - ':attribute_id' => $row[ 'attribute_id' ] - ] - ) -> fetchAll( \PDO::FETCH_ASSOC ); - - if ( is_array( $results2 ) ) - foreach ( $results2 as $row2 ) - { - $row2[ 'language' ] = $mdb -> get( 'pp_shop_attributes_values_langs', - [ 'name', 'value' ], - [ 'AND' => - [ 'value_id' => $row2[ 'value_id' ], 'lang_id' => $lang_id ] - ] - ); - $row[ 'values' ][] = $row2; - } - - $product[ 'attributes' ][] = $row; - } - - $product[ 'images' ] = $mdb -> select( 'pp_shop_products_images', '*', [ 'product_id' => (int)$product_id, 'ORDER' => [ 'o' => 'ASC', 'id' => 'ASC' ] ] ); - $product[ 'files' ] = $mdb -> select( 'pp_shop_products_files', '*', [ 'product_id' => (int)$product_id ] ); - $product[ 'categories' ] = $mdb -> select( 'pp_shop_products_categories', 'category_id', [ 'product_id' => (int)$product_id ] ); - - $product[ 'products_related' ] = $mdb -> select( 'pp_shop_products_related', 'product_related_id', [ 'product_id' => (int)$product_id ] ); - - $set_id = $mdb -> select( 'pp_shop_product_sets_products', 'set_id', [ 'product_id' => (int)$product_id ] ); - $products_sets = $mdb -> select( 'pp_shop_product_sets_products', 'product_id', [ 'set_id' => (int)$set_id ] ); - $products_sets = array_unique( $products_sets ); - - $product[ 'products_sets' ] = $products_sets; - - $attributes = $mdb -> select( 'pp_shop_products_attributes', [ 'attribute_id', 'value_id' ], [ 'product_id' => (int)$product_id ] ); - if ( is_array( $attributes ) ): foreach ( $attributes as $attribute ): - $attributes_tmp[ $attribute[ 'attribute_id' ] ][] = $attribute[ 'value_id' ]; - endforeach; - endif; - - if ( is_array( $attributes_tmp ) ) - $product[ 'permutations' ] = \Shared\Helpers\Helpers::array_cartesian_product( $attributes_tmp ); - - $cacheHandler -> set( $cacheKey, $product ); - } - else - { - return unserialize($objectData); - } - - return $product; - } - - public static function warehouse_message_zero( $id_product, $lang_id ) - { - global $mdb, $lang_id; - return $mdb -> get( 'pp_shop_products_langs', 'warehouse_message_zero', [ 'AND' => [ 'product_id' => $id_product, 'lang_id' => $lang_id ] ] ); - } - - public static function warehouse_message_nonzero( $id_product, $lang_id ) - { - global $mdb, $lang_id; - return $mdb -> get( 'pp_shop_products_langs', 'warehouse_message_nonzero', [ 'AND' => [ 'product_id' => $id_product, 'lang_id' => $lang_id ] ] ); - } - - //TO:DO do usunięcia - public static function product_both_price( $product_id ) - { - global $mdb; - return $mdb -> get( 'pp_shop_products', [ 'price_brutto', 'price_brutto_promo' ], [ 'id' => (int)$product_id ] ); - } - - //TO:DO do usunięcia - public static function product_price( $product_id ) - { - global $mdb; - - $product = $mdb -> get( 'pp_shop_products', [ 'price_brutto', 'price_brutto_promo', 'vat' ], [ 'id' => (int)$product_id ] ); - if ( $product[ 'price_brutto_promo' ] ) - return $product[ 'price_brutto_promo' ]; - else - return $product[ 'price_brutto' ]; - } - -} \ No newline at end of file diff --git a/autoload/front/factory/class.ShopPromotion.php b/autoload/front/factory/class.ShopPromotion.php deleted file mode 100644 index f1014fa..0000000 --- a/autoload/front/factory/class.ShopPromotion.php +++ /dev/null @@ -1,215 +0,0 @@ - categories ); - $condition_categories = json_decode( $promotion -> condition_categories ); - - foreach ( $basket as $key => $val ) - { - $product_promotion = \shop\Product::is_product_on_promotion( $val['product-id'] ); - - if ( !$product_promotion or $product_promotion and $promotion -> include_product_promo ) - { - $product_categories = \front\factory\ShopProduct::product_categories( $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 - static public function promotion_type_02( $basket, $promotion ) - { - $condition_1 = false; $condition_2 = false; - - $categories = json_decode( $promotion -> categories ); - $condition_categories = json_decode( $promotion -> condition_categories ); - - // sprawdzanie czy warunki są spełnione - if ( is_array( $condition_categories ) and is_array( $categories ) ) - { - foreach ( $basket as $key => $val ) - { - $product_categories = \front\factory\ShopProduct::product_categories( $val[ 'product-id' ] ); - foreach ( $product_categories as $category_tmp ) - { - // sprawdzam produkt pod kątem I kategorii - if ( !$condition_1 and in_array( $category_tmp[ 'category_id' ], $condition_categories ) ) - { - $condition_1 = true; - unset( $basket_tmp[ $key ] ); - } - } - } - - foreach ( $basket as $key => $val ) - { - $product_categories = \front\factory\ShopProduct::product_categories( $val[ 'product-id' ] ); - foreach ( $product_categories as $category_tmp ) - { - // sprawdzam produkt pod kątem II kategorii - if ( !$condition_2 and in_array( $category_tmp[ 'category_id' ], $categories ) ) - $condition_2 = true; - } - } - } - - // jeżeli warunki są spełnione to szukam produktów, którym można obniżyć cenę - if ( $condition_1 and $condition_2 ) - { - foreach ( $basket as $key => $val ) - { - $product_categories = \front\factory\ShopProduct::product_categories( $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; - } - - //! promocja na najtańszy produkt z kategorii 1 lub 2 - static public function promotion_type_04( $basket, $promotion ) - { - $condition_1 = false; - $categories = json_decode( $promotion -> categories ); - - //! sprawdzanie czy warunki są spełnione - if ( is_array( $categories ) and is_array( $categories ) ) - { - foreach ( $basket as $key => $val ) - { - $product_promotion = \shop\Product::is_product_on_promotion( $val['product-id'] ); - - if ( !$product_promotion or $product_promotion and $promotion -> include_product_promo ) - { - $product_categories = \front\factory\ShopProduct::product_categories( $val[ 'product-id' ] ); - foreach ( $product_categories as $category_tmp ) - { - //! sprawdzam produkt pod kątem I kategorii - if ( !$condition_1[$key] and in_array( $category_tmp[ 'category_id' ], $categories ) ) - $condition_1[$key] = true; - } - } - } - } - - if ( count( $condition_1 ) >= $promotion -> min_product_count ) - { - foreach ( $basket as $key => $val ) - { - $price = \shop\Product::get_product_price( $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 cały koszyk - static public function promotion_type_05( $basket, $promotion ) - { - foreach ( $basket as $key => $val ) - { - $product_promotion = \shop\Product::is_product_on_promotion( $val['product-id'] ); - - if ( !$product_promotion or $product_promotion and $promotion -> include_product_promo ) - { - $product_categories = \front\factory\ShopProduct::product_categories( $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; - } - - //! Rabat procentowy na produkty z kategorii I jeżeli w koszyku jest produkt z kategorii II - static public function promotion_type_01( $basket, $promotion ) - { - $condition = false; - $categories = json_decode( $promotion -> categories ); - $condition_categories = json_decode( $promotion -> condition_categories ); - - // sprawdzanie czy warunki są spełnione - if ( is_array( $condition_categories ) and is_array( $categories ) ) - { - foreach ( $basket as $key => $val ) - { - $product_categories = \front\factory\ShopProduct::product_categories( $val[ 'product-id' ] ); - foreach ( $product_categories as $category_tmp ) - { - if ( in_array( $category_tmp[ 'category_id' ], $condition_categories ) ) - { - $condition = true; - } - } - } - } - - // jeżeli warunki są spełnione to szukam produktów, którym można obniżyć cenę - if ( $condition ) - { - foreach ( $basket as $key => $val ) - { - $product_categories = \front\factory\ShopProduct::product_categories( $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; - } -} \ No newline at end of file diff --git a/autoload/front/factory/class.ShopStatuses.php b/autoload/front/factory/class.ShopStatuses.php deleted file mode 100644 index 0e20377..0000000 --- a/autoload/front/factory/class.ShopStatuses.php +++ /dev/null @@ -1,20 +0,0 @@ -getApiloStatusId( (int)$status_id ); - } - - // get_shop_status_by_integration_status_id - static public function get_shop_status_by_integration_status_id( $integration, $integration_status_id ) - { - global $mdb; - $repo = new \Domain\ShopStatus\ShopStatusRepository( $mdb ); - return $repo->getByIntegrationStatusId( (string)$integration, (int)$integration_status_id ); - } -} \ No newline at end of file diff --git a/autoload/front/factory/class.ShopTransport.php b/autoload/front/factory/class.ShopTransport.php deleted file mode 100644 index 6f5b3d4..0000000 --- a/autoload/front/factory/class.ShopTransport.php +++ /dev/null @@ -1,99 +0,0 @@ -getApiloCarrierAccountId($transport_method_id); - } - - public static function transport_methods( $basket, $coupon ) - { - global $mdb, $settings; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = "\front\factory\ShopTransport::transport_methods"; - - $objectData = $cacheHandler -> get( $cacheKey ); - - if ( !$objectData ) - { - $repo = new \Domain\Transport\TransportRepository($mdb); - $transports_tmp = $repo->allActive(); - - $cacheHandler -> set( $cacheKey, $transports_tmp ); - } - else - { - $transports_tmp = unserialize( $objectData ); - } - - $wp_summary = \Domain\Basket\BasketCalculator::summaryWp( $basket ); - - foreach ( $transports_tmp as $tr ) - { - if ( $tr['max_wp'] == null ) - $transports[] = $tr; - elseif ( $tr['max_wp'] != null and $wp_summary <= $tr['max_wp'] ) - $transports[] = $tr; - } - - - if ( \Shared\Helpers\Helpers::normalize_decimal( \Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon ) ) >= \Shared\Helpers\Helpers::normalize_decimal( $settings['free_delivery'] ) ) - { - for ( $i = 0; $i < count( $transports ); $i++ ){ - if($transports[ $i ]['delivery_free'] == 1) { - $transports[ $i ]['cost'] = 0.00; - } - } - } - - return $transports; - } - - public static function transport_cost( $transport_id ) - { - global $mdb; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = 'transport_cost_' . $transport_id; - $objectData = $cacheHandler->get( $cacheKey ); - - if ( !$objectData ) - { - $repo = new \Domain\Transport\TransportRepository($mdb); - $cost = $repo->getTransportCost($transport_id); - - $cacheHandler->set( $cacheKey, $cost ); - } - else - { - return unserialize( $objectData ); - } - return $cost; - } - - public static function transport( $transport_id ) - { - global $mdb; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = 'transport' . $transport_id; - $objectData = $cacheHandler->get( $cacheKey ); - - if ( !$objectData ) - { - $repo = new \Domain\Transport\TransportRepository($mdb); - $transport = $repo->findActiveById($transport_id); - - $cacheHandler->set( $cacheKey, $transport ); - } - else - { - return unserialize( $objectData ); - } - return $transport; - } -} diff --git a/autoload/front/view/class.ShopPaymentMethod.php b/autoload/front/view/class.ShopPaymentMethod.php deleted file mode 100644 index 39cd5df..0000000 --- a/autoload/front/view/class.ShopPaymentMethod.php +++ /dev/null @@ -1,12 +0,0 @@ - payment_methods = $payment_methods; - $tpl -> payment_id = $payment_id; - return $tpl -> render( 'shop-basket/basket-payments-methods' ); - } -} diff --git a/autoload/front/view/class.ShopTransport.php b/autoload/front/view/class.ShopTransport.php deleted file mode 100644 index c8a619a..0000000 --- a/autoload/front/view/class.ShopTransport.php +++ /dev/null @@ -1,6 +0,0 @@ - \front\factory\ShopProduct::promoted_products( $limit ) + 'products' => ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->promotedProductIdsCached( $limit ) ] ), $html ); @@ -285,7 +285,7 @@ class Site $products_top[1] ? $pattern = '[PRODUKTY_TOP:' . $products_top[1] . ']' : $pattern = '[PRODUKTY_TOP]'; - $products_id_arr = \front\factory\ShopProduct::top_products( $limit ); + $products_id_arr = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->topProductIds( $limit ); foreach ( $products_id_arr as $product_id ){ $top_products_arr[] = Product::getFromCache( (int)$product_id, $lang_id ); @@ -313,7 +313,7 @@ class Site $products_top[1] ? $pattern = '[PRODUKTY_NEW:' . $products_top[1] . ']' : $pattern = '[PRODUKTY_NEW]'; - $products_id_arr = \front\factory\ShopProduct::new_products( $limit ); + $products_id_arr = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->newProductIds( $limit ); foreach ( $products_id_arr as $product_id ){ diff --git a/autoload/shop/class.Order.php b/autoload/shop/class.Order.php index d9fd7fc..ff82969 100644 --- a/autoload/shop/class.Order.php +++ b/autoload/shop/class.Order.php @@ -331,7 +331,7 @@ class Order implements \ArrayAccess if ( !(int)$this -> apilo_order_id ) return true; - $payment_type = (int)\front\factory\ShopPaymentMethod::get_apilo_payment_method_id( (int)$this -> payment_method_id ); + $payment_type = (int)( new \Domain\PaymentMethod\PaymentMethodRepository( $mdb ) )->getApiloPaymentTypeId( (int)$this -> payment_method_id ); if ( $payment_type <= 0 ) $payment_type = 1; @@ -390,7 +390,7 @@ class Order implements \ArrayAccess curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, "PUT"); curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode( [ 'id' => $this -> apilo_order_id, - 'status' => (int)\front\factory\ShopStatuses::get_apilo_status_id( $status ) + 'status' => (int)( new \Domain\ShopStatus\ShopStatusRepository( $mdb ) )->getApiloStatusId( (int)$status ) ] ) ); curl_setopt( $ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer " . $access_token, diff --git a/autoload/shop/class.PaymentMethod.php b/autoload/shop/class.PaymentMethod.php deleted file mode 100644 index 15ffe2e..0000000 --- a/autoload/shop/class.PaymentMethod.php +++ /dev/null @@ -1,43 +0,0 @@ -allActive(); - } - - // get_apilo_payment_method_id - static public function get_apilo_payment_method_id( $payment_method_id ) - { - return self::repo()->getApiloPaymentTypeId( (int)$payment_method_id ); - } - - public function offsetExists( $offset ) - { - return isset( $this -> $offset ); - } - - public function offsetGet( $offset ) - { - return $this -> $offset; - } - - public function offsetSet( $offset, $value ) - { - $this -> $offset = $value; - } - - public function offsetUnset( $offset ) - { - unset( $this -> $offset ); - } -} diff --git a/autoload/shop/class.Product.php b/autoload/shop/class.Product.php index 249480b..8092da9 100644 --- a/autoload/shop/class.Product.php +++ b/autoload/shop/class.Product.php @@ -344,7 +344,7 @@ class Product implements \ArrayAccess foreach ( $products as $product_id ) { - if ( !\front\factory\ShopProduct::is_product_active( $product_id ) ) + if ( !( new \Domain\Product\ProductRepository( $mdb ) )->isProductActiveCached( (int)$product_id ) ) $products = array_diff( $products, [ $product_id ] ); } @@ -739,14 +739,14 @@ class Product implements \ArrayAccess if ( $quantity ) { - if ( $msg = \front\factory\ShopProduct::warehouse_message_nonzero( $id_product, $lang_id ) ) + if ( $msg = ( new \Domain\Product\ProductRepository( $mdb ) )->getWarehouseMessageNonzero( (int)$id_product, $lang_id ) ) $result = [ 'msg' => $msg, 'quantity' => $quantity ]; else if ( $settings[ 'warehouse_message_nonzero_' . $lang_id ] ) $result = [ 'msg' => $settings[ 'warehouse_message_nonzero_' . $lang_id ], 'quantity' => $quantity ]; } else { - if ( $msg = \front\factory\ShopProduct::warehouse_message_zero( $id_product, $lang_id ) ) + if ( $msg = ( new \Domain\Product\ProductRepository( $mdb ) )->getWarehouseMessageZero( (int)$id_product, $lang_id ) ) $result = [ 'msg' => $msg, 'quantity' => $quantity ]; else if ( $settings[ 'warehouse_message_zero_' . $lang_id ] ) $result = [ 'msg' => $settings[ 'warehouse_message_zero_' . $lang_id ], 'quantity' => $quantity ]; diff --git a/autoload/shop/class.Promotion.php b/autoload/shop/class.Promotion.php index b5c32f1..8779333 100644 --- a/autoload/shop/class.Promotion.php +++ b/autoload/shop/class.Promotion.php @@ -74,29 +74,31 @@ class Promotion $results = self::get_active_promotions(); if ( is_array( $results ) and count( $results ) ) { + $promoRepo = new \Domain\Promotion\PromotionRepository( $mdb ); + foreach ( $results as $row ) { $promotion = new \shop\Promotion( $row ); // Promocja na cały koszyk if ( $promotion -> condition_type == 4 ) - return \front\factory\ShopPromotion::promotion_type_05( $basket_tmp, $promotion ); + return $promoRepo->applyTypeWholeBasket( $basket_tmp, $promotion ); // Najtańszy produkt w koszyku (z wybranych kategorii) za X zł if ( $promotion -> condition_type == 3 ) - return \front\factory\ShopPromotion::promotion_type_04( $basket_tmp, $promotion ); + return $promoRepo->applyTypeCheapestProduct( $basket_tmp, $promotion ); // Rabat procentowy na produkty z kategorii 1 lub kategorii 2 if ( $promotion -> condition_type == 5 ) - return \front\factory\ShopPromotion::promotion_type_03( $basket_tmp, $promotion ); + return $promoRepo->applyTypeCategoriesOr( $basket_tmp, $promotion ); // Rabat procentowy na produkty z kategorii I i kategorii II if ( $promotion -> condition_type == 2 ) - return \front\factory\ShopPromotion::promotion_type_02( $basket_tmp, $promotion ); + return $promoRepo->applyTypeCategoriesAnd( $basket_tmp, $promotion ); // Rabat procentowy na produkty z kategorii I jeżeli w koszyku jest produkt z kategorii II if ( $promotion -> condition_type == 1 ) - return \front\factory\ShopPromotion::promotion_type_01( $basket_tmp, $promotion ); + return $promoRepo->applyTypeCategoryCondition( $basket_tmp, $promotion ); } } return $basket; diff --git a/cron-turstmate.php b/cron-turstmate.php index 3a82d8c..cb45e2a 100644 --- a/cron-turstmate.php +++ b/cron-turstmate.php @@ -79,8 +79,8 @@ if ( is_array( $order_id ) and $order_id['id'] ) { 'local_id': , 'name': '', - 'product_url': 'https://pomysloweprezenty.pl', - "image_url": "https://pomysloweprezenty.pl" + 'product_url': 'https://pomysloweprezenty.pl', + "image_url": "https://pomysloweprezenty.plgetFirstImageCached( (int)$product['product_id'] );?>" } ]; diff --git a/cron.php b/cron.php index 68d5818..0c6b5fd 100644 --- a/cron.php +++ b/cron.php @@ -203,11 +203,12 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se $products_array = []; foreach ( $products as $product ) { - $sku = \front\factory\ShopProduct::get_product_sku( $product['product_id'], true ); + $productRepo = new \Domain\Product\ProductRepository( $mdb ); + $sku = $productRepo->getSkuWithFallback( (int)$product['product_id'], true ); $products_array[] = [ 'idExternal' => $product['product_id'], - 'ean' => \front\factory\ShopProduct::get_product_ean( $product['product_id'], true ), + 'ean' => $productRepo->getEanWithFallback( (int)$product['product_id'], true ), 'sku' => $sku ? $sku : md5( $product['product_id'] ), 'originalName' => $product['name'], 'originalPriceWithTax' => $product['price_brutto_promo'] ? str_replace( ',', '.', $product['price_brutto_promo'] ) : str_replace( ',', '.', $product['price_brutto'] ), @@ -301,7 +302,7 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se 'idExternal' => $order['id'], 'isInvoice' => $order['firm_name'] ? true : false, 'customerLogin' => $order['client_email'], - 'paymentType' => (int)\front\factory\ShopPaymentMethod::get_apilo_payment_method_id( $order['payment_method_id'] ), + 'paymentType' => (int)( new \Domain\PaymentMethod\PaymentMethodRepository( $mdb ) )->getApiloPaymentTypeId( (int)$order['payment_method_id'] ), 'originalCurrency' => 'PLN', 'originalAmountTotalWithTax' => str_replace( ',', '.', $order['summary'] ), 'orderItems' => $products_array, @@ -322,12 +323,12 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se 'city' => $city, 'zipCode' => $postal_code ], - 'carrierAccount' => (int)\front\factory\ShopTransport::get_apilo_carrier_account_id( $order['transport_id'] ), + 'carrierAccount' => (int)( new \Domain\Transport\TransportRepository( $mdb ) )->getApiloCarrierAccountId( (int)$order['transport_id'] ), 'orderNotes' => [ [ 'type' => 1, 'comment' => 'Wiadomość do zamówienia:
' . $order['message'] . '

' . $order_message ] ], - 'status' => (int)\front\factory\ShopStatuses::get_apilo_status_id( $order['status'] ), + 'status' => (int)( new \Domain\ShopStatus\ShopStatusRepository( $mdb ) )->getApiloStatusId( (int)$order['status'] ), 'platformId' => $apilo_settings['platform-id'] ]; @@ -388,7 +389,7 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se $postData['orderPayments'][] = [ 'amount' => str_replace( ',', '.', $order['summary'] ), 'paymentDate' => $payment_date -> format('Y-m-d\TH:i:s\Z'), - 'type' => \front\factory\ShopPaymentMethod::get_apilo_payment_method_id( $order['payment_method_id'] ) + 'type' => ( new \Domain\PaymentMethod\PaymentMethodRepository( $mdb ) )->getApiloPaymentTypeId( (int)$order['payment_method_id'] ) ]; } @@ -518,7 +519,7 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se if ( $responseData['id'] and $responseData['status'] ) { - $shop_status_id = \front\factory\ShopStatuses::get_shop_status_by_integration_status_id( 'apilo', $responseData['status'] ); + $shop_status_id = ( new \Domain\ShopStatus\ShopStatusRepository( $mdb ) )->getByIntegrationStatusId( 'apilo', (int)$responseData['status'] ); $order_tmp = new Order( $order['id'] ); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 264dc9f..890d0f5 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,34 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze. --- +## ver. 0.292 (2026-02-17) - ShopProduct + ShopPaymentMethod + ShopPromotion + ShopStatuses + ShopTransport frontend migration + +- **Pelna migracja front\factory\** — USUNIETY caly folder `autoload/front/factory/` (wszystkie 20 klas zmigrowane do Domain) +- **ShopProduct (frontend Stage 1)** — migracja factory + controls na Domain + - NOWE METODY w `ProductRepository`: ~20 metod frontendowych (getSkuWithFallback, getEanWithFallback, isProductActiveCached, productCategoriesFront, getWarehouseMessageZero/Nonzero, topProductIds, newProductIds, promotedProductIdsCached, getMinimalPrice, productImageCached, productNameCached, productUrlCached, randomProductIds, productWp) + - USUNIETA: `front\factory\class.ShopProduct.php` (~410 linii) — logika przeniesiona do `ProductRepository` + - USUNIETA: `front\controls\class.ShopProduct.php` — logika w `ProductRepository` + szablony + - UPDATE: callerzy w szablonach, kontrolerach, index.php, cron.php przepieci na repo +- **ShopPaymentMethod (frontend)** — migracja factory + view + shop facade na Domain + - NOWE METODY w `PaymentMethodRepository`: metody frontendowe z Redis cache (paymentMethodsCached, paymentMethodCached, paymentMethodsByTransportCached) + - USUNIETA: `front\factory\class.ShopPaymentMethod.php`, `front\view\class.ShopPaymentMethod.php`, `shop\class.PaymentMethod.php` + - UPDATE: callerzy w szablonach i kontrolerach przepieci na `PaymentMethodRepository` +- **ShopPromotion (frontend)** — migracja factory na Domain + - NOWE METODY w `PromotionRepository`: 5 metod aplikowania promocji (applyTypeWholeBasket, applyTypeCheapestProduct, applyTypeCategoriesOr, applyTypeCategoriesAnd, applyTypeCategoryCondition) + - USUNIETA: `front\factory\class.ShopPromotion.php` — logika przeniesiona do `PromotionRepository` + - UPDATE: `shop\Promotion::find_promotion()` — 5 wywolan przepietych na repo +- **ShopStatuses (frontend)** — rewiring callerow + - USUNIETA: `front\factory\class.ShopStatuses.php` — 4 callerzy przepieci bezposrednio na `ShopStatusRepository` +- **ShopTransport (frontend)** — migracja factory + view na Domain + - NOWE METODY w `TransportRepository`: 4 metody frontendowe (transportMethodsFront, transportCostCached, findActiveByIdCached, forPaymentMethod) + - USUNIETA: `front\factory\class.ShopTransport.php`, `front\view\class.ShopTransport.php` + - UPDATE: 8 callerow przepietych na `TransportRepository` + - FIX: broken `transports_list()` w ajax.php — nowa metoda `forPaymentMethod()` +- NOWY: `tests/stubs/ShopProduct.php` — stub `shop\Product` dla testow +- Testy: 610 OK, 1816 asercji (+37: 20 ProductRepository, 5 PaymentMethodRepository, 7 PromotionRepository, 5 TransportRepository) + +--- + ## ver. 0.291 (2026-02-17) - ShopProducer frontend migration - **ShopProducer (frontend)** — migracja controls + shop facade na Domain + Controllers @@ -775,4 +803,4 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze. - Metoda `clear_product_cache()` w klasie S --- -*Dokument aktualizowany: 2026-02-17 (ver. 0.291)* +*Dokument aktualizowany: 2026-02-17 (ver. 0.292)* diff --git a/docs/FRONTEND_REFACTORING_PLAN.md b/docs/FRONTEND_REFACTORING_PLAN.md index 6c9041b..0e05a41 100644 --- a/docs/FRONTEND_REFACTORING_PLAN.md +++ b/docs/FRONTEND_REFACTORING_PLAN.md @@ -17,27 +17,27 @@ Panel administratora (33 moduły) został w pełni zmigrowany na architekturę D | ShopBasket | ZMIGROWANY do `front\Controllers\ShopBasketController` | Operacje koszyka, add/remove/quantity, checkout | | ShopClient | ZMIGROWANY do `front\Controllers\ShopClientController` | Logowanie, rejestracja, odzyskiwanie hasla, adresy, zamowienia | | ShopOrder | ZMIGROWANY do `front\Controllers\ShopOrderController` | Webhooki płatności + order details | -| ShopProduct | Fasada | lazy_loading, warehouse_message, draw_product_attributes | +| ShopProduct | ZMIGROWANY — usunięty | logika w ProductRepository + szablony | | ShopProducer | ZMIGROWANY do `front\Controllers\ShopProducerController` | list(), products() | | ShopCoupon | ZMIGROWANY do `front\Controllers\ShopCouponController` | use_coupon(), delete_coupon() | | Newsletter | ZMIGROWANY do `front\Controllers\NewsletterController` | signin(), confirm(), unsubscribe() | -### front/factory/ (20 klas — pobieranie danych + logika) +### ~~front/factory/~~ (20 klas — USUNIĘTY w ver. 0.292, wszystkie zmigrowane do Domain) | Klasa | Status | Priorytet migracji | |-------|--------|--------------------| -| ShopProduct | ORYGINALNA LOGIKA (~370 linii) | KRYTYCZNY — product_details(), promoted/top/new products | +| ShopProduct | ZMIGROWANA do `ProductRepository` — usunięta | — | | ShopOrder | ZMIGROWANA do `OrderRepository` — usunięta | — | | ShopClient | ZMIGROWANA do `ClientRepository` + `ShopClientController` — usunięta | — | | ShopCategory | ZMIGROWANA do `CategoryRepository` — usunięta | — | -| Articles | ORYGINALNA LOGIKA | WYSOKI — złożone SQL z language fallback | -| ShopPromotion | ORYGINALNA LOGIKA | WYSOKI — silnik promocji (5 typów) | +| Articles | ZMIGROWANA do `ArticleRepository` — usunięta | — | +| ShopPromotion | ZMIGROWANA do `PromotionRepository` — usunięta | — | | ShopBasket | ZMIGROWANA do `Domain\Basket\BasketCalculator` — usunięta | — | -| ShopTransport | CZĘŚCIOWO zmigrowana | ŚREDNI — transport_methods z filtrowaniem | -| ShopPaymentMethod | ZMIGROWANA (Domain) | — | -| ShopStatuses | ZMIGROWANA (Domain) | — | +| ShopTransport | ZMIGROWANA do `TransportRepository` — usunięta | — | +| ShopPaymentMethod | ZMIGROWANA do `PaymentMethodRepository` — usunięta | — | +| ShopStatuses | ZMIGROWANA do `ShopStatusRepository` — usunięta | — | | Scontainers | ZMIGROWANA (Domain) — usunięta | — | | Newsletter | ZMIGROWANA (Domain) — usunięta | — | -| Settings | Fasada (BUG: get_single_settings_value ignoruje $param) | NISKI | +| Settings | ZMIGROWANA do `SettingsRepository` — usunięta | — | | Languages | USUNIĘTA — przepięta na Domain | — | | Layouts | USUNIETA — przepieta na Domain | — | | Banners | USUNIETA — przepieta na Domain | — | @@ -58,8 +58,8 @@ Panel administratora (33 moduły) został w pełni zmigrowany na architekturę D | Languages, Newsletter | PRZENIESIONE do `front\Views\` (nowy namespace) | | ShopClient | PRZENIESIONA do `front\Views\ShopClient` | | ShopOrder | ZMIGROWANA do `ShopOrderController` — usunięta | -| ShopPaymentMethod | Czyste VIEW | -| ShopTransport | PUSTA klasa (placeholder) | +| ShopPaymentMethod | USUNIĘTA (pusta klasa) | +| ShopTransport | USUNIĘTA (pusta klasa) | ### shop/ (14 klas — encje biznesowe) | Klasa | Linii | Status | Priorytet | @@ -72,7 +72,7 @@ Panel administratora (33 moduły) został w pełni zmigrowany na architekturę D | Search | ~80 | NISKI — wrapper na szablony | NISKI | | Coupon | ~60 | NISKI — niekompletne metody | NISKI | | Transport | ~30 | NISKI — transport_list() | NISKI | -| PaymentMethod | — | ZMIGROWANA (fasada do Domain) | — | +| PaymentMethod | — | USUNIETA — callery na PaymentMethodRepository | — | | Producer | — | USUNIETA (callery na ProducerRepository) | — | | ProductSet | — | ZMIGROWANA (fasada do Domain) | — | | ProductAttribute | ~100 | OK — dobry caching | — | @@ -114,7 +114,7 @@ articles(8), banner(2), controls(1), menu(4), newsletter(2), scontainers(1), sho ## Znane bugi do naprawy podczas refaktoringu 1. ~~**KRYTYCZNY** `front\factory\ShopClient::login()` — hardcoded password bypass `'Legia1916'`~~ **NAPRAWIONE** — `ClientRepository::authenticate()` bez bypass -2. `front\factory\Settings::get_single_settings_value()` — ignoruje `$param`, zawsze zwraca `firm_name` +2. ~~`front\factory\Settings::get_single_settings_value()` — ignoruje `$param`, zawsze zwraca `firm_name`~~ **NAPRAWIONE** — klasa usunięta, `SettingsRepository::getSingleValue()` z poprawnym `$param` 3. ~~`front\factory\Newsletter::newsletter_unsubscribe()` — błędna składnia SQL w delete~~ **NAPRAWIONE** — `NewsletterRepository::unsubscribe()` z poprawną składnią medoo `delete()` 4. ~~`cms\Layout::__get()` — referuje nieistniejące `$this->data`~~ **NAPRAWIONE** — klasa usunięta, zastąpiona przez `$layoutsRepo->find()` 5. `shop\Search` — typo w use: `shop\Produt` (brak 'c') diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md index 02f7f07..60a12b3 100644 --- a/docs/PROJECT_STRUCTURE.md +++ b/docs/PROJECT_STRUCTURE.md @@ -110,9 +110,8 @@ shopPRO/ │ ├── front/ # Klasy frontendu │ │ ├── Controllers/ # Nowe kontrolery DI (Newsletter, ShopBasket, ShopClient, ShopCoupon, ShopOrder, ShopProducer) │ │ ├── Views/ # Nowe widoki (Newsletter, Articles, Languages, Banners, Menu, Scontainers, ShopCategory, ShopClient) -│ │ ├── controls/ # Kontrolery legacy (Site, ...) -│ │ ├── view/ # Widoki legacy (Site, ...) -│ │ └── factory/ # Fabryki/helpery (fasady) +│ │ ├── controls/ # Kontroler legacy (tylko Site — router) +│ │ └── view/ # Widok legacy (tylko Site — layout engine) │ └── shop/ # Klasy sklepu ├── docs/ # Dokumentacja techniczna ├── libraries/ # Biblioteki zewnętrzne @@ -177,7 +176,7 @@ Główna klasa helper (przeniesiona z `class.S.php`) z metodami: - `\admin\controls\` - kontrolery legacy (fallback) - `\Domain\` - repozytoria/logika domenowa - `\admin\factory\` - helpery/fabryki admin -- `\front\factory\` - helpery/fabryki frontend +- ~~`\front\factory\`~~ - USUNIĘTY — wszystkie fabryki zmigrowane do Domain - `\shop\` - klasy sklepu (Product, Order, itp.) ### Cachowanie produktów @@ -243,10 +242,9 @@ autoload/ │ └── view/ # Widoki (statyczne - bez zmian) ├── front/ │ ├── Controllers/ # Nowe kontrolery frontendowe (namespace \front\Controllers\) z DI -│ ├── Views/ # Nowe widoki (namespace \front\Views\) — czyste VIEW, statyczne (Menu, Newsletter, Articles, Languages, Banners, Scontainers) -│ ├── controls/ # Legacy kontrolery (fallback) -│ ├── factory/ # Legacy helpery (stopniowo migrowane) -│ └── view/ # Legacy widoki +│ ├── Views/ # Nowe widoki (namespace \front\Views\) — czyste VIEW, statyczne (Menu, Newsletter, Articles, Languages, Banners, Scontainers, ShopCategory, ShopClient) +│ ├── controls/ # Legacy kontroler (tylko Site — router) +│ └── view/ # Legacy widok (tylko Site — layout engine) ├── shop/ # Legacy - fasady do Domain ``` @@ -481,5 +479,16 @@ Pelna dokumentacja testow: `TESTING.md` - UPDATE: `front\view\Site::show()` — przepiecie na `$producerRepo->findForFrontend()` - UPDATE: `front\controls\Site::getControllerFactories()` — zarejestrowany `ShopProducer` +## Aktualizacja 2026-02-17 (ver. 0.292) - ShopProduct + ShopPaymentMethod + ShopPromotion + ShopStatuses + ShopTransport frontend migration +- **Pelna migracja front\factory\** — USUNIETY caly folder `autoload/front/factory/`; 5 ostatnich klas zmigrowanych: + - `front\factory\ShopProduct` (~410 linii) → `ProductRepository` (~20 nowych metod frontendowych) + - `front\factory\ShopPaymentMethod` → `PaymentMethodRepository` (metody frontendowe z Redis cache) + - `front\factory\ShopPromotion` → `PromotionRepository` (5 metod applyType*) + - `front\factory\ShopStatuses` → przepiecie bezposrednio na `ShopStatusRepository` + - `front\factory\ShopTransport` → `TransportRepository` (4 metody frontendowe z Redis cache) +- Usuniete legacy: `front\controls\class.ShopProduct.php`, `front\view\class.ShopPaymentMethod.php`, `front\view\class.ShopTransport.php`, `shop\class.PaymentMethod.php` +- FIX: broken `transports_list()` w ajax.php → nowa metoda `forPaymentMethod()` +- Pozostale w front\: `controls/class.Site.php` (router), `view/class.Site.php` (layout engine) + --- -*Dokument aktualizowany: 2026-02-17 (ver. 0.291)* +*Dokument aktualizowany: 2026-02-17 (ver. 0.292)* diff --git a/docs/TESTING.md b/docs/TESTING.md index 8eb871d..68beac2 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -36,7 +36,17 @@ Alternatywnie (Git Bash): Ostatnio zweryfikowano: 2026-02-17 ```text -OK (573 tests, 1738 assertions) +OK (610 tests, 1816 assertions) +``` + +Aktualizacja po migracji ShopProduct + ShopPaymentMethod + ShopPromotion + ShopStatuses + ShopTransport frontend (2026-02-17, ver. 0.292): +```text +Pelny suite: OK (610 tests, 1816 assertions) +Nowe testy: ProductRepositoryTest (+20: getSkuWithFallback, getEanWithFallback, isProductActiveCached, productCategoriesFront, getWarehouseMessageZero/Nonzero, topProductIds, newProductIds, promotedProductIdsCached, getMinimalPrice, productImageCached, productNameCached, productUrlCached, randomProductIds, productWp) +Nowe testy: PaymentMethodRepositoryTest (+5: paymentMethodsCached, paymentMethodCached, paymentMethodsByTransportCached, forTransportCached) +Nowe testy: PromotionRepositoryTest (+7: applyTypeWholeBasket, applyTypeCategoriesOr, applyTypeCategoryCondition 2 scenariusze, applyTypeCategoriesAnd) +Nowe testy: TransportRepositoryTest (+5: transportCostCached, findActiveByIdCached, findActiveByIdCachedNull, forPaymentMethod, forPaymentMethodEmpty) +Nowy stub: tests/stubs/ShopProduct.php (shop\Product::is_product_on_promotion, get_product_price) ``` Aktualizacja po migracji ShopProducer frontend (2026-02-17, ver. 0.291): @@ -155,6 +165,10 @@ Nowe testy dodane 2026-02-15: ```text tests/ |-- bootstrap.php +|-- stubs/ +| |-- CacheHandler.php +| |-- Helpers.php +| `-- ShopProduct.php |-- Unit/ | |-- Domain/ | | |-- Article/ArticleRepositoryTest.php diff --git a/docs/UPDATE_INSTRUCTIONS.md b/docs/UPDATE_INSTRUCTIONS.md index 2764114..e359107 100644 --- a/docs/UPDATE_INSTRUCTIONS.md +++ b/docs/UPDATE_INSTRUCTIONS.md @@ -18,16 +18,16 @@ Aktualizacje znajdują się w folderze `updates/0.XX/` gdzie XX oznacza dziesią ## Procedura tworzenia nowej aktualizacji -## Status biezacej aktualizacji (ver. 0.291) +## Status biezacej aktualizacji (ver. 0.292) -- Wersja udostepniona: `0.291` (data: 2026-02-17). +- Wersja udostepniona: `0.292` (data: 2026-02-17). - Pliki publikacyjne: - - `updates/0.20/ver_0.291.zip`, `ver_0.291_files.txt` + - `temp/update_build/ver_0.292.zip`, `ver_0.292_files.txt` - Pliki metadanych aktualizacji: - - `updates/changelog.php` (dodany wpis `ver. 0.291`) - - `updates/versions.php` (`$current_ver = 291`) + - `updates/changelog.php` (dodany wpis `ver. 0.292`) + - `updates/versions.php` (`$current_ver = 292`) - Weryfikacja testow przed publikacja: - - `OK (573 tests, 1738 assertions)` + - `OK (610 tests, 1816 assertions)` ### 1. Określ numer wersji Sprawdź ostatnią wersję w `updates/` i zwiększ o 1. diff --git a/index.php b/index.php index 5281b74..7148420 100644 --- a/index.php +++ b/index.php @@ -227,7 +227,7 @@ if ( $settings[ 'piksel' ] ) });'; if ( \Shared\Helpers\Helpers::get( 'product' ) ) - $piskel_code .= PHP_EOL . 'fbq( "track", "ViewContent", { content_category: "produkt", content_name: "' . htmlspecialchars( str_replace( '"', '', \front\factory\ShopProduct::product_name( \Shared\Helpers\Helpers::get( 'product' ) ) ) ) . '", content_ids: ["' . \Shared\Helpers\Helpers::get( 'product' ) . '"], content_type: "product" });'; + $piskel_code .= PHP_EOL . 'fbq( "track", "ViewContent", { content_category: "produkt", content_name: "' . htmlspecialchars( str_replace( '"', '', ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->getProductNameCached( (int)\Shared\Helpers\Helpers::get( 'product' ), $lang_id ) ) ) . '", content_ids: ["' . \Shared\Helpers\Helpers::get( 'product' ) . '"], content_type: "product" });'; $element -> nodeValue = $piskel_code; @@ -243,7 +243,7 @@ if ( \Shared\Helpers\Helpers::get( 'product' ) ) $head = $dom -> getElementsByTagName( 'head' ) -> item( 0 ); - $product = \front\factory\ShopProduct::product_details( \Shared\Helpers\Helpers::get( 'product' ), $lang_id ); + $product = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->productDetailsFrontCached( (int)\Shared\Helpers\Helpers::get( 'product' ), $lang_id ); $product_image = $product[ 'images' ][ 0 ][ 'src' ]; if ( $product_image and file_exists( substr( $product_image, 1, strlen( $product_image ) ) ) ) { diff --git a/temp/update_build/ver_0.292.zip b/temp/update_build/ver_0.292.zip new file mode 100644 index 0000000000000000000000000000000000000000..e9921100d6345c17498aed25e54bb3063fd3c944 GIT binary patch literal 80982 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ǶOxKr|(i`LG*nH6|> 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[U|_I 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

1hf8>5vL`w<@Ib8CZQS1bIY2;wd>$K%p`V7& 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

defaultLanguage() and $url != '#') $url = '/' . \Shared\Helpers\Helpers::get_session('current-lang') . $url; diff --git a/templates/shop-basket/basket-payments-methods.php b/templates/shop-basket/basket-payments-methods.php index 7dd8187..a262f8a 100644 --- a/templates/shop-basket/basket-payments-methods.php +++ b/templates/shop-basket/basket-payments-methods.php @@ -8,7 +8,7 @@ $basket = \Shared\Helpers\Helpers::get_session( 'basket' ); $coupon = \Shared\Helpers\Helpers::get_session( 'coupon' ); - $transport_cost = \front\factory\ShopTransport::transport_cost( \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' ) ); + $transport_cost = ( new \Domain\Transport\TransportRepository( $GLOBALS['mdb'] ) )->transportCostCached( \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' ) ); $basket_summary = \Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon ) + $transport_cost; ?> diff --git a/templates/shop-order/order-details.php b/templates/shop-order/order-details.php index a163b99..18e10e8 100644 --- a/templates/shop-order/order-details.php +++ b/templates/shop-order/order-details.php @@ -1,4 +1,4 @@ - settings['ssl'] ? $base = 'https' : $base = 'http';?> + settings['ssl'] ? $base = 'https' : $base = 'http'; $paymentRepo = new \Domain\PaymentMethod\PaymentMethodRepository( $GLOBALS['mdb'] );?>
: order['number'];?> @@ -22,7 +22,7 @@ order['payment_method_id'] == 1 or $this -> order['payment_method_id'] == 3 ):?>
Co prawda nie wybrałeś żadnej z dostępnych form "szybkich płatności", ale w każdej chwili możesz z nich skorzystać.
- order['payment_method_id'] == 2 or $this -> order['payment_method_id'] == 1 or $this -> order['payment_method_id'] == 3 ) and \front\factory\ShopPaymentMethod::is_payment_active( 2 ) ):?> + order['payment_method_id'] == 2 or $this -> order['payment_method_id'] == 1 or $this -> order['payment_method_id'] == 3 ) and $paymentRepo->isActive( 2 ) ):?>
@@ -43,7 +43,7 @@
- order['payment_method_id'] == 6 and \front\factory\ShopPaymentMethod::is_payment_active( 6 ) ):?> + order['payment_method_id'] == 6 and $paymentRepo->isActive( 6 ) ):?> order['payment_method_id'] == 6 ):?>
lub skorzystaj z płatności online
- order['payment_method_id'] == 6 or $this -> order['payment_method_id'] == 4 or $this -> order['payment_method_id'] == 1 or $this -> order['payment_method_id'] == 3 ) and \front\factory\ShopPaymentMethod::is_payment_active( 4 ) ):?> + order['payment_method_id'] == 6 or $this -> order['payment_method_id'] == 4 or $this -> order['payment_method_id'] == 1 or $this -> order['payment_method_id'] == 3 ) and $paymentRepo->isActive( 4 ) ):?> order['payment_method_id'] == 6 or $this -> order['payment_method_id'] == 5 or $this -> order['payment_method_id'] == 1 or $this -> order['payment_method_id'] == 3 ) and \front\factory\ShopPaymentMethod::is_payment_active( 5 ) ): + if ( ( $this -> order['payment_method_id'] == 6 or $this -> order['payment_method_id'] == 5 or $this -> order['payment_method_id'] == 1 or $this -> order['payment_method_id'] == 3 ) and $paymentRepo->isActive( 5 ) ): $url = 'https://secure.tpay.com'; diff --git a/templates/shop-product/product.php b/templates/shop-product/product.php index 96b932e..1fc2591 100644 --- a/templates/shop-product/product.php +++ b/templates/shop-product/product.php @@ -55,7 +55,7 @@ product -> price_brutto );?>
- Najniższa cena w ciągu ostatnich 30 dni: product['id'], $this -> product -> price_brutto_promo ) );?> zł + Najniższa cena w ciągu ostatnich 30 dni: getMinimalPriceCached( (int)$this -> product['id'], $this -> product -> price_brutto_promo ) );?> zł
product -> weight > 0 && $this -> product -> product_unit_id ):?>
diff --git a/tests/Unit/Domain/PaymentMethod/PaymentMethodRepositoryTest.php b/tests/Unit/Domain/PaymentMethod/PaymentMethodRepositoryTest.php index 6df4a40..6638874 100644 --- a/tests/Unit/Domain/PaymentMethod/PaymentMethodRepositoryTest.php +++ b/tests/Unit/Domain/PaymentMethod/PaymentMethodRepositoryTest.php @@ -301,6 +301,92 @@ class PaymentMethodRepositoryTest extends TestCase $this->assertSame('BANK_TRANSFER', $repository->getApiloPaymentTypeId(3)); } + public function testIsActiveReturnsOneForActivePayment(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_payment_methods', 'status', ['id' => 2]) + ->willReturn('1'); + + $repository = new PaymentMethodRepository($mockDb); + $this->assertSame(1, $repository->isActive(2)); + } + + public function testIsActiveReturnsZeroForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repository = new PaymentMethodRepository($mockDb); + $this->assertSame(0, $repository->isActive(0)); + } + + public function testFindActiveByIdReturnsNormalizedData(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_payment_methods', '*', [ + 'AND' => [ + 'id' => 3, + 'status' => 1, + ], + ]) + ->willReturn([ + 'id' => '3', + 'name' => ' Przelew ', + 'description' => 'Opis', + 'status' => '1', + 'apilo_payment_type_id' => '5', + ]); + + $repository = new PaymentMethodRepository($mockDb); + $result = $repository->findActiveById(3); + + $this->assertIsArray($result); + $this->assertSame(3, $result['id']); + $this->assertSame('Przelew', $result['name']); + $this->assertSame(1, $result['status']); + $this->assertSame(5, $result['apilo_payment_type_id']); + } + + public function testFindActiveByIdReturnsNullForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repository = new PaymentMethodRepository($mockDb); + $this->assertNull($repository->findActiveById(0)); + } + + public function testAllActiveReturnsEmptyOnNull(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('select')->willReturn(null); + + $repository = new PaymentMethodRepository($mockDb); + $this->assertSame([], $repository->allActive()); + } + + public function testGetApiloPaymentTypeIdReturnsNullForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repository = new PaymentMethodRepository($mockDb); + $this->assertNull($repository->getApiloPaymentTypeId(0)); + } + + public function testForTransportReturnsEmptyForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('query'); + + $repository = new PaymentMethodRepository($mockDb); + $this->assertSame([], $repository->forTransport(0)); + } + public function testForTransportReturnsRows(): void { $mockDb = $this->createMock(\medoo::class); diff --git a/tests/Unit/Domain/Product/ProductRepositoryTest.php b/tests/Unit/Domain/Product/ProductRepositoryTest.php index 24e62eb..258abca 100644 --- a/tests/Unit/Domain/Product/ProductRepositoryTest.php +++ b/tests/Unit/Domain/Product/ProductRepositoryTest.php @@ -510,4 +510,371 @@ class ProductRepositoryTest extends TestCase $this->assertIsArray($result); $this->assertNull($result['price_brutto_promo']); } + + // ========================================================================= + // Frontend methods (migrated from front\factory\ShopProduct) + // ========================================================================= + + /** + * Test getSkuWithFallback - zwraca SKU bezpośrednio + */ + public function testGetSkuWithFallbackReturnsSku() + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_products', 'sku', ['id' => 10]) + ->willReturn('SKU-ABC'); + + $repository = new ProductRepository($mockDb); + $result = $repository->getSkuWithFallback(10); + + $this->assertEquals('SKU-ABC', $result); + } + + /** + * Test getSkuWithFallback - fallback na parent_id + */ + public function testGetSkuWithFallbackFromParent() + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get') + ->willReturnCallback(function ($table, $column, $where) { + if ($column === 'sku' && $where['id'] === 10) return null; + if ($column === 'parent_id' && $where['id'] === 10) return 5; + if ($column === 'sku' && $where['id'] === 5) return 'SKU-PARENT'; + return null; + }); + + $repository = new ProductRepository($mockDb); + $result = $repository->getSkuWithFallback(10, true); + + $this->assertEquals('SKU-PARENT', $result); + } + + /** + * Test getSkuWithFallback - zwraca null dla nieprawidłowego ID + */ + public function testGetSkuWithFallbackReturnsNullForInvalidId() + { + $mockDb = $this->createMock(\medoo::class); + $repository = new ProductRepository($mockDb); + + $this->assertNull($repository->getSkuWithFallback(0)); + $this->assertNull($repository->getSkuWithFallback(-1)); + } + + /** + * Test getEanWithFallback - zwraca EAN bezpośrednio + */ + public function testGetEanWithFallbackReturnsEan() + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_products', 'ean', ['id' => 10]) + ->willReturn('1234567890123'); + + $repository = new ProductRepository($mockDb); + $result = $repository->getEanWithFallback(10); + + $this->assertEquals('1234567890123', $result); + } + + /** + * Test getEanWithFallback - fallback na parent_id + */ + public function testGetEanWithFallbackFromParent() + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get') + ->willReturnCallback(function ($table, $column, $where) { + if ($column === 'ean' && $where['id'] === 10) return null; + if ($column === 'parent_id' && $where['id'] === 10) return 5; + if ($column === 'ean' && $where['id'] === 5) return 'EAN-PARENT'; + return null; + }); + + $repository = new ProductRepository($mockDb); + $result = $repository->getEanWithFallback(10, true); + + $this->assertEquals('EAN-PARENT', $result); + } + + /** + * Test isProductActiveCached - zwraca 1 dla aktywnego produktu + */ + public function testIsProductActiveCachedReturnsOneForActive() + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_products', 'status', ['id' => 10]) + ->willReturn(1); + + $repository = new ProductRepository($mockDb); + + $this->assertEquals(1, $repository->isProductActiveCached(10)); + } + + /** + * Test isProductActiveCached - zwraca 0 dla nieaktywnego produktu + */ + public function testIsProductActiveCachedReturnsZeroForInactive() + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get')->willReturn(0); + + $repository = new ProductRepository($mockDb); + + $this->assertEquals(0, $repository->isProductActiveCached(10)); + } + + /** + * Test isProductActiveCached - zwraca 0 dla nieprawidłowego ID + */ + public function testIsProductActiveCachedReturnsZeroForInvalidId() + { + $mockDb = $this->createMock(\medoo::class); + $repository = new ProductRepository($mockDb); + + $this->assertEquals(0, $repository->isProductActiveCached(0)); + $this->assertEquals(0, $repository->isProductActiveCached(-1)); + } + + /** + * Test productCategoriesFront - zwraca kategorie + */ + public function testProductCategoriesFrontReturnsCategories() + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get') + ->with('pp_shop_products', 'parent_id', ['id' => 10]) + ->willReturn(null); + + $mockStmt = $this->createMock(\PDOStatement::class); + $mockStmt->method('fetchAll')->willReturn([ + ['category_id' => 1], + ['category_id' => 5], + ]); + $mockDb->method('query')->willReturn($mockStmt); + + $repository = new ProductRepository($mockDb); + $result = $repository->productCategoriesFront(10); + + $this->assertIsArray($result); + $this->assertCount(2, $result); + $this->assertEquals(1, $result[0]['category_id']); + $this->assertEquals(5, $result[1]['category_id']); + } + + /** + * Test productCategoriesFront - fallback na parent_id + */ + public function testProductCategoriesFrontUsesParentId() + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get') + ->with('pp_shop_products', 'parent_id', ['id' => 10]) + ->willReturn(5); + + $mockStmt = $this->createMock(\PDOStatement::class); + $mockStmt->method('fetchAll')->willReturn([['category_id' => 3]]); + $mockDb->expects($this->once()) + ->method('query') + ->with( + $this->equalTo('SELECT category_id FROM pp_shop_products_categories WHERE product_id = :pid'), + $this->equalTo([':pid' => 5]) + ) + ->willReturn($mockStmt); + + $repository = new ProductRepository($mockDb); + $result = $repository->productCategoriesFront(10); + + $this->assertCount(1, $result); + $this->assertEquals(3, $result[0]['category_id']); + } + + /** + * Test productCategoriesFront - pusta tablica dla nieprawidłowego ID + */ + public function testProductCategoriesFrontReturnsEmptyForInvalidId() + { + $mockDb = $this->createMock(\medoo::class); + $repository = new ProductRepository($mockDb); + + $this->assertEquals([], $repository->productCategoriesFront(0)); + $this->assertEquals([], $repository->productCategoriesFront(-1)); + } + + /** + * Test getWarehouseMessageZero - zwraca wiadomość + */ + public function testGetWarehouseMessageZeroReturnsMessage() + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with( + 'pp_shop_products_langs', + 'warehouse_message_zero', + ['AND' => ['product_id' => 10, 'lang_id' => 'pl']] + ) + ->willReturn('Produkt niedostępny'); + + $repository = new ProductRepository($mockDb); + + $this->assertEquals('Produkt niedostępny', $repository->getWarehouseMessageZero(10, 'pl')); + } + + /** + * Test getWarehouseMessageZero - null dla nieprawidłowego ID + */ + public function testGetWarehouseMessageZeroReturnsNullForInvalidId() + { + $mockDb = $this->createMock(\medoo::class); + $repository = new ProductRepository($mockDb); + + $this->assertNull($repository->getWarehouseMessageZero(0, 'pl')); + } + + /** + * Test getWarehouseMessageNonzero - zwraca wiadomość + */ + public function testGetWarehouseMessageNonzeroReturnsMessage() + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with( + 'pp_shop_products_langs', + 'warehouse_message_nonzero', + ['AND' => ['product_id' => 10, 'lang_id' => 'pl']] + ) + ->willReturn('Wysyłka w 24h'); + + $repository = new ProductRepository($mockDb); + + $this->assertEquals('Wysyłka w 24h', $repository->getWarehouseMessageNonzero(10, 'pl')); + } + + /** + * Test getWarehouseMessageNonzero - null dla nieprawidłowego ID + */ + public function testGetWarehouseMessageNonzeroReturnsNullForInvalidId() + { + $mockDb = $this->createMock(\medoo::class); + $repository = new ProductRepository($mockDb); + + $this->assertNull($repository->getWarehouseMessageNonzero(0, 'pl')); + } + + /** + * Test topProductIds - zwraca ID aktywnych produktów + */ + public function testTopProductIdsReturnsActiveProducts() + { + $mockDb = $this->createMock(\medoo::class); + + $mockStmt = $this->createMock(\PDOStatement::class); + $mockStmt->method('fetchAll')->willReturn([ + ['sell_count' => 10, 'parent_product_id' => 1], + ['sell_count' => 5, 'parent_product_id' => 2], + ['sell_count' => 3, 'parent_product_id' => 3], + ]); + + $mockDb->method('query')->willReturn($mockStmt); + + // isProductActiveCached sprawdza status — produkt 2 nieaktywny + $mockDb->method('get') + ->willReturnCallback(function ($table, $column, $where) { + if ($column === 'status') { + return $where['id'] === 2 ? 0 : 1; + } + return null; + }); + + $repository = new ProductRepository($mockDb); + $result = $repository->topProductIds(6); + + $this->assertIsArray($result); + $this->assertContains(1, $result); + $this->assertNotContains(2, $result); + $this->assertContains(3, $result); + } + + /** + * Test newProductIds - zwraca ID produktów + */ + public function testNewProductIdsReturnsProductIds() + { + $mockDb = $this->createMock(\medoo::class); + + $mockStmt = $this->createMock(\PDOStatement::class); + $mockStmt->method('fetchAll')->willReturn([ + ['id' => 10], + ['id' => 20], + ['id' => 30], + ]); + $mockDb->method('query')->willReturn($mockStmt); + + $repository = new ProductRepository($mockDb); + $result = $repository->newProductIds(3); + + $this->assertIsArray($result); + $this->assertEquals([10, 20, 30], $result); + } + + /** + * Test newProductIds - pusta lista + */ + public function testNewProductIdsReturnsEmptyWhenNoProducts() + { + $mockDb = $this->createMock(\medoo::class); + + $mockStmt = $this->createMock(\PDOStatement::class); + $mockStmt->method('fetchAll')->willReturn([]); + $mockDb->method('query')->willReturn($mockStmt); + + $repository = new ProductRepository($mockDb); + + $this->assertEquals([], $repository->newProductIds(10)); + } + + /** + * Test promotedProductIdsCached - zwraca promowane produkty + */ + public function testPromotedProductIdsCachedReturnsIds() + { + $mockDb = $this->createMock(\medoo::class); + + $mockStmt = $this->createMock(\PDOStatement::class); + $mockStmt->method('fetchAll')->willReturn([ + ['id' => 5], + ['id' => 8], + ]); + $mockDb->method('query')->willReturn($mockStmt); + + $repository = new ProductRepository($mockDb); + $result = $repository->promotedProductIdsCached(2); + + $this->assertIsArray($result); + $this->assertEquals([5, 8], $result); + } + + /** + * Test promotedProductIdsCached - pusta lista + */ + public function testPromotedProductIdsCachedReturnsEmptyWhenNone() + { + $mockDb = $this->createMock(\medoo::class); + + $mockStmt = $this->createMock(\PDOStatement::class); + $mockStmt->method('fetchAll')->willReturn([]); + $mockDb->method('query')->willReturn($mockStmt); + + $repository = new ProductRepository($mockDb); + + $this->assertEquals([], $repository->promotedProductIdsCached(6)); + } } diff --git a/tests/Unit/Domain/Promotion/PromotionRepositoryTest.php b/tests/Unit/Domain/Promotion/PromotionRepositoryTest.php index 709195e..11ed56c 100644 --- a/tests/Unit/Domain/Promotion/PromotionRepositoryTest.php +++ b/tests/Unit/Domain/Promotion/PromotionRepositoryTest.php @@ -176,4 +176,202 @@ class PromotionRepositoryTest extends TestCase $this->assertCount(1, $tree[0]['subcategories']); $this->assertSame(11, (int)$tree[0]['subcategories'][0]['id']); } + + // ========================================================================= + // Frontend: basket promotion logic (migrated from front\factory\ShopPromotion) + // ========================================================================= + + private function mockPromotion(array $data): object + { + return new class($data) { + private $data; + public function __construct($data) { $this->data = $data; } + public function __get($key) { return isset($this->data[$key]) ? $this->data[$key] : null; } + }; + } + + private function makeBasket(array $items): array + { + $basket = []; + foreach ($items as $i => $item) { + $basket[$i] = array_merge(['product-id' => $item['id']], $item); + } + return $basket; + } + + /** + * Test applyTypeWholeBasket — rabat na cały koszyk + */ + public function testApplyTypeWholeBasketAppliesDiscountToAll(): void + { + $mockDb = $this->createMock(\medoo::class); + + // productCategoriesFront zwraca kategorie + $mockDb->method('get')->willReturn(null); // parent_id = null + $mockStmt = $this->createMock(\PDOStatement::class); + $mockStmt->method('fetchAll')->willReturn([['category_id' => 1]]); + $mockDb->method('query')->willReturn($mockStmt); + + $promotion = $this->mockPromotion([ + 'discount_type' => 1, + 'amount' => 10, + 'include_coupon' => 0, + 'include_product_promo' => 0, + ]); + + $basket = $this->makeBasket([ + ['id' => 1], + ['id' => 2], + ]); + + $repository = new PromotionRepository($mockDb); + $result = $repository->applyTypeWholeBasket($basket, $promotion); + + $this->assertSame(1, $result[0]['discount_type']); + $this->assertSame(10, $result[0]['discount_amount']); + $this->assertSame(1, $result[1]['discount_type']); + } + + /** + * Test applyTypeCategoriesOr — rabat na produkty z kat. 1 lub 2 + */ + public function testApplyTypeCategoriesOrAppliesDiscountToMatchingCategories(): void + { + $mockDb = $this->createMock(\medoo::class); + + $mockDb->method('get')->willReturn(null); + $mockStmt = $this->createMock(\PDOStatement::class); + $mockStmt->method('fetchAll')->willReturn([['category_id' => 5]]); + $mockDb->method('query')->willReturn($mockStmt); + + $promotion = $this->mockPromotion([ + 'categories' => json_encode([5]), + 'condition_categories' => json_encode([10]), + 'discount_type' => 1, + 'amount' => 15, + 'include_coupon' => 1, + 'include_product_promo' => 0, + ]); + + $basket = $this->makeBasket([['id' => 1]]); + + $repository = new PromotionRepository($mockDb); + $result = $repository->applyTypeCategoriesOr($basket, $promotion); + + $this->assertSame(1, $result[0]['discount_type']); + $this->assertSame(15, $result[0]['discount_amount']); + $this->assertSame(1, $result[0]['discount_include_coupon']); + } + + /** + * Test applyTypeCategoryCondition — rabat na kat. I jeśli kat. II w koszyku + */ + public function testApplyTypeCategoryConditionAppliesWhenConditionMet(): void + { + $mockDb = $this->createMock(\medoo::class); + + $callCount = 0; + $mockDb->method('get')->willReturn(null); + + $mockStmt1 = $this->createMock(\PDOStatement::class); + $mockStmt1->method('fetchAll')->willReturnOnConsecutiveCalls( + [['category_id' => 10]], // product 1 — condition category + [['category_id' => 5]], // product 2 — target category + [['category_id' => 10]], // product 1 — check for discount (not matching target) + [['category_id' => 5]] // product 2 — check for discount (matching target) + ); + $mockDb->method('query')->willReturn($mockStmt1); + + $promotion = $this->mockPromotion([ + 'categories' => json_encode([5]), + 'condition_categories' => json_encode([10]), + 'discount_type' => 1, + 'amount' => 20, + 'include_coupon' => 0, + 'include_product_promo' => 0, + ]); + + $basket = $this->makeBasket([ + ['id' => 1], + ['id' => 2], + ]); + + $repository = new PromotionRepository($mockDb); + $result = $repository->applyTypeCategoryCondition($basket, $promotion); + + // Produkt 2 (kat. 5) powinien mieć rabat + $this->assertSame(1, $result[1]['discount_type']); + $this->assertSame(20, $result[1]['discount_amount']); + } + + /** + * Test applyTypeCategoryCondition — brak rabatu gdy warunek niespełniony + */ + public function testApplyTypeCategoryConditionNoDiscountWhenConditionNotMet(): void + { + $mockDb = $this->createMock(\medoo::class); + + $mockDb->method('get')->willReturn(null); + $mockStmt = $this->createMock(\PDOStatement::class); + $mockStmt->method('fetchAll')->willReturn([['category_id' => 99]]); // nie pasuje do condition_categories + $mockDb->method('query')->willReturn($mockStmt); + + $promotion = $this->mockPromotion([ + 'categories' => json_encode([5]), + 'condition_categories' => json_encode([10]), + 'discount_type' => 1, + 'amount' => 20, + 'include_coupon' => 0, + 'include_product_promo' => 0, + ]); + + $basket = $this->makeBasket([['id' => 1]]); + + $repository = new PromotionRepository($mockDb); + $result = $repository->applyTypeCategoryCondition($basket, $promotion); + + $this->assertArrayNotHasKey('discount_type', $result[0]); + } + + /** + * Test applyTypeCategoriesAnd — rabat gdy oba warunki spełnione + */ + public function testApplyTypeCategoriesAndAppliesWhenBothConditionsMet(): void + { + $mockDb = $this->createMock(\medoo::class); + + $mockDb->method('get')->willReturn(null); + + $mockStmt = $this->createMock(\PDOStatement::class); + $mockStmt->method('fetchAll')->willReturnOnConsecutiveCalls( + [['category_id' => 10]], // product 1 — condition_categories ✓ + [['category_id' => 5]], // product 2 — categories ✓ (condition_2) + [['category_id' => 10]], // product 1 — check categories ✓ (condition check) + [['category_id' => 5]], // product 2 — check categories ✓ (condition check) + [['category_id' => 10]], // product 1 — discount assignment + [['category_id' => 5]] // product 2 — discount assignment + ); + $mockDb->method('query')->willReturn($mockStmt); + + $promotion = $this->mockPromotion([ + 'categories' => json_encode([5]), + 'condition_categories' => json_encode([10]), + 'discount_type' => 1, + 'amount' => 25, + 'include_coupon' => 1, + 'include_product_promo' => 0, + ]); + + $basket = $this->makeBasket([ + ['id' => 1], + ['id' => 2], + ]); + + $repository = new PromotionRepository($mockDb); + $result = $repository->applyTypeCategoriesAnd($basket, $promotion); + + $this->assertSame(1, $result[0]['discount_type']); + $this->assertSame(25, $result[0]['discount_amount']); + $this->assertSame(1, $result[1]['discount_type']); + } } diff --git a/tests/Unit/Domain/Transport/TransportRepositoryTest.php b/tests/Unit/Domain/Transport/TransportRepositoryTest.php index fab82a9..bf8f1ae 100644 --- a/tests/Unit/Domain/Transport/TransportRepositoryTest.php +++ b/tests/Unit/Domain/Transport/TransportRepositoryTest.php @@ -331,4 +331,93 @@ class TransportRepositoryTest extends TestCase $this->assertSame(1, $rows[1]['default']); $this->assertSame(50, $rows[1]['max_wp']); } + + // ========================================================================= + // Frontend methods (migrated from front\factory\ShopTransport) + // ========================================================================= + + /** + * Test transportCostCached — zwraca koszt transportu + */ + public function testTransportCostCachedReturnsCost(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn('15.99'); + + $repository = new TransportRepository($mockDb); + $this->assertSame(15.99, $repository->transportCostCached(1)); + } + + /** + * Test findActiveByIdCached — zwraca transport + */ + public function testFindActiveByIdCachedReturnsTransport(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn([ + 'id' => '3', 'name' => 'DPD', 'status' => '1', + 'cost' => '15.99', 'max_wp' => null, 'default' => '0', + 'delivery_free' => '1', 'apilo_carrier_account_id' => null, 'o' => '2', + ]); + + $repository = new TransportRepository($mockDb); + $result = $repository->findActiveByIdCached(3); + + $this->assertIsArray($result); + $this->assertSame(3, $result['id']); + $this->assertSame(15.99, $result['cost']); + } + + /** + * Test findActiveByIdCached — null dla nieistniejącego + */ + public function testFindActiveByIdCachedReturnsNullForInvalid(): void + { + $mockDb = $this->createMock(\medoo::class); + $repository = new TransportRepository($mockDb); + + $this->assertNull($repository->findActiveByIdCached(0)); + } + + /** + * Test forPaymentMethod — zwraca transporty powiązane z płatnością + */ + public function testForPaymentMethodReturnsTransports(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('select') + ->willReturnCallback(function ($table, $columns, $where) { + if ($table === 'pp_shop_transport_payment_methods') { + return [1, 3]; + } + if ($table === 'pp_shop_transports') { + return [ + ['id' => '1', 'name' => 'A', 'status' => '1', 'cost' => '5', 'max_wp' => null, 'default' => '0', 'delivery_free' => '0', 'apilo_carrier_account_id' => null, 'o' => '1'], + ]; + } + return []; + }); + + $repository = new TransportRepository($mockDb); + $result = $repository->forPaymentMethod(2); + + $this->assertIsArray($result); + $this->assertCount(1, $result); + $this->assertSame(1, $result[0]['id']); + } + + /** + * Test forPaymentMethod — pusta tablica dla nieprawidłowego ID + */ + public function testForPaymentMethodReturnsEmptyForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $repository = new TransportRepository($mockDb); + + $this->assertEquals([], $repository->forPaymentMethod(0)); + } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 44915d8..e626638 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -75,3 +75,7 @@ if (!class_exists('Shared\\Cache\\CacheHandler')) { } class_alias('CacheHandler', 'Shared\\Cache\\CacheHandler'); } + +if (!class_exists('shop\\Product')) { + require_once __DIR__ . '/stubs/ShopProduct.php'; +} diff --git a/tests/stubs/ShopProduct.php b/tests/stubs/ShopProduct.php new file mode 100644 index 0000000..87738ca --- /dev/null +++ b/tests/stubs/ShopProduct.php @@ -0,0 +1,18 @@ +ver. 0.292 - 17.02.2026
+- UPDATE - pelna migracja front\factory\ do Domain (5 ostatnich klas: ShopProduct, ShopPaymentMethod, ShopPromotion, ShopStatuses, ShopTransport) +- UPDATE - ProductRepository: ~20 nowych metod frontendowych (cache Redis, lazy loading, SKU/EAN fallback) +- UPDATE - PromotionRepository: 5 metod aplikowania promocji (applyTypeWholeBasket/CheapestProduct/CategoriesOr/CategoriesAnd/CategoryCondition) +- UPDATE - TransportRepository: 4 metody frontendowe z cache (transportMethodsFront, transportCostCached, findActiveByIdCached, forPaymentMethod) +- UPDATE - PaymentMethodRepository: metody frontendowe z Redis cache +- CLEANUP - usuniety caly folder front\factory\ (20 klas zmigrowanych) + 4 inne klasy legacy +- FIX - broken transports_list() w ajax.php zastapiony nowa metoda forPaymentMethod() +
ver. 0.291 - 17.02.2026
- UPDATE - migracja front\controls\ShopProducer + shop\Producer do Domain\Producer\ProducerRepository + front\Controllers\ShopProducerController - FIX - bug shop\Producer::__get() referowal nieistniejace $this->data diff --git a/updates/versions.php b/updates/versions.php index ec85d38..07c8c55 100644 --- a/updates/versions.php +++ b/updates/versions.php @@ -1,5 +1,5 @@