PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.22
ووسلام – همگام سازی ووکامرس و باسلام v1.10.22
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 1.10.0 1.9.2 1.9.1 1.9.0 All 56 releases
sync-basalam / includes / Services / Products / AutoConnectProducts.php

AutoConnectProducts.php in ووسلام – همگام سازی ووکامرس و باسلام 1.10.22, at includes/Services/Products/AutoConnectProducts.php

237 lines 8.4 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\Products;
4
5 use SyncBasalam\Logger\Logger;
6 use SyncBasalam\Jobs\Exceptions\RetryableException;
7 use SyncBasalam\Jobs\Exceptions\NonRetryableException;
8 use SyncBasalam\Utilities\ProductMetaKey;
9
10 defined('ABSPATH') || exit;
11
12 class AutoConnectProducts
13 {
14 private const FALLBACK_BATCH_SIZE = 10;
15
16 public function checkSameProduct($title = null, $cursor = null)
17 {
18 try {
19 $getProductData = new FetchProductsData();
20 if ($title) {
21 $title = mb_substr($title, 0, 120);
22 $syncBasalamProducts = $getProductData->getProductData($title);
23 } else {
24 $syncBasalamProducts = $getProductData->getProductData(null, $cursor);
25 }
26
27 if (!is_array($syncBasalamProducts) || !isset($syncBasalamProducts['data'])) {
28 return $title ? [] : [
29 'error' => true,
30 'message' => 'خطا در دریافت اطلاعات �
31 حصولات',
32 'status_code' => 400,
33 'has_more' => false,
34 'next_cursor' => null,
35 ];
36 }
37
38 if ($title) {
39 return $syncBasalamProducts['data'];
40 }
41
42 global $wpdb;
43
44 $matchedProducts = [];
45 $productIdMetaKey = ProductMetaKey::basalamProductId();
46
47 foreach ($syncBasalamProducts['data'] as $syncBasalamProduct) {
48 $normalizedTitle = trim($syncBasalamProduct['title']);
49
50 // A WooCommerce title often adds a model/code suffix that is
51 // not present in Basalam (for example: "... کد 50"). Use a
52 // prefix only for sufficiently specific titles; short,
53 // generic titles must retain exact-match behaviour.
54 $likeTitle = mb_strlen($normalizedTitle, 'UTF-8') >= 12
55 ? $wpdb->esc_like($normalizedTitle) . '%'
56 : $normalizedTitle;
57
58 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct lookup on core posts/postmeta tables; no cache key available for this title match.
59 $productId = $wpdb->get_var(
60 $wpdb->prepare("
61 SELECT p.ID
62 FROM {$wpdb->posts} p
63 LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = %s
64 WHERE p.post_type = 'product'
65 AND p.post_status = 'publish'
66 AND pm.post_id IS NULL
67 AND LOWER(p.post_title) LIKE LOWER(%s)
68 LIMIT 1
69 ", $productIdMetaKey, $likeTitle)
70 );
71
72 if ($productId) {
73 $connectProductService = new ConnectSingleProductService();
74 $result = $connectProductService->connectProductById($productId, $syncBasalamProduct['id']);
75
76 if ($result) {
77 Logger::info($syncBasalamProduct['title'] . ' به �
78 حصول �
79 شابه خود در باسلا�
80 �
81 تصل شد', [
82 'product_id' => $productId,
83 'ع�
84 لیات' => "اتصال اتو�
85 اتیک �
86 حصولات ووکا�
87 رس و باسلا�
88 ",
89 ]);
90 }
91
92 $matchedProducts[] = $syncBasalamProduct;
93 }
94 }
95
96 $hasMore = !empty($syncBasalamProducts['has_more']);
97 $nextCursor = $syncBasalamProducts['next_cursor'] ?? null;
98
99 if ($hasMore && !empty($nextCursor)) {
100 return [
101 'success' => true,
102 'message' => '�
103 حصولات با �
104 وفقیت به صف اتصال افزوده شدند.',
105 'status_code' => 200,
106 'has_more' => true,
107 'next_cursor' => $nextCursor,
108 ];
109 } else {
110 if (!empty($matchedProducts)) {
111 return [
112 'success' => true,
113 'message' => 'اتصال �
114 حصولات کا�
115 ل شد.',
116 'status_code' => 200,
117 'has_more' => false,
118 'next_cursor' => null,
119 'completed' => true,
120 ];
121 } else {
122 return [
123 'error' => true,
124 'message' => '�
125 حصول �
126 شابهی یافت نشد.',
127 'status_code' => 404,
128 'has_more' => false,
129 'next_cursor' => null,
130 'completed' => true,
131 ];
132 }
133 }
134 } catch (RetryableException $e) {
135 Logger::error("خطا در اتصال خودکار �
136 حصولات: " . $e->getMessage(), [
137 'operation' => 'اتصال خودکار �
138 حصولات',
139 ]);
140 throw $e;
141 } catch (NonRetryableException $e) {
142 Logger::error("خطا در اتصال خودکار �
143 حصولات: " . $e->getMessage(), [
144 'operation' => 'اتصال خودکار �
145 حصولات',
146 ]);
147 throw $e;
148 } catch (\Exception $e) {
149 Logger::error("خطا در اتصال خودکار �
150 حصولات: " . $e->getMessage(), [
151 'operation' => 'اتصال خودکار �
152 حصولات',
153 ]);
154 throw $e;
155 }
156 }
157
158 /**
159 * Search Basalam for the remaining unconnected WooCommerce products.
160 *
161 * The first pass is intentionally vendor-list based because it is cheap.
162 * This second pass reuses the exact endpoint used by the single-product
163 * screen, which catches harmless title differences that the first pass
164 * cannot see.
165 *
166 * @return array{has_more: bool, next_product_id: int, connected: int}
167 */
168 public function connectUnconnectedProductsBySearch(int $lastProductId = 0, int $batchSize = self::FALLBACK_BATCH_SIZE): array
169 {
170 global $wpdb;
171
172 $productIdMetaKey = ProductMetaKey::basalamProductId();
173 $batchSize = max(1, min(25, $batchSize));
174
175 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- This is a cursor query over core product tables; every run must see newly connected products.
176 $products = $wpdb->get_results(
177 $wpdb->prepare(
178 "SELECT p.ID, p.post_title
179 FROM {$wpdb->posts} p
180 LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = %s
181 WHERE p.post_type = 'product'
182 AND p.post_status = 'publish'
183 AND pm.post_id IS NULL
184 AND p.ID > %d
185 ORDER BY p.ID ASC
186 LIMIT %d",
187 $productIdMetaKey,
188 $lastProductId,
189 $batchSize
190 )
191 );
192
193 if (empty($products)) {
194 return [
195 'has_more' => false,
196 'next_product_id' => $lastProductId,
197 'connected' => 0,
198 ];
199 }
200
201 $connected = 0;
202 $lastSeenProductId = $lastProductId;
203 $connectProductService = new ConnectSingleProductService();
204
205 foreach ($products as $product) {
206 $lastSeenProductId = (int) $product->ID;
207 $searchResults = $this->checkSameProduct((string) $product->post_title);
208 $match = ProductTitleMatcher::bestMatch((string) $product->post_title, $searchResults);
209
210 if (!$match) continue;
211
212 if ($connectProductService->connectProductById($product->ID, $match['id'])) {
213 $connected++;
214 Logger::info($match['title'] . ' به �
215 حصول �
216 شابه خود در باسلا�
217 �
218 تصل شد', [
219 'product_id' => (int) $product->ID,
220 'basalam_product_id' => $match['id'],
221 'ع�
222 لیات' => 'اتصال جست‌وجویی �
223 حصولات ووکا�
224 رس و باسلا�
225 ',
226 ]);
227 }
228 }
229
230 return [
231 'has_more' => count($products) === $batchSize,
232 'next_product_id' => $lastSeenProductId,
233 'connected' => $connected,
234 ];
235 }
236 }
237