├── Config ├── AbstractConfigProvider.php └── ModuleConfiguration.php ├── Exception └── TrackingDataNotValidException.php ├── LICENSE ├── Logger ├── Handler.php └── Logger.php ├── Model ├── GAClient.php ├── Resolver │ └── GAResolver.php ├── ResourceModel │ ├── SalesOrder.php │ └── SalesOrder │ │ └── Collection.php ├── SalesOrder.php ├── SalesOrderFactory.php ├── SalesOrderRepository.php ├── SendPurchaseEvent.php └── Source │ ├── CurrencySource.php │ ├── Fallback.php │ ├── PaymentMethods.php │ └── TriggerMode.php ├── Observer ├── AfterOrderPayed.php └── AfterOrderPlaced.php ├── Plugin └── SaveGaUserDataToDb.php ├── README.md ├── Service └── UserDataProvider.php ├── Setup ├── Patch │ └── Data │ │ └── RenameConfigPaths.php └── UpgradeData.php ├── composer.json ├── etc ├── adminhtml │ └── system.xml ├── config.xml ├── db_schema.xml ├── db_schema_whitelist.json ├── di.xml ├── events.xml ├── extension_attributes.xml ├── module.xml └── schema.graphqls └── registration.php /Config/AbstractConfigProvider.php: -------------------------------------------------------------------------------- 1 | scopeConfig->isSetFlag( 27 | $xpath, 28 | ScopeInterface::SCOPE_STORE, 29 | scopeCode: $storeId 30 | ); 31 | } 32 | 33 | protected function getConfig(string $xpath, int|null|string $storeId = null): mixed 34 | { 35 | return $this->scopeConfig->getValue( 36 | $xpath, 37 | ScopeInterface::SCOPE_STORE, 38 | $storeId 39 | ); 40 | } 41 | 42 | public function getConfigAsString(string $xpath, int|null|string $storeId = null): ?string 43 | { 44 | return (string)$this->getConfig($xpath, $storeId); 45 | } 46 | 47 | public function getConfigAsInt(string $xpath, int|null|string $storeId = null): int 48 | { 49 | return (int)$this->getConfig($xpath, $storeId); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Config/ModuleConfiguration.php: -------------------------------------------------------------------------------- 1 | storeManager->getStore($storeId)->getId(); 33 | } 34 | 35 | public function isDebugMode(int|null|string $storeId = null): bool 36 | { 37 | return $this->isSetFlag(static::XPATH_DEBUG_MODE, $storeId); 38 | } 39 | 40 | public function isLogging(int|null|string $storeId = null): bool 41 | { 42 | return $this->isSetFlag(static::XPATH_ENABLE_LOGGING, $storeId); 43 | } 44 | 45 | public function getCurrencySource(int|null|string $storeId = null): ?int 46 | { 47 | return $this->getConfigAsInt(static::XPATH_CURRENCY_SOURCE, $storeId); 48 | } 49 | 50 | public function getTaxDisplayType(int|null|string $storeId = null): ?int 51 | { 52 | return $this->getConfigAsInt(static::XPATH_TAX_DISPLAY_TYPE, $storeId); 53 | } 54 | 55 | public function getFallbackGenerationMode(int|null|string $storeId = null): ?int 56 | { 57 | return $this->getConfigAsInt(static::XPATH_FALLBACK_GENERATION_MODE, $storeId); 58 | } 59 | 60 | public function getFallbackSessionIdPrefix(int|null|string $storeId = null): ?string 61 | { 62 | return $this->getConfigAsString(static::XPATH_FALLBACK_SESSION_ID_PREFIX, $storeId); 63 | } 64 | 65 | public function getFallbackSessionId(int|null|string $storeId = null): ?string 66 | { 67 | return $this->getConfigAsString(static::XPATH_FALLBACK_SESSION_ID, $storeId); 68 | } 69 | 70 | public function shouldTriggerOnPayment(int|null|string $storeId = null, ?string $paymentMethodCode = null): ?bool 71 | { 72 | return $this->shouldTriggerOn( 73 | mode: TriggerMode::PAYED, 74 | storeId: $storeId, 75 | paymentMethodCode: $paymentMethodCode 76 | ); 77 | } 78 | 79 | protected function shouldTriggerOn( 80 | int $mode, 81 | int|null|string $storeId = null, 82 | ?string $paymentMethodCode = null 83 | ): ?bool { 84 | return $this->isReadyForUse($storeId) && $this->shouldTriggerOnMode( 85 | mode: $mode, 86 | storeId: $storeId, 87 | paymentMethodCode: $paymentMethodCode 88 | ); 89 | } 90 | 91 | public function isReadyForUse(int|null|string $storeId = null): bool 92 | { 93 | return $this->isEnabled($storeId) && 94 | !empty($this->getApiSecret($storeId)) && 95 | !empty($this->getMeasurementId($storeId)); 96 | } 97 | 98 | public function isEnabled(int|null|string $storeId = null): bool 99 | { 100 | return $this->isSetFlag(static::XPATH_ENABLED, $storeId); 101 | } 102 | 103 | public function getApiSecret(int|null|string $storeId = null): ?string 104 | { 105 | return $this->getConfigAsString(static::XPATH_API_SECRET, $storeId); 106 | } 107 | 108 | public function getMeasurementId(int|null|string $storeId = null): ?string 109 | { 110 | return $this->getConfigAsString(static::XPATH_MEASUREMENT_ID, $storeId); 111 | } 112 | 113 | protected function shouldTriggerOnMode( 114 | int $mode, 115 | int|null|string $storeId = null, 116 | ?string $paymentMethodCode = null 117 | ): ?bool { 118 | $triggerMode = $this->getTriggerMode($storeId); 119 | if ($triggerMode === $mode) { 120 | return true; 121 | } 122 | 123 | if ($triggerMode !== TriggerMode::PAYMENT_METHOD_DEPENDENT) { 124 | return false; 125 | } 126 | 127 | if (!$paymentMethodCode) { 128 | return false; 129 | } 130 | 131 | if (in_array($paymentMethodCode, $this->getPaymentMethodsForTrigger(mode: $mode, storeId: $storeId))) { 132 | return true; 133 | } 134 | 135 | return false; 136 | } 137 | 138 | public function getTriggerMode(int|null|string $storeId = null): ?int 139 | { 140 | $value = $this->getConfigAsInt(static::XPATH_TRIGGER_MODE, $storeId); 141 | if (in_array($value, TriggerMode::ALL_OPTION_VALUES)) { 142 | return $value; 143 | } 144 | 145 | return null; 146 | } 147 | 148 | public function getPaymentMethodsForTrigger(int $mode = null, int|null|string $storeId = null): array 149 | { 150 | switch ($mode) { 151 | case TriggerMode::PLACED: 152 | return explode( 153 | ',', 154 | $this->getConfigAsString(self::XPATH_TRIGGER_ON_PLACED_METHODS, $storeId) 155 | ); 156 | case TriggerMode::PAYED: 157 | return explode( 158 | ',', 159 | $this->getConfigAsString(self::XPATH_TRIGGER_ON_PAYED_METHODS, $storeId) 160 | ); 161 | } 162 | 163 | return []; 164 | } 165 | 166 | public function shouldTriggerOnPlaced(int|null|string $storeId = null, ?string $paymentMethodCode = null): ?bool 167 | { 168 | return $this->shouldTriggerOn( 169 | mode: TriggerMode::PLACED, 170 | storeId: $storeId, 171 | paymentMethodCode: $paymentMethodCode 172 | ); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Exception/TrackingDataNotValidException.php: -------------------------------------------------------------------------------- 1 | getClientId()) { 47 | throw new TrackingDataNotValidException('No client ID is set for GA client.'); 48 | } 49 | 50 | $this->getRequest()->setClientId($data->getClientId()); // '2133506694.1448249699' 51 | 52 | $this->getRequest()->setTimestampMicros($this->getMicroTime()); 53 | } 54 | 55 | public function getRequest() 56 | { 57 | if (isset($this->request)) { 58 | return $this->request; 59 | } 60 | 61 | $this->request = new BaseRequest(); 62 | 63 | return $this->request; 64 | } 65 | 66 | public function getMicroTime(): int 67 | { 68 | return (int)(new DateTime()) 69 | ->format('Uu'); 70 | } 71 | 72 | public function setTransactionData(DataObject $data) 73 | { 74 | foreach ($data->getData() as $key => $param) { 75 | $this->getPurchaseEvent()->setParamValue($key, $param); 76 | } 77 | 78 | $this->getPurchaseEvent()->setParamValue('timestamp_micros', $this->getMicroTime()); 79 | } 80 | 81 | public function getPurchaseEvent() 82 | { 83 | if (isset($this->purchaseEvent)) { 84 | return $this->purchaseEvent; 85 | } 86 | 87 | $this->purchaseEvent = new PurchaseEvent(); 88 | 89 | return $this->purchaseEvent; 90 | } 91 | 92 | /** 93 | * @param DataObject[] $products 94 | */ 95 | public function addProducts(array $products): void 96 | { 97 | foreach ($products as $product) { 98 | $this->addProduct($product); 99 | } 100 | } 101 | 102 | public function addProduct(DataObject $data) 103 | { 104 | $this->productCounter++; 105 | 106 | $itemParameter = new ItemParameter($data->getData()); 107 | $itemParameter->setItemId($data->getSku()) 108 | ->setItemName($data->getName()) 109 | ->setIndex((float)$data->getPosition()) 110 | ->setPrice((float)$data->getPrice()) 111 | ->setQuantity((float)$data->getQuantity()); 112 | 113 | $this->getPurchaseEvent()->addItem($itemParameter); 114 | } 115 | 116 | public function addUserDataItems($userDataItems){ 117 | foreach ($userDataItems as $key => $userDataItem) { 118 | $this->addUserDataItem($key, $userDataItem); 119 | } 120 | } 121 | 122 | public function addUserDataItem($key, $userDataItem){ 123 | if($key === 'address'){ 124 | foreach($userDataItem as $addressData){ 125 | $userAddress = new UserAddress(); 126 | 127 | foreach($addressData as $addressKey => $addressValue){ 128 | $addressDataItem = new UserDataItem($addressKey, $addressValue); 129 | 130 | $userAddress->addUserAddressItem($addressDataItem); 131 | } 132 | 133 | $this->getRequest()->getUserData()->addUserAddress($userAddress); 134 | } 135 | }else{ 136 | $userDataItem = new UserDataItem($key, $userDataItem); 137 | 138 | $this->getRequest()->addUserDataItem($userDataItem); 139 | } 140 | } 141 | 142 | /** 143 | * @throws LocalizedException 144 | */ 145 | public function firePurchaseEvent(int|string|null $storeId) 146 | { 147 | if (!$this->productCounter) { 148 | throw new LocalizedException( 149 | __( 150 | 'No products have been added to transaction %s', 151 | $this->getPurchaseEvent()->getTransactionId() 152 | ) 153 | ); 154 | } 155 | 156 | $baseRequest = $this->getRequest(); 157 | $baseRequest->addEvent($this->getPurchaseEvent()); 158 | 159 | $baseRequest->validate(); 160 | 161 | $send = $this->moduleConfiguration->isDebugMode() ? 'sendDebug' : 'send'; 162 | 163 | $service = $this->getService($storeId); 164 | $response = $this->getService($storeId)->$send($baseRequest); 165 | 166 | $this->createLog( 167 | "Request: ", 168 | [ 169 | "storeId" => $storeId, 170 | "measurementId" => $service->getMeasurementId(), 171 | "body" => $this->getRequest()->export() 172 | ] 173 | ); 174 | $this->createLog('Response: ', [$response->getStatusCode()]); 175 | } 176 | 177 | public function getService(int|string|null $storeId) 178 | { 179 | $storeId = $this->moduleConfiguration->ensureStoreId($storeId); 180 | if (isset($this->service[$storeId])) { 181 | return $this->service[$storeId]; 182 | } 183 | 184 | $this->service[$storeId] = new Service($this->moduleConfiguration->getApiSecret($storeId)); 185 | $this->service[$storeId]->setMeasurementId($this->moduleConfiguration->getMeasurementId($storeId)); 186 | 187 | return $this->service[$storeId]; 188 | } 189 | 190 | public function createLog($message, array $context = []) 191 | { 192 | if (!$this->moduleConfiguration->isLogging()) { 193 | return; 194 | } 195 | 196 | $this->logger->info($message, $context); 197 | } 198 | 199 | /** 200 | * @return string 201 | */ 202 | public function getVersion(): string 203 | { 204 | return $this->version; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /Model/Resolver/GAResolver.php: -------------------------------------------------------------------------------- 1 | getExtensionAttributes()->getIsCustomer()) { 45 | throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); 46 | } 47 | 48 | $cartId = is_numeric($input['cartId']) 49 | ? $input['cartId'] 50 | : $this->maskedQuoteIdToQuoteId->execute($input['cartId']); 51 | 52 | /** @var Collection $elgentosSalesOrderCollection */ 53 | $elgentosSalesOrderCollection = $this->elgentosSalesOrderCollectionFactory->create(); 54 | /** @var SalesOrder $elgentosSalesOrder */ 55 | $elgentosSalesOrder = $elgentosSalesOrderCollection 56 | ->addFieldToFilter('quote_id', $cartId) 57 | ->getFirstItem(); 58 | 59 | $elgentosSalesOrder->setQuoteId($cartId); 60 | if ($elgentosSalesOrder->getGaUserId() !== ($input['gaUserId'] ?? null)) { 61 | $elgentosSalesOrder->setGaUserId($input['gaUserId'] ?? ''); 62 | } 63 | 64 | if ($elgentosSalesOrder->getGaSessionId() !== ($input['gaSessionId'] ?? null)) { 65 | $elgentosSalesOrder->setGaSessionId($input['gaSessionId'] ?? ''); 66 | } 67 | 68 | $this->elgentosSalesOrderRepository->save($elgentosSalesOrder); 69 | 70 | return [ 71 | 'cartId' => $cartId, 72 | 'maskedId' => !is_numeric($input['cartId']) ? $input['cartId'] : null 73 | ]; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Model/ResourceModel/SalesOrder.php: -------------------------------------------------------------------------------- 1 | _init(self::MAIN_TABLE, self::ID_FIELD_NAME); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Model/ResourceModel/SalesOrder/Collection.php: -------------------------------------------------------------------------------- 1 | _init(SalesOrder::class, SalesOrderResource::class); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Model/SalesOrder.php: -------------------------------------------------------------------------------- 1 | _init(ResourceModel\SalesOrder::class); 42 | } 43 | 44 | /** 45 | * Set the ga_user_id and ga_session_id on the current sales order. 46 | * Any missing data will be gathered from the cookies, or generated. 47 | */ 48 | public function setGaData(int|null|string $storeId = null, null|string $gaUserId = null, int|null|string $gaSessionId = null): static 49 | { 50 | $this->setData('ga_user_id', $gaUserId ?? $this->getCurrentGaUserId()); 51 | $this->setData('ga_session_id', $gaSessionId ?? $this->getCurrentGaSessionId($storeId)); 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * Attemt to get the current GA User id from cookies, database or generating it. 58 | */ 59 | public function getCurrentGaUserId(): string 60 | { 61 | $gaUserId = $this->getUserIdFromCookie($this->cookieManager->getCookie('_ga')) ?? $this->getGaUserId(); 62 | 63 | if (!$gaUserId) { 64 | $gaUserId = $this->generateFallbackUserId(); 65 | $this->gaclient->createLog( 66 | 'Google Analytics cookie not found, generated temporary GA User Id: ' . $gaUserId 67 | ); 68 | } 69 | 70 | return (string) $gaUserId; 71 | } 72 | 73 | /** 74 | * Attemt to get the current GA Session id from cookies, database or generating it. 75 | */ 76 | public function getCurrentGaSessionId(int|null|string $storeId = null): string 77 | { 78 | $measurementId = $this->moduleConfiguration->getMeasurementId($storeId); 79 | $gaSessionId = $this->getSessionIdFromCookie( 80 | $this->getSessionIdCookie($measurementId) 81 | ); 82 | 83 | if ($gaSessionId) { 84 | return $gaSessionId; 85 | } 86 | 87 | return (string) ($this->getGaSessionId() ?? $this->generateFallbackSessionId( 88 | $this->moduleConfiguration->getFallbackGenerationMode($storeId), 89 | $storeId 90 | )); 91 | } 92 | 93 | /** 94 | * Try to get the Google Analytics User ID from the cookie 95 | */ 96 | public function getUserIdFromCookie($gaCookie = '' /** GA1.1.99999999.1732012836 */): ?string 97 | { 98 | $gaCookie = explode('.', $gaCookie ?? ''); 99 | 100 | if (count($gaCookie) < 4) { 101 | return null; 102 | } 103 | 104 | [ 105 | $gaCookieVersion, 106 | $gaCookieDomainComponents, 107 | $gaCookieUserId, 108 | $gaCookieTimestamp 109 | ] = $gaCookie; 110 | 111 | if (!$gaCookieUserId || !$gaCookieTimestamp) { 112 | return null; 113 | } 114 | 115 | if ( 116 | $gaCookieVersion != 'GA' . $this->gaclient->getVersion() 117 | ) { 118 | $this->gaclient->createLog( 119 | 'Google Analytics cookie version differs from Measurement Protocol API version; please upgrade.' 120 | ); 121 | return null; 122 | } 123 | 124 | return implode('.', [$gaCookieUserId, $gaCookieTimestamp]); 125 | } 126 | 127 | /** 128 | * Generate a fallback analytics user id 129 | */ 130 | public function generateFallbackUserId(): string 131 | { 132 | $gaCookieUserId = random_int((int)1E8, (int)1E9); 133 | $gaCookieTimestamp = time(); 134 | $gaUserId = implode('.', [$gaCookieUserId, $gaCookieTimestamp]); 135 | 136 | return $gaUserId; 137 | } 138 | 139 | public function getSessionIdCookie($gaMeasurementId = '' /** G-0XX00XXX */ ): string 140 | { 141 | $gaMeasurementId = str_replace('G-', '', $gaMeasurementId ?? ''); 142 | 143 | return $this->cookieManager->getCookie('_ga_' . $gaMeasurementId) ?? ''; 144 | } 145 | 146 | public function getSessionIdFromCookie(?string $gaCookie = '' /** GS1.1.1732016998.2.1.1732018235.0.0.692404937 OR GS2.1.s1748246127$o6$g1$t1748251071$j60$l0$h263196385$dQn_oPl1JlfID64muTcg9EeKK4oR_eD4EGg */): ?string 147 | { 148 | if (!preg_match('/^GS[0-9]\.[0-9]\.s?(?[0-9]+)/', $gaCookie, $matches) || !$matches['session_id']) { 149 | return null; 150 | } 151 | 152 | return (string) $matches['session_id']; 153 | } 154 | 155 | /** 156 | * Generate a fallback analytics session id 157 | */ 158 | public function generateFallbackSessionId($fallbackGenerationMode = Fallback::DEFAULT, int|null|string $storeId = null): string 159 | { 160 | if ($fallbackGenerationMode === Fallback::DEFAULT) { 161 | return (string) $this->moduleConfiguration->getFallbackSessionId() ?? '9999999999999'; 162 | } 163 | 164 | if ($fallbackGenerationMode === Fallback::PREFIX) { 165 | $prefix = $this->moduleConfiguration->getFallbackSessionIdPrefix($storeId); 166 | if (!$prefix) { 167 | $prefix = '9999'; 168 | } 169 | 170 | return (string) $prefix . random_int((int)1E5, (int)1E6); 171 | } 172 | 173 | return (string) random_int((int)1E5, (int)1E9); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /Model/SalesOrderFactory.php: -------------------------------------------------------------------------------- 1 | _objectManager = $objectManager; 42 | $this->_instanceName = $instanceName; 43 | } 44 | 45 | /** 46 | * Create class instance with specified parameters 47 | * 48 | * @param array $data 49 | * 50 | * @return Quote 51 | */ 52 | public function create(array $data = []) 53 | { 54 | return $this->_objectManager->create($this->_instanceName, $data); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Model/SalesOrderRepository.php: -------------------------------------------------------------------------------- 1 | resource->save($data); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Model/SendPurchaseEvent.php: -------------------------------------------------------------------------------- 1 | getInvoiceCollection()->getFirstItem(); 45 | 46 | $orderStoreId = $order->getStoreId(); 47 | 48 | $gaUserDatabaseId = $order->getId(); 49 | 50 | if (!$gaUserDatabaseId) { 51 | $gaUserDatabaseId = $order->getQuoteId(); 52 | } 53 | 54 | if (!$gaUserDatabaseId) { 55 | return; 56 | } 57 | 58 | $this->emulation->startEnvironmentEmulation($orderStoreId, 'adminhtml'); 59 | 60 | if (!$this->moduleConfiguration->isReadyForUse($orderStoreId)) { 61 | $this->emulation->stopEnvironmentEmulation(); 62 | 63 | return; 64 | } 65 | 66 | $elgentosSalesOrder = $this->getElgentosSalesOrder($gaUserDatabaseId); 67 | 68 | if (!$elgentosSalesOrder) { 69 | $this->emulation->stopEnvironmentEmulation(); 70 | 71 | return; 72 | } 73 | 74 | $gaclient = $this->GAClientFactory->create(); 75 | 76 | if ($elgentosSalesOrder->getData('send_at') !== null) { 77 | $this->emulation->stopEnvironmentEmulation(); 78 | if ($this->moduleConfiguration->isLogging($orderStoreId)) { 79 | $gaclient->createLog( 80 | 'The purchase event for order #' . 81 | $order->getIncrementId() . ' was send already by trigger ' . 82 | ($elgentosSalesOrder->getData('trigger') ?? '') . '.', 83 | [ 84 | 'eventName' => $eventName, 85 | ...$elgentosSalesOrder->getData() 86 | ] 87 | ); 88 | } 89 | 90 | return; 91 | } 92 | 93 | if ($this->moduleConfiguration->isLogging($orderStoreId)) { 94 | $gaclient->createLog( 95 | 'Got ' . $eventName . ' event for Ga UserID: ' . $elgentosSalesOrder->getGaUserId(), 96 | [ 97 | 'eventName' => $eventName, 98 | ...$elgentosSalesOrder->getData() 99 | ] 100 | ); 101 | } 102 | 103 | $trackingDataObject = new DataObject( 104 | [ 105 | 'client_id' => $elgentosSalesOrder->getGaUserId(), 106 | 'ip_override' => $order->getRemoteIp(), 107 | 'document_path' => '/checkout/onepage/success/' 108 | ] 109 | ); 110 | 111 | $transactionDataObject = $this->getTransactionDataObject($order, $elgentosSalesOrder); 112 | $products = $this->collectProducts($order); 113 | $userData = $this->collectUserData($order); 114 | 115 | $this->sendPurchaseEvent($gaclient, $transactionDataObject, $products, $trackingDataObject, $userData, $orderStoreId); 116 | 117 | $elgentosSalesOrder->setData('trigger', $eventName); 118 | $elgentosSalesOrder->setData('send_at', date('Y-m-d H:i:s')); 119 | $this->elgentosSalesOrderRepository->save($elgentosSalesOrder); 120 | 121 | $this->emulation->stopEnvironmentEmulation(); 122 | } 123 | 124 | protected function getElgentosSalesOrder($gaUserDatabaseId): ?SalesOrder 125 | { 126 | $elgentosSalesOrderCollection = $this->elgentosSalesOrderCollectionFactory->create(); 127 | /** @var SalesOrder $elgentosSalesOrder */ 128 | $elgentosSalesOrder = $elgentosSalesOrderCollection 129 | ->addFieldToFilter( 130 | ['quote_id', 'order_id'], 131 | [ 132 | ['eq' => $gaUserDatabaseId], 133 | ['eq' => $gaUserDatabaseId] 134 | ] 135 | ) 136 | ->getFirstItem(); 137 | 138 | if ( 139 | !$elgentosSalesOrder->getGaUserId() 140 | || 141 | !$elgentosSalesOrder->getGaSessionId() 142 | ) { 143 | return null; 144 | } 145 | 146 | return $elgentosSalesOrder; 147 | } 148 | 149 | /** 150 | * @return DataObject[] 151 | */ 152 | protected function collectProducts(Order $order): array 153 | { 154 | $products = []; 155 | 156 | /** @var Invoice\Item $item */ 157 | foreach ($order->getAllItems() as $item) { 158 | if (!$item->isDeleted() && !$item->getParentItemId()) { 159 | $product = new DataObject( 160 | [ 161 | 'sku' => $item->getSku(), 162 | 'name' => $item->getName(), 163 | 'price' => $this->getPaidProductPrice($item), 164 | 'quantity' => $item->getQtyOrdered(), 165 | 'position' => $item->getId(), 166 | 'item_brand' => $item->getProduct()?->getAttributeText('manufacturer') 167 | ] 168 | ); 169 | 170 | $this->event->dispatch( 171 | 'elgentos_serversideanalytics_product_item_transport_object', 172 | ['product' => $product, 'item' => $item] 173 | ); 174 | 175 | $products[] = $product; 176 | } 177 | } 178 | 179 | return $products; 180 | } 181 | 182 | protected function collectUserData(Order $order){ 183 | $customerEmail = $order->getCustomerEmail(); 184 | 185 | if ($customerEmail) { 186 | $this->userDataProvider->setEmail($customerEmail); 187 | } 188 | 189 | // Get billing address and set phone number and address details 190 | $billingAddress = $order->getBillingAddress(); 191 | 192 | if ($billingAddress) { 193 | $billingPhoneNumber = $billingAddress->getTelephone(); 194 | if ($billingPhoneNumber) { 195 | $this->userDataProvider->setPhoneNumber($billingPhoneNumber); 196 | } 197 | 198 | // Add address 199 | $this->userDataProvider->addAddress( 200 | $billingAddress->getFirstname(), 201 | $billingAddress->getLastname(), 202 | implode(' ', $billingAddress->getStreet()), 203 | $billingAddress->getCity(), 204 | $billingAddress->getRegion(), 205 | $billingAddress->getPostcode(), 206 | $billingAddress->getCountryId() 207 | ); 208 | } 209 | 210 | // Optionally process shipping address if needed 211 | $shippingAddress = $order->getShippingAddress(); 212 | if ($shippingAddress) { 213 | $shippingPhoneNumber = $shippingAddress->getTelephone(); 214 | if ($shippingPhoneNumber) { 215 | $this->userDataProvider->setPhoneNumber($shippingPhoneNumber); 216 | } 217 | 218 | $this->userDataProvider->addAddress( 219 | $shippingAddress->getFirstname(), 220 | $shippingAddress->getLastname(), 221 | implode(' ', $shippingAddress->getStreet()), 222 | $shippingAddress->getCity(), 223 | $shippingAddress->getRegion(), 224 | $shippingAddress->getPostcode(), 225 | $shippingAddress->getCountryId() 226 | ); 227 | } 228 | 229 | return $this->userDataProvider->toArray(); 230 | } 231 | 232 | /** 233 | * Get the actual price the customer also saw in it's cart. 234 | * @return float 235 | */ 236 | private function getPaidProductPrice(Item $orderItem): float 237 | { 238 | $price = $this->moduleConfiguration 239 | ->getTaxDisplayType($orderItem->getOrder()?->getStoreId()) === Config::DISPLAY_TYPE_EXCLUDING_TAX 240 | ? $orderItem->getBasePrice() 241 | : $orderItem->getBasePriceInclTax(); 242 | 243 | return (float)$price; 244 | } 245 | 246 | public function getTransactionDataObject(Order $order, $elgentosSalesOrder): DataObject 247 | { 248 | $currency = $this->moduleConfiguration->getCurrencySource($order->getStoreId()) === CurrencySource::GLOBAL ? 249 | $order->getGlobalCurrencyCode() : 250 | $order->getBaseCurrencyCode(); 251 | 252 | $shippingCosts = $this->getPaidShippingCosts($order); 253 | 254 | $transactionDataObject = new DataObject( 255 | [ 256 | 'transaction_id' => $order->getIncrementId(), 257 | 'affiliation' => $order->getStoreName(), 258 | 'currency' => $currency, 259 | 'value' => (float)$order->getBaseGrandTotal(), 260 | 'tax' => (float)$order->getBaseTaxAmount(), 261 | 'shipping' => $shippingCosts ?? 0.0, // Use 0.0 if null 262 | 'coupon_code' => $order->getCouponCode(), 263 | 'session_id' => $elgentosSalesOrder->getGaSessionId() 264 | ] 265 | ); 266 | 267 | $this->event->dispatch( 268 | 'elgentos_serversideanalytics_transaction_data_transport_object', 269 | ['transaction_data_object' => $transactionDataObject, 'order' => $order] 270 | ); 271 | 272 | return $transactionDataObject; 273 | } 274 | 275 | /** 276 | * Get shipping costs 277 | * @return float|null 278 | */ 279 | private function getPaidShippingCosts(Order $order): ?float 280 | { 281 | $shippingAmount = $this->moduleConfiguration->getTaxDisplayType($order->getStoreId()) == Config::DISPLAY_TYPE_EXCLUDING_TAX 282 | ? $order->getBaseShippingAmount() 283 | : $order->getBaseShippingInclTax(); 284 | 285 | if ($shippingAmount === null) { 286 | return null; 287 | } 288 | 289 | return (float)$shippingAmount; 290 | } 291 | 292 | public function sendPurchaseEvent( 293 | GAClient $gaclient, 294 | DataObject $transactionDataObject, 295 | array $products, 296 | DataObject $trackingDataObject, 297 | array $userData, 298 | int|string|null $orderStoreId 299 | ): void { 300 | try { 301 | $gaclient->setTransactionData($transactionDataObject); 302 | $gaclient->addUserDataItems($userData); 303 | $gaclient->addProducts($products); 304 | } catch (\Exception $e) { 305 | $gaclient->createLog($e); 306 | 307 | return; 308 | } 309 | 310 | try { 311 | $this->event->dispatch( 312 | 'elgentos_serversideanalytics_tracking_data_transport_object', 313 | ['tracking_data_object' => $trackingDataObject] 314 | ); 315 | 316 | $gaclient->setTrackingData($trackingDataObject); 317 | 318 | $gaclient->firePurchaseEvent($orderStoreId); 319 | } catch (\Exception $e) { 320 | $gaclient->createLog($e); 321 | } 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /Model/Source/CurrencySource.php: -------------------------------------------------------------------------------- 1 | '', 'label' => '