PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.19
ووسلام – همگام سازی ووکامرس و باسلام v1.10.19
1.10.19 1.10.20 1.10.18 1.10.17 1.10.15 1.10.14 1.10.13 1.10.12 1.10.10 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.2 1.9.1 1.9.0 1.8.8 1.8.5 1.8.6 All 53 releases
sync-basalam / includes / Services / Orders / OrderManager.php

OrderManager.php in ووسلام – همگام سازی ووکامرس و باسلام 1.10.19, at includes/Services/Orders/OrderManager.php

802 lines 32.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SyncBasalam\Services\Orders;
4
5 use SyncBasalam\Admin\Settings\SettingsConfig;
6 use SyncBasalam\Config\Endpoints;
7 use SyncBasalam\Logger\Logger;
8 use SyncBasalam\Utilities\GetProvincesData;
9 use SyncBasalam\Utilities\ProductMetaKey;
10 use SyncBasalam\Services\ApiServiceManager;
11
12 defined('ABSPATH') || exit;
13
14 class OrderManager
15 {
16
17 private static function shouldBypassWccfOrderHooks()
18 {
19 return class_exists('WCCF_WC_Order') && !class_exists('RightPress_Product_Price_Shop');
20 }
21
22 private static function removeWccfOrderSaveHooks()
23 {
24 global $wp_filter;
25
26 $removedHooks = [];
27
28 if (!is_array($wp_filter) && !($wp_filter instanceof \ArrayAccess)) {
29 return $removedHooks;
30 }
31
32 foreach ($wp_filter as $hookName => $hook) {
33 if (!isset($hook->callbacks) || !is_array($hook->callbacks)) {
34 continue;
35 }
36
37 foreach ($hook->callbacks as $priority => $callbacks) {
38 foreach ($callbacks as $callbackConfig) {
39 $callback = $callbackConfig['function'] ?? null;
40
41 if (!is_array($callback) || !is_object($callback[0] ?? null) || !isset($callback[1])) {
42 continue;
43 }
44
45 if (!($callback[0] instanceof \WCCF_WC_Order) || $callback[1] !== 'save_order_field_values') {
46 continue;
47 }
48
49 if (remove_filter($hookName, $callback, $priority)) {
50 $removedHooks[] = [
51 'hook' => $hookName,
52 'callback' => $callback,
53 'priority' => $priority,
54 'accepted_args' => $callbackConfig['accepted_args'] ?? 1,
55 ];
56 }
57 }
58 }
59 }
60
61 return $removedHooks;
62 }
63
64 private static function restoreWccfOrderSaveHooks(array $removedHooks)
65 {
66 foreach ($removedHooks as $hookConfig) {
67 add_filter(
68 $hookConfig['hook'],
69 $hookConfig['callback'],
70 $hookConfig['priority'],
71 $hookConfig['accepted_args']
72 );
73 }
74 }
75
76 private static function executeWithoutWccfOrderSaveHooks(callable $callback, $invoice_id, $contextLabel)
77 {
78 $removedWccfHooks = [];
79
80 if (self::shouldBypassWccfOrderHooks()) {
81 $removedWccfHooks = self::removeWccfOrderSaveHooks();
82 }
83
84 try {
85 return $callback();
86 } finally {
87 if (!empty($removedWccfHooks)) {
88 self::restoreWccfOrderSaveHooks($removedWccfHooks);
89 }
90 }
91 }
92
93 public static function createOrderWooFromRequest(\WP_REST_Request $request)
94 {
95 return self::createRestResponse(
96 self::createOrderWoo($request->get_params())
97 );
98 }
99
100 public static function orderManger(\WP_REST_Request $request, $checkSyncStatus = true)
101 {
102 $parsedParams = $request->get_params();
103
104 if ($checkSyncStatus && !syncBasalamSettings()->getSettings(SettingsConfig::SYNC_STATUS_ORDER)) {
105 return self::createRestResponse([
106 'success' => true,
107 'message' => 'Order sync is disabled.',
108 'status' => 200,
109 ]);
110 }
111
112 Logger::debug("دریافت رویداد سفارش: " . json_encode($parsedParams));
113
114 if (isset($parsedParams['event_id']) && $parsedParams['event_id'] == 7) {
115 if ($parsedParams['type'] == 'shipped') {
116 return self::createRestResponse(self::shippedOrderWoo($parsedParams['invoice_id']));
117 } elseif ($parsedParams['type'] == 'cancelled') {
118 return self::createRestResponse(self::cancelOrderWoo($parsedParams['invoice_id']));
119 } elseif ($parsedParams['type'] == 'preparation') {
120 return self::createRestResponse(self::confirmOrderWoo($parsedParams['invoice_id']));
121 }
122 } elseif (isset($parsedParams['event_id']) && $parsedParams['event_id'] == 3) {
123 if ($parsedParams['status'] == '3195') {
124 return self::createRestResponse(self::completeOrderWoo($parsedParams['more_data']['invoice_id']));
125 } elseif ($parsedParams['status'] == '3067' || $parsedParams['status'] == '3233') {
126 return self::createRestResponse(self::cancelOrderWoo($parsedParams['more_data']['invoice_id']));
127 }
128 } else {
129 return self::createOrderWooFromRequest($request);
130 }
131
132 return self::createRestResponse([
133 'success' => false,
134 'message' => 'Unsupported order event.',
135 'error' => 'The incoming event did not match any supported order action.',
136 'status' => 400,
137 ]);
138 }
139
140 public static function createOrderWoo($params)
141 {
142 $payment_id = $params['payment_id'] ?? null;
143 $invoice_id = $params['invoice_id'] ?? null;
144 $user_id = $params['user_id'] ?? null;
145 $city_id = $params['city_id'] ?? null;
146 $province_id = $params['province_id'] ?? null;
147
148 global $wpdb;
149 $table_name = $wpdb->prefix . 'sync_basalam_payments';
150
151 if (empty($invoice_id)) {
152 return [
153 'success' => false,
154 'message' => 'Missing invoice_id.',
155 'error' => 'invoice_id is required to create an order.',
156 'status' => 400,
157 ];
158 }
159
160 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom plugin table; identifier from $wpdb->prefix, not user input.
161 $existingOrderId = $wpdb->get_var(
162 $wpdb->prepare(
163 "SELECT order_id FROM {$table_name} WHERE invoice_id = %d LIMIT 1",
164 $invoice_id
165 )
166 );
167
168 if ($existingOrderId) {
169 return [
170 'success' => true,
171 'message' => 'Order already exists.',
172 'order_id' => (int) $existingOrderId,
173 'status' => 200,
174 ];
175 }
176
177 $lockName = 'sync_basalam_invoice_' . $invoice_id;
178 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Named MySQL advisory lock; no object cache applicable.
179 $gotLock = $wpdb->get_var(
180 $wpdb->prepare("SELECT GET_LOCK(%s, 0)", $lockName)
181 );
182
183 if ($gotLock !== '1' && $gotLock !== 1) {
184 Logger::debug("ریکوئست تکراری webhook برای invoice_id {$invoice_id} ـ نادیده گرفته شد (در حال پردازش توسط ریکوئست دیگر).");
185 return [
186 'success' => true,
187 'message' => 'Order is already being processed by another request.',
188 'status' => 200,
189 ];
190 }
191
192 try {
193 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom plugin table; identifier from $wpdb->prefix, not user input.
194 $existingOrderId = $wpdb->get_var(
195 $wpdb->prepare(
196 "SELECT order_id FROM {$table_name} WHERE invoice_id = %d LIMIT 1",
197 $invoice_id
198 )
199 );
200
201 if ($existingOrderId) {
202 return [
203 'success' => true,
204 'message' => 'Order already exists.',
205 'order_id' => (int) $existingOrderId,
206 'status' => 200,
207 ];
208 }
209
210 return self::createOrderWooLocked($params, $invoice_id, $payment_id, $user_id, $city_id, $province_id, $table_name);
211 } finally {
212 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Named MySQL advisory lock; no object cache applicable.
213 $wpdb->query(
214 $wpdb->prepare("SELECT RELEASE_LOCK(%s)", $lockName)
215 );
216 }
217 }
218
219 private static function createOrderWooLocked($params, $invoice_id, $payment_id, $user_id, $city_id, $province_id, $table_name)
220 {
221 global $wpdb;
222
223 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Transaction control statement; no object cache applicable.
224 $wpdb->query('START TRANSACTION');
225 try {
226
227 $vendor_id = syncBasalamSettings()->getSettings(SettingsConfig::VENDOR_ID);
228
229 $api_url = sprintf(Endpoints::ORDER_DETAIL, $vendor_id, $invoice_id);
230
231 $apiServiceManager = syncBasalamContainer()->get(ApiServiceManager::class);
232
233 $response = $apiServiceManager->get($api_url);
234
235 if (isset($response['success']) && !$response['success']) {
236 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Transaction control statement; no object cache applicable.
237 $wpdb->query('ROLLBACK');
238 Logger::error("درخواست API نا�
239 وفق بود: " . ($response['error'] ?? 'خطای نا�
240 شخص'));
241
242 return [
243 'success' => false,
244 'message' => 'Failed to fetch invoice details.',
245 'error' => $response['error'] ?? 'Unknown error',
246 'status' => 500,
247 ];
248 }
249
250 $api_response = $response['body'] ?? '';
251 $data = json_decode($api_response, true);
252
253 if (json_last_error() !== JSON_ERROR_NONE) {
254 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Transaction control statement; no object cache applicable.
255 $wpdb->query('ROLLBACK');
256
257 return [
258 'success' => false,
259 'message' => 'Failed to parse API response.',
260 'error' => 'Invalid JSON response: ' . json_last_error_msg(),
261 'status' => 500,
262 ];
263 }
264
265 if (empty($data)) {
266 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Transaction control statement; no object cache applicable.
267 $wpdb->query('ROLLBACK');
268 Logger::error("پاسخ خالی از API برای فاکتور دریافت شد: $invoice_id");
269
270 return [
271 'success' => false,
272 'message' => 'Empty API response.',
273 'error' => 'No data received from API',
274 'status' => 500,
275 ];
276 }
277
278 $user_id = $user_id ?? ($data['customer_data']['user']['id'] ?? null);
279 $city_id = $city_id ?? ($data['customer_data']['city']['id'] ?? null);
280 $province_id = $province_id ?? ($data['customer_data']['city']['parent']['id'] ?? null);
281
282 $order = self::executeWithoutWccfOrderSaveHooks(function () {
283 return wc_create_order();
284 }, $invoice_id, 'wc_create_order');
285
286 if (is_wp_error($order)) {
287 throw new \RuntimeException('wc_create_order failed: ' . $order->get_error_message());
288 }
289
290 if (!$order instanceof \WC_Order) {
291 throw new \RuntimeException('wc_create_order did not return a valid WC_Order instance.');
292 }
293 if (isset($data['items']) && is_array($data['items'])) {
294 foreach ($data['items'] as $item) {
295 $sync_basalam_product_id = $item['product']['id'] ?? null;
296 $quantity = $item['quantity'] ?? 1;
297 $item_id = $item['id'] ?? null;
298
299 if ($sync_basalam_product_id) {
300 try {
301 if (!empty($item['variation']['id'])) {
302 $woo_product_id = self::getWooProductVariableId($item['variation']['id']);
303 } else {
304 $woo_product_id = self::getWooProductSimpleId($sync_basalam_product_id);
305 }
306
307 if ($woo_product_id) {
308 $product = wc_get_product($woo_product_id);
309 if ($product) {
310 $order_item_id = $order->add_product($product, $quantity);
311 if ($item_id && $order_item_id) {
312 $order->update_meta_data('_sync_basalam_item_id_' . $order_item_id, $item_id);
313 }
314
315 self::set_item_price_from_financial_report($order, $order_item_id, $item, $quantity);
316 }
317 } else {
318 $placeholder_product_id = self::getPlaceholderProductId();
319 if ($placeholder_product_id) {
320 $placeholder_product = wc_get_product($placeholder_product_id);
321 if ($placeholder_product) {
322 $order_item_id = $order->add_product($placeholder_product, $quantity);
323
324 if ($item_id && $order_item_id) {
325 $order->update_meta_data('_sync_basalam_item_id_' . $order_item_id, $item_id);
326 }
327
328 self::set_item_price_from_financial_report($order, $order_item_id, $item, $quantity);
329 }
330 }
331 }
332 } catch (\Exception $e) {
333 Logger::error('خطا در ایجاد سفارش: ' . $e->getMessage());
334 }
335 }
336 }
337 }
338
339 if (isset($data['customer_data']['recipient']) && is_array($data['customer_data']['recipient'])) {
340 $recipient = $data['customer_data']['recipient'];
341 $province = $data['customer_data']['city']['parent']['title'] ?? '';
342 $city = $data['customer_data']['city']['title'] ?? '';
343
344 $full_name = $recipient['name'] ?? '';
345 $first_name = '';
346 $last_name = '';
347 if (!empty($full_name)) {
348 $parts = explode(' ', trim($full_name));
349 $parts = array_filter($parts);
350
351 if (count($parts) === 1) {
352 $first_name = $parts[0];
353 $last_name = $parts[0];
354 } else {
355 $first_name = array_shift($parts);
356 $last_name = implode(' ', $parts);
357 }
358 }
359
360 $prefix = syncBasalamSettings()->getSettings(SettingsConfig::CUSTOMER_PREFIX_NAME);
361 $suffix = syncBasalamSettings()->getSettings(SettingsConfig::CUSTOMER_SUFFIX_NAME);
362
363 if (!empty($prefix)) $first_name = $prefix . ' ' . $first_name;
364 if (!empty($suffix)) $last_name = $last_name . ' ' . $suffix;
365
366 // Set basic billing info
367 $order->set_billing_first_name($first_name);
368 $order->set_billing_last_name($last_name);
369 $order->set_billing_address_1($recipient['postal_address'] ?? '');
370 $order->set_billing_postcode($recipient['postal_code'] ?? '');
371 $order->set_billing_country('IR');
372 $order->set_billing_phone($recipient['mobile'] ?? '');
373
374 // Set basic shipping info
375 $order->set_shipping_first_name($first_name);
376 $order->set_shipping_last_name($last_name);
377 $order->set_shipping_address_1($recipient['postal_address'] ?? '');
378 $order->set_shipping_postcode($recipient['postal_code'] ?? '');
379 $order->set_shipping_phone($recipient['mobile'] ?? '');
380 $order->set_shipping_country('IR');
381
382 // Set state and city with PWS compatibility
383 $addressData = [
384 'province' => $province,
385 'city' => $city,
386 ];
387 GetProvincesData::setOrderAddress($order, $addressData, 'billing');
388 GetProvincesData::setOrderAddress($order, $addressData, 'shipping');
389
390 // Add shipping method based on settings
391 $shipping_method_setting = syncBasalamSettings()->getSettings(SettingsConfig::ORDER_SHIPPING_METHOD);
392
393 if (isset($data['financial_report']['shipping_submit']['total']['amount'])) {
394 $shipping_cost = $data['financial_report']['shipping_submit']['total']['amount'];
395
396 $currency = get_woocommerce_currency();
397 if ($currency === 'IRT') {
398 $shipping_cost = $shipping_cost / 10;
399 } elseif ($currency === 'IRHT') {
400 $shipping_cost = $shipping_cost / 10000;
401 } elseif ($currency === 'IRHR') {
402 $shipping_cost = $shipping_cost / 1000;
403 }
404
405 $shipping_item = new \WC_Order_Item_Shipping();
406
407 if ($shipping_method_setting === 'basalam') {
408 // Use Basalam shipping method title from API
409 if (isset($data['parcel_detail']['shipping_method']['title'])) {
410 $shipping_method_title = $data['parcel_detail']['shipping_method']['title'];
411 $shipping_item->set_method_title($shipping_method_title);
412 }
413 $shipping_item->set_method_id('basalam_shipping');
414 } elseif (strpos($shipping_method_setting, 'wc_') === 0) {
415 // Use WooCommerce shipping method
416 $wc_method_id = substr($shipping_method_setting, 3); // Remove 'wc_' prefix
417
418 // Find the shipping method instance
419 $method_instance_id = self::findShippingMethodInstanceId($wc_method_id);
420 if ($method_instance_id) {
421 $shipping_item->set_method_id($wc_method_id . ':' . $method_instance_id);
422
423 // Get the method title from WooCommerce
424 $method_title = self::getShippingMethodTitle($wc_method_id, $method_instance_id);
425 if ($method_title) {
426 $shipping_item->set_method_title($method_title);
427 }
428 } else {
429 // Fallback to method id without instance
430 $shipping_item->set_method_id($wc_method_id);
431 $shipping_item->set_method_title($wc_method_id);
432 }
433 }
434
435 $shipping_item->set_total(floatval($shipping_cost));
436 $shipping_item->set_taxes([]);
437 $order->add_item($shipping_item);
438 }
439 }
440
441 $order->calculate_totals();
442
443 $total_price = 0;
444 $products_total = 0;
445
446 if (isset($data['items']) && is_array($data['items'])) {
447 foreach ($data['items'] as $item) {
448 if (isset($item['financial_report']['report_items']) && is_array($item['financial_report']['report_items'])) {
449 foreach ($item['financial_report']['report_items'] as $report_item) {
450 if (isset($report_item['title']) && $report_item['title'] === 'قی�
451 ت �
452 حصول' && isset($report_item['amount'])) {
453 $products_total += (int) $report_item['amount'];
454 break;
455 }
456 }
457 }
458 }
459 }
460
461 if ($products_total > 0) {
462 $total_price = $products_total;
463 } else {
464 if (isset($data['financial_report']['product_cost']['report_items'][0]['amount'])) {
465 $total_price += $data['financial_report']['product_cost']['report_items'][0]['amount'];
466 }
467 }
468
469 if (isset($data['financial_report']['shipping_cost']['total']['amount'])) {
470 $total_price += $data['financial_report']['shipping_cost']['total']['amount'];
471 }
472
473 if ($total_price > 0) {
474 $currency = get_woocommerce_currency();
475 if ($currency === 'IRT') {
476 $total_price = $total_price / 10;
477 } elseif ($currency === 'IRHT') {
478 $total_price = $total_price / 10000;
479 } elseif ($currency === 'IRHR') {
480 $total_price = $total_price / 1000;
481 }
482 $order->set_total($total_price);
483 }
484
485 $order->set_payment_method('basalam payment method');
486 $order->set_payment_method_title('Basalam Payment');
487
488 $orderStatusType = syncBasalamSettings()->getSettings(SettingsConfig::ORDER_STATUES_TYPE);
489
490 $status_map = [
491 3067 => 'bslm-rejected',
492 3739 => 'bslm-wait-vendor',
493 3237 => 'bslm-preparation',
494 3238 => 'bslm-shipping',
495 3195 => 'bslm-completed',
496 3233 => 'bslm-rejected',
497 ];
498 $status_id = $data['status']['id'] ?? null;
499
500 if ($orderStatusType == 'woocommerce_statuses') {
501 $order->set_status('processing');
502 } else {
503 $order_status = $status_map[$status_id] ?? 'bslm-wait-vendor';
504 $order->set_status($order_status);
505 }
506
507 $purchase_count = $data['customer_data']['purchase_count'];
508 $fee_amount = $data['financial_report']['product_cost']['report_items'][2]['amount'] ?? 0;
509 $balance_amount = $data['financial_report']['product_cost']['total']['amount'] ?? 0;
510
511
512 $order->update_meta_data('_basalam_fee_amount', intval($fee_amount / 10));
513 $order->update_meta_data('_basalam_balance_amount', intval($balance_amount / 10));
514 $order->update_meta_data('_basalam_purchase_count', $purchase_count);
515
516 if (isset($data['hash_id'])) {
517 $order->update_meta_data('_sync_basalam_hash_id', $data['hash_id']);
518 }
519
520 self::executeWithoutWccfOrderSaveHooks(function () use ($order) {
521 $order->save();
522 }, $invoice_id, 'order->save()');
523
524 $order_id = $order->get_id();
525 if ($order_id) {
526
527 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table; no object cache for these operational queries.
528 $insert_result = $wpdb->insert(
529 $table_name,
530 [
531 'payment_id' => $payment_id,
532 'invoice_id' => $invoice_id,
533 'user_id' => $user_id,
534 'city_id' => $city_id,
535 'province_id' => $province_id,
536 'order_id' => $order_id,
537 ],
538 ['%d', '%d', '%d', '%d', '%d', '%d']
539 );
540
541 if ($insert_result === false) {
542 throw new \Exception("خطا در ذخیره اطلاعات سفارش در جدول sync_basalam_payments");
543 }
544
545 update_post_meta($order_id, '_is_sync_basalam_order', true);
546
547 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Transaction control statement; no object cache applicable.
548 $wpdb->query('COMMIT');
549
550 return [
551 'success' => true,
552 'message' => 'Order created successfully',
553 'order_id' => $order_id,
554 'status' => 200,
555 ];
556 } else {
557 throw new \Exception("خطا در ایجاد سفارش با شناسه $invoice_id ، از گزینه بررسی سفارشات استفاده ن�
558 ایید.");
559 }
560 } catch (\Exception $e) {
561 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Transaction control statement; no object cache applicable.
562 $wpdb->query('ROLLBACK');
563
564 Logger::error($e->getMessage());
565
566 return [
567 'success' => false,
568 'message' => 'Failed to create order.',
569 'error' => $e->getMessage(),
570 'status' => 500,
571 ];
572 }
573 }
574
575 public static function cancelOrderWoo($invoice_id)
576 {
577 return self::updateOrderStatus($invoice_id, 'bslm-rejected', 'bslm-rejected');
578 }
579
580 public static function completeOrderWoo($invoice_id)
581 {
582 return self::updateOrderStatus($invoice_id, 'bslm-completed', 'bslm-completed');
583 }
584
585 public static function confirmOrderWoo($invoice_id)
586 {
587 return self::updateOrderStatus($invoice_id, 'bslm-preparation', 'bslm-preparation');
588 }
589
590 public static function shippedOrderWoo($invoice_id)
591 {
592 return self::updateOrderStatus($invoice_id, 'bslm-shipping', 'bslm-shipping');
593 }
594
595 public static function updateOrderStatus($invoice_id, $status, $job = null)
596 {
597 global $wpdb;
598 $table_name = $wpdb->prefix . 'sync_basalam_payments';
599
600 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom plugin table; identifier from $wpdb->prefix, not user input.
601 $order_id = $wpdb->get_var(
602 $wpdb->prepare("SELECT order_id FROM {$table_name} WHERE invoice_id = %d", $invoice_id)
603 );
604
605 if (!$order_id) {
606 $create_result = self::createOrderWoo([
607 'invoice_id' => $invoice_id,
608 ]);
609
610 if (!empty($create_result['success']) && !empty($create_result['order_id'])) {
611 $order_id = $create_result['order_id'];
612 }
613 }
614
615 if (!$order_id) {
616 return [
617 'success' => false,
618 'message' => 'Order not found.',
619 'error' => "No WooCommerce order found for invoice_id {$invoice_id}.",
620 'invoice_id' => $invoice_id,
621 'status' => 404,
622 ];
623 }
624
625 $order = wc_get_order($order_id);
626
627 if ($order && $order instanceof \WC_Order) {
628 $order->update_status($status);
629 return [
630 'success' => true,
631 'message' => 'Order status updated successfully.',
632 'order_id' => $order_id,
633 'invoice_id' => $invoice_id,
634 'job' => $job,
635 'status_key' => $status,
636 'status' => 200,
637 ];
638 }
639
640 self::logError("آبجکت سفارش ووکا�
641 رس برای order_id {$order_id} و invoice_id {$invoice_id} �
642 عتبر نیست");
643 return [
644 'success' => false,
645 'message' => 'Invalid WooCommerce order object.',
646 'error' => "Invalid WooCommerce order object for order_id {$order_id} and invoice_id {$invoice_id}.",
647 'order_id' => $order_id,
648 'invoice_id' => $invoice_id,
649 'status' => 500,
650 ];
651 }
652
653 public static function getPlaceholderProductId()
654 {
655 $placeholder_name = 'این �
656 حصول در سایت ش�
657 ا تعریف نشده است ، برای �
658 شاهده جزییات به باسلا�
659
660 راجعه کنید';
661 $product_id = self::productExistsByTitle($placeholder_name);
662 if (!$product_id) {
663 $product = new \WC_Product_Simple();
664 $product->set_name($placeholder_name);
665 $product->set_status('draft');
666 $product->set_sku('placeholder-basalam-product');
667 $product->save();
668 $product_id = $product->get_id();
669 }
670
671 return $product_id;
672 }
673
674 public static function getWooProductSimpleId($sync_basalam_product_id)
675 {
676 $product = get_posts([
677 'post_type' => 'product',
678 'meta_key' => ProductMetaKey::basalamProductId(),
679 'meta_value' => $sync_basalam_product_id,
680 'posts_per_page' => 1,
681 ]);
682
683 return !empty($product) ? $product[0]->ID : null;
684 }
685
686 public static function getWooProductVariableId($sync_basalam_product_variant_id)
687 {
688 $args = [
689 'post_type' => 'product_variation',
690 'posts_per_page' => 1,
691 'meta_key' => 'sync_basalam_variation_id',
692 'meta_value' => $sync_basalam_product_variant_id,
693 'fields' => 'ids',
694 ];
695
696 $variation = get_posts($args);
697
698 return !empty($variation) ? $variation[0] : null;
699 }
700
701 public static function productExistsByTitle($title)
702 {
703 global $wpdb;
704 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct lookup on core posts table; no cache key available for this title match.
705 $product_id = $wpdb->get_var(
706 $wpdb->prepare(
707 "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status != 'private' AND post_title = %s LIMIT 1",
708 $title
709 )
710 );
711
712 return $product_id ? $product_id : false;
713 }
714
715 private static function set_item_price_from_financial_report($order, $order_item_id, $item, $quantity)
716 {
717 $product_price = 0;
718 if (isset($item['financial_report']['report_items']) && is_array($item['financial_report']['report_items'])) {
719 foreach ($item['financial_report']['report_items'] as $report_item) {
720 if (isset($report_item['title']) && $report_item['title'] === 'قی�
721 ت �
722 حصول' && isset($report_item['amount'])) {
723 $product_price = (int) $report_item['amount'];
724 break;
725 }
726 }
727 }
728
729 if ($product_price > 0) {
730 $currency = get_woocommerce_currency();
731 if ($currency === 'IRT') {
732 $product_price = $product_price / 10;
733 } elseif ($currency === 'IRHT') {
734 $product_price = $product_price / 10000;
735 } elseif ($currency === 'IRHR') {
736 $product_price = $product_price / 1000;
737 }
738
739 $order_item = $order->get_item($order_item_id);
740 if ($order_item) {
741 $order_item->set_subtotal($product_price);
742 $order_item->set_total($product_price);
743 $order_item->save();
744 }
745 }
746 }
747
748 private static function findShippingMethodInstanceId($method_id)
749 {
750 if (!class_exists('WC_Shipping_Zones')) {
751 return null;
752 }
753
754 $shipping_zones = \WC_Shipping_Zones::get_zones();
755
756 foreach ($shipping_zones as $zone) {
757 $zone_id = $zone['id'] ?? 0;
758 $shipping_zone = new \WC_Shipping_Zone($zone_id);
759 $methods = $shipping_zone->get_shipping_methods(true);
760
761 foreach ($methods as $method) {
762 if ($method->id === $method_id) {
763 return $method->instance_id;
764 }
765 }
766 }
767
768 return null;
769 }
770
771 private static function getShippingMethodTitle($method_id, $instance_id)
772 {
773 if (!class_exists('WC_Shipping_Zones')) {
774 return null;
775 }
776
777 $shipping_zones = \WC_Shipping_Zones::get_zones();
778
779 foreach ($shipping_zones as $zone) {
780 $zone_id = $zone['id'] ?? 0;
781 $shipping_zone = new \WC_Shipping_Zone($zone_id);
782 $methods = $shipping_zone->get_shipping_methods(true);
783
784 foreach ($methods as $method) {
785 if ($method->id === $method_id && $method->instance_id == $instance_id) {
786 return $method->get_title() ?: $method->get_method_title();
787 }
788 }
789 }
790
791 return null;
792 }
793
794 private static function createRestResponse(array $result)
795 {
796 $status = (int) ($result['status'] ?? (!empty($result['success']) ? 200 : 500));
797 unset($result['status']);
798
799 return new \WP_REST_Response($result, $status);
800 }
801 }
802