From 35dd0de261a4497c01cd6eb54ed0d7032cea5f8b Mon Sep 17 00:00:00 2001 From: Nikita Kiselev Date: Thu, 25 Sep 2025 19:06:15 +0300 Subject: [PATCH] feat(products): show correct product prices --- .../Exceptions/EntityNotFoundException.php | 14 + .../Decorators/OcRegistryDecorator.php | 6 + .../Adapters/OcModelCatalogProductAdapter.php | 9 + .../src/Handlers/ProductsHandler.php | 17 +- .../src/Services/ProductsService.php | 291 +++++++++++------- .../upload/oc_telegram_shop/src/routes.php | 2 +- spa/src/components/ProductNotFound.vue | 19 ++ .../ProductOptions/Types/OptionCheckbox.vue | 6 +- .../ProductOptions/Types/OptionRadio.vue | 4 +- .../ProductOptions/Types/OptionSelect.vue | 6 +- spa/src/components/ProductsList.vue | 8 +- spa/src/views/Product.vue | 31 +- 12 files changed, 270 insertions(+), 143 deletions(-) create mode 100644 module/oc_telegram_shop/upload/oc_telegram_shop/framework/Exceptions/EntityNotFoundException.php create mode 100644 spa/src/components/ProductNotFound.vue diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Exceptions/EntityNotFoundException.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Exceptions/EntityNotFoundException.php new file mode 100644 index 0000000..69ac54f --- /dev/null +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Exceptions/EntityNotFoundException.php @@ -0,0 +1,14 @@ +model->getProductOptions($productId); } + + /** + * @param int $productId + * @return array|false + */ + public function getProduct(int $productId) + { + return $this->model->getProduct($productId); + } } diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ProductsHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ProductsHandler.php index cec8dad..ac2a777 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ProductsHandler.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ProductsHandler.php @@ -7,8 +7,10 @@ namespace App\Handlers; use App\Services\ProductsService; use Exception; use Openguru\OpenCartFramework\Config\Settings; +use Openguru\OpenCartFramework\Exceptions\EntityNotFoundException; use Openguru\OpenCartFramework\Http\JsonResponse; use Openguru\OpenCartFramework\Http\Request; +use Openguru\OpenCartFramework\Http\Response; use Openguru\OpenCartFramework\Logger\LoggerInterface; use RuntimeException; @@ -25,7 +27,7 @@ class ProductsHandler $this->logger = $logger; } - public function handle(Request $request): JsonResponse + public function index(Request $request): JsonResponse { $page = (int) $request->get('page', 1); $perPage = min((int) $request->get('perPage', 6), 15); @@ -60,14 +62,11 @@ class ProductsHandler $imageFullHeight = 1000; try { - $product = $this->productsService->getProduct( - $productId, - $languageId, - $imageWidth, - $imageHeight, - $imageFullWidth, - $imageFullHeight - ); + $product = $this->productsService->getProductById($productId); + } catch (EntityNotFoundException $exception) { + return new JsonResponse([ + 'message' => 'Product with id ' . $productId . ' not found', + ], Response::HTTP_NOT_FOUND); } catch (Exception $exception) { $this->logger->logException($exception); throw new RuntimeException('Error get product with id ' . $productId, 500, $exception); diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php index 1fea83a..cbb1657 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php @@ -2,16 +2,17 @@ namespace App\Services; -use App\Adapters\OcModelCatalogProductAdapter; use Cart\Currency; use Cart\Tax; use Exception; use Openguru\OpenCartFramework\Config\Settings; +use Openguru\OpenCartFramework\Exceptions\EntityNotFoundException; use Openguru\OpenCartFramework\ImageTool\ImageToolInterface; use Openguru\OpenCartFramework\Logger\LoggerInterface; use Openguru\OpenCartFramework\OpenCart\Decorators\OcRegistryDecorator; use Openguru\OpenCartFramework\QueryBuilder\Builder; use Openguru\OpenCartFramework\QueryBuilder\JoinClause; +use Openguru\OpenCartFramework\QueryBuilder\RawExpression; use Openguru\OpenCartFramework\Support\Arr; use Openguru\OpenCartFramework\Support\PaginationHelper; @@ -21,7 +22,6 @@ class ProductsService private Currency $currency; private Tax $tax; private Settings $settings; - private OcModelCatalogProductAdapter $ocModelCatalogProduct; private ImageToolInterface $ocImageTool; private OcRegistryDecorator $oc; private LoggerInterface $logger; @@ -31,7 +31,6 @@ class ProductsService Currency $currency, Tax $tax, Settings $settings, - OcModelCatalogProductAdapter $ocModelCatalogProduct, ImageToolInterface $ocImageTool, OcRegistryDecorator $registry, LoggerInterface $logger @@ -40,7 +39,6 @@ class ProductsService $this->currency = $currency; $this->tax = $tax; $this->settings = $settings; - $this->ocModelCatalogProduct = $ocModelCatalogProduct; $this->ocImageTool = $ocImageTool; $this->oc = $registry; $this->logger = $logger; @@ -71,6 +69,16 @@ class ProductsService ->value('name'); } + $customerGroupId = (int)$this->oc->config->get('config_customer_group_id'); + $specialPriceSql = "(SELECT price + FROM oc_product_special ps + WHERE ps.product_id = products.product_id + AND ps.customer_group_id = $customerGroupId + AND ((ps.date_start = '0000-00-00' OR ps.date_start < NOW()) AND + (ps.date_end = '0000-00-00' OR ps.date_end > NOW())) + ORDER BY ps.priority ASC, ps.price ASC + LIMIT 1) AS special"; + $productsQuery = $this->queryBuilder->newQuery() ->select([ 'products.product_id' => 'product_id', @@ -79,6 +87,7 @@ class ProductsService 'products.price' => 'price', 'products.image' => 'product_image', 'products.tax_class_id' => 'tax_class_id', + new RawExpression($specialPriceSql), ]) ->from(db_table('product'), 'products') ->join( @@ -166,6 +175,18 @@ class ProductsService $this->settings->get('oc_default_currency'), ); + $special = false; + if ($product['special'] && (float) $product['special'] >= 0) { + $special = $this->currency->format( + $this->tax->calculate( + $product['special'], + $product['tax_class_id'], + $this->settings->get('oc_config_tax'), + ), + $this->settings->get('oc_default_currency'), + ); + } + if (! empty($productsImagesMap[$product['product_id']])) { $allImages = array_merge($allImages, $productsImagesMap[$product['product_id']]); } @@ -175,6 +196,7 @@ class ProductsService 'product_quantity' => (int) $product['product_quantity'], 'name' => html_entity_decode($product['product_name'], ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'price' => $price, + 'special' => $special, 'images' => $allImages, ]; }, $products), @@ -187,74 +209,71 @@ class ProductsService } /** + * @throws EntityNotFoundException * @throws Exception */ - public function getProduct( - int $productId, - int $languageId, - int $imageWidth, - int $imageHeight, - int $imageFullWidth, - int $imageFullHeight - ): array { - $product = $this->queryBuilder->newQuery() - ->select([ - 'products.product_id' => 'product_id', - 'product_description.name' => 'product_name', - 'product_description.description' => 'product_description', - 'products.price' => 'price', - 'products.minimum' => 'minimum', - 'products.quantity' => 'quantity', - 'products.image' => 'product_image', - 'products.tax_class_id' => 'tax_class_id', - 'manufacturer.name' => 'product_manufacturer', - ]) - ->from(db_table('product'), 'products') - ->join( - db_table('product_description') . ' AS product_description', - function (JoinClause $join) use ($languageId) { - $join->on('products.product_id', '=', 'product_description.product_id') - ->where('product_description.language_id', '=', $languageId); - } - ) - ->leftJoin( - db_table('manufacturer') . ' AS manufacturer', - function (JoinClause $join) { - $join->on('products.manufacturer_id', '=', 'manufacturer.manufacturer_id'); - } - ) - ->where('products.product_id', '=', $productId) - ->limit(1) - ->firstOrNull(); + public function getProductById(int $productId): array + { + $this->oc->load->language('product/product'); + $this->oc->load->model('catalog/category'); + $this->oc->load->model('catalog/manufacturer'); + $this->oc->load->model('catalog/product'); + $this->oc->load->model('catalog/review'); + $this->oc->load->model('tool/image'); - if (! $product) { - return []; + $imageThumbWidth = 500; + $imageThumbHeight = 500; + $imageFullWidth = 1000; + $imageFullHeight = 1000; + $configTax = $this->oc->config->get('config_tax'); + + $product_info = $this->oc->model_catalog_product->getProduct($productId); + + if (! $product_info) { + throw new EntityNotFoundException('Product with id ' . $productId . ' not found'); } - $productsImages = $this->queryBuilder->newQuery() - ->select([ - 'products_images.product_id' => 'product_id', - 'products_images.image' => 'image', - ]) - ->from(db_table('product_image'), 'products_images') - ->orderBy('products_images.sort_order') - ->where('products_images.product_id', '=', $productId) - ->get(); + $data = []; + $data['text_minimum'] = sprintf($this->oc->language->get('text_minimum'), $product_info['minimum']); - $imagePaths = []; - if ($product['product_image']) { - $imagePaths[] = $product['product_image']; + $data['tab_review'] = sprintf($this->oc->language->get('tab_review'), $product_info['reviews']); + + $data['product_id'] = $productId; + $data['name'] = $product_info['name']; + $data['manufacturer'] = $product_info['manufacturer']; + $data['model'] = $product_info['model']; + $data['reward'] = $product_info['reward']; + $data['points'] = (int) $product_info['points']; + $data['description'] = html_entity_decode($product_info['description'], ENT_QUOTES, 'UTF-8'); + + if ($product_info['quantity'] <= 0) { + $data['stock'] = $product_info['stock_status']; + } elseif ($this->oc->config->get('config_stock_display')) { + $data['stock'] = $product_info['quantity']; + } else { + $data['stock'] = $this->oc->language->get('text_instock'); } - foreach ($productsImages as $item) { - $imagePaths[] = $item['image']; + + $allImages = []; + if ($product_info['image']) { + $allImages[] = $product_info['image']; + } + $results = $this->oc->model_catalog_product->getProductImages($productId); + foreach ($results as $result) { + $allImages[] = $result['image']; } $images = []; - foreach ($imagePaths as $imagePath) { + foreach ($allImages as $imagePath) { try { [$width, $height] = $this->ocImageTool->getRealSize($imagePath); $images[] = [ - 'thumbnailURL' => $this->ocImageTool->resize($imagePath, $imageWidth, $imageHeight, 'placeholder.png'), + 'thumbnailURL' => $this->ocImageTool->resize( + $imagePath, + $imageThumbWidth, + $imageThumbHeight, + 'placeholder.png' + ), 'largeURL' => $this->ocImageTool->resize( $imagePath, $imageFullWidth, @@ -263,96 +282,136 @@ class ProductsService ), 'width' => $width, 'height' => $height, - 'alt' => html_entity_decode($product['product_name'], ENT_QUOTES | ENT_HTML5, 'UTF-8'), + 'alt' => html_entity_decode($product_info['name'], ENT_QUOTES | ENT_HTML5, 'UTF-8'), ]; } catch (Exception $e) { $this->logger->logException($e); } } - $price = $this->currency->format( + $data['images'] = $images; + + $data['price'] = $this->currency->format( $this->tax->calculate( - $product['price'], - $product['tax_class_id'], - $this->settings->get('oc_config_tax'), + $product_info['price'], + $product_info['tax_class_id'], + $configTax, ), - $this->settings->get('oc_default_currency'), + $this->oc->session->data['currency'] ); - return [ - 'id' => $product['product_id'], - 'name' => html_entity_decode($product['product_name'], ENT_QUOTES | ENT_HTML5, 'UTF-8'), - 'description' => html_entity_decode($product['product_description'], ENT_QUOTES | ENT_HTML5, 'UTF-8'), - 'manufacturer' => html_entity_decode($product['product_manufacturer'], ENT_QUOTES | ENT_HTML5, 'UTF-8'), - 'price' => $price, - 'minimum' => $product['minimum'], - 'quantity' => $product['quantity'], - 'images' => $images, - 'options' => $this->loadProductOptions($product), - 'attributes' => $this->loadProductAttributes($product['product_id']), - ]; - } + if (! is_null($product_info['special']) && (float) $product_info['special'] >= 0) { + $data['special'] = $this->currency->format( + $this->tax->calculate( + $product_info['special'], + $product_info['tax_class_id'], + $configTax, + ), + $this->oc->session->data['currency'] + ); + $tax_price = (float) $product_info['special']; + } else { + $data['special'] = false; + $tax_price = (float) $product_info['price']; + } - private function loadProductOptions($product): array - { - $result = []; - $productId = $product['product_id']; - $taxClassId = $product['tax_class_id']; + if ($configTax) { + $data['tax'] = $this->currency->format($tax_price, $this->oc->session->data['currency']); + } else { + $data['tax'] = false; + } - $options = $this->ocModelCatalogProduct->getProductOptions($productId); - $ocConfigTax = $this->settings->get('oc_config_tax'); - $ocDefaultCurrency = $this->settings->get('oc_default_currency'); + $discounts = $this->oc->model_catalog_product->getProductDiscounts($productId); - foreach ($options as $option) { + $data['discounts'] = []; + + foreach ($discounts as $discount) { + $data['discounts'][] = array( + 'quantity' => $discount['quantity'], + 'price' => $this->currency->format( + $this->tax->calculate( + $discount['price'], + $product_info['tax_class_id'], + $configTax, + ), + $this->oc->session->data['currency'] + ) + ); + } + + $data['options'] = []; + + foreach ($this->oc->model_catalog_product->getProductOptions($productId) as $option) { $product_option_value_data = []; foreach ($option['product_option_value'] as $option_value) { if (! $option_value['subtract'] || ($option_value['quantity'] > 0)) { - if ((float) $option_value['price']) { - $priceWithTax = $this->tax->calculate( + $price = $this->currency->format( + $this->tax->calculate( $option_value['price'], - $taxClassId, - $ocConfigTax ? 'Р' : false, - ); + $product_info['tax_class_id'], + $configTax ? 'P' : false + ), + $this->oc->session->data['currency'] + ); - $price = $this->currency->format($priceWithTax, $ocDefaultCurrency); - } else { - $price = false; - } - - $product_option_value_data[] = [ - 'product_option_value_id' => (int) $option_value['product_option_value_id'], - 'option_value_id' => (int) $option_value['option_value_id'], + $product_option_value_data[] = array( + 'product_option_value_id' => $option_value['product_option_value_id'], + 'option_value_id' => $option_value['option_value_id'], 'name' => $option_value['name'], - 'image' => $this->ocImageTool->resize($option_value['image'], 50, 50), + 'image' => $this->oc->model_tool_image->resize($option_value['image'], 50, 50), 'price' => $price, 'price_prefix' => $option_value['price_prefix'], 'selected' => false, - ]; + ); } } - $result[] = [ - 'product_option_id' => (int) $option['product_option_id'], - 'values' => $product_option_value_data, - 'option_id' => (int) $option['option_id'], + $data['options'][] = array( + 'product_option_id' => $option['product_option_id'], + 'product_option_value' => $product_option_value_data, + 'option_id' => $option['option_id'], 'name' => $option['name'], 'type' => $option['type'], 'value' => $option['value'], 'required' => filter_var($option['required'], FILTER_VALIDATE_BOOLEAN), - ]; + ); } - return $result; - } + if ($product_info['minimum']) { + $data['minimum'] = (int) $product_info['minimum']; + } else { + $data['minimum'] = 1; + } - /** - * @throws Exception - */ - private function loadProductAttributes(int $productId): array - { - $this->oc->load->model('catalog/product'); + $data['review_status'] = $this->oc->config->get('config_review_status'); - return $this->oc->model_catalog_product->getProductAttributes($productId); + $data['review_guest'] = true; + + $data['customer_name'] = 'John Doe'; + + $data['reviews'] = sprintf($this->oc->language->get('text_reviews'), (int) $product_info['reviews']); + $data['rating'] = (int) $product_info['rating']; + + $data['attribute_groups'] = $this->oc->model_catalog_product->getProductAttributes($productId); + + $data['tags'] = array(); + + if ($product_info['tag']) { + $tags = explode(',', $product_info['tag']); + + foreach ($tags as $tag) { + $data['tags'][] = array( + 'tag' => trim($tag), + 'href' => $this->oc->url->link('product/search', 'tag=' . trim($tag)) + ); + } + } + + $data['recurrings'] = $this->oc->model_catalog_product->getProfiles($productId); + + $this->oc->model_catalog_product->updateViewed($productId); + + return $data; } } \ No newline at end of file diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/routes.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/routes.php index 57a1998..bac7257 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/routes.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/routes.php @@ -8,7 +8,7 @@ use App\Handlers\SettingsHandler; use App\Handlers\TelegramHandler; return [ - 'products' => [ProductsHandler::class, 'handle'], + 'products' => [ProductsHandler::class, 'index'], 'product_show' => [ProductsHandler::class, 'show'], 'storeOrder' => [OrderHandler::class, 'store'], diff --git a/spa/src/components/ProductNotFound.vue b/spa/src/components/ProductNotFound.vue new file mode 100644 index 0000000..8ce174f --- /dev/null +++ b/spa/src/components/ProductNotFound.vue @@ -0,0 +1,19 @@ + + + diff --git a/spa/src/components/ProductOptions/Types/OptionCheckbox.vue b/spa/src/components/ProductOptions/Types/OptionCheckbox.vue index abe460d..1105d30 100644 --- a/spa/src/components/ProductOptions/Types/OptionCheckbox.vue +++ b/spa/src/components/ProductOptions/Types/OptionCheckbox.vue @@ -3,7 +3,7 @@