PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / trunk
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). vtrunk
3.0.3 3.0.2 3.0.1 trunk 2.2.14 2.2.15 2.2.16 2.2.17 2.2.18 2.2.19 2.3.0 2.3.1 2.3.10 2.3.11 2.3.12 2.3.13 2.3.14 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6 2.3.7 2.3.8 2.3.9 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.5.0 2.5.1 2.5.2 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.6.5 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.7.91 2.7.92 2.7.93 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0
commercebird / includes / classes / zoho-inventory / class-import-price-list.php
commercebird / includes / classes / zoho-inventory Last commit date
class-cmbird-categories-zi.php 2 months ago class-cmbird-image-zi.php 2 weeks ago class-import-items.php 2 weeks ago class-import-price-list.php 3 months ago class-multi-currency.php 8 months ago class-order-sync.php 2 weeks ago class-product.php 2 months ago class-users-contact.php 3 weeks ago index.php 1 year ago
class-import-price-list.php
474 lines
1 <?php
2
3 if ( ! defined( 'ABSPATH' ) ) {
4 exit;
5 }
6
7 /**
8 * Class CMBIRD_Pricelist_ZI
9 *
10 * Handles importing Zoho price lists into WooCommerce and B2B plugins.
11 */
12 class CMBIRD_Pricelist_ZI {
13 use CommerceBird\Admin\Traits\LogWriter;
14
15 /**
16 * Configuration array containing Zoho Inventory API details.
17 *
18 * @var array
19 */
20 private array $config;
21
22 /**
23 * Class constructor.
24 *
25 * Retrieves Zoho Inventory connection configuration options for OID and API URL.
26 */
27 public function __construct() {
28 $this->config = array(
29 'ProductZI' => array(
30 'OID' => get_option( 'cmbird_zoho_inventory_oid' ),
31 'APIURL' => get_option( 'cmbird_zoho_inventory_url' ),
32 ),
33 );
34 }
35
36 /**
37 * Get all Zoho price list
38 * Example response:
39 * {
40 * "currency_code": Code based on currency,
41 * "currency_id": The currenct id of the currency,
42 * "decimal_place": Decimal place for pricebook.,
43 * "description": Description about the pricebook,
44 * "is_default": To check the default pricebook.Allowed values: true,false,
45 * "is_increase": Mark up or Mark down to discounts.Allowed values: true,false,
46 * "name": Name of the pricebook,
47 * "percentage": About percentage of discounts,
48 * "pricebook_id": Unique ID generated by server for the price book,
49 * "pricebook_items": [
50 * {
51 * "item_id": Unique ID generated by server for Item,
52 * "pricebook_rate": Rate of the price book for the Items,
53 * "pricebook_item_id": Unique ID generated by the server for each pricebook line item
54 * }
55 * ],
56 * "pricebook_type": Type of the pricebook.Allowed values: per_item,fixed_percentage,
57 * "rounding_type": Type of the rounding.Allowed values: no_rounding,round_to_dollor,round_to_dollar_minus_01,round_to_half_dollar,round_to_half_dollar_minus_01,
58 * "sales_or_purchase_type": Whether its sales or purchase type.Allowed values: sales,purchases,
59 * "status": Status of the price book
60 * }
61 *
62 * @return array of price list
63 */
64 public function zi_get_all_pricelist(): array {
65 $in_cache = get_transient( 'zoho_pricelist' );
66 if ( $in_cache ) {
67 return $in_cache;
68 }
69
70 $url = $this->config['ProductZI']['APIURL'] . 'inventory/v1/pricebooks?organization_id=' . $this->config['ProductZI']['OID'];
71 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
72 $json = $execute_curl_call_handle->execute_curl_call_get( $url );
73
74 if ( isset( $json->pricebooks ) ) {
75 set_transient( 'zoho_pricelist', $json->pricebooks, MINUTE_IN_SECONDS );
76
77 return $json->pricebooks;
78 }
79
80 return array();
81 }
82
83 /**
84 * Save pricelist function to update prices and discounts for user roles.
85 *
86 * @param array $post The post-data containing user role and price information.
87 * Example:
88 * {
89 * "zoho_inventory_pricelist": Zoho inventory pricelist id
90 * "wp_user_role": WordPress user role
91 * }
92 */
93 public function save_price_list( array $post ): bool {
94 if ( class_exists( 'Addify_B2B_Plugin' ) ) {
95 $success = $this->addify_b2b( $post['zoho_inventory_pricelist'], $post['wp_user_role'] );
96 } elseif ( CMBIRD_Common_Functions::is_wcb2b_active() ) {
97 $success = $this->wc_b2b( $post['wcb2b'] );
98 }
99 return $success;
100 }
101
102 /**
103 * Fetch one page of pricebook data from the Zoho API without caching.
104 *
105 * @param int $pricebook_id Pricebook ID.
106 * @param int $page 1-based page number.
107 *
108 * @return array{pricebook: array, has_more_page: bool}
109 */
110 private function fetch_pricebook_page( int $pricebook_id, int $page ): array {
111 $url = $this->config['ProductZI']['APIURL']
112 . 'inventory/v1/pricebooks/' . $pricebook_id
113 . '?organization_id=' . $this->config['ProductZI']['OID']
114 . '&page=' . $page;
115 $api = new CMBIRD_API_Handler_Zoho();
116 $json = $api->execute_curl_call_get( $url );
117
118 if ( ! is_object( $json ) || ! property_exists( $json, 'pricebook' ) ) {
119 return array(
120 'pricebook' => array(),
121 'has_more_page' => false,
122 );
123 }
124
125 $pricebook = json_decode( wp_json_encode( $json->pricebook ), true );
126 $has_more = ! empty( $json->page_context->has_more_page );
127
128 return array(
129 'pricebook' => $pricebook,
130 'has_more_page' => $has_more,
131 );
132 }
133
134 /**
135 * Get page 1 of a Zoho pricebook (cached for 24 h).
136 *
137 * The returned array includes a '_has_more_page' boolean so callers can
138 * decide whether to queue subsequent pages via Action Scheduler.
139 *
140 * @param int $pricebook_id pricebook id.
141 *
142 * @return array Pricebook data or empty array on failure.
143 */
144 private function get_zoho_pricebook( int $pricebook_id ): array {
145 $in_cache = get_transient( 'zoho_pricelist_' . $pricebook_id );
146 if ( $in_cache ) {
147 return $in_cache;
148 }
149
150 $result = $this->fetch_pricebook_page( $pricebook_id, 1 );
151 $pricebook = $result['pricebook'];
152 if ( empty( $pricebook ) ) {
153 return array();
154 }
155
156 $pricebook['_has_more_page'] = $result['has_more_page'];
157 set_transient( 'zoho_pricelist_' . $pricebook_id, $pricebook, DAY_IN_SECONDS );
158 update_option( 'cmbird_zoho_pricelist_id', $pricebook_id );
159
160 return $pricebook;
161 }
162
163 /**
164 * Get published product ids, based on zi_item_id meta-key
165 *
166 * @return array
167 */
168 private function get_zoho_products(): array {
169 $in_cache = get_transient( 'zoho_products' );
170 if ( $in_cache ) {
171 return $in_cache;
172 }
173 global $wpdb;
174
175 $ids = $wpdb->get_results(
176 "SELECT p.ID AS id, pm.meta_value AS zi_item_id
177 FROM {$wpdb->postmeta} pm
178 INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
179 WHERE pm.meta_key = 'zi_item_id'
180 AND p.post_type in ('product', 'product_variation')
181 AND p.post_status = 'publish'
182 AND pm.meta_value != ''",
183 ARRAY_A
184 );
185
186 $plist = wp_list_pluck( $ids, 'id', 'zi_item_id' );
187 ksort( $plist );
188 set_transient( 'zoho_products', $plist, DAY_IN_SECONDS );
189
190 return $plist;
191 }
192
193 /**
194 * Insert or update a single role-based price entry on a product.
195 *
196 * @param int $product_id WP post ID.
197 * @param string $role User role slug.
198 * @param array $metavalue Discount configuration to store.
199 */
200 private function upsert_role_base_price( int $product_id, string $role, array $metavalue ): void {
201 $postmeta_array = get_post_meta( $product_id, '_role_base_price', true );
202 if ( ! is_array( $postmeta_array ) ) {
203 $postmeta_array = array();
204 }
205 $updated = false;
206
207 if ( ! empty( $postmeta_array ) ) {
208 foreach ( $postmeta_array as &$postmeta ) {
209 if ( isset( $postmeta['user_role'] ) && $postmeta['user_role'] === $role ) {
210 $postmeta = $metavalue;
211 $updated = true;
212 break;
213 }
214 }
215 unset( $postmeta );
216 if ( ! $updated ) {
217 $postmeta_array[] = $metavalue;
218 }
219 } else {
220 $postmeta_array[] = $metavalue;
221 }
222 update_post_meta( $product_id, '_role_base_price', $postmeta_array );
223 }
224
225 /**
226 * Apply one page of per-item pricebook entries to Addify B2B product meta.
227 *
228 * @param array $pricebook_items Items from one API page.
229 * @param string $role WordPress user role.
230 * @param array $zi_item_ids Map of Zoho item ID → WP product ID.
231 */
232 private function apply_addify_items_page( array $pricebook_items, string $role, array $zi_item_ids ): void {
233 foreach ( $pricebook_items as $itemlist ) {
234 if ( ! isset( $zi_item_ids[ $itemlist['item_id'] ] ) ) {
235 continue;
236 }
237 $product_id = (int) $zi_item_ids[ $itemlist['item_id'] ];
238
239 if ( is_array( $itemlist['price_brackets'] ?? null ) && ! empty( $itemlist['price_brackets'] ) ) {
240 $bracket = $itemlist['price_brackets'][0];
241 $formatted_price = array(
242 'start_quantity' => str_replace( '.', wc_get_price_decimal_separator(), $bracket['start_quantity'] ?? 0 ),
243 'end_quantity' => str_replace( '.', wc_get_price_decimal_separator(), $bracket['end_quantity'] ?? 0 ),
244 'pricebook_rate' => str_replace( '.', wc_get_price_decimal_separator(), $bracket['pricebook_rate'] ?? 0 ),
245 );
246 } else {
247 $formatted_price = str_replace( '.', wc_get_price_decimal_separator(), $itemlist['pricebook_rate'] ?? 0 );
248 }
249
250 $metavalue = array(
251 'discount_type' => 'fixed_price',
252 'user_role' => $role,
253 'discount_value' => is_array( $formatted_price ) ? $formatted_price['pricebook_rate'] : $formatted_price,
254 'min_qty' => is_array( $formatted_price ) ? $formatted_price['start_quantity'] : '',
255 'max_qty' => is_array( $formatted_price ) ? $formatted_price['end_quantity'] : '',
256 );
257 $this->upsert_role_base_price( $product_id, $role, $metavalue );
258 }
259 }
260
261 /**
262 * Support For Addify B2B
263 *
264 * Processes page 1 synchronously. For per-item pricebooks with more pages,
265 * subsequent pages are queued via Action Scheduler (cmbird_pricelist_addify_page).
266 *
267 * @param int $pricebook_id pricebook id.
268 * @param string $role user role.
269 */
270 private function addify_b2b( int $pricebook_id, string $role ): bool {
271 $price_book = $this->get_zoho_pricebook( $pricebook_id );
272 if ( empty( $price_book ) ) {
273 return false;
274 }
275
276 $zi_item_ids = $this->get_zoho_products();
277 $pricebook_type = $price_book['pricebook_type'] ?? '';
278
279 if ( 'fixed_percentage' === $pricebook_type ) {
280 $percentage = isset( $price_book['percentage'] ) ? $price_book['percentage'] : 0;
281 $orderby = ! empty( $price_book['is_increase'] ) ? 'percentage_increase' : 'percentage_decrease';
282 foreach ( array_values( $zi_item_ids ) as $product_id ) {
283 $metavalue = array(
284 'discount_type' => $orderby,
285 'user_role' => $role,
286 'discount_value' => str_replace( '.', wc_get_price_decimal_separator(), $percentage ),
287 'min_qty' => '',
288 'max_qty' => '',
289 );
290 $this->upsert_role_base_price( (int) $product_id, $role, $metavalue );
291 }
292 return true;
293 }
294
295 // per_item: process page 1 (already fetched by get_zoho_pricebook), queue the rest.
296 $this->apply_addify_items_page( $price_book['pricebook_items'] ?? array(), $role, $zi_item_ids );
297 if ( ! empty( $price_book['_has_more_page'] ) ) {
298 as_schedule_single_action( time(), 'cmbird_pricelist_addify_page', array( $pricebook_id, $role, 2 ), 'commercebird' );
299 }
300 return true;
301 }
302
303 /**
304 * Action Scheduler handler: process one page of Addify B2B per-item entries.
305 *
306 * @param int $pricebook_id Zoho pricebook ID.
307 * @param string $role WordPress user role.
308 * @param int $page Page number to fetch and process.
309 */
310 public function process_addify_page( int $pricebook_id, string $role, int $page ): void {
311 $result = $this->fetch_pricebook_page( $pricebook_id, $page );
312 if ( empty( $result['pricebook'] ) ) {
313 return;
314 }
315 $zi_item_ids = $this->get_zoho_products();
316 $this->apply_addify_items_page( $result['pricebook']['pricebook_items'] ?? array(), $role, $zi_item_ids );
317 if ( $result['has_more_page'] ) {
318 as_schedule_single_action( time(), 'cmbird_pricelist_addify_page', array( $pricebook_id, $role, $page + 1 ), 'commercebird' );
319 }
320 }
321
322 /**
323 * Apply one page of per-item pricebook entries to WC B2B group product meta.
324 *
325 * @param array $pricebook_items Items from one API page.
326 * @param int $group_id WooCommerce B2B group post ID.
327 * @param array $zi_item_ids Map of Zoho item ID → WP product ID.
328 */
329 private function apply_wc_b2b_items_page( array $pricebook_items, int $group_id, array $zi_item_ids ): void {
330 foreach ( $pricebook_items as $pricebook_item ) {
331 if ( ! isset( $zi_item_ids[ $pricebook_item['item_id'] ] ) ) {
332 continue;
333 }
334 $product_id = $zi_item_ids[ $pricebook_item['item_id'] ];
335 $start_quantity = null;
336 $end_quantity = null;
337
338 if ( is_array( $pricebook_item['price_brackets'] ?? null ) && count( $pricebook_item['price_brackets'] ) > 0 ) {
339 $price = $pricebook_item['price_brackets'][0]['pricebook_rate'];
340 $start_quantity = $pricebook_item['price_brackets'][0]['start_quantity'];
341 $end_quantity = $pricebook_item['price_brackets'][0]['end_quantity'];
342 } else {
343 $price = $pricebook_item['pricebook_rate'];
344 }
345
346 // Merge into existing per-group meta so saving group A does not erase group B's prices on the same product.
347 $existing_prices = get_post_meta( $product_id, 'wcb2b_product_group_prices', true );
348 $existing_min = get_post_meta( $product_id, 'wcb2b_product_group_min', true );
349 $existing_max = get_post_meta( $product_id, 'wcb2b_product_group_max', true );
350 $product_group_prices = is_array( $existing_prices ) ? $existing_prices : array();
351 $product_group_min = is_array( $existing_min ) ? $existing_min : array();
352 $product_group_max = is_array( $existing_max ) ? $existing_max : array();
353
354 if ( ! isset( $product_group_prices[ $group_id ] ) || ! is_array( $product_group_prices[ $group_id ] ) ) {
355 $product_group_prices[ $group_id ] = array();
356 }
357
358 $product_group_prices[ $group_id ]['regular_price'] = $price;
359 update_post_meta( $product_id, 'wcb2b_product_group_prices', $product_group_prices );
360
361 if ( null !== $start_quantity ) {
362 $product_group_min[ $group_id ] = $start_quantity;
363 update_post_meta( $product_id, 'wcb2b_product_group_min', $product_group_min );
364 }
365 if ( null !== $end_quantity ) {
366 $product_group_max[ $group_id ] = $end_quantity;
367 update_post_meta( $product_id, 'wcb2b_product_group_max', $product_group_max );
368 }
369 }
370 }
371
372 /**
373 * Update price book for each group and its products.
374 *
375 * Processes page 1 synchronously. For per-item pricebooks with more pages,
376 * subsequent pages are queued via Action Scheduler (cmbird_pricelist_wc_b2b_page).
377 *
378 * @param string $group_settings Group settings in JSON format.
379 *
380 * @return bool True if successful, false otherwise.
381 */
382 private function wc_b2b( $group_settings ): bool {
383 $group_settings = json_decode( $group_settings, true );
384 if ( empty( $group_settings ) ) {
385 return false;
386 }
387 // Force-refresh the product map so newly imported items are visible to this save.
388 delete_transient( 'zoho_products' );
389 foreach ( $group_settings as $group_setting ) {
390 $group_id = (int) $group_setting['key'];
391 $pricebook_id = (int) $group_setting['value'];
392 // Bypass the 24h pricebook cache so changes made in Zoho take effect on re-import.
393 delete_transient( 'zoho_pricelist_' . $pricebook_id );
394 $price_book = $this->get_zoho_pricebook( $pricebook_id );
395 if ( empty( $price_book ) ) {
396 continue;
397 }
398 $zi_item_ids = $this->get_zoho_products();
399 switch ( $price_book['pricebook_type'] ) {
400 case 'fixed_percentage':
401 $percentage = isset( $price_book['percentage'] ) ? $price_book['percentage'] : 0;
402 update_post_meta( $group_id, 'wcb2b_group_discount', $percentage );
403 update_post_meta( $group_id, 'pricebook_id', $pricebook_id );
404 update_post_meta( $group_id, 'pricebook_name', $price_book['name'] );
405 break;
406 case 'per_item':
407 // Tag the group itself before iterating items so it stays attached even if no products match yet.
408 update_post_meta( $group_id, 'pricebook_id', $pricebook_id );
409 update_post_meta( $group_id, 'pricebook_name', $price_book['name'] );
410 // Process page 1 items (already fetched by get_zoho_pricebook), queue the rest.
411 $this->apply_wc_b2b_items_page( $price_book['pricebook_items'] ?? array(), $group_id, $zi_item_ids );
412 if ( ! empty( $price_book['_has_more_page'] ) ) {
413 as_schedule_single_action( time(), 'cmbird_pricelist_wc_b2b_page', array( $pricebook_id, $group_id, 2 ), 'commercebird' );
414 }
415 break;
416 default:
417 break;
418 }
419 }
420
421 return true;
422 }
423
424 /**
425 * Action Scheduler handler: process one page of per-item WC B2B pricebook entries.
426 *
427 * @param int $pricebook_id Zoho pricebook ID.
428 * @param int $group_id WooCommerce B2B group post ID.
429 * @param int $page Page number to fetch and process.
430 */
431 public function process_wc_b2b_page( int $pricebook_id, int $group_id, int $page ): void {
432 $result = $this->fetch_pricebook_page( $pricebook_id, $page );
433 if ( empty( $result['pricebook'] ) ) {
434 return;
435 }
436 $zi_item_ids = $this->get_zoho_products();
437 $this->apply_wc_b2b_items_page( $result['pricebook']['pricebook_items'] ?? array(), $group_id, $zi_item_ids );
438 if ( $result['has_more_page'] ) {
439 as_schedule_single_action( time(), 'cmbird_pricelist_wc_b2b_page', array( $pricebook_id, $group_id, $page + 1 ), 'commercebird' );
440 }
441 }
442
443 /**
444 * Retrieves and caches the WooCommerce B2B groups.
445 *
446 * @return array An associative array of group IDs as keys and their labels as values.
447 */
448 public function wc_b2b_groups() {
449 // Stored as a wp_option (not a transient) so external object caches cannot evict it
450 // out from under the admin UI; invalidated explicitly on save_post of wcb2b_group.
451 $in_cache = get_option( 'cmbird_wcb2b_groups' );
452 if ( ! empty( $in_cache ) ) {
453 return $in_cache;
454 }
455 global $wpdb;
456
457 $results = $wpdb->get_results(
458 "SELECT ID AS id, post_title AS label
459 FROM {$wpdb->posts} p
460 WHERE p.post_type = 'wcb2b_group'
461 AND p.post_status = 'publish'",
462 ARRAY_A // Return results as an associative array.
463 );
464
465 if ( empty( $results ) ) {
466 return array();
467 }
468 $groups = wp_list_pluck( $results, 'label', 'id' );
469 update_option( 'cmbird_wcb2b_groups', $groups, true );
470
471 return $groups;
472 }
473 }
474