feat(pargar): cap inbound mande/document logs after insert

Keep the newest 2000 success rows per inbound type so dumps stop growing from Pargar webhooks, and store compact document payloads instead of full JSON.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-03 10:19:56 +03:30
co-authored by Cursor
parent c802c67e0d
commit 7e6cd92df4
6 changed files with 323 additions and 5 deletions
+102
View File
@@ -10,6 +10,7 @@ use App\Models\Tahesab;
use App\Models\Zarin;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
@@ -20,6 +21,9 @@ class DatabaseLogPruneService
/** هم‌راستا با حذف قطعی در PriceSnapshotService::pruneHistory */
public const RETENTION_DAYS = 31;
/** سقف ردیف موفق مانده/سند ورودی پرگار — بقیه (سفارش/کیف) را نمی‌بُرد */
public const PARAGAR_INBOUND_KEEP = 2000;
private const CHUNK = 500;
public function pruneHistory(?Carbon $now = null): array
@@ -29,6 +33,7 @@ class DatabaseLogPruneService
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(
@@ -137,4 +142,101 @@ class DatabaseLogPruneService
return ['deleted' => $deleted, 'skipped' => false];
}
/**
* فقط لاگ ورودی پرگار (mande/document). سفارش و سند کیف دست نخورده می‌ماند.
*
* @return array{mande: int, document: int}
*/
public function prunePargarInboundOverflow(int $maxDeletePerType = 0): array
{
if (! Schema::hasTable('pargar_sync_logs')) {
return ['mande' => 0, 'document' => 0];
}
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, $maxDeletePerType),
'document' => $this->pruneOldestSuccessBeyond(PargarSyncService::TYPE_DOCUMENT, $maxDeletePerType),
];
} 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) {
return (int) $query->limit($maxDelete)->delete();
}
return (int) $query->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(?, 0) 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) {
}
}
}