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