array( 'x-api-key' => $api_key, 'Accept' => 'application/json', ), 'timeout' => 15, ); postexketab_log('GET request > ' . $api_url, $args); $response = wp_remote_get($api_url, $args); if (is_wp_error($response)) { postexketab_log('GET failed < ' . $api_url, array('error' => $response->get_error_message()), 'error'); return array( 'success' => false, 'error' => 'Request failed', 'message' => $response->get_error_message(), ); } $status_code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); postexketab_log('GET response < ' . $api_url, array('status' => $status_code, 'body' => $body)); if ($status_code >= 200 && $status_code < 300) { if (isset($data['IsSuccess']) && $data['IsSuccess'] === false) { return array( 'success' => false, 'error' => 'API Error', 'message' => $data['Message'] ?? 'خطای نامشخص', ); } return array( 'success' => true, 'data' => $data, ); } else { return array( 'success' => false, 'status' => $status_code, 'message' => $data['message'] ?? 'API ERROR ' . $status_code ); } } //Send Post API Call function postexketab_post_call($api_url, $body = [], $required_key = true) { $api_key = esc_attr(get_option('postex_api_key', '')); if ($required_key && empty($api_key)) { postexketab_log('POST aborted > ' . $api_url . ' (API key missing)', null, 'warning'); return array('message' => 'دسترسی غیر مجاز'); } $args = array( 'method' => 'POST', 'headers' => array( 'x-api-key' => $api_key, 'Accept' => 'application/json', 'Content-Type' => 'application/json', ), 'timeout' => 45, 'body' => json_encode($body), ); postexketab_log('POST request > ' . $api_url, array('args' => $args, 'body' => $body)); $response = wp_remote_post($api_url, $args); if (is_wp_error($response)) { postexketab_log('POST failed < ' . $api_url, array('error' => $response->get_error_message()), 'error'); return array( 'error' => 'خطا در ارتباط', 'message' => $response->get_error_message() ); } $status_code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); postexketab_log('POST response < ' . $api_url, array('status' => $status_code, 'body' => $body)); // Handle non-200 responses if ($status_code < 200 || $status_code >= 300) { $error_result = json_decode($body, true); $message = is_array($error_result) ? ($error_result['Message'] ?? $error_result['message'] ?? $error_result['Detail'] ?? $error_result['detail'] ?? $error_result['Title'] ?? $error_result['title'] ?? '') : ''; if (is_array($message)) { $message = implode('، ', array_filter(array_map('strval', $message))); } if ($message === '') { $message = 'خطای API با کد ' . $status_code; } return array( 'error' => 'خطا در دریافت اطلاعات', 'status' => $status_code, 'message' => sanitize_text_field($message) ); } return json_decode($body, true); } /** * Stop an AJAX request from a logged in user who may not manage the shop. * A nonce proves where a request came from, not that its sender is allowed * to send it, so every privileged handler needs this as well. */ function postexketab_require_shop_manager() { if (!current_user_can('manage_woocommerce')) { wp_send_json_error(array('message' => 'دسترسی غیر مجاز'), 403); } } //Validation API Key function postexketab_validate_api_key($api_key) { if (!$api_key) { update_option('postex_api_key_valid', 'no'); update_option('postex_shipping_enabled', 'no'); delete_option('postex_api_user_name'); return false; } $response = wp_remote_get(POSTEXKETAB_API_PATH . '/shipping/whoami', [ 'headers' => ['x-api-key' => $api_key, 'Accept' => 'application/json'], 'timeout' => 15, 'redirection' => 5, ]); $status = is_wp_error($response) ? 0 : wp_remote_retrieve_response_code($response); $body = is_wp_error($response) ? null : json_decode(wp_remote_retrieve_body($response), true); $identity = is_array($body) ? ($body['Data'] ?? null) : null; $is_authenticated = is_array($identity) && ($identity['IsAuthenticated'] ?? false) === true; $is_enabled = is_array($identity) && ($identity['IsEnabled'] ?? false) === true; $api_status = is_array($body) ? (int) ($body['StatusCode'] ?? 0) : 0; if ($status === 200 && is_array($body) && ($body['IsSuccess'] ?? false) === true && $is_authenticated && $is_enabled) { $user_name = ''; foreach (array('ManagerName', 'ShopName', 'Username') as $field) { if (isset($identity[$field]) && is_scalar($identity[$field]) && trim((string) $identity[$field]) !== '') { $user_name = sanitize_text_field((string) $identity[$field]); break; } } if ($user_name !== '') { update_option('postex_api_user_name', $user_name); } else { delete_option('postex_api_user_name'); } update_option('postex_api_key_valid', 'yes'); update_option('postex_shipping_enabled', 'yes'); delete_transient('postex_api_key_error'); return $api_key; } //A transport failure says nothing about the key. Treating it as invalid //would disable shipping shop wide because of one timeout, so leave the //stored state untouched and let the merchant try again. if (is_wp_error($response)) { set_transient('postex_api_key_error', 'ارتباط با خانه کتاب برقرار نشد. کلید قبلی حفظ شد، لطفا دوباره تلاش کنید.', 30); return get_option('postex_api_key', ''); } $key_is_rejected = in_array($status, [401, 403], true) || in_array($api_status, [401, 403], true) || (is_array($identity) && array_key_exists('IsAuthenticated', $identity) && $identity['IsAuthenticated'] === false); if (!$key_is_rejected) { set_transient('postex_api_key_error', sprintf('تأیید کلید انجام نشد (HTTP %d). کلید قبلی حفظ شد؛ دوباره تلاش کنید.', $status), 30); return get_option('postex_api_key', ''); } update_option('postex_api_key_valid', 'no'); update_option('postex_shipping_enabled', 'no'); delete_option('postex_api_user_name'); set_transient('postex_api_key_error', 'کلید وارد شده معتبر نیست.', 30); //Return the entered value rather than nothing; returning null wiped the option return $api_key; } add_action('admin_notices', function () { if (!postexketab_is_plugin_admin_page()) { return; } if ($message = get_transient('postex_api_key_error')) { echo '

' . esc_html($message) . '

