From 2c31595b5f9031b4e0e7e9da1add41c699746c8f Mon Sep 17 00:00:00 2001 From: adonis Date: Thu, 27 Aug 2026 14:20:19 +0330 Subject: [PATCH] fix transaction bug --- .../Commands/AuditIntvalGoldLossCommand.php | 123 +++++++++++++++--- 1 file changed, 105 insertions(+), 18 deletions(-) diff --git a/app/Console/Commands/AuditIntvalGoldLossCommand.php b/app/Console/Commands/AuditIntvalGoldLossCommand.php index 7188be1..568ed5a 100644 --- a/app/Console/Commands/AuditIntvalGoldLossCommand.php +++ b/app/Console/Commands/AuditIntvalGoldLossCommand.php @@ -6,29 +6,37 @@ use App\Http\Controllers\Controller; use App\Models\Customer; use App\Models\Product; use Illuminate\Console\Command; +use Illuminate\Support\Facades\DB; +use Rooyesh\Wallet\interfaces\TransactionInterface; +use Rooyesh\Wallet\interfaces\WalletInterface; use Rooyesh\Wallet\models\Transaction; use Rooyesh\Wallet\models\Wallet; /** - * پیدا کردن مشتریانی که با باگ intval روی کیف محصول وزنی، طلا از موجودی پریده - * ولی تراکنش‌ها سر جایشان مانده‌اند (الگوی فاطمه / مشتری ۵۳۵۵). + * پیدا کردن / اصلاح مشتریانی که با باگ intval روی کیف محصول وزنی طلا از موجودی پریده. * - * فقط گزارش می‌دهد — موجودی را عوض نمی‌کند. + * --fix : همان مقدار missing را روی همان کیف برمی‌گرداند (تراکنش داخلی، بدون سند حسابداری بیرونی). + * تراکنش اصلاح با note=fix:intval-gold-loss از محاسبات اسکن بعدی حذف می‌شود تا دوباره در لیست نیاید. */ class AuditIntvalGoldLossCommand extends Command { + public const FIX_NOTE = 'fix:intval-gold-loss'; + protected $signature = 'wallet:audit-intval-gold-loss {--customer= : فقط یک مشتری} {--min=0.001 : حداقل اختلاف گرم برای گزارش} - {--limit=200 : حداکثر ردیف خروجی}'; + {--limit=200 : حداکثر ردیف خروجی} + {--fix : اصلاح موجودی برای ردیف‌های تأییدشدهٔ باگ intval} + {--force : با --fix بدون تأیید تعاملی}'; - protected $description = 'گزارش کیف‌های وزنی که موجودی‌شان با جمع تراکنش‌ها نمی‌خواند (احتمال باگ intval)'; + protected $description = 'گزارش/اصلاح کیف‌های وزنی آسیب‌دیده از باگ intval (بدون سند حسابداری بیرونی)'; public function handle(): int { $min = max(0.000001, (float) $this->option('min')); $limit = max(1, (int) $this->option('limit')); $onlyCustomer = (int) $this->option('customer'); + $doFix = (bool) $this->option('fix'); $weightProductIds = $this->weightBasedProductIds(); if ($weightProductIds === []) { @@ -64,13 +72,13 @@ class AuditIntvalGoldLossCommand extends Command }); if ($rows === []) { - $this->info('موردی با اختلاف معنادار پیدا نشد.'); + $this->info('موردی با الگوی باگ intval پیدا نشد.'); return self::SUCCESS; } $this->table( - ['customer_id', 'name', 'wallet_id', 'product_id', 'tx_net', 'wallet_gold', 'missing', 'matches_intval_bug'], + ['customer_id', 'name', 'wallet_id', 'product_id', 'tx_net', 'wallet_gold', 'missing'], array_map(static fn (array $r) => [ $r['customer_id'], $r['name'], @@ -79,17 +87,77 @@ class AuditIntvalGoldLossCommand extends Command $r['tx_net'], $r['wallet_gold'], $r['missing'], - $r['matches_intval_bug'] ? 'yes' : 'maybe', ], $rows) ); $totalMissing = round(array_sum(array_column($rows, 'missing')), 3); $this->newLine(); - $this->warn('تعداد کیف مشکوک: '.count($rows)); - $this->warn("جمع طلای کم‌شده (تقریبی): {$totalMissing} گرم"); - $this->line('این فقط گزارش است؛ برای برگرداندن موجودی باید جداگانه اصلاح شود.'); + $this->warn('تعداد کیف: '.count($rows)); + $this->warn("جمع طلای کم‌شده: {$totalMissing} گرم"); - return self::SUCCESS; + if (! $doFix) { + $this->line('برای اصلاح (بدون سند حسابداری): php artisan wallet:audit-intval-gold-loss --fix'); + + return self::SUCCESS; + } + + if (! $this->option('force') && ! $this->confirm("موجودی {$totalMissing} گرم برای ".count($rows).' کیف اصلاح شود؟')) { + $this->warn('لغو شد.'); + + return self::SUCCESS; + } + + $fixed = 0; + $failed = 0; + foreach ($rows as $row) { + try { + DB::transaction(function () use ($row) { + $this->applyFix($row); + }); + $fixed++; + $this->info("OK customer={$row['customer_id']} wallet={$row['wallet_id']} +{$row['missing']}g"); + } catch (\Throwable $e) { + $failed++; + $this->error("FAIL customer={$row['customer_id']} wallet={$row['wallet_id']}: ".$e->getMessage()); + report($e); + } + } + + $this->newLine(); + $this->info("اصلاح‌شده: {$fixed} — ناموفق: {$failed}"); + $this->line('اسکن دوباره نباید همین ردیف‌ها را بیاورد (تراکنش fix از محاسبه خارج است).'); + + return $failed > 0 ? self::FAILURE : self::SUCCESS; + } + + /** @param array{customer_id:int,wallet_id:int,product_id:int,missing:float} $row */ + private function applyFix(array $row): void + { + $missing = (float) $row['missing']; + if ($missing < 0.000001) { + return; + } + + $customer = Customer::query()->findOrFail($row['customer_id']); + $ownerType = (int) $customer->type; + $productId = (int) $row['product_id']; + + $walletInterface = new WalletInterface( + (int) $customer->id, + $ownerType, + null, + $productId + ); + + // فقط تراکنش داخلی کیف — بدون Withdraw و بدون push حسابداری + new TransactionInterface($walletInterface, [ + 'cash' => 0, + 'gold' => $missing, + 'product_id' => $productId, + 'expire_at' => now(), + 'note' => self::FIX_NOTE, + 'active' => true, + ], 7, true, false); } /** @return list */ @@ -117,18 +185,23 @@ class AuditIntvalGoldLossCommand extends Command ->whereNotNull('gold') ->where('gold', '!=', 0) ->orderBy('id') - ->get(['id', 'gold', 'type', 'created_at']); + ->get(['id', 'gold', 'type', 'note', 'created_at']); if ($txs->isEmpty()) { return null; } $txNet = 0.0; - $clean = 0.0; $buggy = 0.0; + $hasNonFixGoldTx = false; foreach ($txs as $tx) { + if ($this->isFixTransaction($tx)) { + continue; + } + $delta = (float) $tx->gold; + $hasNonFixGoldTx = true; $txNet += $delta; // الگوی سفارش: قبل از واریز طلای مثبت روی کیف محصول، intval می‌زد @@ -136,20 +209,29 @@ class AuditIntvalGoldLossCommand extends Command $buggy = (float) intval($buggy); } $buggy += $delta; - $clean += $delta; + } + + if (! $hasNonFixGoldTx) { + return null; } $walletGold = (float) $wallet->gold; + // موجودی فعلی منهای اثر تراکنش‌های fix (اگر قبلاً خورده) در خود wallet است؛ + // tx_net بدون fix است → missing باید صفر شود وقتی wallet = tx_net $missing = round($txNet - $walletGold, 6); if ($missing < $min) { return null; } - // اگر موجودی فعلی نزدیک مسیر باگ باشد، با اطمینان بالاتر «همان باگ» است + // فقط وقتی موجودی فعلی با مسیر باگ جور است گزارش/فیکس کن $matchesIntvalBug = abs($walletGold - $buggy) < $min || abs($walletGold - round($buggy, 3)) < $min; + if (! $matchesIntvalBug) { + return null; + } + $customer = Customer::query()->select(['id', 'name'])->find((int) $wallet->owner); return [ @@ -160,9 +242,14 @@ class AuditIntvalGoldLossCommand extends Command 'tx_net' => round($txNet, 3), 'wallet_gold' => round($walletGold, 3), 'missing' => round($missing, 3), - 'matches_intval_bug' => $matchesIntvalBug, - 'clean_replay' => round($clean, 3), 'buggy_replay' => round($buggy, 3), ]; } + + private function isFixTransaction(Transaction $tx): bool + { + $note = trim((string) ($tx->note ?? '')); + + return $note === self::FIX_NOTE || str_starts_with($note, self::FIX_NOTE); + } }