← All changes
|
includes/Services/Products/UpdateSingleProductService.php
+111
-7
1.10.19
→
1.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 | { |