diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/OpenCart/Decorators/OcRegistryDecorator.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/OpenCart/Decorators/OcRegistryDecorator.php index 5f089aa..97eef08 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/OpenCart/Decorators/OcRegistryDecorator.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/OpenCart/Decorators/OcRegistryDecorator.php @@ -7,6 +7,7 @@ use Cart\Currency; use Config; use Language; use Loader; +use ModelCatalogCategory; use ModelCatalogProduct; use ModelDesignBanner; use ModelSettingSetting; @@ -27,6 +28,7 @@ use Url; * @property ModelCatalogProduct $model_catalog_product * @property ModelSettingSetting $model_setting_setting * @property ModelDesignBanner $model_design_banner + * @property ModelCatalogCategory $model_catalog_category */ class OcRegistryDecorator { 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 d964774..9cf5b38 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 @@ -55,7 +55,7 @@ class ProductsHandler ], Response::HTTP_NOT_FOUND); } catch (Exception $exception) { $this->logger->logException($exception); - throw new RuntimeException('Error get product with id ' . $productId, 500, $exception); + throw new RuntimeException('Error get product with id ' . $productId, 500); } return new JsonResponse([ diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/SettingsHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/SettingsHandler.php index 52b2be1..4e2e0bf 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/SettingsHandler.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/SettingsHandler.php @@ -56,6 +56,7 @@ class SettingsHandler 'store_enabled' => $this->settings->get('store_enabled'), 'feature_coupons' => $this->settings->get('feature_coupons') ?? false, 'feature_vouchers' => $this->settings->get('feature_vouchers') ?? false, + 'currency_code' => $this->settings->get('oc_default_currency', 'RUB'), ]); } diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderCreateService.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderCreateService.php index 512ea12..65a7dc3 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderCreateService.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderCreateService.php @@ -195,6 +195,7 @@ class OrderCreateService 'total' => $orderData['total'], 'final_total_numeric' => $orderData['total_numeric'], 'currency' => $currencyCode, + 'products' => $products, ]; } 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 fcfce52..7a421c4 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 @@ -15,6 +15,7 @@ use Openguru\OpenCartFramework\OpenCart\Decorators\OcRegistryDecorator; use Openguru\OpenCartFramework\QueryBuilder\Builder; use Openguru\OpenCartFramework\QueryBuilder\JoinClause; use Openguru\OpenCartFramework\QueryBuilder\RawExpression; +use Openguru\OpenCartFramework\QueryBuilder\Table; use Openguru\OpenCartFramework\Support\Arr; use Openguru\OpenCartFramework\Support\PaginationHelper; @@ -61,6 +62,8 @@ class ProductsService $filters = $params['filters'] ?? []; $customerGroupId = (int) $this->settings->get('oc_customer_group_id'); + $currency = $this->settings->get('oc_default_currency'); + $specialPriceSql = "(SELECT price FROM oc_product_special ps WHERE ps.product_id = products.product_id @@ -78,6 +81,8 @@ class ProductsService 'products.price' => 'price', 'products.image' => 'product_image', 'products.tax_class_id' => 'tax_class_id', + 'manufacturer.name' => 'manufacturer_name', + 'category_description.name' => 'category_name', new RawExpression($specialPriceSql), ]) ->from(db_table('product'), 'products') @@ -88,6 +93,20 @@ class ProductsService ->where('product_description.language_id', '=', $languageId); } ) + ->leftJoin(new Table(db_table('manufacturer'), 'manufacturer'), function (JoinClause $join) { + $join->on('products.manufacturer_id', '=', 'manufacturer.manufacturer_id'); + }) + ->leftJoin(new Table(db_table('product_to_category'), 'product_to_category'), function (JoinClause $join) { + $join->on('products.product_id', '=', 'product_to_category.product_id') + ->where('product_to_category.main_category', '=', 1); + }) + ->leftJoin( + new Table(db_table('category_description'), 'category_description'), + function (JoinClause $join) use ($languageId) { + $join->on('product_to_category.category_id', '=', 'category_description.category_id') + ->where('category_description.language_id', '=', $languageId); + } + ) ->where('products.status', '=', 1) ->whereRaw('products.date_available < NOW()') ->when($search, function (Builder $query) use ($search) { @@ -136,7 +155,7 @@ class ProductsService } return [ - 'data' => array_map(function ($product) use ($productsImagesMap, $imageWidth, $imageHeight) { + 'data' => array_map(function ($product) use ($productsImagesMap, $imageWidth, $imageHeight, $currency) { $allImages = []; $image = $this->ocImageTool->resize( @@ -151,24 +170,24 @@ class ProductsService 'alt' => Utils::htmlEntityEncode($product['product_name']), ]; - $price = $this->currency->format( - $this->tax->calculate( - $product['price'], - $product['tax_class_id'], - $this->settings->get('oc_config_tax'), - ), - $this->settings->get('oc_default_currency'), + $priceNumeric = $this->tax->calculate( + $product['price'], + $product['tax_class_id'], + $this->settings->get('oc_config_tax'), ); + $price = $this->currency->format($priceNumeric, $currency); $special = false; + $specialPriceNumeric = null; if ($product['special'] && (float) $product['special'] >= 0) { + $specialPriceNumeric = $this->tax->calculate( + $product['special'], + $product['tax_class_id'], + $this->settings->get('oc_config_tax'), + ); $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'), + $specialPriceNumeric, + $currency, ); } @@ -183,6 +202,11 @@ class ProductsService 'price' => $price, 'special' => $special, 'images' => $allImages, + 'special_numeric' => $specialPriceNumeric, + 'price_numeric' => $priceNumeric, + 'final_price_numeric' => $specialPriceNumeric ?: $priceNumeric, + 'manufacturer_name' => $product['manufacturer_name'], + 'category_name' => $product['category_name'], ]; }, $products), @@ -400,8 +424,27 @@ class ProductsService $data['recurrings'] = $this->oc->model_catalog_product->getProfiles($productId); + $data['category'] = $this->getProductMainCategory($productId); + $data['id'] = $productId; + $this->oc->model_catalog_product->updateViewed($productId); return $data; } + + private function getProductMainCategory(int $productId): ?array + { + return $this->queryBuilder->newQuery() + ->select([ + 'category_description.category_id' => 'id', + 'category_description.name' => 'name', + ]) + ->from(db_table('category_description'), 'category_description') + ->join(new Table(db_table('product_to_category'), 'product_to_category'), function (JoinClause $join) { + $join->on('product_to_category.category_id', '=', 'category_description.category_id') + ->where('product_to_category.main_category', '=', 1); + }) + ->where('product_to_category.product_id', '=', $productId) + ->firstOrNull(); + } } \ No newline at end of file diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Support/Utils.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Support/Utils.php old mode 100644 new mode 100755 diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/tests/Unit/RequestTest.php b/module/oc_telegram_shop/upload/oc_telegram_shop/tests/Unit/RequestTest.php old mode 100644 new mode 100755 diff --git a/spa/src/components/ProductsList.vue b/spa/src/components/ProductsList.vue index 6da8712..d88181a 100644 --- a/spa/src/components/ProductsList.vue +++ b/spa/src/components/ProductsList.vue @@ -7,11 +7,11 @@ class="products-grid grid grid-cols-2 gap-x-5 gap-y-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8" >

