PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.3.8
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.3.8
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 / Ajax / ExactOnlineAjax.php
commercebird / admin / includes / Actions / Ajax Last commit date
AcfAjax.php 1 year ago ExactOnlineAjax.php 1 year ago ZohoCRMAjax.php 1 year ago ZohoInventoryAjax.php 1 year ago index.php 1 year ago
ExactOnlineAjax.php
625 lines
1 <?php
2
3 namespace CommerceBird\Admin\Actions\Ajax;
4
5 use CommerceBird\Admin\Actions\Sync\ExactOnlineSync;
6 use CommerceBird\Admin\Connectors\CommerceBird;
7 use CommerceBird\Admin\Traits\AjaxRequest;
8 use CommerceBird\Admin\Traits\LogWriter;
9 use CommerceBird\Admin\Traits\OptionStatus;
10 use CommerceBird\Admin\Traits\Singleton;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit;
14 }
15 final class ExactOnlineAjax {
16 use Singleton;
17 use LogWriter;
18 use AjaxRequest;
19 use OptionStatus;
20
21 private const FORMS = array(
22 'connect' => array(
23 'token',
24 ),
25 'product' => array(
26 'importProducts',
27 ),
28 'order' => array( 'range' ),
29 'customer' => array(
30 'importCustomers',
31 ),
32 'webhooks' => array(
33 'enable_StockPosition',
34 'enable_Item',
35 ),
36
37 );
38 private const ACTIONS = array(
39 'save_sync_order_via_cron' => 'sync_order',
40 'save_exact_online_connect' => 'connect_save',
41 'get_exact_online_connect' => 'connect_load',
42 'save_exact_online_cost_center' => 'cost_center_save',
43 'save_exact_online_cost_unit' => 'cost_unit_save',
44 'save_exact_online_gl_account' => 'gl_account_save',
45 'save_exact_online_payment_status' => 'get_payment_save',
46 'map_exact_online_product' => 'product_map',
47 'map_exact_online_customer' => 'customer_map',
48 'map_exact_online_order' => 'order_map',
49 'export_exact_online_order' => 'order_export',
50 'get_exact_online_webhooks' => 'webhooks_get',
51 'save_exact_online_webhooks' => 'webhooks_save',
52 'reset_exact_online_connect' => 'reset_mapping',
53 );
54 private const OPTIONS = array(
55 'connect' => array(
56 'token' => 'commercebird-exact-online-token',
57 ),
58 'cost_center' => 'commercebird-exact-online-cost-center',
59 'cost_unit' => 'commercebird-exact-online-cost-unit',
60 'gl_accounts' => 'commercebird-exact-online-gl-accounts',
61 );
62
63 private const SOURCE = 'exact';
64
65 public function __construct() {
66 $this->load_actions();
67 add_action( 'cmbird_exact_online_sync_orders', array( $this, 'sync_via_cron' ) );
68 }
69
70 /**
71 * Sync orders from Exact Online.
72 *
73 * @return void
74 */
75 public function sync_order(): void {
76 $this->verify( array( 'sync' ) );
77 if ( $this->data['sync'] ) {
78 if ( ! wp_next_scheduled( 'cmbird_exact_online_sync_orders' ) ) {
79 wp_schedule_event( time(), 'daily', 'cmbird_exact_online_sync_orders' );
80 }
81 } else {
82 wp_clear_scheduled_hook( 'cmbird_exact_online_sync_orders' );
83 }
84 update_option( 'cmbird_exact_online_sync_orders', (bool) $this->data['sync'] );
85 $this->response = array(
86 'success' => true,
87 'message' => __( 'Synced', 'commercebird' ),
88 );
89 $this->serve();
90 }
91
92 public function get_payment_save(): void {
93 $this->verify();
94 $this->get_payment_status();
95 }
96
97 /**
98 * Get Payment status from Exact Online. This is used to update the payment status of the order.
99 *
100 * @return void
101 */
102 public function get_payment_status(): void {
103 // $fd = fopen( __DIR__ . '/get_payment_status.log', 'a+' );
104
105 $start_date = gmdate( 'Y-m-d H:i:s', strtotime( '-200 days' ) );
106 $end_date = gmdate( 'Y-m-d H:i:s', strtotime( '-5 days' ) );
107
108 // Get all orders with the included statuses
109 $orders = wc_get_orders(
110 array(
111 'status' => 'wc-on-hold',
112 'limit' => -1,
113 'date_created' => $start_date . '...' . $end_date,
114 'return' => 'ids',
115 )
116 );
117 if ( empty( $orders ) ) {
118 $this->response = array(
119 'success' => false,
120 'message' => __( 'No Invoice orders found', 'commercebird' ),
121 );
122 $this->serve();
123 }
124 foreach ( $orders as $order_id ) {
125 // send each order_id to wc action scheduler to process if not scheduled
126 if ( ! as_has_scheduled_action( 'cmbird_payment_status', array( $order_id ) ) ) {
127 as_schedule_single_action(
128 time(),
129 'cmbird_payment_status',
130 array(
131 $order_id,
132 )
133 );
134 } else {
135 continue;
136 }
137 }
138 // fclose( $fd );
139
140 // Schedule the event to run weekly starting next week
141 if ( ! wp_next_scheduled( 'cmbird_eo_get_payment_statuses' ) ) {
142 $seven_days_in_future = time() + 7 * DAY_IN_SECONDS;
143 wp_schedule_event( $seven_days_in_future, 'weekly', 'cmbird_eo_get_payment_statuses' );
144 }
145 $this->response = array(
146 'success' => true,
147 'message' => __( 'Payment status updated', 'commercebird' ),
148 );
149 $this->serve();
150 }
151
152 private function process_orders( $range ): bool {
153 $result = 0;
154 $orders = ( new CommerceBird() )->order( $range );
155 if ( is_string( $orders ) ) {
156 $this->response = array(
157 'success' => false,
158 'message' => $orders,
159 );
160 $this->serve();
161 }
162 $chunked = array_chunk( $orders['orders'], 20 );
163 foreach ( $chunked as $chunked_order ) {
164 $result = as_schedule_single_action(
165 time(),
166 'cmbird_sync_eo',
167 array(
168 'orders',
169 wp_json_encode( $chunked_order ),
170 false,
171 )
172 );
173 if ( empty( $result ) ) {
174 break;
175 }
176 }
177 return $result > 0;
178 }
179
180 public function order_map() {
181 $this->verify( self::FORMS['order'] );
182 if ( empty( $this->data ) || empty( $this->data['range'] ) ) {
183 $this->response['success'] = false;
184 $this->response['message'] = __( 'Select dates', 'commercebird' );
185 $this->serve();
186 }
187 $result = $this->process_orders(
188 array(
189 'start_date' => gmdate(
190 'Y-m-d\TH:i:s.000\Z',
191 strtotime( $this->data['range'][0] )
192 ),
193 'end_date' => gmdate(
194 'Y-m-d\TH:i:s.000\Z',
195 strtotime( $this->data['range'][1] )
196 ),
197 ),
198 );
199 $this->response['success'] = $result > 0;
200 $this->response['data'] = $this->data['range'];
201 $this->response['message'] = __( 'Mapped', 'commercebird' );
202 $this->serve();
203 }
204
205 public function export_order( $start_date_raw, $end_date_raw ) {
206 // $fd = fopen( __DIR__ . '/export_order.log', 'a+' );
207
208 $start_date = gmdate( 'Y-m-d H:i:s', $start_date_raw );
209 $end_date = gmdate( 'Y-m-d H:i:s', $end_date_raw );
210 // Define the order statuses to exclude
211 $exclude_statuses = array( 'wc-failed', 'wc-pending', 'wc-on-hold', 'wc-cancelled' );
212 $posts_per_page = 20;
213 $paged = 1;
214
215 do {
216 // Query to get orders
217 $args = array(
218 'date_created' => $start_date . '...' . $end_date,
219 'status' => array_diff( array_keys( wc_get_order_statuses() ), $exclude_statuses ),
220 'limit' => $posts_per_page,
221 'paged' => $paged,
222 'orderby' => 'date',
223 'order' => 'ASC',
224 'return' => 'ids',
225 );
226 $orders = wc_get_orders( $args );
227
228 // Loop through orders and add customer note
229 foreach ( $orders as $order_id ) {
230 $order = wc_get_order( $order_id );
231 $order->set_status( $order->get_status() );
232 $order->save();
233 }
234
235 // Increment the offset for the next batch
236 ++$paged;
237 } while ( ! empty( $orders ) );
238 // fclose( $fd );
239 }
240
241 public function order_export() {
242 $this->verify( self::FORMS['order'] );
243 if ( empty( $this->data ) || empty( $this->data['range'] ) ) {
244 $this->response['success'] = false;
245 $this->response['message'] = __( 'Select dates', 'commercebird' );
246 $this->serve();
247 }
248 // Set the date range to last 30 days
249 $start_date = strtotime( $this->data['range'][0] );
250 $end_date = strtotime( $this->data['range'][1] );
251 $this->export_order( $start_date, $end_date );
252 $this->response['success'] = true;
253 $this->response['message'] = __( 'Exported', 'commercebird' );
254 $this->serve();
255 }
256
257 public function product_map() {
258 $this->verify( self::FORMS['product'] );
259 $products = ( new CommerceBird() )->products();
260 if ( is_string( $products ) ) {
261 $this->response = array(
262 'success' => false,
263 'message' => $products,
264 );
265 $this->serve();
266 }
267 $chunked = array_chunk( $products['items'], 100 );
268 foreach ( $chunked as $index => $chunked_products ) {
269 $transient_key = 'cmbird_product_chunk_' . time() . '_' . $index;
270 set_transient( $transient_key, $chunked_products, HOUR_IN_SECONDS );
271 // Wrap the arguments in an array
272 as_schedule_single_action(
273 time(),
274 'cmbird_process_product_chunk',
275 array(
276 array(
277 'transient_key' => $transient_key,
278 'import_products' => (bool) $this->data['importProducts'],
279 ),
280 )
281 );
282 }
283 // handle more pages if response contains next_page_url
284 if ( ! empty( $products['next_page_url'] ) ) {
285 $next_page = $products['next_page_url'];
286 $this->handle_more_pages( $next_page, 'product', 'customs/exact/bulk-items' );
287 }
288 $this->response['message'] = __( 'Items are being mapped in the background. You can visit other tabs :).', 'commercebird' );
289 $this->serve();
290 }
291
292 private function handle_more_pages( $next_page, $type, $endpoint, $params = array() ) {
293 // Add the pagination param
294 $params['__next'] = $next_page;
295
296 $response = ( new CommerceBird() )->request( $endpoint, 'GET', array(), $params );
297
298 if ( is_string( $response ) || $response['code'] !== 200 ) {
299 $this->response = array(
300 'success' => false,
301 'message' => is_string( $response ) ? $response : $response['message'],
302 );
303 $this->serve();
304 }
305
306 $result = $response['data'];
307 $next_page = $result['next_page_url'] ?? null;
308
309 switch ( $type ) {
310 case 'product':
311 if ( empty( $result['items'] ) ) {
312 $this->response = array(
313 'success' => false,
314 'message' => __( 'No more products found', 'commercebird' ),
315 );
316 $this->serve();
317 }
318
319 $chunked = array_chunk( $result['items'], 100 );
320 foreach ( $chunked as $index => $chunked_products ) {
321 // sleep for 0.1 seconds to avoid hitting the API limit
322 usleep( 100000 );
323 $transient_key = 'cmbird_product_chunk_' . time() . '_' . $index;
324 set_transient( $transient_key, $chunked_products, HOUR_IN_SECONDS );
325
326 as_schedule_single_action(
327 time() + 10,
328 'cmbird_process_product_chunk',
329 array(
330 array(
331 'transient_key' => $transient_key,
332 'import_products' => (bool) $this->data['importProducts'],
333 ),
334 )
335 );
336 }
337 break;
338
339 case 'customer':
340 if ( empty( $result['customers'] ) ) {
341 $this->response = array(
342 'success' => false,
343 'message' => __( 'No more customers found', 'commercebird' ),
344 );
345 $this->serve();
346 }
347
348 $chunked = array_chunk( $result['customers'], 100 );
349 foreach ( $chunked as $index => $chunked_customers ) {
350 // sleep for 0.1 seconds to avoid hitting the API limit
351 usleep( microseconds: 100000 );
352 $transient_key = 'cmbird_customer_chunk_' . time() . '_' . $index;
353 set_transient( $transient_key, $chunked_customers, HOUR_IN_SECONDS );
354
355 as_schedule_single_action(
356 time() + 10,
357 'cmbird_process_customer_chunk',
358 array(
359 array(
360 'transient_key' => $transient_key,
361 'import_customers' => (bool) $this->data['importCustomers'],
362 ),
363 )
364 );
365 }
366 break;
367
368 case 'order':
369 $sync = new ExactOnlineSync();
370 $sync->sync( 'order', $result['orders'], (bool) $this->data['importOrders'] );
371 break;
372 }
373
374 // 🔁 Recurse if there is a next page
375 if ( $next_page ) {
376 $this->handle_more_pages( $next_page, $type, $endpoint, $params );
377 }
378 }
379
380 public function customer_map() {
381 $this->verify( self::FORMS['customer'] );
382 $response = ( new CommerceBird() )->customer();
383 if ( is_string( $response ) ) {
384 $this->response = array(
385 'success' => false,
386 'message' => $response,
387 );
388 $this->serve();
389 }
390 $chunked = array_chunk( $response['customers'], 100 );
391 foreach ( $chunked as $index => $chunked_customers ) {
392 $transient_key = 'cmbird_customer_chunk_' . time() . '_' . $index;
393 set_transient( $transient_key, $chunked_customers, HOUR_IN_SECONDS );
394 // Wrap the arguments in an array
395 as_schedule_single_action(
396 time(),
397 'cmbird_process_customer_chunk',
398 array(
399 array(
400 'transient_key' => $transient_key,
401 'import_customers' => (bool) $this->data['importCustomers'],
402 ),
403 )
404 );
405 }
406 // handle more pages if response meta contains next
407 if ( ! empty( $response['next_page_url'] ) ) {
408 $next_page = $response['next_page_url'];
409 $this->handle_more_pages( $next_page, 'customer', 'customs/exact/bulk-customers' );
410 }
411 $this->response['message'] = __( 'Items are being mapped in background. You can visit other tabs :).', 'commercebird' );
412 $this->serve();
413 }
414
415 public function reset_mapping() {
416 $this->verify();
417 // Check if resetAll is true (sent from Vue)
418 $reset_all = isset( $this->data['reset'] ) && $this->data['reset'] == 1;
419 if ( ! $reset_all ) {
420 delete_option( self::OPTIONS['connect']['token'] );
421 $this->response['message'] = __( 'Token removed', 'commercebird' );
422 $this->serve();
423 }
424 global $wpdb;
425 // Delete all transients related to products and customers
426 $transients = array( 'cmbird_product_chunk_', 'cmbird_customer_chunk_' );
427 foreach ( $transients as $transient ) {
428 $wpdb->query(
429 $wpdb->prepare(
430 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
431 $transient . '%'
432 )
433 );
434 }
435 // Delete all product and customer meta keys
436 $wpdb->query( "DELETE FROM {$wpdb->prefix}postmeta WHERE meta_key = 'eo_item_id'" );
437 $wpdb->query( "DELETE FROM {$wpdb->prefix}usermeta WHERE meta_key IN ('eo_account_id', 'eo_contact_id', 'eo_gl_account')" );
438 // return response.
439 $this->response['message'] = __( 'Mapping reset completed', 'commercebird' );
440 $this->serve();
441 }
442
443 public function cost_center_get() {
444 return get_option( self::OPTIONS['cost_center'], array() );
445 }
446
447 public function cost_unit_get() {
448 return get_option( self::OPTIONS['cost_unit'], array() );
449 }
450
451 public function gl_account_get() {
452 return get_option( self::OPTIONS['gl_accounts'], array() );
453 }
454
455 public function get_token() {
456 return get_option( self::OPTIONS['connect']['token'], '' );
457 }
458
459 public function cost_center_save() {
460 $this->verify();
461 $response = ( new CommerceBird() )->cost_centers();
462 if ( empty( $response ) ) {
463 $this->errors['message'] = __( 'Cost centers not found', 'commercebird' );
464 } else {
465 $centers = array_map(
466 function ( $item ) {
467 return "{$item['Code']}-{$item['Description']}";
468 },
469 $response['data']
470 );
471 update_option( self::OPTIONS['cost_center'], $centers );
472 // remove each meta if it does not exists in the cost centers.
473 global $wpdb;
474 if ( ! empty( $centers ) ) {
475 // Sanitize and prepare the units for the SQL query
476 $placeholders = implode( ',', array_fill( 0, count( $centers ), '%s' ) );
477
478 // Prepare the query string
479 $query = "DELETE FROM {$wpdb->prefix}wc_orders_meta
480 WHERE meta_key = 'costcenter'
481 AND meta_value NOT IN ($placeholders)";
482 $wpdb->query( $wpdb->prepare( $query, ...$centers ) ); // phpcs:ignore
483 // also remove from postmeta table.
484 $query2 = "DELETE FROM {$wpdb->prefix}postmeta
485 WHERE meta_key = 'costcenter'
486 AND meta_value NOT IN ($placeholders)";
487 $wpdb->query( $wpdb->prepare( $query2, ...$centers ) ); // phpcs:ignore
488 } else {
489 // If $units is empty, delete all meta keys with 'costunit'
490 $wpdb->query(
491 "DELETE FROM {$wpdb->prefix}wc_orders_meta
492 WHERE meta_key = 'costcenter'"
493 );
494 }
495 $this->response['message'] = __( 'Cost centers saved', 'commercebird' );
496 }
497 $this->serve();
498 }
499
500 public function cost_unit_save() {
501 $this->verify();
502 $response = ( new CommerceBird() )->cost_units();
503 if ( empty( $response ) ) {
504 $this->errors['message'] = __( 'Cost units not found', 'commercebird' );
505 } else {
506 $units = array_map(
507 function ( $item ) {
508 return "{$item['Code']}-{$item['Description']}";
509 },
510 $response['data']
511 );
512 update_option( self::OPTIONS['cost_unit'], $units );
513 // remove each meta if it does not exists in the units.
514 global $wpdb;
515 if ( ! empty( $units ) ) {
516 // Sanitize and prepare the units for the SQL query
517 $placeholders = implode( ',', array_fill( 0, count( $units ), '%s' ) );
518
519 // Prepare the query string
520 $query = "DELETE FROM {$wpdb->prefix}wc_orders_meta
521 WHERE meta_key = 'costunit'
522 AND meta_value NOT IN ($placeholders)";
523 $wpdb->query( $wpdb->prepare( $query, ...$units ) ); // phpcs:ignore
524 // also remove from postmeta table
525 $query2 = "DELETE FROM {$wpdb->prefix}postmeta
526 WHERE meta_key = 'costunit'
527 AND meta_value NOT IN ($placeholders)";
528 $wpdb->query( $wpdb->prepare( $query2, ...$units ) ); // phpcs:ignore
529 } else {
530 // If $units is empty, delete all meta keys with 'costunit'
531 $wpdb->query(
532 "DELETE FROM {$wpdb->prefix}wc_orders_meta
533 WHERE meta_key = 'costunit'"
534 );
535 }
536 $this->response['message'] = __( 'Cost units saved', 'commercebird' );
537 }
538 $this->serve();
539 }
540
541 public function gl_account_save() {
542 $this->verify();
543 $response = ( new CommerceBird() )->gl_accounts();
544 if ( empty( $response ) ) {
545 $this->errors['message'] = __( 'GL accounts not found', 'commercebird' );
546 } else {
547 $accounts = array_map(
548 function ( $item ) {
549 return "{$item['ID']} : {$item['Description']}";
550 },
551 $response['data']
552 );
553 update_option( self::OPTIONS['gl_accounts'], $accounts );
554 $this->response['message'] = __( 'GL accounts saved', 'commercebird' );
555 }
556 $this->serve();
557 }
558
559 public function sync_via_cron() {
560 $start_date = strtotime( '-1 day' );
561 $end_date = strtotime( 'now' );
562
563 $continue = $this->process_orders(
564 array(
565 'start_date' => $start_date,
566 'end_date' => $end_date,
567 )
568 );
569
570 if ( $continue ) {
571 $this->export_order( $start_date, $end_date );
572 }
573 }
574
575 public function connect_save() {
576 $this->verify( self::FORMS['connect'] );
577 if ( isset( $this->data['token'] ) && ! empty( $this->data['token'] ) ) {
578 update_option( self::OPTIONS['connect']['token'], $this->data['token'] );
579 $this->response['message'] = __( 'Saved', 'commercebird' );
580 $this->response['data'] = $this->data;
581 } else {
582 $this->errors['message'] = __( 'Token is required', 'commercebird' );
583 }
584
585 $this->serve();
586 }
587
588 public function connect_load() {
589 $this->verify();
590 $this->response['token'] = get_option( self::OPTIONS['connect']['token'], '' );
591 $this->serve();
592 }
593 /**
594 * Sets the webhooks settings and updates the status.
595 *
596 * @return void
597 */
598 public function webhooks_save(): void {
599 $this->verify( self::FORMS['webhooks'] );
600 $form_data = $this->data;
601 foreach ( $form_data as $topic => $is_active ) {
602 $webhooks[] = array(
603 'topic' => $topic,
604 'status' => $is_active ? 'active' : 'inactive',
605 );
606 }
607 $response = ( new CommerceBird() )->subscribe_exact_webhooks( $webhooks );
608 // error_log( print_r( $response, true ) );
609 $this->option_status_update( $this->data );
610 $this->response = array( 'message' => 'Saved!' );
611 $this->serve();
612 }
613
614 /**
615 * Retrieves the wehbhook settings.
616 *
617 * @return void
618 */
619 public function webhooks_get(): void {
620 $this->verify();
621 $this->response = $this->option_status_get( self::FORMS['webhooks'] );
622 $this->serve();
623 }
624 }
625