PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / trunk
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). vtrunk
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 / admin / includes / Actions / Sync / ExactOnlineSync.php
commercebird / admin / includes / Actions / Sync Last commit date
ExactOnlineSync.php 1 month ago ZohoCRMSync.php 5 months ago ZohoInventorySync.php 2 months ago index.php 1 year ago
ExactOnlineSync.php
765 lines
1 <?php
2
3 namespace CommerceBird\Admin\Actions\Sync;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 use CommerceBird\Admin\Actions\Ajax\ExactOnlineAjax;
10 use CommerceBird\Admin\Connectors\Connector;
11
12 /**
13 * Handles synchronization of products, customers, and orders between WooCommerce and Exact Online.
14 */
15 class ExactOnlineSync {
16
17 /**
18 * Whether to enable stock synchronization.
19 *
20 * @var bool Whether to enable stock synchronization.
21 * @since 2.6.5
22 */
23 public static bool $enable_eo_stock;
24
25 /**
26 * Construct to load options from wp_options
27 */
28 public function __construct() {
29 // Load any required options or settings here if needed.
30 self::$enable_eo_stock = get_option( 'cmbird_enable_eo_stock', false );
31 }
32
33 /**
34 * Sync data from Exact Online.
35 *
36 * @param string $type product|customer.
37 * @param array $data to sync from Exact Online.
38 * @param bool $import import or update.
39 * @return mixed
40 */
41 public function sync( string $type, array $data, bool $import = false ) {
42 if ( empty( $type ) ) {
43 return false;
44 }
45 if ( $import ) {
46 $this->import( $type, $data );
47 } else {
48 $this->update( $type, $data );
49 }
50 }
51 /**
52 * Import data from Exact Online.
53 *
54 * For product data will be like,
55 * {
56 * "Code":string,
57 * "Description": string,
58 * "ID": string,
59 * "IsSalesItem": bool,
60 * "PictureName": null|string,
61 * "PictureUrl": string,
62 * "StandardSalesPrice": float,
63 * "Stock": int
64 * },
65 *
66 * for Order data will be like,
67 * {
68 * "MainContact": null|string,
69 * "Email": null|string,
70 * "ID": string,
71 * "Name": string,
72 * "AddressLine1": null|string,
73 * "AddressLine2": null|string,
74 * "City": string,
75 * "Country": string,
76 * "Phone": null|string,
77 * "Postcode": string
78 * }
79 *
80 * @param string $type of provided data.
81 * @param array $data of import.
82 * @return mixed
83 */
84 public static function import( string $type, array $data ) {
85 // add logging.
86 // $fd = fopen( __DIR__ . '/import.txt', 'a+' );
87 $endpoint = '';
88 $payload = array();
89
90 switch ( $type ) {
91 case 'product':
92 $endpoint = '/wc/v3/products/batch';
93 $filtered_data = array_filter(
94 $data,
95 function ( $item ) {
96 // Exclude item if product already exists via SKU.
97 return ! wc_get_product_id_by_sku( $item['Code'] );
98 }
99 );
100 $payload = array(
101 'create' => array_map(
102 function ( $item ) {
103 // Skip image import if PictureName is 'placeholder_item'.
104 $images = array();
105 if ( isset( $item['PictureName'] ) && 'placeholder_item' !== $item['PictureName'] ) {
106 $image_id = self::get_existing_image_id( $item['PictureName'] );
107 // If image exists, add it to the images array, else upload the image and add it to the images array.
108 if ( $image_id ) {
109 $images[] = array( 'id' => $image_id );
110 } else {
111 $image_id = self::upload_image( $item['PictureUrl'], $item['PictureName'] );
112 // If image upload is successful, add it to the images array.
113 if ( $image_id ) {
114 $images[] = array( 'id' => $image_id );
115 }
116 }
117 }
118 // Check if category exists with $item['ItemGroupDescription'], else create it and get the ID.
119 if ( isset( $item['ItemGroupDescription'] ) ) {
120 $term = get_term_by( 'name', $item['ItemGroupDescription'], 'product_cat' );
121 $term_id = $term->term_id;
122 if ( empty( $term_id ) ) {
123 $term = wp_insert_term(
124 $item['ItemGroupDescription'],
125 'product_cat',
126 array(
127 'parent' => 0,
128 )
129 );
130 $term_id = $term['term_id'];
131 }
132 } else {
133 $term_id = 0;
134 }
135 // if stock exists then set manage_stock to true && if stock is enabled in settings.
136 $manage_stock = ( isset( $item['Stock'] ) && self::$enable_eo_stock ) ? true : false;
137 $item_stock = isset( $item['Stock'] ) ? $item['Stock'] : null;
138 return array(
139 'name' => $item['Description'],
140 'sku' => $item['Code'],
141 'status' => 'publish',
142 'type' => 'simple',
143 'regular_price' => (string) $item['StandardSalesPrice'],
144 'images' => $images,
145 'stock_quantity' => $item_stock,
146 'manage_stock' => $manage_stock,
147 'cost_of_goods_sold' => ( isset( $item['CostPriceStandard'] ) ) ? (float) $item['CostPriceStandard'] : null,
148 'categories' => array(
149 array(
150 'id' => $term_id,
151 ),
152 ),
153 'meta_data' => array(
154 array(
155 'key' => 'eo_item_id',
156 'value' => $item['ID'],
157 ),
158 ),
159 );
160 },
161 $filtered_data
162 ),
163 );
164 break;
165
166 case 'customer':
167 $endpoint = '/wc/v3/customers/batch';
168 $filtered_data = array_filter(
169 $data,
170 function ( $item ) {
171 // Exclude item if customer already exists via email.
172 return ! get_user_by( 'email', $item['Email'] );
173 }
174 );
175 $payload = array(
176 'create' => array_map(
177 function ( $item ) {
178 if ( empty( $item['Email'] ) ) {
179 return null;
180 }
181 if ( ! empty( $item['VATNumber'] ) ) {
182 $first_name = '';
183 $last_name = '';
184 $company = $item['Name'];
185 } else {
186 $names = explode( ' ', $item['Name'] );
187 $first_name = array_shift( $names ); // Take the first word as first name.
188 $last_name = implode( ' ', $names ); // Join the rest as last name.
189 }
190 // generate username based on email.
191 $username = explode( '@', $item['Email'] )[0];
192 // add random number to username if it already exists.
193 $existing_user = get_user_by( 'login', $username );
194 if ( $existing_user ) {
195 $username .= '_' . wp_rand( 1000, 9999 );
196 }
197 $address = array(
198 'first_name' => $first_name,
199 'last_name' => $last_name,
200 'company' => $company ?? '',
201 'address_1' => $item['AddressLine1'] ?? '',
202 'address_2' => $item['AddressLine2'] ?? '',
203 'city' => $item['City'] ?? '',
204 'country' => $item['Country'] ?? '',
205 'postcode' => $item['Postcode'] ?? '',
206 'phone' => $item['Phone'] ?? '',
207 'email' => $item['Email'],
208 );
209 return array(
210 'email' => $item['Email'],
211 'first_name' => $first_name,
212 'last_name' => $last_name,
213 'username' => $username,
214 'billing' => $address,
215 'shipping' => $address,
216 'meta_data' => array(
217 array(
218 'key' => 'eo_account_id',
219 'value' => $item['ID'],
220 ),
221 array(
222 'key' => 'eo_contact_id',
223 'value' => $item['MainContact'] ?? '',
224 ),
225 ),
226 );
227 },
228 array_filter( $filtered_data, fn( $item ) => ! empty( $item['Email'] ) )
229 ),
230 );
231 break;
232
233 default:
234 return false;
235 }
236
237 if ( empty( $payload['create'] ) ) {
238 return false;
239 }
240 // log the payload.
241 // fwrite( $fd, print_r( $payload, true ) );
242 add_filter( 'woocommerce_rest_check_permissions', '__return_true' );
243 $request = new \WP_REST_Request( 'POST', $endpoint );
244 $request->set_body_params( $payload );
245 $response = rest_do_request( $request );
246 remove_filter( 'woocommerce_rest_check_permissions', '__return_true' );
247 // log the response.
248 // fwrite( $fd, print_r( $response, true ) );
249 // fclose( stream: $fd );
250 return $response;
251 }
252
253 /**
254 * Update data based on Exact Online.
255 *
256 * @param string $type of provided data.
257 * @param array $data to update in Exact Online.
258 * @return mixed
259 */
260 public function update( string $type, array $data ) {
261 // $fd = fopen( __DIR__ . '/update.txt', 'w+' );
262 $endpoint = '';
263 $payload = array();
264 $is_product_update = false;
265 $simple_updates = array();
266 $variation_updates = array();
267 // log data.
268 // fwrite( $fd, print_r( $data, true ) );
269
270 switch ( $type ) {
271 case 'product':
272 $is_product_update = true;
273 // Track all SKUs from Exact Online for this batch.
274 $exact_skus = array_column( $data, 'Code' );
275 $existing_synced_skus = get_option( 'cmbird_eo_synced_skus', array() );
276 if ( ! is_array( $existing_synced_skus ) ) {
277 $existing_synced_skus = array();
278 }
279 $existing_synced_skus = array_merge( $existing_synced_skus, $exact_skus );
280 update_option( 'cmbird_eo_synced_skus', array_values( array_unique( $existing_synced_skus ) ), false );
281
282 $filtered_data = array_filter(
283 $data,
284 function ( $item ) {
285 // Exclude item if product does not exists via SKU.
286 return wc_get_product_id_by_sku( $item['Code'] );
287 }
288 );
289
290 foreach ( $filtered_data as $item ) {
291 // Check if product exists via SKU, else get the product ID by title.
292 $product_id = wc_get_product_id_by_sku( $item['Code'] ) ? wc_get_product_id_by_sku( $item['Code'] ) : self::get_product_id_by_title( $item['Description'] );
293 if ( empty( $product_id ) ) {
294 continue;
295 }
296
297 // Check if post exists in the database.
298 if ( ! get_post( $product_id ) ) {
299 continue;
300 }
301
302 $product = wc_get_product( $product_id );
303 if ( ! $product ) {
304 continue;
305 }
306
307 // Mark product/variation as synced with current timestamp.
308 update_post_meta( $product_id, '_eo_last_synced', time() );
309
310 $stock_status = '';
311 $backorders_allowed = get_post_meta( $product_id, '_backorders', true ) === 'yes';
312 if ( isset( $item['Stock'] ) && self::$enable_eo_stock ) {
313 if ( $item['Stock'] <= 0 ) {
314 $stock_status = $backorders_allowed ? 'outofstock' : 'instock';
315 } else {
316 $stock_status = 'instock';
317 }
318 }
319
320 if ( 'product_variation' === get_post_type( $product_id ) ) {
321 $parent_id = (int) wp_get_post_parent_id( $product_id );
322 if ( empty( $parent_id ) ) {
323 continue;
324 }
325
326 $variation_payload = array(
327 'id' => $product_id,
328 'stock_quantity' => ( isset( $item['Stock'] ) && self::$enable_eo_stock ) ? (int) $item['Stock'] : null,
329 'manage_stock' => ( isset( $item['Stock'] ) && self::$enable_eo_stock ) ? true : false,
330 'stock_status' => $stock_status,
331 'cost_of_goods_sold' => ( isset( $item['CostPriceStandard'] ) ) ? (float) $item['CostPriceStandard'] : null,
332 'meta_data' => array(
333 array(
334 'key' => 'eo_item_id',
335 'value' => $item['ID'],
336 ),
337 ),
338 );
339 if ( isset( $item['StandardSalesPrice'] ) && '' !== trim( (string) $item['StandardSalesPrice'] ) ) {
340 $variation_payload['regular_price'] = (string) $item['StandardSalesPrice'];
341 }
342
343 $variation_updates[ $parent_id ][] = $variation_payload;
344 continue;
345 }
346
347 // Only import an Exact image when the Woo product does not already have one.
348 if ( ! $product->get_image_id() && isset( $item['PictureName'] ) && 'placeholder_item' !== $item['PictureName'] ) {
349 $image_id = self::get_existing_image_id( $item['PictureName'] );
350 if ( $image_id ) {
351 set_post_thumbnail( $product_id, $image_id );
352 update_post_meta( $image_id, '_wp_attachment_image_alt', $item['Description'] );
353 update_post_meta( $product_id, '_thumbnail_id', $image_id );
354 wp_update_image_subsizes( $image_id );
355 } else {
356 $image_id = self::upload_image( $item['PictureUrl'], $item['PictureName'] );
357 if ( $image_id ) {
358 set_post_thumbnail( $product_id, $image_id );
359 update_post_meta( $image_id, '_wp_attachment_image_alt', $item['Description'] );
360 update_post_meta( $product_id, '_thumbnail_id', $image_id );
361 wp_update_image_subsizes( $image_id );
362 }
363 }
364 }
365
366 // Update product category for simple/parent products only.
367 if ( isset( $item['ItemGroupDescription'] ) ) {
368 $term = get_term_by( 'name', $item['ItemGroupDescription'], 'product_cat' );
369 $term_id = $term->term_id;
370 if ( empty( $term_id ) ) {
371 $term = wp_insert_term(
372 $item['ItemGroupDescription'],
373 'product_cat',
374 array(
375 'parent' => 0,
376 )
377 );
378 $term_id = $term['term_id'];
379 }
380 wp_set_object_terms( $product_id, $term_id, 'product_cat' );
381 }
382
383 $product_payload = array(
384 'id' => $product_id,
385 'stock_quantity' => ( isset( $item['Stock'] ) && self::$enable_eo_stock && ! $product->is_type( 'variable' ) ) ? (int) $item['Stock'] : null,
386 'manage_stock' => ( isset( $item['Stock'] ) && self::$enable_eo_stock && ! $product->is_type( 'variable' ) ) ? true : false,
387 'stock_status' => $stock_status,
388 'name' => $item['Description'],
389 'slug' => sanitize_title( $item['Description'] ),
390 'cost_of_goods_sold' => ( isset( $item['CostPriceStandard'] ) ) ? (float) $item['CostPriceStandard'] : null,
391 'meta_data' => array(
392 array(
393 'key' => 'eo_item_id',
394 'value' => $item['ID'],
395 ),
396 ),
397 );
398
399 if ( isset( $item['StandardSalesPrice'] ) && '' !== trim( (string) $item['StandardSalesPrice'] ) ) {
400 $product_payload['regular_price'] = (string) $item['StandardSalesPrice'];
401 }
402
403 $simple_updates[] = $product_payload;
404 }
405 break;
406
407 case 'customer':
408 $endpoint = '/wc/v3/customers/batch';
409 $payload = array(
410 'update' => array_map(
411 function ( $item ) {
412 $user = get_user_by( 'email', $item['Email'] );
413 return $user ? array(
414 'id' => $user->ID,
415 'meta_data' => array(
416 array(
417 'key' => 'eo_account_id',
418 'value' => $item['ID'],
419 ),
420 array(
421 'key' => 'eo_contact_id',
422 'value' => $item['MainContact'] ?? '',
423 ),
424 ),
425 // if $item['VATNumber'] is not empty then update the billing company name.
426 'billing' => ! empty( $item['VATNumber'] ) ? array(
427 'company' => $item['Name'],
428 ) : null,
429 ) : null;
430 },
431 array_filter( $data, fn( $item ) => get_user_by( 'email', $item['Email'] ) )
432 ),
433 );
434 break;
435
436 case 'orders':
437 $endpoint = '/wc/v3/orders/batch';
438 $payload = array(
439 'update' => array_map(
440 function ( $item ) {
441 return array(
442 'id' => $item['Description'],
443 'meta_data' => array(
444 array(
445 'key' => 'eo_order_id',
446 'value' => $item['OrderID'],
447 ),
448 array(
449 'key' => 'eo_order_number',
450 'value' => $item['OrderNumber'],
451 ),
452 ),
453 );
454 },
455 $data
456 ),
457 );
458 break;
459
460 default:
461 return false;
462 }
463
464 if ( $is_product_update ) {
465 if ( empty( $simple_updates ) && empty( $variation_updates ) ) {
466 return false;
467 }
468
469 add_filter( 'woocommerce_rest_check_permissions', '__return_true' );
470 $response = false;
471
472 if ( ! empty( $simple_updates ) ) {
473 /**
474 * Filters the batch payload for simple/parent products before it is sent to the WooCommerce REST API.
475 *
476 * Each item in the array corresponds to a single product update and contains keys such as
477 * `id`, `stock_quantity`, `manage_stock`, `stock_status`, `name`, `slug`, `regular_price`,
478 * and `meta_data`. You may remove, modify, or add keys to any item.
479 * Return an empty array to skip the batch request entirely.
480 *
481 * @param array[] $simple_updates Array of product update payloads.
482 */
483 $simple_updates = apply_filters( 'cmbird_eo_product_simple_updates', $simple_updates );
484 if ( ! empty( $simple_updates ) ) {
485 $request = new \WP_REST_Request( 'POST', '/wc/v3/products/batch' );
486 $request->set_body_params(
487 array(
488 'update' => $simple_updates,
489 )
490 );
491 $response = rest_do_request( $request );
492 }
493 }
494
495 foreach ( $variation_updates as $parent_id => $updates ) {
496 foreach ( array_chunk( $updates, 100 ) as $updates_chunk ) {
497 /**
498 * Filters the batch payload for a chunk of variation updates before it is sent to the WooCommerce REST API.
499 *
500 * Each item in the array corresponds to a single variation update and contains keys such as
501 * `id`, `stock_quantity`, `manage_stock`, `stock_status`, `regular_price`, and `meta_data`.
502 * You may remove, modify, or add keys to any item.
503 * Return an empty array to skip this chunk entirely.
504 *
505 * @param array[] $updates_chunk Array of variation update payloads for this chunk.
506 * @param int $parent_id The parent product ID these variations belong to.
507 */
508 $updates_chunk = apply_filters( 'cmbird_eo_product_variation_updates', $updates_chunk, (int) $parent_id );
509 if ( empty( $updates_chunk ) ) {
510 continue;
511 }
512 $variation_endpoint = sprintf( '/wc/v3/products/%d/variations/batch', (int) $parent_id );
513 $request = new \WP_REST_Request( 'POST', $variation_endpoint );
514 $request->set_body_params(
515 array(
516 'update' => $updates_chunk,
517 )
518 );
519 $response = rest_do_request( $request );
520 }
521 }
522
523 remove_filter( 'woocommerce_rest_check_permissions', '__return_true' );
524 return $response;
525 }
526 // fwrite( $fd, print_r( $payload, true ) );
527 if ( empty( $payload['update'] ) ) {
528 return false;
529 }
530 add_filter( 'woocommerce_rest_check_permissions', '__return_true' );
531 $request = new \WP_REST_Request( 'POST', $endpoint );
532 $request->set_body_params( $payload );
533 $response = rest_do_request( $request );
534 remove_filter( 'woocommerce_rest_check_permissions', '__return_true' );
535 // fwrite( $fd, 'response: ' . print_r( $response, true ) );
536 // fclose( $fd );
537 return $response;
538 }
539
540 private static function get_product_id_by_title( string $product_title ) {
541 // Set up the query arguments.
542 $args = array(
543 'post_type' => 'product',
544 'posts_per_page' => 1,
545 'fields' => 'ids',
546 's' => $product_title, // Search by product title.
547 );
548
549 // Run the query.
550 $query = new \WP_Query( $args );
551
552 // Get the product ID from the query results.
553 $product_id = $query->post_count > 0 ? $query->posts[0] : 0;
554
555 // Reset post data.
556 wp_reset_postdata();
557
558 return $product_id;
559 }
560
561 /**
562 * Whether Exact Online cron work should be skipped for configured quiet hours.
563 * Uses the site's configured timezone from WordPress settings.
564 *
565 * @return bool
566 */
567 private static function is_exact_cron_quiet_hours(): bool {
568 $current_hour = (int) wp_date( 'G', time(), wp_timezone() );
569
570 return $current_hour >= 0 && $current_hour < 6;
571 }
572
573 public static function get_payment_status_via_cron() {
574 if ( self::is_exact_cron_quiet_hours() ) {
575 return;
576 }
577
578 // execute get_payment_status of ExactOnlineAjax class.
579 $ajax = new ExactOnlineAjax();
580 $ajax->get_payment_status();
581 }
582
583 /**
584 * Process the payment status of the order via Exact Online.
585 *
586 * @param array $
587 * @return void
588 */
589 public static function cmbird_payment_status() {
590 if ( self::is_exact_cron_quiet_hours() ) {
591 return;
592 }
593
594 $args = func_get_args();
595 $order_id = $args[0];
596 if ( empty( $order_id ) ) {
597 return;
598 }
599 $order = wc_get_order( $order_id );
600 $object = array();
601 $order_id = $order->get_id();
602 $object['OrderID'] = $order_id;
603 $customer_id = $order->get_customer_id();
604 // get the eo_account_id from the user meta.
605 $object['AccountID'] = get_user_meta( $customer_id, 'eo_account_id', true );
606 $response = ( new Connector() )->payment_status( $object );
607 // check response contains "Payment_Status" key.
608 if ( ! isset( $response['Payment_Status'] ) ) {
609 return;
610 }
611 // if response is Paid then update the order status to completed.
612 if ( 'Paid' === $response['Payment_Status'] ) {
613 // set order as paid.
614 if ( $order->get_status() === 'completed' ) {
615 return;
616 }
617 $order->payment_complete();
618 $order->update_status( 'completed', __( 'Payment processed in Exact Online', 'commercebird' ) );
619 $order->save();
620 } elseif ( 'Unpaid' === $response['Payment_Status'] ) {
621 if ( $order->get_status() === 'on-hold' ) {
622 return;
623 }
624 $order->update_status( 'on-hold', __( 'Payment not processed in Exact Online', 'commercebird' ) );
625 $order->save();
626 }
627 }
628
629 /**
630 * Check if the image exists in the media library.
631 *
632 * @param string $picture_name
633 * @return int|false Attachment ID if exists, false otherwise
634 */
635 private static function get_existing_image_id( $picture_name ) {
636 global $wpdb;
637
638 // Strip extension from picture_name.
639 $picture_title = pathinfo( $picture_name, PATHINFO_FILENAME );
640
641 // Prepare a LIKE match to catch sanitized versions.
642 $like = '%' . $wpdb->esc_like( $picture_title ) . '%';
643
644 $attachment_id = $wpdb->get_var(
645 $wpdb->prepare(
646 "
647 SELECT ID FROM {$wpdb->posts}
648 WHERE post_type = 'attachment'
649 AND post_title LIKE %s
650 ORDER BY ID DESC
651 LIMIT 1
652 ",
653 $like
654 )
655 );
656
657 return $attachment_id ? (int) $attachment_id : false;
658 }
659
660 /**
661 * Upload the product image from Exact Online.
662 *
663 * @param string $picture_url URL of the image to upload.
664 * @param string $picture_name Name of the image file (used for naming the attachment).
665 * @return int|false Attachment ID of the uploaded image if successful, otherwise false
666 */
667 private static function upload_image( $picture_url, $picture_name ) {
668 require_once ABSPATH . 'wp-admin/includes/media.php';
669 require_once ABSPATH . 'wp-admin/includes/file.php';
670 require_once ABSPATH . 'wp-admin/includes/image.php';
671
672 // Fetch image content.
673 $response = wp_safe_remote_get( $picture_url, array( 'timeout' => 10 ) );
674
675 if ( is_wp_error( $response ) ) {
676 // error_log( 'Error fetching image: ' . $response->get_error_message() );
677 return false;
678 }
679
680 $image_data = wp_remote_retrieve_body( $response );
681 if ( empty( $image_data ) ) {
682 return false;
683 }
684
685 // Generate filename.
686 $upload_dir = wp_upload_dir();
687 $filename = sanitize_file_name( $picture_name );
688 $file_path = $upload_dir['path'] . '/' . $filename;
689
690 // Save the file.
691 file_put_contents( $file_path, $image_data );
692
693 // Check if file was saved correctly.
694 if ( ! file_exists( $file_path ) ) {
695 return false;
696 }
697
698 // Prepare file array for WordPress.
699 $file = array(
700 'name' => $filename,
701 'type' => mime_content_type( $file_path ),
702 'tmp_name' => $file_path,
703 'size' => filesize( $file_path ),
704 );
705
706 // Upload to WordPress Media Library.
707 $attachment_id = media_handle_sideload( $file, 0 );
708
709 // Check for errors.
710 if ( is_wp_error( $attachment_id ) ) {
711 // error_log( 'Error attaching image: ' . $attachment_id->get_error_message() );
712 return false;
713 }
714
715 return $attachment_id;
716 }
717
718 /**
719 * Sync orders via cron job.
720 * This function will get all orders from the last 30 days that have no eo_order_id or eo_invoice_id as meta key
721 * and send them to the send_orders function in CommerceBird class.
722 */
723 public static function sync_orders_via_cron() {
724 if ( self::is_exact_cron_quiet_hours() ) {
725 return;
726 }
727
728 $logger = wc_get_logger();
729 $context = array( 'source' => 'cmbird_exact_online_orders' );
730 // get all orders from the last 30 days that have no eo_order_id or eo_invoice_id as meta key.
731 $start_date = gmdate( 'Y-m-d H:i:s', strtotime( '-15 days' ) );
732 $end_date = time();
733 $orders = wc_get_orders(
734 array(
735 'status' => array_diff( array_keys( wc_get_order_statuses() ), array( 'wc-failed', 'wc-checkout-draft', 'wc-pending', 'wc-on-hold', 'wc-cancelled' ) ),
736 'limit' => -1,
737 'date_created' => $start_date . '...' . $end_date,
738 'return' => 'ids',
739 'meta_query' => array(
740 array(
741 'key' => 'eo_order_id',
742 'compare' => 'NOT EXISTS',
743 ),
744 ),
745 )
746 );
747 if ( empty( $orders ) ) {
748 return;
749 }
750 // send the orders to the send_orders function in CommerceBird class.
751 $response = ( new Connector() )->send_orders( array( 'orderIds' => $orders ) );
752 if ( is_string( $response ) || 200 !== $response['code'] ) {
753 // log the error.
754 $logger->error(
755 'Error syncing Exact Online orders',
756 $context + array(
757 'response' => $response,
758 'orders' => $orders,
759 )
760 );
761 return;
762 }
763 }
764 }
765