PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.3.10
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.3.10
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 year ago ZohoCRMSync.php 1 year ago index.php 1 year ago
ExactOnlineSync.php
552 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\CommerceBird;
11
12 class ExactOnlineSync {
13
14
15 /**
16 * Sync data from Exact Online.
17 *
18 * @param string $type product|customer
19 * @param array $data to sync from Exact Online
20 * @param bool $import import or update
21 * @return mixed
22 */
23 public static function sync( string $type, array $data, bool $import = false ) {
24 if ( empty( $type ) ) {
25 return false;
26 }
27 if ( $import ) {
28 self::import( $type, $data );
29 } else {
30 self::update( $type, $data );
31 }
32 }
33 /**
34 * Import data from Exact Online.
35 *
36 * for product data will be like,
37 * {
38 * "Code":string,
39 * "Description": string,
40 * "ID": string,
41 * "IsSalesItem": bool,
42 * "PictureName": null|string,
43 * "PictureUrl": string,
44 * "StandardSalesPrice": float,
45 * "Stock": int
46 * },
47 *
48 * for Order data will be like,
49 * {
50 * "MainContact": null|string,
51 * "Email": null|string,
52 * "ID": string,
53 * "Name": string,
54 * "AddressLine1": null|string,
55 * "AddressLine2": null|string,
56 * "City": string,
57 * "Country": string,
58 * "Phone": null|string,
59 * "Postcode": string
60 * }
61 * @param string $type of provided data;
62 * @param array $data of import
63 * @return mixed
64 */
65 public static function import( string $type, array $data ) {
66 // add logging
67 // $fd = fopen( __DIR__ . '/import.txt', 'w+' );
68 $endpoint = '';
69 $payload = array();
70
71 switch ( $type ) {
72 case 'product':
73 $endpoint = '/wc/v3/products/batch';
74 $filtered_data = array_filter(
75 $data,
76 function ( $item ) {
77 // Exclude item if product already exists via SKU
78 return ! wc_get_product_id_by_sku( $item['Code'] );
79 }
80 );
81 $payload = array(
82 'create' => array_map(
83 function ( $item ) {
84 // Skip image import if PictureName is 'placeholder_item'
85 $images = array();
86 if ( isset( $item['PictureName'] ) && 'placeholder_item' !== $item['PictureName'] ) {
87 $image_id = self::get_existing_image_id( $item['PictureName'] );
88 // If image exists, add it to the images array, else upload the image and add it to the images array
89 if ( $image_id ) {
90 $images[] = array( 'id' => $image_id );
91 } else {
92 $image_id = self::upload_image( $item['PictureUrl'], $item['PictureName'] );
93 // If image upload is successful, add it to the images array
94 if ( $image_id ) {
95 $images[] = array( 'id' => $image_id );
96 }
97 }
98 }
99 // Check if category exists with $item['ItemGroupDescription'], else create it and get the ID
100 if ( isset( $item['ItemGroupDescription'] ) ) {
101 $term = get_term_by( 'name', $item['ItemGroupDescription'], 'product_cat' );
102 $term_id = $term->term_id;
103 if ( empty( $term_id ) ) {
104 $term = wp_insert_term(
105 $item['ItemGroupDescription'],
106 'product_cat',
107 array(
108 'parent' => 0,
109 )
110 );
111 $term_id = $term['term_id'];
112 }
113 } else {
114 $term_id = 0;
115 }
116 // if stock exists then set manage_stock to true
117 $manage_stock = isset( $item['Stock'] ) ? true : false;
118 return array(
119 'name' => $item['Description'],
120 'sku' => $item['Code'],
121 'status' => 'publish',
122 'type' => 'simple',
123 'regular_price' => (string) $item['StandardSalesPrice'],
124 'images' => $images,
125 'stock_quantity' => $item['Stock'],
126 'manage_stock' => $manage_stock,
127 'categories' => array(
128 array(
129 'id' => $term_id,
130 ),
131 ),
132 'meta_data' => array(
133 array(
134 'key' => 'eo_item_id',
135 'value' => $item['ID'],
136 ),
137 array(
138 'key' => '_cost_price',
139 'value' => $item['CostPriceStandard'],
140 ),
141 array(
142 'key' => 'eo_unit',
143 'value' => $item['Unit'],
144 ),
145 ),
146 );
147 },
148 $filtered_data
149 ),
150 );
151 break;
152
153 case 'customer':
154 $endpoint = '/wc/v3/customers/batch';
155 $filtered_data = array_filter(
156 $data,
157 function ( $item ) {
158 // Exclude item if customer already exists via email
159 return ! get_user_by( 'email', $item['Email'] );
160 }
161 );
162 $payload = array(
163 'create' => array_map(
164 function ( $item ) {
165 if ( empty( $item['Email'] ) ) {
166 return null;
167 }
168 if ( ! empty( $item['VATNumber'] ) ) {
169 $first_name = '';
170 $last_name = '';
171 $company = $item['Name'];
172 } else {
173 $names = explode( ' ', $item['Name'] );
174 $first_name = array_shift( $names ); // Take the first word as first name
175 $last_name = implode( ' ', $names ); // Join the rest as last name
176 }
177 // generate username based on email
178 $username = explode( '@', $item['Email'] )[0];
179 $address = array(
180 'first_name' => $first_name,
181 'last_name' => $last_name,
182 'company' => $company ?? '',
183 'address_1' => $item['AddressLine1'] ?? '',
184 'address_2' => $item['AddressLine2'] ?? '',
185 'city' => $item['City'] ?? '',
186 'country' => $item['Country'] ?? '',
187 'postcode' => $item['Postcode'] ?? '',
188 'phone' => $item['Phone'] ?? '',
189 'email' => $item['Email'],
190 );
191 return array(
192 'email' => $item['Email'],
193 'first_name' => $first_name,
194 'last_name' => $last_name,
195 'username' => $username,
196 'billing' => $address,
197 'shipping' => $address,
198 'meta_data' => array(
199 array(
200 'key' => 'eo_account_id',
201 'value' => $item['ID'],
202 ),
203 array(
204 'key' => 'eo_contact_id',
205 'value' => $item['MainContact'] ?? '',
206 ),
207 ),
208 );
209 },
210 array_filter( $filtered_data, fn( $item ) => ! empty( $item['Email'] ) )
211 ),
212 );
213 break;
214
215 default:
216 return false;
217 }
218
219 if ( empty( $endpoint ) || empty( $payload['create'] ) ) {
220 return false;
221 }
222 // log the payload
223 // fwrite( $fd, print_r( $payload, true ) );
224 $request = new \WP_REST_Request( 'POST', $endpoint );
225 $request->set_body_params( $payload );
226 $response = rest_do_request( $request );
227 // error_log the message if response is error
228 if ( is_wp_error( $response ) ) {
229 error_log( 'Error ExactOnline Import: ' . $response->get_error_message() );
230 }
231 // fwrite( $fd, print_r( $response, true ) );
232 // fclose( stream: $fd );
233 return $response;
234 }
235
236 /**
237 * Update data based on Exact Online.
238 * @param string $type of provided data
239 * @return mixed
240 */
241 public static function update( string $type, array $data ) {
242 // $fd = fopen( __DIR__ . '/update.txt', 'w+' );
243 $endpoint = '';
244 $payload = array();
245 // log data
246 // fwrite( $fd, print_r( $data, true ) );
247
248 switch ( $type ) {
249 case 'product':
250 $filtered_data = array_filter(
251 $data,
252 function ( $item ) {
253 // Exclude item if product does not exists via SKU
254 return wc_get_product_id_by_sku( $item['Code'] );
255 }
256 );
257 $endpoint = '/wc/v3/products/batch';
258 $payload = array(
259 'update' => array_map(
260 function ( $item ) {
261 // Check if product exists via SKU, else get the product ID by title
262 $product_id = wc_get_product_id_by_sku( $item['Code'] ) ?: self::get_product_id_by_title( $item['Description'] );
263 // Featured image
264 if ( isset( $item['PictureName'] ) && 'placeholder_item' !== $item['PictureName'] ) {
265 $image_id = self::get_existing_image_id( $item['PictureName'] );
266 // If image exists, add it to the images array, else upload the image and add it to the images array
267 if ( $image_id ) {
268 set_post_thumbnail( $product_id, $image_id );
269 update_post_meta( $image_id, '_wp_attachment_image_alt', $item['Description'] );
270 update_post_meta( $product_id, '_thumbnail_id', $image_id );
271 wp_update_image_subsizes( $image_id );
272 } else {
273 $image_id = self::upload_image( $item['PictureUrl'], $item['PictureName'] );
274 // If image upload is successful, add it to the images array
275 if ( $image_id ) {
276 set_post_thumbnail( $product_id, $image_id );
277 update_post_meta( $image_id, '_wp_attachment_image_alt', $item['Description'] );
278 update_post_meta( $product_id, '_thumbnail_id', $image_id );
279 wp_update_image_subsizes( $image_id );
280 }
281 }
282 }
283 // update product category
284 if ( isset( $item['ItemGroupDescription'] ) ) {
285 // Check if term exists by name
286 $term = get_term_by( 'name', $item['ItemGroupDescription'], 'product_cat' );
287 $term_id = $term->term_id;
288 if ( empty( $term_id ) ) {
289 $term = wp_insert_term(
290 $item['ItemGroupDescription'],
291 'product_cat',
292 array(
293 'parent' => 0,
294 )
295 );
296 $term_id = $term['term_id'];
297 }
298 // update product category directly
299 wp_set_object_terms( $product_id, $term_id, 'product_cat' );
300 }
301 // update product name and slug if its different from current one
302 $product = wc_get_product( $product_id );
303 if ( $product->get_name() !== $item['Description'] ) {
304 $product->set_name( $item['Description'] );
305 $product->set_slug( sanitize_title( $item['Description'] ) );
306 $product->save();
307 }
308 // create meta_data array if product does not contain eo_item_id meta
309 $meta_data = get_post_meta( $product_id, 'eo_item_id', true );
310 if ( empty( $meta_data ) ) {
311 update_post_meta( $product_id, 'eo_item_id', $item['ID'] );
312 update_post_meta( $product_id, '_cost_price', $item['CostPriceStandard'] );
313 update_post_meta( $product_id, 'eo_unit', $item['Unit'] );
314 }
315 return array(
316 'id' => $product_id,
317 'regular_price' => (string) $item['StandardSalesPrice'],
318 );
319 },
320 $filtered_data
321 ),
322 );
323 break;
324
325 case 'customer':
326 $endpoint = '/wc/v3/customers/batch';
327 $payload = array(
328 'update' => array_map(
329 function ( $item ) {
330 $user = get_user_by( 'email', $item['Email'] );
331 return $user ? array(
332 'id' => $user->ID,
333 'meta_data' => array(
334 array(
335 'key' => 'eo_account_id',
336 'value' => $item['ID'],
337 ),
338 array(
339 'key' => 'eo_contact_id',
340 'value' => $item['MainContact'] ?? '',
341 ),
342 ),
343 // if $item['VATNumber'] is not empty then update the billing company name
344 'billing' => ! empty( $item['VATNumber'] ) ? array(
345 'company' => $item['Name'],
346 ) : null,
347 ) : null;
348 },
349 array_filter( $data, fn( $item ) => get_user_by( 'email', $item['Email'] ) )
350 ),
351 );
352 break;
353
354 case 'orders':
355 $endpoint = '/wc/v3/orders/batch';
356 $payload = array(
357 'update' => array_map(
358 function ( $item ) {
359 return array(
360 'id' => $item['Description'],
361 'meta_data' => array(
362 array(
363 'key' => 'eo_order_id',
364 'value' => $item['OrderID'],
365 ),
366 array(
367 'key' => 'eo_order_number',
368 'value' => $item['OrderNumber'],
369 ),
370 ),
371 );
372 },
373 $data
374 ),
375 );
376 break;
377
378 default:
379 return false;
380 }
381 // fwrite( $fd, print_r( $payload, true ) );
382 if ( empty( $endpoint ) || empty( $payload['update'] ) ) {
383 return false;
384 }
385 $request = new \WP_REST_Request( 'POST', $endpoint );
386 $request->set_body_params( $payload );
387 $response = rest_do_request( $request );
388 // error_log the message if response is error
389 if ( is_wp_error( $response ) ) {
390 error_log( 'Error ExactOnline Update: ' . $response->get_error_message() );
391 }
392 // fwrite( $fd, 'response: ' . print_r( $response, true ) );
393 // fclose( $fd );
394 return $response;
395 }
396
397 private static function get_product_id_by_title( string $product_title ) {
398 // Set up the query arguments
399 $args = array(
400 'post_type' => 'product',
401 'posts_per_page' => 1,
402 'fields' => 'ids',
403 's' => $product_title, // Search by product title
404 );
405
406 // Run the query
407 $query = new \WP_Query( $args );
408
409 // Get the product ID from the query results
410 $product_id = $query->post_count > 0 ? $query->posts[0] : 0;
411
412 // Reset post data
413 wp_reset_postdata();
414
415 return $product_id;
416 }
417
418 public static function get_payment_status_via_cron() {
419 // execute get_payment_status of ExactOnlineAjax class
420 $ajax = new ExactOnlineAjax();
421 $ajax->get_payment_status();
422 }
423
424 /**
425 * Process the payment status of the order via Exact Online.
426 * @param array $
427 * @return void
428 */
429 public static function cmbird_payment_status() {
430 $args = func_get_args();
431 $order_id = $args[0];
432 if ( empty( $order_id ) ) {
433 return;
434 }
435 $order = wc_get_order( $order_id );
436 $object = array();
437 $order_id = $order->get_id();
438 $object['OrderID'] = $order_id;
439 $customer_id = $order->get_customer_id();
440 // get the eo_account_id from the user meta
441 $object['AccountID'] = get_user_meta( $customer_id, 'eo_account_id', true );
442 $response = ( new CommerceBird() )->payment_status( $object );
443 // check response contains "Payment_Status" key
444 if ( ! isset( $response['Payment_Status'] ) ) {
445 return;
446 }
447 // if response is Paid then update the order status to completed
448 if ( 'Paid' === $response['Payment_Status'] ) {
449 // set order as paid
450 if ( $order->get_status() === 'completed' ) {
451 return;
452 }
453 $order->payment_complete();
454 $order->update_status( 'completed', __( 'Payment processed in Exact Online', 'commercebird' ) );
455 $order->save();
456 } elseif ( 'Unpaid' === $response['Payment_Status'] ) {
457 if ( $order->get_status() === 'on-hold' ) {
458 return;
459 }
460 $order->update_status( 'on-hold', __( 'Payment not processed in Exact Online', 'commercebird' ) );
461 $order->save();
462 }
463 }
464
465 /**
466 * Check if the image exists in the media library.
467 * @param string $picture_name
468 * @return int|false Attachment ID if exists, false otherwise
469 */
470 private static function get_existing_image_id( $picture_name ) {
471 global $wpdb;
472
473 // Strip extension from picture_name
474 $picture_title = pathinfo( $picture_name, PATHINFO_FILENAME );
475
476 // Prepare a LIKE match to catch sanitized versions
477 $like = '%' . $wpdb->esc_like( $picture_title ) . '%';
478
479 $attachment_id = $wpdb->get_var(
480 $wpdb->prepare(
481 "
482 SELECT ID FROM {$wpdb->posts}
483 WHERE post_type = 'attachment'
484 AND post_title LIKE %s
485 ORDER BY ID DESC
486 LIMIT 1
487 ",
488 $like
489 )
490 );
491
492 return $attachment_id ? (int) $attachment_id : false;
493 }
494
495 /**
496 * Upload the product image from Exact Online.
497 * @param string $product_id
498 * @param string $picture_name
499 * @return $attachment_id of the uploaded image if successful, otherwise false
500 */
501 private static function upload_image( $picture_url, $picture_name ) {
502 require_once ABSPATH . 'wp-admin/includes/media.php';
503 require_once ABSPATH . 'wp-admin/includes/file.php';
504 require_once ABSPATH . 'wp-admin/includes/image.php';
505
506 // Fetch image content
507 $response = wp_safe_remote_get( $picture_url, array( 'timeout' => 10 ) );
508
509 if ( is_wp_error( $response ) ) {
510 error_log( 'Error fetching image: ' . $response->get_error_message() );
511 return false;
512 }
513
514 $image_data = wp_remote_retrieve_body( $response );
515 if ( empty( $image_data ) ) {
516 return false;
517 }
518
519 // Generate filename
520 $upload_dir = wp_upload_dir();
521 $filename = sanitize_file_name( $picture_name );
522 $file_path = $upload_dir['path'] . '/' . $filename;
523
524 // Save the file
525 file_put_contents( $file_path, $image_data );
526
527 // Check if file was saved correctly
528 if ( ! file_exists( $file_path ) ) {
529 return false;
530 }
531
532 // Prepare file array for WordPress
533 $file = array(
534 'name' => $filename,
535 'type' => mime_content_type( $file_path ),
536 'tmp_name' => $file_path,
537 'size' => filesize( $file_path ),
538 );
539
540 // Upload to WordPress Media Library
541 $attachment_id = media_handle_sideload( $file, 0 );
542
543 // Check for errors
544 if ( is_wp_error( $attachment_id ) ) {
545 error_log( 'Error attaching image: ' . $attachment_id->get_error_message() );
546 return false;
547 }
548
549 return $attachment_id;
550 }
551 }
552