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-order-sync.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-order-sync.php
1364 lines
1 <?php
2 /**
3 * Class for handling Zoho Inventory order sync related functions.
4 */
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 class CMBIRD_Order_Sync_ZI {
10
11 /**
12 * Zoho Inventory organization ID.
13 *
14 * @var string
15 */
16 private $zoho_inventory_oid;
17
18 /**
19 * Zoho Inventory API base URL.
20 *
21 * @var string
22 */
23 private $zoho_inventory_url;
24
25 /**
26 * Zoho Inventory domain code (e.g. 'in' for India).
27 *
28 * @var string
29 */
30 private $zoho_inventory_domain;
31
32 /**
33 * Location ID for Zoho Inventory.
34 *
35 * @var string
36 */
37 private $location_id;
38
39 /**
40 * Map of location_id => parent_location_id for Zoho Inventory.
41 *
42 * @var array<string, string>
43 */
44 private $location_parent_map = array();
45
46 /**
47 * Map of Zoho location IDs to arrays of WooCommerce billing country codes.
48 * Used to route orders to the correct location based on the customer's country.
49 *
50 * @var array<string, string[]>
51 */
52 private $location_country_map;
53
54 /**
55 * Whether to enable order status sync.
56 *
57 * @var bool
58 */
59 private $enable_order_status;
60
61 /**
62 * Whether to enable auto-numbering for orders.
63 *
64 * @var bool
65 */
66 private $enable_auto_number;
67
68 /**
69 * Whether to enable order cycle sync.
70 *
71 * @var bool
72 */
73 private $enable_order_cycle;
74
75 /**
76 * Prefix for order numbers.
77 *
78 * @var string
79 */
80 private $order_prefix;
81
82 /**
83 * Custom fields for Zoho Inventory sync.
84 *
85 * @var array
86 */
87 private $custom_fields;
88
89 /**
90 * Whether to include tax in calculations.
91 *
92 * @var bool
93 */
94 private $include_tax;
95
96 /**
97 * Initialize the class.
98 */
99 public function __construct() {
100 $zoho_inventory_access_token = get_option( 'cmbird_zoho_inventory_access_token' );
101 if ( ! empty( $zoho_inventory_access_token ) ) {
102 add_action( 'woocommerce_update_order', array( $this, 'salesorder_void' ) );
103 // get options.
104 $this->zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
105 $this->zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );
106 $this->zoho_inventory_domain = get_option( 'cmbird_zoho_inventory_domain' );
107 $this->location_id = get_option( 'cmbird_zoho_location_id_status' );
108 $this->location_parent_map = (array) get_option( 'cmbird_zoho_location_parent_map', array() );
109 $this->location_country_map = (array) get_option( 'cmbird_zoho_location_country_map_status', array() );
110 $this->enable_order_status = get_option( 'cmbird_zoho_enable_order_status_status' );
111 $this->enable_auto_number = get_option( 'cmbird_zoho_enable_auto_number_status' );
112 $this->enable_order_cycle = get_option( 'cmbird_zoho_enable_sales_order_cycle_status' );
113 $this->order_prefix = get_option( 'cmbird_zoho_order_prefix_status' );
114 $stored_custom_fields = json_decode( get_option( 'cmbird_wootozoho_custom_fields' ), true );
115 if ( is_array( $stored_custom_fields ) && ( isset( $stored_custom_fields['salesorder'] ) || isset( $stored_custom_fields['item'] ) ) ) {
116 $this->custom_fields = is_array( $stored_custom_fields['salesorder'] ?? null ) ? $stored_custom_fields['salesorder'] : array();
117 } else {
118 $this->custom_fields = is_array( $stored_custom_fields ) ? $stored_custom_fields : array();
119 }
120 $this->include_tax = get_option( 'woocommerce_prices_include_tax' ) === 'yes';
121 } else {
122 return;
123 }
124 }
125
126 /**
127 * Get the parent_location_id for a given location_id from the saved map.
128 *
129 * @param string $location_id The Zoho location ID to look up.
130 * @return string The parent_location_id, or empty string if not found.
131 */
132 private function get_parent_location_id( string $location_id ): string {
133 if ( ! isset( $this->location_parent_map[ $location_id ] ) ) {
134 return '';
135 }
136 $parent = $this->location_parent_map[ $location_id ];
137 return ( is_string( $parent ) && '' !== $parent ) ? $parent : '';
138 }
139
140 /**
141 * Function to map customer on checkout before placing order
142 *
143 * @param int $order_id Order ID.
144 */
145 public function cmbird_zi_sync_customer_checkout( $order_id ) {
146 // $fd = fopen( __DIR__ . '/cmbird_zi_sync_customer_checkout.txt', 'w+' );
147 $order = wc_get_order( $order_id );
148 $userid = $order->get_user_id();
149 $is_guest_order = empty( $userid );
150 $user_company = $order->get_billing_company();
151 $user_email = $order->get_billing_email();
152
153 // For guest orders, get contact ID from order meta; for regular users, from user meta.
154 if ( $is_guest_order ) {
155 $zi_customer_id = $order->get_meta( 'zi_contact_id', true );
156 } else {
157 $zi_customer_id = get_user_meta( $userid, 'zi_contact_id', true );
158 }
159
160 // Get currency code of the order.
161 if ( $is_guest_order ) {
162 $currency_id = intval( $order->get_meta( 'zi_currency_id', true ) );
163 } else {
164 $currency_id = intval( get_user_meta( $userid, 'zi_currency_id', true ) );
165 }
166 if ( empty( $currency_id ) ) {
167 $currency_code = $order->get_currency();
168 $multi_currency_handle = new CMBIRD_Multicurrency_Zoho();
169 $currency_id = $multi_currency_handle->zoho_currency_data( $currency_code, $userid, $order );
170 }
171
172 if ( $zi_customer_id ) {
173 $zoho_inventory_oid = $this->zoho_inventory_oid;
174 $zoho_inventory_url = $this->zoho_inventory_url;
175 $get_url = $zoho_inventory_url . 'inventory/v1/contacts/' . $zi_customer_id . '/?organization_id=' . $zoho_inventory_oid;
176
177 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
178 $json = $execute_curl_call_handle->execute_curl_call_get( $get_url );
179 // fwrite($fd,PHP_EOL.'customer_json: '.print_r($json, true)); --- IGNORE ---.
180 $code = $json->code;
181 if ( 0 !== $code && '0' !== $code ) {
182 // For guest orders, clear contact data immediately if validation fails.
183 // For registered users, be more cautious - only clear if it's a definitive "not found" error.
184 $should_clear_contact_data = false;
185
186 if ( 1003 === $code || '1003' === $code ) {
187 // For registered users, only clear if it's a specific "contact not found" error (code 1003).
188 // Other errors might be temporary (rate limiting, API issues, etc.).
189 $should_clear_contact_data = true;
190 }
191
192 if ( $should_clear_contact_data ) {
193 // Clear contact data - from order meta for guest orders, user meta for regular users.
194 if ( $is_guest_order ) {
195 $order->delete_meta_data( 'zi_contact_id' );
196 $order->delete_meta_data( 'zi_billing_address_id' );
197 $order->delete_meta_data( 'zi_primary_contact_id' );
198 $order->delete_meta_data( 'zi_shipping_address_id' );
199 $order->delete_meta_data( 'zi_created_time' );
200 $order->delete_meta_data( 'zi_last_modified_time' );
201 $order->save();
202 } else {
203 delete_user_meta( $userid, 'zi_contact_id' );
204 delete_user_meta( $userid, 'zi_billing_address_id' );
205 delete_user_meta( $userid, 'zi_primary_contact_id' );
206 delete_user_meta( $userid, 'zi_shipping_address_id' );
207 delete_user_meta( $userid, 'zi_created_time' );
208 delete_user_meta( $userid, 'zi_last_modified_time' );
209 }
210 $zi_customer_id = '';
211 } else {
212 // save the billing and shipping address IDs if contact exists.
213 foreach ( $json->contact as $key => $value ) {
214 if ( 'billing_address_id' === $key ) {
215 if ( $is_guest_order ) {
216 $order->update_meta_data( 'zi_billing_address_id', $value );
217 $order->save();
218 } else {
219 update_user_meta( $userid, 'zi_billing_address_id', $value );
220 }
221 }
222 if ( 'shipping_address_id' === $key ) {
223 if ( $is_guest_order ) {
224 $order->update_meta_data( 'zi_shipping_address_id', $value );
225 $order->save();
226 } else {
227 update_user_meta( $userid, 'zi_shipping_address_id', $value );
228 }
229 }
230 }
231 }
232 // If we don't clear the contact data, keep the existing $zi_customer_id and proceed.
233 }
234 }
235
236 /**
237 * Syncing customer if its not in Zoho yet.
238 */
239 if ( empty( $zi_customer_id ) ) {
240
241 // First check based on customer email address.
242 $zoho_inventory_oid = $this->zoho_inventory_oid;
243 $zoho_inventory_url = $this->zoho_inventory_url;
244 $url = $zoho_inventory_url . 'inventory/v1/contacts?organization_id=' . $zoho_inventory_oid . '&email=' . $user_email;
245
246 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
247 $json = $execute_curl_call_handle->execute_curl_call_get( $url );
248
249 $code = $json->code;
250 // $message = $json->message;
251 if ( 0 === $code || '0' === $code ) {
252 // fwrite( $fd, PHP_EOL . 'email found: ' . print_r( $json, true ) );
253 if ( empty( $json->contacts ) ) {
254 // Second check based on Company Name.
255 if ( $user_company ) {
256 $company_name = str_replace( ' ', '%20', $user_company );
257 $url = $zoho_inventory_url . 'inventory/v1/contacts?organization_id=' . $zoho_inventory_oid . '&filter_by=Status.Active&search_text=' . $company_name;
258
259 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
260 $json = $execute_curl_call_handle->execute_curl_call_get( $url );
261
262 $code = $json->code;
263 if ( 0 === $code || '0' === $code ) {
264 if ( empty( $json->contacts ) ) {
265 $zi_customer_id = $this->create_contact_for_order( $userid, $order_id, $is_guest_order );
266 } else {
267 foreach ( $json->contacts[0] as $key => $value ) {
268 if ( 'contact_id' === $key ) {
269 $zi_customer_id = $value;
270 // Store contact ID - in order meta for guest orders, user meta for regular users.
271 if ( $is_guest_order ) {
272 $order->update_meta_data( 'zi_contact_id', $zi_customer_id );
273 $order->save();
274 } else {
275 update_user_meta( $userid, 'zi_contact_id', $zi_customer_id );
276 }
277 }
278 }
279 $zi_customer_id = $this->create_contact_person_for_order( $userid, $order_id, $is_guest_order );
280 }
281 } else {
282 // Company search failed, create new contact.
283 $zi_customer_id = $this->create_contact_for_order( $userid, $order_id, $is_guest_order );
284 }
285 } else {
286 $zi_customer_id = $this->create_contact_for_order( $userid, $order_id, $is_guest_order );
287 }
288 } else {
289 // fwrite($fd,PHP_EOL.'Contacts : '.print_r($json->contacts,true));
290 foreach ( $json->contacts[0] as $key => $value ) {
291 if ( 'contact_id' === $key ) {
292 $zi_customer_id = $value;
293 // Store contact ID - in order meta for guest orders, user meta for regular users.
294 if ( $is_guest_order ) {
295 $order->update_meta_data( 'zi_contact_id', $zi_customer_id );
296 $order->save();
297 } else {
298 update_user_meta( $userid, 'zi_contact_id', $zi_customer_id );
299 }
300 }
301 }
302 }
303 } else {
304 // Email search failed, create new contact.
305 $zi_customer_id = $this->create_contact_for_order( $userid, $order_id, $is_guest_order );
306 }
307 // Http request not processed properly.
308 // echo $message;
309 // Final fallback: if still no contact and this is a guest order, always create contact.
310 if ( empty( $zi_customer_id ) && $is_guest_order ) {
311 $zi_customer_id = $this->create_contact_for_order( $userid, $order_id, $is_guest_order );
312 }
313 return $zi_customer_id;
314 } else {
315 $zoho_inventory_oid = $this->zoho_inventory_oid;
316 $zoho_inventory_url = $this->zoho_inventory_url;
317 $get_url = $zoho_inventory_url . 'inventory/v1/contacts/' . $zi_customer_id . '/contactpersons/?organization_id=' . $zoho_inventory_oid;
318
319 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
320 $contactpersons_response = $execute_curl_call_handle->execute_curl_call_get( $get_url );
321
322 // fwrite( $fd, PHP_EOL . 'Contactpersons: ' . print_r($contactpersons_response, true) );
323
324 // first check within contactpersons endpoint and then map it with that contactperson if email-id matches.
325 if ( 0 === $contactpersons_response->code || '0' === $contactpersons_response->code ) {
326 if ( ! empty( $contactpersons_response->contact_persons ) ) {
327 foreach ( $contactpersons_response->contact_persons as $key => $contact_persons ) {
328 $person_email = trim( $contact_persons->email );
329 if ( trim( $user_email ) === $person_email ) {
330 /* Match Contact */
331 $contactid = $contact_persons->contact_person_id;
332 // Store contact person ID - in order meta for guest orders, user meta for regular users.
333 if ( $is_guest_order ) {
334 $order->update_meta_data( 'zi_contactperson_id_' . $key, $contactid );
335 $order->save();
336 } else {
337 update_user_meta( $userid, 'zi_contactperson_id_' . $key, $contactid );
338 }
339 if ( true === $contact_persons->is_primary_contact || 1 === $contact_persons->is_primary_contact ) {
340 if ( ! $is_guest_order ) {
341 $contact_class_handle = new CMBIRD_Contact_ZI();
342 $contact_class_handle->cmbird_contact_update_function( $userid, $order_id );
343 }
344 } elseif ( ! $is_guest_order ) {
345 $contact_class_handle = new CMBIRD_Contact_ZI();
346 $contact_class_handle->cmbird_update_contact_person( $userid, $order_id );
347 }
348 }
349 }
350 } else {
351 $get_url = $zoho_inventory_url . 'inventory/v1/contacts/' . $zi_customer_id . '/?organization_id=' . $zoho_inventory_oid;
352 $contact_res = $execute_curl_call_handle->execute_curl_call_get( $get_url );
353 if ( ( 0 === $contact_res->code || '0' === $contact_res->code ) && ! empty( $contact_res->contact ) ) {
354 // Access the contact object directly (no loop needed - response contains single contact object).
355 if ( trim( $contact_res->contact->email ) === trim( $user_email ) ) {
356 $contact_class_handle = new CMBIRD_Contact_ZI();
357 $contact_class_handle->cmbird_contact_update_function( $userid, $order_id );
358 } else {
359 $contact_class_handle = new CMBIRD_Contact_ZI();
360 $contact_class_handle->cmbird_create_contact_person( $userid );
361 }
362 }
363 }
364 }
365 // fwrite( $fd, PHP_EOL . 'No contactpersons ' );
366 }
367 // fclose( $fd );
368 return $zi_customer_id;
369 }
370
371 /**
372 * Function for admin zoho sync call.
373 *
374 * @param int $order_id Order ID.
375 */
376 public function zi_order_sync( $order_id ) {
377 $order = wc_get_order( $order_id );
378 if ( ! $order ) {
379 return;
380 }
381 // return if there is no zoho inventory access token.
382 $zoho_inventory_access_token = get_option( 'cmbird_zoho_inventory_access_token' );
383 if ( empty( $zoho_inventory_access_token ) ) {
384 return;
385 }
386
387 $current_time = time();
388 $last_time = $order->get_meta( 'zi_last_order_sync_time', true );
389 if ( ! empty( $last_time ) && $current_time - $last_time < 60 ) {
390 return;
391 }
392
393 // Create an action hook to prevent orders getting synced if it contains an array of statuses.
394 $ignored_statuses = apply_filters( 'cmbird_zi_order_sync_ignored_statuses', array( 'cancelled', 'failed', 'draft', 'trash' ) );
395 if ( in_array( $order->get_status(), $ignored_statuses ) ) {
396 return;
397 }
398
399 $zi_sales_order_id = $order->get_meta( 'zi_salesorder_id' );
400 $userid = $order->get_user_id();
401 $is_guest_order = empty( $userid );
402
403 // For guest orders, get contact details from order meta instead of user meta.
404 if ( $is_guest_order ) {
405 $customer_id = $order->get_meta( 'zi_contact_id', true );
406 $billing_id = $order->get_meta( 'zi_billing_address_id', true );
407 $shipping_id = $order->get_meta( 'zi_shipping_address_id', true );
408 } else {
409 $customer_id = get_user_meta( $userid, 'zi_contact_id', true );
410 $billing_id = get_user_meta( $userid, 'zi_billing_address_id', true );
411 $shipping_id = get_user_meta( $userid, 'zi_shipping_address_id', true );
412 }
413 // Sync customer if not exists.
414 if ( empty( $customer_id ) ) {
415 $customer_id = $this->cmbird_zi_sync_customer_checkout( $order_id );
416 } elseif ( empty( $billing_id ) || empty( $shipping_id ) ) {
417 // run cmbird_contact_update_function() to update address IDs.
418 $contact_class_handle = new CMBIRD_Contact_ZI();
419 $contact_class_handle->cmbird_contact_update_function( $userid, $order_id );
420 }
421 // 2nd attempt - For guest orders, get address IDs from order meta; for regular users, from user meta.
422 if ( $is_guest_order ) {
423 $billing_id = $order->get_meta( 'zi_billing_address_id', true );
424 $shipping_id = $order->get_meta( 'zi_shipping_address_id', true );
425 } else {
426 $billing_id = get_user_meta( $userid, 'zi_billing_address_id', true );
427 $shipping_id = get_user_meta( $userid, 'zi_shipping_address_id', true );
428 }
429
430 $line_items = $this->build_line_items( $order );
431 if ( empty( $line_items ) ) {
432 return;
433 }
434
435 // Final validation: Ensure customer_id is not 0 or '0' before creating request.
436 if ( empty( $customer_id ) ) {
437 $order->add_order_note( 'Zoho Order Sync Failed: Contact does not exist or could not be created.' );
438 return;
439 }
440
441 $pdt = array(
442 'customer_id' => $customer_id,
443 'date' => $order->get_date_created()->format( 'Y-m-d' ),
444 'line_items' => $line_items,
445 'is_discount_before_tax' => true,
446 'discount_type' => 'item_level',
447 'price_precision' => '2',
448 'notes' => preg_replace( '/[^A-Za-z0-9\-]/', ' ', $order->get_customer_note() ),
449 'billing_address_id' => $billing_id,
450 'shipping_address_id' => $shipping_id,
451 'delivery_method' => $order->get_shipping_method(),
452 'is_inclusive_tax' => $this->include_tax,
453 'shipping_charge' => $order->get_shipping_total(),
454 'order_status' => $this->enable_order_status ? 'draft' : 'confirmed',
455 );
456
457 if ( $this->location_id ) {
458 $effective_location_id = $this->location_id;
459 $shipping_country = $order->get_shipping_country();
460 if ( ! empty( $shipping_country ) && ! empty( $this->location_country_map ) ) {
461 foreach ( $this->location_country_map as $mapped_location_id => $countries ) {
462 if ( is_array( $countries ) && in_array( $shipping_country, $countries, true ) ) {
463 $effective_location_id = $mapped_location_id;
464 break;
465 }
466 }
467 }
468 $parent_id = $this->get_parent_location_id( $effective_location_id );
469 $pdt['location_id'] = $parent_id ? $parent_id : $effective_location_id;
470 }
471
472 // determine which tax id should be used for shipping charges. this.
473 // method wraps the direct lookup and item-fallback logic so it can be.
474 // unit tested in isolation.
475 $shipping_tax_id = $this->determine_shipping_tax_id( $order, $line_items );
476 if ( $shipping_tax_id ) {
477 $pdt['shipping_charge_tax_id'] = $shipping_tax_id;
478 }
479
480 $fees = $this->get_order_fees( $order );
481 $pdt = array_merge( $pdt, $fees );
482
483 $pdt['custom_fields'] = $this->prepare_custom_fields( $order );
484 $reference = $this->prepare_reference_number( $order );
485
486 if ( $this->enable_auto_number ) {
487 $pdt['reference_number'] = $reference;
488 } else {
489 $pdt['salesorder_number'] = $order->get_id();
490 }
491
492 $userid = $order->get_user_id();
493 // get below data if zoho_inventory_domain is 'in'.
494 if ( 'in' === $this->zoho_inventory_domain ) {
495 $gst_no = get_user_meta( $userid, 'gst_no', true );
496 // if gst_no is empty then get it from order.
497 if ( empty( $gst_no ) ) {
498 $gst_no = $order->get_meta( '_gst_no', true );
499 }
500 if ( $gst_no ) {
501 $pdt['gst_no'] = $gst_no;
502 }
503
504 // Determine GST treatment based on billing company, GST number, and country.
505 $billing_company = $order->get_billing_company();
506 $billing_country = $order->get_billing_country();
507
508 // GST treatment logic - prioritize company + GST combination first.
509 if ( ! empty( $billing_company ) && ! empty( $gst_no ) ) {
510 // Company with GST number.
511 $gst_treatment = 'business_gst';
512 } elseif ( ! empty( $billing_company ) && empty( $gst_no ) && 'IN' === $billing_country ) {
513 // Company without GST number in India.
514 $gst_treatment = 'business_none';
515 } elseif ( 'IN' !== $billing_country ) {
516 // Non-Indian customer (overseas).
517 $gst_treatment = 'overseas';
518 } else {
519 // Individual consumer in India.
520 $gst_treatment = 'consumer';
521 }
522
523 // Allow override from user meta or order meta if exists.
524 $meta_gst_treatment = get_user_meta( $userid, 'gst_treatment', true );
525 if ( empty( $meta_gst_treatment ) ) {
526 $meta_gst_treatment = $order->get_meta( 'gst_treatment', true );
527 }
528 if ( ! empty( $meta_gst_treatment ) ) {
529 $gst_treatment = $meta_gst_treatment;
530 }
531
532 $pdt['gst_treatment'] = $gst_treatment;
533 $pdt['place_of_supply'] = $order->get_billing_state();
534 }
535
536 $is_new_order = ! $zi_sales_order_id;
537 $response_msg = $zi_sales_order_id ? $this->single_saleorder_zoho_inventory_update( $order_id, $zi_sales_order_id, wp_json_encode( $pdt ) ) : $this->single_saleorder_zoho_inventory( wp_json_encode( $pdt ), $order_id );
538
539 $order->update_meta_data( 'zi_body_request', wp_json_encode( $pdt ) );
540 $order->update_meta_data( 'zi_salesorder_id', $response_msg['zi_salesorder_id'] );
541 $order->update_meta_data( 'zi_last_order_sync_time', $current_time );
542 $order->add_order_note( 'Zoho Order Sync: ' . $response_msg['message'] );
543 $order->save();
544
545 // Trigger sales order cycle if enabled and this is a newly created order.
546 if ( $is_new_order && $this->enable_order_cycle && ! empty( $response_msg['zi_salesorder_id'] ) ) {
547 $this->trigger_sales_order_cycle( $order_id, $response_msg['zi_salesorder_id'] );
548 }
549 }
550
551 /**
552 * Build line items for a given order.
553 *
554 * @param WC_Order $order WooCommerce order object.
555 *
556 * @return array Array of line items.
557 */
558 private function build_line_items( $order ) {
559 $items = array();
560
561 // determine the location that should be applied to each line item.
562 // we replicate the same country‑map logic used later when creating the.
563 // parent order object so the line entries match the order location.
564 $resolved_location_id = '';
565 if ( $this->location_id ) {
566 $effective_location_id = $this->location_id;
567 $shipping_country = $order->get_shipping_country();
568 if ( ! empty( $shipping_country ) && ! empty( $this->location_country_map ) ) {
569 foreach ( $this->location_country_map as $mapped_location_id => $countries ) {
570 if ( is_array( $countries ) && in_array( $shipping_country, $countries, true ) ) {
571 $effective_location_id = $mapped_location_id;
572 break;
573 }
574 }
575 }
576 $parent_id = $this->get_parent_location_id( $effective_location_id );
577 $resolved_location_id = $parent_id ? $parent_id : $effective_location_id;
578 }
579
580 foreach ( $order->get_items() as $item_id => $item ) {
581 $product = $item->get_product();
582 if ( ! $product ) {
583 continue;
584 }
585
586 $name = $item->get_name();
587 $meta = $item->get_formatted_meta_data();
588 $desc = '';
589 if ( ! empty( $meta ) ) {
590 foreach ( $meta as $meta_value ) {
591 $desc .= $meta_value->display_key . ': ' . wp_strip_all_tags( $meta_value->display_value ) . ' ';
592 }
593 $desc = sanitize_text_field( trim( $desc ) );
594 }
595
596 $quantity = $item->get_quantity();
597 $subtotal = $item->get_subtotal();
598 $total = $item->get_total();
599 $rate = $subtotal / max( 1, $quantity );
600 $discount = $subtotal > $total ? $subtotal - $total : 0;
601
602 $tax_data = $item->get_taxes();
603 $tax_id = '';
604 $tax_percent = 0;
605
606 // Preserve original tax/discount calculation logic but ensure the line is created.
607 // and added even when there are no taxes for the item.
608 if ( ! empty( $tax_data['subtotal'] ) ) {
609 $tax_rate_ids = array_keys( $tax_data['subtotal'] );
610 $tax_count = count( $tax_rate_ids );
611
612 // Check if this is a compound tax (multiple tax rates).
613 if ( $tax_count > 1 ) {
614 // Calculate total tax percentage from all rates.
615 $total_tax_percent = 0;
616 foreach ( $tax_rate_ids as $tax_rate_id ) {
617 $tax_rate = WC_Tax::_get_tax_rate( $tax_rate_id );
618 $total_tax_percent += floatval( $tax_rate['tax_rate'] );
619 }
620
621 // Try to find a matching tax group.
622 $tax_id = $this->zi_get_tax_group_id( $total_tax_percent );
623
624 if ( $tax_id ) {
625 // Found a matching tax group, use the total percentage.
626 $tax_percent = $total_tax_percent;
627 } else {
628 // No tax group found, fall back to first tax rate only.
629 $tax_rate_id = $tax_rate_ids[0];
630 $tax_rate = WC_Tax::_get_tax_rate( $tax_rate_id );
631 $tax_percent = $tax_rate['tax_rate'];
632
633 // For India domain, use tax name to determine IGST or CGST/SGST.
634 if ( 'in' === $this->zoho_inventory_domain ) {
635 $tax_name = isset( $tax_rate['tax_rate_name'] ) ? $tax_rate['tax_rate_name'] : '';
636 if ( stripos( $tax_name, 'IGST' ) !== false ) {
637 $tax_id = $this->zi_get_igst_tax_id( $tax_percent );
638 } elseif ( stripos( $tax_name, 'SGST' ) !== false || stripos( $tax_name, 'CGST' ) !== false ) {
639 $tax_id = $this->zi_get_tax_id( $tax_percent );
640 } else {
641 // Keep tax_id empty if tax name doesn't indicate IGST or CGST/SGST.
642 $tax_id = '';
643 }
644 } else {
645 // For non-India domains, use regular tax_id.
646 $tax_id = $this->zi_get_tax_id( $tax_percent );
647 }
648 }
649 } else {
650 // Single tax rate - use existing logic.
651 $tax_rate_id = $tax_rate_ids[0];
652 $tax_rate = WC_Tax::_get_tax_rate( $tax_rate_id );
653 $tax_percent = $tax_rate['tax_rate'];
654
655 // For India domain, use tax name to determine IGST or CGST/SGST.
656 if ( 'in' === $this->zoho_inventory_domain ) {
657 $tax_name = isset( $tax_rate['tax_rate_name'] ) ? $tax_rate['tax_rate_name'] : '';
658 if ( stripos( $tax_name, 'IGST' ) !== false ) {
659 $tax_id = $this->zi_get_igst_tax_id( $tax_percent );
660 } elseif ( stripos( $tax_name, 'SGST' ) !== false || stripos( $tax_name, 'CGST' ) !== false ) {
661 $tax_id = $this->zi_get_tax_id( $tax_percent );
662 } else {
663 // Keep tax_id empty if tax name doesn't indicate IGST or CGST/SGST.
664 $tax_id = '';
665 }
666 } else {
667 // For non-India domains, use regular tax_id.
668 $tax_id = $this->zi_get_tax_id( $tax_percent );
669 }
670 }
671 }
672
673 // Always build the line item (previous logic accidentally only added items when taxes existed).
674 $line = array(
675 'item_id' => get_post_meta( $product->get_id(), 'zi_item_id', true ),
676 'name' => $name,
677 'description' => $desc,
678 'quantity' => $quantity,
679 'rate' => number_format( $rate, 2, '.', '' ),
680 );
681
682 if ( $discount > 0 ) {
683 $line['discount'] = number_format( $discount, 2, '.', '' );
684 }
685 if ( $tax_id ) {
686 $line['tax_percentage'] = $tax_percent;
687 $line['tax_id'] = $tax_id;
688 }
689 // add resolved location id if available (may be mapped by country).
690 if ( ! empty( $resolved_location_id ) ) {
691 $line['location_id'] = $resolved_location_id;
692 }
693 $items[] = $line;
694 }
695 return $items;
696 }
697
698 /**
699 * Get the Zoho Inventory tax id for the shipping tax.
700 *
701 * @param WC_Order $order The WooCommerce order object.
702 * @return string The Zoho Inventory tax id, or empty string if no tax is applicable.
703 */
704 private function get_shipping_tax_id( $order ) {
705 $shipping_total = $order->get_shipping_total();
706 $shipping_tax = $order->get_shipping_tax();
707 if ( $shipping_total > 0 && $shipping_tax > 0 ) {
708 // compute the overall tax percentage using the provided.
709 // total tax amount. this is the correct WooCommerce API and.
710 // works in all contexts (including admin overrides).
711 $percent = ( $shipping_tax / $shipping_total ) * 100;
712 $percent = round( $percent, 2 );
713
714 // try to match a compound tax group first.
715 $tax_id = $this->zi_get_tax_group_id( $percent );
716 if ( $tax_id ) {
717 return $tax_id;
718 }
719
720 // no group found, fall back to single tax lookup.
721 return $this->zi_get_tax_id( $percent );
722 }
723 return '';
724 }
725 /**
726 * Determine the appropriate shipping tax id, falling back to line item tax ids
727 * when a direct lookup fails. Added to isolate logic for unit testing and to
728 * ensure shipping uses the same tax group as the order lines when possible.
729 *
730 * @param WC_Order $order The WooCommerce order object.
731 * @param array $line_items Array of line item data previously generated.
732 * @return string Shipping tax id or empty string.
733 */
734 public function determine_shipping_tax_id( $order, $line_items = array() ) {
735 $shipping_tax_id = $this->get_shipping_tax_id( $order );
736 if ( ! $shipping_tax_id && ! empty( $line_items ) ) {
737 foreach ( $line_items as $line ) {
738 if ( ! empty( $line['tax_id'] ) ) {
739 $shipping_tax_id = $line['tax_id'];
740 break;
741 }
742 }
743 }
744 return $shipping_tax_id;
745 }
746
747 /**
748 * Retrieves the fees associated with an order and formats them for Zoho Inventory.
749 *
750 * @param WC_Order $order The WooCommerce order object.
751 *
752 * @return array Associative array containing the fee amount and description.
753 */
754 private function get_order_fees( $order ) {
755 $fees = $order->get_fees();
756 $data = array();
757 foreach ( $fees as $fee ) {
758 $amount = $fee->get_total();
759 if ( $amount > 0 ) {
760 $data['adjustment'] = $amount;
761 $data['adjustment_description'] = $fee->get_name();
762 }
763 }
764 return $data;
765 }
766
767 /**
768 * Prepare custom fields for the order.
769 *
770 * Iterates over the $custom_fields array and creates an associative array
771 * containing the custom field label and value.
772 *
773 * @param WC_Order $order The WooCommerce order object.
774 * @return array Associative array containing the custom field label and value.
775 */
776 private function prepare_custom_fields( $order ) {
777 $fields = array();
778 if ( is_array( $this->custom_fields ) ) {
779 foreach ( $this->custom_fields as $meta_key => $label ) {
780 $fields[] = array(
781 'label' => $label,
782 'value' => $order->get_meta( $meta_key ),
783 );
784 }
785 }
786 return $fields;
787 }
788
789 /**
790 * Prepare the reference number for the sales order.
791 *
792 * The reference number is used to identify the sales order in Zoho Inventory.
793 * It is generated by combining the order prefix (if set) with the transaction ID
794 * (if available) or the order ID.
795 *
796 * @param WC_Order $order The WooCommerce order object.
797 * @return string The reference number for the sales order.
798 */
799 private function prepare_reference_number( $order ) {
800 $transaction_id = $order->get_transaction_id();
801 if ( empty( $transaction_id ) ) {
802 $transaction_id = $order->get_meta( '_order_number', true );
803 }
804 if ( class_exists( 'WCJ_Order_Numbers' ) || class_exists( 'WC_Seq_Order_Number_Pro' ) ) {
805 return $this->order_prefix . $transaction_id;
806 }
807 if ( ! empty( $this->order_prefix ) ) {
808 return $this->order_prefix . '-' . $order->get_id();
809 }
810 return $order->get_id();
811 }
812
813 /**
814 * Trigger the sales order cycle to automatically create packages and invoices.
815 *
816 * This function makes a POST request to the Zoho Inventory API to trigger the
817 * sales order cycle for the specified sales order ID. It sends a request to:
818 * {api_url}/inventory/v1/salesorders/{salesorder_id}/triggersocycle?confirm_order=true&organization_id={org_id}
819 * with the body: {"execute_so_cycle":true}
820 *
821 * @param int $order_id The WooCommerce order ID.
822 * @param string $zi_sales_order_id The Zoho Inventory sales order ID.
823 * @return void
824 */
825 private function trigger_sales_order_cycle( $order_id, $zi_sales_order_id ) {
826 $order = wc_get_order( $order_id );
827 if ( ! $order ) {
828 return;
829 }
830
831 $zoho_inventory_url = $this->zoho_inventory_url;
832 $zoho_inventory_oid = $this->zoho_inventory_oid;
833
834 $url = $zoho_inventory_url . 'inventory/v1/salesorders/' . $zi_sales_order_id . '/triggersocycle?confirm_order=true&organization_id=' . $zoho_inventory_oid;
835 $body = wp_json_encode( array( 'execute_so_cycle' => true ) );
836
837 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
838 $response = $execute_curl_call_handle->execute_curl_call_post(
839 $url,
840 $body,
841 array( 'Content-Type' => 'application/json' )
842 );
843
844 if ( is_wp_error( $response ) ) {
845 $error_msg = 'Failed to trigger Zoho Sales Order Cycle: ' . $response->get_error_message();
846 $order->add_order_note( $error_msg );
847 $message = 'Error triggering Zoho Sales Order Cycle for Order ID: ' . $order_id . '. ' . $error_msg;
848 $class_common = new CMBIRD_Common_Functions();
849 $class_common->send_email( 'Error Triggering Zoho Sales Order Cycle', $message );
850 } else {
851 $json = $response;
852 $code = isset( $json->code ) ? $json->code : null;
853 $msg = isset( $json->message ) ? $json->message : 'Unknown response';
854
855 if ( '0' === $code || 0 === $code ) {
856 $order->add_order_note( 'Zoho Sales Order Cycle Triggered: Packages and invoices will be automatically created.' );
857 } else {
858 $error_msg = 'Failed to trigger Zoho Sales Order Cycle: ' . $msg;
859 $order->add_order_note( $error_msg );
860 $message = 'Error triggering Zoho Sales Order Cycle for Order ID: ' . $order_id . '. ' . $error_msg;
861 $class_common = new CMBIRD_Common_Functions();
862 $class_common->send_email( 'Error Triggering Zoho Sales Order Cycle', $message );
863 }
864 }
865 $order->save();
866 }
867
868 /**
869 * Void the sales order if cancelled in WooCommerce.
870 *
871 * @param int $order_id Order ID.
872 */
873 public function salesorder_void( $order_id ) {
874 if ( ! $order_id ) {
875 return;
876 }
877 $zoho_inventory_access_token = get_option( 'cmbird_zoho_inventory_access_token' );
878 if ( empty( $zoho_inventory_access_token ) ) {
879 return;
880 }
881
882 $order = wc_get_order( $order_id );
883 // return if order is already voided.
884 if ( $order->get_meta( 'zi_salesorder_void', true ) ) {
885 return;
886 }
887 $order_status = $order->get_status();
888
889 if ( 'cancelled' === $order_status || 'wc-merged' === $order_status ) {
890 $zi_sales_order_id = $order->get_meta( 'zi_salesorder_id', true );
891 $zoho_inventory_oid = $this->zoho_inventory_oid;
892 $zoho_inventory_url = $this->zoho_inventory_url;
893
894 $url = $zoho_inventory_url . 'inventory/v1/salesorders/' . $zi_sales_order_id . '/status/void?organization_id=' .
895 $zoho_inventory_oid;
896 $data = '';
897 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
898 $json = $execute_curl_call_handle->execute_curl_call_post( $url, $data );
899
900 $errmsg = $json->message;
901 $code = $json->code;
902 if ( '0' === $code || 0 === $code ) {
903 // Add order meta key "zi_salesorder_void" to true.
904 $order->update_meta_data( 'zi_salesorder_void', true );
905 $order->add_order_note( 'Zoho Order Void: ' . $errmsg );
906 $order->save();
907 return;
908 }
909 } else {
910 return;
911 }
912 }
913
914 /**
915 * Sync order from Woo to Zoho.
916 *
917 * @param string $pdt1 JSON string.
918 * @param int $order_id Order ID.
919 * @return string Error message
920 */
921 public function single_saleorder_zoho_inventory( $pdt1, $order_id ) {
922 // start logging.
923 // $fd = fopen( __DIR__ . '/order-sync-backend.txt', 'w+' );
924
925 $zoho_inventory_oid = $this->zoho_inventory_oid;
926 $zoho_inventory_url = $this->zoho_inventory_url;
927 $data = array(
928 'JSONString' => $pdt1,
929 'organization_id' => $zoho_inventory_oid,
930 );
931
932 // logging.
933 // fwrite($fd, PHP_EOL . 'Data log : ' . print_r($data, true));
934
935 if ( empty( $data['JSONString'] ) ) {
936 return '';
937 }
938 $ignore_auto_no = ( true === $this->enable_auto_number || 'true' === $this->enable_auto_number || '1' === $this->enable_auto_number || 1 === $this->enable_auto_number ) ? 'false' : 'true';
939 $url = $zoho_inventory_url . 'inventory/v1/salesorders?ignore_auto_number_generation=' . $ignore_auto_no;
940 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
941 $json = $execute_curl_call_handle->execute_curl_call_post( $url, $data );
942
943 // fwrite( $fd, PHP_EOL . 'Data log : ' . print_r( $json, true ) );
944 $response = array();
945 $code = $json->code;
946 $errmsg = $json->message;
947 // fwrite($fd, PHP_EOL . 'Code : ' . $code);
948
949 // Retry once if code is 400.
950 if ( 400 === $code || '400' === $code ) {
951 // Wait a brief moment before retry.
952 sleep( 1 );
953 $json = $execute_curl_call_handle->execute_curl_call_post( $url, $data );
954 $code = $json->code;
955 $errmsg = $json->message;
956 }
957
958 if ( '0' === $code || 0 === $code ) {
959 foreach ( $json->salesorder as $key => $value ) {
960
961 if ( 'salesorder_id' === $key ) {
962 $response['zi_salesorder_id'] = $value;
963 // $order->add_meta_data('zi_salesorder_id', $value, true);
964 }
965 }
966 } else {
967 // send email to admin with the error message.
968 // create message that contains the error message and order id.
969 $message = 'Error in Zoho Inventory Order Sync: ' . $errmsg;
970 // append the link to the order edit page.
971 $message .= '<br><br><a href="' . admin_url( 'admin.php?page=wc-orders&action=edit&id=' . $order_id ) . '">Click here to view the order in WP Admin</a>';
972 $message .= '<br><br>Order ID: ' . $order_id;
973 $message .= '<br><br>Request Body: ' . $pdt1;
974 $class_common = new CMBIRD_Common_Functions();
975 $class_common->send_email( 'Error in Zoho Order Sync', $message );
976 }
977 $response['message'] = $errmsg;
978 // fclose( $fd );
979
980 return $response;
981 }
982
983 /**
984 * Function for updating single sales order.
985 *
986 * @param int $order_id Order ID.
987 * @param string $zi_sales_order_id Zoho Sales Order ID.
988 * @param string $pdt1 JSON string.
989 * @return string Error message
990 */
991 public function single_saleorder_zoho_inventory_update( $order_id, $zi_sales_order_id, $pdt1 ) {
992 // $fd = fopen( __DIR__. '/single_saleorder_update.txt', 'w+' );
993
994 $response = array();
995 $zoho_inventory_oid = $this->zoho_inventory_oid;
996 $zoho_inventory_url = $this->zoho_inventory_url;
997
998 $url = $zoho_inventory_url . 'inventory/v1/salesorders/' . $zi_sales_order_id;
999 $data = array(
1000 'JSONString' => $pdt1,
1001 'organization_id' => $zoho_inventory_oid,
1002 );
1003
1004 $order = wc_get_order( $order_id );
1005 // fwrite($fd, PHP_EOL. print_r($data, true)); //logging response.
1006 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
1007 $json = $execute_curl_call_handle->execute_curl_call_put( $url, $data );
1008
1009 // $code = $json->code;
1010 $errmsg = $json->message;
1011 $response['message'] = $errmsg;
1012 $response['zi_salesorder_id'] = $zi_sales_order_id;
1013
1014 // echo '<pre>'; print_r($errmsg);
1015
1016 $package_id = $order->get_meta( 'zi_package_id', true );
1017
1018 if ( ! empty( $package_id ) ) {
1019 // fwrite($fd, PHP_EOL. 'inside package exists'); //logging response.
1020 foreach ( $json->salesorder as $key => $value ) {
1021 if ( 'date' === $key ) {
1022 $ship_date = $value;
1023 }
1024
1025 if ( 'line_items' === $key ) {
1026 foreach ( $value as $kk => $vv ) {
1027 $line_items[] = '{"so_line_item_id": "' . $vv->line_item_id . '","quantity": "' . $vv->quantity . '"}';
1028 }
1029 $impot = implode( ',', $line_items );
1030
1031 $json_package = '"date": "' . $ship_date . '","line_items": [' . $impot . ']';
1032
1033 $url_package = $zoho_inventory_url . 'inventory/v1/packages/' . $package_id;
1034 $data3 = array(
1035 'JSONString' => '{' . $json_package . '}',
1036 'organization_id' => $zoho_inventory_oid,
1037 );
1038
1039 $res_package = $execute_curl_call_handle->execute_curl_call_put( $url_package, $data3 );
1040 }
1041 }
1042 }
1043 // fclose( $fd ); //end of logging.
1044 return $response;
1045 }
1046
1047 /**
1048 * Function to get all Zoho Taxes.
1049 *
1050 * @param int $percentage Tax percentage.
1051 * @return string Tax ID.
1052 */
1053 private function zi_get_tax_id( $percentage ) {
1054 // $fd = fopen( __DIR__ . '/zi_get_tax_id.txt', 'a+' );
1055 // get all options that contain zoho_inventory_tax_rate_ in the name using global $wpdb.
1056 global $wpdb;
1057 $zoho_tax_rates = $wpdb->get_results( "SELECT option_name, option_value FROM $wpdb->options WHERE option_name LIKE 'cmbird_zoho_inventory_tax_rate_%'" );
1058 $input_tax_percentage = floor( $percentage * 10 ) / 10;
1059 // fwrite( $fd, PHP_EOL . 'Input Tax Percentage: ' . $input_tax_percentage );
1060 $tax_id = '';
1061 // for each zoho_tax_rate, check if the tax percentage matches the input percentage. The percentage at the end of the value e.g. 69497000002395146##BTW@@Hoog Exclusief##tax##21.
1062 foreach ( $zoho_tax_rates as $zoho_tax_rate ) {
1063 $tax_rate = explode( '##', $zoho_tax_rate->option_value );
1064 $tax_percentage = $tax_rate[3];
1065 // Round the stored tax percentage to one decimal place for comparison.
1066 $stored_tax_percentage = round( $tax_percentage, 1 );
1067 // Compare the rounded tax percentages with a tolerance for floating-point precision errors.
1068 if ( abs( $stored_tax_percentage - $input_tax_percentage ) < 0.01 ) {
1069 $tax_id = $tax_rate[0];
1070 break;
1071 }
1072 }
1073 return $tax_id;
1074 }
1075
1076 /**
1077 * Get Zoho Inventory tax group ID by total tax percentage.
1078 *
1079 * This function attempts to find a matching tax group when multiple
1080 * WooCommerce tax rates are applied (compound taxes). Tax groups in Zoho
1081 * represent compound taxes as a single combined rate.
1082 *
1083 * @param float $total_percentage Total tax percentage from all WooCommerce tax rates.
1084 * @return string Tax group ID if found, empty string otherwise.
1085 */
1086 private function zi_get_tax_group_id( $total_percentage ) {
1087 $tax_groups = get_option( 'cmbird_zi_tax_groups', array() );
1088
1089 if ( empty( $tax_groups ) || ! is_array( $tax_groups ) ) {
1090 return '';
1091 }
1092
1093 foreach ( $tax_groups as $tax_group ) {
1094 if ( ! isset( $tax_group['tax_id'] ) || ! isset( $tax_group['tax_percentage'] ) ) {
1095 continue;
1096 }
1097
1098 // Allow tolerance of 0.2 for compound tax matching.
1099 if ( abs( $tax_group['tax_percentage'] - $total_percentage ) <= 0.5 ) {
1100 return $tax_group['tax_id'];
1101 }
1102 }
1103
1104 return '';
1105 }
1106
1107 /**
1108 * Function to get Zoho Inventory IGST tax ID for inter-state transactions in India.
1109 *
1110 * @param int $percentage Tax percentage.
1111 * @return string IGST Tax ID.
1112 */
1113 private function zi_get_igst_tax_id( $percentage ) {
1114 // Get all options that contain zoho_inventory_tax_rate_ in the name for IGST.
1115 global $wpdb;
1116 $zoho_tax_rates = $wpdb->get_results( "SELECT option_name, option_value FROM $wpdb->options WHERE option_name LIKE 'cmbird_zoho_inventory_tax_rate_%'" );
1117 $input_tax_percentage = floor( $percentage * 10 ) / 10;
1118 $tax_id = '';
1119
1120 // For each zoho_tax_rate, check if it's an IGST tax and the percentage matches.
1121 foreach ( $zoho_tax_rates as $zoho_tax_rate ) {
1122 $tax_rate = explode( '##', $zoho_tax_rate->option_value );
1123 if ( count( $tax_rate ) >= 4 ) {
1124 $tax_name = $tax_rate[1];
1125 $tax_percentage = $tax_rate[3];
1126
1127 // Check if this is an IGST tax (contains "IGST" in the name).
1128 if ( stripos( $tax_name, 'IGST' ) !== false ) {
1129 // Round the stored tax percentage to one decimal place for comparison.
1130 $stored_tax_percentage = round( $tax_percentage, 1 );
1131 // Compare the rounded tax percentages with a tolerance for floating-point precision errors.
1132 if ( abs( $stored_tax_percentage - $input_tax_percentage ) < 0.01 ) {
1133 $tax_id = $tax_rate[0];
1134 break;
1135 }
1136 }
1137 }
1138 }
1139
1140 // If no IGST tax found, fallback to regular tax_id.
1141 if ( empty( $tax_id ) ) {
1142 $tax_id = $this->zi_get_tax_id( $percentage );
1143 }
1144
1145 return $tax_id;
1146 }
1147
1148 /**
1149 * TODO: Get Order Transaction Fees
1150 *
1151 * @param int $order_id Order ID.
1152 * @return float Transaction fees
1153 */
1154 protected function get_transaction_fees( $order_id ) {
1155 $order = wc_get_order( $order_id );
1156 $fees = $order->get_meta( '_stripe_fee' );
1157 if ( ! empty( $fees ) ) {
1158 return $fees;
1159 }
1160 $fees = $order->get_meta( '_paypal_transaction_fee' );
1161 if ( ! empty( $fees ) ) {
1162 return $fees;
1163 }
1164 return 0;
1165 }
1166
1167 /**
1168 * Create contact for order - handles both regular users and guest orders
1169 *
1170 * @param int $userid User ID (0 for guest orders).
1171 * @param int $order_id Order ID.
1172 * @param bool $is_guest_order Whether this is a guest order.
1173 * @return string Contact ID.
1174 */
1175 private function create_contact_for_order( $userid, $order_id, $is_guest_order ) {
1176 if ( $is_guest_order ) {
1177 // For guest orders, create contact using order data and store contact info in order meta.
1178 $order = wc_get_order( $order_id );
1179 return $this->create_guest_contact( $order );
1180 } else {
1181 // For regular users, use existing contact creation function.
1182 $contact_class_handle = new CMBIRD_Contact_ZI();
1183 return $contact_class_handle->cmbird_contact_create_function( $userid );
1184 }
1185 }
1186
1187 /**
1188 * Create contact person for order - handles both regular users and guest orders
1189 *
1190 * @param int $userid User ID (0 for guest orders).
1191 * @param int $order_id Order ID.
1192 * @param bool $is_guest_order Whether this is a guest order.
1193 * @return string Contact ID.
1194 */
1195 private function create_contact_person_for_order( $userid, $order_id, $is_guest_order ) {
1196 if ( $is_guest_order ) {
1197 // For guest orders, create contact person using order data.
1198 $order = wc_get_order( $order_id );
1199 return $this->create_guest_contact_person( $order );
1200 } else {
1201 // For regular users, use existing contact person creation function.
1202 $contact_class_handle = new CMBIRD_Contact_ZI();
1203 return $contact_class_handle->cmbird_create_contact_person( $userid );
1204 }
1205 }
1206
1207 /**
1208 * Create Zoho contact for guest order
1209 *
1210 * @param WC_Order $order Order object.
1211 * @return string Contact ID.
1212 */
1213 private function create_guest_contact( $order ) {
1214 // Billing Details.
1215 $contact_name = $order->get_billing_first_name() . ' ' . $order->get_billing_last_name();
1216 $company_name = $order->get_billing_company();
1217 $billing_address = $order->get_billing_address_1();
1218 $billing_address2 = $order->get_billing_address_2();
1219 $billing_city = $order->get_billing_city();
1220 $billing_state = $order->get_billing_state();
1221 $billing_postcode = $order->get_billing_postcode();
1222 $billing_country = $order->get_billing_country();
1223
1224 // Shipping Details.
1225 $shipping_attention = trim( $order->get_shipping_first_name() . ' ' . $order->get_shipping_last_name() );
1226 $shipping_address = $order->get_shipping_address_1();
1227 $shipping_address2 = $order->get_shipping_address_2();
1228 $shipping_city = $order->get_shipping_city();
1229 $shipping_state = $order->get_shipping_state();
1230 $shipping_postcode = $order->get_shipping_postcode();
1231 $shipping_country = $order->get_shipping_country();
1232 // If shipping not available assign billing as shipping (same behavior as cmbird_contact_create_function()).
1233 if ( empty( $shipping_address ) ) {
1234 $shipping_address = $billing_address;
1235 }
1236 if ( empty( $shipping_address2 ) ) {
1237 $shipping_address2 = $billing_address2;
1238 }
1239 if ( empty( $shipping_postcode ) ) {
1240 $shipping_postcode = $billing_postcode;
1241 }
1242 if ( empty( $shipping_city ) ) {
1243 $shipping_city = $billing_city;
1244 }
1245 if ( empty( $shipping_country ) ) {
1246 $shipping_country = $billing_country;
1247 }
1248 if ( empty( $shipping_state ) ) {
1249 $shipping_state = $billing_state;
1250 }
1251 if ( '' === $shipping_attention ) {
1252 $shipping_attention = $contact_name;
1253 }
1254 // Customer Details.
1255 $customer_email = $order->get_billing_email();
1256 $customer_phone = $order->get_billing_phone();
1257
1258 // Get currency code.
1259 $currency_code = $order->get_currency();
1260 $multi_currency_handle = new CMBIRD_Multicurrency_Zoho();
1261 $currency_id = $multi_currency_handle->zoho_currency_data( $currency_code, '', $order );
1262
1263 // Build contact data.
1264 $data = array(
1265 'contact_name' => $contact_name,
1266 'company_name' => $company_name,
1267 'contact_type' => 'customer',
1268 'currency_id' => $currency_id,
1269 'contact_persons' => array(
1270 array(
1271 'first_name' => $order->get_billing_first_name(),
1272 'last_name' => $order->get_billing_last_name(),
1273 'email' => $customer_email,
1274 'phone' => $customer_phone,
1275 ),
1276 ),
1277 'billing_address' => array(
1278 'attention' => $contact_name,
1279 'address' => $billing_address,
1280 'street2' => $billing_address2,
1281 'city' => $billing_city,
1282 'state' => $billing_state,
1283 'zip' => $billing_postcode,
1284 'country' => $billing_country,
1285 ),
1286 'shipping_address' => array(
1287 'attention' => $shipping_attention,
1288 'address' => $shipping_address,
1289 'street2' => $shipping_address2,
1290 'city' => $shipping_city,
1291 'state' => $shipping_state,
1292 'zip' => $shipping_postcode,
1293 'country' => $shipping_country,
1294 ),
1295 );
1296 // append place_of_contact if zoho_inventory_domain is 'in'.
1297 if ( 'in' === $this->zoho_inventory_domain ) {
1298 $data['place_of_contact'] = $billing_state;
1299 }
1300
1301 // Create contact in Zoho (data must be JSON string in 'JSONString' key, as in cmbird_contact_create_function()).
1302 $zoho_inventory_oid = $this->zoho_inventory_oid;
1303 $zoho_inventory_url = $this->zoho_inventory_url;
1304 $url = $zoho_inventory_url . 'inventory/v1/contacts';
1305
1306 $post_data = array(
1307 'JSONString' => wp_json_encode( $data ),
1308 'organization_id' => $zoho_inventory_oid,
1309 );
1310
1311 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
1312 $json = $execute_curl_call_handle->execute_curl_call_post( $url, $post_data );
1313
1314 if ( isset( $json->code ) && ( 0 === $json->code || '0' === $json->code ) ) {
1315 $contact_id = $json->contact->contact_id;
1316
1317 // Store contact details in order meta.
1318 $order->update_meta_data( 'zi_contact_id', $contact_id );
1319
1320 if ( isset( $json->contact->billing_address->address_id ) ) {
1321 $order->update_meta_data( 'zi_billing_address_id', $json->contact->billing_address->address_id );
1322 }
1323
1324 if ( isset( $json->contact->shipping_address->address_id ) ) {
1325 $order->update_meta_data( 'zi_shipping_address_id', $json->contact->shipping_address->address_id );
1326 }
1327
1328 if ( isset( $json->contact->primary_contact_id ) ) {
1329 $order->update_meta_data( 'zi_primary_contact_id', $json->contact->primary_contact_id );
1330 }
1331
1332 if ( isset( $json->contact->created_time ) ) {
1333 $order->update_meta_data( 'zi_created_time', $json->contact->created_time );
1334 }
1335
1336 if ( isset( $json->contact->last_modified_time ) ) {
1337 $order->update_meta_data( 'zi_last_modified_time', $json->contact->last_modified_time );
1338 }
1339
1340 if ( isset( $json->contact->currency_id ) ) {
1341 $order->update_meta_data( 'zi_currency_id', $json->contact->currency_id );
1342 }
1343
1344 $order->save();
1345
1346 return $contact_id;
1347 }
1348
1349 return '';
1350 }
1351
1352 /**
1353 * Create Zoho contact person for guest order
1354 *
1355 * @param WC_Order $order Order object.
1356 * @return string Contact ID.
1357 */
1358 private function create_guest_contact_person( $order ) {
1359 // For guest orders, just return the main contact ID since it's already created with contact person.
1360 return $order->get_meta( 'zi_contact_id', true );
1361 }
1362 }
1363 $CMBIRD_Order_Sync_ZI = new CMBIRD_Order_Sync_ZI();
1364