{{ product.name }}

@@ -54,7 +54,9 @@ import ProductImageSwiper from "@/components/ProductImageSwiper.vue"; import {useSettingsStore} from "@/stores/SettingsStore.js"; import {ref} from "vue"; import {useIntersectionObserver} from '@vueuse/core'; +import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js"; +const yaMetrika = useYaMetrikaStore(); const settings = useSettingsStore(); const bottom = ref(null); @@ -87,8 +89,26 @@ const props = defineProps({ } }); -function haptic() { +function productClick(product, index) { window.Telegram.WebApp.HapticFeedback.selectionChanged(); + yaMetrika.dataLayerPush({ + "ecommerce": { + "currencyCode": settings.currency_code, + "click": { + "products": [ + { + "id": product.id, + "name": product.name, + "price": product.final_price_numeric, + "brand": product.manufacturer_name, + "category": product.category_name, + "list": "Главная страница", + "position": index, + } + ] + } + } + }); } useIntersectionObserver( diff --git a/spa/src/constants/yaMetrikaGoals.js b/spa/src/constants/yaMetrikaGoals.js index cdc557e..acb2426 100644 --- a/spa/src/constants/yaMetrikaGoals.js +++ b/spa/src/constants/yaMetrikaGoals.js @@ -1,5 +1,6 @@ export const YA_METRIKA_GOAL = { ADD_TO_CART: 'add_to_cart', + PRODUCT_OPEN_EXTERNAL: 'product_open_external', CREATE_ORDER: 'create_order', ORDER_CREATED_SUCCESS: 'order_created_success', VIEW_PRODUCT: 'view_product', diff --git a/spa/src/stores/CartStore.js b/spa/src/stores/CartStore.js index c390136..bb33e80 100644 --- a/spa/src/stores/CartStore.js +++ b/spa/src/stores/CartStore.js @@ -1,6 +1,8 @@ import {defineStore} from "pinia"; import {isNotEmpty} from "@/helpers.js"; import {addToCart, cartEditItem, cartRemoveItem, getCart, setCoupon, setVoucher} from "@/utils/ftch.js"; +import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js"; +import {useSettingsStore} from "@/stores/SettingsStore.js"; export const useCartStore = defineStore('cart', { state: () => ({ @@ -79,12 +81,27 @@ export const useCartStore = defineStore('cart', { } }, - async removeItem(rowId) { + async removeItem(cartItem, rowId, index = 0) { try { this.isLoading = true; const formData = new FormData(); formData.append('key', rowId); await cartRemoveItem(formData); + useYaMetrikaStore().dataLayerPush({ + "ecommerce": { + "currencyCode": useSettingsStore().currency_code, + "remove": { + "products": [ + { + "id": cartItem.product_id, + "name": cartItem.name, + "quantity": cartItem.quantity, + "position": index + } + ] + } + } + }); await this.getProducts(); } catch (error) { console.error(error); diff --git a/spa/src/stores/CheckoutStore.js b/spa/src/stores/CheckoutStore.js index 13a7a5c..a139c6b 100644 --- a/spa/src/stores/CheckoutStore.js +++ b/spa/src/stores/CheckoutStore.js @@ -2,6 +2,9 @@ import {defineStore} from "pinia"; import {isNotEmpty} from "@/helpers.js"; import {storeOrder} from "@/utils/ftch.js"; import {useCartStore} from "@/stores/CartStore.js"; +import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js"; +import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js"; +import {useSettingsStore} from "@/stores/SettingsStore.js"; export const useCheckoutStore = defineStore('checkout', { state: () => ({ @@ -56,6 +59,37 @@ export const useCheckoutStore = defineStore('checkout', { const response = await storeOrder(this.customer); this.order = response.data; + if (! this.order.id) { + console.debug(response.data); + throw new Error('Ошибка создания заказа.'); + } + + const yaMetrika = useYaMetrikaStore(); + yaMetrika.reachGoal(YA_METRIKA_GOAL.ORDER_CREATED_SUCCESS, { + price: this.order?.final_total_numeric, + currency: this.order?.currency, + }); + yaMetrika.dataLayerPush({ + "ecommerce": { + "currencyCode": useSettingsStore().currency_code, + "purchase": { + "actionField": { + "id": this.order.id, + 'revenue': this.order?.final_total_numeric, + }, + "products": this.order.products ? this.order.products.map((product, index) => { + return { + id: product.product_id, + name: product.name, + price: product.total_numeric, + position: index, + quantity: product.quantif, + }; + }) : [], + } + } + }); + await window.Telegram.WebApp.HapticFeedback.notificationOccurred('success'); await useCartStore().getProducts(); } catch (error) { diff --git a/spa/src/stores/SettingsStore.js b/spa/src/stores/SettingsStore.js index fbe4fbd..9c504f2 100644 --- a/spa/src/stores/SettingsStore.js +++ b/spa/src/stores/SettingsStore.js @@ -17,6 +17,7 @@ export const useSettingsStore = defineStore('settings', { ya_metrika_enabled: false, feature_coupons: false, feature_vouchers: false, + currency_code: null, theme: { light: 'light', dark: 'dark', variables: { '--product_list_title_max_lines': 2, @@ -44,6 +45,7 @@ export const useSettingsStore = defineStore('settings', { this.store_enabled = settings.store_enabled; this.feature_coupons = settings.feature_coupons; this.feature_vouchers = settings.feature_vouchers; + this.currency_code = settings.currency_code; } } }); diff --git a/spa/src/stores/yaMetrikaStore.js b/spa/src/stores/yaMetrikaStore.js index 5fe2074..84bd927 100644 --- a/spa/src/stores/yaMetrikaStore.js +++ b/spa/src/stores/yaMetrikaStore.js @@ -11,8 +11,7 @@ export const useYaMetrikaStore = defineStore('ya_metrika', { actions: { pushHit(url, params = {}) { - const settings = useSettingsStore(); - if (!settings.ya_metrika_enabled) { + if (!useSettingsStore().ya_metrika_enabled) { console.debug('[ym] Yandex Metrika disabled in settings.'); return; } @@ -39,8 +38,7 @@ export const useYaMetrikaStore = defineStore('ya_metrika', { }, reachGoal(target, params = {}) { - const settings = useSettingsStore(); - if (!settings.ya_metrika_enabled) { + if (!useSettingsStore().ya_metrika_enabled) { console.debug('[ym] Yandex Metrika disabled in settings.'); return; } @@ -61,6 +59,11 @@ export const useYaMetrikaStore = defineStore('ya_metrika', { }, initUserParams() { + if (!useSettingsStore().ya_metrika_enabled) { + console.debug('[ym] Yandex Metrika disabled in settings.'); + return; + } + if (typeof window.ym === 'function' && window.YA_METRIKA_ID !== undefined) { let tgID = null; @@ -95,6 +98,9 @@ export const useYaMetrikaStore = defineStore('ya_metrika', { window.ym(window.YA_METRIKA_ID, item.event, item.payload.url, item.payload.params); } else if (item.event === 'reachGoal') { window.ym(window.YA_METRIKA_ID, item.event, item.payload.target, item.payload.params); + } else if (item.event === 'dataLayer') { + console.debug('[ym] queue dataLayer push: ', item.payload); + window.dataLayer.push(item.payload); } else { console.error('[ym] Unsupported queue event: ', item.event); } @@ -102,5 +108,23 @@ export const useYaMetrikaStore = defineStore('ya_metrika', { console.debug('[ym] Queue processing complete. Size: ', this.queue.length); }, + + dataLayerPush(object) { + if (!useSettingsStore().ya_metrika_enabled) { + console.debug('[ym] Yandex Metrika disabled in settings.'); + return; + } + + if (Array.isArray(window.dataLayer)) { + console.debug('[ym] dataLayer push: ', object); + window.dataLayer.push(object); + } else { + console.debug('[ym] dataLayer inaccessible. Put to queue'); + this.queue.push({ + event: 'dataLayer', + payload: object, + }); + } + } }, }); diff --git a/spa/src/utils/yaMetrika.js b/spa/src/utils/yaMetrika.js index ca9cb80..509b6ee 100644 --- a/spa/src/utils/yaMetrika.js +++ b/spa/src/utils/yaMetrika.js @@ -24,6 +24,7 @@ export function injectYaMetrika() { console.debug('[Init] Detected Yandex.Metrika ID:', window.YA_METRIKA_ID); const yaMetrika = useYaMetrikaStore(); yaMetrika.initUserParams(); + window.dataLayer = window.dataLayer || []; yaMetrika.processQueue(); } } diff --git a/spa/src/views/Cart.vue b/spa/src/views/Cart.vue index 7b07744..b317092 100644 --- a/spa/src/views/Cart.vue +++ b/spa/src/views/Cart.vue @@ -21,7 +21,7 @@
-