* @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 */ namespace PrestaShop\Module\PrestashopFacebook\Provider; use Cart; use Context; use Currency; use Order; use PrestaShop\Module\PrestashopFacebook\Adapter\ConfigurationAdapter; use PrestaShop\Module\PrestashopFacebook\Adapter\ToolsAdapter; use PrestaShop\Module\PrestashopFacebook\Repository\GoogleCategoryRepository; use PrestaShop\Module\PrestashopFacebook\Repository\ProductRepository; use PrestaShop\Module\Ps_facebook\Utility\CustomerInformationUtility; use PrestaShop\Module\Ps_facebook\Utility\ProductCatalogUtility; use PrestaShopException; use Product; use Ps_facebook; class EventDataProvider { public const PRODUCT_TYPE = 'product'; public const CATEGORY_TYPE = 'product_group'; /** * @var Context */ private $context; private $locale; /** * @var ToolsAdapter */ private $toolsAdapter; /** * @var ConfigurationAdapter */ private $configurationAdapter; /** * @var ProductRepository */ private $productRepository; /** * @var ps_facebook */ private $module; /** * @var ProductAvailabilityProviderInterface */ private $availabilityProvider; /** * @var GoogleCategoryRepository */ private $googleCategoryRepository; /** * @var GoogleCategoryProvider */ private $googleCategoryProvider; public function __construct( ToolsAdapter $toolsAdapter, ConfigurationAdapter $configurationAdapter, ProductRepository $productRepository, Context $context, ps_facebook $module, ProductAvailabilityProviderInterface $availabilityProvider, GoogleCategoryRepository $googleCategoryRepository, GoogleCategoryProvider $googleCategoryProvider ) { $this->toolsAdapter = $toolsAdapter; $this->context = $context; $this->locale = \Tools::strtoupper($this->context->language->iso_code); $this->configurationAdapter = $configurationAdapter; $this->productRepository = $productRepository; $this->module = $module; $this->availabilityProvider = $availabilityProvider; $this->googleCategoryRepository = $googleCategoryRepository; $this->googleCategoryProvider = $googleCategoryProvider; } public function generateEventData($name, array $params) { switch ($name) { case 'hookDisplayHeader': if (true === \Tools::isSubmit('submitCustomizedData')) { return $this->getCustomEventData(); } if ($this->context->controller instanceof \ProductControllerCore) { return $this->getProductPageData(); } if ($this->context->controller instanceof \CategoryControllerCore) { return $this->getCategoryPageData(); } if ($this->context->controller instanceof \CmsControllerCore) { return $this->getCMSPageData(); } break; case 'hookActionSearch': return $this->getSearchEventData($params); case 'hookActionObjectCustomerMessageAddAfter': return $this->getContactEventData(); case 'hookDisplayOrderConfirmation': return $this->getOrderConfirmationEvent($params); case 'InitiateCheckout': return $this->getInitiateCheckoutEvent(); case 'hookActionCartSave': return $this->getAddToCartEventData(); case 'hookActionNewsletterRegistrationAfter': return $this->getShopSubscriptionEvent($params); case 'hookActionCustomerAccountAdd': return $this->getCompleteRegistrationEventData(); case 'customizeProduct': return $this->getCustomisationEventData($params); case 'hookActionFacebookCallPixel': return $this->getCustomEvent($params); } return false; } private function getProductPageData() { $type = 'ViewContent'; /** @var \ProductControllerCore $controller */ $controller = $this->context->controller; $product = $controller->getTemplateVarProduct(); $fbProductId = ProductCatalogUtility::makeProductId( $product['id_product'], $product['id_product_attribute'] ); $productUrl = $this->context->link->getProductLink($product['id']); $categoryPath = $this->googleCategoryProvider->getCategoryPaths( $product['id_category_default'], $this->context->language->id, $this->context->shop->id ); $content = [ 'id' => $fbProductId, 'title' => \Tools::replaceAccentedChars($product['name']), 'category' => $categoryPath['category_path'], 'item_price' => $product['price_tax_exc'], 'brand' => (new \Manufacturer($product['id_manufacturer']))->name, ]; $customData = [ 'currency' => $this->getCurrency(), 'content_ids' => [$fbProductId], 'contents' => [$content], 'content_type' => self::PRODUCT_TYPE, 'value' => $product['price_tax_exc'], ]; $category = $this->googleCategoryRepository->getGoogleCategoryIdByCategoryId( $product['id_category_default'], $this->context->shop->id ) ?: ''; $this->context->smarty->assign( [ 'retailer_item_id' => $fbProductId, 'product_availability' => $this->availabilityProvider->getProductAvailability( (int) $product['id_product'], (int) $product['id_product_attribute'] ), 'item_group_id' => $category, ] ); return [ 'custom_data' => $customData, 'event_source_url' => $productUrl, ] + $this->getCommonData($type); } private function getCategoryPageData() { $type = 'ViewCategory'; /** @var \CategoryControllerCore $controller */ $controller = $this->context->controller; $category = $controller->getCategory(); $page = $this->toolsAdapter->getValue('page'); $resultsPerPage = $this->configurationAdapter->get('PS_PRODUCTS_PER_PAGE'); $prods = $category->getProducts($this->context->language->id, $page, $resultsPerPage); $categoryUrl = $this->context->link->getCategoryLink($category->id); $breadcrumbs = $controller->getBreadcrumbLinks(); $breadcrumb = implode(' > ', array_column($breadcrumbs['links'], 'title')); $contentIds = []; if ($prods) { foreach ($prods as $product) { $contentIds[] = ProductCatalogUtility::makeProductId( $product['id_product'], $product['id_product_attribute'] ); } } $customData = [ 'content_name' => \Tools::replaceAccentedChars($category->name) . ' ' . $this->locale, 'content_category' => \Tools::replaceAccentedChars($breadcrumb), 'content_type' => self::CATEGORY_TYPE, 'content_ids' => $contentIds ?: null, ]; return [ 'custom_data' => $customData, 'event_source_url' => $categoryUrl, ] + $this->getCommonData($type); } private function getCMSPageData() { $type = 'ViewCMS'; $cms = new \CMS((int) $this->toolsAdapter->getValue('id_cms'), $this->context->language->id); /** @var \CmsControllerCore $controller */ $controller = $this->context->controller; $breadcrumbs = $controller->getBreadcrumbLinks(); $breadcrumb = implode(' > ', array_column($breadcrumbs['links'], 'title')); $customData = [ 'content_name' => \Tools::replaceAccentedChars($cms->meta_title) . ' ' . $this->locale, 'content_category' => \Tools::replaceAccentedChars($breadcrumb), 'content_type' => self::PRODUCT_TYPE, ]; return [ 'custom_data' => $customData, ] + $this->getCommonData($type); } private function getAddToCartEventData() { $action = $this->toolsAdapter->getValue('action'); $quantity = $this->toolsAdapter->getValue('qty'); $idProduct = $this->toolsAdapter->getValue('id_product'); $op = $this->toolsAdapter->getValue('op'); $isDelete = $this->toolsAdapter->getValue('delete'); $idProductAttribute = $this->toolsAdapter->getValue('id_product_attribute'); $attributeGroups = $this->toolsAdapter->getValue('group'); if ($attributeGroups) { try { $idProductAttribute = $this->productRepository->getIdProductAttributeByIdAttributes( $idProduct, $attributeGroups ); } catch (PrestaShopException $e) { return false; } } if ($action !== 'update') { return false; } $type = 'AddToCart'; if ($op) { $type = $op === 'up' ? 'IncreaseProductQuantityInCart' : 'DecreaseProductQuantityInCart'; } elseif ($isDelete) { //todo: when removing product from cart this hook gets called twice $type = 'RemoveProductFromCart'; } $productName = Product::getProductName($idProduct, $idProductAttribute); $cartId = $this->context->cookie->id_cart ?? null; $customData = [ 'content_name' => pSQL($productName), 'content_type' => self::PRODUCT_TYPE, 'content_ids' => [ ProductCatalogUtility::makeProductId( $idProduct, $idProductAttribute ), ], 'currency' => $this->getCurrency(), 'value' => $this->productRepository->getSalePrice($idProduct, $idProductAttribute, $cartId), ]; return [ 'custom_data' => $customData, ] + $this->getCommonData($type); } private function getCompleteRegistrationEventData() { $type = 'CompleteRegistration'; $customData = [ 'content_name' => 'authentication', ]; return [ 'custom_data' => $customData, ] + $this->getCommonData($type); } private function getContactEventData() { return $this->getCommonData('Contact'); } private function getCustomisationEventData($params) { $type = 'CombinationProduct'; $idLang = (int) $this->context->language->id; $productId = $this->toolsAdapter->getValue('id_product'); $attributeIds = $params['attributeIds']; $customData = $this->getCustomAttributeData($productId, $idLang, $attributeIds); return [ 'custom_data' => $customData, ] + $this->getCommonData($type); } private function getCustomEventData() { return $this->getCommonData('CustomizeProduct'); } private function getSearchEventData($params) { $searchQuery = $params['searched_query']; $quantity = $params['total']; $type = 'Search'; $customData = [ 'content_name' => 'searchQuery', 'search_string' => $searchQuery, ]; if ($quantity) { $customData['num_items'] = $quantity; } return [ 'custom_data' => $customData, ] + $this->getCommonData($type); } private function getOrderConfirmationEvent($params) { /** @var Order $order */ $order = $this->module->psVersionIs17 ? $params['order'] : $params['objOrder']; $productList = []; foreach ($order->getCartProducts() as $product) { $productList[] = ProductCatalogUtility::makeProductId( $product['id_product'], $product['id_product_attribute'] ); } $type = 'Purchase'; $customData = [ 'content_name' => 'purchased', 'order_id' => $order->id, 'currency' => $this->getCurrency(), 'content_ids' => $productList, 'content_type' => self::PRODUCT_TYPE, 'value' => (float) ($order->total_paid_tax_excl), ]; return [ 'custom_data' => $customData, ] + $this->getCommonData($type); } private function getInitiateCheckoutEvent() { $type = 'InitiateCheckout'; $cart = $this->context->cart; $contents = $this->getProductContent($cart); $numberOfItems = array_sum(array_column($contents, 'quantity')); $customData = [ 'contents' => $contents, 'content_type' => 'product', 'currency' => $this->getCurrency(), 'value' => $cart->getOrderTotal(false), 'num_items' => $numberOfItems, ]; return [ 'custom_data' => $customData, ] + $this->getCommonData($type); } private function getShopSubscriptionEvent($params) { $type = 'Subscribe'; $customData = [ 'content_name' => pSQL($params['email']), ]; return [ 'custom_data' => $customData, ] + $this->getCommonData($type); } /** * @param Cart $cart * * @return array */ private function getProductContent(Cart $cart) { $contents = []; foreach ($cart->getProducts() as $product) { $categoryPath = $this->googleCategoryProvider->getCategoryPaths( $product['id_category_default'], $this->context->language->id, $this->context->shop->id ); $content = [ 'id' => ProductCatalogUtility::makeProductId($product['id_product'], $product['id_product_attribute']), 'quantity' => $product['quantity'], 'item_price' => $product['price'], 'title' => \Tools::replaceAccentedChars($product['name']), 'brand' => (new \Manufacturer($product['id_manufacturer']))->name, 'category' => $categoryPath['category_path'], ]; $contents[] = $content; } return $contents; } /** * @param array $params * * @return array|null */ private function getCustomEvent($params) { if (!isset($params['eventName']) || !isset($params['module'])) { return null; } $type = pSQL($params['eventName']); $customData = [ 'custom_properties' => [ 'module' => pSQL($params['module']), ], ]; if (isset($params['id_product'])) { $fbProductId = ProductCatalogUtility::makeProductId( $params['id_product'], isset($params['id_product_attribute']) ? $params['id_product_attribute'] : 0 ); $customData['content_ids']['module'] = $fbProductId; } return [ 'custom_data' => $customData, ] + $this->getCommonData($type); } /** * @param int $productId * @param int $idLang * @param int[] $attributeIds * * @return array * * @throws \PrestaShopException */ private function getCustomAttributeData($productId, $idLang, $attributeIds) { $attributes = []; foreach ($attributeIds as $attributeId) { if (class_exists('\ProductAttribute')) { $attributes[] = (new \ProductAttribute($attributeId, $idLang))->name; } elseif (class_exists('\AttributeCore')) { $attributes[] = (new \AttributeCore($attributeId, $idLang))->name; } } try { $idProductAttribute = $this->productRepository->getIdProductAttributeByIdAttributes( $productId, $attributeIds ); } catch (PrestaShopException $e) { $idProductAttribute = 0; } return [ 'content_type' => self::PRODUCT_TYPE, 'content_ids' => [ ProductCatalogUtility::makeProductId($productId, $idProductAttribute), ], 'custom_properties' => [ 'custom_attributes' => $attributes, ], ]; } /** * Generate the array with data that are used for all events * * @see https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events * * @param string $eventType */ private function getCommonData($eventType) { $time = time(); return [ 'event_type' => $eventType, 'event_time' => $time, 'user' => CustomerInformationUtility::getCustomerInformationForPixel($this->context->customer), 'eventID' => uniqid($eventType . '_' . $time . '_', true), ]; } private function getCurrency(): string { if (!empty($this->context->currency->iso_code)) { return \Tools::strtolower($this->context->currency->iso_code); } if (!empty($this->context->cookie->id_currency)) { return \Tools::strtolower((new Currency($this->context->cookie->id_currency))->iso_code); } return \Tools::strtolower(Currency::getDefaultCurrency()->iso_code); } }