PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.19
ووسلام – همگام سازی ووکامرس و باسلام v1.10.19
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 1.8.6 All 53 releases
sync-basalam / includes / Services / VendorSyncPolicy.php

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

423 lines 13.5 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 ];
121
122 $restricted['variants'] = array_map(function ($variant) use ($variantFields) {
123 return is_array($variant) ? array_intersect_key($variant, $variantFields) : [];
124 }, $restricted['variants']);
125
126 $restricted['variants'] = array_values(array_filter(
127 $restricted['variants'],
128 function ($variant) {
129 return !empty($variant['id']);
130 }
131 ));
132
133 if (empty($restricted['variants'])) unset($restricted['variants']);
134 }
135
136 return $restricted;
137 }
138
139 public function getRestrictionMessage(bool $refreshIfDue = true): string
140 {
141 $mode = $this->getMode($refreshIfDue);
142
143 if ($mode === self::MODE_INACTIVE_LIMITED) {
144 return 'غرفه باسلا�
145 ش�
146 ا غیرفعال است؛ ایجاد �
147 حصول جدید �
148 توقف شده و فقط قی�
149 ت و �
150 وجودی �
151 حصولات بروزرسانی �
152 ی‌شود.';
153 }
154
155 if ($mode === self::MODE_INACTIVE_SUSPENDED) {
156 return 'غرفه باسلا�
157 ش�
158 ا بیش از ۳ هفته غیرفعال بوده است؛ ایجاد و بروزرسانی �
159 حصولات تا فعال‌شدن �
160 جدد غرفه �
161 توقف است.';
162 }
163
164 return '';
165 }
166
167 public function getFrontendState(bool $refreshIfDue = true): array
168 {
169 $state = $this->getState($refreshIfDue);
170 $mode = self::resolveMode($state, $this->now());
171 $inactiveSince = isset($state['inactive_since']) ? (int) $state['inactive_since'] : 0;
172 $inactiveSeconds = $inactiveSince > 0 ? max(0, $this->now() - $inactiveSince) : 0;
173
174 return array_merge($state, [
175 'mode' => $mode,
176 'can_create' => $mode === self::MODE_ACTIVE,
177 'can_update' => $mode !== self::MODE_INACTIVE_SUSPENDED,
178 'update_fields' => $mode === self::MODE_INACTIVE_LIMITED
179 ? ['price', 'stock']
180 : ($mode === self::MODE_ACTIVE ? ['all'] : []),
181 'inactive_days' => (int) floor($inactiveSeconds / DAY_IN_SECONDS),
182 'message' => $this->getRestrictionMessage(false),
183 ]);
184 }
185
186 public function refreshIfDue(): array
187 {
188 $vendorId = (int) Settings::getSettings(SettingsConfig::VENDOR_ID);
189 $token = Settings::getSettings(SettingsConfig::TOKEN);
190 $state = $this->getStoredState();
191 $now = $this->now();
192
193 if ($this->isLocalEnvironment()) {
194 return $this->defaultState($vendorId);
195 }
196
197 if (!$vendorId || !$token) {
198 $state = $this->defaultState($vendorId);
199 update_option(self::OPTION_NAME, $state, false);
200
201 return $state;
202 }
203
204 if (!self::isRefreshDue($state, $vendorId, $now)) return $state;
205 if (get_transient(self::REFRESH_LOCK)) {
206 return (int) ($state['vendor_id'] ?? 0) === $vendorId
207 ? $state
208 : $this->defaultState($vendorId);
209 }
210
211 set_transient(self::REFRESH_LOCK, 1, self::REFRESH_LOCK_SECONDS);
212
213 try {
214 return $this->refresh();
215 } finally {
216 delete_transient(self::REFRESH_LOCK);
217 }
218 }
219
220 public function refresh(): array
221 {
222 $vendorId = (int) Settings::getSettings(SettingsConfig::VENDOR_ID);
223 $token = Settings::getSettings(SettingsConfig::TOKEN);
224 $now = $this->now();
225 $state = $this->getStoredState();
226
227 if ($this->isLocalEnvironment()) {
228 $state = $this->defaultState($vendorId);
229 update_option(self::OPTION_NAME, $state, false);
230
231 return $state;
232 }
233
234 if (!$vendorId || !$token) {
235 $state = $this->defaultState($vendorId);
236 update_option(self::OPTION_NAME, $state, false);
237
238 return $state;
239 }
240
241 if ((int) ($state['vendor_id'] ?? 0) !== $vendorId) {
242 $state = $this->defaultState($vendorId);
243 }
244
245 $vendorInfo = $this->vendorInfoService->FetchVendorInfo();
246
247 if (!is_array($vendorInfo)) {
248 $state['checked_at'] = $now;
249 $state['last_error_at'] = $now;
250 update_option(self::OPTION_NAME, $state, false);
251
252 return $state;
253 }
254
255 $state = self::stateFromVendorInfo($state, $vendorInfo, $vendorId, $now);
256 update_option(self::OPTION_NAME, $state, false);
257
258 return $state;
259 }
260
261 public function renderAdminNotice(): void
262 {
263 if ($this->isLocalEnvironment()) return;
264 if (!$this->isRelevantAdminScreen()) return;
265
266 $vendorSyncState = $this->getFrontendState();
267 if ($vendorSyncState['mode'] === self::MODE_ACTIVE) return;
268
269 $template = syncBasalamPlugin()->templatePath('notifications/VendorInactiveAlert.php');
270 if (is_readable($template)) require $template;
271 }
272
273 public static function isRefreshDue(array $state, int $vendorId, int $now): bool
274 {
275 if ((int) ($state['vendor_id'] ?? 0) !== $vendorId) return true;
276
277 $checkedAt = (int) ($state['checked_at'] ?? 0);
278
279 return $checkedAt <= 0
280 || $checkedAt > $now
281 || ($now - $checkedAt) >= self::CHECK_INTERVAL_SECONDS;
282 }
283
284 public static function resolveMode(array $state, int $now): string
285 {
286 if (array_key_exists('is_active', $state) && self::normalizeBoolean($state['is_active'])) {
287 return self::MODE_ACTIVE;
288 }
289
290 $inactiveSince = (int) ($state['inactive_since'] ?? 0);
291 if ($inactiveSince > 0 && ($now - $inactiveSince) >= self::SUSPEND_AFTER_SECONDS) {
292 return self::MODE_INACTIVE_SUSPENDED;
293 }
294
295 return self::MODE_INACTIVE_LIMITED;
296 }
297
298 public static function stateFromVendorInfo(
299 array $previousState,
300 array $vendorInfo,
301 int $vendorId,
302 int $now
303 ): array {
304 $statusCode = self::extractStatusCode($vendorInfo);
305 $statusName = self::extractStatusName($vendorInfo);
306 $isActive = self::extractIsActive($vendorInfo, $statusCode, $statusName);
307 $wasSameInactiveVendor = (int) ($previousState['vendor_id'] ?? 0) === $vendorId
308 && empty($previousState['is_active'])
309 && !empty($previousState['inactive_since']);
310
311 return [
312 'vendor_id' => $vendorId,
313 'is_active' => $isActive,
314 'status_code' => $statusCode,
315 'status_name' => $statusName,
316 'checked_at' => $now,
317 'inactive_since' => $isActive
318 ? null
319 : ($wasSameInactiveVendor ? (int) $previousState['inactive_since'] : $now),
320 'last_error_at' => null,
321 ];
322 }
323
324 private static function extractStatusCode(array $vendorInfo): ?int
325 {
326 $status = $vendorInfo['status'] ?? null;
327
328 if (is_array($status)) {
329 $status = $status['value'] ?? $status['id'] ?? null;
330 }
331
332 return is_numeric($status) ? (int) $status : null;
333 }
334
335 private static function extractStatusName(array $vendorInfo): string
336 {
337 $status = $vendorInfo['status'] ?? null;
338
339 if (is_array($status)) {
340 return trim((string) ($status['name'] ?? $status['title'] ?? ''));
341 }
342
343 return '';
344 }
345
346 private static function extractIsActive(array $vendorInfo, ?int $statusCode, string $statusName): bool
347 {
348 if ($statusCode !== null && in_array($statusCode, self::UNAVAILABLE_STATUS_CODES, true)) {
349 return false;
350 }
351
352 if (array_key_exists('is_active', $vendorInfo)) {
353 return self::normalizeBoolean($vendorInfo['is_active']);
354 }
355
356 $normalizedName = function_exists('mb_strtolower')
357 ? mb_strtolower($statusName, 'UTF-8')
358 : strtolower($statusName);
359
360 foreach (['غیرفعال', 'بسته', 'disabled', 'inactive', 'closed'] as $inactiveLabel) {
361 if ($normalizedName !== '' && strpos($normalizedName, $inactiveLabel) !== false) return false;
362 }
363
364 return true;
365 }
366
367 private static function normalizeBoolean($value): bool
368 {
369 if (is_bool($value)) return $value;
370 if (is_numeric($value)) return (int) $value === 1;
371
372 return in_array(strtolower(trim((string) $value)), ['1', 'true', 'yes', 'on'], true);
373 }
374
375 private function getStoredState(): array
376 {
377 $vendorId = (int) Settings::getSettings(SettingsConfig::VENDOR_ID);
378 $stored = get_option(self::OPTION_NAME, []);
379
380 return array_merge($this->defaultState($vendorId), is_array($stored) ? $stored : []);
381 }
382
383 private function defaultState(int $vendorId): array
384 {
385 return [
386 'vendor_id' => $vendorId,
387 'is_active' => true,
388 'status_code' => null,
389 'status_name' => '',
390 'checked_at' => 0,
391 'inactive_since' => null,
392 'last_error_at' => null,
393 ];
394 }
395
396 private function now(): int
397 {
398 return (int) call_user_func($this->clock);
399 }
400
401 private function isLocalEnvironment(): bool
402 {
403 return function_exists('wp_get_environment_type')
404 && wp_get_environment_type() === 'local';
405 }
406
407 private function isRelevantAdminScreen(): bool
408 {
409 if (method_exists(AdminRegistrar::class, 'isWoosalamRelatedAdminScreen')) {
410 return AdminRegistrar::isWoosalamRelatedAdminScreen();
411 }
412
413 $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : '';
414 if ($page !== '' && strpos($page, 'sync_basalam') === 0) return true;
415 if (in_array($page, ['basalam-onboarding', 'basalam-show-products'], true)) return true;
416
417 $screen = function_exists('get_current_screen') ? get_current_screen() : null;
418 $postType = $screen && isset($screen->post_type) ? (string) $screen->post_type : '';
419
420 return in_array($postType, ['product', 'shop_order'], true);
421 }
422 }
423