ArcWOC/includes/class-hesabix-v2-gateway-fee.php
Babak Alizadeh 464f237885 some progress
2026-05-31 19:49:19 +03:30

538 lines
16 KiB
PHP

<?php
/**
* کارمزد درگاه / Fee ووکامرس: تنظیمات، اضافات/کسورات فاکتور، کارمزد تسویه در سند دریافت.
*
* @package Hesabix_V2
* @subpackage Hesabix_V2/includes
*/
if (!defined('WPINC')) {
die;
}
class Hesabix_V2_Gateway_Fee_Service
{
const FEE_LINE_SERVICE = 'service';
const FEE_LINE_ACCOUNT_ADDITION = 'account_adjustment';
const FEE_NEGATIVE_LINE_DISCOUNT = 'line_discount';
const FEE_NEGATIVE_DEDUCTION = 'deduction_adjustment';
const SETTLEMENT_OFF = 'off';
const SETTLEMENT_PERCENT = 'percent';
const SETTLEMENT_FIXED = 'fixed';
const SETTLEMENT_RULES = 'rules';
const SETTLEMENT_ORDER_META = 'order_meta';
/**
* @param array<string,mixed> $sync
* @return array<string,mixed>
*/
public static function normalize_sync_settings($sync)
{
if (!is_array($sync)) {
$sync = array();
}
$mode = isset($sync['fee_line_mode']) ? sanitize_key((string) $sync['fee_line_mode']) : self::FEE_LINE_SERVICE;
if (!in_array($mode, array(self::FEE_LINE_SERVICE, self::FEE_LINE_ACCOUNT_ADDITION), true)) {
$mode = self::FEE_LINE_SERVICE;
}
$sync['fee_line_mode'] = $mode;
$neg = isset($sync['fee_negative_mode']) ? sanitize_key((string) $sync['fee_negative_mode']) : self::FEE_NEGATIVE_LINE_DISCOUNT;
if (!in_array($neg, array(self::FEE_NEGATIVE_LINE_DISCOUNT, self::FEE_NEGATIVE_DEDUCTION), true)) {
$neg = self::FEE_NEGATIVE_LINE_DISCOUNT;
}
$sync['fee_negative_mode'] = $neg;
$sync['fee_adjustment_account_id'] = isset($sync['fee_adjustment_account_id'])
? absint($sync['fee_adjustment_account_id'])
: 0;
$sync['fee_deduction_account_id'] = isset($sync['fee_deduction_account_id'])
? absint($sync['fee_deduction_account_id'])
: 0;
$sync['fee_exclude_from_profit'] = !empty($sync['fee_exclude_from_profit']);
$settle = isset($sync['gateway_settlement_mode']) ? sanitize_key((string) $sync['gateway_settlement_mode']) : self::SETTLEMENT_OFF;
$allowed_settle = array(
self::SETTLEMENT_OFF,
self::SETTLEMENT_PERCENT,
self::SETTLEMENT_FIXED,
self::SETTLEMENT_RULES,
self::SETTLEMENT_ORDER_META,
);
if (!in_array($settle, $allowed_settle, true)) {
$settle = self::SETTLEMENT_OFF;
}
$sync['gateway_settlement_mode'] = $settle;
$sync['gateway_settlement_percent'] = self::sanitize_percent($sync['gateway_settlement_percent'] ?? 0);
$sync['gateway_settlement_fixed'] = max(0.0, (float) ($sync['gateway_settlement_fixed'] ?? 0));
$rules_raw = isset($sync['gateway_settlement_rules']) ? $sync['gateway_settlement_rules'] : '';
if (is_array($rules_raw)) {
$sync['gateway_settlement_rules'] = self::sanitize_rules_array($rules_raw);
} else {
$sync['gateway_settlement_rules'] = self::parse_rules_text((string) $rules_raw);
}
$sync['gateway_settlement_meta_key'] = isset($sync['gateway_settlement_meta_key'])
? sanitize_key((string) $sync['gateway_settlement_meta_key'])
: '_gateway_settlement_fee';
return $sync;
}
/**
* @param mixed $value
* @return float
*/
private static function sanitize_percent($value)
{
$p = (float) $value;
if ($p < 0) {
return 0.0;
}
if ($p > 100) {
return 100.0;
}
return $p;
}
/**
* قواعد متنی: payment_method|percent|fixed در هر خط
*
* @param string $raw
* @return array<int, array<string, mixed>>
*/
public static function parse_rules_text($raw)
{
$out = array();
$raw = trim($raw);
if ($raw === '') {
return $out;
}
$lines = preg_split('/\r\n|\r|\n/', $raw);
if (!is_array($lines)) {
return $out;
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || strpos($line, '#') === 0) {
continue;
}
$parts = array_map('trim', explode('|', $line));
if (count($parts) < 1 || $parts[0] === '') {
continue;
}
$out[] = array(
'payment_method' => sanitize_key($parts[0]),
'percent' => isset($parts[1]) ? self::sanitize_percent($parts[1]) : 0.0,
'fixed' => isset($parts[2]) ? max(0.0, (float) $parts[2]) : 0.0,
);
}
return $out;
}
/**
* @param array<int, mixed> $raw
* @return array<int, array<string, mixed>>
*/
private static function sanitize_rules_array($raw)
{
$out = array();
foreach ($raw as $row) {
if (!is_array($row)) {
continue;
}
$pm = isset($row['payment_method']) ? sanitize_key((string) $row['payment_method']) : '';
if ($pm === '') {
continue;
}
$out[] = array(
'payment_method' => $pm,
'percent' => self::sanitize_percent($row['percent'] ?? 0),
'fixed' => max(0.0, (float) ($row['fixed'] ?? 0)),
);
}
return $out;
}
/**
* @return array<string,mixed>
*/
private static function get_sync()
{
return self::normalize_sync_settings(get_option('hesabix_v2_sync_settings', array()));
}
/**
* @return bool
*/
public static function positive_fee_uses_account_adjustment()
{
$sync = self::get_sync();
return $sync['fee_line_mode'] === self::FEE_LINE_ACCOUNT_ADDITION;
}
/**
* @return bool
*/
public static function negative_fee_uses_deduction_adjustment()
{
$sync = self::get_sync();
return $sync['fee_negative_mode'] === self::FEE_NEGATIVE_DEDUCTION;
}
/**
* @param string $default_code
* @param string $default_name
* @param string $transient_key
* @param int $configured_id
* @return int
*/
private static function resolve_account_id($default_code, $default_name, $transient_key, $configured_id)
{
if ($configured_id > 0) {
return $configured_id;
}
$cached = get_transient($transient_key);
if ($cached !== false && absint($cached) > 0) {
return absint($cached);
}
if (!class_exists('Hesabix_V2_Mapper')) {
return 0;
}
$api = new Hesabix_V2_Api();
$res = $api->get_accounts_flat();
$items = Hesabix_V2_Mapper::extract_accounts_items_from_api_response_public($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 === $default_code || $name === $default_name) {
$id = absint($row['id']);
if ($id > 0) {
set_transient($transient_key, $id, 12 * HOUR_IN_SECONDS);
return $id;
}
}
}
return 0;
}
/**
* @return int
*/
public static function resolve_fee_income_account_id()
{
$sync = self::get_sync();
return self::resolve_account_id(
'60101',
'درآمد حاصل از فروش خدمات',
'hesabix_v2_fee_adjustment_account_60101',
isset($sync['fee_adjustment_account_id']) ? absint($sync['fee_adjustment_account_id']) : 0
);
}
/**
* @return int
*/
public static function resolve_fee_deduction_account_id()
{
$sync = self::get_sync();
return self::resolve_account_id(
'70902',
'کارمزد خدمات بانکی',
'hesabix_v2_fee_deduction_account_70902',
isset($sync['fee_deduction_account_id']) ? absint($sync['fee_deduction_account_id']) : 0
);
}
/**
* پردازش یک Fee ووکامرس: یا خط خدمت، یا ردیف اضافات/کسورات.
*
* @param WC_Order $order
* @param WC_Order_Item_Fee $fee_item
* @param float $amount_factor
* @param array<int, array> $lines by reference
* @param array<int, array> $invoice_adjustments by reference
* @param float $adjustment_net signed, by reference
* @param float $adjustment_tax signed, by reference
* @return void
*/
public static function apply_wc_fee_item_to_invoice(
$order,
$fee_item,
$amount_factor,
array &$lines,
array &$invoice_adjustments,
&$adjustment_net,
&$adjustment_tax
) {
if (!is_object($fee_item) || !method_exists($fee_item, 'get_total')) {
return;
}
$f = (float) $amount_factor;
if ($f <= 0 || is_nan($f)) {
$f = 1.0;
}
$fee_total = Hesabix_V2_Validation::sanitize_price((float) $fee_item->get_total() * $f);
$fee_tax = method_exists($fee_item, 'get_total_tax')
? Hesabix_V2_Validation::sanitize_price((float) $fee_item->get_total_tax() * $f)
: 0.0;
if (abs($fee_total) < 0.00001 && abs($fee_tax) < 0.00001) {
return;
}
$fee_label = method_exists($fee_item, 'get_name') ? trim((string) $fee_item->get_name()) : '';
if ($fee_label === '') {
$fee_label = __('کارمزد / هزینه سفارش', 'hesabix-v2');
}
$sync = self::get_sync();
$exclude_profit = !empty($sync['fee_exclude_from_profit']);
if ($fee_total < 0 && self::negative_fee_uses_deduction_adjustment()) {
$account_id = self::resolve_fee_deduction_account_id();
if ($account_id > 0) {
$abs_net = Hesabix_V2_Validation::sanitize_price(-(float) $fee_total);
$tax_rate = 0.0;
if ($abs_net > 0 && $fee_tax > 0) {
$tax_rate = ((float) $fee_tax / (float) $abs_net) * 100;
}
$row = array(
'kind' => 'deduction',
'amount' => $abs_net,
'tax_rate' => $tax_rate,
'account_id' => $account_id,
'description' => sanitize_text_field(mb_substr($fee_label, 0, 500)),
'source' => 'woocommerce_fee',
);
if ($exclude_profit) {
$row['exclude_from_profit'] = true;
}
$invoice_adjustments[] = $row;
$adjustment_net -= (float) $abs_net;
$adjustment_tax -= (float) $fee_tax;
return;
}
Hesabix_V2_Log_Service::warning(
'Fee deduction adjustment requested but no expense account resolved; falling back to line discount.',
array('order_id' => $order->get_id(), 'recommended_account_code' => '70902')
);
}
if ($fee_total > 0 && self::positive_fee_uses_account_adjustment()) {
$account_id = self::resolve_fee_income_account_id();
if ($account_id > 0) {
$tax_rate = 0.0;
if ($fee_total > 0 && $fee_tax > 0) {
$tax_rate = ((float) $fee_tax / (float) $fee_total) * 100;
}
$row = array(
'kind' => 'addition',
'amount' => (float) $fee_total,
'tax_rate' => $tax_rate,
'account_id' => $account_id,
'description' => sanitize_text_field(mb_substr($fee_label, 0, 500)),
'source' => 'woocommerce_fee',
);
if ($exclude_profit) {
$row['exclude_from_profit'] = true;
}
$invoice_adjustments[] = $row;
$adjustment_net += (float) $fee_total;
$adjustment_tax += (float) $fee_tax;
return;
}
Hesabix_V2_Log_Service::warning(
'Fee income adjustment requested but no income account resolved; falling back to service line.',
array('order_id' => $order->get_id(), 'recommended_account_code' => '60101')
);
}
self::append_fee_service_line($order, $fee_item, $f, $fee_total, $fee_tax, $fee_label, $lines);
}
/**
* @param WC_Order $order
* @param mixed $fee_item
* @param float $f
* @param float $fee_total
* @param float $fee_tax
* @param string $fee_label
* @param array $lines
* @return void
*/
private static function append_fee_service_line($order, $fee_item, $f, $fee_total, $fee_tax, $fee_label, array &$lines)
{
$fee_product_id = Hesabix_V2_Mapper::get_or_create_fee_product_public();
if (!$fee_product_id) {
Hesabix_V2_Log_Service::warning('Fee line skipped — could not resolve fee service product in Hesabix', array(
'order_id' => $order->get_id(),
'fee_name' => $fee_label,
));
return;
}
$warehouse_id = Hesabix_V2_Invoice_Warehouse_Service::resolve_warehouse_id_for_order($order);
if ($fee_total < 0) {
$line_extra = array(
'unit_price' => 0,
'line_discount' => Hesabix_V2_Validation::sanitize_price(-(float) $fee_total),
'tax_amount' => $fee_tax,
'line_total' => $fee_total + $fee_tax,
'unit' => 'عدد',
'unit_price_source' => 'base',
'discount_type' => 'amount',
'discount_value' => 0,
'tax_rate' => 0,
'movement' => 'out',
'wc_fee_label' => $fee_label,
'wc_fee_negative' => true,
);
} else {
$line_extra = array(
'unit_price' => $fee_total,
'line_discount' => 0,
'tax_amount' => $fee_tax,
'line_total' => $fee_total + $fee_tax,
'unit' => 'عدد',
'unit_price_source' => 'base',
'discount_type' => 'amount',
'discount_value' => 0,
'tax_rate' => 0,
'movement' => 'out',
'wc_fee_label' => $fee_label,
);
}
if ($warehouse_id !== null && $warehouse_id !== '') {
$line_extra['warehouse_id'] = (int) $warehouse_id;
}
$lines[] = array(
'product_id' => $fee_product_id,
'quantity' => 1,
'description' => sanitize_text_field(mb_substr($fee_label, 0, 500)),
'extra_info' => $line_extra,
);
}
/**
* کارمزد تسویه درگاه (کسر از واریز پذیرنده) برای سند دریافت همراه فاکتور.
*
* @param WC_Order $order
* @param float $payment_amount مبلغ دریافتی مشتری (قبل از کسر کارمزد تسویه)
* @param float $amount_factor
* @return float کارمزد (>= 0)
*/
public static function resolve_settlement_commission($order, $payment_amount, $amount_factor = 1.0)
{
$sync = self::get_sync();
$mode = $sync['gateway_settlement_mode'];
if ($mode === self::SETTLEMENT_OFF) {
return (float) apply_filters('hesabix_v2_order_gateway_commission', 0.0, $order, $payment_amount, $amount_factor, $sync);
}
$f = (float) $amount_factor;
if ($f <= 0 || is_nan($f)) {
$f = 1.0;
}
$base = max(0.0, (float) $payment_amount);
$commission = 0.0;
if ($mode === self::SETTLEMENT_ORDER_META) {
$commission = self::read_commission_from_order_meta($order, $f, $sync);
} elseif ($mode === self::SETTLEMENT_PERCENT) {
$commission = $base * ((float) $sync['gateway_settlement_percent'] / 100.0);
} elseif ($mode === self::SETTLEMENT_FIXED) {
$commission = (float) $sync['gateway_settlement_fixed'] * $f;
} elseif ($mode === self::SETTLEMENT_RULES) {
$commission = self::commission_from_rules($order, $base, $f, $sync['gateway_settlement_rules']);
}
$commission = Hesabix_V2_Validation::sanitize_price($commission);
if ($commission < 0) {
$commission = 0.0;
}
if ($commission > $base) {
$commission = $base;
}
return (float) apply_filters('hesabix_v2_order_gateway_commission', $commission, $order, $payment_amount, $amount_factor, $sync);
}
/**
* @param WC_Order $order
* @param float $f
* @param array $sync
* @return float
*/
private static function read_commission_from_order_meta($order, $f, $sync)
{
$keys = array(
isset($sync['gateway_settlement_meta_key']) ? (string) $sync['gateway_settlement_meta_key'] : '',
'_gateway_settlement_fee',
'_transaction_fee',
'gateway_fee',
'_wc_gateway_fee',
'_payment_fee',
);
foreach ($keys as $key) {
$key = trim((string) $key);
if ($key === '') {
continue;
}
$raw = $order->get_meta($key, true);
if ($raw === '' || $raw === null) {
continue;
}
$val = (float) $raw * $f;
if ($val > 0) {
return Hesabix_V2_Validation::sanitize_price($val);
}
}
return 0.0;
}
/**
* @param WC_Order $order
* @param float $base
* @param float $f
* @param array $rules
* @return float
*/
private static function commission_from_rules($order, $base, $f, $rules)
{
if (!is_array($rules) || empty($rules)) {
return 0.0;
}
$method = method_exists($order, 'get_payment_method') ? sanitize_key((string) $order->get_payment_method()) : '';
foreach ($rules as $rule) {
if (!is_array($rule)) {
continue;
}
$pm = isset($rule['payment_method']) ? sanitize_key((string) $rule['payment_method']) : '';
if ($pm === '' || ($method !== '' && $pm !== $method)) {
continue;
}
$percent = isset($rule['percent']) ? (float) $rule['percent'] : 0.0;
$fixed = isset($rule['fixed']) ? (float) $rule['fixed'] : 0.0;
return Hesabix_V2_Validation::sanitize_price($base * ($percent / 100.0) + $fixed * $f);
}
return 0.0;
}
}