PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.6.3
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.6.3
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 8 months ago ExactOnlineAjax.php 8 months ago SettingsAjax.php 8 months ago ZohoCRMAjax.php 8 months ago ZohoInventoryAjax.php 8 months ago index.php 1 year ago
ExactOnlineAjax.php
1212 lines
1 <?php
2
3 namespace CommerceBird\Admin\Actions\Ajax;
4
5 use CommerceBird\Admin\Actions\Sync\ExactOnlineSync;
6 use CommerceBird\Admin\Actions\Ajax\SettingsAjax;
7 use CommerceBird\Admin\Connectors\Connector;
8 use CommerceBird\Admin\Traits\AjaxRequest;
9 use CommerceBird\Admin\Traits\LogWriter;
10 use CommerceBird\Admin\Traits\OptionStatus;
11 use CommerceBird\Admin\Traits\Singleton;
12 use WC_Webhook;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17 final class ExactOnlineAjax {
18 use Singleton;
19 use LogWriter;
20 use AjaxRequest;
21 use OptionStatus;
22
23 private const FORMS = array(
24 'product' => array(
25 'importProducts',
26 ),
27 'order' => array( 'range' ),
28 'customer' => array(
29 'importCustomers',
30 ),
31 'webhooks' => array(
32 'enable_StockPosition',
33 'enable_Item',
34 ),
35 'sync_settings' => array(
36 'allowedInvoicesStores',
37 'allowedOrdersStores',
38 'allowedMandateStores',
39 'errors_notifications_email',
40 ),
41 'general_settings' => array(
42 'divisionId',
43 'cost_center',
44 'cost_unit',
45 'email_layout',
46 'invoice_style',
47 'journal',
48 'products_gla',
49 'send_invoice',
50 'shipping_cost_label',
51 'shipping_gla',
52 'warehouse',
53 ),
54 'tax_mapping' => array( 'taxMapping' ),
55 'payment_status_mapping' => array(
56 'paymentStatusMapping',
57 ),
58 );
59 private const ACTIONS = array(
60 'save_woo_webhooks' => 'create_woo_webhooks',
61 'save_sync_order_via_cron' => 'sync_order',
62 'save_exact_online_connect' => 'connect_exact_online',
63 'get_exact_online_connect' => 'get_exact_connection',
64 'save_exact_online_cost_center' => 'cost_center_save',
65 'save_exact_online_cost_unit' => 'cost_unit_save',
66 'save_exact_online_gl_account' => 'gl_account_save',
67 'save_exact_online_payment_status' => 'get_payment_save',
68 'map_exact_online_product' => 'product_map',
69 'get_exact_orphaned_products' => 'get_orphaned_products',
70 'delete_exact_orphaned_products' => 'delete_orphaned_products',
71 'map_exact_online_customer' => 'customer_map',
72 'map_exact_online_order' => 'order_map',
73 'export_exact_online_order' => 'order_export',
74 'get_exact_online_webhooks' => 'webhooks_get',
75 'save_exact_online_webhooks' => 'webhooks_save',
76 'reset_exact_online_connect' => 'reset_mapping',
77 'get_exact_online_sync_settings' => 'get_exact_sync_settings',
78 'save_exact_online_sync_settings' => 'save_exact_sync_settings',
79 'get_exact_online_general_settings' => 'get_exact_general_settings',
80 'save_exact_online_general_settings' => 'save_exact_general_settings',
81 'get_exact_online_cost_center' => 'cost_center_get',
82 'get_exact_online_cost_unit' => 'cost_unit_get',
83 'get_exact_online_gl_accounts' => 'gl_account_get',
84 'get_exact_online_journals' => 'journals_get',
85 'get_exact_online_warehouses' => 'warehouses_get',
86 'get_exact_online_invoice_layouts' => 'invoice_layouts_get',
87 'get_exact_online_tax_mapping' => 'get_exact_tax_mapping',
88 'save_exact_online_tax_mapping' => 'save_exact_tax_mapping',
89 'get_exact_online_vat_codes' => 'get_exact_vat_codes',
90 'get_exact_online_payment_status_mapping' => 'get_exact_payment_status_mapping',
91 'save_exact_online_payment_status_mapping' => 'save_exact_payment_status_mapping',
92 'get_exact_online_payment_conditions' => 'get_exact_payment_conditions',
93 'get_wc_payment_methods' => 'wc_payment_gateways',
94 );
95 private const OPTIONS = array(
96 'connect' => array(
97 'exact_domain' => 'commercebird-exact-online-domain',
98 'woo_webhook_status' => 'commercebird-woo-webhook-status',
99 ),
100 'cost_center' => 'commercebird-exact-online-cost-center',
101 'cost_unit' => 'commercebird-exact-online-cost-unit',
102 'gl_accounts' => 'commercebird-exact-online-gl-accounts',
103 );
104
105 private const SOURCE = 'exact';
106
107 public function __construct() {
108 $this->load_actions();
109 add_action( 'cmbird_exact_online_sync_orders', array( $this, 'sync_via_cron' ) );
110 add_action( 'cmbird_delete_orphaned_products', array( $this, 'delete_products_batch' ) );
111 }
112 /**
113 * Load actions for the class.
114 *
115 * @return void
116 */
117 public function create_woo_webhooks() {
118 $this->verify( array( 'token', 'override' ) );
119 if ( ! isset( $this->data['token'] ) || empty( $this->data['token'] ) ) {
120 $this->errors['message'] = __( 'Token is required', 'commercebird' );
121 }
122
123 // Check if webhook status has value in option then don't create webhook and response with status.
124 $status = get_option( self::OPTIONS['connect']['woo_webhook_status'] );
125 if ( empty( $this->data['override'] ) && isset( $status ) && ! empty( $status ) ) {
126 $this->response['message'] = esc_html( $status );
127 } else {
128 $response = ( new Connector() )->create_woo_webbhook();
129 if ( empty( $response ) ) {
130 $this->errors['message'] = __( 'Unable to connect', 'commercebird' );
131 } elseif ( 200 === $response['code'] ) {
132 // Update the option with the webhook status.
133 update_option( self::OPTIONS['connect']['woo_webhook_status'], $response['message'] );
134 $this->response['message'] = __( 'Webhook created', 'commercebird' );
135 } else {
136 $this->errors['message'] = $response['message'];
137 }
138 }
139 $this->serve();
140 }
141
142
143 /**
144 * Sync orders from Exact Online.
145 *
146 * @return void
147 */
148 public function sync_order(): void {
149 $this->verify( array( 'sync' ) );
150 if ( $this->data['sync'] ) {
151 if ( ! wp_next_scheduled( 'cmbird_exact_online_sync_orders' ) ) {
152 wp_schedule_event( time(), 'hourly', 'cmbird_exact_online_sync_orders' );
153 }
154 } else {
155 wp_clear_scheduled_hook( 'cmbird_exact_online_sync_orders' );
156 }
157 update_option( 'cmbird_exact_online_sync_orders', (bool) $this->data['sync'] );
158 $this->response = array(
159 'success' => true,
160 'message' => __( 'Synced', 'commercebird' ),
161 );
162 $this->serve();
163 }
164
165 public function get_payment_save(): void {
166 $this->verify();
167 $this->get_payment_status();
168 }
169
170 /**
171 * Get Payment status from Exact Online. This is used to update the payment status of the order.
172 *
173 * @return void
174 */
175 public function get_payment_status(): void {
176 // $fd = fopen( __DIR__ . '/get_payment_status.log', 'a+' );
177
178 $start_date = gmdate( 'Y-m-d H:i:s', strtotime( '-200 days' ) );
179 $end_date = gmdate( 'Y-m-d H:i:s', strtotime( '-5 days' ) );
180
181 // Get all orders with the included statuses.
182 $orders = wc_get_orders(
183 array(
184 'status' => 'wc-on-hold',
185 'limit' => -1,
186 'date_created' => $start_date . '...' . $end_date,
187 'return' => 'ids',
188 )
189 );
190 if ( empty( $orders ) ) {
191 $this->response = array(
192 'success' => false,
193 'message' => __( 'No Invoice orders found', 'commercebird' ),
194 );
195 $this->serve();
196 }
197 foreach ( $orders as $order_id ) {
198 // send each order_id to wc action scheduler to process if not scheduled.
199 if ( ! as_has_scheduled_action( 'cmbird_payment_status', array( $order_id ) ) ) {
200 as_schedule_single_action(
201 time(),
202 'cmbird_payment_status',
203 array(
204 $order_id,
205 )
206 );
207 } else {
208 continue;
209 }
210 }
211 // fclose( $fd );
212
213 // Schedule the event to run weekly starting next week.
214 if ( ! wp_next_scheduled( 'cmbird_eo_get_payment_statuses' ) ) {
215 $seven_days_in_future = time() + 7 * DAY_IN_SECONDS;
216 wp_schedule_event( $seven_days_in_future, 'weekly', 'cmbird_eo_get_payment_statuses' );
217 }
218 $this->response = array(
219 'success' => true,
220 'message' => __( 'Payment status updated', 'commercebird' ),
221 );
222 $this->serve();
223 }
224
225 private function process_orders( $range ): bool {
226 $result = 0;
227 $orders = ( new Connector() )->order( $range );
228 if ( is_string( $orders ) ) {
229 $this->response = array(
230 'success' => false,
231 'message' => $orders,
232 );
233 $this->serve();
234 }
235 $chunked = array_chunk( $orders['orders'], 20 );
236 foreach ( $chunked as $chunked_order ) {
237 $result = as_schedule_single_action(
238 time(),
239 'cmbird_sync_eo',
240 array(
241 'orders',
242 wp_json_encode( $chunked_order ),
243 false,
244 )
245 );
246 if ( empty( $result ) ) {
247 break;
248 }
249 }
250 return $result > 0;
251 }
252
253 public function order_map() {
254 $this->verify( self::FORMS['order'] );
255 if ( empty( $this->data ) || empty( $this->data['range'] ) ) {
256 $this->response['success'] = false;
257 $this->response['message'] = __( 'Select dates', 'commercebird' );
258 $this->serve();
259 }
260 $result = $this->process_orders(
261 array(
262 'start_date' => gmdate(
263 'Y-m-d\TH:i:s.000\Z',
264 strtotime( $this->data['range'][0] )
265 ),
266 'end_date' => gmdate(
267 'Y-m-d\TH:i:s.000\Z',
268 strtotime( $this->data['range'][1] )
269 ),
270 ),
271 );
272 $this->response['success'] = $result > 0;
273 $this->response['data'] = $this->data['range'];
274 $this->response['message'] = __( 'Mapped', 'commercebird' );
275 $this->serve();
276 }
277
278 public function export_order( $start_date_raw, $end_date_raw ) {
279 // $fd = fopen( __DIR__ . '/export_order.log', 'a+' );
280
281 $start_date = gmdate( 'Y-m-d H:i:s', $start_date_raw );
282 $end_date = gmdate( 'Y-m-d H:i:s', $end_date_raw );
283 // Define the order statuses to exclude.
284 $exclude_statuses = array( 'wc-failed', 'wc-pending', 'wc-on-hold', 'wc-cancelled' );
285 $posts_per_page = 20;
286 $paged = 1;
287
288 do {
289 // Query to get orders.
290 $args = array(
291 'date_created' => $start_date . '...' . $end_date,
292 'status' => array_diff( array_keys( wc_get_order_statuses() ), $exclude_statuses ),
293 'limit' => $posts_per_page,
294 'paged' => $paged,
295 'orderby' => 'date',
296 'order' => 'ASC',
297 'return' => 'ids',
298 );
299 $orders = wc_get_orders( $args );
300
301 // Loop through orders and add customer note.
302 foreach ( $orders as $order_id ) {
303 $order = wc_get_order( $order_id );
304 $order->set_status( $order->get_status() );
305 $order->save();
306 }
307
308 // Increment the offset for the next batch.
309 ++$paged;
310 } while ( ! empty( $orders ) );
311 // fclose( $fd );
312 }
313
314 public function order_export() {
315 $this->verify( self::FORMS['order'] );
316 if ( empty( $this->data ) || empty( $this->data['range'] ) ) {
317 $this->response['success'] = false;
318 $this->response['message'] = __( 'Select dates', 'commercebird' );
319 $this->serve();
320 }
321 // Set the date range to last 30 days.
322 $start_date = strtotime( $this->data['range'][0] );
323 $end_date = strtotime( $this->data['range'][1] );
324 $this->export_order( $start_date, $end_date );
325 $this->response['success'] = true;
326 $this->response['message'] = __( 'Exported', 'commercebird' );
327 $this->serve();
328 }
329
330 public function product_map() {
331 $this->verify( self::FORMS['product'] );
332 $products = ( new Connector() )->products();
333 if ( is_string( $products ) ) {
334 $this->response = array(
335 'success' => false,
336 'message' => $products,
337 );
338 $this->serve();
339 }
340 $chunked = array_chunk( $products['items'], 100 );
341 foreach ( $chunked as $index => $chunked_products ) {
342 $transient_key = 'cmbird_product_chunk_' . ( time() + 1 ) . '_' . $index;
343 set_transient( $transient_key, $chunked_products, HOUR_IN_SECONDS );
344 // Wrap the arguments in an array.
345 as_schedule_single_action(
346 time(),
347 'cmbird_process_product_chunk',
348 array(
349 array(
350 'transient_key' => $transient_key,
351 'import_products' => (bool) $this->data['importProducts'],
352 ),
353 )
354 );
355 }
356 // handle more pages if response contains next_page_url.
357 if ( ! empty( $products['next_page_url'] ) ) {
358 $next_page = $products['next_page_url'];
359 $this->handle_more_pages( $next_page, 'product', 'customs/exact/bulk-items' );
360 }
361 $this->response['message'] = __( 'Items are being mapped in the background. You can visit other tabs :).', 'commercebird' );
362 $this->serve();
363 }
364
365 /**
366 * Handles pagination of API responses for products, customers, and orders.
367 *
368 * @param string $next_page The URL of the next page to fetch.
369 * @param string $type The type of data being paginated ('product', 'customer', or 'order').
370 * @param string $endpoint The API endpoint to request.
371 * @param array $params Optional query parameters for the API request.
372 *
373 * @return void
374 */
375 private function handle_more_pages( $next_page, $type, $endpoint, $params = array() ) {
376 // Add the pagination param.
377 $params['__next'] = $next_page;
378
379 $response = ( new Connector() )->request( $endpoint, 'GET', array(), $params );
380
381 if ( is_string( $response ) || $response['code'] !== 200 ) {
382 $this->response = array(
383 'success' => false,
384 'message' => is_string( $response ) ? $response : $response['message'],
385 );
386 $this->serve();
387 }
388
389 $result = $response['data'];
390 $next_page = $result['next_page_url'] ?? null;
391
392 switch ( $type ) {
393 case 'product':
394 if ( empty( $result['items'] ) ) {
395 $this->response = array(
396 'success' => false,
397 'message' => __( 'No more products found', 'commercebird' ),
398 );
399 $this->serve();
400 }
401
402 $chunked = array_chunk( $result['items'], 100 );
403 foreach ( $chunked as $index => $chunked_products ) {
404 // sleep for 0.1 seconds to avoid hitting the API limit.
405 usleep( 100000 );
406 $transient_key = 'cmbird_product_chunk_' . time() . '_' . $index;
407 set_transient( $transient_key, $chunked_products, HOUR_IN_SECONDS );
408
409 as_schedule_single_action(
410 time() + 10,
411 'cmbird_process_product_chunk',
412 array(
413 array(
414 'transient_key' => $transient_key,
415 'import_products' => (bool) $this->data['importProducts'],
416 ),
417 )
418 );
419 }
420 break;
421
422 case 'customer':
423 if ( empty( $result['customers'] ) ) {
424 $this->response = array(
425 'success' => false,
426 'message' => __( 'No more customers found', 'commercebird' ),
427 );
428 $this->serve();
429 }
430
431 $chunked = array_chunk( $result['customers'], 100 );
432 foreach ( $chunked as $index => $chunked_customers ) {
433 // sleep for 0.1 seconds to avoid hitting the API limit.
434 usleep( 100000 );
435 $transient_key = 'cmbird_customer_chunk_' . time() . '_' . $index;
436 set_transient( $transient_key, $chunked_customers, HOUR_IN_SECONDS );
437
438 as_schedule_single_action(
439 time() + 10,
440 'cmbird_process_customer_chunk',
441 array(
442 array(
443 'transient_key' => $transient_key,
444 'import_customers' => (bool) $this->data['importCustomers'],
445 ),
446 )
447 );
448 }
449 break;
450
451 case 'order':
452 $sync = new ExactOnlineSync();
453 $sync->sync( 'order', $result['orders'], (bool) $this->data['importOrders'] );
454 break;
455 }
456
457 // 🔁 Recurse if there is a next page.
458 if ( $next_page ) {
459 $this->handle_more_pages( $next_page, $type, $endpoint, $params );
460 }
461 }
462
463 /**
464 * Maps customers from Exact Online to WooCommerce.
465 *
466 * @return void
467 */
468 public function customer_map() {
469 $this->verify( self::FORMS['customer'] );
470 $response = ( new Connector() )->customer();
471 if ( is_string( $response ) ) {
472 $this->response = array(
473 'success' => false,
474 'message' => $response,
475 );
476 $this->serve();
477 }
478 if ( empty( $response['customers'] ) ) {
479 $this->response = array(
480 'success' => false,
481 'message' => __( 'No customers found', 'commercebird' ),
482 );
483 $this->serve();
484 }
485 $chunked = array_chunk( $response['customers'], 100 );
486 foreach ( $chunked as $index => $chunked_customers ) {
487 $transient_key = 'cmbird_customer_chunk_' . time() . '_' . $index;
488 set_transient( $transient_key, $chunked_customers, HOUR_IN_SECONDS );
489 // Wrap the arguments in an array.
490 as_schedule_single_action(
491 time(),
492 'cmbird_process_customer_chunk',
493 array(
494 array(
495 'transient_key' => $transient_key,
496 'import_customers' => (bool) $this->data['importCustomers'],
497 ),
498 )
499 );
500 }
501 // handle more pages if response meta contains next.
502 if ( ! empty( $response['next_page_url'] ) ) {
503 $next_page = $response['next_page_url'];
504 $this->handle_more_pages( $next_page, 'customer', 'customs/exact/bulk-customers' );
505 }
506 $this->response['message'] = __( 'Items are being mapped in background. You can visit other tabs :).', 'commercebird' );
507 $this->serve();
508 }
509
510 /**
511 * Resets all product and customer mappings from Exact Online.
512 *
513 * @return void
514 */
515 public function reset_mapping() {
516 $this->verify();
517 // Check if resetAll is true (sent from Vue).
518 // $reset_all = isset( $this->data['reset'] ) && $this->data['reset'] == 1;
519 // if ( ! $reset_all ) {.
520 // delete_option(self::OPTIONS['connect']['token']);
521 // $this->response['message'] = __( 'Token removed', 'commercebird' );
522 // $this->serve();
523 // }.
524 global $wpdb;
525 // Delete all transients related to products and customers.
526 $transients = array( 'cmbird_product_chunk_', 'cmbird_customer_chunk_' );
527 foreach ( $transients as $transient ) {
528 $wpdb->query(
529 $wpdb->prepare(
530 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
531 $transient . '%'
532 )
533 );
534 }
535 // Delete all product and customer meta keys.
536 $wpdb->query( "DELETE FROM {$wpdb->prefix}postmeta WHERE meta_key = 'eo_item_id'" );
537 $wpdb->query( "DELETE FROM {$wpdb->prefix}usermeta WHERE meta_key IN ('eo_account_id', 'eo_contact_id', 'eo_gl_account')" );
538 // return response.
539 $this->response['message'] = __( 'Mapping reset completed', 'commercebird' );
540 $this->serve();
541 }
542
543 public function cost_center_get_options() {
544 return get_option( self::OPTIONS['cost_center'], array() );
545 }
546
547 public function cost_unit_get_options() {
548 return get_option( self::OPTIONS['cost_unit'], array() );
549 }
550
551 public function gl_account_get_options() {
552 return get_option( self::OPTIONS['gl_accounts'], array() );
553 }
554
555
556 public function cost_center_save() {
557 $this->verify();
558 $response = ( new Connector() )->cost_centers();
559 if ( empty( $response ) ) {
560 $this->errors['message'] = __( 'Cost centers not found', 'commercebird' );
561 } else {
562 $centers = array_map(
563 function ( $item ) {
564 return "{$item['Code']}-{$item['Description']}";
565 },
566 $response['data']
567 );
568 update_option( self::OPTIONS['cost_center'], $centers );
569 // remove each meta if it does not exists in the cost centers.
570 global $wpdb;
571 if ( ! empty( $centers ) ) {
572 // Sanitize and prepare the units for the SQL query.
573 $placeholders = implode( ',', array_fill( 0, count( $centers ), '%s' ) );
574
575 // Prepare the query string.
576 $query = "DELETE FROM {$wpdb->prefix}wc_orders_meta
577 WHERE meta_key = 'costcenter'
578 AND meta_value NOT IN ($placeholders)";
579 $wpdb->query( $wpdb->prepare( $query, ...$centers ) ); // phpcs:ignore.
580 // also remove from postmeta table.
581 $query2 = "DELETE FROM {$wpdb->prefix}postmeta
582 WHERE meta_key = 'costcenter'
583 AND meta_value NOT IN ($placeholders)";
584 $wpdb->query( $wpdb->prepare( $query2, ...$centers ) ); // phpcs:ignore.
585 } else {
586 // If $units is empty, delete all meta keys with 'costunit'.
587 $wpdb->query(
588 "DELETE FROM {$wpdb->prefix}wc_orders_meta
589 WHERE meta_key = 'costcenter'"
590 );
591 }
592 $this->response['message'] = __( 'Cost centers saved', 'commercebird' );
593 }
594 $this->serve();
595 }
596
597 public function cost_unit_save() {
598 $this->verify();
599 $response = ( new Connector() )->cost_units();
600 if ( empty( $response ) ) {
601 $this->errors['message'] = __( 'Cost units not found', 'commercebird' );
602 } else {
603 $units = array_map(
604 function ( $item ) {
605 return "{$item['Code']}-{$item['Description']}";
606 },
607 $response['data']
608 );
609 update_option( self::OPTIONS['cost_unit'], $units );
610 // remove each meta if it does not exists in the units.
611 global $wpdb;
612 if ( ! empty( $units ) ) {
613 // Sanitize and prepare the units for the SQL query.
614 $placeholders = implode( ',', array_fill( 0, count( $units ), '%s' ) );
615
616 // Prepare the query string.
617 $query = "DELETE FROM {$wpdb->prefix}wc_orders_meta
618 WHERE meta_key = 'costunit'
619 AND meta_value NOT IN ($placeholders)";
620 $wpdb->query( $wpdb->prepare( $query, ...$units ) ); // phpcs:ignore.
621 // also remove from postmeta table.
622 $query2 = "DELETE FROM {$wpdb->prefix}postmeta
623 WHERE meta_key = 'costunit'
624 AND meta_value NOT IN ($placeholders)";
625 $wpdb->query( $wpdb->prepare( $query2, ...$units ) ); // phpcs:ignore.
626 } else {
627 // If $units is empty, delete all meta keys with 'costunit'.
628 $wpdb->query(
629 "DELETE FROM {$wpdb->prefix}wc_orders_meta
630 WHERE meta_key = 'costunit'"
631 );
632 }
633 $this->response['message'] = __( 'Cost units saved', 'commercebird' );
634 }
635 $this->serve();
636 }
637
638 public function gl_account_save() {
639 $this->verify();
640 $response = ( new Connector() )->gl_accounts();
641 if ( empty( $response ) ) {
642 $this->errors['message'] = __( 'GL accounts not found', 'commercebird' );
643 } else {
644 $accounts = array_map(
645 function ( $item ) {
646 return "{$item['ID']} : {$item['Description']}";
647 },
648 $response['data']
649 );
650 update_option( self::OPTIONS['gl_accounts'], $accounts );
651 $this->response['message'] = __( 'GL accounts saved', 'commercebird' );
652 }
653 $this->serve();
654 }
655
656 public function sync_via_cron() {
657 $start_date = strtotime( '-1 day' );
658 $end_date = strtotime( 'now' );
659
660 $continue = $this->process_orders(
661 array(
662 'start_date' => $start_date,
663 'end_date' => $end_date,
664 )
665 );
666
667 if ( $continue ) {
668 $this->export_order( $start_date, $end_date );
669 }
670 }
671
672 public function connect_exact_online() {
673 $this->verify( array( 'exact_domain' ) );
674 if ( ! isset( $this->data['exact_domain'] ) || empty( $this->data['exact_domain'] ) ) {
675 $this->errors['message'] = __( 'Please select exact domain', 'commercebird' );
676 } else {
677 update_option( self::OPTIONS['connect']['exact_domain'], $this->data['exact_domain'] );
678 $response = ( new Connector() )->connect_exact( $this->data['exact_domain'] );
679 if ( empty( $response ) ) {
680 $this->errors['message'] = __( 'Unable to connect', 'commercebird' );
681 } elseif ( 200 === $response['code'] ) {
682 $this->response['message'] = __( 'Account generared', 'commercebird' );
683 $this->response['auth_url'] = $response['data']['auth_url'];
684 $this->response['message'] = __( 'Saved', 'commercebird' );
685 } else {
686 $this->errors['message'] = $response['message'];
687 }
688 }
689
690 $this->serve();
691 }
692
693 public function get_exact_connection() {
694 $this->verify();
695 $exact_domain = get_option( self::OPTIONS['connect']['exact_domain'], '' );
696 $token = SettingsAjax::instance()->get_token();
697 if ( ! empty( $exact_domain ) && ! empty( $token ) ) {
698 $response = ( new Connector() )->get_exact_divisions();
699 if ( empty( $response ) ) {
700 $this->errors['message'] = __( 'Unable to connect', 'commercebird' );
701 } else {
702 $this->response['message'] = $response['message'];
703 if ( 200 === $response['code'] ) {
704 $this->response['connection'] = $response['data'];
705 }
706 }
707 $this->response['exact_domain'] = $exact_domain;
708 $this->response['token'] = $token;
709 }
710 $this->serve();
711 }
712
713
714 /**
715 * @description Retrieves the general account settings.
716 **/
717 public function get_exact_sync_settings() {
718 $this->verify();
719 $response = ( new Connector() )->get_account_settings();
720 if ( empty( $response ) ) {
721 $this->errors['message'] = __( 'Unable to connect', 'commercebird' );
722 } else {
723 $this->response['message'] = $response['message'];
724 if ( 200 === $response['code'] ) {
725 $this->response['settings'] = $response['data'];
726 }
727 }
728 $this->serve();
729 }
730
731 /**
732 * @description Function to save the general account settings.
733 */
734 public function save_exact_sync_settings() {
735 $this->verify( self::FORMS['sync_settings'] );
736
737 $settings = $this->data;
738
739 if ( empty( $settings ) ) {
740 $this->errors['message'] = __( 'Invalid sync settings payload.', 'commercebird' );
741 $this->serve();
742 }
743
744 $response = ( new Connector() )->save_account_settings( $settings );
745
746 if ( empty( $response ) ) {
747 $this->errors['message'] = __( 'Unable to save settings.', 'commercebird' );
748 } else {
749 $this->response['message'] = $response['message'];
750 if ( ! empty( $response['code'] ) && 200 === $response['code'] ) {
751 $this->response['success'] = true;
752 $this->response['settings'] = $response['data'] ?? array();
753 }
754 }
755
756 $this->serve();
757 }
758
759 /**
760 * @description Retrieves mapped general settings for exact.
761 **/
762 public function get_exact_general_settings() {
763 $this->verify();
764 $response = ( new Connector() )->exact_general_settings();
765 if ( empty( $response ) ) {
766 $this->errors['message'] = __( 'Unable to connect', 'commercebird' );
767 } else {
768 $this->response['message'] = $response['message'];
769 if ( $response['code'] === 200 ) {
770 $this->response['settings'] = $response['data'];
771 }
772 }
773 $this->serve();
774 }
775
776 /**
777 * @description Function to save the mapped general settings.
778 */
779 public function save_exact_general_settings() {
780 $this->verify( self::FORMS['general_settings'] );
781
782 $settings = $this->data;
783
784 if ( empty( $settings ) ) {
785 $this->errors['message'] = __( 'Invalid general settings payload.', 'commercebird' );
786 $this->serve();
787 }
788
789 $response = ( new Connector() )->save_exact_general_settings( $settings );
790
791 if ( empty( $response ) ) {
792 $this->errors['message'] = __( 'Unable to save settings.', 'commercebird' );
793 } else {
794 $this->response['message'] = $response['message'];
795 if ( ! empty( $response['code'] ) && $response['code'] === 200 ) {
796 $this->response['success'] = true;
797 $this->response['settings'] = $response['data'] ?? array();
798 }
799 }
800
801 $this->serve();
802 }
803
804 /**
805 * @description Retrieves Journals list from exact.
806 **/
807 public function journals_get() {
808 $this->verify();
809 $response = ( new Connector() )->journals();
810 if ( empty( $response ) ) {
811 $this->errors['message'] = __( 'Unable to connect', 'commercebird' );
812 } else {
813 $this->response['message'] = $response['message'];
814 if ( $response['code'] === 200 ) {
815 $this->response['settings'] = $response['data'];
816 }
817 }
818 $this->serve();
819 }
820
821 /**
822 * @description Retrieves warehoused from exact.
823 **/
824 public function warehouses_get() {
825 $this->verify();
826 $response = ( new Connector() )->exact_warehouses();
827 if ( empty( $response ) ) {
828 $this->errors['message'] = __( 'Unable to connect', 'commercebird' );
829 } else {
830 $this->response['message'] = $response['message'];
831 if ( $response['code'] === 200 ) {
832 $this->response['settings'] = $response['data'];
833 }
834 }
835 $this->serve();
836 }
837
838 /**
839 * @description Retrieves invoice layouts from exact.
840 **/
841 public function invoice_layouts_get() {
842 $this->verify();
843 $response = ( new Connector() )->invoice_layouts();
844 if ( empty( $response ) ) {
845 $this->errors['message'] = __( 'Unable to connect', 'commercebird' );
846 } else {
847 $this->response['message'] = $response['message'];
848 if ( $response['code'] === 200 ) {
849 $this->response['settings'] = $response['data'];
850 }
851 }
852 $this->serve();
853 }
854
855 /**
856 * @description Retrieves invoice layouts from exact.
857 **/
858 public function gl_account_get() {
859 $this->verify();
860 $response = ( new Connector() )->gl_accounts();
861 if ( empty( $response ) ) {
862 $this->errors['message'] = __( 'Unable to connect', 'commercebird' );
863 } else {
864 $this->response['message'] = $response['message'];
865 if ( $response['code'] === 200 ) {
866 $this->response['settings'] = $response['data'];
867 }
868 }
869 $this->serve();
870 }
871
872 /**
873 * @description Retrieves cost centers for exact.
874 **/
875 public function cost_center_get() {
876 $this->verify();
877 $response = ( new Connector() )->cost_centers();
878 if ( empty( $response ) ) {
879 $this->errors['message'] = __( 'Unable to connect', 'commercebird' );
880 } else {
881 $this->response['message'] = $response['message'];
882 if ( $response['code'] === 200 ) {
883 $this->response['settings'] = $response['data'];
884 }
885 }
886 $this->serve();
887 }
888
889 /**
890 * @description Retrieves cost units for exact.
891 **/
892 public function cost_unit_get() {
893 $this->verify();
894 $response = ( new Connector() )->cost_units();
895 if ( empty( $response ) ) {
896 $this->errors['message'] = __( 'Unable to connect', 'commercebird' );
897 } else {
898 $this->response['message'] = $response['message'];
899 if ( $response['code'] === 200 ) {
900 $this->response['settings'] = $response['data'];
901 }
902 }
903 $this->serve();
904 }
905
906 /**
907 * Sets the webhooks settings and updates the status.
908 *
909 * @return void
910 */
911 public function webhooks_save(): void {
912 $this->verify( self::FORMS['webhooks'] );
913 $form_data = $this->data;
914 foreach ( $form_data as $topic => $is_active ) {
915 $webhooks[] = array(
916 'topic' => $topic,
917 'status' => $is_active ? 'active' : 'inactive',
918 );
919 }
920 $response = ( new Connector() )->subscribe_exact_webhooks( $webhooks );
921 // error_log( print_r( $response, true ) );
922 $this->option_status_update( $this->data );
923 $this->response = array( 'message' => 'Saved!' );
924 $this->serve();
925 }
926
927 /**
928 * Retrieves the wehbhook settings.
929 *
930 * @return void
931 */
932 public function webhooks_get(): void {
933 $this->verify();
934 $this->response = $this->option_status_get( self::FORMS['webhooks'] );
935 $this->serve();
936 }
937
938 /**
939 * @description Retrieves exact vat codes.
940 **/
941 public function get_exact_vat_codes() {
942 $this->verify();
943 $response = ( new Connector() )->get_exact_vat_codes();
944 if ( empty( $response ) ) {
945 $this->errors['message'] = __( 'Unable to connect', 'commercebird' );
946 } else {
947 $this->response['message'] = $response['message'];
948 if ( $response['code'] === 200 ) {
949 $this->response['settings'] = $response['data'];
950 }
951 }
952 $this->serve();
953 }
954
955 /**
956 * @description Retrieves mapped tax settings.
957 **/
958 public function get_exact_tax_mapping() {
959 $this->verify();
960 $response = ( new Connector() )->get_exact_tax_mapping();
961 if ( empty( $response ) ) {
962 $this->errors['message'] = __( 'Unable to connect', 'commercebird' );
963 } else {
964 $this->response['message'] = $response['message'];
965 if ( $response['code'] === 200 ) {
966 $this->response['taxMapping'] = $response['data'];
967 }
968 }
969 $this->serve();
970 }
971
972 /**
973 * @description Function to save the mapped taxes.
974 */
975 public function save_exact_tax_mapping() {
976 $this->verify( self::FORMS['tax_mapping'] );
977 $settings = $this->data['taxMapping'] ?? array();
978 if ( empty( $settings ) ) {
979 $this->errors['message'] = __( 'Invalid tax mapping payload.', 'commercebird' );
980 $this->serve();
981 }
982
983 $response = ( new Connector() )->save_exact_tax_mapping( $settings );
984
985 if ( empty( $response ) ) {
986 $this->errors['message'] = __( 'Unable to save tax mapping.', 'commercebird' );
987 } else {
988 $this->response['message'] = $response['message'];
989 if ( ! empty( $response['code'] ) && 200 === $response['code'] ) {
990 $this->response['success'] = true;
991 $this->response['settings'] = $response['data'] ?? array();
992 }
993 }
994
995 $this->serve();
996 }
997
998 /**
999 * @description Retrieves exact payment conditions.
1000 **/
1001 public function get_exact_payment_conditions() {
1002 $this->verify();
1003 $response = ( new Connector() )->get_exact_payment_conditions();
1004 if ( empty( $response ) ) {
1005 $this->errors['message'] = __( 'Unable to connect', 'commercebird' );
1006 } else {
1007 $this->response['message'] = $response['message'];
1008 if ( $response['code'] === 200 ) {
1009 $this->response['payment_conditions'] = $response['data'];
1010 }
1011 }
1012 $this->serve();
1013 }
1014 /**
1015 * Retrieves mapped payment status mapping.
1016 *
1017 * @return void
1018 */
1019 public function get_exact_payment_status_mapping() {
1020 $this->verify();
1021 $response = ( new Connector() )->get_exact_payment_status_mapping();
1022 if ( empty( $response ) ) {
1023 $this->errors['message'] = __( 'Unable to connect', 'commercebird' );
1024 } else {
1025 $this->response['message'] = $response['message'];
1026 if ( $response['code'] === 200 ) {
1027 $this->response['paymentStatusMapping'] = $response['data'];
1028 }
1029 }
1030 $this->serve();
1031 }
1032
1033 /**
1034 * @description Function to save the mapped taxes.
1035 */
1036 public function save_exact_payment_status_mapping() {
1037 $this->verify( self::FORMS['payment_status_mapping'] );
1038 $settings = $this->data['paymentStatusMapping'] ?? array();
1039 if ( empty( $settings ) ) {
1040 $this->errors['message'] = __( 'Invalid payment status mapping payload.', 'commercebird' );
1041 $this->serve();
1042 }
1043
1044 $response = ( new Connector() )->save_exact_payment_status_mapping( $settings );
1045
1046 if ( empty( $response ) ) {
1047 $this->errors['message'] = __( 'Unable to save payment status mapping.', 'commercebird' );
1048 } else {
1049 $this->response['message'] = $response['message'];
1050 if ( ! empty( $response['code'] ) && 200 === $response['code'] ) {
1051 $this->response['success'] = true;
1052 $this->response['paymentStatusMapping'] = $response['data'] ?? array();
1053 }
1054 }
1055
1056 $this->serve();
1057 }
1058
1059 /**
1060 * Retrieves available WooCommerce payment gateways.
1061 *
1062 * @return void
1063 */
1064 public function wc_payment_gateways() {
1065 $gateways = array();
1066 $available_gateways = WC()->payment_gateways->payment_gateways();
1067 // $available_gateways = $wc_gateways->get_available_payment_gateways();
1068 foreach ( $available_gateways as $gateway_id => $gateway ) {
1069 if ( 'yes' !== $gateway->enabled ) {
1070 continue; // skip disabled gateways.
1071 }
1072 $gateways[] = array(
1073 'id' => $gateway_id,
1074 'title' => $gateway->get_title(),
1075 'description' => $gateway->get_description(),
1076 'method_title' => $gateway->method_title,
1077 'enabled' => $gateway->enabled,
1078 'supports' => $gateway->supports,
1079 );
1080 }
1081 $this->response['success'] = true;
1082 $this->response['gateways'] = $gateways;
1083 $this->serve();
1084 }
1085
1086 /**
1087 * Get orphaned products (exist in WooCommerce but not in Exact Online).
1088 *
1089 * @return void
1090 */
1091 public function get_orphaned_products() {
1092 global $wpdb;
1093
1094 $synced_skus = get_transient( 'cmbird_eo_synced_skus' );
1095
1096 if ( false === $synced_skus || empty( $synced_skus ) ) {
1097 $this->response['success'] = true;
1098 $this->response['products'] = array();
1099 $this->response['message'] = __( 'No sync data available. Please run product mapping first.', 'commercebird' );
1100 $this->serve();
1101 return;
1102 }
1103
1104 // Create placeholders for the IN clause.
1105 $placeholders = implode( ',', array_fill( 0, count( $synced_skus ), '%s' ) );
1106
1107 // Query products with eo_item_id that have SKUs NOT in the synced list.
1108 $results = $wpdb->get_results(
1109 $wpdb->prepare(
1110 "SELECT p.ID, p.post_title, pm_sku.meta_value as sku
1111 FROM {$wpdb->posts} p
1112 INNER JOIN {$wpdb->postmeta} pm_eo ON p.ID = pm_eo.post_id AND pm_eo.meta_key = 'eo_item_id'
1113 LEFT JOIN {$wpdb->postmeta} pm_sku ON p.ID = pm_sku.post_id AND pm_sku.meta_key = '_sku'
1114 WHERE p.post_type = 'product'
1115 AND p.post_status = 'publish'
1116 AND (pm_sku.meta_value NOT IN ({$placeholders}) OR pm_sku.meta_value IS NULL)",
1117 ...$synced_skus
1118 ),
1119 ARRAY_A
1120 );
1121 $orphaned_products = array();
1122 foreach ( $results as $row ) {
1123 $orphaned_products[] = array(
1124 'id' => (int) $row['ID'],
1125 'name' => $row['post_title'],
1126 'sku' => $row['sku'] ?? '',
1127 );
1128 }
1129
1130 $this->response['success'] = true;
1131 $this->response['products'] = $orphaned_products;
1132 $this->response['count'] = count( $orphaned_products );
1133 $this->response['message'] = sprintf(
1134 /* translators: %d: number of orphaned products */
1135 _n(
1136 'Found %d product that exists in WooCommerce but not in Exact Online.',
1137 'Found %d products that exist in WooCommerce but not in Exact Online.',
1138 count( $orphaned_products ),
1139 'commercebird'
1140 ),
1141 count( $orphaned_products )
1142 );
1143 $this->serve();
1144 }
1145
1146 /**
1147 * Delete orphaned products from WooCommerce.
1148 *
1149 * @return void
1150 */
1151 public function delete_orphaned_products() {
1152 $this->verify( array( 'productIds' ) );
1153
1154 if ( empty( $this->data['productIds'] ) || ! is_array( $this->data['productIds'] ) ) {
1155 $this->errors['message'] = __( 'No product IDs provided', 'commercebird' );
1156 $this->serve();
1157 return;
1158 }
1159
1160 $product_ids = array_map( 'intval', $this->data['productIds'] );
1161 $deleted = array();
1162 $failed = array();
1163
1164 // Schedule deletion via Action Scheduler for better performance.
1165 $chunks = array_chunk( $product_ids, 20 );
1166 foreach ( $chunks as $index => $chunk ) {
1167 as_schedule_single_action(
1168 time() + ( $index * 5 ),
1169 'cmbird_delete_orphaned_products',
1170 array( 'product_ids' => $chunk )
1171 );
1172 }
1173
1174 // Clear the synced SKUs transient after scheduling deletion.
1175 delete_transient( 'cmbird_eo_synced_skus' );
1176
1177 $this->response['success'] = true;
1178 $this->response['message'] = sprintf(
1179 /* translators: %d: number of products */
1180 _n(
1181 'Scheduled deletion of %d product in the background.',
1182 'Scheduled deletion of %d products in the background.',
1183 count( $product_ids ),
1184 'commercebird'
1185 ),
1186 count( $product_ids )
1187 );
1188 $this->serve();
1189 }
1190
1191 /**
1192 * Deletes products in batch via Action Scheduler.
1193 *
1194 * @param array $args Arguments containing product_ids.
1195 * @return void
1196 */
1197 public function delete_products_batch( $args ) {
1198 if ( empty( $args['product_ids'] ) || ! is_array( $args['product_ids'] ) ) {
1199 return;
1200 }
1201
1202 foreach ( $args['product_ids'] as $product_id ) {
1203 $product = wc_get_product( $product_id );
1204 if ( ! $product ) {
1205 continue;
1206 }
1207 // Permanently delete the product.
1208 $product->delete( true );
1209 }
1210 }
1211 }
1212