fix transaction bug

This commit is contained in:
2026-08-27 14:01:34 +03:30
parent 17d79c0ce6
commit acf229006e
5 changed files with 353 additions and 15 deletions
@@ -0,0 +1,168 @@
<?php
namespace App\Console\Commands;
use App\Http\Controllers\Controller;
use App\Models\Customer;
use App\Models\Product;
use Illuminate\Console\Command;
use Rooyesh\Wallet\models\Transaction;
use Rooyesh\Wallet\models\Wallet;
/**
* پیدا کردن مشتریانی که با باگ intval روی کیف محصول وزنی، طلا از موجودی پریده
* ولی تراکنش‌ها سر جایشان مانده‌اند (الگوی فاطمه / مشتری ۵۳۵۵).
*
* فقط گزارش می‌دهد — موجودی را عوض نمی‌کند.
*/
class AuditIntvalGoldLossCommand extends Command
{
protected $signature = 'wallet:audit-intval-gold-loss
{--customer= : فقط یک مشتری}
{--min=0.001 : حداقل اختلاف گرم برای گزارش}
{--limit=200 : حداکثر ردیف خروجی}';
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');
$weightProductIds = $this->weightBasedProductIds();
if ($weightProductIds === []) {
$this->warn('هیچ محصول وزنی یافت نشد.');
return self::SUCCESS;
}
$walletQuery = Wallet::query()
->whereNull('fund_id')
->whereNotNull('product_id')
->whereIn('product_id', $weightProductIds)
->where('owner_type', 2);
if ($onlyCustomer > 0) {
$walletQuery->where('owner', $onlyCustomer);
}
$rows = [];
$walletQuery->orderBy('id')->chunkById(200, function ($wallets) use (&$rows, $min, $limit) {
foreach ($wallets as $wallet) {
if (count($rows) >= $limit) {
return false;
}
$report = $this->inspectWallet($wallet, $min);
if ($report !== null) {
$rows[] = $report;
}
}
return count($rows) < $limit;
});
if ($rows === []) {
$this->info('موردی با اختلاف معنادار پیدا نشد.');
return self::SUCCESS;
}
$this->table(
['customer_id', 'name', 'wallet_id', 'product_id', 'tx_net', 'wallet_gold', 'missing', 'matches_intval_bug'],
array_map(static fn (array $r) => [
$r['customer_id'],
$r['name'],
$r['wallet_id'],
$r['product_id'],
$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('این فقط گزارش است؛ برای برگرداندن موجودی باید جداگانه اصلاح شود.');
return self::SUCCESS;
}
/** @return list<int> */
private function weightBasedProductIds(): array
{
$ids = [];
Product::query()
->select(['id', 'unit', 'direct_price', 'group_type', 'amount', 'trade_by_quantity'])
->orderBy('id')
->chunkById(200, function ($products) use (&$ids) {
foreach ($products as $product) {
if (! Controller::isCountBasedProduct(Controller::productUnitPayload($product))) {
$ids[] = (int) $product->id;
}
}
});
return $ids;
}
private function inspectWallet(Wallet $wallet, float $min): ?array
{
$txs = Transaction::query()
->where('wallet', $wallet->id)
->whereNotNull('gold')
->where('gold', '!=', 0)
->orderBy('id')
->get(['id', 'gold', 'type', 'created_at']);
if ($txs->isEmpty()) {
return null;
}
$txNet = 0.0;
$clean = 0.0;
$buggy = 0.0;
foreach ($txs as $tx) {
$delta = (float) $tx->gold;
$txNet += $delta;
// الگوی سفارش: قبل از واریز طلای مثبت روی کیف محصول، intval می‌زد
if ($delta > 0) {
$buggy = (float) intval($buggy);
}
$buggy += $delta;
$clean += $delta;
}
$walletGold = (float) $wallet->gold;
$missing = round($txNet - $walletGold, 6);
if ($missing < $min) {
return null;
}
// اگر موجودی فعلی نزدیک مسیر باگ باشد، با اطمینان بالاتر «همان باگ» است
$matchesIntvalBug = abs($walletGold - $buggy) < $min
|| abs($walletGold - round($buggy, 3)) < $min;
$customer = Customer::query()->select(['id', 'name'])->find((int) $wallet->owner);
return [
'customer_id' => (int) $wallet->owner,
'name' => $customer?->name ?? '-',
'wallet_id' => (int) $wallet->id,
'product_id' => (int) $wallet->product_id,
'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),
];
}
}