copy()->subDays(self::RETENTION_DAYS); return [ 'retention_days' => self::RETENTION_DAYS, 'cutoff' => $cutoff->toDateTimeString(), 'pargar_inbound' => $this->prunePargarInboundOverflow(), 'price_sync_logs' => $this->prunePriceSyncLogs($cutoff), 'logs' => $this->pruneGenericLogs($cutoff), 'pargar_sync_logs' => $this->pruneSuccessfulRows( PargarSyncLog::query(), 'pargar_sync_logs', PargarSyncService::STATUS_SUCCESS, $cutoff ), 'zarin' => $this->pruneSuccessfulRows( Zarin::query(), 'zarin', ZarinSyncService::STATUS_SUCCESS, $cutoff ), 'tahesab' => $this->pruneSuccessfulRows( Tahesab::query(), 'tahesab', TahesabSyncService::STATUS_SUCCESS, $cutoff ), ]; } /** @return array{deleted: int, items_deleted: int} */ private function prunePriceSyncLogs(Carbon $cutoff): array { if (! Schema::hasTable('price_sync_logs')) { return ['deleted' => 0, 'items_deleted' => 0]; } $deleted = 0; $itemsDeleted = 0; $hasItems = Schema::hasTable('price_sync_log_items'); PriceSyncLog::query() ->where('created_at', '<', $cutoff) ->orderBy('id') ->select(['id']) ->chunkById(self::CHUNK, function ($rows) use (&$deleted, &$itemsDeleted, $hasItems) { $ids = $rows->pluck('id')->all(); if ($ids === []) { return; } if ($hasItems) { $itemsDeleted += (int) PriceSyncLogItem::query()->whereIn('log_id', $ids)->delete(); } $deleted += (int) PriceSyncLog::query()->whereIn('id', $ids)->delete(); }); return ['deleted' => $deleted, 'items_deleted' => $itemsDeleted]; } /** @return array{deleted: int} */ private function pruneGenericLogs(Carbon $cutoff): array { if (! Schema::hasTable('logs')) { return ['deleted' => 0]; } $deleted = 0; Log::query() ->where('created_at', '<', $cutoff) ->orderBy('id') ->select(['id']) ->chunkById(self::CHUNK, function ($rows) use (&$deleted) { $ids = $rows->pluck('id')->all(); if ($ids === []) { return; } $deleted += (int) Log::query()->whereIn('id', $ids)->delete(); }); return ['deleted' => $deleted]; } /** * فقط ردیف‌های موفق حسابداری — pending/failed برای retry می‌مانند. * * @return array{deleted: int, skipped: bool} */ private function pruneSuccessfulRows(Builder $query, string $table, string $successStatus, Carbon $cutoff): array { if (! Schema::hasTable($table) || ! Schema::hasColumn($table, 'sync_status')) { return ['deleted' => 0, 'skipped' => true]; } if (! Schema::hasColumn($table, 'created_at')) { return ['deleted' => 0, 'skipped' => true]; } $deleted = 0; (clone $query) ->where('sync_status', $successStatus) ->where('created_at', '<', $cutoff) ->orderBy('id') ->select(['id']) ->chunkById(self::CHUNK, function ($rows) use (&$deleted, $query) { $ids = $rows->pluck('id')->all(); if ($ids === []) { return; } $deleted += (int) (clone $query)->whereIn('id', $ids)->delete(); }); return ['deleted' => $deleted, 'skipped' => false]; } /** * فقط لاگ ورودی پرگار (mande/document). سفارش و سند کیف دست نخورده می‌ماند. * * @return array{mande: int, document: int} */ /** هر بار حداکثر — برای afterResponse؛ تریگر MySQL هم LIMIT 4000 دارد */ private const PARAGAR_PRUNE_BATCH = 4000; public function prunePargarInboundOverflow(?int $maxDeletePerType = null): array { if (! Schema::hasTable('pargar_sync_logs')) { return ['mande' => 0, 'document' => 0]; } $batch = $maxDeletePerType ?? self::PARAGAR_PRUNE_BATCH; ignore_user_abort(true); if (function_exists('set_time_limit')) { @set_time_limit(180); } $this->ensureTypeStatusIndex(); $locked = $this->acquirePruneLock(); if ($locked === false) { return ['mande' => 0, 'document' => 0]; } try { return [ 'mande' => $this->pruneOldestSuccessBeyond(PargarSyncService::TYPE_MANDE, $batch), 'document' => $this->pruneOldestSuccessBeyond(PargarSyncService::TYPE_DOCUMENT, $batch), ]; } finally { if ($locked === true) { $this->releasePruneLock(); } } } private function pruneOldestSuccessBeyond(string $type, int $maxDelete): int { $cutoffId = PargarSyncLog::query() ->where('type', $type) ->where('sync_status', PargarSyncService::STATUS_SUCCESS) ->orderByDesc('id') ->skip(self::PARAGAR_INBOUND_KEEP) ->value('id'); if (! $cutoffId) { return 0; } $query = PargarSyncLog::query() ->where('type', $type) ->where('sync_status', PargarSyncService::STATUS_SUCCESS) ->where('id', '<=', $cutoffId) ->orderBy('id'); if ($maxDelete <= 0) { $maxDelete = self::PARAGAR_PRUNE_BATCH; } return (int) $query->limit($maxDelete)->delete(); } private function ensureTypeStatusIndex(): void { try { $names = collect(DB::select('SHOW INDEX FROM `pargar_sync_logs`')) ->pluck('Key_name') ->all(); if (in_array('pargar_sync_logs_type_status_index', $names, true)) { return; } DB::statement( 'ALTER TABLE `pargar_sync_logs` ADD INDEX `pargar_sync_logs_type_status_index` (`type`, `sync_status`)' ); } catch (\Throwable) { } } /** @return bool|null true=locked, false=busy, null=no lock support */ private function acquirePruneLock(): ?bool { try { $row = DB::selectOne('SELECT GET_LOCK(?, 3) AS l', ['khanegi_pargar_inbound_prune']); return (int) ($row->l ?? 0) === 1; } catch (\Throwable) { return null; } } private function releasePruneLock(): void { try { DB::selectOne('SELECT RELEASE_LOCK(?) AS l', ['khanegi_pargar_inbound_prune']); } catch (\Throwable) { } } }