ArcWOC/includes/class-hesabix-v2-api.php
Babak Alizadeh 11e8f522d3 Release 4.10.0: safe disconnect and change-business cleanup
Add a dedicated disconnect flow that truncates mappings/queue, clears
business-scoped IDs (shipping/fee/warehouse/bank), bridge token, sync
meta, and best-effort Hesabix Woo bridge settings. Change-business and
setup-complete now require this cleanup so reconnecting to a new
business cannot reuse stale links. Also expose delete-data-on-uninstall
in settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 16:03:06 +00:00

1441 lines
34 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
/**
* API Client for Hesabix V2
*
* @since 2.0.0
* @package Hesabix_V2
* @subpackage Hesabix_V2/includes
*/
class Hesabix_V2_Api
{
/**
* API base URL
*
* @since 2.0.0
* @access private
* @var string $base_url
*/
private $base_url;
/**
* API Key
*
* @since 2.0.0
* @access private
* @var string $api_key
*/
private $api_key;
/**
* Business ID
*
* @since 2.0.0
* @access private
* @var int $business_id
*/
private $business_id;
/**
* Fiscal Year ID
*
* @since 2.0.0
* @access private
* @var int $fiscal_year_id
*/
private $fiscal_year_id;
/**
* Initialize the class
*
* @since 2.0.0
*/
public function __construct()
{
$this->base_url = get_option('hesabix_v2_api_base_url', HESABIX_V2_API_BASE_URL);
$this->api_key = get_option('hesabix_v2_api_key');
$this->business_id = get_option('hesabix_v2_business_id');
$this->fiscal_year_id = get_option('hesabix_v2_fiscal_year_id');
}
/**
* Make API request
*
* @since 2.0.0
* @param string $method HTTP method
* @param string $endpoint API endpoint
* @param array $data Request data
* @param int $timeout Request timeout
* @return array Response array
*/
private function request($method, $endpoint, $data = null, $timeout = 30)
{
$url = $this->base_url . $endpoint;
$headers = array(
'Content-Type' => 'application/json',
'Accept' => 'application/json',
);
// Add authorization header if API key is set (ApiKey ak_live_... or ak_test_...)
if ($this->api_key) {
$headers['Authorization'] = 'ApiKey ' . $this->api_key;
}
// Add business and fiscal year headers for business endpoints
if (
strpos($endpoint, '/business/') !== false
|| strpos($endpoint, '/businesses/') !== false
|| strpos($endpoint, '/accounts/') !== false
|| strpos($endpoint, '/woocommerce/') !== false
) {
if ($this->business_id) {
$headers['X-Business-ID'] = $this->business_id;
}
if ($this->fiscal_year_id) {
$headers['X-Fiscal-Year-ID'] = $this->fiscal_year_id;
}
}
$audit_headers = Hesabix_V2_Log_Service::sanitize_log_recursive($headers);
$args = array(
'method' => $method,
'headers' => $headers,
'timeout' => $timeout,
'body' => $data ? wp_json_encode($data) : null,
);
if (get_option('hesabix_v2_debug_mode')) {
Hesabix_V2_Log_Service::debug(
__('درخواست ووکامرس → حسابیکس', 'hesabix-v2'),
array(
'entity_type' => 'hesabix_api',
'request' => array(
'direction' => 'woocommerce_to_hesabix',
'method' => $method,
'url' => $url,
'endpoint' => $endpoint,
'headers' => $audit_headers,
'json_body' => $data,
'timeout' => $timeout,
),
)
);
}
$start_time = microtime(true);
$response = wp_remote_request($url, $args);
$execution_time = microtime(true) - $start_time;
if (is_wp_error($response)) {
$error_message = $response->get_error_message();
Hesabix_V2_Log_Service::error(
'API Request Error',
array(
'entity_type' => 'hesabix_api',
'error' => $error_message,
'execution_time' => $execution_time,
'request' => array(
'direction' => 'woocommerce_to_hesabix',
'method' => $method,
'url' => $url,
'endpoint' => $endpoint,
'headers' => $audit_headers,
'json_body' => $data,
'timeout' => $timeout,
),
'response' => array(
'direction' => 'hesabix_to_woocommerce',
'type' => 'transport_error',
'transport_error' => $error_message,
),
)
);
return array(
'success' => false,
'message' => $error_message,
'error_code' => 'REQUEST_FAILED',
);
}
$status_code = wp_remote_retrieve_response_code($response);
$body = wp_remote_retrieve_body($response);
$result = json_decode($body, true);
if (get_option('hesabix_v2_debug_mode')) {
$raw_preview = (string) $body;
if (strlen($raw_preview) > 8192) {
$raw_preview = substr($raw_preview, 0, 8192) . "\n…[truncated in log]";
}
Hesabix_V2_Log_Service::debug(
__('پاسخ حسابیکس ← ووکامرس', 'hesabix-v2'),
array(
'entity_type' => 'hesabix_api',
'execution_time' => $execution_time,
'request' => array(
'_summary' => sprintf('%s %s', $method, $url),
'direction_reference' => 'woocommerce_to_hesabix',
),
'response' => array(
'direction' => 'hesabix_to_woocommerce',
'method' => $method,
'url' => $url,
'endpoint' => $endpoint,
'status_code' => $status_code,
'decoded' => $result,
'raw_body_preview' => $raw_preview,
),
)
);
}
if (json_last_error() !== JSON_ERROR_NONE) {
$raw_preview = (string) $body;
if (strlen($raw_preview) > 8192) {
$raw_preview = substr($raw_preview, 0, 8192) . "\n…[truncated]";
}
Hesabix_V2_Log_Service::error(
'Invalid JSON Response',
array(
'entity_type' => 'hesabix_api',
'error' => json_last_error_msg(),
'request' => array(
'direction' => 'woocommerce_to_hesabix',
'method' => $method,
'url' => $url,
'endpoint' => $endpoint,
'headers' => $audit_headers,
'json_body' => $data,
'timeout' => $timeout,
),
'response' => array(
'direction' => 'hesabix_to_woocommerce',
'type' => 'invalid_json',
'status_code' => $status_code,
'raw_body_preview' => $raw_preview,
'json_error' => json_last_error_msg(),
),
'execution_time' => $execution_time,
)
);
return array(
'success' => false,
'message' => 'Invalid response from server',
'error_code' => 'INVALID_RESPONSE',
);
}
// برای پاسخ‌های خطا، یک پیام قابل‌نمایش بساز (API ممکن است message، error یا errors برگرداند)
if (is_array($result) && ($status_code >= 400 || (isset($result['success']) && $result['success'] === false))) {
$result['success'] = false;
$result['status_code'] = (int) $status_code;
if (empty($result['message'])) {
$result['message'] = self::extract_error_message($result, $status_code, $body);
}
}
// Add execution time to result
if (is_array($result)) {
$result['_execution_time'] = $execution_time;
}
return $result;
}
/**
* استخراج پیام خطا از پاسخ API (پشتیبانی از message، error، errors، detail)
*
* @param array $result
* @param int $status_code
* @param string $raw_body پاسخ خام برای نمایش در صورت نبود پیام
* @return string
*/
public static function extract_error_message($result, $status_code = 0, $raw_body = '')
{
if (!is_array($result)) {
return __('پاسخ نامعتبر از سرور', 'hesabix-v2') . ($raw_body ? ' ' . mb_substr($raw_body, 0, 300) : '');
}
// ساختار خطای حسابیکس: { "success": false, "error": { "code": "...", "message": "...", "details": [...] } }
if (!empty($result['error']) && is_array($result['error'])) {
$err = $result['error'];
$msg = isset($err['message']) && is_string($err['message']) ? $err['message'] : '';
if (!empty($err['details']) && is_array($err['details'])) {
$parts = array();
foreach ($err['details'] as $d) {
if (isset($d['loc'], $d['msg'])) {
$parts[] = implode('.', (array) $d['loc']) . ': ' . $d['msg'];
}
}
if (!empty($parts)) {
$msg .= ($msg ? ' — ' : '') . implode('; ', $parts);
}
}
if ($msg !== '') {
return $msg;
}
}
if (!empty($result['message']) && is_string($result['message'])) {
return $result['message'];
}
if (!empty($result['error']) && is_string($result['error'])) {
return $result['error'];
}
if (!empty($result['detail']) && is_string($result['detail'])) {
return $result['detail'];
}
if (!empty($result['errors'])) {
if (is_string($result['errors'])) {
return $result['errors'];
}
if (is_array($result['errors'])) {
$parts = array();
foreach ($result['errors'] as $key => $val) {
if (is_array($val)) {
$parts[] = $key . ': ' . implode(', ', $val);
} else {
$parts[] = $key . ': ' . $val;
}
}
return implode('; ', $parts);
}
}
// کلیدهای متداول دیگر
if (!empty($result['title']) && is_string($result['title'])) {
return $result['title'];
}
if (!empty($result['msg']) && is_string($result['msg'])) {
return $result['msg'];
}
if ($status_code === 401) {
return __('احراز هویت ناموفق. کلید API یا دسترسی کسب‌وکار را بررسی کنید.', 'hesabix-v2');
}
if ($status_code === 403) {
return __('دسترسی غیرمجاز به این منبع.', 'hesabix-v2');
}
if ($status_code === 404) {
return __('منبع یافت نشد.', 'hesabix-v2');
}
if ($status_code >= 400) {
$msg = sprintf(__('خطای سرور (کد %d)', 'hesabix-v2'), $status_code);
if ($raw_body !== '') {
$snippet = is_string($raw_body) ? mb_substr($raw_body, 0, 350) : wp_json_encode($result);
$msg .= ' — پاسخ: ' . $snippet;
}
return $msg;
}
return __('خطا در همگام‌سازی', 'hesabix-v2');
}
// ==================== Authentication ====================
/**
* Login and get session token
*
* @since 2.0.0
* @param string $email
* @param string $password
* @return array
*/
public function login($email, $password)
{
return $this->request('POST', '/auth/login', array(
'email' => $email,
'password' => $password,
));
}
/**
* Get current user info
*
* @since 2.0.0
* @return array
*/
public function get_me()
{
return $this->request('GET', '/auth/me');
}
/**
* Create personal API key
*
* @since 2.0.0
* @param string $session_token
* @param array $data
* @return array
*/
public function create_api_key($session_token, $data)
{
// Temporarily use session token
$old_key = $this->api_key;
$this->api_key = $session_token;
$result = $this->request('POST', '/auth/api-keys', $data);
$this->api_key = $old_key;
return $result;
}
/**
* Test API connection
*
* @since 2.0.0
* @return array
*/
public function test_connection()
{
$result = $this->get_me();
if (isset($result['success']) && $result['success']) {
$user_data = $result['data'] ?? null;
return array_merge(
array(
'success' => true,
'message' => __('اتصال با موفقیت برقرار شد', 'hesabix-v2'),
'user' => $user_data,
),
$this->build_connection_snapshot_payload(is_array($user_data) ? $user_data : array())
);
}
return array(
'success' => false,
'message' => $result['message'] ?? __('خطا در برقراری ارتباط', 'hesabix-v2'),
);
}
/**
* پس از احراز هویت موفق: کسب‌وکار انتخاب‌شده و سال جاری حسابیکس برای نمایش در پنل.
*
* @since 2.0.6
* @param array $api_user بدنهٔ استاندارد کاربر حسابیکس (همان خروجی /auth/me).
* @return array{connection: array<string,mixed>}
*/
private function build_connection_snapshot_payload(array $api_user)
{
$connection = array(
'stored_business_id' => (int) $this->business_id,
'business' => null,
'business_note' => null,
'fiscal_year' => null,
'fiscal_year_note' => null,
);
$business_id = (int) $this->business_id;
if (!$business_id) {
return array('connection' => $connection);
}
$list_res = $this->request('POST', '/businesses/list?take=500&skip=0&sort_by=name&sort_desc=false', null);
$items = self::normalize_businesses_list_items(is_array($list_res) ? $list_res : array());
$business_row = null;
foreach ($items as $row) {
if (!is_array($row)) {
continue;
}
$rid = (int) ($row['id'] ?? $row['business_id'] ?? 0);
if ($rid === $business_id) {
$business_row = $row;
break;
}
}
if ($business_row) {
$connection['business'] = $business_row;
} else {
if (isset($list_res['success']) && $list_res['success'] === false) {
$connection['business_note'] = isset($list_res['message'])
? (string) $list_res['message']
: __('دریافت فهرست کسب‌وکارها ناموفق بود.', 'hesabix-v2');
} else {
$connection['business_note'] = sprintf(
__('کسب‌وکار #%d در فهرست کسب‌وکارهای این کلید API دیده نشد.', 'hesabix-v2'),
$business_id
);
}
}
$fy_res = $this->get_current_fiscal_year($business_id);
if (isset($fy_res['success']) && $fy_res['success']) {
$fy_payload = isset($fy_res['data']) && is_array($fy_res['data']) ? $fy_res['data'] : null;
if ($fy_payload !== null && $fy_payload !== array()) {
$connection['fiscal_year'] = $fy_payload;
} elseif (($fy_payload === null || $fy_payload === array()) && isset($fy_res['message']) && stripos((string) $fy_res['message'], 'NO_CURRENT') !== false) {
$connection['fiscal_year_note'] = __('برای این کسب‌وکار سال مالی جاری در حسابیکس تنظیم نشده است.', 'hesabix-v2');
}
} else {
$connection['fiscal_year_note'] = isset($fy_res['message']) && $fy_res['message'] !== ''
? self::sanitize_connection_note_message((string) $fy_res['message'])
: __('بدون حق مشاهدهٔ سال مالی کسب‌وکار در حسابیکس؛ در صورت نیاز مجوز مشاهدهٔ سال مالی را به کلید برسانید.', 'hesabix-v2');
}
$connection['owner_display'] = $this->infer_owner_display($business_row, $api_user);
return array('connection' => $connection);
}
/**
* پارس آرایه items از پاسخ POST /businesses/list
*
* @param array $api_result Raw API response body.
* @return array<int, array<string,mixed>>
*/
private static function normalize_businesses_list_items(array $api_result)
{
$data = $api_result['data'] ?? $api_result;
$list = is_array($data) ? ($data['items'] ?? $data['list'] ?? $data['data'] ?? array()) : array();
if (is_array($data) && $list === array() && isset($data[0]) && is_array($data[0])) {
$list = $data;
}
return is_array($list) ? $list : array();
}
/**
* یادداشت خطای کوتاه برای نمایش در پنل (بدون جزییات داخلی API).
*/
private static function sanitize_connection_note_message($msg)
{
$m = wp_strip_all_tags($msg);
if (strlen($m) > 220) {
$m = trim(mb_substr($m, 0, 217)) . '…';
}
return $m;
}
private function infer_owner_display($business_row, array $api_user)
{
if (!is_array($business_row)) {
return null;
}
$owner_id = isset($business_row['owner_id']) ? (int) $business_row['owner_id'] : 0;
if (!$owner_id) {
return null;
}
$user_id = isset($api_user['id']) ? (int) $api_user['id'] : 0;
if ($user_id && $owner_id === $user_id) {
$name = '';
foreach (array('first_name', 'last_name') as $k) {
if (!empty($api_user[ $k ]) && is_string($api_user[ $k ])) {
$name .= trim($api_user[ $k ]) . ' ';
}
}
$name = trim($name);
if ($name === '') {
if (!empty($api_user['email']) && is_string($api_user['email'])) {
$name = $api_user['email'];
} elseif (!empty($api_user['mobile']) && is_string($api_user['mobile'])) {
$name = $api_user['mobile'];
}
}
if ($name !== '') {
return sprintf(__('شما (%s)', 'hesabix-v2'), $name);
}
return sprintf(__('شما — شناسه کاربر: %d', 'hesabix-v2'), $user_id);
}
return sprintf(__('شناسه کاربر مالک در حسابیکس: %d', 'hesabix-v2'), $owner_id);
}
/**
* سال مالی جاری کسب‌وکار ( نیاز به مجوز fiscal_years.view در حسابیکس ).
*
* @since 2.0.6
* @param int $business_id
* @return array
*/
public function get_current_fiscal_year($business_id)
{
$business_id = (int) $business_id;
if (!$business_id) {
return array(
'success' => false,
'message' => __('شناسه کسب‌وکار نامعتبر است.', 'hesabix-v2'),
);
}
return $this->request('GET', '/business/' . $business_id . '/fiscal-years/current');
}
// ==================== Businesses ====================
/**
* Get list of businesses
*
* @since 2.0.0
* @param string $session_token
* @return array
*/
public function get_businesses($session_token = null)
{
if ($session_token) {
$old_key = $this->api_key;
$this->api_key = $session_token;
}
// POST with query params (مطابق API رسمی: take, skip, sort_by, sort_desc)
$result = $this->request('POST', '/businesses/list?take=100&skip=0&sort_by=created_at&sort_desc=true', null);
if ($session_token) {
$this->api_key = $old_key;
}
return $result;
}
/**
* Get fiscal years for a business
*
* @since 2.0.0
* @param int $business_id
* @param string $session_token Optional token for setup wizard
* @return array
*/
public function get_fiscal_years($business_id, $session_token = null)
{
if ($session_token) {
$old_key = $this->api_key;
$this->api_key = $session_token;
}
$result = $this->request('GET', "/business/{$business_id}/fiscal-years");
if ($session_token) {
$this->api_key = $old_key;
}
return $result;
}
// ==================== Products ====================
/**
* Create product
*
* @since 2.0.0
* @param array $data
* @return array
*/
public function create_product($data)
{
return $this->request(
'POST',
'/products/business/' . $this->business_id,
$data
);
}
/**
* Update product
*
* @since 2.0.0
* @param int $product_id
* @param array $data
* @return array
*/
public function update_product($product_id, $data)
{
return $this->request(
'PUT',
"/products/business/{$this->business_id}/{$product_id}",
$data
);
}
/**
* ایجاد/ویرایش گروهی کالا در یک درخواست API.
*
* @since 3.4.0
* @param array $body
* @param int $timeout
* @return array
*/
public function bulk_upsert_products($body, $timeout = 150)
{
$t = (int) max(45, min(900, $timeout));
return $this->request(
'POST',
'/products/business/' . $this->business_id . '/bulk-upsert',
$body,
$t
);
}
/**
* Get product by ID
*
* @since 2.0.0
* @param int $product_id
* @return array
*/
public function get_product($product_id)
{
return $this->request(
'GET',
"/products/business/{$this->business_id}/{$product_id}"
);
}
/**
* Search products
*
* @since 2.0.0
* @param array $query
* @return array
*/
public function search_products($query = array())
{
$default_query = array(
'take' => 50,
'skip' => 0,
);
$query = array_merge($default_query, $query);
return $this->request(
'POST',
"/products/business/{$this->business_id}/search",
$query
);
}
/**
* گزارش موجودی انبار (نیاز به دسترسی reports.view در حسابیکس).
* POST /products/businesses/{business_id}/reports/inventory-stock
*
* @since 3.3.2
* @param array $body پارامترها: product_ids، warehouse_ids، track_inventory، include_zero، skip، take، …
* @param int $timeout
* @return array
*/
public function inventory_stock_report($body = array(), $timeout = 60)
{
return $this->request(
'POST',
'/products/businesses/' . (int) $this->business_id . '/reports/inventory-stock',
$body,
(int) max(25, min(180, $timeout))
);
}
/**
* ایجاد حواله انبار دستی (رسید / خروج / تعدیل / …).
* POST /warehouse-docs/business/{business_id}/create
*
* @since 4.8.0
* @param array $data
* @param int $timeout
* @return array
*/
public function create_warehouse_document($data, $timeout = 60)
{
return $this->request(
'POST',
'/warehouse-docs/business/' . (int) $this->business_id . '/create',
$data,
(int) max(25, min(180, $timeout))
);
}
/**
* قطعی‌سازی حواله انبار.
* POST /warehouse-docs/business/{business_id}/{wh_id}/post
*
* @since 4.8.0
* @param int $warehouse_document_id
* @param int $timeout
* @return array
*/
public function post_warehouse_document($warehouse_document_id, $timeout = 60)
{
$wid = absint($warehouse_document_id);
return $this->request(
'POST',
'/warehouse-docs/business/' . (int) $this->business_id . '/' . $wid . '/post',
array(),
(int) max(25, min(180, $timeout))
);
}
/**
* Delete product
*
* @since 2.0.0
* @param int $product_id
* @return array
*/
public function delete_product($product_id)
{
return $this->request(
'DELETE',
"/products/business/{$this->business_id}/{$product_id}"
);
}
/**
* بررسی استفاده کالا در اسناد (فاکتور، سند، انبار، BOM، …).
*
* @since 4.7.2
* @param int $product_id
* @return array
*/
public function check_product_usage($product_id)
{
return $this->request(
'GET',
"/products/business/{$this->business_id}/" . absint($product_id) . '/usage-check'
);
}
/**
* حذف گروهی کالاها.
*
* @since 4.7.2
* @param int[] $product_ids
* @return array
*/
public function bulk_delete_products(array $product_ids)
{
$ids = array_values(array_filter(array_map('absint', $product_ids)));
if (empty($ids)) {
return array(
'success' => false,
'message' => 'empty ids',
);
}
return $this->request(
'POST',
"/products/business/{$this->business_id}/bulk-delete",
array('ids' => $ids),
90
);
}
// ==================== Persons (Customers) ====================
/**
* Create person
*
* @since 2.0.0
* @param array $data
* @return array
*/
public function create_person($data)
{
return $this->request(
'POST',
"/persons/businesses/{$this->business_id}/persons/create",
$data
);
}
/**
* Update person
*
* @since 2.0.0
* @param int $person_id
* @param array $data
* @return array
*/
public function update_person($person_id, $data)
{
return $this->request(
'PUT',
"/persons/persons/{$person_id}",
$data
);
}
/**
* ایجاد/ویرایش گروهی اشخاص (یک درخواست به API حسابیکس).
*
* @since 3.4.0
* @param array $body بدنه شامل items و اختیاری create_if_update_missing.
* @param int $timeout
* @return array
*/
public function bulk_upsert_persons($body, $timeout = 120)
{
$t = (int) max(45, min(900, $timeout));
return $this->request(
'POST',
"/persons/businesses/{$this->business_id}/persons/bulk-upsert",
$body,
$t
);
}
/**
* ایجاد/ویرایش گروهی فاکتورها.
*
* @since 3.4.0
* @param array $body
* @param int $timeout
* @return array
*/
public function bulk_upsert_invoices($body, $timeout = 180)
{
$t = (int) max(45, min(900, $timeout));
return $this->request(
'POST',
"/invoices/business/{$this->business_id}/bulk-upsert",
$body,
$t
);
}
/**
* Get person by ID
*
* @since 2.0.0
* @param int $person_id
* @return array
*/
public function get_person($person_id)
{
return $this->request(
'GET',
"/persons/persons/{$person_id}"
);
}
/**
* لیست و جستجوی اشخاص کسب‌وکار (بدنه همان QueryInfo سرور: take، skip، search، …).
*
* مسیر رسمی API: POST /persons/businesses/{business_id}/persons — نه .../persons/search.
*
* @since 2.0.0
* @param array $query
* @return array
*/
public function search_persons($query = array())
{
$default_query = array(
'take' => 50,
'skip' => 0,
);
$query = array_merge($default_query, $query);
return $this->request(
'POST',
"/persons/businesses/{$this->business_id}/persons",
$query
);
}
/**
* جستجوی شخص با تطبیق دقیق ایمیل یا موبایل (برای جلوگیری از تکرار مهمان).
*
* @param string $email
* @param string $mobile
* @return int|null
*/
public function find_person_id_by_contact($email, $mobile)
{
$email = mb_strtolower(trim((string) $email));
$mobile = trim((string) $mobile);
if ($email !== '') {
$res = $this->search_persons(
array(
'take' => 15,
'skip' => 0,
'search' => $email,
'search_fields' => array('email'),
)
);
$pid = self::pick_person_id_exact_match($res, 'email', $email);
if ($pid) {
return $pid;
}
}
if ($mobile !== '') {
$res = $this->search_persons(
array(
'take' => 15,
'skip' => 0,
'search' => $mobile,
'search_fields' => array('mobile'),
)
);
$pid = self::pick_person_id_exact_match($res, 'mobile', $mobile);
if ($pid) {
return $pid;
}
}
return null;
}
/**
* @param array $api_result
* @param string $field email|mobile
* @param string $needle_normalized
* @return int|null
*/
private static function pick_person_id_exact_match($api_result, $field, $needle_normalized)
{
if (empty($api_result['success']) || empty($api_result['data']['items']) || !is_array($api_result['data']['items'])) {
return null;
}
foreach ($api_result['data']['items'] as $item) {
if (!isset($item['id'])) {
continue;
}
if (!isset($item[$field])) {
continue;
}
$val = mb_strtolower(trim((string) $item[$field]));
if ($val !== '' && $val === $needle_normalized) {
return (int) $item['id'];
}
}
return null;
}
/**
* Delete person
*
* @since 2.0.0
* @param int $person_id
* @return array
*/
public function delete_person($person_id)
{
return $this->request(
'DELETE',
"/persons/persons/{$person_id}"
);
}
// ==================== Invoices ====================
/**
* لیست برچسب‌های فاکتور کسب‌وکار
*
* @param bool $include_inactive
* @return array
*/
public function list_invoice_tags($include_inactive = false)
{
$q = $include_inactive ? 'true' : 'false';
return $this->request(
'GET',
"/invoices/business/{$this->business_id}/tags?include_inactive={$q}"
);
}
/**
* ایجاد برچسب فاکتور
*
* @param string $name
* @param string|null $color
* @return array
*/
public function create_invoice_tag($name, $color = null)
{
$body = array('name' => $name);
if ($color !== null && $color !== '') {
$body['color'] = $color;
}
return $this->request(
'POST',
"/invoices/business/{$this->business_id}/tags",
$body
);
}
/**
* Create invoice
*
* @since 2.0.0
* @param array $data
* @return array
*/
public function create_invoice($data)
{
return $this->request(
'POST',
"/invoices/business/{$this->business_id}",
$data
);
}
/**
* Update invoice
*
* @since 2.0.0
* @param int $invoice_id
* @param array $data
* @return array
*/
public function update_invoice($invoice_id, $data)
{
return $this->request(
'PUT',
"/invoices/business/{$this->business_id}/{$invoice_id}",
$data
);
}
/**
* Get invoice by ID
*
* @since 2.0.0
* @param int $invoice_id
* @return array
*/
public function get_invoice($invoice_id)
{
return $this->request(
'GET',
"/invoices/business/{$this->business_id}/{$invoice_id}"
);
}
/**
* لیست لایسنس‌های بازار افزونه برای کسب‌وکار فعلی.
*
* @since 4.9.0
* @return array
*/
public function get_business_marketplace_plugins()
{
return $this->request(
'GET',
"/marketplace/business/{$this->business_id}/plugins"
);
}
/**
* Search invoices
*
* @since 2.0.0
* @param array $query
* @return array
*/
public function search_invoices($query = array())
{
$default_query = array(
'take' => 50,
'skip' => 0,
);
$query = array_merge($default_query, $query);
return $this->request(
'POST',
"/invoices/business/{$this->business_id}/search",
$query
);
}
/**
* Delete invoice
*
* @since 2.0.0
* @param int $invoice_id
* @return array
*/
public function delete_invoice($invoice_id)
{
return $this->request(
'DELETE',
"/invoices/business/{$this->business_id}/{$invoice_id}"
);
}
// ==================== Categories ====================
/**
* Get categories
*
* @since 2.0.0
* @return array
*/
public function get_categories()
{
return $this->request(
'POST',
"/categories/business/{$this->business_id}/list",
array('take' => 1000, 'skip' => 0)
);
}
/**
* درخت دسته‌های کسب‌وکار (برای تطبیق نام و والد).
*
* @since 2.0.8
* @param array<string,mixed> $body
* @return array
*/
public function get_categories_tree($body = null)
{
if (!is_array($body)) {
$body = new \stdClass();
}
return $this->request(
'POST',
"/categories/business/{$this->business_id}/tree",
$body
);
}
/**
* Create category
*
* @since 2.0.0
* @param array $data
* @return array
*/
public function create_category($data)
{
return $this->request(
'POST',
"/categories/business/{$this->business_id}",
$data
);
}
/**
* به‌روزرسانی دسته (برچسب و سایر فیلدهای اختیاری)
*
* @since 2.0.8
* @param array<string,mixed> $data category_id، label، ...
* @return array
*/
public function update_category($data)
{
return $this->request(
'POST',
"/categories/business/{$this->business_id}/update",
$data
);
}
/**
* جابه‌جایی دسته در درخت (مثلاً ریشه با new_parent_id برابر null)
*
* @since 2.0.8
* @param array<string,mixed> $data category_id، new_parent_id
* @return array
*/
public function move_category($data)
{
return $this->request(
'POST',
"/categories/business/{$this->business_id}/move",
$data
);
}
// ==================== Warehouses & Bank Accounts (برای انتخاب در تنظیمات فاکتور) ====================
/**
* لیست انبارهای کسب‌وکار
* GET /warehouses/business/{business_id}
*
* @since 2.0.0
* @return array
*/
public function get_warehouses()
{
return $this->request(
'GET',
"/warehouses/business/{$this->business_id}"
);
}
/**
* لیست حساب‌های بانکی کسب‌وکار
* POST /bank-accounts/businesses/{business_id}/bank-accounts با body QueryInfo
*
* @since 2.0.0
* @return array
*/
public function get_bank_accounts()
{
// QueryInfo.take در API حسابیکس حداکثر ۱۰۰ است (le=100)؛ مقدار بالاتر → 422 و لیست خالی در تنظیمات.
return $this->request(
'POST',
"/bank-accounts/businesses/{$this->business_id}/bank-accounts",
array('take' => 100, 'skip' => 0)
);
}
/**
* لیست صندوق‌های کسب‌وکار
* POST /cash-registers/businesses/{business_id}/cash-registers (بدنه QueryInfo)
*
* @since 2.0.0
* @return array
*/
public function get_cash_registers()
{
// QueryInfo.take در API حسابیکس حداکثر ۱۰۰ است (le=100).
return $this->request(
'POST',
"/cash-registers/businesses/{$this->business_id}/cash-registers",
array('take' => 100, 'skip' => 0)
);
}
/**
* ارزهای فعال کسب‌وکار + پیش‌فرض (بدون تکرار).
* GET /currencies/business/{business_id}
*
* @since 2.0.1
* @return array
*/
public function get_business_currencies()
{
if (!$this->business_id) {
return array(
'success' => false,
'message' => __('شناسه کسب‌وکار تنظیم نشده است.', 'hesabix-v2'),
);
}
return $this->request(
'GET',
"/currencies/business/" . (int) $this->business_id
);
}
// ==================== Opening balance ====================
/**
* تراز افتتاحیه سال مالی
*
* @param int|null $fiscal_year_id
* @return array
*/
public function get_opening_balance($fiscal_year_id = null)
{
$bid = (int) $this->business_id;
if (!$bid) {
return array(
'success' => false,
'message' => __('شناسه کسب‌وکار تنظیم نشده است.', 'hesabix-v2'),
);
}
$q = '';
if ($fiscal_year_id !== null && (int) $fiscal_year_id > 0) {
$q = '?fiscal_year_id=' . (int) $fiscal_year_id;
}
return $this->request(
'GET',
"/businesses/{$bid}/opening-balance{$q}",
null,
45
);
}
/**
* ذخیره / به‌روزرسانی تراز افتتاحیه
*
* @param array $body
* @return array
*/
public function upsert_opening_balance($body)
{
$bid = (int) $this->business_id;
if (!$bid) {
return array(
'success' => false,
'message' => __('شناسه کسب‌وکار تنظیم نشده است.', 'hesabix-v2'),
);
}
return $this->request(
'PUT',
"/businesses/{$bid}/opening-balance",
$body,
120
);
}
/**
* نهایی‌سازی تراز افتتاحیه
*
* @param int|null $fiscal_year_id
* @return array
*/
public function post_opening_balance($fiscal_year_id = null)
{
$bid = (int) $this->business_id;
if (!$bid) {
return array(
'success' => false,
'message' => __('شناسه کسب‌وکار تنظیم نشده است.', 'hesabix-v2'),
);
}
$q = '';
if ($fiscal_year_id !== null && (int) $fiscal_year_id > 0) {
$q = '?fiscal_year_id=' . (int) $fiscal_year_id;
}
return $this->request(
'POST',
"/businesses/{$bid}/opening-balance/post{$q}",
null,
60
);
}
/**
* لیست تخت حساب‌ها (کد + نام)
*
* @return array
*/
public function get_accounts_flat()
{
$bid = (int) $this->business_id;
if (!$bid) {
return array(
'success' => false,
'message' => __('شناسه کسب‌وکار تنظیم نشده است.', 'hesabix-v2'),
);
}
return $this->request(
'GET',
'/accounts/business/' . $bid,
null,
60
);
}
/**
* پاک کردن URL فروشگاه و توکن پل ووکامرس در تنظیمات کسب‌وکار حسابیکس.
*
* @param int|null $business_id پیش‌فرض: کسب‌وکار ذخیره‌شده
* @return array
*/
public function clear_woocommerce_bridge_settings($business_id = null)
{
$bid = $business_id !== null ? (int) $business_id : (int) $this->business_id;
if ($bid < 1) {
return array(
'success' => false,
'message' => __('شناسه کسب‌وکار تنظیم نشده است.', 'hesabix-v2'),
);
}
return $this->request(
'PUT',
'/woocommerce/business/' . $bid . '/settings',
array(
'store_base_url' => '',
'bridge_token' => '',
),
30
);
}
}