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