fix transaction bug
This commit is contained in:
@@ -6,29 +6,37 @@ use App\Http\Controllers\Controller;
|
|||||||
use App\Models\Customer;
|
use App\Models\Customer;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use Illuminate\Console\Command;
|
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\Transaction;
|
||||||
use Rooyesh\Wallet\models\Wallet;
|
use Rooyesh\Wallet\models\Wallet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* پیدا کردن مشتریانی که با باگ intval روی کیف محصول وزنی، طلا از موجودی پریده
|
* پیدا کردن / اصلاح مشتریانی که با باگ intval روی کیف محصول وزنی طلا از موجودی پریده.
|
||||||
* ولی تراکنشها سر جایشان ماندهاند (الگوی فاطمه / مشتری ۵۳۵۵).
|
|
||||||
*
|
*
|
||||||
* فقط گزارش میدهد — موجودی را عوض نمیکند.
|
* --fix : همان مقدار missing را روی همان کیف برمیگرداند (تراکنش داخلی، بدون سند حسابداری بیرونی).
|
||||||
|
* تراکنش اصلاح با note=fix:intval-gold-loss از محاسبات اسکن بعدی حذف میشود تا دوباره در لیست نیاید.
|
||||||
*/
|
*/
|
||||||
class AuditIntvalGoldLossCommand extends Command
|
class AuditIntvalGoldLossCommand extends Command
|
||||||
{
|
{
|
||||||
|
public const FIX_NOTE = 'fix:intval-gold-loss';
|
||||||
|
|
||||||
protected $signature = 'wallet:audit-intval-gold-loss
|
protected $signature = 'wallet:audit-intval-gold-loss
|
||||||
{--customer= : فقط یک مشتری}
|
{--customer= : فقط یک مشتری}
|
||||||
{--min=0.001 : حداقل اختلاف گرم برای گزارش}
|
{--min=0.001 : حداقل اختلاف گرم برای گزارش}
|
||||||
{--limit=200 : حداکثر ردیف خروجی}';
|
{--limit=200 : حداکثر ردیف خروجی}
|
||||||
|
{--fix : اصلاح موجودی برای ردیفهای تأییدشدهٔ باگ intval}
|
||||||
|
{--force : با --fix بدون تأیید تعاملی}';
|
||||||
|
|
||||||
protected $description = 'گزارش کیفهای وزنی که موجودیشان با جمع تراکنشها نمیخواند (احتمال باگ intval)';
|
protected $description = 'گزارش/اصلاح کیفهای وزنی آسیبدیده از باگ intval (بدون سند حسابداری بیرونی)';
|
||||||
|
|
||||||
public function handle(): int
|
public function handle(): int
|
||||||
{
|
{
|
||||||
$min = max(0.000001, (float) $this->option('min'));
|
$min = max(0.000001, (float) $this->option('min'));
|
||||||
$limit = max(1, (int) $this->option('limit'));
|
$limit = max(1, (int) $this->option('limit'));
|
||||||
$onlyCustomer = (int) $this->option('customer');
|
$onlyCustomer = (int) $this->option('customer');
|
||||||
|
$doFix = (bool) $this->option('fix');
|
||||||
|
|
||||||
$weightProductIds = $this->weightBasedProductIds();
|
$weightProductIds = $this->weightBasedProductIds();
|
||||||
if ($weightProductIds === []) {
|
if ($weightProductIds === []) {
|
||||||
@@ -64,13 +72,13 @@ class AuditIntvalGoldLossCommand extends Command
|
|||||||
});
|
});
|
||||||
|
|
||||||
if ($rows === []) {
|
if ($rows === []) {
|
||||||
$this->info('موردی با اختلاف معنادار پیدا نشد.');
|
$this->info('موردی با الگوی باگ intval پیدا نشد.');
|
||||||
|
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->table(
|
$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) => [
|
array_map(static fn (array $r) => [
|
||||||
$r['customer_id'],
|
$r['customer_id'],
|
||||||
$r['name'],
|
$r['name'],
|
||||||
@@ -79,17 +87,77 @@ class AuditIntvalGoldLossCommand extends Command
|
|||||||
$r['tx_net'],
|
$r['tx_net'],
|
||||||
$r['wallet_gold'],
|
$r['wallet_gold'],
|
||||||
$r['missing'],
|
$r['missing'],
|
||||||
$r['matches_intval_bug'] ? 'yes' : 'maybe',
|
|
||||||
], $rows)
|
], $rows)
|
||||||
);
|
);
|
||||||
|
|
||||||
$totalMissing = round(array_sum(array_column($rows, 'missing')), 3);
|
$totalMissing = round(array_sum(array_column($rows, 'missing')), 3);
|
||||||
$this->newLine();
|
$this->newLine();
|
||||||
$this->warn('تعداد کیف مشکوک: '.count($rows));
|
$this->warn('تعداد کیف: '.count($rows));
|
||||||
$this->warn("جمع طلای کمشده (تقریبی): {$totalMissing} گرم");
|
$this->warn("جمع طلای کمشده: {$totalMissing} گرم");
|
||||||
$this->line('این فقط گزارش است؛ برای برگرداندن موجودی باید جداگانه اصلاح شود.');
|
|
||||||
|
|
||||||
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> */
|
/** @return list<int> */
|
||||||
@@ -117,18 +185,23 @@ class AuditIntvalGoldLossCommand extends Command
|
|||||||
->whereNotNull('gold')
|
->whereNotNull('gold')
|
||||||
->where('gold', '!=', 0)
|
->where('gold', '!=', 0)
|
||||||
->orderBy('id')
|
->orderBy('id')
|
||||||
->get(['id', 'gold', 'type', 'created_at']);
|
->get(['id', 'gold', 'type', 'note', 'created_at']);
|
||||||
|
|
||||||
if ($txs->isEmpty()) {
|
if ($txs->isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$txNet = 0.0;
|
$txNet = 0.0;
|
||||||
$clean = 0.0;
|
|
||||||
$buggy = 0.0;
|
$buggy = 0.0;
|
||||||
|
$hasNonFixGoldTx = false;
|
||||||
|
|
||||||
foreach ($txs as $tx) {
|
foreach ($txs as $tx) {
|
||||||
|
if ($this->isFixTransaction($tx)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$delta = (float) $tx->gold;
|
$delta = (float) $tx->gold;
|
||||||
|
$hasNonFixGoldTx = true;
|
||||||
$txNet += $delta;
|
$txNet += $delta;
|
||||||
|
|
||||||
// الگوی سفارش: قبل از واریز طلای مثبت روی کیف محصول، intval میزد
|
// الگوی سفارش: قبل از واریز طلای مثبت روی کیف محصول، intval میزد
|
||||||
@@ -136,20 +209,29 @@ class AuditIntvalGoldLossCommand extends Command
|
|||||||
$buggy = (float) intval($buggy);
|
$buggy = (float) intval($buggy);
|
||||||
}
|
}
|
||||||
$buggy += $delta;
|
$buggy += $delta;
|
||||||
$clean += $delta;
|
}
|
||||||
|
|
||||||
|
if (! $hasNonFixGoldTx) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$walletGold = (float) $wallet->gold;
|
$walletGold = (float) $wallet->gold;
|
||||||
|
// موجودی فعلی منهای اثر تراکنشهای fix (اگر قبلاً خورده) در خود wallet است؛
|
||||||
|
// tx_net بدون fix است → missing باید صفر شود وقتی wallet = tx_net
|
||||||
$missing = round($txNet - $walletGold, 6);
|
$missing = round($txNet - $walletGold, 6);
|
||||||
|
|
||||||
if ($missing < $min) {
|
if ($missing < $min) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// اگر موجودی فعلی نزدیک مسیر باگ باشد، با اطمینان بالاتر «همان باگ» است
|
// فقط وقتی موجودی فعلی با مسیر باگ جور است گزارش/فیکس کن
|
||||||
$matchesIntvalBug = abs($walletGold - $buggy) < $min
|
$matchesIntvalBug = abs($walletGold - $buggy) < $min
|
||||||
|| abs($walletGold - round($buggy, 3)) < $min;
|
|| abs($walletGold - round($buggy, 3)) < $min;
|
||||||
|
|
||||||
|
if (! $matchesIntvalBug) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$customer = Customer::query()->select(['id', 'name'])->find((int) $wallet->owner);
|
$customer = Customer::query()->select(['id', 'name'])->find((int) $wallet->owner);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -160,9 +242,14 @@ class AuditIntvalGoldLossCommand extends Command
|
|||||||
'tx_net' => round($txNet, 3),
|
'tx_net' => round($txNet, 3),
|
||||||
'wallet_gold' => round($walletGold, 3),
|
'wallet_gold' => round($walletGold, 3),
|
||||||
'missing' => round($missing, 3),
|
'missing' => round($missing, 3),
|
||||||
'matches_intval_bug' => $matchesIntvalBug,
|
|
||||||
'clean_replay' => round($clean, 3),
|
|
||||||
'buggy_replay' => round($buggy, 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