PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.5.0
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.5.0
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 9 months ago class-cmbird-image-zi.php 9 months ago class-import-items.php 9 months ago class-import-price-list.php 9 months ago class-multi-currency.php 9 months ago class-order-sync.php 9 months ago class-product.php 9 months ago class-users-contact.php 9 months ago index.php 1 year ago
class-order-sync.php
1117 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 * Whether to enable order status sync.
48 *
49 * @var bool
50 */
51 private $enable_order_status;
52
53 /**
54 * Whether to enable auto-numbering for orders.
55 *
56 * @var bool
57 */
58 private $enable_auto_number;
59
60 /**
61 * Prefix for order numbers.
62 *
63 * @var string
64 */
65 private $order_prefix;
66
67 /**
68 * Custom fields for Zoho Inventory sync.
69 *
70 * @var array
71 */
72 private $custom_fields;
73
74 /**
75 * Whether to include tax in calculations.
76 *
77 * @var bool
78 */
79 private $include_tax;
80
81 /**
82 * Initialize the class.
83 */
84 public function __construct() {
85 $zoho_inventory_access_token = get_option( 'cmbird_zoho_inventory_access_token' );
86 if ( ! empty( $zoho_inventory_access_token ) ) {
87 add_action( 'woocommerce_update_order', array( $this, 'salesorder_void' ) );
88 // get options.
89 $this->zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
90 $this->zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );
91 $this->zoho_inventory_domain = get_option( 'cmbird_zoho_inventory_domain' );
92 $this->location_id = get_option( 'cmbird_zoho_location_id_status' );
93 $this->parent_location_id = get_option( 'cmbird_zoho_parent_location_id_status' );
94 $this->enable_order_status = get_option( 'cmbird_zoho_enable_order_status_status' );
95 $this->enable_auto_number = get_option( 'cmbird_zoho_enable_auto_number_status' );
96 $this->order_prefix = get_option( 'cmbird_zoho_order_prefix_status' );
97 $this->custom_fields = json_decode( get_option( 'cmbird_wootozoho_custom_fields' ), true );
98 $this->include_tax = get_option( 'woocommerce_prices_include_tax' ) === 'yes';
99 } else {
100 return;
101 }
102 }
103
104 /**
105 * Function to map customer on checkout before placing order
106 *
107 * @param int $order_id Order ID.
108 */
109 public function cmbird_zi_sync_customer_checkout( $order_id ) {
110 // $fd = fopen( __DIR__ . '/cmbird_zi_sync_customer_checkout.txt', 'w+' );
111
112 $order = wc_get_order( $order_id );
113 $userid = $order->get_user_id();
114 $is_guest_order = empty( $userid );
115 $user_company = $order->get_billing_company();
116 $user_email = $order->get_billing_email();
117
118 // For guest orders, get contact ID from order meta; for regular users, from user meta.
119 if ( $is_guest_order ) {
120 $zi_customer_id = $order->get_meta( 'zi_contact_id', true );
121 } else {
122 $zi_customer_id = get_user_meta( $userid, 'zi_contact_id', true );
123 }
124
125 // Get currency code of the order.
126 if ( $is_guest_order ) {
127 $currency_id = intval( $order->get_meta( 'zi_currency_id', true ) );
128 } else {
129 $currency_id = intval( get_user_meta( $userid, 'zi_currency_id', true ) );
130 }
131 if ( empty( $currency_id ) ) {
132 $currency_code = $order->get_currency();
133 $multi_currency_handle = new CMBIRD_Multicurrency_Zoho();
134 $currency_id = $multi_currency_handle->zoho_currency_data( $currency_code, $userid );
135 }
136
137 if ( $zi_customer_id ) {
138 $zoho_inventory_oid = $this->zoho_inventory_oid;
139 $zoho_inventory_url = $this->zoho_inventory_url;
140 $get_url = $zoho_inventory_url . 'inventory/v1/contacts/' . $zi_customer_id . '/?organization_id=' . $zoho_inventory_oid;
141
142 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
143 $json = $execute_curl_call_handle->execute_curl_call_get( $get_url );
144
145 // fwrite($fd,PHP_EOL.'customer_json: '.print_r($json, true));
146
147 $code = $json->code;
148 if ( 0 !== $code && '0' !== $code ) {
149 // For guest orders, clear contact data immediately if validation fails.
150 // For registered users, be more cautious - only clear if it's a definitive "not found" error.
151 $should_clear_contact_data = false;
152
153 if ( $is_guest_order ) {
154 // For guest orders, always clear on any error since guest contacts are order-specific.
155 $should_clear_contact_data = true;
156 } elseif ( 1003 === $code || '1003' === $code ) {
157 // For registered users, only clear if it's a specific "contact not found" error (code 1003).
158 // Other errors might be temporary (rate limiting, API issues, etc.).
159 $should_clear_contact_data = true;
160 }
161
162 if ( $should_clear_contact_data ) {
163 // Clear contact data - from order meta for guest orders, user meta for regular users.
164 if ( $is_guest_order ) {
165 $order->delete_meta_data( 'zi_contact_id' );
166 $order->delete_meta_data( 'zi_billing_address_id' );
167 $order->delete_meta_data( 'zi_primary_contact_id' );
168 $order->delete_meta_data( 'zi_shipping_address_id' );
169 $order->delete_meta_data( 'zi_created_time' );
170 $order->delete_meta_data( 'zi_last_modified_time' );
171 $order->save();
172 } else {
173 delete_user_meta( $userid, 'zi_contact_id' );
174 delete_user_meta( $userid, 'zi_billing_address_id' );
175 delete_user_meta( $userid, 'zi_primary_contact_id' );
176 delete_user_meta( $userid, 'zi_shipping_address_id' );
177 delete_user_meta( $userid, 'zi_created_time' );
178 delete_user_meta( $userid, 'zi_last_modified_time' );
179 }
180 $zi_customer_id = '';
181 }
182 // If we don't clear the contact data, keep the existing $zi_customer_id and proceed.
183 }
184 }
185
186 /**
187 * Syncing customer if its not in Zoho yet.
188 */
189 if ( empty( $zi_customer_id ) ) {
190
191 // First check based on customer email address.
192 $zoho_inventory_oid = $this->zoho_inventory_oid;
193 $zoho_inventory_url = $this->zoho_inventory_url;
194 // fwrite($fd,PHP_EOL.'$user_mail : '.$user_email);
195 $url = $zoho_inventory_url . 'inventory/v1/contacts?organization_id=' . $zoho_inventory_oid . '&email=' . $user_email;
196
197 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
198 $json = $execute_curl_call_handle->execute_curl_call_get( $url );
199
200 $code = $json->code;
201 $message = $json->message;
202 if ( 0 === $code || '0' === $code ) {
203 // fwrite( $fd, PHP_EOL . 'email found: ' . print_r( $json, true ) );
204 if ( empty( $json->contacts ) ) {
205 // Second check based on Company Name.
206 if ( $user_company ) {
207 $company_name = str_replace( ' ', '%20', $user_company );
208 $url = $zoho_inventory_url . 'inventory/v1/contacts?organization_id=' . $zoho_inventory_oid . '&filter_by=Status.Active&search_text=' . $company_name;
209
210 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
211 $json = $execute_curl_call_handle->execute_curl_call_get( $url );
212
213 $code = $json->code;
214 if ( 0 === $code || '0' === $code ) {
215 if ( empty( $json->contacts ) ) {
216 $zi_customer_id = $this->create_contact_for_order( $userid, $order_id, $is_guest_order );
217 } else {
218 foreach ( $json->contacts[0] as $key => $value ) {
219 if ( 'contact_id' === $key ) {
220 $zi_customer_id = $value;
221 // Store contact ID - in order meta for guest orders, user meta for regular users.
222 if ( $is_guest_order ) {
223 $order->update_meta_data( 'zi_contact_id', $zi_customer_id );
224 $order->save();
225 } else {
226 update_user_meta( $userid, 'zi_contact_id', $zi_customer_id );
227 }
228 }
229 }
230 $zi_customer_id = $this->create_contact_person_for_order( $userid, $order_id, $is_guest_order );
231 }
232 } else {
233 // Company search failed, create new contact.
234 $zi_customer_id = $this->create_contact_for_order( $userid, $order_id, $is_guest_order );
235 }
236 } else {
237 $zi_customer_id = $this->create_contact_for_order( $userid, $order_id, $is_guest_order );
238 }
239 } else {
240 // fwrite($fd,PHP_EOL.'Contacts : '.print_r($json->contacts,true));
241 foreach ( $json->contacts[0] as $key => $value ) {
242 if ( 'contact_id' === $key ) {
243 $zi_customer_id = $value;
244 // Store contact ID - in order meta for guest orders, user meta for regular users.
245 if ( $is_guest_order ) {
246 $order->update_meta_data( 'zi_contact_id', $zi_customer_id );
247 $order->save();
248 } else {
249 update_user_meta( $userid, 'zi_contact_id', $zi_customer_id );
250 }
251 }
252 }
253 }
254 } else {
255 // Email search failed, create new contact.
256 $zi_customer_id = $this->create_contact_for_order( $userid, $order_id, $is_guest_order );
257 }
258 // Http request not processed properly.
259 // echo $message;
260 return $zi_customer_id;
261 } else {
262 $zoho_inventory_oid = $this->zoho_inventory_oid;
263 $zoho_inventory_url = $this->zoho_inventory_url;
264 $get_url = $zoho_inventory_url . 'inventory/v1/contacts/' . $zi_customer_id . '/contactpersons/?organization_id=' . $zoho_inventory_oid;
265
266 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
267 $contactpersons_response = $execute_curl_call_handle->execute_curl_call_get( $get_url );
268
269 // fwrite( $fd, PHP_EOL . 'Contactpersons: ' . print_r($contactpersons_response, true) );
270
271 // first check within contactpersons endpoint and then map it with that contactperson if email-id matches.
272 if ( 0 === $contactpersons_response->code || '0' === $contactpersons_response->code ) {
273 if ( ! empty( $contactpersons_response->contact_persons ) ) {
274 foreach ( $contactpersons_response->contact_persons as $key => $contact_persons ) {
275 $person_email = trim( $contact_persons->email );
276 if ( trim( $user_email ) === $person_email ) {
277 /* Match Contact */
278 $contactid = $contact_persons->contact_person_id;
279 // Store contact person ID - in order meta for guest orders, user meta for regular users.
280 if ( $is_guest_order ) {
281 $order->update_meta_data( 'zi_contactperson_id_' . $key, $contactid );
282 $order->save();
283 } else {
284 update_user_meta( $userid, 'zi_contactperson_id_' . $key, $contactid );
285 }
286 if ( true === $contact_persons->is_primary_contact || 1 === $contact_persons->is_primary_contact ) {
287 if ( ! $is_guest_order ) {
288 $contact_class_handle = new CMBIRD_Contact_ZI();
289 $contact_class_handle->cmbird_contact_update_function( $userid, $order_id );
290 }
291 } elseif ( ! $is_guest_order ) {
292 $contact_class_handle = new CMBIRD_Contact_ZI();
293 $contact_class_handle->cmbird_update_contact_person( $userid, $order_id );
294 }
295 }
296 }
297 } else {
298 $get_url = $zoho_inventory_url . 'inventory/v1/contacts/' . $zi_customer_id . '/?organization_id=' . $zoho_inventory_oid;
299 $contact_res = $execute_curl_call_handle->execute_curl_call_get( $get_url );
300 if ( ( 0 === $contact_res->code || '0' === $contact_res->code ) && ! empty( $contact_res->contact ) ) {
301 foreach ( $contact_res as $contact_ ) {
302 if ( trim( $contact_->email ) == trim( $user_email ) ) {
303 // fwrite( $fd, PHP_EOL . 'Inside cmbird_contact_update_function' );
304 if ( ! $is_guest_order ) {
305 $contact_class_handle = new CMBIRD_Contact_ZI();
306 $contact_class_handle->cmbird_contact_update_function( $userid, $order_id );
307 }
308 } else {
309 // fwrite( $fd, PHP_EOL . 'Inside cmbird_create_contact_person' );
310 if ( ! $is_guest_order ) {
311 $contact_class_handle = new CMBIRD_Contact_ZI();
312 $contact_class_handle->cmbird_create_contact_person( $userid );
313 }
314 }
315 }
316 }
317 }
318 }
319 // fwrite( $fd, PHP_EOL . 'No contactpersons ' );
320 }
321 // fclose( $fd );
322 return $zi_customer_id;
323 }
324
325 /**
326 * Function for admin zoho sync call.
327 *
328 * @param int $order_id Order ID.
329 */
330 public function zi_order_sync( $order_id ) {
331 $order = wc_get_order( $order_id );
332 if ( ! $order ) {
333 return;
334 }
335 // return if there is no zoho inventory access token.
336 $zoho_inventory_access_token = get_option( 'cmbird_zoho_inventory_access_token' );
337 if ( empty( $zoho_inventory_access_token ) ) {
338 return;
339 }
340
341 $current_time = time();
342 $last_time = $order->get_meta( 'zi_last_order_sync_time', true );
343 if ( ! empty( $last_time ) && $current_time - $last_time < 60 ) {
344 return;
345 }
346
347 // Create an action hook to prevent orders getting synced if it contains an array of statuses.
348 $ignored_statuses = apply_filters( 'cmbird_zi_order_sync_ignored_statuses', array( 'cancelled', 'failed', 'draft', 'trash' ) );
349 if ( in_array( $order->get_status(), $ignored_statuses ) ) {
350 return;
351 }
352
353 $zi_sales_order_id = $order->get_meta( 'zi_salesorder_id' );
354 $userid = $order->get_user_id();
355 $is_guest_order = empty( $userid );
356
357 // For guest orders, get contact details from order meta instead of user meta.
358 if ( $is_guest_order ) {
359 $customer_id = $order->get_meta( 'zi_contact_id', true );
360 $billing_id = $order->get_meta( 'zi_billing_address_id', true );
361 $shipping_id = $order->get_meta( 'zi_shipping_address_id', true );
362 } else {
363 $customer_id = get_user_meta( $userid, 'zi_contact_id', true );
364 $billing_id = get_user_meta( $userid, 'zi_billing_address_id', true );
365 $shipping_id = get_user_meta( $userid, 'zi_shipping_address_id', true );
366 }
367
368 if ( empty( $customer_id ) ) {
369 $customer_id = $this->cmbird_zi_sync_customer_checkout( $order_id );
370 } elseif ( ! $is_guest_order ) {
371 $contact_class = new CMBIRD_Contact_ZI();
372 $contact_class->cmbird_contact_update_function( $userid, $order_id );
373 }
374
375 $line_items = $this->build_line_items( $order );
376 if ( empty( $line_items ) ) {
377 return;
378 }
379
380 // Final validation: Ensure customer_id is not 0 or '0' before creating request.
381 if ( empty( $customer_id ) || '0' === $customer_id || 0 === $customer_id ) {
382 $order->add_order_note( 'Zoho Order Sync Failed: Contact does not exist or could not be created.' );
383 return;
384 }
385
386 $pdt = array(
387 'customer_id' => $customer_id,
388 'date' => $order->get_date_created()->format( 'Y-m-d' ),
389 'line_items' => $line_items,
390 'is_discount_before_tax' => true,
391 'discount_type' => 'item_level',
392 'price_precision' => '2',
393 'notes' => preg_replace( '/[^A-Za-z0-9\-]/', ' ', $order->get_customer_note() ),
394 'billing_address_id' => $billing_id,
395 'shipping_address_id' => $shipping_id,
396 'delivery_method' => $order->get_shipping_method(),
397 'is_inclusive_tax' => $this->include_tax,
398 'shipping_charge' => $order->get_shipping_total(),
399 'order_status' => $this->enable_order_status ? 'draft' : 'confirmed',
400 );
401
402 if ( $this->parent_location_id && $this->location_id ) {
403 $pdt['location_id'] = $this->parent_location_id;
404 }
405
406 $shipping_tax_id = $this->get_shipping_tax_id( $order );
407 if ( $shipping_tax_id ) {
408 $pdt['shipping_charge_tax_id'] = $shipping_tax_id;
409 }
410
411 $fees = $this->get_order_fees( $order );
412 $pdt = array_merge( $pdt, $fees );
413
414 $pdt['custom_fields'] = $this->prepare_custom_fields( $order );
415 $reference = $this->prepare_reference_number( $order );
416
417 if ( $this->enable_auto_number ) {
418 $pdt['reference_number'] = $reference;
419 } else {
420 $pdt['salesorder_number'] = $order->get_id();
421 }
422
423 $userid = $order->get_user_id();
424 // get below data if zoho_inventory_domain is 'in'.
425 if ( 'in' === $this->zoho_inventory_domain ) {
426 $gst_no = get_user_meta( $userid, 'gst_no', true );
427 // if gst_no is empty then get it from order.
428 if ( empty( $gst_no ) ) {
429 $gst_no = $order->get_meta( '_gst_no', true );
430 }
431 if ( $gst_no ) {
432 $pdt['gst_no'] = $gst_no;
433 }
434
435 // Determine GST treatment based on billing company, GST number, and country.
436 $billing_company = $order->get_billing_company();
437 $billing_country = $order->get_billing_country();
438
439 // GST treatment logic - prioritize company + GST combination first.
440 if ( ! empty( $billing_company ) && ! empty( $gst_no ) ) {
441 // Company with GST number.
442 $gst_treatment = 'business_gst';
443 } elseif ( ! empty( $billing_company ) && empty( $gst_no ) && 'IN' === $billing_country ) {
444 // Company without GST number in India.
445 $gst_treatment = 'business_none';
446 } elseif ( 'IN' !== $billing_country ) {
447 // Non-Indian customer (overseas).
448 $gst_treatment = 'overseas';
449 } else {
450 // Individual consumer in India.
451 $gst_treatment = 'consumer';
452 }
453
454 // Allow override from user meta or order meta if exists.
455 $meta_gst_treatment = get_user_meta( $userid, 'gst_treatment', true );
456 if ( empty( $meta_gst_treatment ) ) {
457 $meta_gst_treatment = $order->get_meta( 'gst_treatment', true );
458 }
459 if ( ! empty( $meta_gst_treatment ) ) {
460 $gst_treatment = $meta_gst_treatment;
461 }
462
463 $pdt['gst_treatment'] = $gst_treatment;
464 $pdt['place_of_supply'] = $order->get_billing_state();
465 }
466
467 $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 );
468
469 $order->update_meta_data( 'zi_body_request', wp_json_encode( $pdt ) );
470 $order->update_meta_data( 'zi_salesorder_id', $response_msg['zi_salesorder_id'] );
471 $order->update_meta_data( 'zi_last_order_sync_time', $current_time );
472 $order->add_order_note( 'Zoho Order Sync: ' . $response_msg['message'] );
473 $order->save();
474 }
475
476 /**
477 * Build line items for a given order.
478 *
479 * @param WC_Order $order WooCommerce order object.
480 *
481 * @return array Array of line items.
482 */
483 private function build_line_items( $order ) {
484 $items = array();
485 $draft_items = array();
486
487 foreach ( $order->get_items() as $item_id => $item ) {
488 $product = $item->get_product();
489 if ( ! $product ) {
490 continue;
491 }
492
493 // Check if product is published.
494 if ( 'publish' !== $product->get_status() ) {
495 $draft_items[] = $item->get_name();
496 continue;
497 }
498
499 $name = $item->get_name();
500 $meta = $item->get_formatted_meta_data();
501 $desc = '';
502 if ( ! empty( $meta ) ) {
503 foreach ( $meta as $meta_value ) {
504 $desc .= $meta_value->display_key . ': ' . wp_strip_all_tags( $meta_value->display_value ) . ' ';
505 }
506 $desc = sanitize_text_field( trim( $desc ) );
507 }
508
509 $quantity = $item->get_quantity();
510 $subtotal = $item->get_subtotal();
511 $total = $item->get_total();
512 $rate = $subtotal / max( 1, $quantity );
513 $discount = $subtotal > $total ? $subtotal - $total : 0;
514
515 $tax_data = $item->get_taxes();
516 $tax_id = '';
517 $tax_percent = 0;
518 if ( ! empty( $tax_data['subtotal'] ) ) {
519 $tax_rate_id = key( $tax_data['subtotal'] );
520 $tax_percent = WC_Tax::_get_tax_rate( $tax_rate_id )['tax_rate'];
521
522 // For India domain, check if IGST should be used for inter-state transactions.
523 if ( 'in' === $this->zoho_inventory_domain ) {
524 $billing_state = $order->get_billing_state();
525 $store_state = get_option( 'woocommerce_store_address_2' );
526 if ( empty( $store_state ) ) {
527 $store_state = get_option( 'woocommerce_default_location' );
528 }
529
530 // Extract state from store location if it's in format "country:state".
531 if ( strpos( $store_state, ':' ) !== false ) {
532 $store_state = explode( ':', $store_state )[1];
533 }
534
535 // Check if this is an inter-state transaction.
536 $is_interstate = ! empty( $billing_state ) && ! empty( $store_state ) && $billing_state !== $store_state;
537
538 if ( $is_interstate ) {
539 // Use IGST tax_id for inter-state transactions.
540 $tax_id = $this->zi_get_igst_tax_id( $tax_percent );
541 } else {
542 // Use regular tax_id (CGST+SGST) for intra-state transactions.
543 $tax_id = $this->zi_get_tax_id( $tax_percent );
544 }
545 } else {
546 // For non-India domains, use regular tax_id.
547 $tax_id = $this->zi_get_tax_id( $tax_percent );
548 }
549
550 if ( $this->include_tax ) {
551 $rate += $rate * ( $tax_percent / 100 );
552 $discount += $discount * ( $tax_percent / 100 );
553 }
554 }
555
556 $line = array(
557 'item_id' => get_post_meta( $product->get_id(), 'zi_item_id', true ),
558 'name' => $name,
559 'description' => $desc,
560 'quantity' => $quantity,
561 'rate' => round( $rate, 2 ),
562 );
563
564 if ( $discount > 0 ) {
565 $line['discount'] = round( $discount, 2 );
566 }
567 if ( $tax_id ) {
568 $line['tax_percentage'] = $tax_percent;
569 $line['tax_id'] = $tax_id;
570 }
571 // add location id if set.
572 if ( ! empty( $this->location_id ) ) {
573 $line['location_id'] = $this->location_id;
574 }
575 $items[] = $line;
576 }
577
578 // If there are draft items, add order note and return empty array to stop sync.
579 if ( ! empty( $draft_items ) ) {
580 $draft_list = '"' . implode( '", "', $draft_items ) . '"';
581 $order->add_order_note( 'Zoho Order Sync: cannot sync draft items ' . $draft_list );
582 $order->save();
583 return array();
584 }
585
586 return $items;
587 }
588
589 /**
590 * Get the Zoho Inventory tax id for the shipping tax.
591 *
592 * @param WC_Order $order The WooCommerce order object.
593 * @return string The Zoho Inventory tax id, or empty string if no tax is applicable.
594 */
595 private function get_shipping_tax_id( $order ) {
596 $shipping_total = $order->get_shipping_total();
597 $shipping_tax = $order->get_shipping_tax();
598 if ( $shipping_total > 0 && $shipping_tax > 0 ) {
599 $percent = ( $shipping_tax / $shipping_total ) * 100;
600 return $this->zi_get_tax_id( round( $percent, 2 ) );
601 }
602 return '';
603 }
604
605 /**
606 * Retrieves the fees associated with an order and formats them for Zoho Inventory.
607 *
608 * @param WC_Order $order The WooCommerce order object.
609 *
610 * @return array Associative array containing the fee amount and description.
611 */
612 private function get_order_fees( $order ) {
613 $fees = $order->get_fees();
614 $data = array();
615 foreach ( $fees as $fee ) {
616 $amount = $fee->get_total();
617 if ( $amount > 0 ) {
618 $data['adjustment'] = $amount;
619 $data['adjustment_description'] = $fee->get_name();
620 }
621 }
622 return $data;
623 }
624
625 /**
626 * Prepare custom fields for the order.
627 *
628 * Iterates over the $custom_fields array and creates an associative array
629 * containing the custom field label and value.
630 *
631 * @param WC_Order $order The WooCommerce order object.
632 * @return array Associative array containing the custom field label and value.
633 */
634 private function prepare_custom_fields( $order ) {
635 $fields = array();
636 if ( is_array( $this->custom_fields ) ) {
637 foreach ( $this->custom_fields as $meta_key => $label ) {
638 $fields[] = array(
639 'label' => $label,
640 'value' => $order->get_meta( $meta_key ),
641 );
642 }
643 }
644 return $fields;
645 }
646
647 /**
648 * Prepare the reference number for the sales order.
649 *
650 * The reference number is used to identify the sales order in Zoho Inventory.
651 * It is generated by combining the order prefix (if set) with the transaction ID
652 * (if available) or the order ID.
653 *
654 * @param WC_Order $order The WooCommerce order object.
655 * @return string The reference number for the sales order.
656 */
657 private function prepare_reference_number( $order ) {
658 $transaction_id = $order->get_transaction_id();
659 if ( empty( $transaction_id ) ) {
660 $transaction_id = $order->get_meta( '_order_number', true );
661 }
662 if ( class_exists( 'WCJ_Order_Numbers' ) || class_exists( 'WC_Seq_Order_Number_Pro' ) ) {
663 return $this->order_prefix . $transaction_id;
664 }
665 if ( ! empty( $this->order_prefix ) ) {
666 return $this->order_prefix . '-' . $order->get_id();
667 }
668 return $order->get_id();
669 }
670
671 /**
672 * Void the sales order if cancelled in WooCommerce.
673 *
674 * @param int $order_id Order ID.
675 */
676 public function salesorder_void( $order_id ) {
677 if ( ! $order_id ) {
678 return;
679 }
680 $zoho_inventory_access_token = get_option( 'cmbird_zoho_inventory_access_token' );
681 if ( empty( $zoho_inventory_access_token ) ) {
682 return;
683 }
684
685 $order = wc_get_order( $order_id );
686 // return if order is already voided.
687 if ( $order->get_meta( 'zi_salesorder_void', true ) ) {
688 return;
689 }
690 $order_status = $order->get_status();
691
692 if ( 'cancelled' === $order_status || 'wc-merged' === $order_status ) {
693 $zi_sales_order_id = $order->get_meta( 'zi_salesorder_id', true );
694 $zoho_inventory_oid = $this->zoho_inventory_oid;
695 $zoho_inventory_url = $this->zoho_inventory_url;
696
697 $url = $zoho_inventory_url . 'inventory/v1/salesorders/' . $zi_sales_order_id . '/status/void?organization_id=' .
698 $zoho_inventory_oid;
699 $data = '';
700 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
701 $json = $execute_curl_call_handle->execute_curl_call_post( $url, $data );
702
703 $errmsg = $json->message;
704 $code = $json->code;
705 if ( '0' === $code || 0 === $code ) {
706 // Add order meta key "zi_salesorder_void" to true.
707 $order->update_meta_data( 'zi_salesorder_void', true );
708 $order->add_order_note( 'Zoho Order Void: ' . $errmsg );
709 $order->save();
710 return;
711 }
712 } else {
713 return;
714 }
715 }
716
717 /**
718 * Sync order from Woo to Zoho.
719 *
720 * @param string $pdt1 JSON string
721 * @param int $order_id Order ID.
722 * @return string Error message
723 */
724 public function single_saleorder_zoho_inventory( $pdt1, $order_id ) {
725 // start logging.
726 // $fd = fopen( __DIR__ . '/order-sync-backend.txt', 'w+' );
727
728 $zoho_inventory_oid = $this->zoho_inventory_oid;
729 $zoho_inventory_url = $this->zoho_inventory_url;
730 $data = array(
731 'JSONString' => $pdt1,
732 'organization_id' => $zoho_inventory_oid,
733 );
734
735 // logging.
736 // fwrite($fd, PHP_EOL . 'Data log : ' . print_r($data, true));
737
738 if ( empty( $data['JSONString'] ) ) {
739 return '';
740 }
741 $ignore_auto_no = $this->enable_auto_number ? 'false' : 'true';
742 $url = $zoho_inventory_url . 'inventory/v1/salesorders?ignore_auto_number_generation=' . $ignore_auto_no;
743
744 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
745 $json = $execute_curl_call_handle->execute_curl_call_post( $url, $data );
746
747 // fwrite( $fd, PHP_EOL . 'Data log : ' . print_r( $json, true ) );
748 $response = array();
749 $code = $json->code;
750 $errmsg = $json->message;
751 // fwrite($fd, PHP_EOL . 'Code : ' . $code);
752
753 // Retry once if code is 400.
754 if ( 400 === $code || '400' === $code ) {
755 // Wait a brief moment before retry.
756 sleep( 1 );
757 $json = $execute_curl_call_handle->execute_curl_call_post( $url, $data );
758 $code = $json->code;
759 $errmsg = $json->message;
760 }
761
762 if ( '0' === $code || 0 === $code ) {
763 foreach ( $json->salesorder as $key => $value ) {
764
765 if ( 'salesorder_id' === $key ) {
766 $response['zi_salesorder_id'] = $value;
767 // $order->add_meta_data('zi_salesorder_id', $value, true);
768 }
769 }
770 } else {
771 // send email to admin with the error message.
772 // create message that contains the error message and order id.
773 $message = 'Error in Zoho Inventory Order Sync: ' . $errmsg;
774 // append the link to the order edit page.
775 $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>';
776 $message .= '<br><br>Order ID: ' . $order_id;
777 $message .= '<br><br>Request Body: ' . $pdt1;
778 $class_common = new CMBIRD_Common_Functions();
779 $class_common->send_email( 'Error in Zoho Order Sync', $message );
780 }
781 $response['message'] = $errmsg;
782 // fclose( $fd );
783
784 return $response;
785 }
786
787 /**
788 * Function for updating single sales order.
789 *
790 * @param int $order_id Order ID.
791 * @param string $zi_sales_order_id Zoho Sales Order ID.
792 * @param string $pdt1 JSON string.
793 * @return string Error message
794 */
795 public function single_saleorder_zoho_inventory_update( $order_id, $zi_sales_order_id, $pdt1 ) {
796 // $fd = fopen( __DIR__. '/single_saleorder_update.txt', 'w+' );
797
798 $response = array();
799 $zoho_inventory_oid = $this->zoho_inventory_oid;
800 $zoho_inventory_url = $this->zoho_inventory_url;
801
802 $url = $zoho_inventory_url . 'inventory/v1/salesorders/' . $zi_sales_order_id;
803 $data = array(
804 'JSONString' => $pdt1,
805 'organization_id' => $zoho_inventory_oid,
806 );
807
808 $order = wc_get_order( $order_id );
809 // fwrite($fd, PHP_EOL. print_r($data, true)); //logging response.
810 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
811 $json = $execute_curl_call_handle->execute_curl_call_put( $url, $data );
812
813 // $code = $json->code;
814 $errmsg = $json->message;
815 $response['message'] = $errmsg;
816 $response['zi_salesorder_id'] = $zi_sales_order_id;
817
818 // echo '<pre>'; print_r($errmsg);
819
820 $package_id = $order->get_meta( 'zi_package_id', true );
821
822 if ( ! empty( $package_id ) ) {
823 // fwrite($fd, PHP_EOL. 'inside package exists'); //logging response.
824 foreach ( $json->salesorder as $key => $value ) {
825 if ( 'date' === $key ) {
826 $ship_date = $value;
827 }
828
829 if ( 'line_items' === $key ) {
830 foreach ( $value as $kk => $vv ) {
831 $line_items[] = '{"so_line_item_id": "' . $vv->line_item_id . '","quantity": "' . $vv->quantity . '"}';
832 }
833 $impot = implode( ',', $line_items );
834
835 $json_package = '"date": "' . $ship_date . '","line_items": [' . $impot . ']';
836
837 $url_package = $zoho_inventory_url . 'inventory/v1/packages/' . $package_id;
838 $data3 = array(
839 'JSONString' => '{' . $json_package . '}',
840 'organization_id' => $zoho_inventory_oid,
841 );
842
843 $res_package = $execute_curl_call_handle->execute_curl_call_put( $url_package, $data3 );
844 }
845 }
846 }
847 // fclose( $fd ); //end of logging.
848 return $response;
849 }
850
851 /**
852 * Function to get all Zoho Taxes.
853 *
854 * @param int $percentage Tax percentage.
855 * @return string Tax ID.
856 */
857 private function zi_get_tax_id( $percentage ) {
858 // $fd = fopen( __DIR__ . '/zi_get_tax_id.txt', 'a+' );
859 // get all options that contain zoho_inventory_tax_rate_ in the name using global $wpdb.
860 global $wpdb;
861 $zoho_tax_rates = $wpdb->get_results( "SELECT option_name, option_value FROM $wpdb->options WHERE option_name LIKE 'cmbird_zoho_inventory_tax_rate_%'" );
862 $input_tax_percentage = floor( $percentage * 10 ) / 10;
863 // fwrite( $fd, PHP_EOL . 'Input Tax Percentage: ' . $input_tax_percentage );
864 $tax_id = '';
865 // 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.
866 foreach ( $zoho_tax_rates as $zoho_tax_rate ) {
867 $tax_rate = explode( '##', $zoho_tax_rate->option_value );
868 $tax_percentage = $tax_rate[3];
869 // Round the stored tax percentage to one decimal place for comparison.
870 $stored_tax_percentage = round( $tax_percentage, 1 );
871 // Compare the rounded tax percentages with a tolerance for floating-point precision errors.
872 if ( abs( $stored_tax_percentage - $input_tax_percentage ) < 0.01 ) {
873 $tax_id = $tax_rate[0];
874 break;
875 }
876 }
877 return $tax_id;
878 }
879
880 /**
881 * Function to get Zoho Inventory IGST tax ID for inter-state transactions in India.
882 *
883 * @param int $percentage Tax percentage.
884 * @return string IGST Tax ID.
885 */
886 private function zi_get_igst_tax_id( $percentage ) {
887 // Get all options that contain zoho_inventory_tax_rate_ in the name for IGST.
888 global $wpdb;
889 $zoho_tax_rates = $wpdb->get_results( "SELECT option_name, option_value FROM $wpdb->options WHERE option_name LIKE 'cmbird_zoho_inventory_tax_rate_%'" );
890 $input_tax_percentage = floor( $percentage * 10 ) / 10;
891 $tax_id = '';
892
893 // For each zoho_tax_rate, check if it's an IGST tax and the percentage matches.
894 foreach ( $zoho_tax_rates as $zoho_tax_rate ) {
895 $tax_rate = explode( '##', $zoho_tax_rate->option_value );
896 if ( count( $tax_rate ) >= 4 ) {
897 $tax_name = $tax_rate[1];
898 $tax_percentage = $tax_rate[3];
899
900 // Check if this is an IGST tax (contains "IGST" in the name).
901 if ( stripos( $tax_name, 'IGST' ) !== false ) {
902 // Round the stored tax percentage to one decimal place for comparison.
903 $stored_tax_percentage = round( $tax_percentage, 1 );
904 // Compare the rounded tax percentages with a tolerance for floating-point precision errors.
905 if ( abs( $stored_tax_percentage - $input_tax_percentage ) < 0.01 ) {
906 $tax_id = $tax_rate[0];
907 break;
908 }
909 }
910 }
911 }
912
913 // If no IGST tax found, fallback to regular tax_id.
914 if ( empty( $tax_id ) ) {
915 $tax_id = $this->zi_get_tax_id( $percentage );
916 }
917
918 return $tax_id;
919 }
920
921 /**
922 * TODO: Get Order Transaction Fees
923 *
924 * @param int $order_id Order ID.
925 * @return float Transaction fees
926 */
927 protected function get_transaction_fees( $order_id ) {
928 $order = wc_get_order( $order_id );
929 $fees = $order->get_meta( '_stripe_fee' );
930 if ( ! empty( $fees ) ) {
931 return $fees;
932 }
933 $fees = $order->get_meta( '_paypal_transaction_fee' );
934 if ( ! empty( $fees ) ) {
935 return $fees;
936 }
937 return 0;
938 }
939
940 /**
941 * Create contact for order - handles both regular users and guest orders
942 *
943 * @param int $userid User ID (0 for guest orders).
944 * @param int $order_id Order ID.
945 * @param bool $is_guest_order Whether this is a guest order.
946 * @return string Contact ID.
947 */
948 private function create_contact_for_order( $userid, $order_id, $is_guest_order ) {
949 if ( $is_guest_order ) {
950 // For guest orders, create contact using order data and store contact info in order meta.
951 $order = wc_get_order( $order_id );
952 return $this->create_guest_contact( $order );
953 } else {
954 // For regular users, use existing contact creation function.
955 $contact_class_handle = new CMBIRD_Contact_ZI();
956 return $contact_class_handle->cmbird_contact_create_function( $userid );
957 }
958 }
959
960 /**
961 * Create contact person for order - handles both regular users and guest orders
962 *
963 * @param int $userid User ID (0 for guest orders).
964 * @param int $order_id Order ID.
965 * @param bool $is_guest_order Whether this is a guest order.
966 * @return string Contact ID.
967 */
968 private function create_contact_person_for_order( $userid, $order_id, $is_guest_order ) {
969 if ( $is_guest_order ) {
970 // For guest orders, create contact person using order data.
971 $order = wc_get_order( $order_id );
972 return $this->create_guest_contact_person( $order );
973 } else {
974 // For regular users, use existing contact person creation function.
975 $contact_class_handle = new CMBIRD_Contact_ZI();
976 return $contact_class_handle->cmbird_create_contact_person( $userid );
977 }
978 }
979
980 /**
981 * Create Zoho contact for guest order
982 *
983 * @param WC_Order $order Order object.
984 * @return string Contact ID.
985 */
986 private function create_guest_contact( $order ) {
987 // Billing Details.
988 $contact_name = $order->get_billing_first_name() . ' ' . $order->get_billing_last_name();
989 $company_name = $order->get_billing_company();
990 $billing_address = $order->get_billing_address_1();
991 $billing_address2 = $order->get_billing_address_2();
992 $billing_city = $order->get_billing_city();
993 $billing_state = $order->get_billing_state();
994 $billing_postcode = $order->get_billing_postcode();
995 $billing_country = $order->get_billing_country();
996
997 // Shipping Details.
998 $shipping_attention = $order->get_shipping_first_name() . ' ' . $order->get_shipping_last_name();
999 $shipping_address = $order->get_shipping_address_1();
1000 $shipping_address2 = $order->get_shipping_address_2();
1001 $shipping_city = $order->get_shipping_city();
1002 $shipping_state = $order->get_shipping_state();
1003 $shipping_postcode = $order->get_shipping_postcode();
1004 $shipping_country = $order->get_shipping_country();
1005 if ( empty( $shipping_address ) ) {
1006 $shipping_attention = $contact_name;
1007 $shipping_address = $billing_address;
1008 $shipping_address2 = $billing_address2;
1009 $shipping_city = $billing_city;
1010 $shipping_state = $billing_state;
1011 $shipping_postcode = $billing_postcode;
1012 $shipping_country = $billing_country;
1013 }
1014 // Customer Details.
1015 $customer_email = $order->get_billing_email();
1016 $customer_phone = $order->get_billing_phone();
1017
1018 // Get currency code.
1019 $currency_code = $order->get_currency();
1020 $multi_currency_handle = new CMBIRD_Multicurrency_Zoho();
1021 $currency_id = $multi_currency_handle->zoho_currency_data( $currency_code, 0, $order );
1022
1023 // Build contact data.
1024 $data = array(
1025 'contact_name' => $contact_name,
1026 'company_name' => $company_name,
1027 'contact_type' => 'customer',
1028 'customer_sub_type' => 'individual',
1029 'currency_id' => $currency_id,
1030 'website' => get_site_url(),
1031 'contact_persons' => array(
1032 array(
1033 'first_name' => $order->get_billing_first_name(),
1034 'last_name' => $order->get_billing_last_name(),
1035 'email' => $customer_email,
1036 'phone' => $customer_phone,
1037 ),
1038 ),
1039 'billing_address' => array(
1040 'attention' => $contact_name,
1041 'address' => $billing_address,
1042 'street2' => $billing_address2,
1043 'city' => $billing_city,
1044 'state' => $billing_state,
1045 'zip' => $billing_postcode,
1046 'country' => $billing_country,
1047 ),
1048 'shipping_address' => array(
1049 'attention' => $shipping_attention,
1050 'address' => $shipping_address,
1051 'street2' => $shipping_address2,
1052 'city' => $shipping_city,
1053 'state' => $shipping_state,
1054 'zip' => $shipping_postcode,
1055 'country' => $shipping_country,
1056 ),
1057 );
1058
1059 // Create contact in Zoho.
1060 $zoho_inventory_oid = $this->zoho_inventory_oid;
1061 $zoho_inventory_url = $this->zoho_inventory_url;
1062 $url = $zoho_inventory_url . 'inventory/v1/contacts?organization_id=' . $zoho_inventory_oid;
1063
1064 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
1065 $json = $execute_curl_call_handle->execute_curl_call_post( $url, $data );
1066
1067 if ( isset( $json->code ) && ( 0 === $json->code || '0' === $json->code ) ) {
1068 $contact_id = $json->contact->contact_id;
1069
1070 // Store contact details in order meta.
1071 $order->update_meta_data( 'zi_contact_id', $contact_id );
1072
1073 if ( isset( $json->contact->billing_address->address_id ) ) {
1074 $order->update_meta_data( 'zi_billing_address_id', $json->contact->billing_address->address_id );
1075 }
1076
1077 if ( isset( $json->contact->shipping_address->address_id ) ) {
1078 $order->update_meta_data( 'zi_shipping_address_id', $json->contact->shipping_address->address_id );
1079 }
1080
1081 if ( isset( $json->contact->primary_contact_id ) ) {
1082 $order->update_meta_data( 'zi_primary_contact_id', $json->contact->primary_contact_id );
1083 }
1084
1085 if ( isset( $json->contact->created_time ) ) {
1086 $order->update_meta_data( 'zi_created_time', $json->contact->created_time );
1087 }
1088
1089 if ( isset( $json->contact->last_modified_time ) ) {
1090 $order->update_meta_data( 'zi_last_modified_time', $json->contact->last_modified_time );
1091 }
1092
1093 if ( isset( $json->contact->currency_id ) ) {
1094 $order->update_meta_data( 'zi_currency_id', $json->contact->currency_id );
1095 }
1096
1097 $order->save();
1098
1099 return $contact_id;
1100 }
1101
1102 return '';
1103 }
1104
1105 /**
1106 * Create Zoho contact person for guest order
1107 *
1108 * @param WC_Order $order Order object.
1109 * @return string Contact ID.
1110 */
1111 private function create_guest_contact_person( $order ) {
1112 // For guest orders, just return the main contact ID since it's already created with contact person.
1113 return $order->get_meta( 'zi_contact_id', true );
1114 }
1115 }
1116 $CMBIRD_Order_Sync_ZI = new CMBIRD_Order_Sync_ZI();
1117