'; delete_transient('postex_api_key_error'); } }); /** * Checks whether the current WordPress administration screen belongs to this plugin. * * @param string $page Optional exact page slug. * @return bool */ function postexketab_is_plugin_admin_page($page = '') { if (!is_admin()) { return false; } $current_page = isset($_GET['page']) && is_string($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : ''; if ($page !== '') { return $current_page === $page; } return strpos($current_page, 'postexketab-') === 0; } function postexketab_normalize_locality_name($name) { return preg_replace('/[\s\x{200c}\x{200f}]+/u', '', str_replace(['ي', 'ك', 'آ'], ['ی', 'ک', 'ا'], (string) $name)); } function postexketab_get_locality_data($resource) { $cache_key = 'postex_locality_v3_' . md5(POSTEXKETAB_API_PATH . get_option('postex_api_key', '') . $resource); $cached = get_transient($cache_key); if (is_array($cached)) return $cached; $response = postexketab_get_call(POSTEXKETAB_API_PATH . '/shipping/' . $resource); $data = $response['data']['Data'] ?? null; if (empty($response['success']) || !is_array($data)) return false; if ($data) set_transient($cache_key, $data, DAY_IN_SECONDS); return $data; } //Get provinces function postexketab_get_states() { $states = [ 'KHZ' => [ 'postex_id' => 18, 'state_name' => 'خوزستان' ], 'THR' => [ 'postex_id' => 1, 'state_name' => 'تهران' ], 'ILM' => [ 'postex_id' => 24, 'state_name' => 'ایلام' ], 'BHR' => [ 'postex_id' => 23, 'state_name' => 'بوشهر' ], 'ADL' => [ 'postex_id' => 27, 'state_name' => 'اردبیل' ], 'ESF' => [ 'postex_id' => 26, 'state_name' => 'اصفهان' ], 'YZD' => [ 'postex_id' => 31, 'state_name' => 'یزد' ], 'KRH' => [ 'postex_id' => 9, 'state_name' => 'کرمانشاه' ], 'KRN' => [ 'postex_id' => 10, 'state_name' => 'کرمان' ], 'HDN' => [ 'postex_id' => 15, 'state_name' => 'همدان' ], 'GZN' => [ 'postex_id' => 13, 'state_name' => 'قزوین' ], 'ZJN' => [ 'postex_id' => 17, 'state_name' => 'زنجان' ], 'LRS' => [ 'postex_id' => 5, 'state_name' => 'لرستان' ], 'ABZ' => [ 'postex_id' => 25, 'state_name' => 'البرز' ], 'EAZ' => [ 'postex_id' => 29, 'state_name' => 'آذربایجان شرقی' ], 'WAZ' => [ 'postex_id' => 28, 'state_name' => 'آذربایجان غربی' ], 'CHB' => [ 'postex_id' => 22, 'state_name' => 'چهارمحال و بختیاری' ], 'SKH' => [ 'postex_id' => 21, 'state_name' => 'خراسان جنوبی' ], 'RKH' => [ 'postex_id' => 20, 'state_name' => 'خراسان رضوی' ], 'NKH' => [ 'postex_id' => 19, 'state_name' => 'خراسان شمالی' ], 'SMN' => [ 'postex_id' => 30, 'state_name' => 'سمنان' ], 'FRS' => [ 'postex_id' => 14, 'state_name' => 'فارس' ], 'QHM' => [ 'postex_id' => 12, 'state_name' => 'قم' ], 'KRD' => [ 'postex_id' => 11, 'state_name' => 'کردستان' ], 'KBD' => [ 'postex_id' => 8, 'state_name' => 'کهگیلویه و بویراحمد' ], 'GLS' => [ 'postex_id' => 7, 'state_name' => 'گلستان' ], 'GIL' => [ 'postex_id' => 6, 'state_name' => 'گیلان' ], 'MZN' => [ 'postex_id' => 4, 'state_name' => 'مازندران' ], 'MKZ' => [ 'postex_id' => 3, 'state_name' => 'مرکزی' ], 'HRZ' => [ 'postex_id' => 2, 'state_name' => 'هرمزگان' ], 'SBN' => [ 'postex_id' => 16, 'state_name' => 'سیستان و بلوچستان' ] ]; $provinces = postexketab_get_locality_data('provinces'); if ($provinces === false) return $states; $result = []; foreach ($provinces as $province) { if (!isset($province['Id'], $province['Name'])) continue; $key = 'province_' . (int) $province['Id']; foreach ($states as $wc_code => $state) { if (postexketab_normalize_locality_name($state['state_name']) === postexketab_normalize_locality_name($province['Name'])) { $key = $wc_code; break; } } $result[$key] = ['postex_id' => (int) $province['Id'], 'state_name' => $province['Name']]; } return $result; } //Get Cities function postexketab_get_cities_by_state() { if (!isset($_GET['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['nonce'])), 'postex_general_nonce')) { wp_send_json_error(array('message' => 'خطای امنیتی: اعتبارسنجی ناموفق بود.')); wp_die(); } postexketab_require_shop_manager(); if (isset($_GET['state_code'])) { $state_code = sanitize_text_field(wp_unslash($_GET['state_code'])); $filtered_cities = postexketab_get_cities($state_code); if ($filtered_cities === false) { wp_send_json_error(['message' => 'خطا در دریافت شهرها؛ استان را دوباره انتخاب کنید.']); } if (!empty($filtered_cities)) { $output = ''; $source = isset($_GET['source']) ? sanitize_text_field(wp_unslash($_GET['source'])) : ''; if ($source == 'shop') { $city = get_option('shop_city'); } elseif (strlen($source) > 0) { $city = $source; } else { $city = ''; } $is_selected = false; $selected_city_name = ''; foreach ($filtered_cities as $item) { $selected = ($city == $item['id'] || $city == $item['name']) ? 'selected' : ''; if ($selected) { $is_selected = true; $selected_city_name = $item['name']; } ob_start(); ?> شهر را انتخاب کنید'; wp_send_json_success(array('html' => $default_option . $output, 'city_label' => $selected_city_name)); } else { wp_send_json_error(array('message' => 'شهری یافت نشد')); } } else { wp_send_json_error(array('message' => 'شناسه استان اشتباه است')); } } add_action('wp_ajax_postex_get_cities_by_state', 'postexketab_get_cities_by_state'); //Validate & Correction Mobile Number function postexketab_validate_format_phone($phone) { // Remove any non-numeric characters $phone = preg_replace('/\D/', '', $phone); // Check if the phone number is valid (must start with 09 and be 11 digits) if (preg_match('/^(\+98|98|0)?9\d{9}$/', $phone)) { // If the number starts with +98 or 98, replace it with 0 if (substr($phone, 0, 2) == '98') { $phone = '0' . substr($phone, 2); } elseif (substr($phone, 0, 1) == '+') { // Remove the '+' and replace with 0 $phone = '0' . substr($phone, 1); } return $phone; // Return the formatted number } // If it's not a valid phone number, return false return ""; } /** * Value-added services are temporarily unavailable. Keeping this switch in one * place makes restoring the feature a one-line change and prevents old saved * defaults from being sent to the API while it is disabled. */ function postexketab_value_added_services_enabled() { return defined('POSTEXKETAB_VALUE_ADDED_SERVICES_ENABLED') && POSTEXKETAB_VALUE_ADDED_SERVICES_ENABLED; } function postexketab_get_default_additional_services() { if (!postexketab_value_added_services_enabled()) { return []; } $services = get_option('postex_default_additional'); return is_array($services) ? $services : []; } function postexketab_sanitize_default_additional_services($services) { if (!postexketab_value_added_services_enabled() || !is_array($services)) { return []; } return postexketab_sanitize_array_recursive($services); } function postexketab_get_value_added_service_value($post_data, $key) { if (!postexketab_value_added_services_enabled()) { return false; } return in_array($post_data[$key] ?? '0', ['1', 'true'], true); } //Get shop data function postexketab_get_shop_data() { $shop_data['postex_default_payment'] = sanitize_text_field(get_option('postex_default_payment')); $shop_data['postex_default_collect_type'] = sanitize_text_field(get_option('postex_default_collect_type')); $shop_data['postex_default_additional'] = postexketab_get_default_additional_services(); $shop_data['postex_default_box'] = sanitize_text_field(get_option('postex_default_box')); $shop_data['postex_default_shipping_method'] = sanitize_text_field(get_option('postex_default_shipping_method')); return $shop_data; } //Get MetaBox Form Data function postexketab_get_request_data($post_data) { return [ 'customer_name' => isset($post_data ['customer_name']) ? sanitize_text_field(wp_unslash($post_data ['customer_name'])) : '', 'customer_lastname' => isset($post_data ['customer_lastname']) ? sanitize_text_field(wp_unslash($post_data ['customer_lastname'])) : '', 'customer_phone' => isset($post_data ['customer_phone']) ? sanitize_text_field(wp_unslash($post_data ['customer_phone'])) : '', 'customer_address' => isset($post_data ['customer_address']) ? sanitize_textarea_field(wp_unslash($post_data ['customer_address'])) : '', 'customer_postal_code' => isset($post_data ['customer_postal_code']) ? sanitize_text_field(wp_unslash($post_data ['customer_postal_code'])) : '', 'customer_city_label' => isset($post_data ['customer_city_label']) ? sanitize_text_field(wp_unslash($post_data ['customer_city_label'])) : '', 'customer_city' => isset($post_data ['customer_city']) ? intval(wp_unslash($post_data ['customer_city'])) : '', 'payment_type' => 'SENDER', 'box_type_id' => isset($post_data ['box_type_id']) ? intval($post_data ['box_type_id']) : '', 'collect_type' => isset($post_data ['collect_type']) ? sanitize_text_field(wp_unslash($post_data ['collect_type'])) : '', 'is_fragile' => postexketab_get_value_added_service_value($post_data, 'is_fragile'), 'is_liquid' => postexketab_get_value_added_service_value($post_data, 'is_liquid'), 'package_request' => postexketab_get_value_added_service_value($post_data, 'package_request'), 'sms_request' => postexketab_get_value_added_service_value($post_data, 'sms_request'), 'insurance_request' => postexketab_get_value_added_service_value($post_data, 'insurance_request'), 'label_request' => postexketab_get_value_added_service_value($post_data, 'label_request'), 'total_weight' => isset($post_data ['total_weight']) ? floatval($post_data ['total_weight']) : 0, 'height' => isset($post_data ['height']) ? intval($post_data ['height']) : '', 'width' => isset($post_data ['width']) ? intval($post_data ['width']) : '', 'length' => isset($post_data ['length']) ? intval($post_data ['length']) : '', 'total_value' => isset($post_data ['total_value']) ? floatval($post_data ['total_value']) : 0, 'order_id' => isset($post_data ['order_id']) ? floatval($post_data ['order_id']) : 0, ]; } /** * Build the courier object required by the quotes endpoint. Shipping methods * are stored by the plugin as "courierCode|courierServiceCode". */ function postexketab_get_quote_courier($shipping_method = '') { if ($shipping_method === '') { $shipping_method = get_option('postex_default_shipping_method', ''); } $courier = explode('|', (string) $shipping_method, 2); return [ 'courier_code' => sanitize_text_field($courier[0] ?? ''), 'service_type' => sanitize_text_field($courier[1] ?? ''), ]; } //Get Quote function postexketab_shipping_price_request() { if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'postex_nonce_action')) { wp_send_json_error('Invalid request.'); } postexketab_require_shop_manager(); $allowed_keys = [ 'customer_city', 'customer_city_label', 'customer_name', 'customer_lastname', 'customer_phone', 'customer_address', 'customer_postal_code', 'payment_type', 'box_type_id', 'is_fragile', 'is_liquid', 'label_request', 'package_request', 'sms_request', 'insurance_request', 'total_value', 'total_weight', 'height', 'width', 'length', 'total_value_currency', 'nonce', 'collect_type', 'order_id', ]; $form_data = []; foreach ($allowed_keys as $key) { if (isset($_POST[$key])) { $form_data[$key] = sanitize_text_field(wp_unslash($_POST[$key])); } } //Form information $form_data = postexketab_get_request_data($form_data); $order_id = isset($form_data['order_id']) ? intval($form_data['order_id']) : 0; if ($order_id > 0) { $order = wc_get_order($order_id); if (!$order || !postexketab_order_needs_shipping($order)) { wp_send_json_error('این سفارش محصول فیزیکی قابل ارسال ندارد.'); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized recursively below. $selected_products = isset($_POST['selected_products']) ? postexketab_sanitize_array_recursive(wp_unslash($_POST['selected_products'])) : []; $selected_products = postexketab_filter_selected_shippable_products($order, $selected_products); if (empty($selected_products)) { wp_send_json_error('هیچ محصول فیزیکی برای ارسال انتخاب نشده است.'); } $selected_product_ids = array_column($selected_products, 'sku'); $form_data['total_value'] = postexketab_convert_order_total_value( postexketab_get_shippable_order_value($order, $selected_product_ids) ); } $request_data = [ 'collection_type' => $form_data['collect_type'], 'submit_channel' => POSTEXKETAB_SUBMIT_CHANNEL, 'submit_source' => POSTEXKETAB_SUBMIT_SOURCE, 'courier' => postexketab_get_quote_courier(), 'parcels' => [ [ "custom_parcel_id" => "", "to_city_code" => $form_data['customer_city'], "payment_type" => $form_data['payment_type'], "submit_channel" => POSTEXKETAB_SUBMIT_CHANNEL, "submit_source" => POSTEXKETAB_SUBMIT_SOURCE, "parcel_properties" => [ "length" => $form_data['length'], "width" => $form_data['width'], "height" => $form_data['height'], "box_type_id" => $form_data['box_type_id'], "is_fragile" => $form_data['is_fragile'], "is_liquid" => $form_data['is_liquid'], "total_weight" => postexketab_sanitize_price($form_data['total_weight']), "total_value" => postexketab_sanitize_price($form_data['total_value']), "pre_paid_amount" => 0, "total_value_currency" => "IRR" ], ], ], 'value_added_service' => [ 'request_label' => $form_data['label_request'], 'request_packaging' => $form_data['package_request'], 'request_sms_notification' => $form_data['sms_request'], 'print_logo' => false, ], ]; $response = postexketab_post_call(POSTEXKETAB_API_PATH . '/shipping/quotes', $request_data); $page_source = isset($_POST['source']) ? sanitize_text_field(wp_unslash($_POST['source'])) : 'default'; $base_path = POSTEXKETAB_PLUGIN_PATH . 'template-parts/ajax-templates/'; $file_path = $base_path . basename($page_source) . '.php'; if (is_array($response)) { $api_success = $response['IsSuccess'] ?? $response['isSuccess'] ?? null; $api_message = $response['Message'] ?? $response['message'] ?? 'خطا در استعلام قیمت'; $quote_methods = $response['Data'] ?? $response['data'] ?? null; if (isset($response['error']) || $api_success !== true) { wp_send_json_error($api_message); } if (!is_array($quote_methods) || empty($quote_methods) || !file_exists($file_path)) { wp_send_json_error('خطا : لطفا اطلاعات را بررسی کنید'); } ob_start(); include $file_path; $output = ob_get_clean(); wp_send_json_success(['html' => $output]); } else { wp_send_json_error('شاختار درخواست اشتباه است'); } wp_die(); } add_action('wp_ajax_postex_get_shipping_prices', 'postexketab_shipping_price_request'); function postexketab_get_order_national_code($order) { if (!$order || !is_callable([$order, 'get_meta'])) { return ''; } foreach (['_billing_national_code', 'billing_national_code', '_national_code'] as $meta_key) { $national_code = $order->get_meta($meta_key, true); if (is_scalar($national_code) && trim((string) $national_code) !== '') { return sanitize_text_field((string) $national_code); } } return ''; } function postexketab_build_api_parcel_items($selected_products) { $parcel_items = []; foreach ((array) $selected_products as $selected_product) { if (!is_array($selected_product)) { continue; } $product_id = absint($selected_product['sku'] ?? 0); if (!$product_id) { continue; } $dimensions = postexketab_get_product_dimensions_weight($product_id); $parcel_items[] = [ 'product_id' => $product_id, 'description' => postexketab_limit_parcel_item_description($selected_product['description'] ?? ''), 'quantity' => max(1, absint($selected_product['quantity'] ?? 1)), 'price' => postexketab_sanitize_price($selected_product['price'] ?? 0), 'weight' => is_array($dimensions) ? (float) ($dimensions['weight'] ?? 0) : 0, ]; } return $parcel_items; } function postexketab_limit_parcel_item_description($description) { $description = sanitize_text_field($description); if (function_exists('mb_substr')) { return mb_substr($description, 0, 50, 'UTF-8'); } return substr($description, 0, 50); } /** * Create a 5–15 character API identifier in the form OrderNumber-12345. */ function postexketab_generate_unique_parcel_identifier($identifier) { $identifier = preg_replace('/[^A-Za-z0-9]/', '', (string) $identifier); $identifier = $identifier !== '' ? substr($identifier, 0, 9) : 'direct'; return $identifier . '-' . wp_rand(10000, 99999); } /** * Convert the current shipping/orders response to the format consumed by the * WooCommerce result template and shipment metadata writer. */ function postexketab_normalize_bulk_submission_response($response) { if (!is_array($response)) { return ['error' => true, 'message' => 'پاسخ سرویس ثبت مرسوله نامعتبر است.']; } if (($response['IsSuccess'] ?? false) !== true) { return [ 'error' => true, 'message' => sanitize_text_field($response['Message'] ?? $response['message'] ?? 'ثبت مرسوله ناموفق بود.'), ]; } $submitted_parcels = $response['Data'] ?? null; if (!is_array($submitted_parcels)) { return ['error' => true, 'message' => 'جزئیات مرسوله ثبت‌شده در پاسخ سرویس موجود نیست.']; } $result = []; foreach ($submitted_parcels as $parcel) { if (!is_array($parcel)) { continue; } $is_success = ($parcel['IsSuccess'] ?? false) === true; $reference_no = (string) ($parcel['CustomReferenceNo'] ?? ''); $result[] = [ 'isSuccess' => $is_success, 'message' => sanitize_text_field($parcel['Message'] ?? 'ثبت مرسوله ناموفق بود.'), 'data' => [ 'custom_reference_no' => $reference_no, 'custom_order_no' => sanitize_text_field($parcel['CustomOrderNo'] ?? ''), 'parcel_no' => (string) ($parcel['ParcelNo'] ?? ''), 'parcel_shenase' => sanitize_text_field($parcel['ParcelShenase'] ?? ''), 'shipments' => [[ 'courier' => [ 'courierServiceName' => sanitize_text_field($parcel['ServiceName'] ?? $parcel['CourierName'] ?? ''), ], 'tracking' => [ 'barcode' => sanitize_text_field($parcel['Barcode'] ?? ''), ], 'custom_reference_no' => $reference_no, ]], ], ]; } if (empty($result)) { return ['error' => true, 'message' => 'هیچ نتیجه‌ای برای مرسوله‌های ثبت‌شده دریافت نشد.']; } return ['result' => $result]; } //Send Order To Postex function postexketab_submit_form() { if (!isset($_POST['security']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['security'])), 'postex_submit_nonce')) { wp_send_json_error(['message' => 'دسترسی غیرمجاز']); } postexketab_require_shop_manager(); $allowed_keys = [ 'customer_name', 'customer_lastname', 'customer_phone', 'customer_address', 'customer_postal_code', 'customer_city_label', 'customer_city', 'payment_type', 'box_type_id', 'collect_type', 'is_fragile', 'is_liquid', 'package_request', 'sms_request', 'insurance_request', 'label_request', 'total_weight', 'height', 'width', 'length', 'total_value', ]; $form_data = []; foreach ($allowed_keys as $key) { if (isset($_POST[$key])) { $form_data[$key] = sanitize_text_field(wp_unslash($_POST[$key])); } } //Form information $form_data = postexketab_get_request_data($form_data); $form_data['customer_phone'] = postexketab_validate_format_phone($form_data['customer_phone']); if ($form_data['customer_phone'] === '') { wp_send_json_error(['message' => 'شماره موبایل گیرنده معتبر نیست.']); } //courier information $courier_type = sanitize_text_field(wp_unslash($_POST['courier_type'] ?? '')); $courier_name = sanitize_text_field(wp_unslash($_POST['courier_name'] ?? '')); $order_id = sanitize_text_field(wp_unslash($_POST['order_id'] ?? '')); // Custom recursive sanitize function is used here for sanitizing nested arrays. // PHPCS warning suppressed intentionally as postex_sanitize_array_recursive() safely handles all values. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized via postex_sanitize_array_recursive() $selected_products = isset($_POST['selected_products']) ? postexketab_sanitize_array_recursive(wp_unslash($_POST['selected_products'])) : array(); $order = null; if (ctype_digit((string) $order_id)) { $order = wc_get_order(absint($order_id)); if (!$order || !postexketab_order_needs_shipping($order)) { wp_send_json_error('این سفارش محصول فیزیکی قابل ارسال ندارد.'); } $selected_products = postexketab_filter_selected_shippable_products($order, $selected_products); if (empty($selected_products)) { wp_send_json_error('هیچ محصول فیزیکی برای ارسال انتخاب نشده است.'); } $selected_product_ids = array_column($selected_products, 'sku'); $form_data['total_value'] = postexketab_convert_order_total_value( postexketab_get_shippable_order_value($order, $selected_product_ids) ); } $order_note = $order ? sanitize_textarea_field($order->get_customer_note()) : ''; $custom_order_no = postexketab_generate_unique_parcel_identifier($order ? $order->get_order_number() : 'direct'); $custom_reference_no = postexketab_generate_unique_parcel_identifier($order ? $order->get_id() : 'direct'); // Prepare the API request body $body = [ "custom_batch_no" => (string) $order_id, "collection_type" => $form_data['collect_type'], "submit_channel" => POSTEXKETAB_SUBMIT_CHANNEL, "submit_source" => POSTEXKETAB_SUBMIT_SOURCE, "remark" => $order_note, "parcels" => [ [ "custom_order_no" => $custom_order_no, "custom_reference_no" => $custom_reference_no, "submit_channel" => POSTEXKETAB_SUBMIT_CHANNEL, "submit_source" => POSTEXKETAB_SUBMIT_SOURCE, "to" => [ "contact" => [ "first_name" => $form_data['customer_name'], "last_name" => $form_data['customer_lastname'], "mobile_no" => $form_data['customer_phone'], "national_code" => postexketab_get_order_national_code($order), ], "location" => [ "city_id" => $form_data['customer_city'], "address" => $form_data['customer_address'], "post_code" => $form_data['customer_postal_code'], "country" => "IR", ] ], "parcel_properties" => [ "box_type_id" => $form_data['box_type_id'], "total_weight" => $form_data['total_weight'], "total_value" => $form_data['total_value'], "is_fragile" => $form_data['is_fragile'], "is_liquid" => $form_data['is_liquid'], ], "courier" => [ "name" => $courier_name, "service_type" => $courier_type, "payment_type" => $form_data['payment_type'], ], "added_service" => [ "request_label" => $form_data['label_request'], "request_packaging" => $form_data['package_request'], "request_sms_notification" => $form_data['sms_request'], ], "parcel_items" => postexketab_build_api_parcel_items($selected_products), "remarks" => $order_note, ] ] ]; $api_url = POSTEXKETAB_API_PATH . "/shipping/orders"; $response = postexketab_normalize_bulk_submission_response(postexketab_post_call($api_url, $body)); $page_source = !empty($_POST['source']) ? sanitize_text_field(wp_unslash($_POST['source'])) : 'default'; $base_path = POSTEXKETAB_PLUGIN_PATH . 'template-parts/ajax-templates/'; $file_path = $base_path . basename($page_source) . '.php'; if ((is_array($response) && isset($response['error'])) || !file_exists($file_path)) { wp_send_json_error($response['message'] ?? 'نامشخص'); } else { if (isset($response['result'])) { if (isset($response['result'][0]['isSuccess']) && !$response['result'][0]['isSuccess']) { wp_send_json_error($response['result'][0]['message']); } else { ob_start(); include $file_path; $response_content = ob_get_clean(); wp_send_json_success($response_content); } } } wp_send_json_error('نتیجه ثبت مرسوله قابل پردازش نیست.'); } add_action('wp_ajax_postex_submit_form', 'postexketab_submit_form'); //Sanitized Array function postexketab_sanitize_array_recursive($array) { if (!is_array($array)) { return []; } foreach ($array as $key => $value) { if (is_array($value)) { $array[$key] = postexketab_sanitize_array_recursive($value); } else { $array[$key] = sanitize_text_field(wp_unslash($value)); } } return $array; } //Get Postex Boxes function postexketab_get_size() { $cached_boxes = get_transient('postex_size_cache_v2'); if (is_array($cached_boxes)) { return $cached_boxes; } $response = postexketab_get_call(POSTEXKETAB_API_PATH . '/shipping/boxes'); $items = $response['data']['Data'] ?? null; if (empty($response['success']) || !is_array($items)) { return false; } // Preserve the field names consumed by all existing forms and request builders. $boxes = []; foreach ($items as $item) { if (!isset($item['Id'], $item['Name'])) continue; $boxes[] = [ 'id' => (int) $item['Id'], 'box_type_name' => (string) $item['Name'], 'length' => isset($item['Length']) ? (float) $item['Length'] : 0, 'width' => isset($item['Width']) ? (float) $item['Width'] : 0, 'height' => isset($item['Height']) ? (float) $item['Height'] : 0, ]; } set_transient('postex_size_cache_v2', $boxes, DAY_IN_SECONDS); return $boxes; } //Get Postex Cities function postexketab_get_cities($province_id) { $province_id = absint($province_id); if (!$province_id) return []; $data = postexketab_get_locality_data('cities?provinceId=' . $province_id); if ($data === false) return false; $cities = []; foreach ($data as $item) { if (!isset($item['Code'], $item['ProvinceId'], $item['Name']) || (int) $item['ProvinceId'] !== $province_id) continue; // The quote/order APIs expect the city Code, not the internal Id. $cities[] = ['id' => (int) $item['Code'], 'name' => $item['Name']]; } return $cities; } // Resolve WooCommerce city names using the same current service as the forms. function postexketab_find_city_id_by_name($state_code, $city_name) { $states = postexketab_get_states(); $province_id = $states[$state_code]['postex_id'] ?? (is_numeric($state_code) ? (int) $state_code : 0); if (!$province_id || !$city_name) return null; $cities = postexketab_get_cities($province_id); if (!is_array($cities)) return null; foreach ($cities as $city) { if ((string) $city['id'] === (string) $city_name || postexketab_normalize_locality_name($city['name']) === postexketab_normalize_locality_name($city_name)) { return $city['id']; } } return null; } //Get Postex Shipping Methods function postexketab_get_shipping_methods() { $allowed_couriers = ['CHAPAR', 'POSTEX_EXPRESS', 'IR_POST', 'DEKAPOST']; // New cache key prevents results from the retired endpoint being shown for // up to a day after upgrading the plugin. $cached_shipping_methods = get_transient('postex_shipping_methods_cache_v2'); if (!empty($cached_shipping_methods)) { return $cached_shipping_methods; } $shipping_methods = postexketab_get_call(POSTEXKETAB_API_PATH . '/shipping-methods?activeOnly=true'); $data = $shipping_methods['data']['Data'] ?? $shipping_methods['data']['data'] ?? []; if (isset($shipping_methods['success']) && $shipping_methods['success'] === true && is_array($data)) { $filtered = []; foreach ($data as $item) { if (!is_array($item)) { continue; } // The new API uses PascalCase, while the existing select fields // consume camelCase values. Normalize at the API boundary. $courier_code = $item['CourierCode'] ?? $item['courierCode'] ?? ''; $service_code = $item['CourierServiceCode'] ?? $item['courierServiceCode'] ?? ''; $service_name = $item['CourierServiceName'] ?? $item['courierServiceName'] ?? ''; if (!in_array($courier_code, $allowed_couriers, true) || $service_code === 'DGX_GOLD') { continue; } $filtered[] = [ 'courierCode' => $courier_code, 'courierServiceCode' => $service_code, 'courierServiceName' => $service_name, ]; } set_transient('postex_shipping_methods_cache_v2', $filtered, DAY_IN_SECONDS); return $filtered; } else { return false; } } //Get Postex Payment Methods function postexketab_get_payment_methods() { return [['value' => 'SENDER', 'name' => 'پیش کرایه (آنلاین)']]; } //Get Postex Label function postexketab_get_label() { check_ajax_referer('postex_general_nonce', 'nonce'); postexketab_require_shop_manager(); if (empty($_POST['tracking_code'])) { wp_send_json_error(['message' => 'کد مرسوله وجود ندارد']); } $api_key = esc_attr(get_option('postex_api_key', '')); $label_identifier = sanitize_text_field(wp_unslash($_POST['tracking_code'])); $api_url = POSTEXKETAB_API_PATH . '/parcels/' . rawurlencode($label_identifier) . '/label'; $response = wp_remote_get( $api_url, [ 'headers' => [ 'x-api-key' => $api_key, ], 'timeout' => 30, ] ); if (is_wp_error($response)) { wp_send_json_error(['message' => 'خطا در ارتباط با سرور: ' . $response->get_error_message()]); } $response_code = wp_remote_retrieve_response_code($response); $response_body = wp_remote_retrieve_body($response); if (200 !== $response_code) { $error_data = json_decode($response_body, true); $error_message = is_array($error_data) ? ($error_data['Message'] ?? $error_data['message'] ?? $error_data['Detail'] ?? $error_data['detail'] ?? '') : ''; if (is_array($error_message)) { $error_message = implode('، ', array_filter(array_map('strval', $error_message))); } wp_send_json_error([ 'message' => sanitize_text_field($error_message ?: 'مشکل در دریافت لیبل. کد: ' . $response_code), ]); } $pdf_content = $response_body; if (empty($pdf_content)) { wp_send_json_error(['message' => 'فایل دریافت نشد.']); } if (strncmp($pdf_content, '%PDF-', 5) !== 0) { $error_data = json_decode($pdf_content, true); $error_message = is_array($error_data) ? ($error_data['Message'] ?? $error_data['message'] ?? 'خطا در دریافت فایل لیبل.') : 'پاسخ دریافتی فایل PDF نیست.'; wp_send_json_error(['message' => sanitize_text_field($error_message)]); } // Set headers for file download nocache_headers(); header('Content-Description: File Transfer'); header('Content-Type: application/pdf'); header("Content-Disposition: attachment; filename=\"postex_label_{$label_identifier}.pdf\""); header('Expires: 0'); header('Cache-Control: must-revalidate'); header('Pragma: public'); header('Content-Length: ' . strlen($pdf_content)); // Output PDF content directly to browser. // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Binary PDF content being sent directly to the browser. echo $pdf_content; exit; } add_action('wp_ajax_postex_get_label', 'postexketab_get_label'); //Sanitize Price function postexketab_sanitize_price($value) { if (!is_scalar($value)) { return 0; } return absint(str_replace(',', '', (string) $value)); } //Show Bulk Send Modal Ajax add_action('wp_ajax_postex_order_details_modal', 'postexketab_load_order_details_modal'); function postexketab_load_order_details_modal() { if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'postex_general_nonce')) { wp_send_json_error(array('message' => 'خطای امنیتی: اعتبارسنجی ناموفق بود.')); wp_die(); } postexketab_require_shop_manager(); ob_start(); $order_id = isset($_POST['order_id']) ? intval(wp_unslash($_POST['order_id'])) : 0; $order = wc_get_order($order_id); if (!$order || !postexketab_order_needs_shipping($order)) { echo '

این سفارش محصول فیزیکی قابل ارسال ندارد.

'; wp_die(); } $file_path = POSTEXKETAB_PLUGIN_PATH . 'template-parts/panel-views/order-details-modal.php'; include($file_path); $output = ob_get_clean(); echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped wp_die(); } //Set Product and Order status (Shipped Or Not) function postexketab_products_shipped_setter($order_id, $result_item, $selected_products) { // Get existing shipping data $order = wc_get_order($order_id); $existing_data = $order->get_meta('postex_shipping_data'); if (!is_array($existing_data)) { $existing_data = []; } // Add the new parcel data to existing data $existing_data[] = array( 'parcel_no' => $result_item['data']['parcel_no'], 'parcel_shenase' => $result_item['data']['parcel_shenase'] ?? '', 'custom_order_no' => $result_item['data']['custom_order_no'] ?? '', 'courier_name' => $result_item['data']['shipments'][0]['courier']['courierServiceName'], 'tracking_code' => $result_item['data']['shipments'][0]['tracking']['barcode'], 'products_included' => $selected_products ); // Get the order details $order_product_ids = []; // Collect product IDs from the order foreach (postexketab_get_shippable_order_items($order) as $item) { $order_product_ids[] = postexketab_get_order_item_shipping_product_id($item); } // Compare the product IDs in the existing data with the order's products $product_ids = []; if (is_array($existing_data) && count($existing_data) > 0) { foreach ($existing_data as $item) { foreach ($item['products_included'] as $product) { $product_ids[] = $product['sku']; } } $missing_product_ids = array_diff($order_product_ids, $product_ids); if (count($missing_product_ids) === 0) { $order->update_meta_data('postex_shipping_status', 'complete'); } else { $order->update_meta_data('postex_shipping_status', 'incomplete'); } } $order->update_meta_data('postex_shipping_data', $existing_data); $order->save(); } //Create Request Body For Bulk Quote And Submit function postexketab_bulk_quote_data_creator($data, $source) { $orders = $data['orders'] ?? []; $payment_type = 'SENDER'; $collect_type = $data['collect_type'] ?? ""; $box_type_id = $data['box_type_id'] ?? ""; $shipping_method = $data['shipping_method'] ?? ""; $shipping_method_parts = explode('|', $shipping_method, 2); if (empty($orders)) { return false; } $shop_data = postexketab_get_shop_data(); $counter = 0; $parcels = []; foreach ($orders as $order_id => $fields) { $order = wc_get_order($order_id); if (!$order || !postexketab_order_needs_shipping($order)) { continue; } $fields = $data['orders'][$order_id]; $fields['total_with_tax'] = postexketab_convert_order_total_value( postexketab_get_shippable_order_value($order) ); $total_weight = $fields['weight']; $height = 0; $width = 0; $length = 0; $boxes = postexketab_get_size(); foreach ($boxes as $box) { if ($box_type_id == $box['id']) { $height = $box['height']; $width = $box['width']; $length = $box['length']; } } $city_code = $fields['shipping_city']; if ($source == 'quote') { $parcels[$counter] = array( "custom_parcel_id" => strval($order_id), "to_city_code" => $city_code, "payment_type" => $payment_type, "submit_channel" => POSTEXKETAB_SUBMIT_CHANNEL, "submit_source" => POSTEXKETAB_SUBMIT_SOURCE, "parcel_properties" => [ "length" => $length, "width" => $width, "height" => $height, "box_type_id" => $box_type_id, "is_fragile" => in_array('is_fragile', $shop_data['postex_default_additional']), "is_liquid" => in_array('is_liquid', $shop_data['postex_default_additional']), "total_weight" => $total_weight, "total_value" => $fields['total_with_tax'], "pre_paid_amount" => 0, "total_value_currency" => "IRR" ], ); $counter++; } if ($source == 'bulk') { if (!isset($city_code) || intval($city_code) < 1) { $city_code = 0; } $parcel_items = []; foreach (postexketab_get_shippable_order_items($order) as $item) { $product_id = postexketab_get_order_item_shipping_product_id($item); $dimensions = postexketab_get_product_dimensions_weight($product_id); $quantity = max(1, (int) $item->get_quantity()); $unit_price = postexketab_convert_order_total_value( ((float) $item->get_total() + (float) $item->get_total_tax()) / $quantity ); $parcel_items[] = [ 'product_id' => $product_id, 'description' => postexketab_limit_parcel_item_description($item->get_name()), 'quantity' => $quantity, 'price' => $unit_price, 'weight' => is_array($dimensions) ? (float) ($dimensions['weight'] ?? 0) : 0, ]; } $custom_order_no = postexketab_generate_unique_parcel_identifier($order->get_order_number()); $custom_reference_no = postexketab_generate_unique_parcel_identifier($order->get_id()); $parcels[$counter] = [ "custom_order_no" => $custom_order_no, "custom_reference_no" => $custom_reference_no, "submit_channel" => POSTEXKETAB_SUBMIT_CHANNEL, "submit_source" => POSTEXKETAB_SUBMIT_SOURCE, "to" => [ "contact" => [ "first_name" => $fields['shipping_first_name'], "last_name" => $fields['shipping_last_name'], "mobile_no" => postexketab_validate_format_phone($fields['shipping_phone']), "national_code" => postexketab_get_order_national_code($order), ], "location" => [ "city_id" => $city_code, "address" => $fields['shipping_address_1'], "post_code" => $fields['shipping_postcode'], "country" => "IR", ] ], "parcel_properties" => [ "box_type_id" => $box_type_id, "total_weight" => $total_weight, "total_value" => $fields['total_with_tax'], "is_fragile" => in_array('is_fragile', $shop_data['postex_default_additional']), "is_liquid" => in_array('is_liquid', $shop_data['postex_default_additional']), ], "courier" => [ "name" => $shipping_method_parts[0], "service_type" => $shipping_method_parts[1] ?? '', "payment_type" => $payment_type, ], "added_service" => [ "request_label" => in_array('label_request', $shop_data['postex_default_additional']), "request_packaging" => in_array('package_request', $shop_data['postex_default_additional']), "request_sms_notification" => in_array('sms_request', $shop_data['postex_default_additional']), ], "parcel_items" => $parcel_items, "remarks" => sanitize_textarea_field($order->get_customer_note()), ]; $counter++; } } if (empty($parcels)) { return false; } if ($source == 'quote') { $request_data = [ 'collection_type' => $collect_type, 'submit_channel' => POSTEXKETAB_SUBMIT_CHANNEL, 'submit_source' => POSTEXKETAB_SUBMIT_SOURCE, 'courier' => postexketab_get_quote_courier($shipping_method), 'parcels' => $parcels, 'value_added_service' => [ 'request_label' => in_array('label_request', $shop_data['postex_default_additional']), 'request_packaging' => in_array('package_request', $shop_data['postex_default_additional']), 'request_sms_notification' => in_array('sms_request', $shop_data['postex_default_additional']), 'print_logo' => false, ], ]; } if ($source == 'bulk') { $request_data = [ "custom_batch_no" => "wp-plugin-" . gmdate('YmdHis'), "collection_type" => $collect_type, "submit_channel" => POSTEXKETAB_SUBMIT_CHANNEL, "submit_source" => POSTEXKETAB_SUBMIT_SOURCE, "remark" => "", "parcels" => $parcels ]; } return $request_data; } //Bulk Quote Price Handler add_action('wp_ajax_postex_bulk_quote_price', 'postexketab_bulk_quote_price'); function postexketab_bulk_quote_price() { if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'postex_general_nonce')) { wp_send_json_error(array('message' => 'خطای امنیتی: اعتبارسنجی ناموفق بود.')); wp_die(); } postexketab_require_shop_manager(); $filtered_data = [ // Custom recursive sanitize function is used here for sanitizing nested arrays. // PHPCS warning suppressed intentionally as postex_sanitize_array_recursive() safely handles all values. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized via postex_sanitize_array_recursive() 'orders' => isset($_POST['orders']) ? postexketab_sanitize_array_recursive(wp_unslash($_POST['orders'])) : array(), 'payment_type' => isset($_POST['payment_type']) ? sanitize_text_field(wp_unslash($_POST['payment_type'])) : '', 'collect_type' => isset($_POST['collect_type']) ? sanitize_text_field(wp_unslash($_POST['collect_type'])) : '', 'box_type_id' => isset($_POST['box_type_id']) ? intval($_POST['box_type_id']) : 0, 'shipping_method' => isset($_POST['shipping_method']) ? sanitize_text_field(wp_unslash($_POST['shipping_method'])) : '', ]; $request_data = postexketab_bulk_quote_data_creator($filtered_data, 'quote'); if (!$request_data) { wp_send_json_error('هیچ سفارشی دریافت نشد'); } $response = postexketab_post_call(POSTEXKETAB_API_PATH . '/shipping/quotes', $request_data); if (!is_array($response)) { wp_send_json_error(['message' => 'ساختار پاسخ استعلام قیمت نامعتبر است.']); } if (isset($response['error']) || ($response['IsSuccess'] ?? false) !== true) { wp_send_json_error(['message' => $response['Message'] ?? $response['message'] ?? 'خطا در استعلام قیمت']); } $quote_methods = $response['Data'] ?? []; $result = ['initPrice' => [], 'totalPrice' => []]; $fallback_order_ids = array_keys($filtered_data['orders']); foreach ($quote_methods as $index => $quote_method) { if (!is_array($quote_method)) { continue; } $parcel_id = $quote_method['CustomParcelId'] ?? $quote_method['custom_parcel_id'] ?? $quote_method['ParcelId'] ?? $quote_method['parcel_id'] ?? (is_string($index) ? $index : ''); if ($parcel_id === '' && count($fallback_order_ids) === 1) { $parcel_id = (string) $fallback_order_ids[0]; } $total_price = $quote_method['TotalPrice'] ?? $quote_method['total_price'] ?? $quote_method['Price'] ?? $quote_method['price'] ?? null; if ($parcel_id === '' || !is_numeric($total_price)) { continue; } $pickup_price = $quote_method['PickupPrice'] ?? $quote_method['pickup_price'] ?? 0; $initial_price = $quote_method['InitPrice'] ?? $quote_method['init_price'] ?? $quote_method['ShippingPrice'] ?? $quote_method['shipping_price'] ?? $total_price; $result['initPrice'][(string) $parcel_id] = (float) $initial_price + (float) $pickup_price; $result['totalPrice'][(string) $parcel_id] = (float) $total_price + (float) $pickup_price; } // The API may omit CustomParcelId for some parcels in a multi-parcel // response. Requote only those parcels individually so every selected // order can still receive a price in the bulk table. foreach ($request_data['parcels'] as $request_parcel) { $parcel_id = (string) ($request_parcel['custom_parcel_id'] ?? ''); if ($parcel_id === '' || array_key_exists($parcel_id, $result['totalPrice'])) { continue; } $single_request = $request_data; $single_request['parcels'] = [$request_parcel]; $single_response = postexketab_post_call(POSTEXKETAB_API_PATH . '/shipping/quotes', $single_request); if (!is_array($single_response) || ($single_response['IsSuccess'] ?? false) !== true) { continue; } foreach ((array) ($single_response['Data'] ?? []) as $quote_method) { if (!is_array($quote_method)) { continue; } $total_price = $quote_method['TotalPrice'] ?? $quote_method['total_price'] ?? $quote_method['Price'] ?? $quote_method['price'] ?? null; if (!is_numeric($total_price)) { continue; } $pickup_price = $quote_method['PickupPrice'] ?? $quote_method['pickup_price'] ?? 0; $initial_price = $quote_method['InitPrice'] ?? $quote_method['init_price'] ?? $quote_method['ShippingPrice'] ?? $quote_method['shipping_price'] ?? $total_price; $result['initPrice'][$parcel_id] = (float) $initial_price + (float) $pickup_price; $result['totalPrice'][$parcel_id] = (float) $total_price + (float) $pickup_price; break; } } if (empty($result['totalPrice'])) { wp_send_json_error(['message' => $response['Message'] ?? 'برای سفارش‌های انتخاب‌شده قیمت قابل‌نمایش دریافت نشد.']); } wp_send_json_success($result); wp_die(); } //Submit Bulk Handler add_action('wp_ajax_postex_bulk_send', 'postexketab_bulk_send'); function postexketab_bulk_send() { if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'postex_general_nonce')) { wp_send_json_error(array('message' => 'خطای امنیتی: اعتبارسنجی ناموفق بود.')); wp_die(); } postexketab_require_shop_manager(); $filtered_data = [ // Custom recursive sanitize function is used here for sanitizing nested arrays. // PHPCS warning suppressed intentionally as postex_sanitize_array_recursive() safely handles all values. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized via postex_sanitize_array_recursive() 'orders' => isset($_POST['orders']) ? postexketab_sanitize_array_recursive(wp_unslash($_POST['orders'])) : array(), 'payment_type' => isset($_POST['payment_type']) ? sanitize_text_field(wp_unslash($_POST['payment_type'])) : '', 'collect_type' => isset($_POST['collect_type']) ? sanitize_text_field(wp_unslash($_POST['collect_type'])) : '', 'box_type_id' => isset($_POST['box_type_id']) ? intval($_POST['box_type_id']) : 0, 'shipping_method' => isset($_POST['shipping_method']) ? sanitize_text_field(wp_unslash($_POST['shipping_method'])) : '', ]; $request_data = postexketab_bulk_quote_data_creator($filtered_data, 'bulk'); if (!$request_data) { wp_send_json_error('هیچ سفارش دارای محصول فیزیکی انتخاب نشده است.'); } // Keep the WooCommerce item identifiers locally for recording successful // shipments; API parcel_items use product_id instead of this internal sku key. $selected_products = []; foreach (array_keys($filtered_data['orders']) as $selected_order_id) { $selected_order = wc_get_order($selected_order_id); if (!$selected_order || !postexketab_order_needs_shipping($selected_order)) { continue; } $selected_products[$selected_order_id] = []; foreach (postexketab_get_shippable_order_items($selected_order) as $item) { $selected_products[$selected_order_id][] = [ 'description' => postexketab_limit_parcel_item_description($item->get_name()), 'sku' => strval(postexketab_get_order_item_shipping_product_id($item)), 'quantity' => $item->get_quantity(), 'price' => $item->get_subtotal(), ]; } } if (!$request_data) { wp_send_json_error('هیچ سفارشی دریافت نشد'); } $custom_reference_order_map = []; foreach ($request_data['parcels'] as $request_parcel) { $reference_no = (string) ($request_parcel['custom_reference_no'] ?? ''); if ($reference_no !== '' && preg_match('/^(\d+)-\d{5}$/', $reference_no, $matches)) { $custom_reference_order_map[$reference_no] = absint($matches[1]); } } $api_url = POSTEXKETAB_API_PATH . "/shipping/orders"; $response = postexketab_normalize_bulk_submission_response(postexketab_post_call($api_url, $request_data)); if (is_array($response)) { if (isset($response['error'])) { wp_send_json_error($response['message'] ?? 'ثبت مرسوله ناموفق بود.'); } if (isset($response['result'])) { foreach ($response['result'] as $shipped_parcel) { $reference_no = (string) ($shipped_parcel['data']['custom_reference_no'] ?? ''); $order_id = $custom_reference_order_map[$reference_no] ?? 0; $id = $order_id ?: $reference_no; if ((is_array($shipped_parcel) && isset($shipped_parcel['isSuccess']) && !$shipped_parcel['isSuccess'])) { $bulk_result[$id]['success'] = false; $bulk_result[$id]['message'] = $shipped_parcel['message']; } else { $bulk_result[$id]['success'] = true; $bulk_result[$id]['barcode'] = $shipped_parcel['data']['shipments'][0]['tracking']['barcode']; $bulk_result[$id]['parcel_no'] = $shipped_parcel['data']['parcel_no']; $bulk_result[$id]['parcel_shenase'] = $shipped_parcel['data']['parcel_shenase'] ?? ''; $bulk_result[$id]['custom_order_no'] = $shipped_parcel['data']['custom_order_no'] ?? ''; postexketab_products_shipped_setter($order_id, $shipped_parcel, $selected_products[$order_id]); } } if (isset($bulk_result)) { wp_send_json_success($bulk_result); } } } wp_die(); }