PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.21
ووسلام – همگام سازی ووکامرس و باسلام v1.10.21
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 1.8.8 1.8.5 All 54 releases
← All changes | includes/Services/Products/UpdateSingleProductService.php +111 -7 1.10.191.10.21 View file →
@@ -1,12 +1,17 @@
1 1 <?php
2 2
3 3 namespace SyncBasalam\Services\Products;
4 4
5 +use SyncBasalam\Admin\Product\Data\Services\VariantService;
6 +use SyncBasalam\Admin\Settings\SettingsConfig;
7 +use SyncBasalam\Admin\Settings\SettingsManager;
5 8 use SyncBasalam\Config\Endpoints;
6 9 use SyncBasalam\Services\ApiServiceManager;
7 10 use SyncBasalam\Jobs\Exceptions\RetryableException;
8 11 use SyncBasalam\Jobs\Exceptions\NonRetryableException;
12 +use SyncBasalam\Jobs\Exceptions\StaleVariationException;
13 +use SyncBasalam\Logger\Logger;
9 14 use SyncBasalam\Utilities\ProductMetaKey;
10 15 use SyncBasalam\Services\VendorSyncPolicy;
11 16
12 17 defined('ABSPATH') || exit;
@@ -14,13 +19,19 @@
14 19 class UpdateSingleProductService
15 20 {
16 21 private $apiservice;
17 22 private $variationsService;
23 + private $variantDataService;
18 24
19 - public function __construct()
25 + public function __construct(
26 + $apiservice = null,
27 + $variationsService = null,
28 + $variantDataService = null
29 + )
20 30 {
21 - $this->apiservice = syncBasalamContainer()->get(ApiServiceManager::class);
22 - $this->variationsService = syncBasalamContainer()->get(UpdateProductVariationsService::class);
31 + $this->apiservice = $apiservice ?: syncBasalamContainer()->get(ApiServiceManager::class);
32 + $this->variationsService = $variationsService ?: syncBasalamContainer()->get(UpdateProductVariationsService::class);
33 + $this->variantDataService = $variantDataService ?: new VariantService();
23 34 }
24 35
25 36 public function updateProductInBasalam($productData, $productId)
26 37 {
@@ -38,19 +49,56 @@
38 49
39 50 $syncBasalamProductId = get_post_meta($productId, ProductMetaKey::basalamProductId(), true);
40 51 ProductConnection::assertUnique($productId, $syncBasalamProductId);
41 52
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.
53 + // The dedicated variation endpoint is only used in the "custom" update mode with the
54 + // variation price/stock settings ticked. Every other mode (and every not-yet-connected
55 + // variation) sends the complete variants section inside one product PATCH, exactly like
56 + // earlier major versions: no Basalam variation ids in the payload, and the response
57 + // mapping stores the current ids.
44 58 if ($this->shouldUpdateVariationsSeparately($productId, $productData)) {
45 - $this->variationsService->updateVariations($syncBasalamProductId, $productData['variants'], $productId);
59 + $variationMappingRecovered = false;
46 60
47 - unset($productData['variants'], $productData['primary_price'], $productData['stock']);
61 + try {
62 + $this->variationsService->updateVariations($syncBasalamProductId, $productData['variants'], $productId);
63 + } catch (StaleVariationException $e) {
64 + // Core v4 returns 404 when Basalam has recreated/replaced a
65 + // variation but WooCommerce still holds its old id. This is not
66 + // an error for the user: rebuild a complete variants payload and
67 + // let the normal product PATCH return the current ids, so the
68 + // next custom-field update finds fresh variation ids again.
69 + $productData = $this->prepareVariationRemapPayload($productId, $productData);
70 + $variationMappingRecovered = true;
48 71
72 + Logger::warning('شناسه‌های قدیمی متغیرهای باسلام شناسایی شد؛ نگاشت متغیرها به‌صورت خودکار بازسازی می‌شود.', [
73 + 'product_id' => $productId,
74 + 'basalam_product_id' => $e->getBasalamProductId(),
75 + 'stale_basalam_variation_id' => $e->getBasalamVariationId(),
76 + ]);
77 + }
78 +
79 + // A stale mapping keeps the rebuilt variants in the product PATCH.
80 + // The ordinary successful path has already updated each variation,
81 + // so duplicate price/stock fields must still be removed.
82 + if (!$variationMappingRecovered) {
83 + unset($productData['variants'], $productData['primary_price'], $productData['stock']);
84 + }
85 +
49 86 if (!$this->hasProductFieldsToUpdate($productData)) {
50 87 return $this->finishUpdate($productId, [], 'متغیرهای محصول با موفقیت بروزرسانی شدند.');
51 88 }
52 89 }
90 +
91 + // The complete variants section must not carry Basalam variation ids: the API
92 + // matches variations by their properties and returns the current ids, which
93 + // are stored after the request. This is exactly the pre-1.10.4 behaviour.
94 + if (isset($productData['variants']) && is_array($productData['variants'])) {
95 + foreach ($productData['variants'] as &$variant) {
96 + if (is_array($variant)) unset($variant['id']);
97 + }
98 + unset($variant);
99 + }
100 +
53 101 $url = sprintf(Endpoints::PRODUCT_UPDATE, $syncBasalamProductId);
54 102
55 103 $maxDescriptionRetries = 3;
56 104 $descriptionRetry = 0;
@@ -186,8 +234,15 @@
186 234 if (isset($syncBasalamVariations[$key])) {
187 235 update_post_meta($wcVarId, 'sync_basalam_variation_id', $syncBasalamVariations[$key]);
188 236 }
189 237 }
238 +
239 + // Some legacy products have a single empty Basalam property
240 + // value, so neither side produces a usable property key. A
241 + // one-to-one mapping is unambiguous and safe in that case.
242 + if (count($variations) === 1 && count($body['variants']) === 1 && !empty($body['variants'][0]['id'])) {
243 + update_post_meta($variations[0], 'sync_basalam_variation_id', $body['variants'][0]['id']);
244 + }
190 245 }
191 246 }
192 247
193 248 return $this->finishUpdate($productId, $body, 'فرایند بروزرسانی محصول با موفقیت انجام شد.');
@@ -214,9 +269,58 @@
214 269
215 270 $product = \wc_get_product($productId);
216 271 if (!$product || !$product->is_type('variable')) return false;
217 272
273 + $vendorSyncPolicy = syncBasalamContainer()->get(VendorSyncPolicy::class);
274 +
275 + // A limited inactive vendor still uses the product endpoint. Its variants
276 + // payload contains price/stock plus the unchanged properties needed to
277 + // identify each variant; it must not fall back to one request per stored
278 + // variation id because those ids can be recreated by Basalam.
279 + if ($vendorSyncPolicy->shouldRestrictUpdateFields(false)) return false;
280 +
281 + // Only the "custom" mode with the variation price/stock fields ticked uses the
282 + // dedicated variation endpoint. "All fields" and "price & stock" always send
283 + // the complete product payload, exactly like earlier major versions.
284 + $syncFields = SettingsManager::getSettings(SettingsConfig::SYNC_PRODUCT_FIELDS);
285 + if ($syncFields !== 'custom') return false;
286 +
287 + $syncVariantPrice = SettingsManager::getSettings(SettingsConfig::SYNC_PRODUCT_FIELD_VARIANT_PRICE);
288 + $syncVariantStock = SettingsManager::getSettings(SettingsConfig::SYNC_PRODUCT_FIELD_VARIANT_STOCK);
289 + if ($syncVariantPrice != 1 && $syncVariantStock != 1) return false;
290 +
291 + // A variation that is not connected to Basalam yet must be created through
292 + // the product payload, not the variation endpoint.
218 293 return UpdateProductVariationsService::allVariantsHaveBasalamId($productData['variants']);
294 + }
295 +
296 + private function prepareVariationRemapPayload(int $productId, array $productData): array
297 + {
298 + $product = \wc_get_product($productId);
299 + if (!$product || !$product->is_type('variable')) {
300 + throw NonRetryableException::invalidData('محصول متغیر برای بازسازی نگاشت‌ها یافت نشد.');
301 + }
302 +
303 + $variants = $this->variantDataService->getVariants($product);
304 + if (empty($variants)) {
305 + throw NonRetryableException::invalidData('اطلاعات متغیرهای محصول برای بازسازی نگاشت‌ها کامل نیست.');
306 + }
307 +
308 + foreach ($variants as &$variant) {
309 + unset($variant['id']);
310 + }
311 + unset($variant);
312 +
313 + // Clear every old id only after the complete replacement payload has
314 + // been built. If the following API request fails, the next job retries
315 + // the safe full-product remapping path instead of the stale endpoint.
316 + foreach ($product->get_children() as $variationId) {
317 + delete_post_meta($variationId, 'sync_basalam_variation_id');
318 + }
319 +
320 + $productData['variants'] = $variants;
321 +
322 + return $productData;
219 323 }
220 324
221 325 private function hasProductFieldsToUpdate(array $productData): bool
222 326 {