| 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§ion=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§ion=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;">▼</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') ? '▼' : '▲'; |
| 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>×</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 |
|