fix transaction bug
This commit is contained in:
@@ -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<int> */
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user