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