BxAdsSourceShopifyAdmin.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. <?php defined('BX_DOL') or die('hack attempt');
  2. /**
  3. * Copyright (c) UNA, Inc - https://una.io
  4. * MIT License - https://opensource.org/licenses/MIT
  5. *
  6. * @defgroup Ads Ads
  7. * @ingroup UnaModules
  8. *
  9. * @{
  10. */
  11. require_once('BxAdsSource.php');
  12. /*
  13. * Login for Owner/Collaborator account:
  14. * https://admin.shopify.com/store/your_shop/
  15. *
  16. * Integration:
  17. * 0. Custom apps creation guide (for a shop Owner/Collaborator, not a Partner):
  18. * https://help.shopify.com/en/manual/apps/app-types/custom-apps
  19. *
  20. * 1. Make authenticated requests to the REST Admin API:
  21. * https://shopify.dev/docs/apps/auth/admin-app-access-tokens
  22. *
  23. * 2. REST Admin API reference:
  24. * https://shopify.dev/docs/api/admin-rest
  25. *
  26. * 3. REST Admin API -> Product
  27. * https://shopify.dev/docs/api/admin-rest/2023-10/resources/product
  28. *
  29. * 4. REST Admin API -> Order
  30. * https://shopify.dev/docs/api/admin-rest/2023-10/resources/order
  31. *
  32. */
  33. class BxAdsSourceShopifyAdmin extends BxAdsSource
  34. {
  35. public static $NAME = 'shopify_admin';
  36. protected $_sShopDomain;
  37. protected $_sAccessToken;
  38. protected $_sEndpoint;
  39. protected $_sStorefront;
  40. public function __construct($iProfile, $aSource, &$oModule)
  41. {
  42. $this->_sName = self::$NAME;
  43. parent::__construct($iProfile, $aSource, $oModule);
  44. $this->_sShopDomain = $this->getOption('shop_domain');
  45. $this->_sAccessToken = $this->getOption('access_token');
  46. $this->_sEndpoint = "https://{$this->_sShopDomain}/admin/api/2023-10/";
  47. $this->_sStorefront = "https://{$this->_sShopDomain}/";
  48. }
  49. public function getEntry($sId)
  50. {
  51. $CNF = &$this->_oModule->_oConfig->CNF;
  52. $sProduct = $this->_call('products/' . $sId . '.json', [
  53. 'fields' => 'id,title,handle,body_html,tags,variants',
  54. ], 'get');
  55. if(empty($sProduct))
  56. return [];
  57. $aProduct = json_decode($sProduct, true);
  58. if(empty($aProduct) || !is_array($aProduct) || empty($aProduct['product']))
  59. return [];
  60. return $this->_prepareProduct($aProduct['product']);
  61. }
  62. public function getEntries($aParams)
  63. {
  64. $CNF = &$this->_oModule->_oConfig->CNF;
  65. $sSample = $aParams['sample'];
  66. $aRequest = [
  67. 'fields' => 'id,title,handle,body_html,tags,variants',
  68. 'status' => isset($aParams['status']) ? $aParams['status'] : 'active'
  69. ];
  70. switch($sSample) {
  71. case 'ids':
  72. $aRequest = array_merge($aRequest, [
  73. 'ids' => implode(',', $aParams['ids']),
  74. ]);
  75. break;
  76. case 'title':
  77. $aRequest = array_merge($aRequest, [
  78. 'title' => $aParams['title'],
  79. ]);
  80. break;
  81. case 'all_id_title_pairs':
  82. $aRequest = array_merge($aRequest, [
  83. 'fields' => 'id,title',
  84. ]);
  85. break;
  86. case 'all':
  87. break;
  88. }
  89. $sProducts = $this->_call('products.json', $aRequest, 'get');
  90. if(empty($sProducts))
  91. return [];
  92. $aProducts = json_decode($sProducts, true);
  93. if(empty($aProducts) || !is_array($aProducts) || empty($aProducts['products']))
  94. return [];
  95. $aResults = [];
  96. foreach($aProducts['products'] as $aProduct) {
  97. switch($sSample) {
  98. case 'all_id_title_pairs':
  99. $aResults[] = [
  100. $CNF['FIELD_SOURCE'] => $aProduct['id'],
  101. $CNF['FIELD_TITLE'] => $aProduct['title']
  102. ];
  103. break;
  104. default:
  105. $aResults[] = $this->_prepareProduct($aProduct);
  106. }
  107. }
  108. return $aResults;
  109. }
  110. protected function _prepareProduct($aProduct)
  111. {
  112. $CNF = &$this->_oModule->_oConfig->CNF;
  113. $aResult = [
  114. $CNF['FIELD_SOURCE'] => $aProduct['id'],
  115. $CNF['FIELD_TITLE'] => $aProduct['title'],
  116. $CNF['FIELD_URL'] => $this->_sStorefront . 'products/' . $aProduct['handle'],
  117. $CNF['FIELD_TEXT'] => $aProduct['body_html'],
  118. $CNF['FIELD_TAGS'] => $aProduct['tags'],
  119. ];
  120. if(!empty($aProduct['variants']) && is_array($aProduct['variants'])) {
  121. $aVariant = array_shift($aProduct['variants']);
  122. if(isset($aVariant['price']))
  123. $aResult[$CNF['FIELD_PRICE']] = (float)$aVariant['price'];
  124. if(isset($aVariant['inventory_quantity']))
  125. $aResult[$CNF['FIELD_QUANTITY']] = (int)$aVariant['inventory_quantity'];
  126. }
  127. return $aResult;
  128. }
  129. public function getOrders($aParams)
  130. {
  131. $sSample = $aParams['sample'];
  132. $aRequest = [
  133. 'fields' => 'id,name,created_at,processed_at,current_total_price,current_total_tax,line_items',
  134. 'financial_status' => isset($aParams['status']) ? $aParams['status'] : 'paid'
  135. ];
  136. switch($sSample) {
  137. case 'ids':
  138. $aRequest = array_merge($aRequest, [
  139. 'ids' => implode(',', $aParams['ids']),
  140. ]);
  141. break;
  142. case 'sales_by_product_id':
  143. $aRequest = array_merge($aRequest, [
  144. 'processed_at_min' => $this->_dateI2S($aParams['date_from']),
  145. ]);
  146. break;
  147. }
  148. $sOrders = $this->_call('orders.json', $aRequest, 'get');
  149. if(empty($sOrders))
  150. return [];
  151. $aOrders = json_decode($sOrders, true);
  152. if(empty($aOrders) || !is_array($aOrders) || empty($aOrders['orders']))
  153. return [];
  154. $aOrders = $aOrders['orders'];
  155. $aResults = [];
  156. foreach($aOrders as $aOrder)
  157. foreach($aOrder['line_items'] as $aLineItem) {
  158. if($sSample == 'sales_by_product_id' && $aLineItem['product_id'] != $aParams['product_id'])
  159. continue;
  160. $aResults[] = [
  161. 'id' => $aOrder['id'],
  162. 'created' => $aOrder['created_at'],
  163. 'processed' => $aOrder['processed_at'],
  164. 'name' => $aOrder['name'],
  165. 'product_id' => $aLineItem['product_id'],
  166. 'product_price' => (float)$aLineItem['price'],
  167. 'product_discounted' => (float)$aLineItem['pre_tax_price'],
  168. 'quantity' => (int)$aLineItem['quantity'],
  169. 'amount' => (int)$aLineItem['quantity'] * (float)$aLineItem['pre_tax_price']
  170. ];
  171. }
  172. return $aResults;
  173. }
  174. /**
  175. * Internal methods.
  176. */
  177. protected function _call($sRequest, $aParams, $sMethod = 'post-json', $aHeaders = [])
  178. {
  179. $aHeaders[] = 'Content-Type: application/json';
  180. if(!empty($this->_sAccessToken))
  181. $aHeaders[] = 'X-Shopify-Access-Token: ' . $this->_sAccessToken;
  182. else
  183. $aHeaders[] = 'Authorization: Basic ' . base64_encode($this->_sApiKey . ':' . $this->_sApiSecretKey);
  184. return bx_file_get_contents($this->_sEndpoint . $sRequest, $aParams, $sMethod, $aHeaders);
  185. }
  186. protected function _dateI2S($iTimestamp)
  187. {
  188. return date("Y-m-d", $iTimestamp);
  189. }
  190. }