PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.8.4
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.8.4
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 2 months ago class-api-for-exact-webhooks.php 4 months ago class-api-for-product-webhook.php 2 months ago class-api-for-shipping-status.php 9 months ago class-api-for-woo-order.php 2 months ago class-api-for-zoho-inventory.php 9 months ago class-commercebird-list-items-api-controller.php 10 months ago class-commercebird-media-api-controller.php 10 months ago class-commercebird-metadata-controller.php 5 months ago index.php 1 year ago trait-api-permission.php 7 months ago
class-api-for-woo-order.php
564 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 WC_Coupon;
11 use WC_Customer;
12 use WP_REST_Response;
13
14 class CreateOrderWebhook {
15
16 use Api;
17 use LogWriter;
18
19 private static string $endpoint = 'create-woo-order';
20
21
22 public function __construct() {
23 register_rest_route(
24 self::$namespace,
25 self::$endpoint,
26 array(
27 'methods' => 'POST',
28 'callback' => array( $this, 'handle' ),
29 'permission_callback' => array( $this, 'permission_check' ),
30 )
31 );
32 }
33
34 /**
35 * @param $address
36 *
37 * @return array
38 */
39 public function format_address( $address ): array {
40 if ( array_key_exists( 'attention', $address ) ) {
41 $names = explode( ' ', $address['attention'] );
42 $first_name = $names[0] ?? '';
43 $last_name = $names[1] ?? '';
44 }
45 return array(
46 'first_name' => $first_name ?? '',
47 'last_name' => $last_name ?? '',
48 'address_1' => $address['address'] ?? '',
49 'address_2' => $address['street2'] ?? '',
50 'city' => $address['city'] ?? '',
51 'state' => $address['state_code'] ?? '',
52 'postcode' => $address['zip'] ?? '',
53 'country' => $address['country_code'] ?? '',
54 'phone' => $address['phone'] ?? '',
55 );
56 }
57
58 private function process( array $order_data ): WP_REST_Response {
59 $response = new WP_REST_Response();
60
61 // Check if the order data is valid.
62 if ( empty( $order_data ) || empty( $order_data['salesorder'] ) ) {
63 $response->set_data( $this->empty_response );
64 $response->set_status( 404 );
65 return $response;
66 }
67
68 // Validate salesorder_id exists.
69 if ( ! isset( $order_data['salesorder']['salesorder_id'] ) || empty( $order_data['salesorder']['salesorder_id'] ) ) {
70 /* translators: %1$s: Store name */
71 $message = sprintf( __( 'Zoho order could not be created in your store %1$s because of missing order ID.', 'commercebird' ), get_bloginfo( 'name' ) );
72 $common_class = new \CMBIRD_Common_Functions();
73 $common_class->send_email( __( 'Zoho Order Sync', 'commercebird' ), $message );
74 $response->set_status( 500 );
75 $response->set_data( $message );
76 return $response;
77 }
78
79 $allowed_keys = array(
80 'salesorder_id',
81 'salesorder_number',
82 'customer_id',
83 'billing_address',
84 'shipping_address',
85 'delivery_method',
86 'currency_code',
87 'line_items',
88 'contact_person_details',
89 'shipping_charge',
90 'order_status',
91 'paid_status',
92 'invoiced_status',
93 'discount',
94 'discount_percent',
95 'notes',
96 'salesperson_name',
97 );
98
99 $order_data = array_intersect_key( $order_data['salesorder'], array_flip( $allowed_keys ) );
100 $billing_address = $this->format_address( $order_data['billing_address'] );
101 $shipping_address = $this->format_address( $order_data['shipping_address'] );
102
103 // Handle customer creation/update.
104 $customer = null;
105 if ( isset( $order_data['contact_person_details'][0]['email'] ) ) {
106 $customer_data = $order_data['contact_person_details'][0];
107 $customer_mail = $customer_data['email'];
108 $customer = get_user_by( 'email', $customer_mail );
109 if ( empty( $customer ) ) {
110 $customer_id = wc_create_new_customer( $customer_mail );
111 $wc_customer = new \WC_Customer( $customer_id );
112 // Set billing details.
113 $wc_customer->set_billing_first_name( $billing_address['first_name'] );
114 $wc_customer->set_billing_last_name( $billing_address['last_name'] );
115 $wc_customer->set_billing_address_1( $billing_address['address_1'] );
116 $wc_customer->set_billing_address_2( $billing_address['address_2'] );
117 $wc_customer->set_billing_city( $billing_address['city'] );
118 $wc_customer->set_billing_postcode( $billing_address['postcode'] );
119 $wc_customer->set_billing_country( $billing_address['country'] );
120 $wc_customer->set_billing_state( $billing_address['state'] );
121 $wc_customer->set_billing_email( $customer_mail );
122 $wc_customer->set_billing_phone( $billing_address['phone'] );
123 // Set shipping details.
124 $wc_customer->set_shipping_first_name( $shipping_address['first_name'] );
125 $wc_customer->set_shipping_last_name( $shipping_address['last_name'] );
126 $wc_customer->set_shipping_address_1( $shipping_address['address_1'] );
127 $wc_customer->set_shipping_address_2( $shipping_address['address_2'] );
128 $wc_customer->set_shipping_city( $shipping_address['city'] );
129 $wc_customer->set_shipping_postcode( $shipping_address['postcode'] );
130 $wc_customer->set_shipping_country( $shipping_address['country'] );
131 $wc_customer->set_shipping_state( $shipping_address['state'] );
132 $wc_customer->save();
133 $customer = get_user_by( 'id', $customer_id );
134 } else {
135 $wc_customer = new \WC_Customer( $customer->ID );
136 // Set billing details.
137 $wc_customer->set_billing_first_name( $billing_address['first_name'] );
138 $wc_customer->set_billing_last_name( $billing_address['last_name'] );
139 $wc_customer->set_billing_address_1( $billing_address['address_1'] );
140 $wc_customer->set_billing_address_2( $billing_address['address_2'] );
141 $wc_customer->set_billing_city( $billing_address['city'] );
142 $wc_customer->set_billing_postcode( $billing_address['postcode'] );
143 $wc_customer->set_billing_country( $billing_address['country'] );
144 $wc_customer->set_billing_state( $billing_address['state'] );
145 $wc_customer->set_billing_email( $customer_mail );
146 $wc_customer->set_billing_phone( $billing_address['phone'] );
147 // Set shipping details.
148 $wc_customer->set_shipping_first_name( $shipping_address['first_name'] );
149 $wc_customer->set_shipping_last_name( $shipping_address['last_name'] );
150 $wc_customer->set_shipping_address_1( $shipping_address['address_1'] );
151 $wc_customer->set_shipping_address_2( $shipping_address['address_2'] );
152 $wc_customer->set_shipping_city( $shipping_address['city'] );
153 $wc_customer->set_shipping_postcode( $shipping_address['postcode'] );
154 $wc_customer->set_shipping_country( $shipping_address['country'] );
155 $wc_customer->set_shipping_state( $shipping_address['state'] );
156 $wc_customer->save();
157 }
158 }
159
160 // Validate line items exist.
161 if ( empty( $order_data['line_items'] ) ) {
162 // translators: 1: order number, 2: Store name.
163 $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' ) );
164 $common_class = new \CMBIRD_Common_Functions();
165 $common_class->send_email( __( 'Zoho Order Sync', 'commercebird' ), $message );
166 $response->set_status( 500 );
167 $response->set_data( $message );
168 return $response;
169 }
170
171 $line_items = $this->get_items( $order_data['line_items'] );
172
173 if ( ! empty( $line_items['not_found'] ) ) {
174 // translators: 1: order number, 2: Store name, 3: missing items.
175 $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['not_found'] ) );
176 $common_class = new \CMBIRD_Common_Functions();
177 $common_class->send_email( __( 'Zoho Order Sync', 'commercebird' ), $message );
178 $response->set_status( 500 );
179 $response->set_data( $message );
180 return $response;
181 }
182
183 $discount_percent = $order_data['discount_percent'] ?? '';
184 if ( ! empty( $discount_percent ) && $discount_percent > 0 ) {
185 $coupon_code = $this->get_coupon_code( $order_data );
186 }
187
188 // Check if order already exists.
189 $orders = wc_get_orders(
190 array(
191 'limit' => 1,
192 'meta_key' => 'zi_salesorder_id',
193 'meta_value' => $order_data['salesorder_id'],
194 'meta_compare' => '=',
195 'return' => 'ids',
196 )
197 );
198 if ( ! empty( $orders ) ) {
199 $existing_order = wc_get_order( $orders[0] );
200 }
201
202 // Update existing order.
203 if ( ! empty( $existing_order ) && empty( $line_items['not_found'] ) ) {
204 // if order status is completed or processing, do not update the order and return error response.
205 if ( in_array( $existing_order->get_status(), array( 'completed', 'processing' ), true ) ) {
206 /* translators: %1$s: Zoho order number, %2$s: Store name, %3$s: Order status */
207 $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() );
208 $common_class = new \CMBIRD_Common_Functions();
209 $common_class->send_email( __( 'Zoho Order Sync', 'commercebird' ), $message );
210 $response->set_status( 200 );
211 $response->set_data( $message );
212 return $response;
213 }
214
215 $existing_order->set_address( $billing_address, 'billing' );
216 $existing_order->set_address( $shipping_address, 'shipping' );
217 // Remove existing order items.
218 $existing_order->remove_order_items( 'line_item' );
219 // Get all applied coupons and remove them.
220 $applied_coupons = $existing_order->get_coupon_codes();
221 foreach ( $applied_coupons as $coupon_code_old ) {
222 $existing_order->remove_coupon( $coupon_code_old );
223 }
224 // Add products to the order.
225 foreach ( $line_items as $line_item ) {
226 $product_id = $line_item['id'];
227 $quantity = $line_item['quantity'];
228 $rate = $line_item['rate'] ?? null;
229 if ( ! empty( $product_id ) ) {
230 $product = wc_get_product( $product_id );
231 if ( $product && $product->is_type( 'variation' ) ) {
232 // Get variation object.
233 $variation = wc_get_product_object( 'variation', $product_id );
234 if ( $variation ) {
235 $item_id = $existing_order->add_product( $variation, $quantity );
236 if ( null !== $rate && $item_id ) {
237 $item = $existing_order->get_item( $item_id );
238 $line_total = $rate * $quantity;
239 $item->set_subtotal( $line_total );
240 $item->set_total( $line_total );
241 $item->save();
242 }
243 }
244 } else {
245 $item_id = $existing_order->add_product( $product, $quantity );
246 if ( null !== $rate && $item_id ) {
247 $item = $existing_order->get_item( $item_id );
248 $line_total = $rate * $quantity;
249 $item->set_subtotal( $line_total );
250 $item->set_total( $line_total );
251 $item->save();
252 }
253 }
254 }
255 }
256
257 // Set order details.
258 $existing_order->set_customer_note( isset( $order_data['notes'] ) ? $order_data['notes'] : '' );
259 $existing_order->update_status( $this->map_status( $order_data['paid_status'], $order_data['invoiced_status'] ?? '' ) );
260
261 // Handle shipping.
262 // Remove existing shipping items first.
263 foreach ( $existing_order->get_items( 'shipping' ) as $item_id => $item ) {
264 $existing_order->remove_item( $item_id );
265 }
266
267 if ( isset( $order_data['shipping_charge'] ) && $order_data['shipping_charge'] > 0 ) {
268 $shipping = new \WC_Order_Item_Shipping();
269 $shipping->set_method_title( $order_data['delivery_method'] ?? 'Flat Rate' );
270 $shipping->set_method_id( 'flat_rate' );
271 $shipping->set_total( $order_data['shipping_charge'] );
272 $existing_order->add_item( $shipping );
273 }
274
275 $existing_order->calculate_totals();
276 $existing_order->save();
277 $note = 'Updated in Zoho Inventory';
278 if ( ! empty( $order_data['salesperson_name'] ) ) {
279 $note .= ' by ' . $order_data['salesperson_name'];
280 }
281 $existing_order->add_order_note( $note );
282 // get new order object and calculate totals again.
283 $updated_order = wc_get_order( $existing_order->get_id() );
284 $updated_order->calculate_totals();
285 $updated_order->save();
286
287 $response->set_data(
288 array(
289 'order_id' => $existing_order->get_id(),
290 'total' => $existing_order->get_total(),
291 'status' => $existing_order->get_status(),
292 )
293 );
294 $response->set_status( 200 );
295 } elseif ( empty( $line_items['not_found'] ) ) {
296 // Create new order.
297 $order = wc_create_order();
298 if ( $customer ) {
299 $order->set_customer_id( $customer->ID );
300 }
301 // Add products to the order.
302 foreach ( $line_items as $line_item ) {
303 $product_id = $line_item['id'];
304 $quantity = $line_item['quantity'];
305 $rate = $line_item['rate'] ?? null;
306 if ( ! empty( $product_id ) ) {
307 $product = wc_get_product( $product_id );
308 if ( $product && $product->is_type( 'variation' ) ) {
309 // Get variation object.
310 $variation = wc_get_product_object( 'variation', $product_id );
311 if ( $variation ) {
312 $item_id = $order->add_product( $variation, $quantity );
313 if ( null !== $rate && $item_id ) {
314 $item = $order->get_item( $item_id );
315 $line_total = $rate * $quantity;
316 $item->set_subtotal( $line_total );
317 $item->set_total( $line_total );
318 $item->save();
319 }
320 }
321 } else {
322 $item_id = $order->add_product( $product, $quantity );
323 if ( null !== $rate && $item_id ) {
324 $item = $order->get_item( $item_id );
325 $line_total = $rate * $quantity;
326 $item->set_subtotal( $line_total );
327 $item->set_total( $line_total );
328 $item->save();
329 }
330 }
331 }
332 }
333
334 $order->set_address( $billing_address, 'billing' );
335 $order->set_address( $shipping_address, 'shipping' );
336 $order->set_currency( $order_data['currency_code'] );
337 $order->set_status( $this->map_status( $order_data['paid_status'], $order_data['invoiced_status'] ?? '' ) );
338 // Handle shipping.
339 if ( isset( $order_data['shipping_charge'] ) && $order_data['shipping_charge'] > 0 ) {
340 $shipping = new \WC_Order_Item_Shipping();
341 $shipping->set_method_title( $order_data['delivery_method'] ?? 'Flat Rate' );
342 $shipping->set_method_id( 'flat_rate' );
343 $shipping->set_total( $order_data['shipping_charge'] );
344 $order->add_item( $shipping );
345 }
346
347 // Apply coupon if discount exists.
348 if ( isset( $coupon_code ) && ! empty( $coupon_code ) ) {
349 $order->apply_coupon( $coupon_code );
350 }
351 $order->calculate_totals();
352 $order->set_customer_note( $order_data['notes'] ?? '' );
353 $order->save();
354 $order->update_meta_data( 'zi_salesorder_id', $order_data['salesorder_id'] );
355 $order->save();
356 $note = 'Synced in Zoho Inventory';
357 if ( ! empty( $order_data['salesperson_name'] ) ) {
358 $note .= ' by ' . $order_data['salesperson_name'];
359 }
360 $order->add_order_note( $note );
361
362 // Update Zoho sales order with WooCommerce order ID as reference number.
363 $this->update_zoho_reference_number( $order, $order_data['salesorder_id'] );
364
365 $response->set_data(
366 array(
367 'order_id' => $order->get_id(),
368 'status' => $order->get_status(),
369 )
370 );
371 $response->set_status( 200 );
372 } else {
373 $response->set_data(
374 array(
375 'status' => 'Something went wrong. Please try again.',
376 )
377 );
378 $response->set_status( 500 );
379 }
380
381 return $response;
382 }
383
384
385 private function map_status( $paid_status, $invoiced_status = '' ): string {
386 // If invoiced, status must be processing.
387 if ( 'invoiced' === $invoiced_status ) {
388 return 'wc-processing';
389 }
390
391 // Otherwise check paid status.
392 switch ( $paid_status ) {
393 case 'paid':
394 return 'wc-completed';
395 default:
396 return 'wc-pending';
397 }
398 }
399
400
401 /**
402 * Update Zoho sales order reference number with WooCommerce order ID.
403 *
404 * @param \WC_Order $order WooCommerce order object.
405 * @param string $salesorder_id Zoho sales order ID.
406 * @return void
407 */
408 private function update_zoho_reference_number( $order, $salesorder_id ): void {
409 // Get Zoho Inventory configuration.
410 $zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );
411 $zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
412
413 if ( empty( $zoho_inventory_url ) || empty( $zoho_inventory_oid ) || empty( $salesorder_id ) ) {
414 return;
415 }
416
417 // Prepare reference number using the same logic as order sync.
418 $transaction_id = $order->get_transaction_id();
419 if ( empty( $transaction_id ) ) {
420 $transaction_id = $order->get_meta( '_order_number', true );
421 }
422
423 $order_prefix = get_option( 'cmbird_zoho_order_prefix_status' );
424 $reference_number = '';
425
426 if ( class_exists( 'WCJ_Order_Numbers' ) || class_exists( 'WC_Seq_Order_Number_Pro' ) ) {
427 $reference_number = $order_prefix . $transaction_id;
428 } elseif ( ! empty( $order_prefix ) ) {
429 $reference_number = $order_prefix . '-' . $order->get_id();
430 } else {
431 $reference_number = $order->get_id();
432 }
433
434 // Build the API URL.
435 $url = $zoho_inventory_url . 'inventory/v1/salesorders/' . $salesorder_id . '?organization_id=' . $zoho_inventory_oid;
436
437 // Prepare data for update.
438 $data = array(
439 'JSONString' => wp_json_encode(
440 array(
441 'reference_number' => $reference_number,
442 )
443 ),
444 );
445
446 // Make the API call to update Zoho sales order.
447 $api_handler = new \CMBIRD_API_Handler_Zoho();
448 $response = $api_handler->execute_curl_call_put( $url, $data );
449
450 // Log any errors.
451 $logger = wc_get_logger();
452 if ( is_wp_error( $response ) ) {
453 $logger->error(
454 'Error updating Zoho reference number: ' . $response->get_error_message(),
455 array(
456 'source' => 'commercebird',
457 'order_id' => $order->get_id(),
458 'salesorder_id' => $salesorder_id,
459 )
460 );
461 } elseif ( isset( $response->code ) && 0 !== $response->code ) {
462 $error_message = isset( $response->message ) ? $response->message : 'Unknown error';
463 $logger->error(
464 'Error updating Zoho reference number: ' . $error_message,
465 array(
466 'source' => 'commercebird',
467 'order_id' => $order->get_id(),
468 'salesorder_id' => $salesorder_id,
469 )
470 );
471 } else {
472 $order->add_order_note( 'Updated Zoho reference number to: ' . $reference_number );
473 }
474 }
475
476
477 /**
478 * Find the product ids from the line items from Zoho Inventory.
479 *
480 * @param mixed $line_items Line items from the salesorder.
481 * @return array
482 */
483 private function get_items( $line_items ): array {
484 $product_ids = array();
485 $not_found = array();
486
487 foreach ( $line_items as $item ) {
488 // Check if the sku key is present (case-insensitive check).
489 $sku = isset( $item['sku'] ) ? $item['sku'] : ( isset( $item['SKU'] ) ? $item['SKU'] : null );
490 $quantity = $item['quantity'] ?? 1;
491 $rate = $item['rate'] ?? null;
492
493 if ( null !== $sku ) {
494 $product_id = wc_get_product_id_by_sku( $sku );
495 if ( $product_id ) {
496 // Append to product_ids instead of overwriting.
497 $product_ids[] = array(
498 'id' => $product_id,
499 'quantity' => $quantity,
500 'rate' => $rate,
501 );
502 } else {
503 $not_found[] = $sku;
504 }
505 } else {
506 $not_found[] = 'Unknown SKU';
507 }
508 }
509
510 if ( ! empty( $not_found ) ) {
511 return array( 'not_found' => $not_found );
512 }
513
514 return $product_ids;
515 }
516
517 /**
518 * Find the coupon code based on Discount or create the coupon code and return it.
519 * @param mixed $order_data The order data from salesforce.
520 * @return string The coupon code.
521 */
522 private function get_coupon_code( $order_data ): string {
523 if ( isset( $order_data['discount_percent'] ) && ! empty( $order_data['discount_percent'] ) ) {
524 $fixed_cart_amount = $order_data['discount_percent'];
525 // Search for an existing coupon by fixed cart amount.
526 $existing_coupon = get_posts(
527 array(
528 'post_type' => 'shop_coupon',
529 'post_status' => 'publish',
530 'post_per_page' => -1,
531 'meta_query' => array(
532 'relation' => 'AND',
533 array(
534 'key' => 'discount_type',
535 'value' => 'percent',
536 ),
537 array(
538 'key' => 'coupon_amount',
539 'value' => $fixed_cart_amount,
540 'compare' => '=',
541 ),
542 ),
543 )
544 );
545
546 if ( $existing_coupon ) {
547 $coupon_code = $existing_coupon[0]->post_title;
548 return $coupon_code;
549 } else {
550 // If not found, create a new coupon with a random code.
551 $coupon_code = 'AUTO_' . wp_generate_password( 8, false );
552 $coupon = new \WC_Coupon();
553 $coupon->set_code( $coupon_code );
554 $coupon->set_description( 'Auto-generated coupon.' );
555 $coupon->set_discount_type( 'percent' );
556 $coupon->set_amount( $fixed_cart_amount );
557 $coupon->save();
558 return $coupon_code;
559 }
560 }
561 return '';
562 }
563 }
564