PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.12
ووسلام – همگام سازی ووکامرس و باسلام v1.10.12
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.12, at includes/Services/VendorSyncPolicy.php

396 lines 12.7 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 (wp_next_scheduled(self::CRON_HOOK) !== false) return;
45
46 wp_schedule_event($this->now() + MINUTE_IN_SECONDS, 'daily', self::CRON_HOOK);
47 }
48
49 public static function unschedule(): void
50 {
51 wp_clear_scheduled_hook(self::CRON_HOOK);
52 }
53
54 public function getState(bool $refreshIfDue = true): array
55 {
56 if ($refreshIfDue) return $this->refreshIfDue();
57
58 return $this->getStoredState();
59 }
60
61 public function getMode(bool $refreshIfDue = true): string
62 {
63 return self::resolveMode($this->getState($refreshIfDue), $this->now());
64 }
65
66 public function canCreate(bool $refreshIfDue = true): bool
67 {
68 return $this->getMode($refreshIfDue) === self::MODE_ACTIVE;
69 }
70
71 public function canUpdate(bool $refreshIfDue = true): bool
72 {
73 return $this->getMode($refreshIfDue) !== self::MODE_INACTIVE_SUSPENDED;
74 }
75
76 public function canUpdateProductStatus(bool $refreshIfDue = true): bool
77 {
78 return $this->getMode($refreshIfDue) === self::MODE_ACTIVE;
79 }
80
81 public function shouldRestrictUpdateFields(bool $refreshIfDue = true): bool
82 {
83 return $this->getMode($refreshIfDue) === self::MODE_INACTIVE_LIMITED;
84 }
85
86 public function restrictUpdatePayload(array $productData, bool $refreshIfDue = true): array
87 {
88 return self::restrictUpdatePayloadForMode($productData, $this->getMode($refreshIfDue));
89 }
90
91 public static function restrictUpdatePayloadForMode(array $productData, string $mode): array
92 {
93 if ($mode === self::MODE_ACTIVE) return $productData;
94 if ($mode === self::MODE_INACTIVE_SUSPENDED) return [];
95
96 $allowed = [
97 'id' => true,
98 'type' => true,
99 'primary_price' => true,
100 'stock' => true,
101 'variants' => true,
102 ];
103
104 $restricted = array_intersect_key($productData, $allowed);
105
106 if (isset($restricted['variants']) && is_array($restricted['variants'])) {
107 $variantFields = [
108 'id' => true,
109 'primary_price' => true,
110 'stock' => true,
111 ];
112
113 $restricted['variants'] = array_map(function ($variant) use ($variantFields) {
114 return is_array($variant) ? array_intersect_key($variant, $variantFields) : [];
115 }, $restricted['variants']);
116
117 $restricted['variants'] = array_values(array_filter(
118 $restricted['variants'],
119 function ($variant) {
120 return !empty($variant['id']);
121 }
122 ));
123
124 if (empty($restricted['variants'])) unset($restricted['variants']);
125 }
126
127 return $restricted;
128 }
129
130 public function getRestrictionMessage(bool $refreshIfDue = true): string
131 {
132 $mode = $this->getMode($refreshIfDue);
133
134 if ($mode === self::MODE_INACTIVE_LIMITED) {
135 return 'غرفه باسلا�
136 ش�
137 ا غیرفعال است؛ ایجاد �
138 حصول جدید �
139 توقف شده و فقط قی�
140 ت و �
141 وجودی �
142 حصولات بروزرسانی �
143 ی‌شود.';
144 }
145
146 if ($mode === self::MODE_INACTIVE_SUSPENDED) {
147 return 'غرفه باسلا�
148 ش�
149 ا بیش از ۳ هفته غیرفعال بوده است؛ ایجاد و بروزرسانی �
150 حصولات تا فعال‌شدن �
151 جدد غرفه �
152 توقف است.';
153 }
154
155 return '';
156 }
157
158 public function getFrontendState(bool $refreshIfDue = true): array
159 {
160 $state = $this->getState($refreshIfDue);
161 $mode = self::resolveMode($state, $this->now());
162 $inactiveSince = isset($state['inactive_since']) ? (int) $state['inactive_since'] : 0;
163 $inactiveSeconds = $inactiveSince > 0 ? max(0, $this->now() - $inactiveSince) : 0;
164
165 return array_merge($state, [
166 'mode' => $mode,
167 'can_create' => $mode === self::MODE_ACTIVE,
168 'can_update' => $mode !== self::MODE_INACTIVE_SUSPENDED,
169 'update_fields' => $mode === self::MODE_INACTIVE_LIMITED
170 ? ['price', 'stock']
171 : ($mode === self::MODE_ACTIVE ? ['all'] : []),
172 'inactive_days' => (int) floor($inactiveSeconds / DAY_IN_SECONDS),
173 'message' => $this->getRestrictionMessage(false),
174 ]);
175 }
176
177 public function refreshIfDue(): array
178 {
179 $vendorId = (int) Settings::getSettings(SettingsConfig::VENDOR_ID);
180 $token = Settings::getSettings(SettingsConfig::TOKEN);
181 $state = $this->getStoredState();
182 $now = $this->now();
183
184 if (!$vendorId || !$token) {
185 $state = $this->defaultState($vendorId);
186 update_option(self::OPTION_NAME, $state, false);
187
188 return $state;
189 }
190
191 if (!self::isRefreshDue($state, $vendorId, $now)) return $state;
192 if (get_transient(self::REFRESH_LOCK)) {
193 return (int) ($state['vendor_id'] ?? 0) === $vendorId
194 ? $state
195 : $this->defaultState($vendorId);
196 }
197
198 set_transient(self::REFRESH_LOCK, 1, self::REFRESH_LOCK_SECONDS);
199
200 try {
201 return $this->refresh();
202 } finally {
203 delete_transient(self::REFRESH_LOCK);
204 }
205 }
206
207 public function refresh(): array
208 {
209 $vendorId = (int) Settings::getSettings(SettingsConfig::VENDOR_ID);
210 $token = Settings::getSettings(SettingsConfig::TOKEN);
211 $now = $this->now();
212 $state = $this->getStoredState();
213
214 if (!$vendorId || !$token) {
215 $state = $this->defaultState($vendorId);
216 update_option(self::OPTION_NAME, $state, false);
217
218 return $state;
219 }
220
221 if ((int) ($state['vendor_id'] ?? 0) !== $vendorId) {
222 $state = $this->defaultState($vendorId);
223 }
224
225 $vendorInfo = $this->vendorInfoService->FetchVendorInfo();
226
227 if (!is_array($vendorInfo)) {
228 $state['checked_at'] = $now;
229 $state['last_error_at'] = $now;
230 update_option(self::OPTION_NAME, $state, false);
231
232 return $state;
233 }
234
235 $state = self::stateFromVendorInfo($state, $vendorInfo, $vendorId, $now);
236 update_option(self::OPTION_NAME, $state, false);
237
238 return $state;
239 }
240
241 public function renderAdminNotice(): void
242 {
243 if (!$this->isRelevantAdminScreen()) return;
244
245 $vendorSyncState = $this->getFrontendState();
246 if ($vendorSyncState['mode'] === self::MODE_ACTIVE) return;
247
248 $template = syncBasalamPlugin()->templatePath('notifications/VendorInactiveAlert.php');
249 if (is_readable($template)) require $template;
250 }
251
252 public static function isRefreshDue(array $state, int $vendorId, int $now): bool
253 {
254 if ((int) ($state['vendor_id'] ?? 0) !== $vendorId) return true;
255
256 $checkedAt = (int) ($state['checked_at'] ?? 0);
257
258 return $checkedAt <= 0
259 || $checkedAt > $now
260 || ($now - $checkedAt) >= self::CHECK_INTERVAL_SECONDS;
261 }
262
263 public static function resolveMode(array $state, int $now): string
264 {
265 if (array_key_exists('is_active', $state) && self::normalizeBoolean($state['is_active'])) {
266 return self::MODE_ACTIVE;
267 }
268
269 $inactiveSince = (int) ($state['inactive_since'] ?? 0);
270 if ($inactiveSince > 0 && ($now - $inactiveSince) >= self::SUSPEND_AFTER_SECONDS) {
271 return self::MODE_INACTIVE_SUSPENDED;
272 }
273
274 return self::MODE_INACTIVE_LIMITED;
275 }
276
277 public static function stateFromVendorInfo(
278 array $previousState,
279 array $vendorInfo,
280 int $vendorId,
281 int $now
282 ): array {
283 $statusCode = self::extractStatusCode($vendorInfo);
284 $statusName = self::extractStatusName($vendorInfo);
285 $isActive = self::extractIsActive($vendorInfo, $statusCode, $statusName);
286 $wasSameInactiveVendor = (int) ($previousState['vendor_id'] ?? 0) === $vendorId
287 && empty($previousState['is_active'])
288 && !empty($previousState['inactive_since']);
289
290 return [
291 'vendor_id' => $vendorId,
292 'is_active' => $isActive,
293 'status_code' => $statusCode,
294 'status_name' => $statusName,
295 'checked_at' => $now,
296 'inactive_since' => $isActive
297 ? null
298 : ($wasSameInactiveVendor ? (int) $previousState['inactive_since'] : $now),
299 'last_error_at' => null,
300 ];
301 }
302
303 private static function extractStatusCode(array $vendorInfo): ?int
304 {
305 $status = $vendorInfo['status'] ?? null;
306
307 if (is_array($status)) {
308 $status = $status['value'] ?? $status['id'] ?? null;
309 }
310
311 return is_numeric($status) ? (int) $status : null;
312 }
313
314 private static function extractStatusName(array $vendorInfo): string
315 {
316 $status = $vendorInfo['status'] ?? null;
317
318 if (is_array($status)) {
319 return trim((string) ($status['name'] ?? $status['title'] ?? ''));
320 }
321
322 return '';
323 }
324
325 private static function extractIsActive(array $vendorInfo, ?int $statusCode, string $statusName): bool
326 {
327 if ($statusCode !== null && in_array($statusCode, self::UNAVAILABLE_STATUS_CODES, true)) {
328 return false;
329 }
330
331 if (array_key_exists('is_active', $vendorInfo)) {
332 return self::normalizeBoolean($vendorInfo['is_active']);
333 }
334
335 $normalizedName = function_exists('mb_strtolower')
336 ? mb_strtolower($statusName, 'UTF-8')
337 : strtolower($statusName);
338
339 foreach (['غیرفعال', 'بسته', 'disabled', 'inactive', 'closed'] as $inactiveLabel) {
340 if ($normalizedName !== '' && strpos($normalizedName, $inactiveLabel) !== false) return false;
341 }
342
343 return true;
344 }
345
346 private static function normalizeBoolean($value): bool
347 {
348 if (is_bool($value)) return $value;
349 if (is_numeric($value)) return (int) $value === 1;
350
351 return in_array(strtolower(trim((string) $value)), ['1', 'true', 'yes', 'on'], true);
352 }
353
354 private function getStoredState(): array
355 {
356 $vendorId = (int) Settings::getSettings(SettingsConfig::VENDOR_ID);
357 $stored = get_option(self::OPTION_NAME, []);
358
359 return array_merge($this->defaultState($vendorId), is_array($stored) ? $stored : []);
360 }
361
362 private function defaultState(int $vendorId): array
363 {
364 return [
365 'vendor_id' => $vendorId,
366 'is_active' => true,
367 'status_code' => null,
368 'status_name' => '',
369 'checked_at' => 0,
370 'inactive_since' => null,
371 'last_error_at' => null,
372 ];
373 }
374
375 private function now(): int
376 {
377 return (int) call_user_func($this->clock);
378 }
379
380 private function isRelevantAdminScreen(): bool
381 {
382 if (method_exists(AdminRegistrar::class, 'isWoosalamRelatedAdminScreen')) {
383 return AdminRegistrar::isWoosalamRelatedAdminScreen();
384 }
385
386 $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : '';
387 if ($page !== '' && strpos($page, 'sync_basalam') === 0) return true;
388 if (in_array($page, ['basalam-onboarding', 'basalam-show-products'], true)) return true;
389
390 $screen = function_exists('get_current_screen') ? get_current_screen() : null;
391 $postType = $screen && isset($screen->post_type) ? (string) $screen->post_type : '';
392
393 return in_array($postType, ['product', 'shop_order'], true);
394 }
395 }
396