PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.28
ووسلام – همگام سازی ووکامرس و باسلام v1.10.28
1.10.28 1.1.26 1.10.27 1.10.24 1.10.25 1.10.23 1.10.22 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 All 61 releases
sync-basalam / includes / Admin / Product / Data / Services / VariantService.php

VariantService.php in ووسلام – همگام سازی ووکامرس و باسلام 1.10.28, at includes/Admin/Product/Data/Services/VariantService.php

265 lines 9.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SyncBasalam\Admin\Product\Data\Services;
4
5 use SyncBasalam\Admin\Settings\SettingsConfig;
6
7 defined('ABSPATH') || exit;
8
9 class VariantService
10 {
11 private $priceService;
12 private array $settings;
13
14 public function __construct()
15 {
16 $this->priceService = new PriceService();
17 $this->settings = syncBasalamSettings()->getSettings();
18 }
19
20 public function getVariants($product): array
21 {
22 if (!$product instanceof \WC_Product_Variable) return [];
23
24 return array_column($this->getVariantEntries($product), 'data');
25 }
26
27 private function getVariantEntries($product): array
28 {
29 $entries = [];
30 $seen = [];
31
32 // Children follow WooCommerce's menu order: the first matching variation
33 // owns a combination when a specific variation overlaps an "Any" one.
34 foreach ($product->get_children() as $variationId) {
35 $variation = wc_get_product($variationId);
36 if (!$variation || $variation->get_status() !== 'publish') continue;
37
38 $combinations = $this->expandVariationAttributes($variation, $product);
39 $variant = $this->createVariant($variationId, $product);
40 $mapping = get_post_meta($variationId, 'sync_basalam_variation_map', true);
41 $isWildcard = $this->isWildcardVariation($variation, $product);
42
43 foreach ($combinations as $attributes) {
44 $keyAttributes = $attributes;
45 ksort($keyAttributes);
46 $key = wp_json_encode($keyAttributes);
47 if (isset($seen[$key])) continue;
48 $seen[$key] = true;
49 if (!$variant) continue;
50
51 $data = $variant;
52 $data['properties'] = $this->getVariantProperties($variation, $product, $attributes);
53 $propertyKey = $this->getPropertyKey($data['properties']);
54 if (is_array($mapping)) {
55 unset($data['id']);
56 if (!empty($mapping[$propertyKey])) $data['id'] = $mapping[$propertyKey];
57 } elseif ($isWildcard) {
58 // Rebuild legacy empty-property variants through a product PATCH.
59 unset($data['id']);
60 }
61
62 $entries[] = ['variation_id' => $variationId, 'data' => $data];
63 }
64 }
65
66 return $entries;
67 }
68
69 private function expandVariationAttributes($variation, $product): array
70 {
71 $selected = $variation->get_variation_attributes();
72 if (!$product->get_variation_attributes()) return [];
73 $combinations = [[]];
74
75 foreach ($product->get_variation_attributes() as $name => $options) {
76 $attributeName = 'attribute_' . sanitize_title($name);
77 $value = $selected[$attributeName] ?? '';
78 $values = $value === '' ? $options : [$value];
79 $values = array_values(array_unique(array_filter($values, static function ($option) {
80 return trim((string) $option) !== '';
81 })));
82
83 $expanded = [];
84 foreach ($combinations as $combination) {
85 foreach ($values as $option) {
86 $expanded[] = array_merge($combination, [$attributeName => (string) $option]);
87 }
88 }
89 $combinations = $expanded;
90 }
91
92 return $combinations;
93 }
94
95 private function isWildcardVariation($variation, $product): bool
96 {
97 $attributes = $variation->get_variation_attributes();
98 foreach ($product->get_variation_attributes() as $name => $options) {
99 if (($attributes['attribute_' . sanitize_title($name)] ?? '') === '') return true;
100 }
101
102 return false;
103 }
104
105 private function needsExpandedMapping($product): bool
106 {
107 if (!$product instanceof \WC_Product_Variable) return false;
108
109 foreach ($product->get_children() as $variationId) {
110 $variation = wc_get_product($variationId);
111 if (!$variation) continue;
112 if ($this->isWildcardVariation($variation, $product)) return true;
113 if (get_post_meta($variationId, 'sync_basalam_variation_map', true)) return true;
114 }
115
116 return false;
117 }
118
119 /** Map every expanded remote variant back to its original WooCommerce variation. */
120 public function syncExpandedVariationIds($product, array $variants): void
121 {
122 if (!$this->needsExpandedMapping($product)) return;
123
124 $remoteIds = [];
125 foreach ($variants as $variant) {
126 if (!empty($variant['id'])) {
127 $remoteIds[$this->getPropertyKey($variant['properties'] ?? [])] = $variant['id'];
128 }
129 }
130
131 $mappings = [];
132 foreach ($this->getVariantEntries($product) as $entry) {
133 $key = $this->getPropertyKey($entry['data']['properties']);
134 if (isset($remoteIds[$key])) $mappings[$entry['variation_id']][$key] = $remoteIds[$key];
135 }
136
137 foreach ($product->get_children() as $variationId) {
138 delete_post_meta($variationId, 'sync_basalam_variation_id');
139 delete_post_meta($variationId, 'sync_basalam_variation_map');
140 if (empty($mappings[$variationId])) continue;
141
142 update_post_meta($variationId, 'sync_basalam_variation_map', $mappings[$variationId]);
143 // Separate rows keep the existing order lookup working for every id.
144 foreach (array_unique($mappings[$variationId]) as $remoteId) {
145 add_post_meta($variationId, 'sync_basalam_variation_id', $remoteId);
146 }
147 }
148 }
149
150 private function getPropertyKey(array $properties): string
151 {
152 $values = [];
153 foreach ($properties as $property) {
154 $name = $property['property'] ?? '';
155 $value = $property['value'] ?? '';
156 if (is_array($name)) $name = $name['title'] ?? $name['name'] ?? '';
157 if (is_array($value)) $value = $value['title'] ?? $value['name'] ?? '';
158 $values[$this->normalizeProperty($name)] = $this->normalizeProperty($value);
159 }
160 ksort($values);
161
162 return wp_json_encode($values);
163 }
164
165 private function normalizeProperty(string $value): string
166 {
167 $value = mb_strtolower(trim(rawurldecode($value)), 'UTF-8');
168 $value = str_replace(['ي', 'ك', '-', '_', '–', '—'], ['ی', 'ک', ' ', ' ', ' ', ' '], $value);
169
170 return preg_replace('/\s+/u', ' ', $value);
171 }
172
173 private function createVariant(int $variationId, $parentProduct): ?array
174 {
175 $variation = wc_get_product($variationId);
176 if (!$variation) return null;
177
178 $price = $this->priceService->calculateFinalPrice($variation);
179 if (!$price) return null;
180
181 $basalamVariantId = get_post_meta($variationId, 'sync_basalam_variation_id', true);
182
183 $variantData = [
184 'primary_price' => $price,
185 'stock' => $this->getVariantStock($variation, $parentProduct),
186 'properties' => $this->getVariantProperties($variation, $parentProduct),
187 ];
188
189 $sku = trim((string) $variation->get_sku());
190 if ($sku !== '') {
191 $variantData['sku'] = $sku;
192 }
193
194 // Add Basalam variant ID if it exists
195 if (!empty($basalamVariantId)) {
196 $variantData['id'] = $basalamVariantId;
197 }
198
199 return $variantData;
200 }
201
202 private function getVariantStock($variation, $parentProduct): int
203 {
204 $defaultStock = $this->settings[SettingsConfig::DEFAULT_STOCK_QUANTITY];
205 $safeStock = $this->settings[SettingsConfig::SAFE_STOCK];
206 $stockSource = $this->settings[SettingsConfig::VARIABLE_PRODUCT_STOCK_SOURCE];
207
208 [$stock, $stockStatus] = $this->resolveStockByPriority($stockSource, $variation, $parentProduct);
209
210 $calculatedStock = $stockStatus === 'instock' ? $stock ?? $defaultStock : 0;
211
212 if ($safeStock > 0 && $calculatedStock <= $safeStock) return 0;
213
214 return $calculatedStock;
215 }
216
217 private function resolveStockByPriority(string $stockSource, $variation, $parentProduct): array
218 {
219 $preferredProduct = $stockSource === 'product' ? $parentProduct : $variation;
220 $fallbackProduct = $stockSource === 'product' ? $variation : $parentProduct;
221
222 $stock = $preferredProduct->get_stock_quantity();
223 $stockStatus = $preferredProduct->get_stock_status();
224
225 // If preferred source has no numeric stock, fallback to the other source.
226 if ($stock === null && $stockStatus === 'instock') {
227 $fallbackStock = $fallbackProduct->get_stock_quantity();
228 $fallbackStockStatus = $fallbackProduct->get_stock_status();
229
230 if ($fallbackStock !== null || $fallbackStockStatus !== 'instock') {
231 $stock = $fallbackStock;
232 $stockStatus = $fallbackStockStatus;
233 }
234 }
235
236 return [$stock, $stockStatus];
237 }
238
239 private function getVariantProperties($variation, $parentProduct, ?array $attributes = null): array
240 {
241 $properties = [];
242 $variationData = $attributes ?? $variation->get_variation_attributes();
243
244 foreach ($variationData as $attributeName => $attributeValue) {
245 $taxonomyName = str_replace('attribute_', '', $attributeName);
246 $attributeLabel = str_replace(['pa_', '-'], ' ', wc_attribute_label($taxonomyName, $parentProduct));
247
248 $valueName = rawurldecode($attributeValue);
249 if (taxonomy_exists($taxonomyName)) {
250 $term = get_term_by('slug', $attributeValue, $taxonomyName);
251 if ($term && !is_wp_error($term)) {
252 $valueName = $term->name;
253 }
254 }
255
256 $properties[] = [
257 'property' => $attributeLabel,
258 'value' => str_replace('-', ' ', mb_convert_encoding($valueName, 'UTF-8', 'auto')),
259 ];
260 }
261
262 return $properties;
263 }
264 }
265