933 lines
28 KiB
PHP
933 lines
28 KiB
PHP
<?php
|
|
/**
|
|
* تشخیص و پاکسازی امن کالاهای یتیم «والد متغیر بهعنوان ساده» در حسابیکس.
|
|
*
|
|
* @since 4.7.2
|
|
* @package Hesabix_V2
|
|
* @subpackage Hesabix_V2/admin/services
|
|
*/
|
|
|
|
if (!defined('WPINC')) {
|
|
die;
|
|
}
|
|
|
|
class Hesabix_V2_Orphan_Product_Service
|
|
{
|
|
const OPTION_REGISTRY = 'hesabix_v2_orphan_parent_products';
|
|
const TIER_HIGH = 'high';
|
|
const TIER_MEDIUM = 'medium';
|
|
|
|
/** @var Hesabix_V2_Api */
|
|
private $api;
|
|
|
|
/** @var Hesabix_V2_DB_Service */
|
|
private $db;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->api = new Hesabix_V2_Api();
|
|
$this->db = new Hesabix_V2_DB_Service();
|
|
}
|
|
|
|
/**
|
|
* شناسههای کالا که هرگز نباید حذف/غیرفعال شوند (حملونقل، کارمزد، …).
|
|
*
|
|
* @return int[]
|
|
*/
|
|
public static function protected_hesabix_ids()
|
|
{
|
|
$ids = array(
|
|
(int) get_option('hesabix_v2_shipping_product_id', 0),
|
|
(int) get_option('hesabix_v2_fee_product_id', 0),
|
|
);
|
|
|
|
/**
|
|
* شناسههای حسابیکس که از پاکسازی یتیمها مستثنی هستند.
|
|
*
|
|
* @param int[] $ids
|
|
*/
|
|
$filtered = apply_filters('hesabix_v2_orphan_protected_hesabix_ids', $ids);
|
|
if (!is_array($filtered)) {
|
|
$filtered = $ids;
|
|
}
|
|
|
|
$out = array();
|
|
foreach ($filtered as $id) {
|
|
$id = absint($id);
|
|
if ($id > 0) {
|
|
$out[ $id ] = $id;
|
|
}
|
|
}
|
|
|
|
return array_values($out);
|
|
}
|
|
|
|
/**
|
|
* ثبت یک کالای یتیم در رجیستری محلی (مثلاً هنگام پاک شدن نگاشت والد).
|
|
*
|
|
* @param array<string, mixed> $row
|
|
* @return void
|
|
*/
|
|
public static function register(array $row)
|
|
{
|
|
$hesabix_id = isset($row['hesabix_id']) ? absint($row['hesabix_id']) : 0;
|
|
if ($hesabix_id < 1) {
|
|
return;
|
|
}
|
|
|
|
if (in_array($hesabix_id, self::protected_hesabix_ids(), true)) {
|
|
return;
|
|
}
|
|
|
|
$registry = self::get_registry();
|
|
$existing = isset($registry[ $hesabix_id ]) && is_array($registry[ $hesabix_id ])
|
|
? $registry[ $hesabix_id ]
|
|
: array();
|
|
|
|
$registry[ $hesabix_id ] = array_merge(
|
|
array(
|
|
'hesabix_id' => $hesabix_id,
|
|
'wc_parent_id' => 0,
|
|
'parent_name' => '',
|
|
'source' => 'manual',
|
|
'tier' => self::TIER_HIGH,
|
|
'status' => 'pending',
|
|
'last_message' => '',
|
|
'detected_at' => current_time('mysql'),
|
|
'document_types' => array(),
|
|
),
|
|
$existing,
|
|
$row,
|
|
array(
|
|
'hesabix_id' => $hesabix_id,
|
|
'updated_at' => current_time('mysql'),
|
|
)
|
|
);
|
|
|
|
// اگر قبلاً حذف شده، دوباره pending نشود مگر صریحاً خواسته شود
|
|
if (!empty($existing['status']) && $existing['status'] === 'deleted' && empty($row['force_reopen'])) {
|
|
$registry[ $hesabix_id ]['status'] = 'deleted';
|
|
} elseif (empty($row['status'])) {
|
|
$registry[ $hesabix_id ]['status'] = 'pending';
|
|
}
|
|
|
|
self::save_registry($registry);
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public static function get_registry()
|
|
{
|
|
$raw = get_option(self::OPTION_REGISTRY, array());
|
|
if (!is_array($raw)) {
|
|
return array();
|
|
}
|
|
|
|
$out = array();
|
|
foreach ($raw as $key => $row) {
|
|
if (!is_array($row)) {
|
|
continue;
|
|
}
|
|
$id = isset($row['hesabix_id']) ? absint($row['hesabix_id']) : absint($key);
|
|
if ($id < 1) {
|
|
continue;
|
|
}
|
|
$row['hesabix_id'] = $id;
|
|
$out[ $id ] = $row;
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $registry
|
|
* @return void
|
|
*/
|
|
public static function save_registry(array $registry)
|
|
{
|
|
update_option(self::OPTION_REGISTRY, $registry, false);
|
|
}
|
|
|
|
/**
|
|
* اسکن کاندیداهای یتیم و بهروزرسانی رجیستری.
|
|
*
|
|
* @param array{include_name_heuristics?:bool} $args
|
|
* @return array{success:bool,candidates:array,counts:array,message:string}
|
|
*/
|
|
public function scan_candidates(array $args = array())
|
|
{
|
|
$include_heuristics = !empty($args['include_name_heuristics']);
|
|
$protected = array_fill_keys(self::protected_hesabix_ids(), true);
|
|
$mappings = $this->db->get_all_product_mappings();
|
|
|
|
$mapped_hesabix = array();
|
|
$parent_null_by_wc = array();
|
|
$variation_parents = array();
|
|
|
|
foreach ($mappings as $m) {
|
|
if (!is_array($m)) {
|
|
continue;
|
|
}
|
|
$hid = isset($m['hesabix_id']) ? absint($m['hesabix_id']) : 0;
|
|
$wc_id = isset($m['wc_id']) ? absint($m['wc_id']) : 0;
|
|
$wc_parent = array_key_exists('wc_parent_id', $m) && $m['wc_parent_id'] !== null && $m['wc_parent_id'] !== ''
|
|
? absint($m['wc_parent_id'])
|
|
: null;
|
|
|
|
if ($hid > 0) {
|
|
$mapped_hesabix[ $hid ] = true;
|
|
}
|
|
|
|
if ($wc_parent === null || $wc_parent === 0) {
|
|
if ($wc_id > 0 && $hid > 0) {
|
|
$parent_null_by_wc[ $wc_id ] = $hid;
|
|
}
|
|
} else {
|
|
$variation_parents[ $wc_parent ] = true;
|
|
}
|
|
}
|
|
|
|
$candidates = array();
|
|
$registry = self::get_registry();
|
|
|
|
// سطح بالا: نگاشت والد با wc_parent_id خالی روی محصول متغیر WC
|
|
foreach ($parent_null_by_wc as $wc_parent_id => $hesabix_id) {
|
|
if (isset($protected[ $hesabix_id ])) {
|
|
continue;
|
|
}
|
|
|
|
$product = wc_get_product($wc_parent_id);
|
|
if (!$product || !$product->is_type('variable')) {
|
|
continue;
|
|
}
|
|
|
|
$row = $this->build_candidate_row(
|
|
$hesabix_id,
|
|
$wc_parent_id,
|
|
$product->get_title(),
|
|
self::TIER_HIGH,
|
|
'mapping',
|
|
__('نگاشت والد متغیر بهعنوان محصول ساده', 'hesabix-v2')
|
|
);
|
|
$candidates[ $hesabix_id ] = $row;
|
|
self::register(array_merge($row, array('status' => 'pending')));
|
|
}
|
|
|
|
// رجیستری قبلی (پس از پاک شدن نگاشت)
|
|
foreach ($registry as $hid => $row) {
|
|
$hid = absint($hid);
|
|
if ($hid < 1 || isset($protected[ $hid ]) || isset($candidates[ $hid ])) {
|
|
continue;
|
|
}
|
|
|
|
$status = isset($row['status']) ? (string) $row['status'] : 'pending';
|
|
if (in_array($status, array('deleted'), true)) {
|
|
continue;
|
|
}
|
|
|
|
// اگر این hesabix_id هنوز نگاشت معتبری دارد (غیر از همان والد یتیم)، رد کن
|
|
if (isset($mapped_hesabix[ $hid ])) {
|
|
$still_parent_orphan = false;
|
|
$wc_pid = isset($row['wc_parent_id']) ? absint($row['wc_parent_id']) : 0;
|
|
if ($wc_pid > 0 && isset($parent_null_by_wc[ $wc_pid ]) && (int) $parent_null_by_wc[ $wc_pid ] === $hid) {
|
|
$still_parent_orphan = true;
|
|
}
|
|
if (!$still_parent_orphan) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$wc_parent_id = isset($row['wc_parent_id']) ? absint($row['wc_parent_id']) : 0;
|
|
$parent_name = isset($row['parent_name']) ? (string) $row['parent_name'] : '';
|
|
if ($wc_parent_id > 0) {
|
|
$p = wc_get_product($wc_parent_id);
|
|
if ($p && $p->is_type('variable')) {
|
|
$parent_name = $p->get_title();
|
|
} elseif ($p && !$p->is_type('variable')) {
|
|
// دیگر متغیر نیست — از لیست فعال خارج کن
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$cand = $this->build_candidate_row(
|
|
$hid,
|
|
$wc_parent_id,
|
|
$parent_name,
|
|
isset($row['tier']) ? (string) $row['tier'] : self::TIER_HIGH,
|
|
isset($row['source']) ? (string) $row['source'] : 'registry',
|
|
__('ثبتشده در رجیستری یتیمها (نگاشت محلی قبلاً پاک شده)', 'hesabix-v2')
|
|
);
|
|
$cand['status'] = $status;
|
|
$candidates[ $hid ] = $cand;
|
|
}
|
|
|
|
// سطح متوسط (اختیاری): نام دقیق والد متغیر در حسابیکس بدون نگاشت محلی
|
|
if ($include_heuristics) {
|
|
$heuristic = $this->detect_name_heuristics($mapped_hesabix, $variation_parents, $protected, $candidates);
|
|
foreach ($heuristic as $hid => $row) {
|
|
$candidates[ $hid ] = $row;
|
|
self::register(array_merge($row, array('status' => 'pending', 'force_reopen' => true)));
|
|
}
|
|
}
|
|
|
|
$counts = array(
|
|
'total' => count($candidates),
|
|
'high' => 0,
|
|
'medium' => 0,
|
|
'pending' => 0,
|
|
'deactivated' => 0,
|
|
);
|
|
foreach ($candidates as $c) {
|
|
if (($c['tier'] ?? '') === self::TIER_MEDIUM) {
|
|
$counts['medium']++;
|
|
} else {
|
|
$counts['high']++;
|
|
}
|
|
$st = isset($c['status']) ? (string) $c['status'] : 'pending';
|
|
if ($st === 'deactivated') {
|
|
$counts['deactivated']++;
|
|
} elseif ($st !== 'deleted') {
|
|
$counts['pending']++;
|
|
}
|
|
}
|
|
|
|
$list = array_values($candidates);
|
|
usort(
|
|
$list,
|
|
static function ($a, $b) {
|
|
$ta = ($a['tier'] ?? '') === self::TIER_HIGH ? 0 : 1;
|
|
$tb = ($b['tier'] ?? '') === self::TIER_HIGH ? 0 : 1;
|
|
if ($ta !== $tb) {
|
|
return $ta - $tb;
|
|
}
|
|
return ((int) ($a['hesabix_id'] ?? 0)) - ((int) ($b['hesabix_id'] ?? 0));
|
|
}
|
|
);
|
|
|
|
Hesabix_V2_Log_Service::info(
|
|
'Orphan parent products scan completed',
|
|
array(
|
|
'entity_type' => 'product',
|
|
'counts' => $counts,
|
|
'include_name_heuristics' => $include_heuristics,
|
|
)
|
|
);
|
|
|
|
return array(
|
|
'success' => true,
|
|
'candidates' => $list,
|
|
'counts' => $counts,
|
|
'message' => sprintf(
|
|
/* translators: %d candidate count */
|
|
__('اسکن انجام شد: %d کاندیدا یافت شد.', 'hesabix-v2'),
|
|
count($list)
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* پاکسازی دستهای کاندیداها.
|
|
*
|
|
* @param int[] $hesabix_ids
|
|
* @param array{dry_run?:bool,deactivate_if_used?:bool,allow_medium?:bool} $args
|
|
* @return array{success:bool,results:array,summary:array,message:string}
|
|
*/
|
|
public function cleanup_batch(array $hesabix_ids, array $args = array())
|
|
{
|
|
$dry_run = !empty($args['dry_run']);
|
|
$deactivate_if_used = !isset($args['deactivate_if_used']) || !empty($args['deactivate_if_used']);
|
|
$allow_medium = !empty($args['allow_medium']);
|
|
|
|
$results = array();
|
|
$summary = array(
|
|
'deleted' => 0,
|
|
'deactivated' => 0,
|
|
'skipped' => 0,
|
|
'errors' => 0,
|
|
'dry_run_deletable' => 0,
|
|
'dry_run_in_use' => 0,
|
|
);
|
|
|
|
$ids = array_values(array_unique(array_filter(array_map('absint', $hesabix_ids))));
|
|
foreach ($ids as $hid) {
|
|
$one = $this->cleanup_one(
|
|
$hid,
|
|
array(
|
|
'dry_run' => $dry_run,
|
|
'deactivate_if_used' => $deactivate_if_used,
|
|
'allow_medium' => $allow_medium,
|
|
)
|
|
);
|
|
$results[] = $one;
|
|
|
|
$action = isset($one['action']) ? (string) $one['action'] : '';
|
|
if (!empty($one['success'])) {
|
|
if ($action === 'deleted') {
|
|
$summary['deleted']++;
|
|
} elseif ($action === 'deactivated') {
|
|
$summary['deactivated']++;
|
|
} elseif ($action === 'would_delete') {
|
|
$summary['dry_run_deletable']++;
|
|
} elseif ($action === 'would_deactivate' || $action === 'in_use') {
|
|
$summary['dry_run_in_use']++;
|
|
} else {
|
|
$summary['skipped']++;
|
|
}
|
|
} else {
|
|
if ($action === 'skipped') {
|
|
$summary['skipped']++;
|
|
} else {
|
|
$summary['errors']++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($dry_run) {
|
|
$message = sprintf(
|
|
/* translators: 1: deletable 2: in use 3: skipped/errors */
|
|
__('پیشنمایش: %1$d قابل حذف، %2$d در استفاده، %3$d رد/خطا.', 'hesabix-v2'),
|
|
$summary['dry_run_deletable'],
|
|
$summary['dry_run_in_use'],
|
|
$summary['skipped'] + $summary['errors']
|
|
);
|
|
} else {
|
|
$message = sprintf(
|
|
/* translators: 1: deleted 2: deactivated 3: skipped 4: errors */
|
|
__('پاکسازی: %1$d حذف، %2$d غیرفعال، %3$d رد، %4$d خطا.', 'hesabix-v2'),
|
|
$summary['deleted'],
|
|
$summary['deactivated'],
|
|
$summary['skipped'],
|
|
$summary['errors']
|
|
);
|
|
}
|
|
|
|
return array(
|
|
'success' => true,
|
|
'results' => $results,
|
|
'summary' => $summary,
|
|
'message' => $message,
|
|
'dry_run' => $dry_run,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* پاکسازی یک کالا.
|
|
*
|
|
* @param int $hesabix_id
|
|
* @param array $args
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function cleanup_one($hesabix_id, array $args = array())
|
|
{
|
|
$hesabix_id = absint($hesabix_id);
|
|
$dry_run = !empty($args['dry_run']);
|
|
$deactivate_if_used = !isset($args['deactivate_if_used']) || !empty($args['deactivate_if_used']);
|
|
$allow_medium = !empty($args['allow_medium']);
|
|
|
|
$base = array(
|
|
'hesabix_id' => $hesabix_id,
|
|
'success' => false,
|
|
'action' => 'error',
|
|
'message' => '',
|
|
'document_types' => array(),
|
|
'can_delete' => null,
|
|
'tier' => self::TIER_HIGH,
|
|
'wc_parent_id' => 0,
|
|
'parent_name' => '',
|
|
);
|
|
|
|
if ($hesabix_id < 1) {
|
|
$base['message'] = __('شناسهٔ کالا نامعتبر است.', 'hesabix-v2');
|
|
return $base;
|
|
}
|
|
|
|
if (in_array($hesabix_id, self::protected_hesabix_ids(), true)) {
|
|
$base['action'] = 'skipped';
|
|
$base['success'] = true;
|
|
$base['message'] = __('این کالا محافظتشده است (حملونقل/کارمزد) و پاکسازی نمیشود.', 'hesabix-v2');
|
|
return $base;
|
|
}
|
|
|
|
$validation = $this->validate_candidate($hesabix_id, $allow_medium);
|
|
if (empty($validation['ok'])) {
|
|
$base['action'] = 'skipped';
|
|
$base['success'] = true;
|
|
$base['message'] = isset($validation['message']) ? (string) $validation['message'] : __('کاندیدای معتبر نیست.', 'hesabix-v2');
|
|
$base['tier'] = isset($validation['tier']) ? (string) $validation['tier'] : self::TIER_HIGH;
|
|
return $base;
|
|
}
|
|
|
|
$base['tier'] = (string) $validation['tier'];
|
|
$base['wc_parent_id'] = (int) $validation['wc_parent_id'];
|
|
$base['parent_name'] = (string) $validation['parent_name'];
|
|
|
|
$usage = $this->check_usage($hesabix_id);
|
|
$base['document_types'] = isset($usage['document_types']) && is_array($usage['document_types'])
|
|
? $usage['document_types']
|
|
: array();
|
|
$base['can_delete'] = !empty($usage['can_delete']);
|
|
$base['usage_source'] = isset($usage['source']) ? (string) $usage['source'] : '';
|
|
|
|
if (!empty($usage['not_found'])) {
|
|
$this->update_registry_status($hesabix_id, 'deleted', __('کالا در حسابیکس یافت نشد؛ از رجیستری حذف شد.', 'hesabix-v2'));
|
|
$this->db->delete_parent_only_product_mapping_by_hesabix_id($hesabix_id);
|
|
$base['success'] = true;
|
|
$base['action'] = 'deleted';
|
|
$base['message'] = __('کالا در حسابیکس وجود ندارد؛ نگاشت/رجیستری پاک شد.', 'hesabix-v2');
|
|
return $base;
|
|
}
|
|
|
|
if (!empty($usage['error']) && $usage['can_delete'] === null) {
|
|
$base['message'] = isset($usage['message']) ? (string) $usage['message'] : __('خطا در بررسی استفاده کالا.', 'hesabix-v2');
|
|
$this->update_registry_status($hesabix_id, 'error', $base['message'], $base['document_types']);
|
|
return $base;
|
|
}
|
|
|
|
if ($dry_run) {
|
|
if (!empty($usage['uncertain'])) {
|
|
$base['success'] = true;
|
|
$base['action'] = 'uncertain';
|
|
$base['message'] = isset($usage['message'])
|
|
? (string) $usage['message']
|
|
: __('وضعیت استفاده نامشخص است (API قدیمی). در پاکسازی واقعی گارد حذف اعمال میشود.', 'hesabix-v2');
|
|
return $base;
|
|
}
|
|
if (!empty($usage['can_delete'])) {
|
|
$base['success'] = true;
|
|
$base['action'] = 'would_delete';
|
|
$base['message'] = __('قابل حذف: در فاکتور/سند/انبار استفاده نشده است.', 'hesabix-v2');
|
|
} else {
|
|
$types = !empty($base['document_types']) ? implode('، ', $base['document_types']) : __('نامشخص', 'hesabix-v2');
|
|
$base['success'] = true;
|
|
$base['action'] = $deactivate_if_used ? 'would_deactivate' : 'in_use';
|
|
$base['message'] = sprintf(
|
|
/* translators: %s document types */
|
|
__('در استفاده است (%s) — حذف نمیشود؛ در پاکسازی واقعی غیرفعال میشود.', 'hesabix-v2'),
|
|
$types
|
|
);
|
|
}
|
|
return $base;
|
|
}
|
|
|
|
if (!empty($usage['can_delete'])) {
|
|
$del = $this->api->delete_product($hesabix_id);
|
|
if (!empty($del['success'])) {
|
|
$this->db->delete_parent_only_product_mapping_by_hesabix_id($hesabix_id);
|
|
// اگر هنوز نگاشت روی wc_parent با این id باشد
|
|
if (!empty($validation['wc_parent_id'])) {
|
|
$m = $this->db->get_mapping('product', (int) $validation['wc_parent_id'], null);
|
|
if ($m && (int) $m['hesabix_id'] === $hesabix_id) {
|
|
$this->db->delete_parent_only_product_mapping((int) $validation['wc_parent_id']);
|
|
}
|
|
}
|
|
$this->update_registry_status($hesabix_id, 'deleted', __('حذف شد.', 'hesabix-v2'));
|
|
$base['success'] = true;
|
|
$base['action'] = 'deleted';
|
|
$base['message'] = __('کالای یتیم با موفقیت از حسابیکس حذف شد.', 'hesabix-v2');
|
|
|
|
Hesabix_V2_Log_Service::info(
|
|
'Orphan parent product deleted',
|
|
array(
|
|
'entity_type' => 'product',
|
|
'hesabix_id' => $hesabix_id,
|
|
'wc_parent_id' => (int) $validation['wc_parent_id'],
|
|
)
|
|
);
|
|
|
|
return $base;
|
|
}
|
|
|
|
// حذف شکست خورد — شاید همزمان استفاده شده
|
|
$msg = isset($del['message']) ? (string) $del['message'] : __('حذف ناموفق بود.', 'hesabix-v2');
|
|
$looks_in_use = (false !== strpos($msg, 'اسناد مرتبط') || false !== strpos($msg, 'استفاده'));
|
|
|
|
if ($looks_in_use && $deactivate_if_used) {
|
|
return $this->deactivate_orphan($hesabix_id, $validation, $base, $msg);
|
|
}
|
|
|
|
$base['message'] = $msg;
|
|
$this->update_registry_status($hesabix_id, 'error', $msg);
|
|
return $base;
|
|
}
|
|
|
|
// در استفاده
|
|
if ($deactivate_if_used) {
|
|
$types = !empty($base['document_types']) ? implode('، ', $base['document_types']) : '';
|
|
return $this->deactivate_orphan(
|
|
$hesabix_id,
|
|
$validation,
|
|
$base,
|
|
$types !== ''
|
|
? sprintf(__('در استفاده: %s', 'hesabix-v2'), $types)
|
|
: __('در اسناد مرتبط استفاده شده است.', 'hesabix-v2')
|
|
);
|
|
}
|
|
|
|
$base['success'] = true;
|
|
$base['action'] = 'skipped';
|
|
$base['message'] = __('کالا در استفاده است و حذف/غیرفعالسازی انجام نشد.', 'hesabix-v2');
|
|
$this->update_registry_status($hesabix_id, 'skipped', $base['message'], $base['document_types']);
|
|
return $base;
|
|
}
|
|
|
|
/**
|
|
* بررسی استفاده کالا در حسابیکس.
|
|
*
|
|
* @param int $hesabix_id
|
|
* @return array{can_delete:?bool,is_used:?bool,document_types:array,source:string,error?:bool,not_found?:bool,message?:string}
|
|
*/
|
|
public function check_usage($hesabix_id)
|
|
{
|
|
$hesabix_id = absint($hesabix_id);
|
|
$res = $this->api->check_product_usage($hesabix_id);
|
|
|
|
if (!empty($res['success']) && is_array($res['data'])) {
|
|
$data = $res['data'];
|
|
$can = array_key_exists('can_delete', $data) ? (bool) $data['can_delete'] : null;
|
|
$used = array_key_exists('is_used', $data) ? (bool) $data['is_used'] : ($can === null ? null : !$can);
|
|
$types = isset($data['document_types']) && is_array($data['document_types']) ? $data['document_types'] : array();
|
|
|
|
return array(
|
|
'can_delete' => $can,
|
|
'is_used' => $used,
|
|
'document_types' => $types,
|
|
'source' => 'usage_check',
|
|
);
|
|
}
|
|
|
|
$status = isset($res['status_code']) ? (int) $res['status_code'] : 0;
|
|
$msg = isset($res['message']) ? (string) $res['message'] : '';
|
|
|
|
if ($status === 404 || false !== strpos($msg, 'یافت نشد')) {
|
|
return array(
|
|
'can_delete' => true,
|
|
'is_used' => false,
|
|
'document_types' => array(),
|
|
'source' => 'not_found',
|
|
'not_found' => true,
|
|
'message' => $msg,
|
|
);
|
|
}
|
|
|
|
// سرور قدیمی بدون usage-check: وجود کالا را چک کن
|
|
$get = $this->api->get_product($hesabix_id);
|
|
if (empty($get['success'])) {
|
|
$gmsg = isset($get['message']) ? (string) $get['message'] : '';
|
|
$gstatus = isset($get['status_code']) ? (int) $get['status_code'] : 0;
|
|
if ($gstatus === 404 || false !== strpos($gmsg, 'یافت نشد')) {
|
|
return array(
|
|
'can_delete' => true,
|
|
'is_used' => false,
|
|
'document_types' => array(),
|
|
'source' => 'not_found',
|
|
'not_found' => true,
|
|
'message' => $gmsg,
|
|
);
|
|
}
|
|
|
|
return array(
|
|
'can_delete' => null,
|
|
'is_used' => null,
|
|
'document_types' => array(),
|
|
'source' => 'error',
|
|
'error' => true,
|
|
'message' => $msg !== '' ? $msg : ($gmsg !== '' ? $gmsg : __('خطا در بررسی کالا.', 'hesabix-v2')),
|
|
);
|
|
}
|
|
|
|
// usage-check نیست؛ برای حذف واقعی به گارد DELETE تکیه میکنیم؛ برای dry-run نامشخص است
|
|
return array(
|
|
'can_delete' => true,
|
|
'is_used' => false,
|
|
'document_types' => array(),
|
|
'source' => 'fallback_probe_delete',
|
|
'uncertain' => true,
|
|
'message' => __('endpoint بررسی استفاده در دسترس نیست؛ حذف توسط گارد سرور کنترل میشود.', 'hesabix-v2'),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param int $hesabix_id
|
|
* @param int $wc_parent_id
|
|
* @param string $parent_name
|
|
* @param string $tier
|
|
* @param string $source
|
|
* @param string $reason
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function build_candidate_row($hesabix_id, $wc_parent_id, $parent_name, $tier, $source, $reason)
|
|
{
|
|
return array(
|
|
'hesabix_id' => (int) $hesabix_id,
|
|
'wc_parent_id' => (int) $wc_parent_id,
|
|
'parent_name' => (string) $parent_name,
|
|
'tier' => $tier === self::TIER_MEDIUM ? self::TIER_MEDIUM : self::TIER_HIGH,
|
|
'source' => (string) $source,
|
|
'reason' => (string) $reason,
|
|
'status' => 'pending',
|
|
'detected_at' => current_time('mysql'),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array<int,bool> $mapped_hesabix
|
|
* @param array<int,bool> $variation_parents
|
|
* @param array<int,bool> $protected
|
|
* @param array<int,array> $already
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function detect_name_heuristics(array $mapped_hesabix, array $variation_parents, array $protected, array $already)
|
|
{
|
|
$found = array();
|
|
|
|
foreach (array_keys($variation_parents) as $wc_parent_id) {
|
|
$wc_parent_id = absint($wc_parent_id);
|
|
if ($wc_parent_id < 1) {
|
|
continue;
|
|
}
|
|
|
|
$product = wc_get_product($wc_parent_id);
|
|
if (!$product || !$product->is_type('variable')) {
|
|
continue;
|
|
}
|
|
|
|
$title = trim((string) $product->get_title());
|
|
if ($title === '') {
|
|
continue;
|
|
}
|
|
|
|
$search = $this->api->search_products(
|
|
array(
|
|
'search' => $title,
|
|
'take' => 20,
|
|
'skip' => 0,
|
|
)
|
|
);
|
|
|
|
if (empty($search['success']) || empty($search['data'])) {
|
|
continue;
|
|
}
|
|
|
|
$items = array();
|
|
if (isset($search['data']['items']) && is_array($search['data']['items'])) {
|
|
$items = $search['data']['items'];
|
|
} elseif (isset($search['data']['results']) && is_array($search['data']['results'])) {
|
|
$items = $search['data']['results'];
|
|
} elseif (is_array($search['data']) && isset($search['data'][0])) {
|
|
$items = $search['data'];
|
|
}
|
|
|
|
foreach ($items as $item) {
|
|
if (!is_array($item)) {
|
|
continue;
|
|
}
|
|
$hid = isset($item['id']) ? absint($item['id']) : 0;
|
|
$name = isset($item['name']) ? trim((string) $item['name']) : '';
|
|
if ($hid < 1 || $name === '') {
|
|
continue;
|
|
}
|
|
if (isset($protected[ $hid ]) || isset($mapped_hesabix[ $hid ]) || isset($already[ $hid ]) || isset($found[ $hid ])) {
|
|
continue;
|
|
}
|
|
if ($this->normalize_name($name) !== $this->normalize_name($title)) {
|
|
continue;
|
|
}
|
|
|
|
$found[ $hid ] = $this->build_candidate_row(
|
|
$hid,
|
|
$wc_parent_id,
|
|
$title,
|
|
self::TIER_MEDIUM,
|
|
'name_heuristic',
|
|
__('نام دقیق برابر عنوان والد متغیر و بدون نگاشت محلی (نیاز به تأیید)', 'hesabix-v2')
|
|
);
|
|
}
|
|
}
|
|
|
|
return $found;
|
|
}
|
|
|
|
/**
|
|
* @param string $name
|
|
* @return string
|
|
*/
|
|
private function normalize_name($name)
|
|
{
|
|
$name = wp_strip_all_tags((string) $name);
|
|
$name = preg_replace('/\s+/u', ' ', $name);
|
|
return mb_strtolower(trim((string) $name));
|
|
}
|
|
|
|
/**
|
|
* @param int $hesabix_id
|
|
* @param bool $allow_medium
|
|
* @return array{ok:bool,message?:string,tier?:string,wc_parent_id?:int,parent_name?:string}
|
|
*/
|
|
private function validate_candidate($hesabix_id, $allow_medium)
|
|
{
|
|
$registry = self::get_registry();
|
|
$row = isset($registry[ $hesabix_id ]) ? $registry[ $hesabix_id ] : null;
|
|
|
|
$mappings = $this->db->get_all_product_mappings();
|
|
$parent_null_match = null;
|
|
$used_elsewhere = false;
|
|
|
|
foreach ($mappings as $m) {
|
|
if (!is_array($m) || (int) ($m['hesabix_id'] ?? 0) !== $hesabix_id) {
|
|
continue;
|
|
}
|
|
$wc_id = absint($m['wc_id'] ?? 0);
|
|
$wc_parent = array_key_exists('wc_parent_id', $m) && $m['wc_parent_id'] !== null && $m['wc_parent_id'] !== ''
|
|
? absint($m['wc_parent_id'])
|
|
: null;
|
|
|
|
if ($wc_parent === null || $wc_parent === 0) {
|
|
$p = wc_get_product($wc_id);
|
|
if ($p && $p->is_type('variable')) {
|
|
$parent_null_match = array(
|
|
'wc_parent_id' => $wc_id,
|
|
'parent_name' => $p->get_title(),
|
|
'tier' => self::TIER_HIGH,
|
|
);
|
|
} else {
|
|
$used_elsewhere = true;
|
|
}
|
|
} else {
|
|
$used_elsewhere = true;
|
|
}
|
|
}
|
|
|
|
if ($used_elsewhere && !$parent_null_match) {
|
|
return array(
|
|
'ok' => false,
|
|
'message' => __('این شناسه هنوز نگاشت معتبر دارد و یتیم محسوب نمیشود.', 'hesabix-v2'),
|
|
);
|
|
}
|
|
|
|
if ($parent_null_match) {
|
|
return array(
|
|
'ok' => true,
|
|
'tier' => self::TIER_HIGH,
|
|
'wc_parent_id' => (int) $parent_null_match['wc_parent_id'],
|
|
'parent_name' => (string) $parent_null_match['parent_name'],
|
|
);
|
|
}
|
|
|
|
if (is_array($row)) {
|
|
$tier = isset($row['tier']) ? (string) $row['tier'] : self::TIER_HIGH;
|
|
if ($tier === self::TIER_MEDIUM && !$allow_medium) {
|
|
return array(
|
|
'ok' => false,
|
|
'message' => __('کاندیدای سطح متوسط فقط با تأیید صریح پاکسازی میشود.', 'hesabix-v2'),
|
|
'tier' => self::TIER_MEDIUM,
|
|
);
|
|
}
|
|
|
|
$status = isset($row['status']) ? (string) $row['status'] : 'pending';
|
|
if ($status === 'deleted') {
|
|
return array(
|
|
'ok' => false,
|
|
'message' => __('این مورد قبلاً حذف شده است.', 'hesabix-v2'),
|
|
);
|
|
}
|
|
|
|
return array(
|
|
'ok' => true,
|
|
'tier' => $tier === self::TIER_MEDIUM ? self::TIER_MEDIUM : self::TIER_HIGH,
|
|
'wc_parent_id' => isset($row['wc_parent_id']) ? absint($row['wc_parent_id']) : 0,
|
|
'parent_name' => isset($row['parent_name']) ? (string) $row['parent_name'] : '',
|
|
);
|
|
}
|
|
|
|
return array(
|
|
'ok' => false,
|
|
'message' => __('کاندیدا در اسکن/رجیستری یافت نشد. ابتدا اسکن کنید.', 'hesabix-v2'),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param int $hesabix_id
|
|
* @param array<string,mixed> $validation
|
|
* @param array<string,mixed> $base
|
|
* @param string $reason
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function deactivate_orphan($hesabix_id, array $validation, array $base, $reason)
|
|
{
|
|
$upd = $this->api->update_product(
|
|
$hesabix_id,
|
|
array(
|
|
'is_active' => false,
|
|
)
|
|
);
|
|
|
|
if (!empty($upd['success'])) {
|
|
// نگاشت محلی والد را قطع کن تا دوباره لینک نشود
|
|
if (!empty($validation['wc_parent_id'])) {
|
|
$m = $this->db->get_mapping('product', (int) $validation['wc_parent_id'], null);
|
|
if ($m && (int) $m['hesabix_id'] === $hesabix_id) {
|
|
$this->db->delete_parent_only_product_mapping((int) $validation['wc_parent_id']);
|
|
}
|
|
}
|
|
$this->db->delete_parent_only_product_mapping_by_hesabix_id($hesabix_id);
|
|
|
|
$msg = sprintf(
|
|
/* translators: %s reason */
|
|
__('غیرفعال شد (حذف نشد): %s', 'hesabix-v2'),
|
|
$reason
|
|
);
|
|
$this->update_registry_status($hesabix_id, 'deactivated', $msg, isset($base['document_types']) ? $base['document_types'] : array());
|
|
|
|
$base['success'] = true;
|
|
$base['action'] = 'deactivated';
|
|
$base['message'] = $msg;
|
|
|
|
Hesabix_V2_Log_Service::info(
|
|
'Orphan parent product deactivated',
|
|
array(
|
|
'entity_type' => 'product',
|
|
'hesabix_id' => $hesabix_id,
|
|
'wc_parent_id' => (int) ($validation['wc_parent_id'] ?? 0),
|
|
'reason' => $reason,
|
|
)
|
|
);
|
|
|
|
return $base;
|
|
}
|
|
|
|
$base['message'] = isset($upd['message'])
|
|
? (string) $upd['message']
|
|
: __('غیرفعالسازی ناموفق بود.', 'hesabix-v2');
|
|
$this->update_registry_status($hesabix_id, 'error', $base['message']);
|
|
return $base;
|
|
}
|
|
|
|
/**
|
|
* @param int $hesabix_id
|
|
* @param string $status
|
|
* @param string $message
|
|
* @param array $document_types
|
|
* @return void
|
|
*/
|
|
private function update_registry_status($hesabix_id, $status, $message = '', array $document_types = array())
|
|
{
|
|
$registry = self::get_registry();
|
|
if (!isset($registry[ $hesabix_id ]) || !is_array($registry[ $hesabix_id ])) {
|
|
$registry[ $hesabix_id ] = array(
|
|
'hesabix_id' => $hesabix_id,
|
|
'tier' => self::TIER_HIGH,
|
|
'source' => 'cleanup',
|
|
);
|
|
}
|
|
|
|
$registry[ $hesabix_id ]['status'] = $status;
|
|
$registry[ $hesabix_id ]['last_message'] = $message;
|
|
$registry[ $hesabix_id ]['updated_at'] = current_time('mysql');
|
|
if (!empty($document_types)) {
|
|
$registry[ $hesabix_id ]['document_types'] = $document_types;
|
|
}
|
|
|
|
self::save_registry($registry);
|
|
}
|
|
}
|