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();
}