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 / Products / UpdateSingleProductService.php

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

340 lines 13.0 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\Config\Endpoints;
6 use SyncBasalam\Services\ApiServiceManager;
7 use SyncBasalam\Jobs\Exceptions\RetryableException;
8 use SyncBasalam\Jobs\Exceptions\NonRetryableException;
9 use SyncBasalam\Utilities\ProductMetaKey;
10 use SyncBasalam\Services\VendorSyncPolicy;
11
12 defined('ABSPATH') || exit;
13
14 class UpdateSingleProductService
15 {
16 private $apiservice;
17 private $variationsService;
18
19 public function __construct()
20 {
21 $this->apiservice = syncBasalamContainer()->get(ApiServiceManager::class);
22 $this->variationsService = syncBasalamContainer()->get(UpdateProductVariationsService::class);
23 }
24
25 public function updateProductInBasalam($productData, $productId)
26 {
27 if (!get_post_type($productId) === 'product') throw NonRetryableException::invalidData('نوع post �
28 حصول نیست.');
29
30 $vendorSyncPolicy = syncBasalamContainer()->get(VendorSyncPolicy::class);
31 if (!$vendorSyncPolicy->canUpdate()) {
32 throw NonRetryableException::invalidData($vendorSyncPolicy->getRestrictionMessage(false));
33 }
34
35 $productData = apply_filters('sync_basalam_product_data_before_update', $productData, $productId);
36 $productData = $vendorSyncPolicy->restrictUpdatePayload($productData, false);
37
38 do_action('sync_basalam_before_update_product_api', $productId, $productData);
39
40 $syncBasalamProductId = get_post_meta($productId, ProductMetaKey::basalamProductId(), true);
41 ProductConnection::assertUnique($productId, $syncBasalamProductId);
42
43 // Variable products whose variations are all connected to Basalam are updated with one
44 // request per variation, so price and stock must not be part of the product payload.
45 if ($this->shouldUpdateVariationsSeparately($productId, $productData)) {
46 $this->variationsService->updateVariations($syncBasalamProductId, $productData['variants'], $productId);
47
48 unset($productData['variants'], $productData['primary_price'], $productData['stock']);
49
50 if (!$this->hasProductFieldsToUpdate($productData)) {
51 return $this->finishUpdate($productId, [], '�
52 تغیرهای �
53 حصول با �
54 وفقیت بروزرسانی شدند.');
55 }
56 }
57 $url = sprintf(Endpoints::PRODUCT_UPDATE, $syncBasalamProductId);
58
59 $maxDescriptionRetries = 3;
60 $descriptionRetry = 0;
61 $skuRetry = false;
62
63 while (true) {
64 try {
65 $request = $this->apiservice->patch($url, $productData);
66 } catch (RetryableException $e) {
67 if ($this->retryWithoutDuplicateSku($productData, $e, $skuRetry)) {
68 continue;
69 }
70
71 throw $e;
72 } catch (NonRetryableException $e) {
73 if ($this->retryWithoutDuplicateSku($productData, $e, $skuRetry)) {
74 continue;
75 }
76
77 if ($descriptionRetry < $maxDescriptionRetries && $this->stripForbiddenDescription($e, $productData, $productId, $descriptionRetry)) {
78 $descriptionRetry++;
79 continue;
80 }
81
82 throw $e;
83 } catch (\Exception $e) {
84 if ($this->retryWithoutDuplicateSku($productData, $e, $skuRetry)) {
85 continue;
86 }
87
88 throw new \Exception(esc_html('خطا در ارتباط با API باسلا�
89 : ' . $e->getMessage()));
90 }
91
92 // Some API adapters return a non-2xx response instead of throwing it.
93 if ($this->retryWithoutDuplicateSku($productData, $request, $skuRetry)) {
94 continue;
95 }
96
97 break;
98 }
99
100 $body = $request['body'] ?? '';
101
102 if (is_string($body)) $body = json_decode($body, true);
103
104 if ($request['status_code'] != 200) {
105 if ($request['status_code'] == 403) throw NonRetryableException::unauthorized("این �
106 حصول �
107 تعلق به غرفه فعلی نیست.");
108
109 if (!is_array($body)) $body = [];
110
111 if (isset($body['messages'][0]['message'])) $message = $body['messages'][0]['message'];
112 elseif (isset($body[0]['message'])) $message = $body[0]['message'];
113 else $message = '';
114
115 if (isset($body['messages'][0]['fields'][0])) $field = $body['messages'][0]['fields'][0];
116 elseif (isset($body[0]['fields'][0])) $field = $body[0]['fields'][0];
117 else $field = '';
118
119 $errorMessage = $message ?: 'درخواست با خطا �
120 واجه شد.';
121 if ($field) $errorMessage .= ' (فیلد: ' . $field . ')';
122
123 throw NonRetryableException::permanent(esc_html($errorMessage));
124 }
125
126 if (is_wp_error($request)) throw NonRetryableException::permanent('خطایی در ارتباط با سرور رخ داد.');
127
128 // Basalam may return a successful response with a stale/null product SKU
129 // when another product field (most commonly a long description) is
130 // present in the same patch. Send the SKU on its own when the response
131 // does not contain the value we requested, so the product-level SKU is
132 // not lost for variable products.
133 // When the duplicate-SKU fallback was used, deliberately keep the
134 // second request SKU-free; do not issue a follow-up SKU-only patch.
135 if (!$skuRetry) $this->ensureProductSkuUpdated($url, $productData, $body);
136
137 $product = \wc_get_product($productId);
138 if ($product && $product->is_type('variable')) {
139 $variations = $product->get_children();
140 if (isset($body['variants'])) {
141 $wcVariations = [];
142 $attributes = $product->get_attributes();
143
144 foreach ($variations as $variationId) {
145 $variation = \wc_get_product($variationId);
146 $attributeValues = [];
147
148 foreach ($attributes as $attributeName => $attribute) {
149 if ($attribute->get_variation()) {
150 $cleanAttributeName = str_replace('attribute_', '', $attributeName);
151 $value = $variation->get_attribute($cleanAttributeName);
152
153 $value = urldecode($value);
154 $value = trim($value);
155 $value = mb_strtolower($value, 'UTF-8');
156 $value = str_replace(['ي', 'ك'], ['ی', 'ک'], $value);
157 $value = str_replace(['-', '_', '', ''], ' ', $value);
158 $value = preg_replace('/\s+/', ' ', $value);
159
160 if (!empty($value)) $attributeValues[] = $value;
161 }
162 }
163
164 if (!empty($attributeValues)) {
165 $key = implode("_", $attributeValues);
166 $wcVariations[$key] = $variationId;
167 }
168 }
169
170 $syncBasalamVariations = [];
171 foreach ($body['variants'] as $variant) {
172 $attributeValues = [];
173 if (!empty($variant['properties'])) {
174 foreach ($variant['properties'] as $property) {
175 $val = $property['value']['title'];
176
177 $val = trim($val);
178 $val = mb_strtolower($val, 'UTF-8');
179 $val = str_replace(['ي', 'ك'], ['ی', 'ک'], $val);
180 $val = str_replace(['-', '_', '', ''], ' ', $val);
181 $val = preg_replace('/\s+/', ' ', $val);
182
183 if (!empty($val)) $attributeValues[] = $val;
184 }
185 }
186
187 if (!empty($attributeValues)) {
188 $key = implode("_", $attributeValues);
189 $syncBasalamVariations[$key] = $variant['id'];
190 }
191 }
192
193 foreach ($wcVariations as $key => $wcVarId) {
194 if (isset($syncBasalamVariations[$key])) {
195 update_post_meta($wcVarId, 'sync_basalam_variation_id', $syncBasalamVariations[$key]);
196 }
197 }
198 }
199 }
200
201 return $this->finishUpdate($productId, $body, 'فرایند بروزرسانی �
202 حصول با �
203 وفقیت انجا�
204 شد.');
205 }
206
207 private function finishUpdate($productId, $body, string $message): array
208 {
209 update_post_meta($productId, ProductMetaKey::basalamProductSyncStatus(), 'synced');
210
211 $result = [
212 'success' => true,
213 'message' => $message,
214 'status_code' => 200,
215 ];
216
217 do_action('sync_basalam_after_update_product_api', $productId, $body, $result);
218
219 return $result;
220 }
221
222 private function shouldUpdateVariationsSeparately($productId, array $productData): bool
223 {
224 if (empty($productData['variants']) || !is_array($productData['variants'])) return false;
225
226 $product = \wc_get_product($productId);
227 if (!$product || !$product->is_type('variable')) return false;
228
229 return UpdateProductVariationsService::allVariantsHaveBasalamId($productData['variants']);
230 }
231
232 private function hasProductFieldsToUpdate(array $productData): bool
233 {
234 $identifiers = ['id' => true, 'type' => true];
235
236 return !empty(array_diff_key($productData, $identifiers));
237 }
238
239 private function stripForbiddenDescription(NonRetryableException $e, array &$productData, int $productId, int $attempt): bool
240 {
241 if (!isset($productData['description']) || !is_string($productData['description'])) return false;
242
243 $values = DescriptionErrorSanitizer::extractDescriptionValues($e->getResponseData());
244 if (empty($values)) return false;
245
246 $cleaned = DescriptionErrorSanitizer::sanitize($productData['description'], $values);
247 if ($cleaned === $productData['description']) return false;
248
249 $productData['description'] = $cleaned;
250
251 return true;
252 }
253
254 private function retryWithoutDuplicateSku(array &$productData, $error, bool &$retried): bool
255 {
256 if ($retried || !ProductSkuRetry::hasSku($productData)) return false;
257 if (!ProductSkuRetry::isDuplicateSkuError($error)) return false;
258
259 $productData = ProductSkuRetry::withoutSkus($productData);
260 $retried = true;
261
262 return true;
263 }
264
265 public function updateProductStatus($productId, $status)
266 {
267 $vendorSyncPolicy = syncBasalamContainer()->get(VendorSyncPolicy::class);
268 if (!$vendorSyncPolicy->canUpdateProductStatus()) {
269 throw NonRetryableException::invalidData($vendorSyncPolicy->getRestrictionMessage(false));
270 }
271
272 $syncBasalamProductId = get_post_meta($productId, ProductMetaKey::basalamProductId(), true);
273
274 ProductConnection::assertUnique($productId, $syncBasalamProductId);
275
276 $url = sprintf(Endpoints::PRODUCT_UPDATE, $syncBasalamProductId);
277
278 $data = ["status" => $status];
279
280 $data = apply_filters('sync_basalam_product_status_data_before_update', $data, $productId, $status);
281
282 do_action('sync_basalam_before_update_product_status', $productId, $status, $data);
283
284 try {
285 $request = $this->apiservice->patch($url, $data);
286 } catch (RetryableException $e) {
287 throw $e;
288 } catch (NonRetryableException $e) {
289 throw $e;
290 } catch (\Exception $e) {
291 throw NonRetryableException::permanent(esc_html($e->getMessage()));
292 }
293
294 if (!is_wp_error($request)) {
295 update_post_meta($productId, ProductMetaKey::basalamProductSyncStatus(), 'synced');
296 update_post_meta($productId, ProductMetaKey::basalamProductStatus(), $status);
297
298 $result = [
299 'success' => true,
300 'message' => 'وضعیت �
301 حصول با �
302 وفقیت در باسلا�
303 تغییر کرد.',
304 'status_code' => 200,
305 ];
306
307 do_action('sync_basalam_after_update_product_status', $productId, $status, $result);
308
309 return $result;
310 }
311
312 throw NonRetryableException::permanent("تغییر وضعیت �
313 حصول در باسلا�
314 نا�
315 وفق بود.");
316 }
317
318 private function ensureProductSkuUpdated(string $url, array $productData, $responseBody): void
319 {
320 if (!array_key_exists('sku', $productData) || $productData['sku'] === null) return;
321
322 $requestedSku = (string) $productData['sku'];
323 $responseSku = null;
324
325 if (is_array($responseBody) && array_key_exists('sku', $responseBody)) {
326 $responseSku = $responseBody['sku'];
327 }
328
329 if ($responseSku !== null && (string) $responseSku === $requestedSku) return;
330
331 $skuRequest = $this->apiservice->patch($url, ['sku' => $productData['sku']]);
332 if (!is_array($skuRequest) || (int) ($skuRequest['status_code'] ?? 0) !== 200) {
333 throw NonRetryableException::permanent('بروزرسانی شناسه �
334 حصول (SKU) در باسلا�
335 نا�
336 وفق بود.');
337 }
338 }
339 }
340