PluginProbe
Bosta WooCommerce / trunk
Bosta WooCommerce vtrunk
4.5.7 4.5.6 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 4.0.0 4.0.1 4.0.3 4.0.4 4.0.5 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.3.2 All 84 releases
bosta-woocommerce / bosta-woocommerce.php

bosta-woocommerce.php in Bosta WooCommerce trunk, at bosta-woocommerce.php

3,553 lines 121.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Plugin Name: Bosta WooCommerce
5 * Description: WooCommerce integration for Bosta eCommerce
6 * Author: Bosta
7 * Author URI: https://www.bosta.co/
8 * Version: 4.5.7
9 * Requires at least: 5.0
10 * php version 7.0
11 * Tested up to: 6.9
12 * WC requires at least: 2.6
13 * WC tested up to: 10.0.4
14 * Text Domain: bosta-woocommerce
15 * Domain Path: /languages
16 *
17 */
18
19 // Define plugin file constant
20 if (!defined('BOSTA_PLUGIN_FILE')) {
21 define('BOSTA_PLUGIN_FILE', __FILE__);
22 }
23
24 // Include webhook loader
25 require_once plugin_dir_path(__FILE__) . 'includes/class-bosta-webhook-loader.php';
26
27 // Include webhook manager for activation hooks
28 require_once plugin_dir_path(__FILE__) . 'includes/class-bosta-webhook-manager.php';
29
30 // Include plugin hooks for activation/deactivation
31 require_once plugin_dir_path(__FILE__) . 'includes/class-bosta-plugin-hooks.php';
32
33 // Register activation and deactivation hooks immediately
34 Bosta_Plugin_Hooks::register_hooks();
35
36 // Include fulfillment classes
37 require_once plugin_dir_path(__FILE__) . 'includes/class-bosta-fulfillment.php';
38 require_once plugin_dir_path(__FILE__) . 'includes/class-bosta-fulfillment-ui.php';
39 require_once plugin_dir_path(__FILE__) . 'includes/class-bosta-fulfillment-cache.php';
40
41 // Include consignee ranking class
42 require_once plugin_dir_path(__FILE__) . 'includes/class-bosta-consignee-ranking.php';
43
44 // Include payment method handler
45 require_once plugin_dir_path(__FILE__) . 'includes/class-bosta-payment-method-handler.php';
46
47 // Include product sync class
48 require_once plugin_dir_path(__FILE__) . 'includes/class-bosta-product-sync.php';
49
50 // Include product info class
51 require_once plugin_dir_path(__FILE__) . 'includes/class-bosta-product-info.php';
52
53
54
55 // Initialize webhook system after WordPress is loaded
56 add_action('init', function() {
57 Bosta_Webhook_Loader::init();
58 Bosta_Payment_Method_Handler::init();
59 });
60
61 // Initialize fulfillment functionality
62 add_action('init', function() {
63 Bosta_Fulfillment_Cache::init();
64 Bosta_Fulfillment::init();
65 Bosta_Fulfillment_UI::init();
66 Bosta_Consignee_Ranking::init();
67 Bosta_Product_Sync::init();
68
69 // Handle form submissions for custom actions
70 bosta_handle_form_submissions();
71 });
72
73
74
75
76
77 add_action('before_woocommerce_init', function () {
78 if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) {
79 \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('custom_order_tables', __FILE__, true);
80 }
81 });
82
83 include plugin_dir_path(__FILE__) . 'components/pickups/pickups.php';
84 if (!defined('ABSPATH')) {
85 exit; // Exit if accessed directly
86 }
87
88 add_action('admin_print_styles', 'bosta_stylesheet');
89 function bosta_stylesheet()
90 {
91 $main_css_file = plugin_dir_path(__FILE__) . 'Css/main.css';
92 $pickups_css_file = plugin_dir_path(__FILE__) . 'components/pickups/pickups.css';
93 $flexship_css_file = plugin_dir_path(__FILE__) . 'components/settings/flexship/flexship.css';
94 $fulfillment_css_file = plugin_dir_path(__FILE__) . 'Css/fulfillment.css';
95
96 $main_css_version = filemtime($main_css_file);
97 $pickups_css_version = filemtime($pickups_css_file);
98 $flexship_css_version = filemtime($flexship_css_file);
99 $fulfillment_css_version = filemtime($fulfillment_css_file);
100
101 wp_enqueue_style(
102 'myCSS',
103 plugins_url('/Css/main.css', __FILE__),
104 array(),
105 $main_css_version
106 );
107 wp_enqueue_style(
108 'pickupsCSS',
109 plugins_url('components/pickups/pickups.css', __FILE__),
110 array(),
111 $pickups_css_version
112 );
113 wp_enqueue_style(
114 'flexshipCSS',
115 plugins_url('components/settings/flexship/flexship.css', __FILE__),
116 array(),
117 $flexship_css_version
118 );
119 wp_enqueue_style(
120 'fulfillmentCSS',
121 plugins_url('Css/fulfillment.css', __FILE__),
122 array(),
123 $fulfillment_css_version
124 );
125 }
126
127 const BOSTA_ENV_URL_V0 = 'https://app.bosta.co/api/v0';
128 const BOSTA_ENV_URL_V2 = 'https://app.bosta.co/api/v2';
129 const PLUGIN_VERSION = '4.5.7';
130 const bosta_cache_duration = 86400;
131 const bosta_country_id_duration = 604800;
132 const BOSTA_EGYPT_COUNTRY_ID = "60e4482c7cb7d4bc4849c4d5";
133 const FLEX_SHIPPING_DEFAULT_VALUE = 75;
134 //region Bosta Utils Functions
135
136 function bosta_send_api_request($method, $url, $APIKey = null, $body = null)
137 {
138 $args = [
139 'timeout' => 300,
140 'method' => strtoupper($method),
141 'headers' => [
142 'Content-Type' => 'application/json',
143 'X-Requested-By' => 'WooCommerce',
144 'X-Plugin-Version' => PLUGIN_VERSION,
145 ],
146 ];
147
148 if (!empty($APIKey)) {
149 $args['headers']['authorization'] = $APIKey;
150 }
151
152 if ($body) {
153 $args['body'] = json_encode($body);
154 }
155
156 $response = wp_remote_request($url, $args);
157 if (is_wp_error($response)) {
158 return [
159 'success' => false,
160 'error' => $response->get_error_message(),
161 ];
162 }
163 $response_body = wp_remote_retrieve_body($response);
164 $response_code = wp_remote_retrieve_response_code($response);
165
166 if ($response_code < 200 || $response_code >= 300 || empty($response_body)) {
167 $decoded_body = json_decode($response_body, true);
168
169 if (isset($decoded_body['message'])) {
170 $error_message = $decoded_body['message'];
171 } elseif (isset($decoded_body[0]['message'])) {
172 $error_message = $decoded_body[0]['message'];
173 } else {
174 $error_message = 'Unknown error';
175 }
176
177 $error_messages = [
178 'success' => false,
179 'error' => $error_message,
180 ];
181
182 if (isset($decoded_body['data'])) {
183 $error_messages['data'] = $decoded_body['data'];
184 }
185
186 return $error_messages;
187 }
188
189 return [
190 'success' => true,
191 'code' => $response_code,
192 'body' => json_decode($response_body, true),
193 ];
194 }
195
196 function bosta_validate_api_key($bostaApiKey)
197 {
198 if ($bostaApiKey == null) {
199 return false;
200 }
201
202 $url = BOSTA_ENV_URL_V0 . '/businesses/' . esc_html($bostaApiKey) . '/info';
203 $response = bosta_send_api_request('GET', $url);
204
205 if (!$response['success']) {
206 return false;
207 }
208
209 return true;
210 }
211
212 function bosta_get_api_key()
213 {
214 $apikey = get_option('woocommerce_bosta_settings', [])['APIKey'] ?? null;
215 if (isset($apikey)) {
216 return sanitize_text_field($apikey);
217 }
218 }
219
220 function bosta_check_disable_bosta_zoning_checkbox()
221 {
222 $bosta_settings = get_option('woocommerce_bosta_settings', []);
223 $disable_bosta_zoning = $bosta_settings['DisableBostaZoning'] ?? null;
224 return $disable_bosta_zoning === 'yes';
225 }
226
227 function bosta_get_country_id()
228 {
229 return BOSTA_EGYPT_COUNTRY_ID;
230 }
231
232 function bosta_check_area_coverage($area)
233 {
234 return isset($area['dropOffAvailability']) && $area['dropOffAvailability'] == true;
235 }
236
237 function bosta_get_zoning()
238 {
239 $country_id = bosta_get_country_id();
240 $bosta_zoning_key_cache = 'bosta_zoning';
241 $bosta_zoning = get_transient($bosta_zoning_key_cache);
242
243 if (!$bosta_zoning) {
244 $url = BOSTA_ENV_URL_V2 . '/cities/getAllDistricts?countryId=' . esc_html($country_id);
245 $response = bosta_send_api_request('GET', $url);
246 if (!$response['success']) {
247 return array();
248 }
249 // Check if response body is valid and contains expected structure
250 if (!$response['body'] || !is_array($response['body']) || !isset($response['body']['data'])) {
251 return array();
252 }
253 $bosta_zoning = $response['body']['data'];
254 set_transient($bosta_zoning_key_cache, $bosta_zoning, bosta_cache_duration);
255 }
256
257 return $bosta_zoning ? $bosta_zoning : [];
258 }
259
260 /**
261 * Get the current language code (either 'en' or 'ar')
262 * Checks WPML first, then WordPress native locale
263 *
264 * @return string 'en' for English, 'ar' for Arabic
265 */
266 function bosta_get_current_language()
267 {
268 // Check WPML first if available
269 if (defined('ICL_SITEPRESS_VERSION')) {
270 $current_language = apply_filters('wpml_current_language', null);
271 if ($current_language === 'en') {
272 return 'en';
273 }
274 if ($current_language === 'ar') {
275 return 'ar';
276 }
277 }
278
279 // Fall back to WordPress native locale
280 $locale = get_locale();
281 if (function_exists('get_user_locale') && is_admin()) {
282 $locale = get_user_locale();
283 }
284
285 // Check if locale starts with 'en' (en, en_US, en_GB, etc.)
286 if (strpos($locale, 'en') === 0) {
287 return 'en';
288 }
289
290 // Check if locale starts with 'ar' (ar, ar_EG, ar_SA, etc.)
291 if (strpos($locale, 'ar') === 0) {
292 return 'ar';
293 }
294
295 // Default to Arabic for backward compatibility
296 return 'ar';
297 }
298
299 function bosta_get_cities()
300 {
301 $bosta_zoning = bosta_get_zoning();
302 $bosta_cities = [];
303
304 $current_language = bosta_get_current_language();
305 $is_arabic = $current_language === 'ar';
306
307 foreach ($bosta_zoning as $city) {
308 if (!isset($city['cityOtherName']) || !bosta_check_area_coverage($city)) {
309 continue;
310 }
311 $city_code = $city['cityCode'];
312 $city_name = $is_arabic ? $city['cityOtherName'] : $city['cityName'];
313 $bosta_cities[$city_code] = $city_name;
314 }
315 return $bosta_cities;
316 }
317
318 function bosta_get_city_areas()
319 {
320 $bosta_zoning = bosta_get_zoning();
321
322 $current_language = bosta_get_current_language();
323 $is_arabic = $current_language === 'ar';
324
325 $bosta_city_areas_cache_key = 'bosta_city_areas' . '_' . $current_language;
326 $bosta_city_areas = get_transient($bosta_city_areas_cache_key);
327 if (!$bosta_city_areas) {
328 $bosta_city_areas = [];
329 foreach ($bosta_zoning as $city) {
330 $city_code = $city['cityCode'];
331 $city_areas = '';
332 foreach ($city['districts'] as $district) {
333 $zone_name = $is_arabic ? $district['zoneOtherName'] : $district['zoneName'];
334 $district_name = $is_arabic ? $district['districtOtherName'] : $district['districtName'];
335
336 if ($zone_name === $district_name) {
337 $area = $district_name;
338 } else {
339 $area = $zone_name . ' - ' . $district_name;
340 }
341
342 $city_areas .= sprintf(
343 '<option value="%s">%s</option>',
344 esc_attr($district['districtId']),
345 esc_html($area)
346 );
347 }
348 $bosta_city_areas[$city_code] = $city_areas;
349 }
350 set_transient($bosta_city_areas_cache_key, $bosta_city_areas, bosta_cache_duration);
351 }
352 return $bosta_city_areas ? $bosta_city_areas : [];
353 }
354
355 function bosta_format_failed_order_message($error_message, $order_id = null)
356 {
357 $formatted_error_message = '<p>' . ($order_id ? '<strong>Order ID:</strong> ' . esc_html($order_id) . '<br>' : '') .
358 '<strong>Reason:</strong> ' . esc_html(print_r($error_message, true)) . '</p>';
359 bosta_set_transient('bosta_failed_orders', $formatted_error_message);
360 }
361
362 function bosta_format_date($date)
363 {
364 try {
365 $pos = strrpos($date, '(');
366 $clean_date = $pos !== false ? substr($date, 0, $pos) : $date;
367 $datetime = new DateTime($clean_date, new DateTimeZone('UTC'));
368 $datetime->setTimezone(new DateTimeZone('Africa/Cairo'));
369 return $datetime->format('l, d/m/Y h:ia');
370 } catch (Exception $e) {
371 error_log('Error parsing date: ' . $e->getMessage());
372 return null;
373 }
374 }
375
376 function bosta_set_transient($key, $value, $expiration = HOUR_IN_SECONDS)
377 {
378 $existing_value = get_transient($key) ?: '';
379 $updated_value = $existing_value . $value;
380 set_transient($key, $updated_value, $expiration);
381 }
382
383 function bosta_render_pdf($pdf_data)
384 {
385 header('Content-Type: application/pdf');
386 header('Cache-Control: public, must-revalidate, max-age=0');
387 header('Pragma: public');
388 ob_clean();
389 flush();
390 echo $pdf_data;
391 exit;
392 }
393
394 function bosta_redirect_to_settings_page()
395 {
396 $redirect_url = admin_url('admin.php?') . 'page=wc-settings&tab=shipping&section=bosta';
397 wp_redirect($redirect_url);
398 exit;
399 }
400
401 function bosta_redirect_to_orders_page()
402 {
403 $redirect_url = admin_url('edit.php?') . 'post_type=shop_order&paged=1';
404 wp_redirect($redirect_url);
405 }
406
407 function bosta_redirect_to_dashboard_page()
408 {
409 $redirect_url = 'https://bosta.co/tracking-shipments';
410 wp_redirect($redirect_url);
411 }
412
413 function bosta_redirect_to_documentation_page()
414 {
415 $redirect_url = 'https://docs.bosta.co/docs/plugins-and-sdks/integrate-with-woocommerce';
416 wp_redirect($redirect_url);
417 }
418
419 function bosta_inventory_page()
420 {
421 // Check if user has permissions
422 if (!current_user_can('manage_woocommerce')) {
423 wp_die(__('You do not have sufficient permissions to access this page.', 'bosta'));
424 }
425
426 // Handle form submissions
427 if (isset($_POST['bosta_inventory_action']) && wp_verify_nonce($_POST['bosta_inventory_nonce'], 'bosta_inventory_action')) {
428 $action = sanitize_text_field($_POST['bosta_inventory_action']);
429
430 switch ($action) {
431 case 'sync_quantity':
432 // Handle quantity sync
433 $result = Bosta_Fulfillment::sync_quantity();
434 if ($result['success']) {
435 // Store success message in a transient for display
436 set_transient('bosta_inventory_success', $result['message'], 60);
437 } else {
438 // Store error message in a transient for display
439 set_transient('bosta_inventory_error', $result['message'], 60);
440 }
441 // Redirect to prevent form resubmission
442 wp_redirect(admin_url('admin.php?page=bosta-woocommerce-inventory'));
443 exit;
444 break;
445 }
446 }
447
448 // Get current settings
449 $api_key = bosta_get_api_key();
450 $is_fulfillment_enabled = Bosta_Fulfillment_Cache::is_fulfillment_enabled();
451 $cache_info = Bosta_Fulfillment_Cache::get_cache_info();
452
453 ?>
454 <div class="wrap">
455 <h1><?php _e('Bosta Inventory Management', 'bosta'); ?></h1>
456
457 <?php
458 // Display success message
459 $success_message = get_transient('bosta_inventory_success');
460 if ($success_message) {
461 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($success_message) . '</p></div>';
462 delete_transient('bosta_inventory_success');
463 }
464
465 // Display error message
466 $error_message = get_transient('bosta_inventory_error');
467 if ($error_message) {
468 echo '<div class="notice notice-error is-dismissible"><p>' . esc_html($error_message) . '</p></div>';
469 delete_transient('bosta_inventory_error');
470 }
471 ?>
472
473 <?php if (empty($api_key)): ?>
474 <div class="notice notice-error">
475 <p><?php _e('API Key is required to manage inventory. Please configure your API key in the Bosta settings.', 'bosta'); ?></p>
476 <p><a href="<?php echo admin_url('admin.php?page=wc-settings&tab=shipping&section=bosta'); ?>" class="button button-primary"><?php _e('Go to Settings', 'bosta'); ?></a></p>
477 </div>
478 <?php else: ?>
479 <div class="bosta-inventory-container">
480 <div class="bosta-inventory-section">
481
482
483 <?php if ($is_fulfillment_enabled): ?>
484 <div class="bosta-inventory-actions">
485 <h3><?php _e('Inventory Actions', 'bosta'); ?></h3>
486
487 <form method="post" action="">
488 <?php wp_nonce_field('bosta_inventory_action', 'bosta_inventory_nonce'); ?>
489
490 <div class="bosta-action-buttons">
491 <button type="submit" name="bosta_inventory_action" value="sync_quantity" class="button button-primary">
492 <?php _e('Sync Quantity', 'bosta'); ?>
493 </button>
494 </div>
495
496 <div class="bosta-action-descriptions">
497 <p><strong><?php _e('Sync Quantity:', 'bosta'); ?></strong> <?php _e('Sync Quantity will force sync bosta quantity', 'bosta'); ?></p>
498 </div>
499 </form>
500 </div>
501 <?php else: ?>
502 <div class="notice notice-warning">
503 <p><?php _e('Fulfillment features are not available for your account. Please contact Bosta support to enable fulfillment services.', 'bosta'); ?></p>
504 </div>
505
506 <!-- Temporary fallback for testing -->
507 <div style="background: #fff3cd; padding: 10px; margin: 10px 0; border-radius: 4px; border: 1px solid #ffeaa7;">
508 <strong><?php _e('Testing Mode:', 'bosta'); ?></strong> <?php _e('Buttons shown for testing purposes', 'bosta'); ?>
509
510 <form method="post" action="">
511 <?php wp_nonce_field('bosta_inventory_action', 'bosta_inventory_nonce'); ?>
512
513 <div class="bosta-action-buttons">
514 <button type="submit" name="bosta_inventory_action" value="sync_quantity" class="button button-primary">
515 <?php _e('Sync Quantity', 'bosta'); ?>
516 </button>
517 </div>
518
519 <div class="bosta-action-descriptions">
520 <p><strong><?php _e('Sync Quantity:', 'bosta'); ?></strong> <?php _e('Sync Quantity will force sync bosta quantity', 'bosta'); ?></p>
521 </div>
522 </form>
523 </div>
524 <?php endif; ?>
525
526 <div class="bosta-cache-info">
527 <h3><?php _e('Cache Information', 'bosta'); ?></h3>
528 <p class="description">
529 <strong><?php _e('Cache Status:', 'bosta'); ?></strong>
530 <?php
531 if ($cache_info['transient_exists']) {
532 $expiry_time = $cache_info['transient_expiry'];
533 $time_remaining = $expiry_time - time();
534 if ($time_remaining > 0) {
535 $minutes = floor($time_remaining / 60);
536 printf(__('Cached (expires in %d minutes)', 'bosta'), $minutes);
537 } else {
538 _e('Cache expired', 'bosta');
539 }
540 } else {
541 _e('Not cached', 'bosta');
542 }
543 ?>
544 <button type="button" id="refresh-fulfillment-status" class="button button-small" style="margin-left: 10px;">
545 <?php _e('Refresh Status', 'bosta'); ?>
546 </button>
547 </p>
548 </div>
549 </div>
550 </div>
551
552 <style>
553 .bosta-inventory-container {
554 max-width: 800px;
555 }
556
557 .bosta-inventory-section {
558 background: #fff;
559 padding: 20px;
560 border: 1px solid #ccd0d4;
561 border-radius: 4px;
562 margin-top: 20px;
563 }
564
565 .bosta-action-buttons {
566 margin: 20px 0;
567 }
568
569 .bosta-action-buttons .button {
570 margin-right: 10px;
571 }
572
573 .bosta-action-descriptions {
574 margin-top: 15px;
575 }
576
577 .bosta-action-descriptions p {
578 margin-bottom: 10px;
579 color: #666;
580 }
581
582 .bosta-cache-info {
583 margin-top: 30px;
584 padding-top: 20px;
585 border-top: 1px solid #eee;
586 }
587 </style>
588
589 <script type="text/javascript">
590 jQuery(document).ready(function($) {
591 // Refresh fulfillment status button
592 $('#refresh-fulfillment-status').on('click', function() {
593 var button = $(this);
594 var originalText = button.text();
595
596 button.prop('disabled', true).text('<?php _e('Refreshing...', 'bosta'); ?>');
597
598 $.ajax({
599 url: ajaxurl,
600 type: 'POST',
601 data: {
602 action: 'bosta_refresh_fulfillment_status',
603 nonce: '<?php echo wp_create_nonce('bosta_refresh_fulfillment_status'); ?>'
604 },
605 success: function(response) {
606 if (response.success) {
607 // Reload the page to reflect the new status
608 location.reload();
609 } else {
610 alert('<?php _e('Failed to refresh fulfillment status:', 'bosta'); ?> ' + response.data);
611 }
612 },
613 error: function() {
614 alert('<?php _e('An error occurred while refreshing fulfillment status.', 'bosta'); ?>');
615 },
616 complete: function() {
617 button.prop('disabled', false).text(originalText);
618 }
619 });
620 });
621 });
622 </script>
623 <?php endif; ?>
624 </div>
625 <?php
626 }
627
628 function bosta_get_order_by_metadata($meta_key, $meta_value)
629 {
630 try {
631 $page_num = isset($_GET['page_num']) ? $_GET['page_num'] : 1;
632 $query = new WC_Order_Query([
633 'limit' => 1,
634 'meta_key' => $meta_key,
635 'meta_value' => $meta_value,
636 'paged' => $page_num
637 ]);
638 $orders = $query->get_orders();
639 return !empty($orders) ? $orders[0] : null;
640 } catch (Exception $e) {
641 error_log('Bosta Plugin: Failed to get order by metadata. Meta Key: ' . $meta_key . ', Meta Value: ' . $meta_value . '. Error: ' . $e->getMessage());
642
643 $message = 'Bosta Plugin: Failed to get order by metadata. Meta Key: ' . esc_html($meta_key) . ', Meta Value: ' . esc_html($meta_value) . '. Error: ' . esc_html($e->getMessage());
644 bosta_set_transient('bosta_errors', "<p>{$message}</p>");
645 return null;
646 }
647 }
648
649 function bosta_update_order_metadata($order, $bosta_data)
650 {
651 try {
652 $is_order_delivered = $bosta_data['state']['code'] == 45;
653 $deliveried_at = $is_order_delivered ? bosta_format_date($bosta_data['state']['delivering']['time']) : null;
654 $meta_mapping = [
655 'bosta_delivery_id' => $bosta_data['_id'] ?? null,
656 'bosta_status' => $bosta_data['state']['value'] ?? null,
657 'bosta_tracking_number' => $bosta_data['trackingNumber'] ?? null,
658 'bosta_customer_phone' => $bosta_data['receiver']['phone'] ?? null,
659 'bosta_delivery_date' => $deliveried_at
660 ];
661
662 foreach ($meta_mapping as $meta_key => $meta_value) {
663 if (!empty($meta_value)) {
664 $order->update_meta_data($meta_key, $meta_value);
665 }
666 }
667
668 // Check if this is a fulfillment order (FXF_SEND type)
669 if (isset($bosta_data['type']) && $bosta_data['type'] === 'FXF_SEND') {
670 $order->update_meta_data('bosta_is_fulfillment', true);
671 }
672
673 // Clean up sync-related metadata when order is successfully synced
674 // This ensures orders that previously failed auto-sync are no longer filtered as failed
675 if (!empty($bosta_data['trackingNumber'])) {
676 $order->update_meta_data('bosta_sync_status', 'success');
677 $order->delete_meta_data('bosta_sync_error');
678 }
679
680 $order->save();
681 } catch (Exception $e) {
682 $order_id = is_object($order) && method_exists($order, 'get_id') ? $order->get_id() : '';
683 error_log('Bosta Plugin: Failed to update order metadata. Order ID: ' . $order_id . '. Error: ' . $e->getMessage());
684
685 $message = 'Bosta Plugin: Failed to update order metadata. Order ID: ' . esc_html($order_id) . '. Error: ' . esc_html($e->getMessage());
686 bosta_set_transient('bosta_errors', "<p>{$message}</p>");
687 }
688 }
689
690 function bosta_delete_order_metadata($order)
691 {
692 try {
693 $meta_keys = [
694 'bosta_delivery_id',
695 'bosta_status',
696 'bosta_tracking_number',
697 'bosta_customer_phone',
698 'bosta_delivery_date',
699 'bosta_sync_status',
700 'bosta_sync_error',
701 'bosta_is_fulfillment'
702 ];
703
704 foreach ($meta_keys as $meta_key) {
705 $order->delete_meta_data($meta_key);
706 }
707
708 $order->save();
709 } catch (Exception $e) {
710 $order_id = is_object($order) && method_exists($order, 'get_id') ? $order->get_id() : '';
711 error_log('Bosta Plugin: Failed to delete order metadata. Order ID: ' . $order_id . '. Error: ' . $e->getMessage());
712
713 $message = 'Bosta Plugin: Failed to delete order metadata. Order ID: ' . esc_html($order_id) . '. Error: ' . esc_html($e->getMessage());
714 bosta_set_transient('bosta_errors', "<p>{$message}</p>");
715 }
716 }
717 //endregion
718
719 //region Bosta Customize City Fields
720
721 add_filter('woocommerce_states', 'bosta_custom_woocommerce_states');
722 function bosta_custom_woocommerce_states($states)
723 {
724 $bosta_cities = bosta_get_cities();
725 $states['EG'] = $bosta_cities;
726 return $states;
727 }
728
729 add_filter('woocommerce_checkout_fields', 'bosta_add_dynamic_area_dropdown_to_checkout', 20);
730 function bosta_add_dynamic_area_dropdown_to_checkout($fields)
731 {
732 if (!bosta_check_disable_bosta_zoning_checkbox()) {
733 $field_priority = 50;
734 if (isset($fields['billing']['billing_state']['priority'])) {
735 $field_priority = $fields['billing']['billing_state']['priority'] + 1;
736 }
737
738 $fields['billing']['billing_area'] = array(
739 'type' => 'select',
740 'label' => __('Area', 'woocommerce'),
741 'required' => false,
742 'options' => array(
743 '' => __('Select an option...', 'woocommerce'),
744 ),
745 'input_class' => array(
746 'wc-enhanced-select',
747 ),
748 'priority' => $field_priority,
749 );
750
751 wc_enqueue_js("
752 jQuery(document).ready(function($) {
753 $(':input.wc-enhanced-select').filter(':not(.enhanced)').each(function() {
754 var select2_args = { minimumResultsForSearch: 5 };
755 $(this).select2(select2_args).addClass('enhanced');
756 });
757
758 console.log('Bosta Plugin: Adding dynamic area dropdown to checkout');
759
760 $('select.wc-enhanced-select').val('').trigger('change');
761 $('#billing_state').val('').trigger('change');
762 });
763 ");
764 }
765
766 return $fields;
767 }
768
769 add_action('woocommerce_admin_order_data_after_billing_address', 'bosta_add_area_dropdown_admin_order', 10, 1);
770 function bosta_add_area_dropdown_admin_order($order)
771 {
772 if (!bosta_check_disable_bosta_zoning_checkbox()) {
773
774
775 $current_state = get_post_meta($order->get_id(), '_billing_state', true);
776 $current_area = get_post_meta($order->get_id(), '_billing_area', true);
777
778 $bosta_city_areas = bosta_get_city_areas();
779 $areas_options = '<option value="">' . __('Select an area...', 'woocommerce') . '</option>';
780
781 if (!empty($bosta_city_areas[$current_state])) {
782 $areas = explode('</option>', $bosta_city_areas[$current_state]);
783 foreach ($areas as $area_option) {
784 if (strpos($area_option, 'value="' . esc_attr($current_area) . '"') !== false) {
785 $area_option = str_replace('<option', '<option selected="selected"', $area_option);
786 }
787 $areas_options .= $area_option . '</option>';
788 }
789 }
790
791 echo '<p class="form-field" style="width:100%">' .
792 '<label for="billing_area">' . __('Area', 'woocommerce') . '</label>' .
793 '<select name="billing_area" id="billing_area" class="wc-enhanced-select">' .
794 $areas_options .
795 '</select>' .
796 '</p>';
797 }
798 }
799
800 add_action('wp_footer', 'bosta_enqueue_dynamic_area_dropdown_script');
801 add_action('admin_footer', 'bosta_enqueue_dynamic_area_dropdown_script');
802 function bosta_enqueue_dynamic_area_dropdown_script()
803 {
804 if (!bosta_check_disable_bosta_zoning_checkbox()) {
805 $bosta_city_areas = bosta_get_city_areas();
806 $city_areas_js = json_encode($bosta_city_areas);
807
808 $is_valid_screen = false;
809 $is_checkout = is_checkout();
810 $is_admin = is_admin();
811 if($is_checkout) {
812 $is_valid_screen = true;
813 }
814 if($is_admin) {
815 $current_screen = get_current_screen();
816 if ($current_screen && isset($current_screen->post_type) && $current_screen->post_type === 'shop_order') {
817 $is_valid_screen = true;
818 }
819 }
820 if (!$is_valid_screen) {
821 return;
822 }
823
824 ?>
825 <script type="text/javascript">
826 jQuery(function($) {
827 function updateAreaDropdown(stateSelector, areaSelector, cityAreas) {
828 $(document).on('change', stateSelector, function() {
829 var selectedState = $(this).val();
830 var areaDropdown = $(areaSelector);
831
832 areaDropdown.empty();
833 areaDropdown.append($('<option></option>').attr('value', '').text('Select an option...'));
834
835 if (selectedState && cityAreas[selectedState]) {
836 var areas = cityAreas[selectedState];
837 areaDropdown.append(areas);
838 } else {
839 areaDropdown.append($('<option></option>').attr('value', '').text('No areas available'));
840 }
841
842 areaDropdown.trigger('change');
843 });
844 }
845
846 var cityAreasJs = <?php echo $city_areas_js; ?>;
847
848 <?php if ($is_checkout): ?>
849 updateAreaDropdown('#billing_state', '#billing_area', cityAreasJs);
850 <?php endif; ?>
851
852 <?php if ($is_admin): ?>
853 updateAreaDropdown('#_billing_state', '#billing_area', cityAreasJs);
854 <?php endif; ?>
855 });
856 </script>
857 <?php
858 }
859 }
860
861 add_action('woocommerce_checkout_update_order_meta', 'bosta_save_billing_area_to_order_metadata', 10, 2);
862 add_action('woocommerce_process_shop_order_meta', 'bosta_save_billing_area_to_order_metadata', 10, 2);
863 function bosta_save_billing_area_to_order_metadata($order_id, $posted_data)
864 {
865 if (bosta_check_disable_bosta_zoning_checkbox()) {
866 return;
867 }
868
869 if (isset($_POST['billing_area'])) {
870 $billing_area = sanitize_text_field($_POST['billing_area']);
871 $order = wc_get_order($order_id);
872 $order->update_meta_data('_billing_area', $billing_area);
873 $order->save();
874 }
875 }
876
877 //endregion
878
879 //region Bosta Notice Messages
880
881 add_action('admin_notices', 'bosta_woocommerce_notice');
882 function bosta_woocommerce_notice()
883 {
884 //check if woocommerce installed and activated
885 if (!class_exists('WooCommerce')) {
886 echo
887 '<div class="error notice-warning text-bold">
888 <p>
889 <img src="' . esc_url(plugins_url('assets/images/bosta.svg', __FILE__)) . '" alt="Bosta" style="height:13px; width:25px;">
890 <strong>' . sprintf(esc_html__('Bosta requires WooCommerce to be installed and active. You can download %s here.'), '<a href="https://woocommerce.com/" target="_blank">WooCommerce</a>') . '</strong>
891 </p>
892 </div>';
893 }
894
895 $success_count = get_transient('bosta_success_count');
896 if ($success_count) {
897 bosta_render_success_notice($success_count);
898 delete_transient('bosta_success_count');
899 }
900
901 $bosta_success = get_transient('bosta_success');
902 if ($bosta_success) {
903 bosta_render_success_message($bosta_success);
904 delete_transient('bosta_success');
905 }
906
907 $bosta_errors = get_transient('bosta_errors');
908 if ($bosta_errors) {
909 bosta_render_error_notice($bosta_errors);
910 delete_transient('bosta_errors');
911 }
912
913 $failed_orders = get_transient('bosta_failed_orders');
914 if ($failed_orders) {
915 bosta_render_failed_orders_notice($failed_orders);
916 delete_transient('bosta_failed_orders');
917 }
918 }
919
920 function bosta_render_success_notice($success_count)
921 {
922 if ($success_count) {
923 echo '<div class="notice notice-success is-dismissible">';
924 echo '<p>' . sprintf(esc_html__('%d orders successfully synced at Bosta.'), $success_count) . '</p>';
925 echo '</div>';
926 }
927 }
928
929 function bosta_render_success_message($message)
930 {
931 echo '<div class="notice notice-success is-dismissible">';
932 echo '<p>' . wp_kses_post($message) . '</p>';
933 echo '</div>';
934 }
935
936 function bosta_render_error_notice($error_message)
937 {
938 echo '<div class="notice notice-error is-dismissible" style="padding: 10px;">';
939 echo $error_message;
940 echo '</div>';
941 }
942
943 function bosta_render_failed_orders_notice($failed_orders)
944 {
945 echo '<div class="notice notice-error is-dismissible">';
946 echo '<p>Some orders failed to be synced at Bosta. <span class="toggle-details" style="cursor: pointer; color: red;">&#9660;</span></p>';
947 echo '<div class="details hidden" style="max-height: 150px; overflow-y: auto; margin: 10px;">';
948 echo $failed_orders;
949 echo '</div>';
950 echo '</div>';
951 ?>
952 <script>
953 document.addEventListener('DOMContentLoaded', () => {
954 document.querySelector('.toggle-details').addEventListener('click', function() {
955 const detailsSection = document.querySelector('.details');
956 detailsSection.classList.toggle('hidden');
957 this.innerHTML = detailsSection.classList.contains('hidden') ? '&#9660;' : '&#9650;';
958 });
959 });
960 </script>
961 <?php
962 }
963
964 //endregion
965
966 //region Bosta Customize Orders Table
967
968 add_filter('manage_edit-shop_order_columns', 'bosta_wco_add_columns');
969 add_filter('manage_woocommerce_page_wc-orders_columns', 'bosta_wco_add_columns');
970 function bosta_wco_add_columns($columns)
971 {
972 $order_total = $columns['order_total'];
973 $order_date = $columns['order_date'];
974 $order_status = $columns['order_status'];
975
976 unset($columns['order_date']);
977 unset($columns['order_status']);
978 unset($columns['order_total']);
979
980 $columns["bosta_customer_info"] = __("Customer Info", "themeprefix");
981 $columns["bosta_tracking_number"] = __("Bosta Tracking Number", "themeprefix");
982 $columns['order_date'] = $order_date;
983 $columns['order_status'] = $order_status;
984 $columns["bosta_status"] = __("Bosta Status", "themeprefix");
985 $columns["bosta_delivery_date"] = __("Delivered at", "themeprefix");
986 $columns["bosta_sync_status"] = __("Bosta Sync Status", "themeprefix");
987 $columns['order_total'] = $order_total;
988
989 return $columns;
990 }
991
992 add_action('manage_shop_order_posts_custom_column', 'bosta_wco_column_cb_data', 10, 2);
993 add_action('manage_woocommerce_page_wc-orders_custom_column', 'bosta_wco_column_cb_data', 10, 2);
994 function bosta_wco_column_cb_data($colName, $orderId)
995 {
996 $order = wc_get_order($orderId);
997
998 $status = $order->get_meta('bosta_status', true);
999 $trackingNumber = $order->get_meta('bosta_tracking_number', true);
1000 $deliveryDate = $order->get_meta('bosta_delivery_date', true);
1001 $customerPhone = $order->get_meta('bosta_customer_phone', true);
1002 $syncStatus = $order->get_meta('bosta_sync_status', true);
1003 $syncError = $order->get_meta('bosta_sync_error', true);
1004
1005 if ($colName == 'bosta_status') {
1006 echo !empty($status) ? esc_html($status) : "---";
1007 }
1008
1009 if ($colName == 'bosta_tracking_number') {
1010 echo !empty($trackingNumber) ? esc_html($trackingNumber) : "---";
1011 }
1012
1013 if ($colName == 'bosta_delivery_date') {
1014 echo !empty($deliveryDate) ? $deliveryDate : "---";
1015 }
1016
1017 if ($colName == 'bosta_customer_info') {
1018 echo Bosta_Consignee_Ranking::format_customer_info_display($order);
1019 }
1020
1021 if ($colName == 'bosta_sync_status') {
1022 if (!empty($trackingNumber)) {
1023 // Order is synced successfully
1024 echo '<div class="bosta-sync-status-wrapper">';
1025 echo '<span class="bosta-status-badge bosta-status-success" data-tooltip="Successfully synced with Bosta">';
1026 echo '<span class="bosta-status-text">Synced</span>';
1027 echo '</span>';
1028 echo '</div>';
1029 } elseif ($syncStatus === 'failed') {
1030 // Order sync failed
1031 $tooltip = !empty($syncError) ? esc_attr($syncError) : 'Sync failed';
1032 echo '<div class="bosta-sync-status-wrapper">';
1033 echo '<span class="bosta-status-badge bosta-status-failed" data-tooltip="' . $tooltip . '">';
1034 echo '<span class="bosta-status-text">Failed</span>';
1035 echo '</span>';
1036 echo '</div>';
1037 } elseif ($syncStatus === 'processing') {
1038 // Order is being processed
1039 echo '<div class="bosta-sync-status-wrapper">';
1040 echo '<span class="bosta-status-badge bosta-status-processing" data-tooltip="Syncing with Bosta...">';
1041 echo '<span class="bosta-status-text">Processing</span>';
1042 echo '</span>';
1043 echo '</div>';
1044 } else {
1045 // Not synced yet
1046 echo '<div class="bosta-sync-status-wrapper">';
1047 echo '<span class="bosta-status-badge bosta-status-none" data-tooltip="Not synced yet">';
1048 echo '<span class="bosta-status-text">Not Synced</span>';
1049 echo '</span>';
1050 echo '</div>';
1051 }
1052 }
1053 }
1054
1055 //endregion
1056
1057 //region Bosta Bulk Actions
1058
1059 add_filter('bulk_actions-edit-shop_order', 'bosta_sync_cash_collection_orders', 20);
1060 add_filter('bulk_actions-woocommerce_page_wc-orders', 'bosta_sync_cash_collection_orders', 20);
1061 function bosta_sync_cash_collection_orders($actions)
1062 {
1063 $actions['sync_cash_collection_orders'] = __('Send Cash Collection Orders', 'woocommerce');
1064 return $actions;
1065 }
1066
1067 add_filter('bulk_actions-edit-shop_order', 'bosta_sync', 20);
1068 add_filter('bulk_actions-woocommerce_page_wc-orders', 'bosta_sync', 20);
1069 function bosta_sync($actions)
1070 {
1071 $actions['sync_to_bosta'] = __('Send To Bosta', 'woocommerce');
1072 return $actions;
1073 }
1074
1075 // Add fulfillment modal for bulk actions - Moved to Bosta_Fulfillment_UI class
1076
1077 add_filter('bulk_actions-edit-shop_order', 'bosta_print_awb', 20);
1078 add_filter('bulk_actions-woocommerce_page_wc-orders', 'bosta_print_awb', 20);
1079 function bosta_print_awb($actions)
1080 {
1081 $actions['print_bosta_awb'] = __('Print Bosta AirWaybill', 'woocommerce');
1082 return $actions;
1083 }
1084
1085
1086 add_filter('handle_bulk_actions-edit-shop_order', 'bosta_handle_bulk_action', 10, 3);
1087 add_filter('handle_bulk_actions-woocommerce_page_wc-orders', 'bosta_handle_bulk_action', 10, 3);
1088 function bosta_handle_bulk_action($redirect_to, $action, $order_ids, $fulfillment_type = null)
1089 {
1090 // Get fulfillment type from POST or GET if available (fallback to parameter)
1091 if ($fulfillment_type === null) {
1092 $fulfillment_type = isset($_POST['fulfillment_type']) ? sanitize_text_field($_POST['fulfillment_type']) : null;
1093 if ($fulfillment_type === null) {
1094 $fulfillment_type = isset($_GET['fulfillment_type']) ? sanitize_text_field($_GET['fulfillment_type']) : null;
1095 }
1096 }
1097
1098
1099
1100 $order_action = bosta_handle_order_action($action, $fulfillment_type);
1101 if (!$order_action) {
1102 return;
1103 }
1104
1105
1106
1107 $APIKey = bosta_get_api_key();
1108 if (empty($APIKey)) {
1109 $message = 'API Key is required to be able to sync with Bosta';
1110 bosta_set_transient('bosta_errors', "<p>{$message}</p>");
1111 bosta_redirect_to_settings_page();
1112 return;
1113 }
1114
1115 $orders = wc_get_orders([
1116 'limit' => -1,
1117 'post__in' => $order_ids,
1118 ]);
1119
1120 if (!empty($orders)) {
1121 switch ($order_action['actionType']) {
1122 case 'sync_orders':
1123 bosta_handle_send_orders_bulk_action([
1124 'APIKey' => $APIKey,
1125 'redirect_to' => $redirect_to,
1126 'orders' => $orders,
1127 'order_action' => $order_action,
1128 ]);
1129 break;
1130
1131 case 'auto_sync_orders':
1132 bosta_handle_auto_sync_bulk_action([
1133 'APIKey' => $APIKey,
1134 'redirect_to' => $redirect_to,
1135 ]);
1136 break;
1137
1138 case 'print_awbs':
1139 bosta_handle_print_awbs_bulk_action([
1140 'APIKey' => $APIKey,
1141 'orders' => $orders,
1142 ]);
1143 break;
1144
1145 case 'fetch_status':
1146 bosta_handle_fetch_status_bulk_action([
1147 'APIKey' => $APIKey,
1148 'redirect_to' => $redirect_to,
1149 'orders' => $orders,
1150 ]);
1151 break;
1152 default:
1153 throw new Exception('Unknown action type: ' . $order_action['actionType']);
1154 }
1155 }
1156 return $redirect_to;
1157 }
1158
1159 function bosta_handle_order_action($action, $fulfillment_type = null)
1160 {
1161 // Handle fulfillment-related actions through the modular class
1162 $fulfillment_result = Bosta_Fulfillment::handle_order_action($action, $fulfillment_type);
1163 if ($fulfillment_result !== null) {
1164 return $fulfillment_result;
1165 }
1166
1167 // Handle other actions
1168 switch ($action) {
1169 case 'sync_cash_collection_orders':
1170 return [
1171 'actionType' => 'sync_orders',
1172 'orderType' => 15,
1173 'addressType' => 'pickupAddress',
1174 ];
1175 case 'print_bosta_awb':
1176 return [
1177 'actionType' => 'print_awbs',
1178 ];
1179 case 'fetch_latest_status':
1180 return [
1181 'actionType' => 'fetch_status',
1182 ];
1183 default:
1184 return null;
1185 }
1186 }
1187
1188 function bosta_validate_order_fields($order, $order_action = null) {
1189 $errors = [];
1190 $msg = '';
1191
1192 $fields = [
1193 'First Name' => $order->get_billing_first_name() ?: $order->get_shipping_first_name(),
1194 'Phone' => $order->get_billing_phone() ?: $order->get_shipping_phone(),
1195 'Address line 1' => $order->get_billing_address_1() ?: $order->get_shipping_address_1(),
1196 'State / County' => $order->get_billing_state() ?: $order->get_shipping_state(),
1197 ];
1198
1199 foreach ($fields as $label => $value) {
1200 if (empty($value)) {
1201 $errors[] = '[' . $label . '] ' . 'is required.';
1202 }
1203 }
1204
1205 $firstLine = $fields['Address line 1'];
1206 if (!empty($firstLine) && strlen($firstLine) < 10) {
1207 $errors[] = 'Error: character limit. The address entered is less than 10 characters. Please modify it and try again.';
1208 }
1209
1210 // Check if this is a Bosta fulfillment sync and validate Bosta SKU
1211 if ($order_action && isset($order_action['fulfillmentType']) && $order_action['fulfillmentType'] === 'bosta_fulfillment') {
1212 $products_without_sku = [];
1213
1214 foreach ($order->get_items() as $item) {
1215 $product = $item->get_product();
1216 if ($product) {
1217 $product_id = $product->get_id();
1218 $bosta_sku = '';
1219
1220 // If it's a variation, get the SKU from the variation
1221 if ($product->is_type('variation')) {
1222 $bosta_sku = get_post_meta($product_id, '_bosta_sku', true);
1223 } else {
1224 // For simple products, get the SKU from the product
1225 $bosta_sku = Bosta_Fulfillment::get_product_sku($product_id);
1226 }
1227
1228 if (empty($bosta_sku)) {
1229 $products_without_sku[] = $product->get_name();
1230 }
1231 }
1232 }
1233
1234 if (!empty($products_without_sku)) {
1235 $errors[] = 'Bosta SKU is required for products: ' . implode(', ', $products_without_sku);
1236 }
1237 }
1238
1239 if (!empty($errors)) {
1240 $msg = implode('', $errors);
1241 }
1242
1243 return $msg;
1244 }
1245
1246 function bosta_handle_send_orders_bulk_action($params)
1247 {
1248 $APIKey = $params['APIKey'];
1249 $orders = $params['orders'];
1250 $order_action = $params['order_action'];
1251
1252 // Set all orders to processing status first
1253 foreach ($orders as $order) {
1254 $order->update_meta_data('bosta_sync_status', 'processing');
1255 $order->delete_meta_data('bosta_sync_error');
1256 $order->save();
1257 }
1258
1259 $formatted_orders = [];
1260 $failed_orders = [];
1261
1262 foreach ($orders as $order) {
1263 $isOrderSyncedWithBosta = !empty($order->get_meta('bosta_tracking_number'));
1264 if (!$isOrderSyncedWithBosta) {
1265 $validation_error = bosta_validate_order_fields($order, $order_action);
1266 if ($validation_error) {
1267 $order_id = '#' . $order->get_id();
1268 bosta_format_failed_order_message($validation_error, $order_id);
1269 // Update order metadata with error
1270 $order->update_meta_data('bosta_sync_status', 'failed');
1271 $order->update_meta_data('bosta_sync_error', $validation_error);
1272 $order->save();
1273 $failed_orders[] = ['order_id' => $order->get_id(), 'error' => $validation_error];
1274 continue;
1275 }
1276 $formatted_orders[] = bosta_format_order_payload($order, $order_action);
1277 } else {
1278 // Already synced - update status to success if not already set
1279 if (empty($order->get_meta('bosta_sync_status'))) {
1280 $order->update_meta_data('bosta_sync_status', 'success');
1281 $order->delete_meta_data('bosta_sync_error');
1282 $order->save();
1283 }
1284 }
1285 }
1286
1287 if (empty($formatted_orders)) {
1288 wp_safe_redirect(add_query_arg(['post_type' => 'shop_order'], admin_url('edit.php')));
1289 exit;
1290 }
1291
1292 // Determine API endpoint based on fulfillment type
1293 $is_fulfillment_sync = isset($order_action['fulfillmentType']) && $order_action['fulfillmentType'] === 'bosta_fulfillment';
1294 $url = BOSTA_ENV_URL_V2 . '/deliveries/bulk';
1295
1296 $chunkSize = 100;
1297 $chunks = array_chunk($formatted_orders, $chunkSize);
1298 $successfulDeliveriesCount = 0;
1299 $allFailedDeliveries = [];
1300
1301 foreach ($chunks as $chunk) {
1302 $body = (object)[
1303 'deliveries' => $chunk,
1304 'deleteFailedDeliveries' => false
1305 ];
1306
1307 $response = bosta_send_api_request('POST', $url, $APIKey, $body);
1308
1309 if (!$response['success']) {
1310 $error_message = $response['error'] ?? $response['message'] ?? 'Unknown error';
1311 bosta_set_transient('bosta_errors', $error_message);
1312 // Update all orders in this chunk to failed status
1313 foreach ($chunk as $order_payload) {
1314 // Convert stdClass to array if needed
1315 if (is_object($order_payload)) {
1316 $order_payload = json_decode(json_encode($order_payload), true);
1317 }
1318
1319 $business_reference = $order_payload['businessReference'] ?? '';
1320 if (strpos($business_reference, 'Woocommerce_') === 0) {
1321 $order_id = str_replace('Woocommerce_', '', $business_reference);
1322 if (is_numeric($order_id)) {
1323 $order = wc_get_order($order_id);
1324 if ($order) {
1325 $order->update_meta_data('bosta_sync_status', 'failed');
1326 $order->update_meta_data('bosta_sync_error', $error_message);
1327 $order->save();
1328 }
1329 }
1330 }
1331 }
1332 continue; // Continue with next chunk instead of returning
1333 }
1334
1335 // Regular sync response structure
1336 if (!$response['body'] || !is_array($response['body']) || !isset($response['body']['data'])) {
1337 $error_message = 'Invalid response structure from API';
1338 bosta_set_transient('bosta_errors', $error_message);
1339 // Update all orders in this chunk to failed status
1340 foreach ($chunk as $order_payload) {
1341 // Convert stdClass to array if needed
1342 if (is_object($order_payload)) {
1343 $order_payload = json_decode(json_encode($order_payload), true);
1344 }
1345
1346 $business_reference = $order_payload['businessReference'] ?? '';
1347 if (strpos($business_reference, 'Woocommerce_') === 0) {
1348 $order_id = str_replace('Woocommerce_', '', $business_reference);
1349 if (is_numeric($order_id)) {
1350 $order = wc_get_order($order_id);
1351 if ($order) {
1352 $order->update_meta_data('bosta_sync_status', 'failed');
1353 $order->update_meta_data('bosta_sync_error', $error_message);
1354 $order->save();
1355 }
1356 }
1357 }
1358 }
1359 continue; // Continue with next chunk instead of returning
1360 }
1361 $data = $response['body']['data'];
1362 $failedDeliveries = $data['failedDeliveries'] ?? [];
1363 $createdDeliveriesIds = $data['createdDeliveriesIds'] ?? $data;
1364
1365 // Update order metadata (this will also clean up sync status and errors for successful orders)
1366 bosta_get_woocommerce_deliveries_data($createdDeliveriesIds, $APIKey);
1367
1368 if (!empty($failedDeliveries)) {
1369 $allFailedDeliveries = array_merge($allFailedDeliveries, $failedDeliveries);
1370 }
1371
1372 $successfulDeliveriesCount += count($createdDeliveriesIds);
1373 }
1374
1375 // Process failed deliveries and update order metadata
1376 if (!empty($allFailedDeliveries)) {
1377 foreach ($allFailedDeliveries as $failed_delivery) {
1378 $business_reference = $failed_delivery['businessReference'] ?? 'Unknown';
1379 $error_message = $failed_delivery['errorMessage'] ?? 'Unknown error';
1380
1381 // Extract order ID from business reference (format: Woocommerce_123)
1382 $order_id = null;
1383 if (strpos($business_reference, 'Woocommerce_') === 0) {
1384 $order_id = str_replace('Woocommerce_', '', $business_reference);
1385 }
1386
1387 if ($order_id && is_numeric($order_id)) {
1388 $order = wc_get_order($order_id);
1389 if ($order) {
1390 // Update order metadata with error
1391 $order->update_meta_data('bosta_sync_status', 'failed');
1392 $order->update_meta_data('bosta_sync_error', $error_message);
1393 $order->save();
1394 }
1395 }
1396
1397 bosta_format_failed_order_message($error_message, $business_reference);
1398 }
1399 }
1400
1401 // Final cleanup: Update any orders still in 'processing' status that have tracking numbers to 'success'
1402 foreach ($orders as $order) {
1403 if ($order->get_meta('bosta_sync_status') === 'processing' && !empty($order->get_meta('bosta_tracking_number'))) {
1404 $order->update_meta_data('bosta_sync_status', 'success');
1405 $order->delete_meta_data('bosta_sync_error');
1406 $order->save();
1407 }
1408 }
1409
1410 if ($successfulDeliveriesCount > 0) {
1411 set_transient('bosta_success_count', $successfulDeliveriesCount, HOUR_IN_SECONDS);
1412 }
1413
1414 wp_safe_redirect(add_query_arg(['post_type' => 'shop_order'], admin_url('edit.php')));
1415 exit;
1416 }
1417
1418 function bosta_handle_print_awbs_bulk_action($params)
1419 {
1420 $APIKey = $params['APIKey'];
1421 $orders = $params['orders'];
1422
1423 $delivery_ids = array_filter(array_map(function ($order) {
1424 return $order->get_meta('bosta_delivery_id');
1425 }, $orders));
1426
1427 if (empty($delivery_ids)) {
1428 $error_message = '<p>No orders have been synced with Bosta for AWB printing</p>';
1429 bosta_set_transient('bosta_errors', $error_message);
1430 return;
1431 }
1432
1433 $url = BOSTA_ENV_URL_V2 . '/deliveries/mass-awb?ids=' . implode(',', $delivery_ids) . '&lang=ar';
1434 $response = bosta_send_api_request('GET', $url, $APIKey);
1435
1436 if (!$response['success']) {
1437 bosta_format_failed_order_message($response['error']);
1438 return;
1439 }
1440
1441 // Check if response body is valid and contains expected structure
1442 if (!$response['body'] || !is_array($response['body']) || !isset($response['body']['data'])) {
1443 $error_message = '<p>Invalid response structure from API</p>';
1444 bosta_set_transient('bosta_errors', $error_message);
1445 return;
1446 }
1447 $pdf_data = base64_decode($response['body']['data'], true);
1448
1449 if ($pdf_data === false) {
1450 $error_message = '<p>Failed to decode PDF data</p>';
1451 bosta_set_transient('bosta_errors', $error_message);
1452 return;
1453 }
1454
1455 bosta_render_pdf($pdf_data);
1456 }
1457
1458 function bosta_handle_auto_sync_bulk_action($params)
1459 {
1460 $APIKey = $params['APIKey'];
1461 $redirect_to = $params['redirect_to'];
1462
1463 // Check if auto sync is enabled
1464 if (!Bosta_Auto_Sync::is_enabled()) {
1465 $error_message = '<p>Auto sync is disabled. Please enable it in the Bosta settings.</p>';
1466 bosta_set_transient('bosta_errors', $error_message);
1467 bosta_redirect_to_orders_page();
1468 return;
1469 }
1470
1471 // Get orders that need to be synced
1472 $order_ids = Bosta_Auto_Sync::get_orders_to_sync(100);
1473
1474 if (empty($order_ids)) {
1475 $message = '<p>No orders found that need to be synced with Bosta.</p>';
1476 bosta_set_transient('bosta_success', $message);
1477 bosta_redirect_to_orders_page();
1478 return;
1479 }
1480
1481 // Sync orders using the auto sync class
1482 $result = Bosta_Auto_Sync::sync_orders_bulk($order_ids);
1483
1484 if ($result['success'] > 0) {
1485 $success_message = '<p>Successfully synced ' . $result['success'] . ' orders with Bosta.</p>';
1486 bosta_set_transient('bosta_success', $success_message);
1487 }
1488
1489 if ($result['failed'] > 0) {
1490 $error_message = '<p>Failed to sync ' . $result['failed'] . ' orders: ' . $result['errors'] . '</p>';
1491 bosta_set_transient('bosta_errors', $error_message);
1492 }
1493
1494 bosta_redirect_to_orders_page();
1495 }
1496
1497 function bosta_handle_fetch_status_bulk_action($params)
1498 {
1499 $APIKey = $params['APIKey'];
1500 $redirect_to = $params['redirect_to'];
1501 $orders = $params['orders'];
1502
1503 $deliveriesIds = [];
1504 foreach ($orders as $order) {
1505 $deliveryId = $order->get_meta('bosta_delivery_id', true);
1506 if (!empty($deliveryId)) {
1507 $deliveriesIds[] = $deliveryId;
1508 }
1509 }
1510
1511 $chunkSize = 50;
1512 $chunks = array_chunk($deliveriesIds, $chunkSize);
1513 foreach ($chunks as $chunk) {
1514 bosta_get_woocommerce_deliveries_data($chunk, $APIKey);
1515 }
1516
1517 if (!empty($redirect_to)) {
1518 wp_safe_redirect($redirect_to);
1519 } else {
1520 wp_safe_redirect(add_query_arg(['post_type' => 'shop_order'], admin_url('edit.php')));
1521 }
1522 exit;
1523 }
1524
1525 function bosta_get_woocommerce_deliveries_data($deliveriesIds, $APIKey)
1526 {
1527 try {
1528 if (!empty($deliveriesIds)) {
1529 $url = BOSTA_ENV_URL_V2 . '/deliveries/woocommerce-data';
1530 $body = (object)[
1531 'deliveriesIds' => $deliveriesIds,
1532 ];
1533
1534 $response = bosta_send_api_request('POST', $url, $APIKey, $body);
1535 if (!$response['success']) {
1536 $errorMessage = $response['error'] ?? '';
1537 throw new Exception($errorMessage);
1538 }
1539
1540 $deliveriesData = $response['body']['data'] ?? [];
1541
1542 $returnedDeliveryIds = [];
1543 foreach ($deliveriesData as $deliveryData) {
1544 $order_id = substr($deliveryData['uniqueBusinessReference'], 3);
1545 $order = wc_get_order($order_id);
1546 if ($order) {
1547 bosta_update_order_metadata($order, $deliveryData);
1548 }
1549 $returnedDeliveryIds[$deliveryData['_id']] = $deliveryData['_id'];
1550 }
1551
1552 if (count($deliveriesIds) !== count($returnedDeliveryIds)) {
1553 foreach ($deliveriesIds as $deliveryId) {
1554 $order = bosta_get_order_by_metadata('bosta_delivery_id', $deliveryId);
1555 if (!isset($returnedDeliveryIds[$deliveryId])) {
1556
1557 if ($order) {
1558 bosta_delete_order_metadata($order);
1559 }
1560 }
1561 }
1562 }
1563 }
1564 } catch (Exception $e) {
1565 error_log('Bosta Plugin: Failed to fetch orders status. Error: ' . $e->getMessage());
1566
1567 $message = 'Bosta Plugin: Failed to fetch orders status. Error: ' . esc_html($e->getMessage());
1568 bosta_set_transient('bosta_errors', "<p>{$message}</p>");
1569 }
1570 }
1571
1572 function bosta_format_order_payload($order, $order_action)
1573 {
1574 $bosta_settings = get_option('woocommerce_bosta_settings');
1575 $productDescription = isset($bosta_settings['ProductDescription']) ? $bosta_settings['ProductDescription'] : array('name');
1576 $includeProductDescriptions = isset($bosta_settings['IncludeProductDescriptions']) ? $bosta_settings['IncludeProductDescriptions'] : 'yes';
1577 $allowToOpenPackage = get_option('woocommerce_bosta_settings')['AllowToOpenPackage'];
1578 $orderRef = get_option('woocommerce_bosta_settings')['OrderRef'];
1579
1580 $newOrder = new stdClass();
1581 $newOrder->id = $order->get_id();
1582 $newOrder->type = $order_action['orderType'];
1583 $newOrder->notes = $order->get_customer_note();
1584 $newOrder->uniqueBusinessReference = "WC_" . $order->get_id();
1585 $newOrder->specs = new stdClass();
1586
1587 // Only include package details if the checkbox is enabled
1588 if ($includeProductDescriptions === 'yes') {
1589 $newOrder->specs->packageDetails = bosta_format_package_details($order, $productDescription);
1590 }
1591
1592 // Only add webhook fields if not running on localhost
1593 $site_url = get_site_url();
1594 if (strpos($site_url, 'localhost') === false && strpos($site_url, '127.0.0.1') === false) {
1595 $newOrder->webhookUrl = $site_url . '/wp-json/bosta/v1/webhook/orders/' . $order->get_id();
1596 $newOrder->webhookCustomHeaders = new stdClass();
1597 $newOrder->webhookCustomHeaders->{"X-Bosta-Secret"} = Bosta_Webhook_Manager::get_webhook_secret();
1598 }
1599
1600 if ($allowToOpenPackage === 'yes') {
1601 $newOrder->allowToOpenPackage = true;
1602 }
1603
1604 if ($orderRef === 'yes') {
1605 $newOrder->businessReference = 'Woocommerce_' . $order->get_id();
1606 }
1607
1608 $newOrder->receiver = bosta_format_receiver_details($order);
1609 $newOrder->{$order_action['addressType']} = bosta_format_address_details($order);
1610 if ($order->get_payment_method() === 'cod') {
1611 $newOrder->cod = (float) $order->get_total();
1612 } else {
1613 $newOrder->cod = 0;
1614 }
1615
1616 $newOrder->goodsInfo = bosta_format_goods_info($order);
1617
1618 // Add fulfillment info if using Bosta fulfillment
1619 if (isset($order_action['fulfillmentType']) && $order_action['fulfillmentType'] === 'bosta_fulfillment') {
1620 $newOrder->fulfillmentInfo = Bosta_Fulfillment::format_fulfillment_info($order);
1621 }
1622
1623 // Add productInfo if all products have bosta_product_id
1624 $productInfo = Bosta_Product_Info::format_product_info($order);
1625 if (!empty($productInfo)) {
1626 $newOrder->productInfo = $productInfo;
1627 }
1628
1629 return $newOrder;
1630 }
1631
1632 function bosta_format_goods_info($order) {
1633 $goodsInfo = new stdClass();
1634
1635 $order_regular_price = 0;
1636 foreach ($order->get_items() as $item) {
1637 $product = $item->get_product();
1638 $product_regular_price = (float) ($product->get_regular_price() ?? 0);
1639 $item_quantity = (float) ($item->get_quantity() ?? 0);
1640 $order_regular_price += $product_regular_price * $item_quantity;
1641 }
1642
1643 $goodsInfo->amount = $order_regular_price;
1644
1645 return $goodsInfo;
1646 }
1647
1648 // Moved to Bosta_Fulfillment class
1649
1650 function bosta_format_package_details($order, $productDescription)
1651 {
1652 $items = $order->get_items();
1653 $itemsCount = 0;
1654 $descArray = [];
1655 $index = 1;
1656
1657 foreach ($items as $item) {
1658 $product = $item->get_product();
1659
1660 // If product type includes bundle or woosb, don't count it
1661 $product_type = $product->get_type();
1662 if (strpos($product_type, 'bundle') === false && strpos($product_type, 'woosb') === false) {
1663 $itemsCount += $item->get_quantity();
1664 }
1665
1666 $descArray[] = bosta_format_order_description($productDescription, $product->get_sku(), $product->get_name(), $item->get_quantity());
1667 $index++;
1668 }
1669
1670 $packageDetails = new stdClass();
1671 $packageDetails->itemsCount = $itemsCount;
1672 $description = implode(", ", $descArray);
1673
1674 // Limit description to 500 characters as per requirements
1675 if (mb_strlen($description) > 500) {
1676 $description = mb_substr($description, 0, 497) . '...';
1677 }
1678
1679 $packageDetails->description = $description;
1680
1681 return $packageDetails;
1682 }
1683
1684 function bosta_format_order_description($productDescription, $sku, $name, $quantity)
1685 {
1686 $desc = "";
1687 $bosta_settings = get_option('woocommerce_bosta_settings');
1688 $maintainOldDescription = isset($bosta_settings['MaintainOldDescription']) ? $bosta_settings['MaintainOldDescription'] : 'no';
1689
1690 $name = html_entity_decode($name, ENT_QUOTES | ENT_HTML5, 'UTF-8');
1691
1692 if (is_array($productDescription)) {
1693 if (in_array('name', $productDescription)) {
1694 $desc .= $name;
1695 }
1696
1697 if ($maintainOldDescription === 'yes') {
1698 // Old format: Product Name [SKU] (Quantity)
1699 if (in_array('sku', $productDescription) && !empty($sku)) {
1700 $desc .= " [$sku]";
1701 }
1702 $desc .= " ($quantity)";
1703 } else {
1704 // New format: Product Name x Quantity (SKU)
1705 $desc .= " x $quantity";
1706 if (in_array('sku', $productDescription) && !empty($sku)) {
1707 $desc .= " ($sku)";
1708 }
1709 }
1710 }
1711 return $desc;
1712 }
1713
1714 function bosta_format_receiver_details($order)
1715 {
1716 $firstname = $order->get_billing_first_name() ?: $order->get_shipping_first_name();
1717 $lastname = $order->get_billing_last_name() ?: $order->get_shipping_last_name();
1718 $receiver = new stdClass();
1719 $receiver->firstName = mb_substr($firstname, 0, 50);
1720 $receiver->lastName = $lastname;
1721 $receiver->phone = $order->get_billing_phone() ?: $order->get_shipping_phone();
1722
1723 // Add second phone number if billing_phone_2 field exists and has a value
1724 $second_phone = $order->get_meta('_billing_phone_2', true);
1725 if (!empty($second_phone)) {
1726 $receiver->secondPhone = $second_phone;
1727 }
1728
1729 return $receiver;
1730 }
1731
1732 function bosta_format_address_details($order)
1733 {
1734 $states = WC()->countries->get_states('EG');
1735 $address = new stdClass();
1736
1737 $address->firstLine = $order->get_billing_address_1() ?: $order->get_shipping_address_1();
1738 $address->secondLine = $order->get_billing_address_2() ?: $order->get_shipping_address_2();
1739
1740 $city_code = $order->get_billing_state() ?: $order->get_shipping_state();
1741 if (isset($city_code) && isset($states[$city_code])) {
1742 $address->city = $states[$city_code];
1743 }
1744
1745 $district_id = $order->get_meta('_billing_area');
1746 if (!empty($district_id)) {
1747 $address->districtId = $district_id;
1748 }
1749 return $address;
1750 }
1751
1752 //endregion
1753
1754 //region Bosta Update and Delete Actions
1755
1756 add_action('wp_trash_post', 'bosta_custom_delete_function');
1757 function bosta_custom_delete_function($id)
1758 {
1759 $screen = get_current_screen();
1760 if (!isset($screen->post_type) || 'shop_order' != $screen->post_type) {
1761 return;
1762 }
1763
1764 $order = wc_get_order($id);
1765 if (!$order) {
1766 return;
1767 }
1768
1769 $bostaStatus = $order->get_meta('bosta_status', true);
1770 if ($bostaStatus != 'Pickup requested' && $bostaStatus != 'Created') {
1771 $error_message = '<p>Failed to delete order in the current Bosta Status</p>';
1772 bosta_set_transient('bosta_errors', $error_message);
1773 return;
1774 }
1775
1776 $APIKey = bosta_get_api_key();
1777 if (empty($APIKey)) {
1778 $error_message = '<p>API Key is required to be able to sync with Bosta</p>';
1779 bosta_set_transient('bosta_errors', $error_message);
1780 bosta_redirect_to_settings_page();
1781 return;
1782 }
1783
1784 $deliveryId = $order->get_meta('bosta_delivery_id', true);
1785
1786 $url = BOSTA_ENV_URL_V2 . '/deliveries/business/' . $deliveryId . '/terminate';
1787
1788 $response = bosta_send_api_request('DELETE', $url, $APIKey);
1789
1790 if (!$response['success']) {
1791 bosta_format_failed_order_message($response['error'], $id);
1792 } else {
1793 bosta_delete_order_metadata($order);
1794 $success_count = get_transient('bosta_success_count') ?: 0;
1795 set_transient('bosta_success_count', ++$success_count, HOUR_IN_SECONDS);
1796 }
1797 }
1798
1799 add_action('woocommerce_update_order', 'bosta_enqueue_order_update_logic', 10, 1);
1800 function bosta_enqueue_order_update_logic($id)
1801 {
1802 if (is_admin() && isset($_POST['action']) && ($_POST['action'] === 'edit_order' || $_POST['action'] === 'editpost')) {
1803 set_transient('deferred_order_update', $id, 10);
1804 add_action('shutdown', 'bosta_handle_order_update_action');
1805 }
1806 }
1807
1808 function bosta_handle_order_update_action()
1809 {
1810 $bosta_settings = get_option('woocommerce_bosta_settings', []);
1811 $disable_sync = $bosta_settings['DisableSyncOrderUpdates'] ?? 'no';
1812 if ($disable_sync === 'yes') {
1813 return;
1814 }
1815
1816 $id = get_transient('deferred_order_update');
1817
1818 if (!$id) {
1819 return;
1820 }
1821
1822 $order = wc_get_order($id);
1823 if (!$order) {
1824 return;
1825 }
1826
1827 $bostaStatus = $order->get_meta('bosta_status', true);
1828 if (empty($bostaStatus)) {
1829 return;
1830 }
1831
1832 if ($bostaStatus != 'Pickup requested' && $bostaStatus != 'Created') {
1833 $error_message = '<p>Failed to update order in the current Bosta Status</p>';
1834 bosta_set_transient('bosta_errors', $error_message);
1835 return;
1836 }
1837
1838 $APIKey = bosta_get_api_key();
1839 if (empty($APIKey)) {
1840 $error_message = '<p>API Key is required to be able to sync with Bosta</p>';
1841 bosta_set_transient('bosta_errors', $error_message);
1842 // bosta_redirect_to_settings_page();
1843 return;
1844 }
1845
1846 $deliveryId = $order->get_meta('bosta_delivery_id', true);
1847 $newOrder = bosta_format_updated_order($order);
1848
1849 $url = BOSTA_ENV_URL_V2 . '/deliveries/business/' . $deliveryId;
1850
1851 $response = bosta_send_api_request('PUT', $url, $APIKey, $newOrder);
1852
1853 if (!$response['success']) {
1854 bosta_format_failed_order_message($response['error'], $id);
1855 } else {
1856 set_transient('bosta_success_count', 1, HOUR_IN_SECONDS);
1857 }
1858 }
1859
1860 function bosta_format_updated_order($order)
1861 {
1862 $states = WC()->countries->get_states('EG');
1863 $newOrder = new stdClass();
1864
1865 $newOrder->notes = $order->get_customer_note();
1866
1867 $fullname = $order->get_billing_first_name() . ' ' . $order->get_billing_last_name();
1868 $newOrder->receiver = (object) [
1869 'fullName' => mb_substr($fullname, 0, 50),
1870 'phone' => $order->get_billing_phone(),
1871 ];
1872
1873 $city_code = $order->get_billing_state();
1874 $city_name = null;
1875 if (isset($city_code) && isset($states[$city_code])) {
1876 $city_name = $states[$city_code];
1877 }
1878
1879 $district_id = $order->get_meta('_billing_area');
1880 $district_id = !empty($district_id) ? $district_id : null;
1881
1882 $newOrder->dropOffAddress = (object) [
1883 'firstLine' => $order->get_billing_address_1(),
1884 'secondLine' => $order->get_billing_address_2(),
1885 'city' => $city_name,
1886 'districtId' => $district_id,
1887 ];
1888
1889 // Add second phone number if billing_phone_2 field exists and has a value
1890 $second_phone = $order->get_meta('_billing_phone_2', true);
1891 if (!empty($second_phone)) {
1892 $newOrder->receiver->secondPhone = $second_phone;
1893 }
1894
1895 $newOrder->goodsInfo = bosta_format_goods_info($order);
1896
1897 $bosta_settings = get_option('woocommerce_bosta_settings');
1898 $productDescription = isset($bosta_settings['ProductDescription']) ? $bosta_settings['ProductDescription'] : array('name');
1899 $includeProductDescriptions = isset($bosta_settings['IncludeProductDescriptions']) ? $bosta_settings['IncludeProductDescriptions'] : 'yes';
1900 $newOrder->specs = new stdClass();
1901
1902 // Check if this is a fulfillment order
1903 $is_fulfillment = $order->get_meta('bosta_is_fulfillment');
1904
1905 // Only include package details if the checkbox is enabled AND it's not a fulfillment order
1906 if ($includeProductDescriptions === 'yes' && !$is_fulfillment) {
1907 $newOrder->specs->packageDetails = bosta_format_package_details($order, $productDescription);
1908 }
1909
1910 // Add COD handling
1911 if ($order->get_payment_method() === 'cod') {
1912 $newOrder->cod = (float) $order->get_total();
1913 } else {
1914 $newOrder->cod = 0;
1915 }
1916
1917 return $newOrder;
1918 }
1919
1920
1921 //endregion
1922
1923 //region Bosta Order Page Custom Buttons
1924
1925 function bosta_render_custom_buttons($send_all_nonce, $fetch_status)
1926 {
1927 $auto_sync_nonce = wp_create_nonce('bosta_auto_sync_nonce');
1928 $api_key = bosta_get_api_key();
1929 $is_fulfillment_enabled = false;
1930
1931 // Check if business has fulfillment enabled
1932 $is_fulfillment_enabled = Bosta_Fulfillment_Cache::is_fulfillment_enabled();
1933
1934 ?>
1935 <div class="alignleft bosta_custom_buttons_div">
1936 <div class="rightDiv">
1937 <button type="submit" name="create_pickup" class="orders-button bosta_custom_button" value="yes">Create Pickup</button>
1938 <button type="button" id="send-all-orders-btn" class="orders-button bosta_custom_button">Send all Orders to Bosta</button>
1939 <!-- <button type="submit" name="auto_sync_orders" class="orders-button bosta_custom_button" value="yes">Auto Sync Orders</button> -->
1940 <input type="hidden" name="bosta_send_all_nonce_field" value="<?php echo esc_attr($send_all_nonce); ?>">
1941 <input type="hidden" name="bosta_auto_sync_nonce_field" value="<?php echo esc_attr($auto_sync_nonce); ?>">
1942 </div>
1943 <div class="leftDiv">
1944 <button type="submit" name="fetch_status" class="danger-button bosta_custom_button" value="yes">
1945 <img class="refreshIcon" src="<?php echo esc_url(plugins_url("assets/images/refreshIcon.png", __FILE__)); ?>" alt="Bosta"> Refresh Bosta Status
1946 </button>
1947 <input type="hidden" name="bosta_fetch_status_nonce_field" value="<?php echo esc_attr($fetch_status); ?>">
1948 </div>
1949 <input type="hidden" name="page_num" value="<?php echo esc_attr($_GET['paged'] ?? '1'); ?>">
1950 </div>
1951
1952 <?php
1953 // Render fulfillment modal using modular class
1954 if ($is_fulfillment_enabled) {
1955 Bosta_Fulfillment_UI::render_fulfillment_modal();
1956 }
1957 ?>
1958 <?php
1959 }
1960
1961 function bosta_render_status_search_tags()
1962 {
1963 $current_sync_status = isset($_GET['bosta_sync_status']) ? sanitize_text_field($_GET['bosta_sync_status']) : '';
1964 $current_bosta_status = isset($_GET['s']) ? sanitize_text_field($_GET['s']) : '';
1965 $current_ranking_status = isset($_GET['bosta_ranking_status']) ? sanitize_text_field($_GET['bosta_ranking_status']) : '';
1966 ?>
1967 <div class="alignleft">
1968 <p class="bosta_custom_p">Filter with Bosta status:</p>
1969 </div>
1970 <div class="alignleft bosta_status_search_tags">
1971 <select id="bosta_status_filter" class="bosta-filter-select">
1972 <option value="">Select Bosta Status</option>
1973 <option value="created" <?php echo ($current_bosta_status === 'created') ? 'selected' : ''; ?>>Created</option>
1974 <option value="delivered" <?php echo ($current_bosta_status === 'delivered') ? 'selected' : ''; ?>>Delivered</option>
1975 <option value="terminated" <?php echo ($current_bosta_status === 'terminated') ? 'selected' : ''; ?>>Terminated</option>
1976 <option value="returned" <?php echo ($current_bosta_status === 'returned') ? 'selected' : ''; ?>>Returned</option>
1977 </select>
1978 </div>
1979 <div class="alignleft">
1980 <p class="bosta_custom_p">Filter with Bosta Sync Status:</p>
1981 </div>
1982 <div class="alignleft bosta_status_search_tags">
1983 <select id="bosta_sync_status_filter" class="bosta-filter-select">
1984 <option value="">Select Sync Status</option>
1985 <option value="none" <?php echo ($current_sync_status === 'none') ? 'selected' : ''; ?>>Not Synced</option>
1986 <option value="failed" <?php echo ($current_sync_status === 'failed') ? 'selected' : ''; ?>>Failed</option>
1987 <option value="synced" <?php echo ($current_sync_status === 'synced') ? 'selected' : ''; ?>>Synced</option>
1988 </select>
1989 </div>
1990 <div class="alignleft">
1991 <p class="bosta_custom_p">Filter with Consignee Ranking:</p>
1992 </div>
1993 <div class="alignleft bosta_status_search_tags">
1994 <select id="bosta_ranking_status_filter" class="bosta-filter-select">
1995 <option value="">Select Ranking Status</option>
1996 <option value="new_customer" <?php echo ($current_ranking_status === 'new_customer') ? 'selected' : ''; ?>>New Customer</option>
1997 <option value="high_rate" <?php echo ($current_ranking_status === 'high_rate') ? 'selected' : ''; ?>>High Acceptance Rate</option>
1998 <option value="average_rate" <?php echo ($current_ranking_status === 'average_rate') ? 'selected' : ''; ?>>Average Acceptance Rate</option>
1999 <option value="low_rate" <?php echo ($current_ranking_status === 'low_rate') ? 'selected' : ''; ?>>Low Acceptance Rate</option>
2000 </select>
2001 </div>
2002 <div class="alignleft bosta_status_search_tags">
2003 <button type="button" id="bosta_apply_filters" class="bosta-filter-button">Apply Filters</button>
2004 <?php if (!empty($current_sync_status) || !empty($current_bosta_status) || !empty($current_ranking_status)): ?>
2005 <button type="button" id="bosta_clear_filters" class="bosta-clear-button">Clear Filters</button>
2006 <?php endif; ?>
2007 </div>
2008
2009 <script>
2010 document.addEventListener('DOMContentLoaded', function() {
2011 // Apply filters button
2012 document.getElementById('bosta_apply_filters').addEventListener('click', function() {
2013 var bostaStatus = document.getElementById('bosta_status_filter').value;
2014 var syncStatus = document.getElementById('bosta_sync_status_filter').value;
2015 var rankingStatus = document.getElementById('bosta_ranking_status_filter').value;
2016
2017 var url = 'edit.php?post_type=shop_order&paged=1';
2018
2019 if (bostaStatus) {
2020 url += '&s=' + encodeURIComponent(bostaStatus);
2021 }
2022
2023 if (syncStatus) {
2024 url += '&bosta_sync_status=' + encodeURIComponent(syncStatus);
2025 }
2026
2027 if (rankingStatus) {
2028 url += '&bosta_ranking_status=' + encodeURIComponent(rankingStatus);
2029 }
2030
2031 window.location.href = url;
2032 });
2033
2034 // Clear filters button
2035 document.getElementById('bosta_clear_filters').addEventListener('click', function() {
2036 window.location.href = 'edit.php?post_type=shop_order&paged=1';
2037 });
2038
2039 // Allow Enter key to apply filters
2040 document.getElementById('bosta_status_filter').addEventListener('keypress', function(e) {
2041 if (e.key === 'Enter') {
2042 document.getElementById('bosta_apply_filters').click();
2043 }
2044 });
2045
2046 document.getElementById('bosta_sync_status_filter').addEventListener('keypress', function(e) {
2047 if (e.key === 'Enter') {
2048 document.getElementById('bosta_apply_filters').click();
2049 }
2050 });
2051
2052 document.getElementById('bosta_ranking_status_filter').addEventListener('keypress', function(e) {
2053 if (e.key === 'Enter') {
2054 document.getElementById('bosta_apply_filters').click();
2055 }
2056 });
2057 });
2058 </script>
2059 <?php
2060 }
2061
2062 add_filter('woocommerce_order_table_search_query_meta_keys', 'woocommerce_shop_order_search_order_total');
2063 add_filter('woocommerce_shop_order_search_fields', 'woocommerce_shop_order_search_order_total');
2064 function woocommerce_shop_order_search_order_total($search_fields)
2065 {
2066 $search_fields[] = 'bosta_tracking_number';
2067 $search_fields[] = 'bosta_customer_info';
2068 $search_fields[] = 'bosta_status';
2069 $search_fields[] = 'bosta_sync_status';
2070
2071 return $search_fields;
2072 }
2073
2074 /**
2075 * Filter orders by Bosta Sync Status
2076 *
2077 * This function filters orders based on their Bosta sync status:
2078 * - 'synced': Orders that have been successfully synced with Bosta (have tracking number)
2079 * - 'failed': Orders that failed to sync with Bosta
2080 * - 'none': Orders that haven't been synced yet
2081 *
2082 * @param array $query_args The query arguments for the orders list
2083 * @return array Modified query arguments
2084 */
2085 function bosta_filter_orders_by_sync_status($query_args)
2086 {
2087 if (!isset($_GET['bosta_sync_status']) || empty($_GET['bosta_sync_status'])) {
2088 return $query_args;
2089 }
2090
2091 $sync_status = sanitize_text_field($_GET['bosta_sync_status']);
2092
2093 // Initialize meta_query if it doesn't exist
2094 if (!isset($query_args['meta_query'])) {
2095 $query_args['meta_query'] = array();
2096 }
2097
2098 switch ($sync_status) {
2099 case 'synced':
2100 // Orders that have a tracking number (successfully synced)
2101 $query_args['meta_query'][] = array(
2102 'relation' => 'AND',
2103 array(
2104 'key' => 'bosta_tracking_number',
2105 'compare' => 'EXISTS'
2106 ),
2107 array(
2108 'key' => 'bosta_tracking_number',
2109 'compare' => '!=',
2110 'value' => ''
2111 )
2112 );
2113 break;
2114
2115 case 'failed':
2116 // Orders that have failed sync status
2117 $query_args['meta_query'][] = array(
2118 'key' => 'bosta_sync_status',
2119 'value' => 'failed',
2120 'compare' => '='
2121 );
2122 break;
2123
2124 case 'none':
2125 // Orders that have no tracking number and no sync status (not synced)
2126 $query_args['meta_query'][] = array(
2127 'relation' => 'AND',
2128 array(
2129 'key' => 'bosta_tracking_number',
2130 'compare' => 'NOT EXISTS'
2131 ),
2132 array(
2133 'key' => 'bosta_sync_status',
2134 'compare' => 'NOT EXISTS'
2135 )
2136 );
2137 break;
2138 }
2139
2140 return $query_args;
2141 }
2142
2143 // Filter orders by Bosta Consignee Ranking - HPOS (High-Performance Order Storage) system
2144 function bosta_filter_orders_by_ranking_status($query_args)
2145 {
2146 if (!isset($_GET['bosta_ranking_status']) || empty($_GET['bosta_ranking_status'])) {
2147 return $query_args;
2148 }
2149
2150 $ranking_status = sanitize_text_field($_GET['bosta_ranking_status']);
2151
2152 // Store the ranking status for our custom filter
2153 set_transient('bosta_current_ranking_filter', $ranking_status, 60);
2154
2155 // Add a marker to indicate we need custom filtering
2156 $query_args['meta_query'][] = array(
2157 'key' => 'bosta_ranking_filter_marker',
2158 'value' => $ranking_status,
2159 'compare' => 'EXISTS'
2160 );
2161
2162 return $query_args;
2163 }
2164
2165 // Custom filter to handle ranking status filtering after query execution
2166 add_filter('woocommerce_order_list_table_prepare_items_query_args', 'bosta_apply_ranking_filter_post_query', 20, 1);
2167 add_filter('woocommerce_shop_order_list_table_prepare_items_query_args', 'bosta_apply_ranking_filter_post_query', 20, 1);
2168
2169 function bosta_apply_ranking_filter_post_query($query_args)
2170 {
2171 $ranking_status = get_transient('bosta_current_ranking_filter');
2172 if (!$ranking_status) {
2173 return $query_args;
2174 }
2175
2176 // Remove our marker filter
2177 $query_args['meta_query'] = array_filter($query_args['meta_query'], function($meta) {
2178 return $meta['key'] !== 'bosta_ranking_filter_marker';
2179 });
2180
2181 // Get all orders first, then filter them based on ranking data
2182 $all_orders = wc_get_orders(array(
2183 'limit' => -1,
2184 'return' => 'ids',
2185 'status' => array_keys(wc_get_order_statuses()),
2186 'meta_query' => $query_args['meta_query']
2187 ));
2188
2189 $filtered_order_ids = array();
2190
2191 foreach ($all_orders as $order_id) {
2192 $order = wc_get_order($order_id);
2193 if (!$order) continue;
2194
2195 $ranking_data = $order->get_meta('bosta_consignee_ranking', true);
2196
2197 switch ($ranking_status) {
2198 case 'new_customer':
2199 // Include both new customers and orders with no ranking data
2200 if (empty($ranking_data) || (!empty($ranking_data) && isset($ranking_data['isNewCustomer']) && $ranking_data['isNewCustomer'] === true)) {
2201 $filtered_order_ids[] = $order_id;
2202 }
2203 break;
2204
2205 case 'high_rate':
2206 if (!empty($ranking_data) && isset($ranking_data['deliverySuccessRate']) &&
2207 isset($ranking_data['isNewCustomer']) && $ranking_data['isNewCustomer'] === false &&
2208 $ranking_data['deliverySuccessRate'] > 70) {
2209 $filtered_order_ids[] = $order_id;
2210 }
2211 break;
2212
2213 case 'average_rate':
2214 if (!empty($ranking_data) && isset($ranking_data['deliverySuccessRate']) &&
2215 isset($ranking_data['isNewCustomer']) && $ranking_data['isNewCustomer'] === false &&
2216 $ranking_data['deliverySuccessRate'] >= 40 && $ranking_data['deliverySuccessRate'] <= 70) {
2217 $filtered_order_ids[] = $order_id;
2218 }
2219 break;
2220
2221 case 'low_rate':
2222 if (!empty($ranking_data) && isset($ranking_data['deliverySuccessRate']) &&
2223 isset($ranking_data['isNewCustomer']) && $ranking_data['isNewCustomer'] === false &&
2224 $ranking_data['deliverySuccessRate'] < 40) {
2225 $filtered_order_ids[] = $order_id;
2226 }
2227 break;
2228 }
2229 }
2230
2231 // Apply the filtered order IDs
2232 if (!empty($filtered_order_ids)) {
2233 $query_args['post__in'] = $filtered_order_ids;
2234 $query_args['meta_query'] = array(); // Clear meta query since we're filtering by IDs
2235 } else {
2236 // No orders match, return empty result
2237 $query_args['post__in'] = array(0);
2238 $query_args['meta_query'] = array();
2239 }
2240
2241 // Clean up transient
2242 delete_transient('bosta_current_ranking_filter');
2243
2244 return $query_args;
2245 }
2246
2247 // Filter orders by Bosta Sync Status - HPOS (High-Performance Order Storage) system
2248 add_filter('woocommerce_order_list_table_prepare_items_query_args', 'bosta_filter_orders_by_sync_status', 10, 1);
2249 add_filter('woocommerce_shop_order_list_table_prepare_items_query_args', 'bosta_filter_orders_by_sync_status', 10, 1);
2250
2251 // Filter orders by Bosta Consignee Ranking - HPOS (High-Performance Order Storage) system
2252 add_filter('woocommerce_order_list_table_prepare_items_query_args', 'bosta_filter_orders_by_ranking_status', 10, 1);
2253 add_filter('woocommerce_shop_order_list_table_prepare_items_query_args', 'bosta_filter_orders_by_ranking_status', 10, 1);
2254
2255 // Legacy support for post-based orders table
2256 add_action('pre_get_posts', 'bosta_filter_orders_by_sync_status_legacy');
2257 /**
2258 * Legacy filter for post-based orders table
2259 *
2260 * This function provides the same filtering functionality for the legacy post-based
2261 * orders table system used in older WooCommerce versions.
2262 *
2263 * @param WP_Query $query The WordPress query object
2264 */
2265 function bosta_filter_orders_by_sync_status_legacy($query)
2266 {
2267 // Only apply to admin orders list
2268 if (!is_admin() || !$query->is_main_query()) {
2269 return;
2270 }
2271
2272 $screen = get_current_screen();
2273 if (!$screen || $screen->post_type !== 'shop_order') {
2274 return;
2275 }
2276
2277 if (!isset($_GET['bosta_sync_status']) || empty($_GET['bosta_sync_status'])) {
2278 return;
2279 }
2280
2281 $sync_status = sanitize_text_field($_GET['bosta_sync_status']);
2282
2283 // Initialize meta_query if it doesn't exist
2284 if (!isset($query->query_vars['meta_query'])) {
2285 $query->query_vars['meta_query'] = array();
2286 }
2287
2288 switch ($sync_status) {
2289 case 'synced':
2290 // Orders that have a tracking number (successfully synced)
2291 $query->query_vars['meta_query'][] = array(
2292 'relation' => 'AND',
2293 array(
2294 'key' => 'bosta_tracking_number',
2295 'compare' => 'EXISTS'
2296 ),
2297 array(
2298 'key' => 'bosta_tracking_number',
2299 'compare' => '!=',
2300 'value' => ''
2301 )
2302 );
2303 break;
2304
2305 case 'failed':
2306 // Orders that have failed sync status
2307 $query->query_vars['meta_query'][] = array(
2308 'key' => 'bosta_sync_status',
2309 'value' => 'failed',
2310 'compare' => '='
2311 );
2312 break;
2313
2314 case 'none':
2315 // Orders that have no tracking number and no sync status (not synced)
2316 $query->query_vars['meta_query'][] = array(
2317 'relation' => 'AND',
2318 array(
2319 'key' => 'bosta_tracking_number',
2320 'compare' => 'NOT EXISTS'
2321 ),
2322 array(
2323 'key' => 'bosta_sync_status',
2324 'compare' => 'NOT EXISTS'
2325 )
2326 );
2327 break;
2328 }
2329 }
2330
2331 // Legacy support for post-based orders table - Consignee Ranking
2332 add_action('pre_get_posts', 'bosta_filter_orders_by_ranking_status_legacy');
2333 /**
2334 * Legacy filter for post-based orders table - Consignee Ranking
2335 *
2336 * This function provides the same filtering functionality for the legacy post-based
2337 * orders table system used in older WooCommerce versions.
2338 *
2339 * @param WP_Query $query The WordPress query object
2340 */
2341 function bosta_filter_orders_by_ranking_status_legacy($query)
2342 {
2343 // Only apply to admin orders list
2344 if (!is_admin() || !$query->is_main_query()) {
2345 return;
2346 }
2347
2348 $screen = get_current_screen();
2349 if (!$screen || $screen->post_type !== 'shop_order') {
2350 return;
2351 }
2352
2353 if (!isset($_GET['bosta_ranking_status']) || empty($_GET['bosta_ranking_status'])) {
2354 return;
2355 }
2356
2357 $ranking_status = sanitize_text_field($_GET['bosta_ranking_status']);
2358
2359 // Store the ranking status for our custom filter
2360 set_transient('bosta_current_ranking_filter_legacy', $ranking_status, 60);
2361
2362 // Get all orders first, then filter them based on ranking data
2363 $all_orders = get_posts(array(
2364 'post_type' => 'shop_order',
2365 'post_status' => array_keys(wc_get_order_statuses()),
2366 'numberposts' => -1,
2367 'fields' => 'ids',
2368 'meta_query' => $query->query_vars['meta_query'] ?? array()
2369 ));
2370
2371 $filtered_order_ids = array();
2372
2373 foreach ($all_orders as $order_id) {
2374 $order = wc_get_order($order_id);
2375 if (!$order) continue;
2376
2377 $ranking_data = $order->get_meta('bosta_consignee_ranking', true);
2378
2379 switch ($ranking_status) {
2380 case 'new_customer':
2381 // Include both new customers and orders with no ranking data
2382 if (empty($ranking_data) || (!empty($ranking_data) && isset($ranking_data['isNewCustomer']) && $ranking_data['isNewCustomer'] === true)) {
2383 $filtered_order_ids[] = $order_id;
2384 }
2385 break;
2386
2387 case 'high_rate':
2388 if (!empty($ranking_data) && isset($ranking_data['deliverySuccessRate']) &&
2389 isset($ranking_data['isNewCustomer']) && $ranking_data['isNewCustomer'] === false &&
2390 $ranking_data['deliverySuccessRate'] > 70) {
2391 $filtered_order_ids[] = $order_id;
2392 }
2393 break;
2394
2395 case 'average_rate':
2396 if (!empty($ranking_data) && isset($ranking_data['deliverySuccessRate']) &&
2397 isset($ranking_data['isNewCustomer']) && $ranking_data['isNewCustomer'] === false &&
2398 $ranking_data['deliverySuccessRate'] >= 40 && $ranking_data['deliverySuccessRate'] <= 70) {
2399 $filtered_order_ids[] = $order_id;
2400 }
2401 break;
2402
2403 case 'low_rate':
2404 if (!empty($ranking_data) && isset($ranking_data['deliverySuccessRate']) &&
2405 isset($ranking_data['isNewCustomer']) && $ranking_data['isNewCustomer'] === false &&
2406 $ranking_data['deliverySuccessRate'] < 40) {
2407 $filtered_order_ids[] = $order_id;
2408 }
2409 break;
2410 }
2411 }
2412
2413 // Apply the filtered order IDs
2414 if (!empty($filtered_order_ids)) {
2415 $query->set('post__in', $filtered_order_ids);
2416 $query->set('meta_query', array()); // Clear meta query since we're filtering by IDs
2417 } else {
2418 // No orders match, return empty result
2419 $query->set('post__in', array(0));
2420 $query->set('meta_query', array());
2421 }
2422
2423 // Clean up transient
2424 delete_transient('bosta_current_ranking_filter_legacy');
2425 }
2426
2427 // Handle form submissions at init stage
2428 function bosta_handle_form_submissions() {
2429 // Check if we're on the orders page and have a send_all_orders action
2430 if (isset($_GET['send_all_orders']) && $_GET['send_all_orders'] === 'yes') {
2431 // Verify nonce
2432 $nonce_field = 'bosta_send_all_nonce_field';
2433 if (!isset($_GET[$nonce_field]) || !wp_verify_nonce($_GET[$nonce_field], 'bosta_send_all_nonce')) {
2434 wp_die('Security check failed');
2435 }
2436
2437 // Get fulfillment type
2438 $fulfillment_type = isset($_GET['fulfillment_type']) ? sanitize_text_field($_GET['fulfillment_type']) : null;
2439
2440 // Handle the action
2441 bosta_handle_custom_bulk_action(
2442 'bosta_send_all_nonce',
2443 $nonce_field,
2444 'sync_to_bosta',
2445 $fulfillment_type
2446 );
2447
2448 // Redirect to prevent form resubmission
2449 $redirect_url = remove_query_arg(['send_all_orders', 'fulfillment_type', $nonce_field], $_SERVER['REQUEST_URI']);
2450 wp_safe_redirect($redirect_url);
2451 exit;
2452 }
2453 }
2454
2455 add_action('woocommerce_order_list_table_extra_tablenav', 'bosta_add_extra_tablenav_components_hpos', 20, 2);
2456 function bosta_add_extra_tablenav_components_hpos($post_type, $which)
2457 {
2458 if ('shop_order' !== $post_type || 'top' !== $which) {
2459 return;
2460 }
2461
2462 $nonces = [
2463 'send_all' => wp_create_nonce('bosta_send_all_nonce'),
2464 'fetch_status' => wp_create_nonce('bosta_fetch_status_nonce'),
2465 ];
2466
2467 // Handle other actions that might still be processed here
2468 $action_handlers = [
2469 'create_pickup' => function () {
2470 $redirect_url = add_query_arg('page', 'bosta-woocommerce-create-edit-pickup', admin_url('admin.php'));
2471 wp_safe_redirect($redirect_url);
2472 exit;
2473 },
2474 'auto_sync_orders' => function () {
2475 bosta_handle_custom_bulk_action(
2476 'bosta_auto_sync_nonce',
2477 'bosta_auto_sync_nonce_field',
2478 'auto_sync_orders'
2479 );
2480 },
2481 'fetch_status' => function () {
2482 bosta_handle_custom_bulk_action(
2483 'bosta_fetch_status_nonce',
2484 'bosta_fetch_status_nonce_field',
2485 'fetch_latest_status'
2486 );
2487 },
2488 ];
2489
2490 foreach ($action_handlers as $action => $handler) {
2491 $value = isset($_GET[$action]) ? sanitize_text_field($_GET[$action]) : null;
2492 if ($value === 'yes') {
2493 $handler();
2494 break;
2495 }
2496 }
2497
2498 bosta_render_custom_buttons($nonces['send_all'], $nonces['fetch_status']);
2499 bosta_render_status_search_tags();
2500 }
2501
2502 add_action('manage_posts_extra_tablenav', 'bosta_add_extra_tablenav_components', 20);
2503 function bosta_add_extra_tablenav_components($which)
2504 {
2505 $screen = get_current_screen();
2506 if ($screen->post_type === 'product' && $which === 'top') {
2507 bosta_add_product_sync_button();
2508 } else {
2509 bosta_add_extra_tablenav_components_hpos($screen->post_type, $which);
2510 }
2511 }
2512
2513 /**
2514 * Add product sync button to products listing page
2515 */
2516 function bosta_add_product_sync_button() {
2517 $api_key = bosta_get_api_key();
2518 if (empty($api_key)) {
2519 return;
2520 }
2521
2522 // Hide button only if auto-sync is enabled AND initial sync via button has been completed
2523 $is_auto_sync_enabled = Bosta_Product_Sync::is_auto_sync_enabled();
2524 $initial_sync_completed = get_option('bosta_initial_product_sync_completed', false);
2525
2526 if ($is_auto_sync_enabled && $initial_sync_completed) {
2527 return;
2528 }
2529
2530 ?>
2531 <div class="alignleft bosta_product_sync_button_div" style="margin: 10px 0;">
2532 <button type="button" id="sync-products-to-bosta-btn" class="button button-primary">
2533 <?php _e('Sync my products to Bosta', 'bosta'); ?>
2534 </button>
2535 </div>
2536 <?php
2537 }
2538
2539 /**
2540 * Enqueue product sync CSS on products listing page
2541 */
2542 add_action('admin_enqueue_scripts', 'bosta_enqueue_product_sync_styles');
2543 function bosta_enqueue_product_sync_styles($hook) {
2544 $screen = get_current_screen();
2545 if (!$screen || $screen->post_type !== 'product' || $screen->base !== 'edit') {
2546 return;
2547 }
2548
2549 wp_enqueue_style(
2550 'bosta-product-sync-css',
2551 plugins_url('Css/product-sync.css', __FILE__),
2552 array(),
2553 filemtime(plugin_dir_path(__FILE__) . 'Css/product-sync.css')
2554 );
2555 }
2556
2557 /**
2558 * Get total count of products for modal display
2559 * Since we sync all products (create unsynced and update synced), we show total count
2560 */
2561 function bosta_get_unsynced_products_count() {
2562 $products = Bosta_Product_Sync::get_all_products();
2563 return count($products);
2564 }
2565
2566 /**
2567 * Add product sync modal and JavaScript to products listing page
2568 */
2569 add_action('admin_footer', 'bosta_add_product_sync_modal');
2570 function bosta_add_product_sync_modal() {
2571 $screen = get_current_screen();
2572 if (!$screen || $screen->post_type !== 'product' || $screen->base !== 'edit') {
2573 return;
2574 }
2575
2576 $api_key = bosta_get_api_key();
2577 if (empty($api_key)) {
2578 return;
2579 }
2580
2581 // Get total count of products (we sync all products: create unsynced and update synced)
2582 $product_count = bosta_get_unsynced_products_count();
2583
2584 ?>
2585 <!-- Product Sync Confirmation Modal -->
2586 <div id="bosta-product-sync-modal" class="bosta-product-sync-modal" style="display: none;">
2587 <div class="bosta-product-sync-modal-overlay"></div>
2588 <div class="bosta-product-sync-modal-container">
2589 <div class="bosta-product-sync-modal-header">
2590 <h3><?php _e('Syncing to Bosta', 'bosta'); ?></h3>
2591 <button type="button" class="bosta-product-sync-modal-close" aria-label="<?php _e('Close', 'bosta'); ?>">
2592 <span>&times;</span>
2593 </button>
2594 </div>
2595 <div class="bosta-product-sync-modal-body">
2596 <p class="bosta-product-sync-modal-question"><?php printf(__('Syncing %d Products', 'bosta'), $product_count); ?></p>
2597 <p class="bosta-product-sync-modal-description"><?php _e('You\'re about to sync your products to Bosta. Once activated, product updates sync automatically. You can disable this anytime from plugin settings.', 'bosta'); ?></p>
2598 </div>
2599 <div class="bosta-product-sync-modal-footer">
2600 <button type="button" id="product-sync-modal-cancel" class="button bosta-product-sync-btn-cancel"><?php _e('Cancel', 'bosta'); ?></button>
2601 <button type="button" id="product-sync-modal-confirm" class="button button-primary bosta-product-sync-btn-confirm"><?php _e('Start Syncing', 'bosta'); ?></button>
2602 </div>
2603 </div>
2604 </div>
2605
2606 <script type="text/javascript">
2607 jQuery(document).ready(function($) {
2608 // Product Sync to Bosta functionality
2609 var syncButton = $('#sync-products-to-bosta-btn');
2610 var syncModal = $('#bosta-product-sync-modal');
2611 var confirmButton = $('#product-sync-modal-confirm');
2612 var cancelButton = $('#product-sync-modal-cancel');
2613 var modalClose = $('.bosta-product-sync-modal-close');
2614 var isSyncing = false;
2615 var originalButtonText = confirmButton.text();
2616
2617 // Function to show toast notification
2618 function showToast(message, type) {
2619 var tagText = type === 'error' ? '<?php _e('Failed', 'bosta'); ?>' : '<?php _e('Success', 'bosta'); ?>';
2620 var tagClass = type === 'error' ? 'bosta-toast-tag-error' : 'bosta-toast-tag-success';
2621 var toast = $('<div class="bosta-toast bosta-toast-' + (type || 'success') + '">' +
2622 '<span class="bosta-toast-tag ' + tagClass + '">' + tagText + '</span>' +
2623 '<span class="bosta-toast-message">' + message + '</span>' +
2624 '</div>');
2625 $('body').append(toast);
2626 setTimeout(function() {
2627 toast.addClass('show');
2628 }, 100);
2629 setTimeout(function() {
2630 toast.removeClass('show');
2631 setTimeout(function() {
2632 toast.remove();
2633 }, 300);
2634 }, 3000);
2635 }
2636
2637 // Show modal when sync button is clicked
2638 syncButton.on('click', function(e) {
2639 e.preventDefault();
2640 // Reset button state
2641 confirmButton.prop('disabled', false).text(originalButtonText);
2642 isSyncing = false;
2643 syncModal.show();
2644 });
2645
2646 // Close modal when cancel button or close icon is clicked (only if not syncing)
2647 cancelButton.add(modalClose).on('click', function() {
2648 if (!isSyncing) {
2649 syncModal.hide();
2650 }
2651 });
2652
2653 // Prevent closing modal when clicking overlay during sync
2654 $('.bosta-product-sync-modal-overlay').on('click', function(e) {
2655 if (!isSyncing) {
2656 syncModal.hide();
2657 }
2658 });
2659
2660 // Confirm sync
2661 confirmButton.on('click', function() {
2662 isSyncing = true;
2663
2664 // Set button to loading state
2665 confirmButton.prop('disabled', true).html('<span class="spinner is-active" style="float: none; margin: 0 5px 0 0; visibility: visible;"></span><?php _e('Syncing...', 'bosta'); ?>');
2666
2667 $.ajax({
2668 url: ajaxurl,
2669 type: 'POST',
2670 data: {
2671 action: 'bosta_sync_products_to_bosta',
2672 nonce: '<?php echo wp_create_nonce('bosta_sync_products_to_bosta'); ?>'
2673 },
2674 success: function(response) {
2675 // Close modal
2676 syncModal.hide();
2677
2678 if (response.success) {
2679 // Show success toast (green)
2680 showToast(response.data, 'success');
2681 // Reload page after toast to update button state
2682 setTimeout(function() {
2683 location.reload();
2684 }, 3500);
2685 } else {
2686 // Show error toast (red)
2687 showToast(response.data, 'error');
2688 // Reset button state
2689 confirmButton.prop('disabled', false).text(originalButtonText);
2690 isSyncing = false;
2691 }
2692 },
2693 error: function() {
2694 // Close modal
2695 syncModal.hide();
2696 // Show error toast (red)
2697 showToast('<?php _e('An error occurred while syncing products.', 'bosta'); ?>', 'error');
2698 // Reset button state
2699 confirmButton.prop('disabled', false).text(originalButtonText);
2700 isSyncing = false;
2701 }
2702 });
2703 });
2704 });
2705 </script>
2706 <?php
2707 }
2708
2709 function bosta_handle_custom_bulk_action($nonce_action, $nonce_field, $action_type, $fulfillment_type = null)
2710 {
2711 $nonce_value = isset($_GET[$nonce_field]) ? sanitize_text_field($_GET[$nonce_field]) : null;
2712
2713 if ($nonce_value && check_admin_referer($nonce_action, $nonce_field)) {
2714 $current_user_id = get_current_user_id();
2715 $current_page = isset($_GET['page_num']) ? $_GET['page_num'] : 1;
2716 $orders_per_page = get_user_option('edit_shop_order_per_page', $current_user_id);
2717
2718 $orderIds = wc_get_orders([
2719 'limit' => $orders_per_page,
2720 'paged' => $current_page,
2721 'return' => 'ids',
2722 ]);
2723
2724 $redirect_url = add_query_arg('paged', $current_page, admin_url('edit.php?post_type=shop_order'));
2725 bosta_handle_bulk_action($redirect_url, $action_type, $orderIds, $fulfillment_type);
2726 } else {
2727 wp_die(__('Invalid nonce! Something went wrong.', 'bosta'));
2728 }
2729 }
2730
2731 //endregion
2732
2733 //region Bosta Settings Functions
2734
2735 if (!function_exists('bosta_add_custom_box')) {
2736 function bosta_add_custom_box()
2737 {
2738 if (class_exists('Automattic\WooCommerce\Utilities\OrderUtil') && Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled()) {
2739 add_meta_box('wporg_box_id', __('My Field', 'woocommerce'), 'bosta_wporg_custom_box_html', 'woocommerce_page_wc-orders', 'side', 'core');
2740 } else {
2741 add_meta_box('wporg_box_id', __('My Field', 'woocommerce'), 'bosta_wporg_custom_box_html', 'shop_order', 'side', 'core');
2742 }
2743 }
2744 }
2745
2746 add_action('add_meta_boxes', 'bosta_add_custom_box');
2747 function bosta_wporg_custom_box_html($post)
2748 {
2749 $screen = get_current_screen();
2750 if (!isset($screen->post_type) || 'shop_order' != $screen->post_type) {
2751 return;
2752 }
2753
2754 $order = wc_get_order($post->ID);
2755 if (!$order) {
2756 return;
2757 }
2758
2759 $APIKey = bosta_get_api_key();
2760 if (empty($APIKey)) {
2761 $error_message = '<p>API Key is required to be able to sync with Bosta</p>';
2762 bosta_set_transient('bosta_errors', $error_message);
2763 return;
2764 }
2765
2766
2767 $trackingNumber = $order->get_meta('bosta_tracking_number', true);
2768
2769 if (empty($trackingNumber)) {
2770 return;
2771 }
2772
2773 $body = [
2774 'trackingNumbers' => $trackingNumber,
2775 ];
2776
2777 $url = BOSTA_ENV_URL_V2 . '/deliveries/search';
2778 $response = bosta_send_api_request('POST', $url, $APIKey, $body);
2779
2780 if (!$response['success']) {
2781 bosta_format_failed_order_message($response['error'], $post->ID);
2782 } else {
2783 // Check if response body is valid and contains expected structure
2784 if (!$response['body'] || !is_array($response['body']) || !isset($response['body']['data']) || !isset($response['body']['data']['deliveries']) || empty($response['body']['data']['deliveries'])) {
2785 bosta_format_failed_order_message('Invalid response structure from API', $post->ID);
2786 return;
2787 }
2788 $delivery = $response['body']['data']['deliveries'][0];
2789 if ($delivery['state']['value'] != 'Created' && $delivery['state']['value'] != 'Pickup requested') {
2790 ?>
2791 <script>
2792 let div = document.createElement("div");
2793 let p = document.createElement("p");
2794 let textnode = document.createTextNode("The order is being shipped by bosta. Any updating or deleting on the order info will not reflect to bosta system. For support email help@bosta.co");
2795 p.appendChild(textnode);
2796 div.appendChild(p);
2797 div.setAttribute('class', 'error error-note');
2798 const parent = document.getElementsByClassName("wrap")[0];
2799 parent.insertBefore(div, parent.children[3]);
2800 </script>
2801 <?php
2802 }
2803 }
2804 }
2805
2806 add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'bosta_plugin_action_links');
2807 function bosta_plugin_action_links($links)
2808 {
2809 $plugin_links = array(
2810 '<a href="' . menu_page_url('bosta-woocommerce', false) . '">' . __('Settings') . '</a>',
2811 );
2812 return array_merge($plugin_links, $links);
2813 }
2814
2815 add_action('plugins_loaded', 'bosta_init_shipping_class');
2816 add_action('woocommerce_shipping_init', 'bosta_init_shipping_class');
2817 function bosta_init_shipping_class()
2818 {
2819 if (!class_exists('WooCommerce')) {
2820 return;
2821 }
2822
2823 if (!class_exists('bosta_Shipping_Method')) {
2824 class bosta_Shipping_Method extends WC_Shipping_Method
2825 {
2826 public function __construct()
2827 {
2828 parent::__construct();
2829
2830 $this->id = 'bosta';
2831 $this->method_title = __('Bosta Shipping', 'bosta');
2832 $this->method_description = __('Custom Shipping Method for bosta', 'bosta');
2833 $this->init();
2834 $this->enabled = isset($this->settings['enabled']) ? $this->settings['enabled'] : 'yes';
2835 $this->title = isset($this->settings['title']) ? $this->settings['title'] : __('bosta Shipping', 'bosta');
2836 }
2837
2838 function init()
2839 {
2840 $this->init_form_fields();
2841 $this->init_settings();
2842 $this->auto_update_flex_ship_on_settings_page();
2843 $this->bosta_generate_webhook_secret();
2844 add_action('woocommerce_update_options_shipping_' . $this->id, array(
2845 $this,
2846 'process_admin_options',
2847 ));
2848 add_action('admin_footer', array($this, 'handle_flex_shipping_toggle'));
2849 // Fulfillment section field rendering moved to Bosta_Fulfillment_UI class
2850 }
2851
2852 private function is_bosta_shipping_settings_screen() {
2853 if (!is_admin()) {
2854 return false;
2855 }
2856 if (!isset($_GET['page'], $_GET['tab'], $_GET['section'])) {
2857 return false;
2858 }
2859 return $_GET['page'] === 'wc-settings' && $_GET['tab'] === 'shipping' && $_GET['section'] === 'bosta';
2860 }
2861
2862
2863 function handle_flex_shipping_toggle()
2864 {
2865 if (!$this->is_bosta_shipping_settings_screen()) {
2866 return;
2867 }
2868 $settings_html_file = plugin_dir_path(__FILE__) . 'components/settings/flexship/flexship.html';
2869 if (file_exists($settings_html_file)) {
2870 include $settings_html_file;
2871 }
2872 }
2873
2874
2875
2876 // Fulfillment section field rendering moved to Bosta_Fulfillment_UI class
2877
2878 private function get_business_info($api_key)
2879 {
2880 $url = BOSTA_ENV_URL_V0 . '/businesses/' . esc_html($api_key) . '/info';
2881 $response = bosta_send_api_request('GET', $url);
2882
2883 if ($response['success']) {
2884 return $response['body'];
2885 }
2886
2887 return array();
2888 }
2889
2890 function auto_update_flex_ship_on_settings_page()
2891 {
2892 if ($this->is_bosta_shipping_settings_screen()) {
2893 $apiKey = $this->get_option('APIKey');
2894 if (!empty($apiKey)) {
2895 $this->update_plugin_flex_ship_settings($apiKey);
2896
2897
2898 }
2899 }
2900 }
2901
2902 function bosta_generate_webhook_secret()
2903 {
2904 // Handle webhook registration for existing users
2905 $webhook_secret = Bosta_Webhook_Manager::get_webhook_secret();
2906 if (empty($webhook_secret)) {
2907 $webhook_secret = Bosta_Webhook_Manager::generate_webhook_secret();
2908 Bosta_Webhook_Manager::set_webhook_secret($webhook_secret);
2909 Bosta_Webhook_Manager::register_webhook_with_bosta($webhook_secret);
2910 }
2911 }
2912
2913 function init_form_fields()
2914 {
2915 $this->form_fields = array(
2916 'APIKey' => array(
2917 'title' => __('APIKey', 'bosta'),
2918 'type' => 'text',
2919 ),
2920 'IncludeProductDescriptions' => array(
2921 'label' => 'Include Woocommerce Product Descriptions in Bosta Airway Bills',
2922 'title' => __('Product Descriptions', 'bosta'),
2923 'type' => 'checkbox',
2924 'default' => 'yes',
2925 'description' => __('Product descriptions included in the airway bill shall not exceed 500 characters.', 'bosta'),
2926 ),
2927 'ProductDescription' => array(
2928 'label' => 'Select product info to display in AWB',
2929 'title' => __('Product Description Fields', 'bosta'),
2930 'type' => 'multiselect',
2931 'class' => 'wc-enhanced-select',
2932 'options' => array(
2933 'name' => __('Product Name', 'bosta'),
2934 'sku' => __('Product SKU', 'bosta'),
2935 ),
2936 'default' => array('name'),
2937 'description' => __('Select one or more product fields to display in the AWB.', 'bosta'),
2938 ),
2939 'MaintainOldDescription' => array(
2940 'label' => 'Maintain old description',
2941 'title' => __('Maintain Old Description', 'bosta'),
2942 'type' => 'checkbox',
2943 'default' => 'no',
2944 'description' => __('If checked, maintains the old description format: Product Name [SKU] (Quantity). If unchecked, uses new format: Product Name x Quantity (SKU).', 'bosta'),
2945 ),
2946 'AllowToOpenPackage' => array(
2947 'label' => 'Allow customer to open package',
2948 'title' => __('Allow to open package', 'bosta'),
2949 'type' => 'checkbox',
2950 'default' => 'no',
2951 ),
2952 'EnableFlexShipping' => array(
2953 'label' => 'Enable FlexShip',
2954 'title' => __('Enable FlexShip', 'bosta'),
2955 'type' => 'checkbox',
2956 'default' => 'no',
2957 'description' => 'Charge your customer a service fee in case of package refusal to reduce your refund costs',
2958 ),
2959 'FlexShippingValue' => array(
2960 'type' => 'hidden',
2961 'default' => FLEX_SHIPPING_DEFAULT_VALUE,
2962 ),
2963 'OrderRef' => array(
2964 'label' => 'Display Woocomerce order reference in AWB',
2965 'title' => __('Order reference', 'bosta'),
2966 'type' => 'checkbox',
2967 'default' => 'yes',
2968 ),
2969 'DisableBostaZoning' => array(
2970 'label' => 'Disable Bosta area fields',
2971 'title' => __('Disable Bosta Zoning', 'bosta'),
2972 'type' => 'checkbox',
2973 'default' => 'no',
2974 ),
2975 'ResetZoningCache' => array(
2976 'label' => 'Reset zoning cache',
2977 'title' => __('Reset Zoning Cache', 'bosta'),
2978 'type' => 'checkbox',
2979 'default' => 'no',
2980 ),
2981 // 'WebhookSecret' => array(
2982 // 'title' => __('Webhook Secret', 'bosta'),
2983 // 'type' => 'text',
2984 // 'default' => Bosta_Webhook_Manager::get_webhook_secret(),
2985 // 'custom_attributes' => array(
2986 // 'readonly' => 'readonly',
2987 // ),
2988 // 'description' => __('This secret key is used to validate incoming webhooks from Bosta. Keep this secure.', 'bosta'),
2989 // ),
2990 'DisableSyncOrderUpdates' => array(
2991 'label' => 'Disable syncing order updates to Bosta',
2992 'title' => __('Disable Order Updates Sync', 'bosta'),
2993 'type' => 'checkbox',
2994 'default' => 'no',
2995 'description' => __('When enabled, changes to existing orders will not be synced to Bosta.', 'bosta'),
2996 ),
2997 'EnableAutoSync' => array(
2998 'label' => 'Enable automatic order synchronization',
2999 'title' => __('Enable Auto Sync', 'bosta'),
3000 'type' => 'checkbox',
3001 'default' => 'no',
3002 'description' => __('Automatically sync new orders with Bosta when they are created. Orders will be sent to Bosta immediately after checkout completion.', 'bosta'),
3003 ),
3004 'AutoSyncFulfillmentType' => array(
3005 'title' => __('Auto Sync Type', 'bosta'),
3006 'type' => 'select',
3007 'class' => 'wc-enhanced-select',
3008 'options' => array(
3009 'your_location' => __('From Your Location', 'bosta'),
3010 'bosta_fulfillment' => __('Bosta Fulfillment Center', 'bosta'),
3011 ),
3012 'default' => 'your_location',
3013 'description' => __('Choose where orders will be fulfilled from when auto sync is enabled.', 'bosta'),
3014 ),
3015 'EnableProductAutoSync' => array(
3016 'label' => 'Enable automatic product sync',
3017 'title' => __('Enable Product Sync', 'bosta'),
3018 'type' => 'checkbox',
3019 'default' => 'no',
3020 'description' => __('Automatically sync your products to Bosta when they are created or updated.', 'bosta'),
3021 ),
3022 );
3023 }
3024
3025 private function bosta_check_reset_zoning_cache_toggle()
3026 {
3027 $is_toggle_enabled = $this->get_option('ResetZoningCache') === 'yes';
3028 if ($is_toggle_enabled) {
3029 delete_transient('bosta_zoning');
3030 delete_transient('bosta_city_areas');
3031 delete_transient('bosta_country_id_Transient');
3032
3033 $this->update_option('ResetZoningCache', 'no');
3034 }
3035 }
3036
3037 public function process_admin_options()
3038 {
3039 $oldAPIKey = $this->get_option('APIKey');
3040 $oldFlexship = $this->get_option('EnableFlexShipping');
3041 $oldFlexshipValue = $this->get_option('FlexShippingValue');
3042 $oldProductDescription = $this->get_option('ProductDescription');
3043 $oldProductAutoSync = $this->get_option('EnableProductAutoSync');
3044 $regenerateWebhookSecret = $this->get_option('RegenerateWebhookSecret');
3045 $testWebhook = $this->get_option('TestWebhook');
3046
3047 $productDescription = isset($_POST['woocommerce_bosta_ProductDescription']) ? $_POST['woocommerce_bosta_ProductDescription'] : array();
3048 if (empty($productDescription) || (is_array($productDescription) && count($productDescription) === 0)) {
3049 WC_Admin_Settings::add_error(__('Please select at least one product info field for AWB.', 'bosta'));
3050 $this->settings['ProductDescription'] = $oldProductDescription;
3051 return false;
3052 }
3053
3054 $settings_saved = parent::process_admin_options();
3055 $this->bosta_check_reset_zoning_cache_toggle();
3056
3057 // Check if Product Auto Sync is disabled (clear flag whenever it's disabled)
3058 $newProductAutoSync = isset($_POST['woocommerce_bosta_EnableProductAutoSync']) ? 'yes' : 'no';
3059 if ($newProductAutoSync === 'no') {
3060 // Clear the initial sync flag when auto-sync is disabled, so button can reappear
3061 delete_option('bosta_initial_product_sync_completed');
3062
3063 // Update feature configuration to indicate products are not synced
3064 Bosta_Product_Sync::update_feature_configuration(false);
3065 }
3066
3067 $newAPIKey = $this->get_option('APIKey');
3068 $newFlexship = $this->get_option('EnableFlexShipping');
3069 $newFlexshipValue = $this->get_option('FlexShippingValue');
3070
3071 if (empty($newAPIKey)) {
3072 WC_Admin_Settings::add_error(__('Error: API Key is required.', 'bosta'));
3073 return false;
3074 }
3075 if (!bosta_validate_api_key($newAPIKey)) {
3076 WC_Admin_Settings::add_error(__('Error: API Key is invalid.', 'bosta'));
3077 return false;
3078 }
3079
3080 if (!$settings_saved) {
3081 return false;
3082 }
3083
3084 if ($newAPIKey !== $oldAPIKey) {
3085 // Register webhook with new API key
3086 $webhook_secret = Bosta_Webhook_Manager::get_webhook_secret();
3087 if (!empty($webhook_secret)) {
3088 Bosta_Webhook_Manager::register_webhook_with_bosta($webhook_secret);
3089 }
3090
3091 if ($newFlexship === 'yes') {
3092 $this->update_bosta_flex_ship_settings($newAPIKey, $newFlexship, $newFlexshipValue);
3093 } else {
3094 $this->update_plugin_flex_ship_settings($newAPIKey);
3095 }
3096 return true;
3097 }
3098
3099 if ($newFlexship !== $oldFlexship || $newFlexshipValue !== $oldFlexshipValue) {
3100 $this->update_bosta_flex_ship_settings($newAPIKey, $newFlexship, $newFlexshipValue);
3101 }
3102
3103 // Handle webhook secret regeneration
3104 if ($regenerateWebhookSecret === 'yes') {
3105 $new_webhook_secret = Bosta_Webhook_Manager::generate_webhook_secret();
3106 Bosta_Webhook_Manager::set_webhook_secret($new_webhook_secret);
3107 Bosta_Webhook_Manager::register_webhook_with_bosta($new_webhook_secret);
3108 $this->update_option('RegenerateWebhookSecret', 'no');
3109 WC_Admin_Settings::add_message(__('Webhook secret has been regenerated and registered with Bosta.', 'bosta'));
3110 }
3111
3112 // Handle webhook testing
3113 if ($testWebhook === 'yes') {
3114 $test_result = Bosta_Webhook_Manager::test_webhook_endpoint();
3115 $this->update_option('TestWebhook', 'no');
3116
3117 if ($test_result) {
3118 WC_Admin_Settings::add_message(__('Webhook test successful! The endpoint is working correctly.', 'bosta'));
3119 } else {
3120 WC_Admin_Settings::add_error(__('Webhook test failed! Please check your server configuration and try again.', 'bosta'));
3121 }
3122 }
3123
3124 return true;
3125 }
3126
3127 private function update_plugin_flex_ship_settings($apikey)
3128 {
3129 $url = BOSTA_ENV_URL_V0 . '/businesses/' . esc_html($apikey) . '/info';
3130 $response = bosta_send_api_request('GET', $url);
3131
3132 if ($response['success'] && isset($response['body']['flexShip'])) {
3133 $flexShip = $response['body']['flexShip'];
3134 $isSubscribed = !empty($flexShip['isSubscribed']);
3135 $isAppliedToAllOrders = !empty($flexShip['isAppliedToAllOrders']);
3136 if ($isSubscribed && $isAppliedToAllOrders) {
3137 $this->update_option('EnableFlexShipping', 'yes');
3138 $this->update_option('FlexShippingValue', $flexShip['amountToBeCollected'] ?? FLEX_SHIPPING_DEFAULT_VALUE);
3139 $this->update_option('AllowToOpenPackage', 'yes');
3140 } else {
3141 $this->update_option('EnableFlexShipping', 'no');
3142 $this->update_option('FlexShippingValue', FLEX_SHIPPING_DEFAULT_VALUE);
3143 }
3144 } else {
3145 $this->update_option('EnableFlexShipping', 'no');
3146 $this->update_option('FlexShippingValue', FLEX_SHIPPING_DEFAULT_VALUE);
3147 }
3148 }
3149
3150 private function update_bosta_flex_ship_settings($apikey, $flexship, $flexship_value)
3151 {
3152 $url = BOSTA_ENV_URL_V2 . '/businesses/flex-ship';
3153 $body = [
3154 'isSubscribed' => ($flexship === 'yes'),
3155 'amountToBeCollected' => intval($flexship_value),
3156 'isAppliedToAllOrders' => true
3157 ];
3158 $response = bosta_send_api_request('PUT', $url, $apikey, $body);
3159
3160 if (!$response['success']) {
3161 $error_message = 'Failed to update FlexShip settings: ' . $response['error'];
3162 WC_Admin_Settings::add_error(__($error_message, 'bosta'));
3163 } else {
3164 $success_message = $flexship === 'yes'
3165 ? 'FlexShip service has been activated successfully.'
3166 : 'FlexShip service has been deactivated successfully.';
3167 WC_Admin_Settings::add_message(__($success_message, 'bosta'));
3168 }
3169 }
3170 }
3171 }
3172 }
3173
3174 add_filter('woocommerce_shipping_methods', 'bosta_add_shipping_method');
3175 function bosta_add_shipping_method($methods)
3176 {
3177 $methods[] = 'bosta_Shipping_Method';
3178 return $methods;
3179 }
3180
3181 add_action('admin_menu', 'bosta_setup_menu', 20);
3182 function bosta_setup_menu()
3183 {
3184 //check if woocommerce is activated
3185 if (!class_exists('WooCommerce')) {
3186 return;
3187 }
3188
3189 add_menu_page('Test Plugin Page', 'Bosta', 'manage_options', 'bosta-woocommerce', 'bosta_redirect_to_settings_page', esc_url(plugins_url('assets/images/bosta.svg', __FILE__)), 56);
3190
3191 // link to plugin settings
3192 add_submenu_page('bosta-woocommerce', 'Setting', 'Setting', 'manage_options', 'bosta-woocommerce', 'bosta_redirect_to_settings_page');
3193
3194 // link to inventory management
3195 add_submenu_page('bosta-woocommerce', 'Inventory', 'Inventory', 'manage_options', 'bosta-woocommerce-inventory', 'bosta_inventory_page');
3196
3197 // link to woocommerce orders
3198 add_submenu_page('bosta-woocommerce', 'Send Orders', 'Send Orders', 'manage_options', 'bosta-woocommerce-orders', 'bosta_redirect_to_orders_page');
3199
3200 // create pickup request
3201 add_submenu_page('bosta-woocommerce', 'Create Pickup', 'Create Pickup', 'manage_options', 'bosta-woocommerce-create-edit-pickup', 'bosta_create_edit_pickup_form');
3202
3203 //view pickups
3204 add_submenu_page('bosta-woocommerce', 'Pickup Requests', 'Pickup Requests', 'manage_options', 'bosta-woocommerce-view-pickups', 'bosta_view_scheduled_pickups');
3205
3206 // link to bosta shipments
3207 add_submenu_page('bosta-woocommerce', 'Track Bosta Orders', 'Track Bosta Orders', 'manage_options', 'bosta-woocommerce-shipments', 'bosta_redirect_to_dashboard_page');
3208
3209 // link to bosta documentation
3210 add_submenu_page('bosta-woocommerce', 'Bosta Documentation', 'Bosta Documentation', 'manage_options', 'bosta-woocommerce-documentation', 'bosta_redirect_to_documentation_page');
3211 }
3212
3213 add_action('admin_enqueue_scripts', function($hook) {
3214 if ($hook === 'woocommerce_page_wc-settings' && isset($_GET['section']) && $_GET['section'] === 'bosta') {
3215 // Enqueue flexship script
3216 wp_enqueue_script(
3217 'bosta-flexship-js',
3218 plugins_url('components/settings/flexship/flexship.js', __FILE__),
3219 array('jquery'),
3220 filemtime(plugin_dir_path(__FILE__) . 'components/settings/flexship/flexship.js'),
3221 true
3222 );
3223
3224 // Enqueue auto sync warning script
3225 wp_enqueue_script(
3226 'bosta-auto-sync-warning-js',
3227 plugins_url('assets/js/auto-sync-warning.js', __FILE__),
3228 array('jquery'),
3229 filemtime(plugin_dir_path(__FILE__) . 'assets/js/auto-sync-warning.js'),
3230 true
3231 );
3232
3233 // Enqueue auto sync warning CSS
3234 wp_enqueue_style(
3235 'bosta-auto-sync-warning-css',
3236 plugins_url('Css/auto-sync-warning.css', __FILE__),
3237 array(),
3238 filemtime(plugin_dir_path(__FILE__) . 'Css/auto-sync-warning.css')
3239 );
3240
3241 // Enqueue product description settings CSS
3242 wp_enqueue_style(
3243 'bosta-product-description-settings-css',
3244 plugins_url('Css/product-description-settings.css', __FILE__),
3245 array(),
3246 filemtime(plugin_dir_path(__FILE__) . 'Css/product-description-settings.css')
3247 );
3248
3249 // Enqueue product description settings JavaScript
3250 wp_enqueue_script(
3251 'bosta-product-description-settings-js',
3252 plugins_url('assets/js/product-description-settings.js', __FILE__),
3253 array('jquery'),
3254 filemtime(plugin_dir_path(__FILE__) . 'assets/js/product-description-settings.js'),
3255 true
3256 );
3257 }
3258
3259 // Enqueue tooltip script for orders page
3260 if (in_array($hook, ['edit.php', 'woocommerce_page_wc-orders']) &&
3261 (isset($_GET['post_type']) && $_GET['post_type'] === 'shop_order') ||
3262 $hook === 'woocommerce_page_wc-orders') {
3263 wp_enqueue_script(
3264 'bosta-tooltip-js',
3265 plugins_url('assets/js/tooltip.js', __FILE__),
3266 array('jquery'),
3267 filemtime(plugin_dir_path(__FILE__) . 'assets/js/tooltip.js'),
3268 true
3269 );
3270 }
3271 });
3272
3273 // AJAX handlers for fulfillment sync - Moved to Bosta_Fulfillment class
3274 //endregion
3275
3276
3277
3278 //region Bosta Preview Functions
3279
3280 add_filter('woocommerce_admin_order_preview_get_order_details', 'bosta_admin_order_preview_add_custom_meta_data', 10, 2);
3281 function bosta_admin_order_preview_add_custom_meta_data($data, $order)
3282 {
3283 $APIKey = bosta_get_api_key();
3284 if (empty($APIKey)) {
3285 $message = 'API Key is required to be able to sync with Bosta';
3286 bosta_set_transient('bosta_errors', "<p>{$message}</p>");
3287 bosta_redirect_to_settings_page();
3288 return $data;
3289 }
3290
3291 $trackingNumber = $order->get_meta('bosta_tracking_number', true);
3292 if (empty($trackingNumber)) {
3293 $message = 'Order is not synced at Bosta';
3294 bosta_set_transient('bosta_errors', "<p>{$message}</p>");
3295 return $data;
3296 }
3297
3298 $url = BOSTA_ENV_URL_V2 . '/deliveries/business/' . $trackingNumber;
3299 $response = bosta_send_api_request('GET', $url, $APIKey);
3300 if (!$response['success']) {
3301 bosta_format_failed_order_message($response['error']);
3302 return $data;
3303 }
3304 // Check if response body is valid and contains expected structure
3305 if (!$response['body'] || !is_array($response['body']) || !isset($response['body']['data'])) {
3306 bosta_format_failed_order_message('Invalid response structure from API');
3307 return $data;
3308 }
3309 $orderDetails = $response['body']['data'];
3310 $data = array_merge($data, bosta_preview_extract_order_timeline_details($orderDetails));
3311 $data = array_merge($data, bosta_preview_extract_order_details($orderDetails));
3312 $data = array_merge($data, bosta_preview_extract_customer_info($orderDetails));
3313 $data = array_merge($data, bosta_preview_extract_pickup_info($orderDetails));
3314 $data = array_merge($data, bosta_preview_extract_bosta_performance_info($orderDetails));
3315
3316 return $data;
3317 }
3318
3319 function bosta_preview_extract_order_timeline_details($orderDetails)
3320 {
3321 $timelineData = [];
3322
3323 if (!empty($orderDetails['timeline'])) {
3324 foreach ($orderDetails['timeline'] as $x => $timeline) {
3325 $timelineData["timeline_value_$x"] = $timeline['value'] ?? 'N/A';
3326 $timelineData["timeline_date_$x"] = isset($timeline['date']) ? bosta_format_date($timeline['date']) : 'N/A';
3327 $isDone = $timeline['done'] ?? false;
3328 $timelineData["timeline_done_$x"] = $isDone ? 'status_done' : 'status_not_done';
3329 if ($isDone) {
3330 $timelineData["timeline_next_action"] = $timeline['nextAction'] ?? 'N/A';
3331 $timelineData["timeline_shipment_age"] = $timeline['nextAction'] ?? 'N/A';
3332 }
3333 }
3334 $timelineLength = count($orderDetails['timeline']);
3335 set_transient('bosta_timelineLength', $timelineLength, HOUR_IN_SECONDS);
3336 }
3337
3338 return $timelineData;
3339 }
3340
3341 function bosta_preview_extract_order_details($orderDetails)
3342 {
3343 return [
3344 'trackingNumber' => $orderDetails['trackingNumber'] ?? 'N/A',
3345 'status' => $orderDetails['state']['value'] ?? 'N/A',
3346 'type' => $orderDetails['type']['value'] ?? 'N/A',
3347 'cod' => $orderDetails['cod'] ?? '0',
3348 'createdAt' => bosta_format_date($orderDetails['createdAt']),
3349 'updatedAt' => bosta_format_date($orderDetails['updatedAt']),
3350 'itemsCount' => $orderDetails['specs']['packageDetails']['itemsCount'] ?? 'N/A',
3351 'notes' => $orderDetails['notes'] ?? 'N/A'
3352 ];
3353 }
3354
3355 function bosta_preview_extract_customer_info($orderDetails)
3356 {
3357 return [
3358 'fullName' => $orderDetails['receiver']['fullName'] ?? 'N/A',
3359 'phone' => $orderDetails['receiver']['phone'] ?? 'N/A',
3360 'dropOffAddressCity' => $orderDetails['dropOffAddress']['city']['name'] ?? 'N/A',
3361 'dropOffAddressZone' => $orderDetails['dropOffAddress']['zone']['name'] ?? 'N/A',
3362 'dropOffAddressDistrict' => $orderDetails['dropOffAddress']['district']['name'] ?? 'N/A',
3363 'dropOffAddressFistLine' => $orderDetails['dropOffAddress']['firstLine'] ?? 'N/A',
3364 'dropOffAddressBuilding' => $orderDetails['dropOffAddress']['buildingNumber'] ?? 'N/A',
3365 'dropOffAddressFloor' => $orderDetails['dropOffAddress']['floor'] ?? 'N/A',
3366 'dropOffAddressApartment' => $orderDetails['dropOffAddress']['apartment'] ?? 'N/A'
3367 ];
3368 }
3369
3370 function bosta_preview_extract_pickup_info($orderDetails)
3371 {
3372 return [
3373 'pickupAddressCity' => $orderDetails['pickupAddress']['city']['name'] ?? 'N/A',
3374 'pickupAddressZone' => $orderDetails['pickupAddress']['zone']['name'] ?? 'N/A',
3375 'pickupAddressDistrict' => $orderDetails['pickupAddress']['district']['name'] ?? 'N/A',
3376 'pickupAddressFistLine' => $orderDetails['pickupAddress']['firstLine'] ?? 'N/A',
3377 'pickupAddressBuilding' => $orderDetails['pickupAddress']['buildingNumber'] ?? 'N/A',
3378 'pickupAddressFloor' => $orderDetails['pickupAddress']['floor'] ?? 'N/A',
3379 'pickupAddressApartment' => $orderDetails['pickupAddress']['apartment'] ?? 'N/A',
3380 'pickupRequestId' => $orderDetails['pickupRequestId'] ?? 'N/A'
3381 ];
3382 }
3383
3384 function bosta_preview_extract_bosta_performance_info($orderDetails)
3385 {
3386 $promise = 'Not started yet';
3387 if (!empty($orderDetails['sla'])) {
3388 $isExceeded = $orderDetails['sla']['e2eSla']['isExceededE2ESla'] ?? false;
3389 $data['promise'] = $isExceeded ? 'Not met' : 'Met';
3390 }
3391
3392 return [
3393 'outboundActionsCount' => $orderDetails['outboundActionsCount'] ?? '0',
3394 'deliveryAttemptsLength' => $orderDetails['deliveryAttemptsLength'] ?? '0',
3395 'promise' => $promise
3396 ];
3397 }
3398
3399 add_action('woocommerce_admin_order_preview_start', 'bosta_custom_display_order_data_in_admin');
3400 function bosta_custom_display_order_data_in_admin()
3401 {
3402 $timelineLength = get_transient('bosta_timelineLength') ?? 0;
3403
3404 ?>
3405 <div class="container-div">
3406 <h4 class="table-title">Order Timeline</h4>
3407 <div class="timeline-table">
3408 <?php for ($x = 0; $x < $timelineLength; $x++): ?>
3409 <div class="timeline-entry">
3410 <div class="entry-progress">
3411 <span class="<?php echo "{{data.timeline_done_" . esc_attr($x) . "}}" ?>"></span>
3412 <span class="<?php echo "{{data.timeline_done_" . esc_attr($x) . "}}_line"; ?>"></span>
3413 </div>
3414 <div class="entry-data">
3415 <span class="data-title"><?php echo "{{data.timeline_value_$x}}"; ?></span>
3416 <span class="data-date"><?php echo "{{data.timeline_date_$x}}"; ?></span>
3417 </div>
3418 </div>
3419 <?php endfor; ?>
3420 </div>
3421 <div class="timeline-next-action">
3422 <span class="next-action-title">Next Action:</span>
3423 <span> {{data.timeline_next_action}} </span>
3424 </div>
3425 </div>
3426 <?php
3427
3428 ?>
3429 <div class="container-div">
3430 <h4 class="table-title">Order Details</h4>
3431 <div class="container-table">
3432 <div class="cell">
3433 <p class="cell-header">Bosta tracking number: </p>
3434 <p class="cell-data"><?php echo "{{data.trackingNumber}}"; ?></p>
3435 </div>
3436 <div class="cell">
3437 <p class="cell-header">Status: </p>
3438 <p class="cell-data"><?php echo "{{data.status}}"; ?></p>
3439 </div>
3440 <div class="cell">
3441 <p class="cell-header">Type: </p>
3442 <p class="cell-data"><?php echo "{{data.type}}"; ?></p>
3443 </div>
3444 <div class="cell">
3445 <p class="cell-header">Cash on delivery: </p>
3446 <p class="cell-data"><?php echo "{{data.cod}}"; ?></p>
3447 </div>
3448 <div class="cell">
3449 <p class="cell-header">Creation date: </p>
3450 <p class="cell-data"><?php echo "{{data.createdAt}}"; ?></p>
3451 </div>
3452 <div class="cell">
3453 <p class="cell-header">Last update date: </p>
3454 <p class="cell-data"><?php echo "{{data.updatedAt}}"; ?></p>
3455 </div>
3456 <div class="cell">
3457 <p class="cell-header">Items count: </p>
3458 <p class="cell-data"><?php echo "{{data.itemsCount}}"; ?></p>
3459 </div>
3460 <div class="cell">
3461 <p class="cell-header">Delivery Notes: </p>
3462 <p class="cell-data"><?php echo "{{data.notes}}"; ?></p>
3463 </div>
3464 </div>
3465 </div>
3466 <?php
3467
3468 ?>
3469 <div class="container-div">
3470 <h4 class="table-title">Customer Info</h4>
3471 <div class="container-table">
3472 <div class="cell">
3473 <p class="cell-header">Customer name: </p>
3474 <p class="cell-data"><?php echo "{{data.fullName}}"; ?></p>
3475 </div>
3476 <div class="cell">
3477 <p class="cell-header">Phone number: </p>
3478 <p class="cell-data"><?php echo "{{data.phone}}"; ?></p>
3479 </div>
3480 <div class="cell">
3481 <p class="cell-header">Area, City: </p>
3482 <p class="cell-data"><?php echo "{{data.dropOffAddressZone}} - {{data.dropOffAddressDistrict}}, {{data.dropOffAddressCity}}"; ?></p>
3483 </div>
3484 <div class="cell">
3485 <p class="cell-header">Customer address: </p>
3486 <p class="cell-data"><?php echo "{{data.dropOffAddressFistLine}}"; ?></p>
3487 </div>
3488 <div class="cell">
3489 <p class="cell-header">Building number: </p>
3490 <p class="cell-data"><?php echo "{{data.dropOffAddressBuilding}}"; ?></p>
3491 </div>
3492 <div class="cell">
3493 <p class="cell-header">Floor, Apartment: </p>
3494 <p class="cell-data"><?php echo "{{data.dropOffAddressFloor}}, {{data.dropOffAddressApartment}}"; ?></p>
3495 </div>
3496 </div>
3497 </div>
3498 <?php
3499
3500 ?>
3501 <div class="container-div">
3502 <h4 class="table-title">Pickup Info</h4>
3503 <div class="container-table">
3504 <div class="cell">
3505 <p class="cell-header">City: </p>
3506 <p class="cell-data"><?php echo "{{data.pickupAddressCity}}"; ?></p>
3507 </div>
3508 <div class="cell">
3509 <p class="cell-header">Area: </p>
3510 <p class="cell-data"><?php echo "{{data.pickupAddressZone}} - {{data.pickupAddressDistrict}}"; ?></p>
3511 </div>
3512 <div class="cell">
3513 <p class="cell-header">Street name: </p>
3514 <p class="cell-data"><?php echo "{{data.pickupAddressFistLine}}"; ?></p>
3515 </div>
3516 <div class="cell">
3517 <p class="cell-header">Building number: </p>
3518 <p class="cell-data"><?php echo "{{data.pickupAddressBuilding}}"; ?></p>
3519 </div>
3520 <div class="cell">
3521 <p class="cell-header">Floor, Apartment: </p>
3522 <p class="cell-data"><?php echo "{{data.pickupAddressFloor}}, {{data.pickupAddressApartment}}"; ?></p>
3523 </div>
3524 <div class="cell">
3525 <p class="cell-header">Pickup ID: </p>
3526 <p class="cell-data"><?php echo "{{data.pickupRequestId}}"; ?></p>
3527 </div>
3528 </div>
3529 </div>
3530 <?php
3531
3532 ?>
3533 <div class="container-div">
3534 <h4 class="table-title">Bosta Performance</h4>
3535 <div class="container-table">
3536 <div class="cell">
3537 <p class="cell-header" data-tooltip="Number of times Bosta tried to deliver the order">Delivery attempts:</p>
3538 <p class="cell-data"><?php echo "{{data.deliveryAttemptsLength}}"; ?></p>
3539 </div>
3540 <div class="cell">
3541 <p class="cell-header" data-tooltip="Number of calls made by the outbound team to verify the star actions and take corrective actions if needed to deliver the order on time">Outbound calls:</p>
3542 <p class="cell-data"><?php echo "{{data.outboundActionsCount}}"; ?></p>
3543 </div>
3544 <div class="cell">
3545 <p class="cell-header" data-tooltip="Bosta promises next day delivery (calculated from the pickup date) for orders with Cairo as the pickup and drop city. The expected delivery period increases to two or three days depending on the distance between the pick up and the drop off cities i.e. Alexandria, Delta or Upper Egypt.">Delivery promise:</p>
3546 <p class="cell-data"><?php echo "{{data.promise}}"; ?></p>
3547 </div>
3548 </div>
3549 </div>
3550 <?php
3551 }
3552
3553