← All changes
|
includes/Services/Products/UpdateSingleProductService.php
+144
-9
1.9.2
→
1.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(esc_html('خطا در ارتباط با 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 | |
| @@ -65,8 +116,17 @@ | ||
| 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 | |
| @@ -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 | } |