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