PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 3.0.1
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v3.0.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 / apis / class-api-for-woo-order.php
commercebird / includes / classes / apis Last commit date
class-api-for-cmbird.php 4 months ago class-api-for-exact-webhooks.php 5 months ago class-api-for-product-webhook.php 3 months ago class-api-for-shipping-status.php 10 months ago class-api-for-woo-order.php 3 months ago class-api-for-zoho-inventory.php 11 months ago class-commercebird-list-items-api-controller.php 11 months ago class-commercebird-media-api-controller.php 11 months ago class-commercebird-metadata-controller.php 3 months ago index.php 1 year ago trait-api-permission.php 2 months ago
class-api-for-woo-order.php
681 lines
1 <?php
2
3 namespace CommerceBird\API;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 use CommerceBird\Admin\Traits\LogWriter;
10 use WP_REST_Response;
11
12 class CreateOrderWebhook {
13
14 use Api;
15 use LogWriter;
16
17 private static string $endpoint = 'create-woo-order';
18
19
20 public function __construct() {
21 register_rest_route(
22 self::$namespace,
23 self::$endpoint,
24 array(
25 'methods' => 'POST',
26 'callback' => array( $this, 'handle' ),
27 'permission_callback' => array( $this, 'permission_check' ),
28 )
29 );
30 }
31
32 /**
33 * @param $address
34 *
35 * @return array
36 */
37 public function format_address( $address ): array {
38 if ( array_key_exists( 'attention', $address ) ) {
39 $names = explode( ' ', $address['attention'] );
40 $first_name = $names[0] ?? '';
41 $last_name = $names[1] ?? '';
42 }
43 return array(
44 'first_name' => $this->normalize_customer_text( $first_name ?? '' ),
45 'last_name' => $this->normalize_customer_text( $last_name ?? '' ),
46 'address_1' => $this->normalize_customer_text( $address['address'] ?? '' ),
47 'address_2' => $this->normalize_customer_text( $address['street2'] ?? '' ),
48 'city' => $this->normalize_customer_text( $address['city'] ?? '' ),
49 'state' => $this->normalize_customer_text( $address['state_code'] ?? '' ),
50 'postcode' => $this->normalize_customer_text( $address['zip'] ?? '' ),
51 'country' => $this->normalize_customer_text( $address['country_code'] ?? '' ),
52 'phone' => $this->normalize_customer_text( $address['phone'] ?? '' ),
53 );
54 }
55
56 private function normalize_customer_text( $value ): string {
57 if ( ! is_scalar( $value ) ) {
58 return '';
59 }
60
61 $normalized = remove_accents( (string) $value );
62 $normalized = preg_replace( '/[^\x20-\x7E]/', '', $normalized );
63
64 if ( null === $normalized ) {
65 $normalized = '';
66 }
67
68 return trim( sanitize_text_field( $normalized ) );
69 }
70
71 private function process( array $order_data ): WP_REST_Response {
72 $response = new WP_REST_Response();
73
74 // Check if the order data is valid.
75 if ( empty( $order_data ) || empty( $order_data['salesorder'] ) ) {
76 $response->set_data( $this->empty_response );
77 $response->set_status( 404 );
78 return $response;
79 }
80
81 // Validate salesorder_id exists.
82 if ( ! isset( $order_data['salesorder']['salesorder_id'] ) || empty( $order_data['salesorder']['salesorder_id'] ) ) {
83 /* translators: %1$s: Store name */
84 $message = sprintf( __( 'Zoho order could not be created in your store %1$s because of missing order ID.', 'commercebird' ), get_bloginfo( 'name' ) );
85 $common_class = new \CMBIRD_Common_Functions();
86 $common_class->send_email( __( 'Zoho Order Sync', 'commercebird' ), $message );
87 $response->set_status( 500 );
88 $response->set_data( $message );
89 return $response;
90 }
91
92 $allowed_keys = array(
93 'salesorder_id',
94 'salesorder_number',
95 'customer_id',
96 'customer_name',
97 'billing_address',
98 'shipping_address',
99 'delivery_method',
100 'currency_code',
101 'line_items',
102 'contact_person_details',
103 'shipping_charge',
104 'order_status',
105 'paid_status',
106 'invoiced_status',
107 'total',
108 'discount',
109 'discount_total',
110 'discount_percent',
111 'notes',
112 'salesperson_name',
113 'reference_number',
114 );
115
116 $order_data = array_intersect_key( $order_data['salesorder'], array_flip( $allowed_keys ) );
117 $billing_address = $this->format_address( $order_data['billing_address'] );
118 $shipping_address = $this->format_address( $order_data['shipping_address'] );
119 $billing_company = $this->normalize_customer_text( $order_data['customer_name'] ?? '' );
120 $billing_address['company'] = $billing_company;
121
122 $zoho_contact = array();
123
124 // Always fetch full contact details from Zoho API when customer_id is available,.
125 // since the webhook payload omits company_name, first_name, and last_name.
126 if ( ! empty( $order_data['customer_id'] ) ) {
127 $zoho_contact = $this->get_zoho_contact_details( $order_data['customer_id'] );
128 if ( ! empty( $zoho_contact ) ) {
129 $order_data['contact_person_details'] = $zoho_contact;
130 }
131 }
132
133 // Handle customer creation/update.
134 $customer = null;
135 $contact_data = $zoho_contact[0] ?? ( $order_data['contact_person_details'][0] ?? null );
136 if ( isset( $contact_data['email'] ) ) {
137 $customer_mail = $contact_data['email'];
138 $first_name = $this->normalize_customer_text( $contact_data['first_name'] ?? '' );
139 $last_name = $this->normalize_customer_text( $contact_data['last_name'] ?? '' );
140 $company_name = $this->normalize_customer_text( $contact_data['company_name'] ?? '' );
141 $eu_vat_number = $contact_data['company_id'] ?? '';
142 if ( '' === $company_name ) {
143 $company_name = $billing_company;
144 }
145 $customer = get_user_by( 'email', $customer_mail );
146 if ( empty( $customer ) ) {
147 $customer_id = wc_create_new_customer(
148 $customer_mail,
149 '',
150 '',
151 array(
152 'role' => 'customer',
153 'first_name' => $first_name,
154 'last_name' => $last_name,
155 )
156 );
157 $wc_customer = new \WC_Customer( $customer_id );
158 // Set billing details.
159 $wc_customer->set_billing_first_name( $first_name );
160 $wc_customer->set_billing_last_name( $last_name );
161 $wc_customer->set_billing_company( $company_name );
162 $wc_customer->set_billing_address_1( $billing_address['address_1'] );
163 $wc_customer->set_billing_address_2( $billing_address['address_2'] );
164 $wc_customer->set_billing_city( $billing_address['city'] );
165 $wc_customer->set_billing_postcode( $billing_address['postcode'] );
166 $wc_customer->set_billing_country( $billing_address['country'] );
167 $wc_customer->set_billing_state( $billing_address['state'] );
168 $wc_customer->set_billing_email( $customer_mail );
169 $wc_customer->set_billing_phone( $billing_address['phone'] );
170 // Set shipping details.
171 $wc_customer->set_shipping_first_name( $first_name );
172 $wc_customer->set_shipping_last_name( $last_name );
173 $wc_customer->set_shipping_address_1( $shipping_address['address_1'] );
174 $wc_customer->set_shipping_address_2( $shipping_address['address_2'] );
175 $wc_customer->set_shipping_city( $shipping_address['city'] );
176 $wc_customer->set_shipping_postcode( $shipping_address['postcode'] );
177 $wc_customer->set_shipping_country( $shipping_address['country'] );
178 $wc_customer->set_shipping_state( $shipping_address['state'] );
179 $wc_customer->save();
180 $customer = get_user_by( 'id', $customer_id );
181 } else {
182 $wc_customer = new \WC_Customer( $customer->ID );
183 // Set billing details.
184 $wc_customer->set_billing_first_name( $first_name );
185 $wc_customer->set_billing_last_name( $last_name );
186 $wc_customer->set_billing_company( $company_name );
187 $wc_customer->set_billing_address_1( $billing_address['address_1'] );
188 $wc_customer->set_billing_address_2( $billing_address['address_2'] );
189 $wc_customer->set_billing_city( $billing_address['city'] );
190 $wc_customer->set_billing_postcode( $billing_address['postcode'] );
191 $wc_customer->set_billing_country( $billing_address['country'] );
192 $wc_customer->set_billing_state( $billing_address['state'] );
193 $wc_customer->set_billing_email( $customer_mail );
194 $wc_customer->set_billing_phone( $billing_address['phone'] );
195 // Set shipping details.
196 $wc_customer->set_shipping_first_name( $first_name );
197 $wc_customer->set_shipping_last_name( $last_name );
198 $wc_customer->set_shipping_address_1( $shipping_address['address_1'] );
199 $wc_customer->set_shipping_address_2( $shipping_address['address_2'] );
200 $wc_customer->set_shipping_city( $shipping_address['city'] );
201 $wc_customer->set_shipping_postcode( $shipping_address['postcode'] );
202 $wc_customer->set_shipping_country( $shipping_address['country'] );
203 $wc_customer->set_shipping_state( $shipping_address['state'] );
204 $wc_customer->save();
205 }
206
207 if ( class_exists( 'WC_EU_VAT_Number' ) && '' !== $eu_vat_number && ! empty( $customer ) ) {
208 update_user_meta( $customer->ID, 'vat_number', $eu_vat_number );
209 }
210 }
211
212 // Validate line items exist.
213 if ( empty( $order_data['line_items'] ) ) {
214 // translators: 1: order number, 2: Store name.
215 $message = sprintf( __( 'Zoho order #%1$s could not be created in your store %2$s because of missing line items.', 'commercebird' ), $order_data['salesorder_number'], get_bloginfo( 'name' ) );
216 $common_class = new \CMBIRD_Common_Functions();
217 $common_class->send_email( __( 'Zoho Order Sync', 'commercebird' ), $message );
218 $response->set_status( 500 );
219 $response->set_data( $message );
220 return $response;
221 }
222
223 $line_items_result = $this->get_items( $order_data['line_items'] );
224 $line_items = $line_items_result['items'];
225 $shipping_charge = ! empty( $line_items_result['shipping_total'] )
226 ? (float) $line_items_result['shipping_total']
227 : ( isset( $order_data['shipping_charge'] ) ? (float) $order_data['shipping_charge'] : 0.0 );
228
229 if ( ! empty( $line_items_result['not_found'] ) ) {
230 // translators: 1: order number, 2: Store name, 3: missing items.
231 $message = sprintf( __( 'Zoho order #%1$s could not be created in your store %2$s because of missing items: %3$s', 'commercebird' ), $order_data['salesorder_number'], get_bloginfo( 'name' ), implode( ', ', $line_items_result['not_found'] ) );
232 $common_class = new \CMBIRD_Common_Functions();
233 $common_class->send_email( __( 'Zoho Order Sync', 'commercebird' ), $message );
234 $response->set_status( 500 );
235 $response->set_data( $message );
236 return $response;
237 }
238
239 $order_discount_total = isset( $order_data['discount_total'] ) ? (float) $order_data['discount_total'] : 0.0;
240 $sales_order_total = isset( $order_data['total'] ) ? (float) $order_data['total'] : null;
241
242 // Check if order already exists.
243 $orders = wc_get_orders(
244 array(
245 'limit' => 1,
246 'meta_key' => 'zi_salesorder_id',
247 'meta_value' => $order_data['salesorder_id'],
248 'meta_compare' => '=',
249 'return' => 'ids',
250 )
251 );
252 if ( ! empty( $orders ) ) {
253 $existing_order = wc_get_order( $orders[0] );
254 }
255
256 // Update existing order.
257 if ( ! empty( $existing_order ) && empty( $line_items_result['not_found'] ) ) {
258 // if order status is completed or processing, do not update the order and return error response.
259 if ( in_array( $existing_order->get_status(), array( 'completed', 'processing' ), true ) ) {
260 /* translators: %1$s: Zoho order number, %2$s: Store name, %3$s: Order status */
261 $message = sprintf( __( 'Zoho order #%1$s could not be updated in your store %2$s because the order is already in %3$s status.', 'commercebird' ), $order_data['salesorder_number'], get_bloginfo( 'name' ), $existing_order->get_status() );
262 $common_class = new \CMBIRD_Common_Functions();
263 $common_class->send_email( __( 'Zoho Order Sync', 'commercebird' ), $message );
264 $response->set_status( 200 );
265 $response->set_data( $message );
266 return $response;
267 }
268 if ( $customer ) {
269 $wc_customer_data = ( new \WC_Customer( $customer->ID ) )->get_data();
270 $existing_order->set_address( $wc_customer_data['billing'], 'billing' );
271 $existing_order->set_address( $wc_customer_data['shipping'], 'shipping' );
272 } else {
273 $existing_order->set_address( $billing_address, 'billing' );
274 $existing_order->set_address( $shipping_address, 'shipping' );
275 }
276 $existing_order->set_billing_company( $billing_company );
277 // Remove existing order items.
278 $existing_order->remove_order_items( 'line_item' );
279 // Add products to the order.
280 foreach ( $line_items as $line_item ) {
281 $product_id = $line_item['id'];
282 $quantity = $line_item['quantity'];
283 $rate = $line_item['rate'] ?? null;
284 if ( ! empty( $product_id ) ) {
285 $product = wc_get_product( $product_id );
286 if ( $product && $product->is_type( 'variation' ) ) {
287 // Get variation object.
288 $variation = wc_get_product_object( 'variation', $product_id );
289 if ( $variation ) {
290 $item_id = $existing_order->add_product( $variation, $quantity );
291 }
292 } else {
293 $item_id = $existing_order->add_product( $product, $quantity );
294 }
295 }
296 }
297
298 // Set order details.
299 $existing_order->set_customer_note( isset( $order_data['notes'] ) ? $order_data['notes'] : '' );
300 $existing_order->update_status( $this->map_status( $order_data['paid_status'], $order_data['invoiced_status'] ?? '' ) );
301
302 // Handle shipping.
303 // Remove existing shipping items first.
304 foreach ( $existing_order->get_items( 'shipping' ) as $item_id => $item ) {
305 $existing_order->remove_item( $item_id );
306 }
307
308 if ( $shipping_charge > 0 ) {
309 $shipping = new \WC_Order_Item_Shipping();
310 $shipping->set_method_title( $order_data['delivery_method'] ?? 'Flat Rate' );
311 $shipping->set_method_id( 'flat_rate' );
312 $shipping->set_total( $shipping_charge );
313 $existing_order->add_item( $shipping );
314 }
315 // Set order totals and save.
316 $existing_order->calculate_totals();
317 $this->apply_order_discount_total( $existing_order, $order_discount_total, $sales_order_total );
318 $existing_order->save();
319 $note = 'Updated in Zoho Inventory';
320 if ( ! empty( $order_data['salesperson_name'] ) ) {
321 $note .= ' by ' . $order_data['salesperson_name'];
322 }
323 $existing_order->add_order_note( $note );
324
325 $response->set_data(
326 array(
327 'order_id' => $existing_order->get_id(),
328 'total' => $existing_order->get_total(),
329 'status' => $existing_order->get_status(),
330 )
331 );
332 $response->set_status( 200 );
333 } elseif ( empty( $line_items_result['not_found'] ) ) {
334 // Create new order.
335 $order = wc_create_order();
336 if ( $customer ) {
337 $order->set_customer_id( $customer->ID );
338 $wc_customer_data = ( new \WC_Customer( $customer->ID ) )->get_data();
339 $order->set_address( $wc_customer_data['billing'], 'billing' );
340 $order->set_address( $wc_customer_data['shipping'], 'shipping' );
341 }
342 $order->set_billing_company( $billing_company );
343 // Add products to the order.
344 foreach ( $line_items as $line_item ) {
345 $product_id = $line_item['id'];
346 $quantity = $line_item['quantity'];
347 $rate = $line_item['rate'] ?? null;
348 if ( ! empty( $product_id ) ) {
349 $product = wc_get_product( $product_id );
350 if ( $product && $product->is_type( 'variation' ) ) {
351 // Get variation object.
352 $variation = wc_get_product_object( 'variation', $product_id );
353 if ( $variation ) {
354 $item_id = $order->add_product( $variation, $quantity );
355 }
356 } else {
357 $item_id = $order->add_product( $product, $quantity );
358 }
359 }
360 }
361 $order->set_currency( $order_data['currency_code'] );
362 $order->set_status( $this->map_status( $order_data['paid_status'], $order_data['invoiced_status'] ?? '' ) );
363 // Handle shipping.
364 if ( $shipping_charge > 0 ) {
365 $shipping = new \WC_Order_Item_Shipping();
366 $shipping->set_method_title( $order_data['delivery_method'] ?? 'Flat Rate' );
367 $shipping->set_method_id( 'flat_rate' );
368 $shipping->set_total( $shipping_charge );
369 $order->add_item( $shipping );
370 }
371
372 $order->calculate_totals();
373 $this->apply_order_discount_total( $order, $order_discount_total, $sales_order_total );
374 $order->set_customer_note( $order_data['notes'] ?? '' );
375 $order->save();
376 $order->update_meta_data( 'zi_salesorder_id', $order_data['salesorder_id'] );
377 $order->save();
378 $note = 'Synced in Zoho Inventory';
379 if ( ! empty( $order_data['salesperson_name'] ) ) {
380 $note .= ' by ' . $order_data['salesperson_name'];
381 }
382 $order->add_order_note( $note );
383
384 // Update Zoho sales order with WooCommerce order ID as reference number.
385 if ( empty( $order_data['reference_number'] ) ) {
386 $this->update_zoho_reference_number( $order, $order_data['salesorder_id'] );
387 }
388 $response->set_data(
389 array(
390 'order_id' => $order->get_id(),
391 'status' => $order->get_status(),
392 )
393 );
394 $response->set_status( 200 );
395 } else {
396 $response->set_data(
397 array(
398 'status' => 'Something went wrong. Please try again.',
399 )
400 );
401 $response->set_status( 500 );
402 }
403
404 return $response;
405 }
406
407 /**
408 * Apply external discount amount from Zoho to WooCommerce order totals.
409 *
410 * @param \WC_Order $order WooCommerce order object.
411 * @param float $discount_total Discount total from payload.
412 * @param float|null $sales_order_total Zoho sales order total from payload.
413 * @return void
414 */
415 private function apply_order_discount_total( \WC_Order $order, float $discount_total, ?float $sales_order_total = null ): void {
416 $current_total = (float) $order->get_total();
417
418 // When the Zoho total is provided, the gap between the calculated WC total and the
419 // Zoho total is the authoritative discount — using discount_total on top would double-count.
420 if ( null !== $sales_order_total && $sales_order_total >= 0 && $sales_order_total < $current_total ) {
421 $effective_discount = $current_total - $sales_order_total;
422 } else {
423 $effective_discount = max( 0, $discount_total );
424 }
425
426 if ( $effective_discount <= 0 ) {
427 $order->set_discount_total( 0 );
428 $order->set_discount_tax( 0 );
429 return;
430 }
431
432 $effective_discount = min( $effective_discount, $current_total );
433 $order->set_discount_total( $effective_discount );
434 $order->set_discount_tax( 0 );
435 $order->set_total( max( 0, $current_total - $effective_discount ) );
436 }
437
438
439 private function map_status( $paid_status, $invoiced_status = '' ): string {
440 // If invoiced, status must be processing.
441 if ( 'invoiced' === $invoiced_status ) {
442 return 'wc-processing';
443 }
444
445 // Otherwise check paid status.
446 switch ( $paid_status ) {
447 case 'paid':
448 return 'wc-completed';
449 default:
450 return 'wc-pending';
451 }
452 }
453
454
455 /**
456 * Update Zoho sales order reference number with WooCommerce order ID.
457 *
458 * @param \WC_Order $order WooCommerce order object.
459 * @param string $salesorder_id Zoho sales order ID.
460 * @return void
461 */
462 private function update_zoho_reference_number( $order, $salesorder_id ): void {
463 // Get Zoho Inventory configuration.
464 $zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );
465 $zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
466
467 if ( empty( $zoho_inventory_url ) || empty( $zoho_inventory_oid ) || empty( $salesorder_id ) ) {
468 return;
469 }
470
471 // Return if the order status is processing or completed, since the reference number should not be updated in that case.
472 if ( in_array( $order->get_status(), array( 'processing', 'completed' ), true ) ) {
473 return;
474 }
475
476 // Prepare reference number using the same logic as order sync.
477 $transaction_id = $order->get_transaction_id();
478 if ( empty( $transaction_id ) ) {
479 $transaction_id = $order->get_meta( '_order_number', true );
480 }
481
482 $order_prefix = get_option( 'cmbird_zoho_order_prefix_status' );
483 $reference_number = '';
484
485 if ( class_exists( 'WCJ_Order_Numbers' ) || class_exists( 'WC_Seq_Order_Number_Pro' ) ) {
486 $reference_number = $order_prefix . $transaction_id;
487 } elseif ( ! empty( $order_prefix ) ) {
488 $reference_number = $order_prefix . '-' . $order->get_id();
489 } else {
490 $reference_number = $order->get_id();
491 }
492
493 // Build the API URL.
494 $url = $zoho_inventory_url . 'inventory/v1/salesorders/' . $salesorder_id . '?organization_id=' . $zoho_inventory_oid;
495
496 // Prepare data for update.
497 $data = array(
498 'JSONString' => wp_json_encode(
499 array(
500 'reference_number' => $reference_number,
501 )
502 ),
503 );
504
505 // Make the API call to update Zoho sales order.
506 $api_handler = new \CMBIRD_API_Handler_Zoho();
507 $response = $api_handler->execute_curl_call_put( $url, $data );
508
509 // Log any errors.
510 $logger = wc_get_logger();
511 if ( is_wp_error( $response ) ) {
512 $logger->error(
513 'Error updating Zoho reference number: ' . $response->get_error_message(),
514 array(
515 'source' => 'commercebird',
516 'order_id' => $order->get_id(),
517 'salesorder_id' => $salesorder_id,
518 )
519 );
520 } elseif ( isset( $response->code ) && 0 !== $response->code ) {
521 $error_message = isset( $response->message ) ? $response->message : 'Unknown error';
522 $logger->error(
523 'Error updating Zoho reference number: ' . $error_message,
524 array(
525 'source' => 'commercebird',
526 'order_id' => $order->get_id(),
527 'salesorder_id' => $salesorder_id,
528 )
529 );
530 } else {
531 $order->add_order_note( 'Updated Zoho reference number to: ' . $reference_number );
532 }
533 }
534
535
536 /**
537 * Find the product ids from the line items from Zoho Inventory.
538 *
539 * @param mixed $line_items Line items from the salesorder.
540 * @return array
541 */
542 private function get_items( $line_items ): array {
543 $product_ids = array();
544 $not_found = array();
545 $shipping_total = 0.0;
546
547 foreach ( $line_items as $item ) {
548 $line_item_name = isset( $item['name'] ) ? trim( (string) $item['name'] ) : trim( (string) ( $item['item_name'] ?? '' ) );
549 if ( 'shipping' === strtolower( $line_item_name ) ) {
550 $line_item_total = isset( $item['item_total'] ) ? (float) $item['item_total'] : 0.0;
551 if ( $line_item_total <= 0 && isset( $item['rate'] ) ) {
552 $quantity = isset( $item['quantity'] ) ? (float) $item['quantity'] : 1;
553 $line_item_total = (float) $item['rate'] * $quantity;
554 }
555 $shipping_total += $line_item_total;
556 continue;
557 }
558
559 // Check if the sku key is present (case-insensitive check).
560 $sku = isset( $item['sku'] ) ? $item['sku'] : ( isset( $item['SKU'] ) ? $item['SKU'] : null );
561 $quantity = $item['quantity'] ?? 1;
562 $rate = $item['rate'] ?? null;
563
564 if ( null !== $sku ) {
565 $product_id = wc_get_product_id_by_sku( $sku );
566 if ( $product_id ) {
567 // Append to product_ids instead of overwriting.
568 $product_ids[] = array(
569 'id' => $product_id,
570 'quantity' => $quantity,
571 'rate' => $rate,
572 );
573 } else {
574 // create the item in WooCommerce.
575 try {
576 $new_product = new \WC_Product_Simple();
577 $new_product->set_name( $line_item_name );
578 $new_product->set_sku( $sku );
579 $new_product->set_regular_price( $rate );
580 $new_product->set_status( 'publish' );
581 $new_product->set_manage_stock( false );
582 // add meta zi_item_id to link the product with Zoho item.
583 $new_product->add_meta_data( 'zi_item_id', $item['item_id'] ?? '' );
584 $product_id = $new_product->save();
585 if ( $product_id > 0 ) {
586 $product_ids[] = array(
587 'id' => $product_id,
588 'quantity' => $quantity,
589 'rate' => $rate,
590 );
591 } else {
592 $error_msg = 'Unknown error';
593 $logger = wc_get_logger();
594 $logger->error(
595 'Failed to create product with SKU ' . $sku . ': ' . $error_msg,
596 array( 'source' => 'commercebird' )
597 );
598 $not_found[] = $line_item_name . ' (SKU: ' . $sku . ')';
599 }
600 } catch ( \Throwable $e ) {
601 $logger = wc_get_logger();
602 $logger->error(
603 'Exception creating product with SKU ' . $sku . ': ' . $e->getMessage(),
604 array( 'source' => 'commercebird' )
605 );
606 $not_found[] = $line_item_name . ' (SKU: ' . $sku . ')';
607 }
608 }
609 } else {
610 $not_found[] = 'Unknown SKU';
611 }
612 }
613
614 return array(
615 'items' => $product_ids,
616 'not_found' => $not_found,
617 'shipping_total' => $shipping_total,
618 );
619 }
620
621 /**
622 * Fetch contact person details from Zoho Inventory using a customer_id.
623 * Returns an array compatible with contact_person_details format.
624 *
625 * @param string $customer_id Zoho customer/contact ID.
626 * @return array
627 */
628 private function get_zoho_contact_details( string $customer_id ): array {
629 $zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );
630 $zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
631
632 if ( empty( $zoho_inventory_url ) || empty( $zoho_inventory_oid ) ) {
633 return array();
634 }
635
636 $url = $zoho_inventory_url . 'inventory/v1/contacts/' . $customer_id . '?organization_id=' . $zoho_inventory_oid;
637
638 $api_handler = new \CMBIRD_API_Handler_Zoho();
639 $response = $api_handler->execute_curl_call_get( $url );
640
641 if ( is_wp_error( $response ) || empty( $response->contact ) ) {
642 return array();
643 }
644
645 $contact = (array) $response->contact;
646 $contact_persons = array();
647
648 $company_name = $contact['company_name'] ?? '';
649 $company_id = $contact['company_id'] ?? '';
650
651 if ( ! empty( $contact['contact_persons'] ) ) {
652 foreach ( $contact['contact_persons'] as $person ) {
653 $person = (array) $person;
654 if ( ! empty( $person['is_primary_contact'] ) && ! empty( $person['email'] ) ) {
655 $contact_persons[] = array(
656 'email' => $person['email'],
657 'first_name' => $person['first_name'] ?? '',
658 'last_name' => $person['last_name'] ?? '',
659 'company_name' => $company_name,
660 'company_id' => $company_id,
661 );
662 break;
663 }
664 }
665 }
666
667 // Fall back to the contact's own email if no contact persons with email found.
668 if ( empty( $contact_persons ) && ! empty( $contact['email'] ) ) {
669 $contact_persons[] = array(
670 'email' => $contact['email'],
671 'first_name' => $contact['first_name'] ?? '',
672 'last_name' => $contact['last_name'] ?? '',
673 'company_name' => $company_name,
674 'company_id' => $company_id,
675 );
676 }
677
678 return $contact_persons;
679 }
680 }
681