ArcWOC/includes/class-hesabix-v2-mapper.php
morrning 8aedcf2500 Release 4.9.0: invoice profit in WP + marketplace license banner
Show Hesabix invoice profit on ArcWOC orders list and order meta box,
refresh after sync, and surface WooCommerce marketplace license upsell
(hidden when active). Also includes 4.8.2 bank/cash take≤100 fixes.
2026-08-04 12:56:06 +00:00

1725 lines
53 KiB
PHP
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Data Mapper Class - Convert WooCommerce data to Hesabix V2 API format
*
* @since 2.0.0
* @package Hesabix_V2
* @subpackage Hesabix_V2/includes
*/
class Hesabix_V2_Mapper
{
/**
* کش مسطح درخت دسته‌های حسابیکس برای تطبیق نام (هر درخواست PHP).
*
* @var array<int, array{id:int,parent_id:?int,label:string}>|null
*/
private static $hx_category_flat_cache = null;
/**
* کش شناسه‌های ترم product_cat مرتب‌شده بر اساس عمق (هر درخواست PHP).
*
* @var int[]|null
*/
private static $wc_product_cat_ids_sorted_cache = null;
/**
* Convert WooCommerce product to Hesabix API format
*
* @since 2.0.0
* @param WC_Product $product
* @param int $wc_id
* @return array
*/
public static function wc_product_to_api($product, $wc_id, $amount_factor = 1.0)
{
$name_fa = Hesabix_V2_Validation::sanitize_product_name($product->get_title());
$f = (float) $amount_factor;
if ($f <= 0 || is_nan($f)) {
$f = 1.0;
}
$sync_settings = get_option('hesabix_v2_sync_settings', array());
$cats_on = !isset($sync_settings['sync_product_categories']) || !empty($sync_settings['sync_product_categories']);
// Get category
$category_id = null;
if ($cats_on) {
$wc_cat = self::pick_wc_product_category_term_id($product);
if ($wc_cat !== null) {
$category_id = self::get_or_create_category_mapping($wc_cat);
}
}
// Get price
$sell_price = 0;
if ($product->get_regular_price()) {
$sell_price = Hesabix_V2_Validation::sanitize_price((float) $product->get_regular_price() * $f);
} elseif ($product->get_price()) {
$sell_price = Hesabix_V2_Validation::sanitize_price((float) $product->get_price() * $f);
}
return array(
'name' => $name_fa,
'item_type' => $product->is_virtual() ? 'خدمت' : 'کالا',
'main_unit' => 'عدد',
'base_sales_price' => $sell_price,
'barcode' => Hesabix_V2_Validation::sanitize_barcode($product->get_sku()),
'category_id' => $category_id,
'track_inventory' => $product->managing_stock(),
'description' => $product->get_description() ? mb_substr($product->get_description(), 0, 500) : null,
'is_active' => true,
);
}
/**
* Convert WooCommerce product variation to Hesabix API format
*
* @since 2.0.0
* @param WC_Product $parent_product
* @param WC_Product_Variation $variation
* @param int $parent_id
* @return array
*/
public static function wc_variation_to_api($parent_product, $variation, $parent_id, $amount_factor = 1.0)
{
$variation_id = $variation->get_id();
$parent_name = $parent_product->get_title();
$variation_name = $variation->get_attribute_summary();
$f = (float) $amount_factor;
if ($f <= 0 || is_nan($f)) {
$f = 1.0;
}
// Build full name
$full_name = $parent_name;
if (!empty($variation_name)) {
$full_name .= ' - ' . $variation_name;
}
$full_name = Hesabix_V2_Validation::sanitize_product_name($full_name);
// Get category from parent
$sync_settings = get_option('hesabix_v2_sync_settings', array());
$cats_on = !isset($sync_settings['sync_product_categories']) || !empty($sync_settings['sync_product_categories']);
$category_id = null;
if ($cats_on) {
$wc_cat = self::pick_wc_product_category_term_id($parent_product);
if ($wc_cat !== null) {
$category_id = self::get_or_create_category_mapping($wc_cat);
}
}
// Get price
$sell_price = 0;
if ($variation->get_regular_price()) {
$sell_price = Hesabix_V2_Validation::sanitize_price((float) $variation->get_regular_price() * $f);
} elseif ($variation->get_price()) {
$sell_price = Hesabix_V2_Validation::sanitize_price((float) $variation->get_price() * $f);
}
return array(
'name' => $full_name,
'item_type' => $variation->is_virtual() ? 'خدمت' : 'کالا',
'main_unit' => 'عدد',
'base_sales_price' => $sell_price,
'barcode' => Hesabix_V2_Validation::sanitize_barcode($variation->get_sku()),
'category_id' => $category_id,
'track_inventory' => $variation->managing_stock(),
'is_active' => true,
);
}
/**
* مقدار track_inventory حسابیکس بر اساس سیاست انتخابی (وقتی همگام‌سازی موجودی روشن باشد).
*
* @param WC_Product $product محصول ساده یا واریانت.
* @param string $policy یکی از: wc، physical_always، always_on، always_off
* @return bool
*/
public static function resolve_track_inventory_by_policy($product, $policy)
{
if (!$product instanceof WC_Product) {
return false;
}
$p = is_string($policy) ? sanitize_key($policy) : 'wc';
switch ($p) {
case 'physical_always':
return !$product->is_virtual();
case 'always_on':
return true;
case 'always_off':
return false;
case 'wc':
default:
return $product->managing_stock();
}
}
/**
* آیا برای موجودی اولیه / نمایش موجودی ووکامرس از این قلم استفاده شود؟
*
* @param WC_Product $product
* @param string $policy
* @return bool
*/
public static function wc_product_qualifies_for_opening_stock_qty($product, $policy)
{
if (!$product instanceof WC_Product || $product->is_virtual()) {
return false;
}
$p = is_string($policy) ? sanitize_key($policy) : 'wc';
switch ($p) {
case 'physical_always':
case 'always_on':
return true;
case 'always_off':
return $product->managing_stock();
case 'wc':
default:
return $product->managing_stock();
}
}
/**
* Convert WooCommerce customer to Hesabix API format
*
* @since 2.0.0
* @param WC_Customer $customer
* @param WC_Order $order Optional order for additional data
* @return array
*/
public static function wc_customer_to_api($customer, $order = null)
{
$customer_id = $customer->get_id();
$first_name = $customer->get_first_name();
$last_name = $customer->get_last_name();
// Fallback: از سفارش (اطلاعات صورتحساب)
if (empty($first_name) && empty($last_name) && $order) {
$first_name = $order->get_billing_first_name();
$last_name = $order->get_billing_last_name();
}
// Fallback: متای کاربر وردپرس / ووکامرس (بilling یا نام پروفایل)
if (empty($first_name) && empty($last_name) && $customer_id) {
$first_name = get_user_meta($customer_id, 'billing_first_name', true)
?: get_user_meta($customer_id, 'first_name', true);
$last_name = get_user_meta($customer_id, 'billing_last_name', true)
?: get_user_meta($customer_id, 'last_name', true);
}
// Fallback: نام نمایشی کاربر (مثلاً "علی محمدی")
if (empty($first_name) && empty($last_name) && $customer_id) {
$user = get_userdata($customer_id);
if ($user && !empty($user->display_name)) {
$parts = preg_split('/\s+/u', trim($user->display_name), 2);
$first_name = $parts[0] ?? '';
$last_name = $parts[1] ?? '';
}
}
// فقط در صورت خالی ماندن هر دو: مقدار پیش‌فرض
if (empty($first_name) && empty($last_name)) {
$first_name = 'مشتری';
$last_name = 'گرامی';
}
$data = array(
'alias_name' => trim($first_name . ' ' . $last_name),
'first_name' => $first_name ?: 'مشتری',
'last_name' => $last_name ?: 'گرامی',
'person_type' => 'مشتری',
'mobile' => Hesabix_V2_Validation::sanitize_mobile($customer->get_billing_phone()),
'email' => Hesabix_V2_Validation::sanitize_email($customer->get_email()),
);
// Add address data from order if available
if ($order) {
$address = $order->get_billing_address_1();
if ($order->get_billing_address_2()) {
$address .= ' - ' . $order->get_billing_address_2();
}
$data['address'] = Hesabix_V2_Validation::sanitize_address($address);
$data['city'] = $order->get_billing_city();
$data['postal_code'] = Hesabix_V2_Validation::sanitize_postal_code($order->get_billing_postcode());
$national_id = $order->get_meta('_billing_hesabix_v2_national_id');
if ($national_id) {
$data['national_id'] = Hesabix_V2_Validation::sanitize_national_id($national_id);
}
$economic_code = $order->get_meta('_billing_hesabix_v2_economic_code');
if ($economic_code) {
$data['economic_id'] = $economic_code;
}
} elseif ($customer_id) {
self::merge_wc_customer_billing_into_person_payload($customer, $data);
}
// اگر هر دو موبایل و ایمیل خالی باشند، API اغلب 422 برمی‌گرداند؛ یک مقدار placeholder بگذار
if (empty($data['mobile']) && empty($data['email'])) {
$data['email'] = 'wc-' . $customer->get_id() . '@woocommerce-placeholder.local';
}
if (!empty($data['mobile']) && preg_match('/^0+$/', $data['mobile'])) {
$data['mobile'] = '09100000000';
}
// Remove null values
return array_filter($data, function($value) {
return $value !== null && $value !== '';
});
}
/**
* پر کردن آدرس و تماس از صورت‌حساب ذخیره‌شده روی WC_Customer وقتی سفارش در دست نیست.
*
* @param WC_Customer $customer
* @param array $data ارجاع به آرایهٔ در حال ساخت برای API
* @return void
*/
private static function merge_wc_customer_billing_into_person_payload($customer, array &$data)
{
$uid = (int) $customer->get_id();
if ($uid < 1) {
return;
}
$line1 = trim((string) $customer->get_billing_address_1());
$line2 = trim((string) $customer->get_billing_address_2());
$address = $line1;
if ($line2 !== '') {
$address .= ($address !== '' ? ' - ' : '') . $line2;
}
if ($address !== '') {
$san = Hesabix_V2_Validation::sanitize_address($address);
if ($san) {
$data['address'] = $san;
}
}
$city = trim((string) $customer->get_billing_city());
if ($city !== '') {
$data['city'] = sanitize_text_field($city);
}
$postcode_raw = (string) $customer->get_billing_postcode();
$postcode = Hesabix_V2_Validation::sanitize_postal_code($postcode_raw);
if (!$postcode && $postcode_raw !== '') {
$digits = preg_replace('/[^0-9]/', '', $postcode_raw);
if ($digits !== '') {
$postcode = substr($digits, 0, 20);
}
}
if ($postcode) {
$data['postal_code'] = $postcode;
}
$state = trim((string) $customer->get_billing_state());
if ($state !== '') {
$data['province'] = mb_substr(sanitize_text_field($state), 0, 100);
}
$country = trim((string) $customer->get_billing_country());
if ($country !== '') {
$data['country'] = mb_substr(sanitize_text_field($country), 0, 100);
}
$company = trim((string) $customer->get_billing_company());
if ($company !== '') {
$data['company_name'] = mb_substr(sanitize_text_field($company), 0, 255);
}
$billing_phone_alt = Hesabix_V2_Validation::sanitize_mobile($customer->get_billing_phone());
if (empty($data['mobile']) && $billing_phone_alt) {
$data['mobile'] = $billing_phone_alt;
}
$nid = get_user_meta($uid, 'billing_hesabix_v2_national_id', true);
if (!$nid) {
$nid = get_user_meta($uid, '_billing_hesabix_v2_national_id', true);
}
if ($nid) {
$san_nid = Hesabix_V2_Validation::sanitize_national_id($nid);
if ($san_nid) {
$data['national_id'] = $san_nid;
}
}
$econ = get_user_meta($uid, 'billing_hesabix_v2_economic_code', true);
if (!$econ) {
$econ = get_user_meta($uid, '_billing_hesabix_v2_economic_code', true);
}
if ($econ) {
$data['economic_id'] = mb_substr(sanitize_text_field((string) $econ), 0, 50);
}
}
/**
* اعمال دادهٔ شخص حسابیکس روی مشتری ووکامرس + متای کاربر.
*
* @param int $user_id
* @param array $person ردیف شخص از API (مثل items[])
* @return void
*/
public static function apply_hesabix_person_to_wc_customer($user_id, array $person)
{
$user_id = (int) $user_id;
if ($user_id < 1 || empty($person['id'])) {
return;
}
$wc = new WC_Customer($user_id);
$fn = isset($person['first_name']) ? trim((string) $person['first_name']) : '';
$ln = isset($person['last_name']) ? trim((string) $person['last_name']) : '';
if ($fn === '' && $ln === '' && !empty($person['alias_name'])) {
$parts = preg_split('/\s+/u', trim((string) $person['alias_name']), 2);
$fn = $parts[0] ?? '';
$ln = isset($parts[1]) ? $parts[1] : '';
}
if ($fn !== '') {
$wc->set_first_name(mb_substr($fn, 0, 100));
}
if ($ln !== '') {
$wc->set_last_name(mb_substr($ln, 0, 100));
}
if ($fn !== '' || $ln !== '') {
$wc->set_display_name(trim($fn . ' ' . $ln));
wp_update_user(
array(
'ID' => $user_id,
'first_name' => $wc->get_first_name(),
'last_name' => $wc->get_last_name(),
'display_name' => trim($wc->get_first_name() . ' ' . $wc->get_last_name()),
)
);
}
if (!empty($person['email'])) {
$new_email = Hesabix_V2_Validation::sanitize_email((string) $person['email']);
if ($new_email) {
$existing = email_exists($new_email);
if (!$existing || (int) $existing === $user_id) {
wp_update_user(array(
'ID' => $user_id,
'user_email' => $new_email,
));
$wc->set_email($new_email);
}
}
}
if (!empty($person['mobile'])) {
$m = Hesabix_V2_Validation::sanitize_mobile((string) $person['mobile']);
if ($m) {
$wc->set_billing_phone($m);
}
} elseif (!empty($person['phone'])) {
$wc->set_billing_phone(mb_substr(sanitize_text_field((string) $person['phone']), 0, 20));
}
if (!empty($person['company_name'])) {
$wc->set_billing_company(mb_substr(sanitize_text_field((string) $person['company_name']), 0, 200));
}
if (!empty($person['address'])) {
$wc->set_billing_address_1(mb_substr(Hesabix_V2_Validation::sanitize_address((string) $person['address']), 0, 200));
}
if (!empty($person['city'])) {
$wc->set_billing_city(mb_substr(sanitize_text_field((string) $person['city']), 0, 100));
}
if (!empty($person['postal_code'])) {
$pc = Hesabix_V2_Validation::sanitize_postal_code((string) $person['postal_code']);
if (!$pc) {
$digits = preg_replace('/[^0-9]/', '', (string) $person['postal_code']);
$pc = $digits !== '' ? substr($digits, 0, 20) : '';
}
if ($pc) {
$wc->set_billing_postcode($pc);
}
}
if (!empty($person['province'])) {
$wc->set_billing_state(mb_substr(sanitize_text_field((string) $person['province']), 0, 100));
}
if (!empty($person['country'])) {
$raw = trim((string) $person['country']);
if (preg_match('/^[A-Za-z]{2}$/', $raw)) {
$wc->set_billing_country(strtoupper($raw));
}
}
if (!empty($person['national_id'])) {
$n = Hesabix_V2_Validation::sanitize_national_id((string) $person['national_id']);
if ($n) {
update_user_meta($user_id, 'billing_hesabix_v2_national_id', $n);
}
}
if (!empty($person['economic_id'])) {
update_user_meta($user_id, 'billing_hesabix_v2_economic_code', mb_substr(sanitize_text_field((string) $person['economic_id']), 0, 50));
}
$wc->save();
}
/**
* Convert guest customer from order to Hesabix API format
*
* @since 2.0.0
* @param WC_Order $order
* @return array
*/
public static function wc_guest_to_api($order)
{
$first_name = $order->get_billing_first_name();
$last_name = $order->get_billing_last_name();
if (empty($first_name) && empty($last_name)) {
$first_name = 'مهمان';
$last_name = 'گرامی';
}
$address = $order->get_billing_address_1();
if ($order->get_billing_address_2()) {
$address .= ' - ' . $order->get_billing_address_2();
}
$mobile = Hesabix_V2_Validation::sanitize_mobile($order->get_billing_phone());
$email = Hesabix_V2_Validation::sanitize_email($order->get_billing_email());
if (empty($mobile) && empty($email)) {
$email = 'guest-' . $order->get_id() . '@woocommerce-placeholder.local';
}
if (!empty($mobile) && preg_match('/^0+$/', $mobile)) {
$mobile = '09100000000';
}
$data = array(
'alias_name' => trim($first_name . ' ' . $last_name),
'first_name' => $first_name ?: 'مهمان',
'last_name' => $last_name ?: 'گرامی',
'person_type' => 'مشتری',
'mobile' => $mobile,
'email' => $email,
'address' => Hesabix_V2_Validation::sanitize_address($address),
'city' => $order->get_billing_city(),
'postal_code' => Hesabix_V2_Validation::sanitize_postal_code($order->get_billing_postcode()),
);
$national_id = $order->get_meta('_billing_hesabix_v2_national_id');
if ($national_id) {
$data['national_id'] = Hesabix_V2_Validation::sanitize_national_id($national_id);
}
$economic_code = $order->get_meta('_billing_hesabix_v2_economic_code');
if ($economic_code) {
$data['economic_id'] = $economic_code;
}
// Remove null values
return array_filter($data, function($value) {
return $value !== null && $value !== '';
});
}
/**
* Convert WooCommerce order to Hesabix invoice format
*
* @since 2.0.0
* @param WC_Order $order
* @param int $person_id
* @param float|null $amount_factor ضریب مبلغ (مثلاً ۱۰ برای تومان→ریال).
* @param int|null $invoice_currency_id شناسهٔ ارز در حسابیکس؛ اگر تهی باشد از تنظیمات/پیش‌فرض حل می‌شود.
* @param array|null $date_overrides اختیاری: `document_date` (Y-m-d)، `payment_date_ymd` (Y-m-d برای بخش تاریخ transaction_date پرداخت).
* @return array
*/
public static function wc_order_to_invoice($order, $person_id, $amount_factor = 1.0, $invoice_currency_id = null, $date_overrides = null)
{
$f = (float) $amount_factor;
if ($f <= 0 || is_nan($f)) {
$f = 1.0;
}
$date_opts = is_array($date_overrides) ? $date_overrides : array();
$document_date_override = isset($date_opts['document_date']) && is_string($date_opts['document_date']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_opts['document_date'])
? $date_opts['document_date']
: null;
$payment_date_ymd = isset($date_opts['payment_date_ymd']) && is_string($date_opts['payment_date_ymd']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_opts['payment_date_ymd'])
? $date_opts['payment_date_ymd']
: null;
$lines = array();
$invoice_adjustments = array();
$shipping_adjustment_net = 0.0;
$shipping_adjustment_tax = 0.0;
$fee_adjustment_net = 0.0;
$fee_adjustment_tax = 0.0;
$db_service = new Hesabix_V2_DB_Service();
$warehouse_id = Hesabix_V2_Invoice_Warehouse_Service::resolve_warehouse_id_for_order($order);
// Add order items (فرمت API حسابیکس: lines[].product_id, quantity, extra_info)
foreach ($order->get_items() as $item) {
$product = $item->get_product();
if (!$product) {
continue;
}
$variation_id = absint($item->get_variation_id());
$parent_id = absint($item->get_product_id());
if ($variation_id > 0) {
$hesabix_product_id = $db_service->get_hesabix_id('product', $variation_id, $parent_id);
$sync_product_id = $parent_id;
$sync_variation_id = $variation_id;
$log_wc_id = $variation_id;
} else {
$hesabix_product_id = $db_service->get_hesabix_id('product', $parent_id, null);
$sync_product_id = $parent_id;
$sync_variation_id = null;
$log_wc_id = $parent_id;
}
if (!$hesabix_product_id) {
$sync_service = new Hesabix_V2_Sync_Service();
$sync_result = $sync_service->sync_product($sync_product_id, $sync_variation_id, $order->get_currency());
if (!empty($sync_result['success']) && !empty($sync_result['hesabix_id'])) {
$hesabix_product_id = $sync_result['hesabix_id'];
} else {
Hesabix_V2_Log_Service::warning('Product not synced, skipping from invoice', array(
'wc_product_id' => $log_wc_id,
'wc_parent_id' => $variation_id > 0 ? $parent_id : null,
'order_id' => $order->get_id(),
'message' => isset($sync_result['message']) ? (string) $sync_result['message'] : '',
));
continue;
}
}
$quantity = $item->get_quantity();
$total = (float) $item->get_total() * $f;
$unit_price = $quantity > 0 ? $total / $quantity : 0;
$tax_amount = Hesabix_V2_Validation::sanitize_price((float) $item->get_total_tax() * $f);
$line_total = Hesabix_V2_Validation::sanitize_price($total) + $tax_amount;
$line_extra = array(
'unit_price' => Hesabix_V2_Validation::sanitize_price($unit_price),
'line_discount' => 0,
'tax_amount' => $tax_amount,
'line_total' => $line_total,
'unit' => 'عدد',
'unit_price_source' => 'base',
'discount_type' => 'amount',
'discount_value' => 0,
'tax_rate' => 0,
'movement' => 'out',
);
if ($warehouse_id !== null && $warehouse_id !== '') {
$line_extra['warehouse_id'] = (int) $warehouse_id;
}
$lines[] = array(
'product_id' => $hesabix_product_id,
'quantity' => $quantity,
'extra_info' => $line_extra,
);
}
// هزینه حمل و نقل
if ($order->get_shipping_total() > 0) {
$ship_total = Hesabix_V2_Validation::sanitize_price((float) $order->get_shipping_total() * $f);
$ship_tax = Hesabix_V2_Validation::sanitize_price((float) $order->get_shipping_tax() * $f);
$shipping_as_account = self::shipping_should_be_account_adjustment();
if ($shipping_as_account) {
$shipping_account_id = self::resolve_shipping_adjustment_account_id();
if ($shipping_account_id > 0) {
$shipping_adjustment_net = (float) $ship_total;
$shipping_adjustment_tax = (float) $ship_tax;
$tax_rate = 0.0;
if ($ship_total > 0 && $ship_tax > 0) {
$tax_rate = ((float) $ship_tax / (float) $ship_total) * 100;
}
$invoice_adjustments[] = array(
'kind' => 'addition',
'amount' => $ship_total,
'tax_rate' => $tax_rate,
'account_id' => $shipping_account_id,
'description' => __('هزینه حمل و نقل ووکامرس', 'hesabix-v2'),
'source' => 'woocommerce_shipping',
'exclude_from_profit' => true,
);
} else {
Hesabix_V2_Log_Service::warning('Shipping account adjustment requested but no shipping income account could be resolved; falling back to service line.', array(
'order_id' => $order->get_id(),
'recommended_account_code' => '60104',
));
$shipping_as_account = false;
}
}
if (!$shipping_as_account) {
$shipping_product_id = self::get_or_create_shipping_product();
if (!$shipping_product_id) {
$shipping_product_id = null;
}
} else {
$shipping_product_id = null;
}
if ($shipping_product_id) {
$line_extra = array(
'unit_price' => $ship_total,
'line_discount' => 0,
'tax_amount' => $ship_tax,
'line_total' => $ship_total + $ship_tax,
'unit' => 'عدد',
'unit_price_source' => 'base',
'discount_type' => 'amount',
'discount_value' => 0,
'tax_rate' => 0,
'movement' => 'out',
);
if ($warehouse_id !== null && $warehouse_id !== '') {
$line_extra['warehouse_id'] = (int) $warehouse_id;
}
$lines[] = array(
'product_id' => $shipping_product_id,
'quantity' => 1,
'extra_info' => $line_extra,
);
}
}
// کارمزد درگاه، هزینه‌های اضافی و سایر Feeهای ووکامرس
foreach ($order->get_items('fee') as $fee_item) {
Hesabix_V2_Gateway_Fee_Service::apply_wc_fee_item_to_invoice(
$order,
$fee_item,
$f,
$lines,
$invoice_adjustments,
$fee_adjustment_net,
$fee_adjustment_tax
);
}
$order_total = (float) $order->get_total() * $f;
$order_tax = (float) $order->get_total_tax() * $f;
$order_discount = (float) $order->get_discount_total() * $f;
$target_net_rounded = (int) round($order_total, 0);
$lines_target_net_rounded = (int) round(
$order_total
- $shipping_adjustment_net
- $shipping_adjustment_tax
- $fee_adjustment_net
- $fee_adjustment_tax,
0
);
self::adjust_invoice_lines_rounding_to_order_net($lines, $lines_target_net_rounded, $order->get_id());
$gross = $order_total + $order_discount - $order_tax - $shipping_adjustment_net - $fee_adjustment_net;
$header_tax_total = $order_tax - $shipping_adjustment_tax - $fee_adjustment_tax;
if ($header_tax_total < 0) {
$header_tax_total = 0;
}
// حسابیکس مبلغ قطعی فاکتور را از gross − discount + tax می‌گیرد؛ پرداخت افزونه از round(order_total).
// گرد کردن جداگانه ممکن است ۱–۲ واحد اختلاف بدهد؛ gross را در حد تحمل اصلاح می‌کنیم.
$gross_r = (int) round($gross, 0);
$discount_r = (int) round($order_discount, 0);
$tax_r = (int) round($header_tax_total, 0);
$adj_net_r = (int) round($shipping_adjustment_net + $fee_adjustment_net, 0);
$adj_tax_r = (int) round($shipping_adjustment_tax + $fee_adjustment_tax, 0);
$from_header = $gross_r - $discount_r + $tax_r + $adj_net_r + $adj_tax_r;
$hdr_delta = $target_net_rounded - $from_header;
if ($hdr_delta !== 0) {
$hdr_tol = (int) apply_filters('hesabix_v2_invoice_header_totals_tolerance', 5);
if ($hdr_tol < 1) {
$hdr_tol = 1;
}
if (abs($hdr_delta) <= $hdr_tol && ($gross_r + $hdr_delta) >= 0) {
$gross_r += $hdr_delta;
} elseif (abs($hdr_delta) <= $hdr_tol && ($gross_r + $hdr_delta) < 0) {
Hesabix_V2_Log_Service::warning(
'Invoice header gross adjustment skipped (would become negative).',
array(
'order_id' => $order->get_id(),
'gross_r' => $gross_r,
'hdr_delta' => $hdr_delta,
)
);
} elseif (abs($hdr_delta) > $hdr_tol) {
Hesabix_V2_Log_Service::warning(
'Invoice header totals (gross−discount+tax) vs WooCommerce order total differ beyond tolerance; payment amount may not match AR.',
array(
'order_id' => $order->get_id(),
'gross_rounded' => (int) round($gross, 0),
'discount_r' => $discount_r,
'tax_r' => $tax_r,
'adjustments_net_r' => $adj_net_r,
'adjustments_tax_r' => $adj_tax_r,
'from_header' => $from_header,
'target_net_rounded' => $target_net_rounded,
'delta' => $hdr_delta,
'tolerance' => $hdr_tol,
)
);
}
}
$sync = Hesabix_V2_Invoice_Helper::normalize_sync_settings(get_option('hesabix_v2_sync_settings', array()));
if (!empty($sync['invoice_is_proforma'])) {
$is_proforma = ! Hesabix_V2_Invoice_Helper::order_invoice_should_be_final($order, $sync);
} else {
$is_proforma = false;
}
$currency_id = $invoice_currency_id !== null
? (int) $invoice_currency_id
: Hesabix_V2_Currency_Service::resolve_invoice_currency_id();
$_created = $order->get_date_created();
$document_date = $document_date_override !== null
? $document_date_override
: ($_created ? $_created->format('Y-m-d') : current_time('Y-m-d'));
$payload = array(
'invoice_type' => 'invoice_sales',
'document_date' => $document_date,
'currency_id' => $currency_id,
'is_proforma' => $is_proforma,
'extra_info' => array(
'totals' => array(
'gross' => $gross_r,
'discount' => $discount_r,
'tax' => $tax_r,
'net' => $target_net_rounded,
'adjustments_net' => $adj_net_r,
'adjustments_tax' => $adj_tax_r,
),
'post_inventory' => true,
'ignore_credit_check' => false,
'person_id' => $person_id,
'source' => 'woocommerce',
'wc_order_id' => $order->get_id(),
),
'lines' => $lines,
);
if (!empty($invoice_adjustments)) {
$payload['extra_info']['invoice_adjustments'] = $invoice_adjustments;
}
$tag_ids = Hesabix_V2_Invoice_Helper::resolve_invoice_tag_ids();
if (!empty($tag_ids)) {
$payload['tag_ids'] = $tag_ids;
}
$payload['payments'] = self::build_wc_order_invoice_payments($order, $is_proforma, $f, $payment_date_ymd);
return $payload;
}
/**
* اختلاف گرد کردن بین جمع line_total خطوط و مبلغ نهایی سفارش ( IRR / واحدهای صحیح ).
*
* حداکثر اختلاف قابل قبول با فیلتر `hesabix_v2_invoice_rounding_tolerance` (پیش‌فرض ۲ واحد پول).
*
* @param array<int, array<string, mixed>> $lines
* @param int $target_net_rounded round(order_total)
* @param int $order_id
* @return void
*/
private static function adjust_invoice_lines_rounding_to_order_net(array &$lines, $target_net_rounded, $order_id)
{
if (empty($lines)) {
return;
}
$sum = 0.0;
foreach ($lines as $ln) {
$ei = isset($ln['extra_info']) && is_array($ln['extra_info']) ? $ln['extra_info'] : array();
if (!isset($ei['line_total'])) {
continue;
}
$sum += (float) $ei['line_total'];
}
$delta = $target_net_rounded - (int) round($sum, 0);
if ($delta === 0) {
return;
}
$tolerance = (int) apply_filters('hesabix_v2_invoice_rounding_tolerance', 2);
if ($tolerance < 1) {
$tolerance = 1;
}
if (abs($delta) > $tolerance) {
Hesabix_V2_Log_Service::warning(
'Invoice lines sum vs WooCommerce order total exceeds rounding tolerance; no auto-adjust applied.',
array(
'order_id' => $order_id,
'lines_sum_rounded' => (int) round($sum, 0),
'target_net_rounded' => $target_net_rounded,
'delta' => $delta,
'tolerance' => $tolerance,
)
);
return;
}
$fee_pid = (int) get_option('hesabix_v2_fee_product_id', 0);
$ship_pid = (int) get_option('hesabix_v2_shipping_product_id', 0);
$idx = null;
for ($i = count($lines) - 1; $i >= 0; $i--) {
$pid = isset($lines[ $i ]['product_id']) ? (int) $lines[ $i ]['product_id'] : 0;
if ($fee_pid > 0 && $pid === $fee_pid) {
$idx = $i;
break;
}
}
if ($idx === null) {
for ($i = count($lines) - 1; $i >= 0; $i--) {
$pid = isset($lines[ $i ]['product_id']) ? (int) $lines[ $i ]['product_id'] : 0;
if ($ship_pid > 0 && $pid === $ship_pid) {
$idx = $i;
break;
}
}
}
if ($idx === null) {
$idx = count($lines) - 1;
}
$qty = isset($lines[ $idx ]['quantity']) ? (float) $lines[ $idx ]['quantity'] : 1.0;
if ($qty <= 0) {
$qty = 1.0;
}
if (!isset($lines[ $idx ]['extra_info']) || !is_array($lines[ $idx ]['extra_info'])) {
return;
}
$ei = &$lines[ $idx ]['extra_info'];
$ei['line_total'] = (float) $ei['line_total'] + $delta;
if (!empty($ei['wc_fee_negative'])) {
$ei['line_discount'] = (float) $ei['line_discount'] - $delta;
} else {
$ei['unit_price'] = (float) $ei['unit_price'] + ($delta / $qty);
}
$ei['wc_rounding_adjust'] = $delta;
if (get_option('hesabix_v2_debug_mode')) {
Hesabix_V2_Log_Service::debug(
'Invoice lines rounded to match WooCommerce order net.',
array(
'order_id' => $order_id,
'adjusted_line_index' => $idx,
'delta' => $delta,
)
);
}
}
/**
* پرداخت‌های همراه فاکتور (سند دریافت در حسابیکس) فقط برای فاکتور قطعی و سفارش پرداخت‌شده.
*
* @param WC_Order $order
* @param bool $is_proforma
* @param float $amount_factor ضریب مبلغ نهایی سفارش برای پرداخت هم‌تراز با خطوط فاکتور.
* @param string|null $payment_date_ymd اگر ست باشد، بخش تاریخ transaction_date هم‌راستا با این مقدار (بخش ساعت از تاریخ پرداخت یا زمان کنونی).
* @return array<int, array<string, mixed>>
*/
private static function build_wc_order_invoice_payments($order, $is_proforma, $amount_factor = 1.0, $payment_date_ymd = null)
{
$payments = array();
if ($is_proforma || !$order->is_paid()) {
return apply_filters('hesabix_v2_invoice_payments', $payments, $order, $amount_factor);
}
$f = (float) $amount_factor;
if ($f <= 0 || is_nan($f)) {
$f = 1.0;
}
$amount = round((float) $order->get_total() * $f, 0);
if ($amount <= 0) {
return apply_filters('hesabix_v2_invoice_payments', $payments, $order, $amount_factor);
}
$settlement_commission = 0.0;
if (class_exists('Hesabix_V2_Gateway_Fee_Service')) {
$settlement_commission = Hesabix_V2_Gateway_Fee_Service::resolve_settlement_commission($order, (float) $amount, $f);
}
$settlement_commission = (int) round((float) $settlement_commission, 0);
$date_paid = $order->get_date_paid();
if (is_string($payment_date_ymd) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $payment_date_ymd)) {
$time_part = $date_paid ? $date_paid->format('H:i:s.v') : current_time('H:i:s.v');
$transaction_date = $payment_date_ymd . 'T' . $time_part;
} elseif ($date_paid) {
$transaction_date = $date_paid->format('Y-m-d\TH:i:s.v');
} else {
$transaction_date = current_time('Y-m-d\TH:i:s.v');
}
$desc = sprintf(
/* translators: %s: WooCommerce order number */
__('پرداخت سفارش #%s', 'hesabix-v2'),
$order->get_order_number()
);
$destination = get_option('hesabix_v2_invoice_payment_destination', 'bank');
if ($destination !== 'cash_register') {
$destination = 'bank';
}
if ($destination === 'cash_register') {
$cash_id = get_option('hesabix_v2_default_cash_register_id', '');
if ($cash_id !== '' && $cash_id !== null && (int) $cash_id > 0) {
$payment_row = array(
'type' => 'cash_register',
'transaction_type' => 'cash_register',
'cash_register_id' => (string) (int) $cash_id,
'amount' => $amount,
'transaction_date' => $transaction_date,
'description' => $desc,
);
if ($settlement_commission > 0) {
$payment_row['commission'] = $settlement_commission;
}
$payments[] = $payment_row;
} else {
Hesabix_V2_Log_Service::warning(
'Paid order: cash_register selected for invoice payments but no cash register configured.',
array('order_id' => $order->get_id())
);
}
} else {
$bank_id = get_option('hesabix_v2_default_bank_id', '');
if ($bank_id !== '' && $bank_id !== null) {
$payment_row = array(
'type' => 'bank',
'transaction_type' => 'bank',
'bank_id' => (string) $bank_id,
'amount' => $amount,
'transaction_date' => $transaction_date,
'description' => $desc,
);
if ($settlement_commission > 0) {
$payment_row['commission'] = $settlement_commission;
}
$payments[] = $payment_row;
} else {
Hesabix_V2_Log_Service::warning(
'Paid order: bank selected for invoice payments but no bank account configured.',
array('order_id' => $order->get_id())
);
}
}
return apply_filters('hesabix_v2_invoice_payments', $payments, $order, $amount_factor);
}
/**
* انتخاب یک ترم product_cat برای همگام با حسابیکس (چند دسته: اصلی SEO یا عمیق‌ترین شاخه).
*
* @since 2.0.8
* @param WC_Product $product
* @return int|null term_id
*/
private static function pick_wc_product_category_term_id($product)
{
$ids = array_values(array_unique(array_filter(array_map('intval', (array) $product->get_category_ids()))));
if (empty($ids)) {
return null;
}
$picked = apply_filters('hesabix_v2_primary_product_cat_term_id', null, $product, $ids);
if (is_int($picked) && $picked > 0 && in_array($picked, $ids, true)) {
return (int) apply_filters('hesabix_v2_picked_product_cat_term_id', $picked, $product, $ids);
}
$yoast = (int) get_post_meta($product->get_id(), '_yoast_wpseo_primary_product_cat', true);
if ($yoast > 0 && in_array($yoast, $ids, true)) {
return (int) apply_filters('hesabix_v2_picked_product_cat_term_id', $yoast, $product, $ids);
}
$rank_math = (int) get_post_meta($product->get_id(), 'rank_math_primary_product_cat', true);
if ($rank_math < 1) {
$rank_math = (int) get_post_meta($product->get_id(), '_rank_math_primary_product_cat', true);
}
if ($rank_math > 0 && in_array($rank_math, $ids, true)) {
return (int) apply_filters('hesabix_v2_picked_product_cat_term_id', $rank_math, $product, $ids);
}
$best_id = null;
$best_depth = -1;
foreach ($ids as $tid) {
$d = self::wc_product_cat_hierarchy_depth($tid);
if ($d > $best_depth) {
$best_depth = $d;
$best_id = $tid;
} elseif ($d === $best_depth && $best_id !== null && $tid < $best_id) {
$best_id = $tid;
}
}
return apply_filters('hesabix_v2_picked_product_cat_term_id', $best_id, $product, $ids);
}
/**
* عمق سلسلهٔ والدها برای ترم product_cat (خود ترم شمرده می‌شود).
*
* @since 2.0.8
* @param int $term_id
* @return int
*/
private static function wc_product_cat_hierarchy_depth($term_id)
{
$depth = 0;
$tid = (int) $term_id;
$guard = 0;
while ($tid > 0 && $guard++ < 64) {
$t = get_term($tid, 'product_cat');
if (!$t || is_wp_error($t)) {
break;
}
$depth++;
$tid = (int) $t->parent;
}
return $depth;
}
/**
* ذخیرهٔ اسنپ‌شات ووکامرس/حسابیکس برای تشخیص تغییر بعدی نام یا والد.
*
* @since 2.0.8
* @param Hesabix_V2_DB_Service $db
* @param int $wc_category_id
* @param int $hesabix_category_id
* @param \WP_Term $term
* @param int|null $hesabix_parent_id
* @param array<string,mixed> $extra_meta
* @return void
*/
private static function persist_category_sync_snapshot($db, $wc_category_id, $hesabix_category_id, $term, $hesabix_parent_id, $extra_meta = array())
{
$meta = array(
'wc_snapshot_v' => 1,
'wc_label' => $term->name,
'wc_parent_term_id' => (int) $term->parent,
'hesabix_parent_id' => $hesabix_parent_id,
);
if (is_array($extra_meta) && !empty($extra_meta)) {
$meta = array_merge($meta, $extra_meta);
}
$db->save_mapping(
'category',
$wc_category_id,
null,
$hesabix_category_id,
null,
$meta
);
}
/**
* اگر نام یا والد ترم در ووکامرس عوض شده، دستهٔ متناظر در حسابیکس را به‌روز یا جابه‌جا کن.
*
* @since 2.0.8
* @param int $wc_category_id
* @param int $hesabix_category_id
* @param array<string,mixed> $mapping_row
* @return void
*/
private static function refresh_mapped_wc_category_if_stale($wc_category_id, $hesabix_category_id, array $mapping_row)
{
$term = get_term($wc_category_id, 'product_cat');
if (!$term || is_wp_error($term)) {
return;
}
$db = new Hesabix_V2_DB_Service();
$meta = array();
if (!empty($mapping_row['meta_data'])) {
$decoded = json_decode((string) $mapping_row['meta_data'], true);
if (is_array($decoded)) {
$meta = $decoded;
}
}
$desired_parent_hx = null;
if (!empty($term->parent) && (int) $term->parent > 0) {
$desired_parent_hx = self::get_or_create_category_mapping((int) $term->parent);
}
if (empty($meta['wc_snapshot_v'])) {
self::persist_category_sync_snapshot($db, $wc_category_id, $hesabix_category_id, $term, $desired_parent_hx);
return;
}
$old_label = isset($meta['wc_label']) ? (string) $meta['wc_label'] : '';
$old_wc_parent = array_key_exists('wc_parent_term_id', $meta) ? (int) $meta['wc_parent_term_id'] : null;
$old_hx_parent = array_key_exists('hesabix_parent_id', $meta) ? $meta['hesabix_parent_id'] : '__unset__';
if ($old_hx_parent !== null && $old_hx_parent !== '__unset__') {
$old_hx_parent = (int) $old_hx_parent;
}
$label_stale = ($old_label !== (string) $term->name);
$wc_parent_stale = ($old_wc_parent !== (int) $term->parent);
$hx_parent_norm = ($old_hx_parent === '__unset__' ? null : $old_hx_parent);
$parent_stale = $wc_parent_stale || ($hx_parent_norm !== $desired_parent_hx);
if (!$label_stale && !$parent_stale) {
return;
}
$api = new Hesabix_V2_Api();
$ok = true;
if ($parent_stale) {
$mv = $api->move_category(array(
'category_id' => $hesabix_category_id,
'new_parent_id' => $desired_parent_hx,
));
if (empty($mv['success'])) {
$ok = false;
Hesabix_V2_Log_Service::warning('Hesabix category move failed (WC category sync)', array(
'wc_category_id' => $wc_category_id,
'hesabix_category_id' => $hesabix_category_id,
'new_parent_id' => $desired_parent_hx,
'response' => $mv,
));
}
}
if ($ok && $label_stale) {
$up = $api->update_category(array(
'category_id' => $hesabix_category_id,
'label' => $term->name,
));
if (empty($up['success'])) {
$ok = false;
Hesabix_V2_Log_Service::warning('Hesabix category label update failed (WC category sync)', array(
'wc_category_id' => $wc_category_id,
'hesabix_category_id' => $hesabix_category_id,
'response' => $up,
));
}
}
if ($ok) {
self::persist_category_sync_snapshot($db, $wc_category_id, $hesabix_category_id, $term, $desired_parent_hx);
self::invalidate_hesabix_category_flat_cache();
}
}
/**
* Get or create category mapping
*
* @since 2.0.0
* @param int $wc_category_id
* @return int|null
*/
private static function get_or_create_category_mapping($wc_category_id)
{
$db_service = new Hesabix_V2_DB_Service();
$mapping_row = $db_service->get_mapping('category', $wc_category_id, null);
if ($mapping_row && !empty($mapping_row['hesabix_id'])) {
$hid = (int) $mapping_row['hesabix_id'];
self::refresh_mapped_wc_category_if_stale((int) $wc_category_id, $hid, $mapping_row);
return $hid;
}
// Get WooCommerce category
$term = get_term($wc_category_id, 'product_cat');
if (!$term || is_wp_error($term)) {
return null;
}
// تطبیق والد ووکامرس با دستهٔ والد در حسابیکس (در صورت وجود)
$parent_hesabix_id = null;
if (!empty($term->parent) && (int) $term->parent > 0) {
$parent_hesabix_id = self::get_or_create_category_mapping((int) $term->parent);
}
$sync_settings = get_option('hesabix_v2_sync_settings', array());
if (!empty($sync_settings['sync_category_link_by_name_in_hesabix'])) {
$try_link = apply_filters('hesabix_v2_should_link_category_by_name', true, $term, $parent_hesabix_id);
if ($try_link) {
$linked_id = self::find_hesabix_category_id_by_wc_term($term->name, $parent_hesabix_id);
if ($linked_id) {
self::persist_category_sync_snapshot(
$db_service,
(int) $wc_category_id,
$linked_id,
$term,
$parent_hesabix_id,
array('linked_existing_by_name' => true)
);
return $linked_id;
}
}
}
// Create category in Hesabix (API از فیلد label برای title_translations استفاده می‌کند)
$api = new Hesabix_V2_Api();
$result = $api->create_category(array(
'label' => $term->name,
'parent_id' => $parent_hesabix_id,
));
if (isset($result['success']) && $result['success']) {
$item = (isset($result['data']['item']) && is_array($result['data']['item']))
? $result['data']['item']
: null;
$hesabix_category_id = null;
if ($item && isset($item['id'])) {
$hesabix_category_id = (int) $item['id'];
} elseif (isset($result['data']['id'])) {
$hesabix_category_id = (int) $result['data']['id'];
}
if (!$hesabix_category_id) {
return null;
}
self::invalidate_hesabix_category_flat_cache();
self::persist_category_sync_snapshot(
$db_service,
(int) $wc_category_id,
$hesabix_category_id,
$term,
$parent_hesabix_id
);
return $hesabix_category_id;
}
return null;
}
/**
* یک مرحلهٔ همگام‌سازی همهٔ ترم‌های product_cat (حتی بدون محصول).
*
* @since 2.0.8
* @param int $offset
* @param int $batch_size
* @return array<string,mixed>
*/
public static function bulk_sync_wc_product_categories_chunk($offset, $batch_size)
{
$offset = max(0, (int) $offset);
$batch_size = max(1, min((int) Hesabix_V2_Sync_Service::BULK_WC_CHUNK_MAX_ITEMS, (int) $batch_size));
$ids = self::get_sorted_wc_product_cat_term_ids();
$total = count($ids);
$slice = array_slice($ids, $offset, $batch_size);
$ok = 0;
$fail = 0;
$errors = array();
foreach ($slice as $tid) {
$hid = self::get_or_create_category_mapping((int) $tid);
if ($hid) {
$ok++;
} else {
$fail++;
if (count($errors) < 40) {
$errors[] = array(
'wc_category_id' => (int) $tid,
'message' => __('ایجاد/تطبیق دسته در حسابیکس ناموفق بود', 'hesabix-v2'),
);
}
}
}
$next = $offset + count($slice);
return array(
'success' => true,
'done' => ($next >= $total || empty($slice)),
'next_offset' => $next,
'estimated_catalog_total_wc_categories' => $total,
'processed_wc_categories_in_chunk' => count($slice),
'chunk_results' => array(
'success' => $ok,
'failed' => $fail,
'total' => count($slice),
'errors_preview' => $errors,
'errors_total' => $fail,
),
'message' => sprintf(
/* translators: 1: processed count, 2: total WC categories */
__('مرحله انجام شد (%1$d از %2$d دسته در این بسته).', 'hesabix-v2'),
count($slice),
$total
),
);
}
/**
* @since 2.0.8
* @return int[]
*/
private static function get_sorted_wc_product_cat_term_ids()
{
if (self::$wc_product_cat_ids_sorted_cache !== null) {
return self::$wc_product_cat_ids_sorted_cache;
}
$terms = get_terms(array(
'taxonomy' => 'product_cat',
'hide_empty' => false,
'number' => 0,
'orderby' => 'term_id',
'order' => 'ASC',
));
if (is_wp_error($terms) || !is_array($terms)) {
self::$wc_product_cat_ids_sorted_cache = array();
return self::$wc_product_cat_ids_sorted_cache;
}
$ids = array();
foreach ($terms as $t) {
if (isset($t->term_id)) {
$ids[] = (int) $t->term_id;
}
}
$ids = array_values(array_unique(array_filter($ids)));
usort(
$ids,
function ($a, $b) {
$da = self::wc_product_cat_hierarchy_depth($a);
$db = self::wc_product_cat_hierarchy_depth($b);
if ($da !== $db) {
return $da - $db;
}
return $a - $b;
}
);
self::$wc_product_cat_ids_sorted_cache = $ids;
return self::$wc_product_cat_ids_sorted_cache;
}
/**
* @since 2.0.8
* @return void
*/
private static function invalidate_hesabix_category_flat_cache()
{
self::$hx_category_flat_cache = null;
}
/**
* نرمال‌سازی برچسب برای مقایسهٔ تطبیق نام.
*
* @since 2.0.8
* @param string $label
* @return string
*/
private static function normalize_category_label_for_match($label)
{
$s = wp_strip_all_tags((string) $label);
$s = preg_replace('/\s+/u', ' ', trim($s));
if (function_exists('mb_strtolower')) {
return mb_strtolower($s, 'UTF-8');
}
return strtolower($s);
}
/**
* @since 2.0.8
* @param int|null $a
* @param int|null $b
* @return bool
*/
private static function hesabix_parent_ids_equal($a, $b)
{
$ai = ($a === null || $a === '' || $a === false) ? null : (int) $a;
$bi = ($b === null || $b === '' || $b === false) ? null : (int) $b;
return $ai === $bi;
}
/**
* فهرست مسطح دسته‌های حسابیکس از درخت API.
*
* @since 2.0.8
* @return array<int, array{id:int,parent_id:?int,label:string}>
*/
private static function load_hesabix_categories_flat_index()
{
if (self::$hx_category_flat_cache !== null) {
return self::$hx_category_flat_cache;
}
$api = new Hesabix_V2_Api();
$res = $api->get_categories_tree();
$flat = array();
if (empty($res['success'])) {
self::$hx_category_flat_cache = $flat;
return self::$hx_category_flat_cache;
}
$items = array();
if (isset($res['data']['items']) && is_array($res['data']['items'])) {
$items = $res['data']['items'];
}
$walk = function ($nodes) use (&$walk, &$flat) {
foreach ((array) $nodes as $n) {
if (!is_array($n)) {
continue;
}
$pid = null;
if (array_key_exists('parent_id', $n)) {
$pv = $n['parent_id'];
$pid = ($pv === null || $pv === '' || $pv === false) ? null : (int) $pv;
}
$flat[] = array(
'id' => isset($n['id']) ? (int) $n['id'] : 0,
'parent_id' => $pid,
'label' => isset($n['label']) ? (string) $n['label'] : '',
);
if (!empty($n['children']) && is_array($n['children'])) {
$walk($n['children']);
}
}
};
$walk($items);
self::$hx_category_flat_cache = $flat;
return self::$hx_category_flat_cache;
}
/**
* پیدا کردن شناسهٔ دسته در حسابیکس با نام و والد (بدون ساخت رکورد جدید).
*
* @since 2.0.8
* @param string $wc_term_name
* @param int|null $expected_parent_hesabix_id
* @return int|null
*/
private static function find_hesabix_category_id_by_wc_term($wc_term_name, $expected_parent_hesabix_id)
{
$want = self::normalize_category_label_for_match($wc_term_name);
if ($want === '') {
return null;
}
$rows = self::load_hesabix_categories_flat_index();
if (empty($rows)) {
return null;
}
$candidates = array();
foreach ($rows as $row) {
if (empty($row['id'])) {
continue;
}
if (self::normalize_category_label_for_match($row['label']) !== $want) {
continue;
}
if (!self::hesabix_parent_ids_equal($row['parent_id'], $expected_parent_hesabix_id)) {
continue;
}
$candidates[] = (int) $row['id'];
}
if (empty($candidates)) {
return null;
}
sort($candidates, SORT_NUMERIC);
$chosen = (int) $candidates[0];
return (int) apply_filters('hesabix_v2_linked_hesabix_category_id', $chosen, $wc_term_name, $expected_parent_hesabix_id, $candidates);
}
/**
* آیا هزینه حمل ووکامرس به‌جای خدمت، به‌صورت ردیف حساب فاکتور ارسال شود؟
*
* @return bool
*/
private static function shipping_should_be_account_adjustment()
{
$sync = Hesabix_V2_Invoice_Helper::normalize_sync_settings(get_option('hesabix_v2_sync_settings', array()));
return isset($sync['shipping_line_mode']) && $sync['shipping_line_mode'] === 'account_adjustment';
}
/**
* شناسه حساب درآمد حمل کالا در حسابیکس؛ اولویت با تنظیم کاربر، سپس حساب پیش‌فرض 60104.
*
* @return int
*/
private static function resolve_shipping_adjustment_account_id()
{
$sync = Hesabix_V2_Invoice_Helper::normalize_sync_settings(get_option('hesabix_v2_sync_settings', array()));
$configured = isset($sync['shipping_adjustment_account_id']) ? absint($sync['shipping_adjustment_account_id']) : 0;
if ($configured > 0) {
return $configured;
}
$cached = get_transient('hesabix_v2_shipping_adjustment_account_60104');
if ($cached !== false && absint($cached) > 0) {
return absint($cached);
}
$api = new Hesabix_V2_Api();
$res = $api->get_accounts_flat();
$items = self::extract_accounts_items_from_api_response($res);
foreach ($items as $row) {
if (!is_array($row) || empty($row['id'])) {
continue;
}
$code = isset($row['code']) ? trim((string) $row['code']) : '';
$name = isset($row['name']) ? trim((string) $row['name']) : '';
if ($code === '60104' || $name === 'درآمد حمل کالا') {
$id = absint($row['id']);
if ($id > 0) {
set_transient('hesabix_v2_shipping_adjustment_account_60104', $id, 12 * HOUR_IN_SECONDS);
return $id;
}
}
}
return 0;
}
/**
* استخراج آرایه حساب‌ها از پاسخ‌های مختلف API حسابیکس.
*
* @param array $res
* @return array<int, array<string, mixed>>
*/
/**
* @param array $res
* @return array<int, array<string, mixed>>
*/
public static function extract_accounts_items_from_api_response_public($res)
{
return self::extract_accounts_items_from_api_response($res);
}
/**
* @return int|null
*/
public static function get_or_create_fee_product_public()
{
return self::get_or_create_fee_product();
}
private static function extract_accounts_items_from_api_response($res)
{
if (!is_array($res)) {
return array();
}
if (isset($res['data']['items']) && is_array($res['data']['items'])) {
return $res['data']['items'];
}
if (isset($res['data']) && is_array($res['data']) && array_keys($res['data']) === range(0, count($res['data']) - 1)) {
return $res['data'];
}
if (isset($res['items']) && is_array($res['items'])) {
return $res['items'];
}
return array();
}
/**
* Get or create shipping product in Hesabix
*
* @since 2.0.0
* @return int|null
*/
private static function get_or_create_shipping_product()
{
$shipping_product_id = get_option('hesabix_v2_shipping_product_id');
if ($shipping_product_id) {
return $shipping_product_id;
}
// Create shipping product (فرمت API حسابیکس: name، item_type، main_unit، base_sales_price)
$api = new Hesabix_V2_Api();
$result = $api->create_product(array(
'name' => 'هزینه حمل و نقل',
'item_type' => 'خدمت',
'main_unit' => 'عدد',
'base_sales_price' => 0,
'track_inventory' => false,
'is_active' => true,
));
if (isset($result['success']) && $result['success']) {
$shipping_product_id = $result['data']['id'];
update_option('hesabix_v2_shipping_product_id', $shipping_product_id);
return $shipping_product_id;
}
return null;
}
/**
* کالای خدمتی برای خطوط کارمزد/Fee ووکامرس در فاکتور
*
* @return int|null
*/
private static function get_or_create_fee_product()
{
$fee_product_id = get_option('hesabix_v2_fee_product_id');
if ($fee_product_id) {
return (int) $fee_product_id;
}
$api = new Hesabix_V2_Api();
$result = $api->create_product(array(
'name' => 'کارمزد و هزینه سفارش (ووکامرس)',
'item_type' => 'خدمت',
'main_unit' => 'عدد',
'base_sales_price' => 0,
'track_inventory' => false,
'is_active' => true,
));
if (isset($result['success']) && $result['success']) {
$fee_product_id = null;
if (isset($result['data']['item']['id'])) {
$fee_product_id = (int) $result['data']['item']['id'];
} elseif (isset($result['data']['id'])) {
$fee_product_id = (int) $result['data']['id'];
}
if ($fee_product_id && $fee_product_id > 0) {
update_option('hesabix_v2_fee_product_id', $fee_product_id);
return $fee_product_id;
}
}
return null;
}
}