PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.1.26
ووسلام – همگام سازی ووکامرس و باسلام v1.1.26
1.1.26 1.10.27 1.10.24 1.10.25 1.10.23 1.10.22 1.10.21 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 All 60 releases
sync-basalam / includes / Services / Orders / OrderManager.php

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

871 lines 33.9 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 self::addBasalamItemToOrder($order, $item);
296 }
297 }
298
299 if (isset($data['customer_data']['recipient']) && is_array($data['customer_data']['recipient'])) {
300 $recipient = $data['customer_data']['recipient'];
301 $province = $data['customer_data']['city']['parent']['title'] ?? '';
302 $city = $data['customer_data']['city']['title'] ?? '';
303
304 $full_name = $recipient['name'] ?? '';
305 $first_name = '';
306 $last_name = '';
307 if (!empty($full_name)) {
308 $parts = explode(' ', trim($full_name));
309 $parts = array_filter($parts);
310
311 if (count($parts) === 1) {
312 $first_name = $parts[0];
313 $last_name = $parts[0];
314 } else {
315 $first_name = array_shift($parts);
316 $last_name = implode(' ', $parts);
317 }
318 }
319
320 $prefix = syncBasalamSettings()->getSettings(SettingsConfig::CUSTOMER_PREFIX_NAME);
321 $suffix = syncBasalamSettings()->getSettings(SettingsConfig::CUSTOMER_SUFFIX_NAME);
322
323 if (!empty($prefix)) $first_name = $prefix . ' ' . $first_name;
324 if (!empty($suffix)) $last_name = $last_name . ' ' . $suffix;
325
326 // Set basic billing info
327 $order->set_billing_first_name($first_name);
328 $order->set_billing_last_name($last_name);
329 $order->set_billing_address_1($recipient['postal_address'] ?? '');
330 $order->set_billing_postcode($recipient['postal_code'] ?? '');
331 $order->set_billing_country('IR');
332 $order->set_billing_phone($recipient['mobile'] ?? '');
333
334 // Set basic shipping info
335 $order->set_shipping_first_name($first_name);
336 $order->set_shipping_last_name($last_name);
337 $order->set_shipping_address_1($recipient['postal_address'] ?? '');
338 $order->set_shipping_postcode($recipient['postal_code'] ?? '');
339 $order->set_shipping_phone($recipient['mobile'] ?? '');
340 $order->set_shipping_country('IR');
341
342 // Set state and city with PWS compatibility
343 $addressData = [
344 'province' => $province,
345 'city' => $city,
346 ];
347 GetProvincesData::setOrderAddress($order, $addressData, 'billing');
348 GetProvincesData::setOrderAddress($order, $addressData, 'shipping');
349
350 // Add shipping method based on settings
351 $shipping_method_setting = syncBasalamSettings()->getSettings(SettingsConfig::ORDER_SHIPPING_METHOD);
352
353 if (isset($data['financial_report']['shipping_submit']['total']['amount'])) {
354 $shipping_cost = $data['financial_report']['shipping_submit']['total']['amount'];
355
356 $currency = get_woocommerce_currency();
357 if ($currency === 'IRT') {
358 $shipping_cost = $shipping_cost / 10;
359 } elseif ($currency === 'IRHT') {
360 $shipping_cost = $shipping_cost / 10000;
361 } elseif ($currency === 'IRHR') {
362 $shipping_cost = $shipping_cost / 1000;
363 }
364
365 $shipping_item = new \WC_Order_Item_Shipping();
366
367 if ($shipping_method_setting === 'basalam') {
368 // Use Basalam shipping method title from API
369 if (isset($data['parcel_detail']['shipping_method']['title'])) {
370 $shipping_method_title = $data['parcel_detail']['shipping_method']['title'];
371 $shipping_item->set_method_title($shipping_method_title);
372 }
373 $shipping_item->set_method_id('basalam_shipping');
374 } elseif (strpos($shipping_method_setting, 'wc_') === 0) {
375 // Use WooCommerce shipping method
376 $wc_method_id = substr($shipping_method_setting, 3); // Remove 'wc_' prefix
377
378 // Find the shipping method instance
379 $method_instance_id = self::findShippingMethodInstanceId($wc_method_id);
380 if ($method_instance_id) {
381 $shipping_item->set_method_id($wc_method_id . ':' . $method_instance_id);
382
383 // Get the method title from WooCommerce
384 $method_title = self::getShippingMethodTitle($wc_method_id, $method_instance_id);
385 if ($method_title) {
386 $shipping_item->set_method_title($method_title);
387 }
388 } else {
389 // Fallback to method id without instance
390 $shipping_item->set_method_id($wc_method_id);
391 $shipping_item->set_method_title($wc_method_id);
392 }
393 }
394
395 $shipping_item->set_total(floatval($shipping_cost));
396 $shipping_item->set_taxes([]);
397 $order->add_item($shipping_item);
398 }
399 }
400
401 $order->calculate_totals();
402
403 $total_price = 0;
404 $products_total = 0;
405
406 if (isset($data['items']) && is_array($data['items'])) {
407 foreach ($data['items'] as $item) {
408 if (isset($item['financial_report']['report_items']) && is_array($item['financial_report']['report_items'])) {
409 foreach ($item['financial_report']['report_items'] as $report_item) {
410 if (isset($report_item['title']) && $report_item['title'] === 'قی�
411 ت �
412 حصول' && isset($report_item['amount'])) {
413 $products_total += (int) $report_item['amount'];
414 break;
415 }
416 }
417 }
418 }
419 }
420
421 if ($products_total > 0) {
422 $total_price = $products_total;
423 } else {
424 if (isset($data['financial_report']['product_cost']['report_items'][0]['amount'])) {
425 $total_price += $data['financial_report']['product_cost']['report_items'][0]['amount'];
426 }
427 }
428
429 if (isset($data['financial_report']['shipping_cost']['total']['amount'])) {
430 $total_price += $data['financial_report']['shipping_cost']['total']['amount'];
431 }
432
433 if ($total_price > 0) {
434 $currency = get_woocommerce_currency();
435 if ($currency === 'IRT') {
436 $total_price = $total_price / 10;
437 } elseif ($currency === 'IRHT') {
438 $total_price = $total_price / 10000;
439 } elseif ($currency === 'IRHR') {
440 $total_price = $total_price / 1000;
441 }
442 $order->set_total($total_price);
443 }
444
445 $order->set_payment_method('basalam payment method');
446 $order->set_payment_method_title('Basalam Payment');
447
448 $orderStatusType = syncBasalamSettings()->getSettings(SettingsConfig::ORDER_STATUES_TYPE);
449
450 $status_map = [
451 3067 => 'bslm-rejected',
452 3739 => 'bslm-wait-vendor',
453 3237 => 'bslm-preparation',
454 3238 => 'bslm-shipping',
455 3195 => 'bslm-completed',
456 3233 => 'bslm-rejected',
457 ];
458 $status_id = $data['status']['id'] ?? null;
459
460 if ($orderStatusType == 'woocommerce_statuses') {
461 $order->set_status('processing');
462 } else {
463 $order_status = $status_map[$status_id] ?? 'bslm-wait-vendor';
464 $order->set_status($order_status);
465 }
466
467 $purchase_count = $data['customer_data']['purchase_count'];
468 $fee_amount = $data['financial_report']['product_cost']['report_items'][2]['amount'] ?? 0;
469 $balance_amount = $data['financial_report']['product_cost']['total']['amount'] ?? 0;
470
471
472 $order->update_meta_data('_basalam_fee_amount', intval($fee_amount / 10));
473 $order->update_meta_data('_basalam_balance_amount', intval($balance_amount / 10));
474 $order->update_meta_data('_basalam_purchase_count', $purchase_count);
475
476 if (isset($data['hash_id'])) {
477 $order->update_meta_data('_sync_basalam_hash_id', $data['hash_id']);
478 }
479
480 self::executeWithoutWccfOrderSaveHooks(function () use ($order) {
481 $order->save();
482 }, $invoice_id, 'order->save()');
483
484 $order_id = $order->get_id();
485 if ($order_id) {
486
487 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table; no object cache for these operational queries.
488 $insert_result = $wpdb->insert(
489 $table_name,
490 [
491 'payment_id' => $payment_id,
492 'invoice_id' => $invoice_id,
493 'user_id' => $user_id,
494 'city_id' => $city_id,
495 'province_id' => $province_id,
496 'order_id' => $order_id,
497 ],
498 ['%d', '%d', '%d', '%d', '%d', '%d']
499 );
500
501 if ($insert_result === false) {
502 throw new \Exception("خطا در ذخیره اطلاعات سفارش در جدول sync_basalam_payments");
503 }
504
505 update_post_meta($order_id, '_is_sync_basalam_order', true);
506
507 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Transaction control statement; no object cache applicable.
508 $wpdb->query('COMMIT');
509
510 return [
511 'success' => true,
512 'message' => 'Order created successfully',
513 'order_id' => $order_id,
514 'status' => 200,
515 ];
516 } else {
517 throw new \Exception("خطا در ایجاد سفارش با شناسه $invoice_id ، از گزینه بررسی سفارشات استفاده ن�
518 ایید.");
519 }
520 } catch (\Exception $e) {
521 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Transaction control statement; no object cache applicable.
522 $wpdb->query('ROLLBACK');
523
524 Logger::error($e->getMessage());
525
526 return [
527 'success' => false,
528 'message' => 'Failed to create order.',
529 'error' => $e->getMessage(),
530 'status' => 500,
531 ];
532 }
533 }
534
535 public static function cancelOrderWoo($invoice_id)
536 {
537 return self::updateOrderStatus($invoice_id, 'bslm-rejected', 'bslm-rejected');
538 }
539
540 public static function completeOrderWoo($invoice_id)
541 {
542 return self::updateOrderStatus($invoice_id, 'bslm-completed', 'bslm-completed');
543 }
544
545 public static function confirmOrderWoo($invoice_id)
546 {
547 return self::updateOrderStatus($invoice_id, 'bslm-preparation', 'bslm-preparation');
548 }
549
550 public static function shippedOrderWoo($invoice_id)
551 {
552 return self::updateOrderStatus($invoice_id, 'bslm-shipping', 'bslm-shipping');
553 }
554
555 public static function updateOrderStatus($invoice_id, $status, $job = null)
556 {
557 global $wpdb;
558 $table_name = $wpdb->prefix . 'sync_basalam_payments';
559
560 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom plugin table; identifier from $wpdb->prefix, not user input.
561 $order_id = $wpdb->get_var(
562 $wpdb->prepare("SELECT order_id FROM {$table_name} WHERE invoice_id = %d", $invoice_id)
563 );
564
565 if (!$order_id) {
566 $create_result = self::createOrderWoo([
567 'invoice_id' => $invoice_id,
568 ]);
569
570 if (!empty($create_result['success']) && !empty($create_result['order_id'])) {
571 $order_id = $create_result['order_id'];
572 }
573 }
574
575 if (!$order_id) {
576 return [
577 'success' => false,
578 'message' => 'Order not found.',
579 'error' => "No WooCommerce order found for invoice_id {$invoice_id}.",
580 'invoice_id' => $invoice_id,
581 'status' => 404,
582 ];
583 }
584
585 $order = wc_get_order($order_id);
586
587 if ($order && $order instanceof \WC_Order) {
588 $order->update_status($status);
589 return [
590 'success' => true,
591 'message' => 'Order status updated successfully.',
592 'order_id' => $order_id,
593 'invoice_id' => $invoice_id,
594 'job' => $job,
595 'status_key' => $status,
596 'status' => 200,
597 ];
598 }
599
600 self::logError("آبجکت سفارش ووکا�
601 رس برای order_id {$order_id} و invoice_id {$invoice_id} �
602 عتبر نیست");
603 return [
604 'success' => false,
605 'message' => 'Invalid WooCommerce order object.',
606 'error' => "Invalid WooCommerce order object for order_id {$order_id} and invoice_id {$invoice_id}.",
607 'order_id' => $order_id,
608 'invoice_id' => $invoice_id,
609 'status' => 500,
610 ];
611 }
612
613 public static function getPlaceholderProductId()
614 {
615 $placeholder_name = 'این �
616 حصول در سایت ش�
617 ا تعریف نشده است ، برای �
618 شاهده جزییات به باسلا�
619 �
620 راجعه کنید';
621 $product_id = self::productExistsByTitle($placeholder_name);
622 if (!$product_id) {
623 $product = new \WC_Product_Simple();
624 $product->set_name($placeholder_name);
625 $product->set_status('draft');
626 $product->set_sku('placeholder-basalam-product');
627 $product->save();
628 $product_id = $product->get_id();
629 }
630
631 return $product_id;
632 }
633
634 public static function getWooProductSimpleId($sync_basalam_product_id)
635 {
636 $product = get_posts([
637 'post_type' => 'product',
638 'meta_key' => ProductMetaKey::basalamProductId(),
639 'meta_value' => $sync_basalam_product_id,
640 'posts_per_page' => 1,
641 ]);
642
643 return !empty($product) ? $product[0]->ID : null;
644 }
645
646 public static function getWooProductVariableId($sync_basalam_product_variant_id)
647 {
648 $args = [
649 'post_type' => 'product_variation',
650 'posts_per_page' => 1,
651 'meta_key' => 'sync_basalam_variation_id',
652 'meta_value' => $sync_basalam_product_variant_id,
653 'fields' => 'ids',
654 ];
655
656 $variation = get_posts($args);
657
658 return !empty($variation) ? $variation[0] : null;
659 }
660
661 public static function productExistsByTitle($title)
662 {
663 global $wpdb;
664 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct lookup on core posts table; no cache key available for this title match.
665 $product_id = $wpdb->get_var(
666 $wpdb->prepare(
667 "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status != 'private' AND post_title = %s LIMIT 1",
668 $title
669 )
670 );
671
672 return $product_id ? $product_id : false;
673 }
674
675 public const BASALAM_ITEM_NAME_META_KEY = 'نا�
676 �
677 حصول در باسلا�
678 ';
679 public const BASALAM_ITEM_LINK_META_KEY = 'لینک �
680 حصول در باسلا�
681 ';
682
683 public static function addBasalamItemToOrder(\WC_Order $order, array $item)
684 {
685 $sync_basalam_product_id = $item['product']['id'] ?? null;
686 $quantity = $item['quantity'] ?? 1;
687 $item_id = $item['id'] ?? null;
688
689 if (!$sync_basalam_product_id) {
690 return;
691 }
692
693 try {
694 $basalam_item_name = self::extractBasalamItemName($item);
695
696 if (!empty($item['variation']['id'])) {
697 $woo_product_id = self::getWooProductVariableId($item['variation']['id']);
698 } else {
699 $woo_product_id = self::getWooProductSimpleId($sync_basalam_product_id);
700 }
701
702 $product = null;
703
704 if ($woo_product_id) {
705 $product = wc_get_product($woo_product_id);
706 }
707
708
709
710 if (!$product) {
711 $placeholder_product_id = self::getPlaceholderProductId();
712
713 if ($placeholder_product_id) {
714 $product = wc_get_product($placeholder_product_id);
715 }
716 }
717
718 if (!$product) {
719 return;
720 }
721
722 $order_item_id = $order->add_product($product, $quantity);
723
724 if ($item_id && $order_item_id) {
725 $order->update_meta_data('_sync_basalam_item_id_' . $order_item_id, $item_id);
726 }
727
728 self::setBasalamItemNameOnOrderItem($order, $order_item_id, $basalam_item_name, $sync_basalam_product_id);
729 self::set_item_price_from_financial_report($order, $order_item_id, $item, $quantity);
730 } catch (\Exception $e) {
731 Logger::error('خطا در ایجاد سفارش: ' . $e->getMessage());
732 }
733 }
734
735 public static function extractBasalamItemName(array $item): string
736 {
737 $product = is_array($item['product'] ?? null) ? $item['product'] : [];
738 $productTitle = trim((string) ($product['title'] ?? ($product['name'] ?? '')));
739
740 if ($productTitle === '') {
741 return '';
742 }
743
744 $variationTitle = '';
745 if (is_array($item['variation'] ?? null)) {
746 $variationTitle = trim((string) ($item['variation']['title'] ?? ($item['variation']['name'] ?? '')));
747 }
748
749 if ($variationTitle !== '' && mb_stripos($productTitle, $variationTitle) === false) {
750 return $productTitle . ' - ' . $variationTitle;
751 }
752
753 return $productTitle;
754 }
755
756 public static function setBasalamItemNameOnOrderItem($order, $order_item_id, $basalam_item_name, $basalam_product_id = null)
757 {
758 $basalam_item_name = trim((string) $basalam_item_name);
759
760 if (empty($order_item_id) || $basalam_item_name === '' || !$order instanceof \WC_Order) {
761 return false;
762 }
763
764 $order_item = $order->get_item($order_item_id);
765 if (!$order_item instanceof \WC_Order_Item_Product) {
766 return false;
767 }
768
769 $order_item->update_meta_data(self::BASALAM_ITEM_NAME_META_KEY, $basalam_item_name);
770
771 if ($basalam_product_id) {
772 $link = 'https://basalam.com/p/' . (int) $basalam_product_id;
773 $order_item->update_meta_data(
774 self::BASALAM_ITEM_LINK_META_KEY,
775 '<a href="' . esc_url($link) . '" target="_blank" rel="noopener">' . esc_html($basalam_item_name) . '</a>'
776 );
777 }
778
779 $order_item->save();
780
781 return true;
782 }
783
784 private static function set_item_price_from_financial_report($order, $order_item_id, $item, $quantity)
785 {
786 $product_price = 0;
787 if (isset($item['financial_report']['report_items']) && is_array($item['financial_report']['report_items'])) {
788 foreach ($item['financial_report']['report_items'] as $report_item) {
789 if (isset($report_item['title']) && $report_item['title'] === 'قی�
790 ت �
791 حصول' && isset($report_item['amount'])) {
792 $product_price = (int) $report_item['amount'];
793 break;
794 }
795 }
796 }
797
798 if ($product_price > 0) {
799 $currency = get_woocommerce_currency();
800 if ($currency === 'IRT') {
801 $product_price = $product_price / 10;
802 } elseif ($currency === 'IRHT') {
803 $product_price = $product_price / 10000;
804 } elseif ($currency === 'IRHR') {
805 $product_price = $product_price / 1000;
806 }
807
808 $order_item = $order->get_item($order_item_id);
809 if ($order_item) {
810 $order_item->set_subtotal($product_price);
811 $order_item->set_total($product_price);
812 $order_item->save();
813 }
814 }
815 }
816
817 private static function findShippingMethodInstanceId($method_id)
818 {
819 if (!class_exists('WC_Shipping_Zones')) {
820 return null;
821 }
822
823 $shipping_zones = \WC_Shipping_Zones::get_zones();
824
825 foreach ($shipping_zones as $zone) {
826 $zone_id = $zone['id'] ?? 0;
827 $shipping_zone = new \WC_Shipping_Zone($zone_id);
828 $methods = $shipping_zone->get_shipping_methods(true);
829
830 foreach ($methods as $method) {
831 if ($method->id === $method_id) {
832 return $method->instance_id;
833 }
834 }
835 }
836
837 return null;
838 }
839
840 private static function getShippingMethodTitle($method_id, $instance_id)
841 {
842 if (!class_exists('WC_Shipping_Zones')) {
843 return null;
844 }
845
846 $shipping_zones = \WC_Shipping_Zones::get_zones();
847
848 foreach ($shipping_zones as $zone) {
849 $zone_id = $zone['id'] ?? 0;
850 $shipping_zone = new \WC_Shipping_Zone($zone_id);
851 $methods = $shipping_zone->get_shipping_methods(true);
852
853 foreach ($methods as $method) {
854 if ($method->id === $method_id && $method->instance_id == $instance_id) {
855 return $method->get_title() ?: $method->get_method_title();
856 }
857 }
858 }
859
860 return null;
861 }
862
863 private static function createRestResponse(array $result)
864 {
865 $status = (int) ($result['status'] ?? (!empty($result['success']) ? 200 : 500));
866 unset($result['status']);
867
868 return new \WP_REST_Response($result, $status);
869 }
870 }
871