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
sync-basalam / includes / Services / VendorSyncPolicy.php

VendorSyncPolicy.php in ووسلام – همگام سازی ووکامرس و باسلام 1.10.21, at includes/Services/VendorSyncPolicy.php

428 lines 13.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SyncBasalam\Services;
4
5 use SyncBasalam\Admin\Settings\SettingsConfig;
6 use SyncBasalam\Admin\Settings;
7 use SyncBasalam\Registrar\AdminRegistrar;
8
9 defined('ABSPATH') || exit;
10
11 class VendorSyncPolicy
12 {
13 public const MODE_ACTIVE = 'active';
14 public const MODE_INACTIVE_LIMITED = 'inactive_limited';
15 public const MODE_INACTIVE_SUSPENDED = 'inactive_suspended';
16
17 public const CRON_HOOK = 'sync_basalam_daily_vendor_status_check';
18 public const OPTION_NAME = 'sync_basalam_vendor_sync_state';
19
20 private const CHECK_INTERVAL_SECONDS = 24 * HOUR_IN_SECONDS;
21 private const SUSPEND_AFTER_SECONDS = 21 * DAY_IN_SECONDS;
22 private const REFRESH_LOCK = 'sync_basalam_vendor_status_refresh_lock';
23 private const REFRESH_LOCK_SECONDS = 60;
24 private const UNAVAILABLE_STATUS_CODES = [2988, 3199];
25
26 private $vendorInfoService;
27 private $clock;
28
29 public function __construct($vendorInfoService = null, $clock = null)
30 {
31 $this->vendorInfoService = $vendorInfoService ?: new VendorInfoService();
32 $this->clock = is_callable($clock) ? $clock : 'time';
33 }
34
35 public function registerHooks(): void
36 {
37 add_action(self::CRON_HOOK, [$this, 'refresh']);
38 add_action('init', [$this, 'ensureScheduled']);
39 add_action('admin_notices', [$this, 'renderAdminNotice']);
40 }
41
42 public function ensureScheduled(): void
43 {
44 if ($this->isLocalEnvironment()) {
45 self::unschedule();
46 return;
47 }
48
49 if (wp_next_scheduled(self::CRON_HOOK) !== false) return;
50
51 wp_schedule_event($this->now() + MINUTE_IN_SECONDS, 'daily', self::CRON_HOOK);
52 }
53
54 public static function unschedule(): void
55 {
56 wp_clear_scheduled_hook(self::CRON_HOOK);
57 }
58
59 public function getState(bool $refreshIfDue = true): array
60 {
61 if ($this->isLocalEnvironment()) {
62 return $this->defaultState((int) Settings::getSettings(SettingsConfig::VENDOR_ID));
63 }
64
65 if ($refreshIfDue) return $this->refreshIfDue();
66
67 return $this->getStoredState();
68 }
69
70 public function getMode(bool $refreshIfDue = true): string
71 {
72 return self::resolveMode($this->getState($refreshIfDue), $this->now());
73 }
74
75 public function canCreate(bool $refreshIfDue = true): bool
76 {
77 return $this->getMode($refreshIfDue) === self::MODE_ACTIVE;
78 }
79
80 public function canUpdate(bool $refreshIfDue = true): bool
81 {
82 return $this->getMode($refreshIfDue) !== self::MODE_INACTIVE_SUSPENDED;
83 }
84
85 public function canUpdateProductStatus(bool $refreshIfDue = true): bool
86 {
87 return $this->getMode($refreshIfDue) === self::MODE_ACTIVE;
88 }
89
90 public function shouldRestrictUpdateFields(bool $refreshIfDue = true): bool
91 {
92 return $this->getMode($refreshIfDue) === self::MODE_INACTIVE_LIMITED;
93 }
94
95 public function restrictUpdatePayload(array $productData, bool $refreshIfDue = true): array
96 {
97 return self::restrictUpdatePayloadForMode($productData, $this->getMode($refreshIfDue));
98 }
99
100 public static function restrictUpdatePayloadForMode(array $productData, string $mode): array
101 {
102 if ($mode === self::MODE_ACTIVE) return $productData;
103 if ($mode === self::MODE_INACTIVE_SUSPENDED) return [];
104
105 $allowed = [
106 'id' => true,
107 'type' => true,
108 'primary_price' => true,
109 'stock' => true,
110 'variants' => true,
111 ];
112
113 $restricted = array_intersect_key($productData, $allowed);
114
115 if (isset($restricted['variants']) && is_array($restricted['variants'])) {
116 $variantFields = [
117 'id' => true,
118 'primary_price' => true,
119 'stock' => true,
120 // Product PATCH identifies a variant by its properties. Keep
121 // those properties as identity data even in the limited mode;
122 // they are not mutable fields, and the API's variants schema
123 // requires them when the single product endpoint is used.
124 'properties' => true,
125 ];
126
127 $restricted['variants'] = array_map(function ($variant) use ($variantFields) {
128 return is_array($variant) ? array_intersect_key($variant, $variantFields) : [];
129 }, $restricted['variants']);
130
131 $restricted['variants'] = array_values(array_filter(
132 $restricted['variants'],
133 function ($variant) {
134 return !empty($variant['id']);
135 }
136 ));
137
138 if (empty($restricted['variants'])) unset($restricted['variants']);
139 }
140
141 return $restricted;
142 }
143
144 public function getRestrictionMessage(bool $refreshIfDue = true): string
145 {
146 $mode = $this->getMode($refreshIfDue);
147
148 if ($mode === self::MODE_INACTIVE_LIMITED) {
149 return 'غرفه باسلا�
150 ش�
151 ا غیرفعال است؛ ایجاد �
152 حصول جدید �
153 توقف شده و فقط قی�
154 ت و �
155 وجودی �
156 حصولات بروزرسانی �
157 ی‌شود.';
158 }
159
160 if ($mode === self::MODE_INACTIVE_SUSPENDED) {
161 return 'غرفه باسلا�
162 ش�
163 ا بیش از ۳ هفته غیرفعال بوده است؛ ایجاد و بروزرسانی �
164 حصولات تا فعال‌شدن �
165 جدد غرفه �
166 توقف است.';
167 }
168
169 return '';
170 }
171
172 public function getFrontendState(bool $refreshIfDue = true): array
173 {
174 $state = $this->getState($refreshIfDue);
175 $mode = self::resolveMode($state, $this->now());
176 $inactiveSince = isset($state['inactive_since']) ? (int) $state['inactive_since'] : 0;
177 $inactiveSeconds = $inactiveSince > 0 ? max(0, $this->now() - $inactiveSince) : 0;
178
179 return array_merge($state, [
180 'mode' => $mode,
181 'can_create' => $mode === self::MODE_ACTIVE,
182 'can_update' => $mode !== self::MODE_INACTIVE_SUSPENDED,
183 'update_fields' => $mode === self::MODE_INACTIVE_LIMITED
184 ? ['price', 'stock']
185 : ($mode === self::MODE_ACTIVE ? ['all'] : []),
186 'inactive_days' => (int) floor($inactiveSeconds / DAY_IN_SECONDS),
187 'message' => $this->getRestrictionMessage(false),
188 ]);
189 }
190
191 public function refreshIfDue(): array
192 {
193 $vendorId = (int) Settings::getSettings(SettingsConfig::VENDOR_ID);
194 $token = Settings::getSettings(SettingsConfig::TOKEN);
195 $state = $this->getStoredState();
196 $now = $this->now();
197
198 if ($this->isLocalEnvironment()) {
199 return $this->defaultState($vendorId);
200 }
201
202 if (!$vendorId || !$token) {
203 $state = $this->defaultState($vendorId);
204 update_option(self::OPTION_NAME, $state, false);
205
206 return $state;
207 }
208
209 if (!self::isRefreshDue($state, $vendorId, $now)) return $state;
210 if (get_transient(self::REFRESH_LOCK)) {
211 return (int) ($state['vendor_id'] ?? 0) === $vendorId
212 ? $state
213 : $this->defaultState($vendorId);
214 }
215
216 set_transient(self::REFRESH_LOCK, 1, self::REFRESH_LOCK_SECONDS);
217
218 try {
219 return $this->refresh();
220 } finally {
221 delete_transient(self::REFRESH_LOCK);
222 }
223 }
224
225 public function refresh(): array
226 {
227 $vendorId = (int) Settings::getSettings(SettingsConfig::VENDOR_ID);
228 $token = Settings::getSettings(SettingsConfig::TOKEN);
229 $now = $this->now();
230 $state = $this->getStoredState();
231
232 if ($this->isLocalEnvironment()) {
233 $state = $this->defaultState($vendorId);
234 update_option(self::OPTION_NAME, $state, false);
235
236 return $state;
237 }
238
239 if (!$vendorId || !$token) {
240 $state = $this->defaultState($vendorId);
241 update_option(self::OPTION_NAME, $state, false);
242
243 return $state;
244 }
245
246 if ((int) ($state['vendor_id'] ?? 0) !== $vendorId) {
247 $state = $this->defaultState($vendorId);
248 }
249
250 $vendorInfo = $this->vendorInfoService->FetchVendorInfo();
251
252 if (!is_array($vendorInfo)) {
253 $state['checked_at'] = $now;
254 $state['last_error_at'] = $now;
255 update_option(self::OPTION_NAME, $state, false);
256
257 return $state;
258 }
259
260 $state = self::stateFromVendorInfo($state, $vendorInfo, $vendorId, $now);
261 update_option(self::OPTION_NAME, $state, false);
262
263 return $state;
264 }
265
266 public function renderAdminNotice(): void
267 {
268 if ($this->isLocalEnvironment()) return;
269 if (!$this->isRelevantAdminScreen()) return;
270
271 $vendorSyncState = $this->getFrontendState();
272 if ($vendorSyncState['mode'] === self::MODE_ACTIVE) return;
273
274 $template = syncBasalamPlugin()->templatePath('notifications/VendorInactiveAlert.php');
275 if (is_readable($template)) require $template;
276 }
277
278 public static function isRefreshDue(array $state, int $vendorId, int $now): bool
279 {
280 if ((int) ($state['vendor_id'] ?? 0) !== $vendorId) return true;
281
282 $checkedAt = (int) ($state['checked_at'] ?? 0);
283
284 return $checkedAt <= 0
285 || $checkedAt > $now
286 || ($now - $checkedAt) >= self::CHECK_INTERVAL_SECONDS;
287 }
288
289 public static function resolveMode(array $state, int $now): string
290 {
291 if (array_key_exists('is_active', $state) && self::normalizeBoolean($state['is_active'])) {
292 return self::MODE_ACTIVE;
293 }
294
295 $inactiveSince = (int) ($state['inactive_since'] ?? 0);
296 if ($inactiveSince > 0 && ($now - $inactiveSince) >= self::SUSPEND_AFTER_SECONDS) {
297 return self::MODE_INACTIVE_SUSPENDED;
298 }
299
300 return self::MODE_INACTIVE_LIMITED;
301 }
302
303 public static function stateFromVendorInfo(
304 array $previousState,
305 array $vendorInfo,
306 int $vendorId,
307 int $now
308 ): array {
309 $statusCode = self::extractStatusCode($vendorInfo);
310 $statusName = self::extractStatusName($vendorInfo);
311 $isActive = self::extractIsActive($vendorInfo, $statusCode, $statusName);
312 $wasSameInactiveVendor = (int) ($previousState['vendor_id'] ?? 0) === $vendorId
313 && empty($previousState['is_active'])
314 && !empty($previousState['inactive_since']);
315
316 return [
317 'vendor_id' => $vendorId,
318 'is_active' => $isActive,
319 'status_code' => $statusCode,
320 'status_name' => $statusName,
321 'checked_at' => $now,
322 'inactive_since' => $isActive
323 ? null
324 : ($wasSameInactiveVendor ? (int) $previousState['inactive_since'] : $now),
325 'last_error_at' => null,
326 ];
327 }
328
329 private static function extractStatusCode(array $vendorInfo): ?int
330 {
331 $status = $vendorInfo['status'] ?? null;
332
333 if (is_array($status)) {
334 $status = $status['value'] ?? $status['id'] ?? null;
335 }
336
337 return is_numeric($status) ? (int) $status : null;
338 }
339
340 private static function extractStatusName(array $vendorInfo): string
341 {
342 $status = $vendorInfo['status'] ?? null;
343
344 if (is_array($status)) {
345 return trim((string) ($status['name'] ?? $status['title'] ?? ''));
346 }
347
348 return '';
349 }
350
351 private static function extractIsActive(array $vendorInfo, ?int $statusCode, string $statusName): bool
352 {
353 if ($statusCode !== null && in_array($statusCode, self::UNAVAILABLE_STATUS_CODES, true)) {
354 return false;
355 }
356
357 if (array_key_exists('is_active', $vendorInfo)) {
358 return self::normalizeBoolean($vendorInfo['is_active']);
359 }
360
361 $normalizedName = function_exists('mb_strtolower')
362 ? mb_strtolower($statusName, 'UTF-8')
363 : strtolower($statusName);
364
365 foreach (['غیرفعال', 'بسته', 'disabled', 'inactive', 'closed'] as $inactiveLabel) {
366 if ($normalizedName !== '' && strpos($normalizedName, $inactiveLabel) !== false) return false;
367 }
368
369 return true;
370 }
371
372 private static function normalizeBoolean($value): bool
373 {
374 if (is_bool($value)) return $value;
375 if (is_numeric($value)) return (int) $value === 1;
376
377 return in_array(strtolower(trim((string) $value)), ['1', 'true', 'yes', 'on'], true);
378 }
379
380 private function getStoredState(): array
381 {
382 $vendorId = (int) Settings::getSettings(SettingsConfig::VENDOR_ID);
383 $stored = get_option(self::OPTION_NAME, []);
384
385 return array_merge($this->defaultState($vendorId), is_array($stored) ? $stored : []);
386 }
387
388 private function defaultState(int $vendorId): array
389 {
390 return [
391 'vendor_id' => $vendorId,
392 'is_active' => true,
393 'status_code' => null,
394 'status_name' => '',
395 'checked_at' => 0,
396 'inactive_since' => null,
397 'last_error_at' => null,
398 ];
399 }
400
401 private function now(): int
402 {
403 return (int) call_user_func($this->clock);
404 }
405
406 private function isLocalEnvironment(): bool
407 {
408 return function_exists('wp_get_environment_type')
409 && wp_get_environment_type() === 'local';
410 }
411
412 private function isRelevantAdminScreen(): bool
413 {
414 if (method_exists(AdminRegistrar::class, 'isWoosalamRelatedAdminScreen')) {
415 return AdminRegistrar::isWoosalamRelatedAdminScreen();
416 }
417
418 $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : '';
419 if ($page !== '' && strpos($page, 'sync_basalam') === 0) return true;
420 if (in_array($page, ['basalam-onboarding', 'basalam-show-products'], true)) return true;
421
422 $screen = function_exists('get_current_screen') ? get_current_screen() : null;
423 $postType = $screen && isset($screen->post_type) ? (string) $screen->post_type : '';
424
425 return in_array($postType, ['product', 'shop_order'], true);
426 }
427 }
428