option('order'); $customerId = (int) $this->option('customer'); $transactionId = (int) $this->option('transaction'); $goldOpt = $this->option('gold'); $goldForced = $goldOpt !== null && $goldOpt !== '' ? (float) $goldOpt : null; $rechargeRial = (int) round((float) $this->option('recharge-rial')); $skipRecharge = (bool) $this->option('skip-recharge'); $execute = (bool) $this->option('execute'); $resolved = $this->resolveTarget($orderId, $customerId, $transactionId, $goldForced); if ($resolved === null) { return self::FAILURE; } /** @var Customer $customer */ $customer = $resolved['customer']; /** @var Order|null $order */ $order = $resolved['order']; /** @var Collection $txs */ $txs = $resolved['txs']; $orphanOrderId = $resolved['orphan_order_id']; $label = $resolved['label']; $goldByWallet = []; foreach ($txs as $tx) { $wid = (int) $tx->wallet; $goldByWallet[$wid] = ($goldByWallet[$wid] ?? 0.0) + (float) $tx->gold; } $goldToStrip = []; foreach ($goldByWallet as $wid => $delta) { if ($delta > 1e-12) { $goldToStrip[$wid] = $delta; } } if ($goldToStrip === []) { $main = $this->mainWallet((int) $customer->id, (int) $customer->type); $fallbackGold = $goldForced ?? ($order ? (float) $order->amount : null) ?? 0.105; if ($main && $fallbackGold > 0) { $goldToStrip[(int) $main->id] = $fallbackGold; $this->warn("تراکنش طلای مثبت پیدا نشد — از مقدار {$fallbackGold} گرم روی کیف اصلی استفاده می‌شود."); } } if ($goldToStrip === []) { $this->error('مقداری برای کسر طلا مشخص نشد. --gold=0.105 بدهید.'); return self::FAILURE; } $refOrderId = $order?->id ?? $orphanOrderId; $adminRows = $refOrderId ? TransactionsAdmin::withTrashed()->where('order_id', $refOrderId)->get() : collect(); $zarinRows = ($refOrderId && Schema::hasTable('zarin')) ? Zarin::query()->where('order_id', $refOrderId)->get() : collect(); $tahesabRows = ($refOrderId && Schema::hasTable('tahesab')) ? Tahesab::query()->where('order_id', $refOrderId)->get() : collect(); // تراکنش‌های ادمین لینک‌شده به همین transaction idها $txIds = $txs->pluck('id')->all(); if ($txIds !== []) { $adminByTx = TransactionsAdmin::withTrashed() ->whereIn('transaction_id', $txIds) ->get(); $adminRows = $adminRows->merge($adminByTx)->unique('id'); } $payment = (float) ($order?->payment_amount ?: $order?->total_price ?: 0); if ($payment <= 0) { $cashTaken = 0.0; foreach ($txs as $tx) { if ((float) $tx->cash < 0) { $cashTaken += abs((float) $tx->cash); } } $payment = $cashTaken; } if ($rechargeRial <= 0) { $rechargeRial = (int) round($payment > 0 ? $payment : 100_000_000); } $this->info('=== پیش‌نمایش پاک‌کردن خرید اشتباه ==='); $this->line("مشتری: {$customer->id} — " . trim(($customer->fname ?? '') . ' ' . ($customer->lname ?? '') . ' ' . ($customer->name ?? ''))); $this->line('هدف: ' . $label); if ($order) { $this->line("سفارش موجود: #{$order->id} | amount={$order->amount} | price={$order->price} | payment={$payment}"); } elseif ($orphanOrderId) { $this->warn("سفارش #{$orphanOrderId} در جدول orders نیست (احتمالاً دستی پاک شده) — فقط اثر باقی‌مانده پاک می‌شود."); } else { $this->warn('بدون ردیف سفارش — فقط تراکنش/موجودی اصلاح می‌شود.'); } $this->line('تراکنش‌ها: ' . ($txIds ? implode(', ', $txIds) : '—')); foreach ($txs as $tx) { $this->line(" tx#{$tx->id} wallet={$tx->wallet} type={$tx->type} gold={$tx->gold} cash={$tx->cash} order_id=" . ($tx->order_id ?? 'null') . ' title=' . ($tx->title ?? '')); } $this->line('دفترکل محلی: ' . $adminRows->count()); $this->line('zarin محلی: ' . $zarinRows->count() . ' | tahesab محلی: ' . $tahesabRows->count()); $this->newLine(); $this->info('--- کسر طلا از کیف (بدون تراکنش برگشتی) ---'); foreach ($goldToStrip as $wid => $g) { $w = Wallet::find($wid); if (!$w) { $this->error("کیف #{$wid} یافت نشد."); return self::FAILURE; } $before = (float) $w->gold; $after = $before - $g; $this->line("wallet#{$wid} gold: {$before} → {$after} (−{$g})"); if ($after < -1e-6) { $this->error('بعد از کسر طلا منفی می‌شود — متوقف شد.'); return self::FAILURE; } } if (!$skipRecharge) { $main = $this->mainWallet((int) $customer->id, (int) $customer->type); $cashBefore = (float) ($main?->cash ?? 0); $this->newLine(); $this->info('--- شارژ مجدد ریالی ---'); $this->line('مبلغ: ' . number_format($rechargeRial) . ' ریال (= ' . number_format($rechargeRial / 10) . ' تومان)'); $this->line('کیف اصلی cash: ' . number_format($cashBefore) . ' → ' . number_format($cashBefore + $rechargeRial)); } $this->newLine(); $this->warn('Montzar / زرین راه دور لغو نمی‌شود.'); if (!$execute) { $this->newLine(); $this->warn('پیش‌نمایش — برای اجرا یکی از این‌ها:'); if ($order) { $this->line(" php artisan wallet:erase-bad-buy-recharge --order={$order->id} --execute"); } elseif ($txIds !== []) { $this->line(' php artisan wallet:erase-bad-buy-recharge --customer=' . $customer->id . ' --transaction=' . $txIds[0] . ' --execute'); } else { $g = reset($goldToStrip); $this->line(' php artisan wallet:erase-bad-buy-recharge --customer=' . $customer->id . ' --gold=' . $g . ' --execute'); } return self::SUCCESS; } $adminUserId = (int) ($this->option('user') ?: 0); if ($adminUserId <= 0) { $adminUserId = (int) (User::query()->orderBy('id')->value('id') ?: 0); } if (!$skipRecharge && $adminUserId <= 0) { $this->error('کاربر ادمین پیدا نشد؛ --user بدهید.'); return self::FAILURE; } $noteTag = $order?->id ?? $orphanOrderId ?? ($txIds[0] ?? 'manual'); DB::transaction(function () use ( $order, $customer, $txs, $goldToStrip, $adminRows, $zarinRows, $tahesabRows, $skipRecharge, $rechargeRial, $adminUserId, $noteTag, $refOrderId ) { foreach ($goldToStrip as $wid => $g) { $w = Wallet::query()->lockForUpdate()->find($wid); if (!$w) { throw new \RuntimeException("کیف #{$wid} یافت نشد"); } $w->gold = (float) $w->gold - $g; if ($w->gold < -1e-6) { throw new \RuntimeException("طلای کیف #{$wid} منفی شد"); } $w->save(); } $ids = $txs->pluck('id')->all(); if ($ids !== []) { if (Schema::hasTable('transaction_metas')) { TransactionMeta::query()->whereIn('transaction', $ids)->delete(); } Transaction::query()->whereIn('id', $ids)->delete(); } foreach ($adminRows as $row) { $row->forceDelete(); } foreach ($zarinRows as $row) { $row->delete(); } foreach ($tahesabRows as $row) { $row->delete(); } if ($refOrderId && Schema::hasColumn('transactions_admins', 'order_id')) { TransactionsAdmin::withTrashed()->where('order_id', $refOrderId)->forceDelete(); } if ($order) { Order::query()->where('id', $order->id)->delete(); } if (!$skipRecharge) { $walletInterface = new WalletInterface((int) $customer->id, (int) $customer->type); $withdraw = Withdraw::create([ 'user_id' => $adminUserId, 'customer_id' => $customer->id, 'cash' => $rechargeRial, 'note' => 'شارژ اصلاحی پس از حذف خرید اشتباه #' . $noteTag, 'sts' => 4, ]); new TransactionInterface($walletInterface, [ 'gold' => 0, 'cash' => $rechargeRial, 'expire_at' => now(), 'note' => $withdraw->note, 'withdraw_id' => $withdraw->id, 'active' => true, ], 3, true, true, 0); $tx = Transaction::query()->where('withdraw_id', $withdraw->id)->latest('id')->first(); TransactionsAdmin::create([ 'customer_id' => $customer->id, 'customer_to_id' => $customer->id, 'user_id' => $adminUserId, 'wallet' => null, 'transaction_id' => $tx?->id, 'cash' => $rechargeRial, 'note' => $withdraw->note, 'type' => 1, 'gold' => 0, 'balance_cash' => 0, 'withdraw_id' => $withdraw->id, ]); } }); try { app(WalletBroadcastService::class)->broadcastForCustomer( (int) $customer->id, (int) $customer->type ); } catch (\Throwable $e) { report($e); } $this->newLine(); $this->info('انجام شد' . ($skipRecharge ? '.' : ' — شارژ ریالی هم ثبت شد.')); $main = $this->mainWallet((int) $customer->id, (int) $customer->type); if ($main) { $this->line('موجودی کیف اصلی — cash=' . number_format((float) $main->cash) . ' | gold=' . round((float) $main->gold, 6)); } return self::SUCCESS; } /** * @return array{customer: Customer, order: ?Order, txs: Collection, orphan_order_id: ?int, label: string}|null */ private function resolveTarget(int $orderId, int $customerId, int $transactionId, ?float $goldForced): ?array { // ۱) سفارش هنوز هست if ($orderId > 0) { $order = Order::find($orderId); if (!$order) { $this->warn("سفارش #{$orderId} در orders نیست — سراغ تراکنش‌های order_id={$orderId} می‌رویم."); $txs = Transaction::query()->where('order_id', $orderId)->orderBy('id')->get(); if ($txs->isEmpty() && $customerId <= 0) { $this->error('تراکنشی با این order_id هم نیست. --customer و --gold یا --transaction بدهید.'); return null; } $customer = $customerId > 0 ? Customer::find($customerId) : Customer::find((int) ($txs->first()?->customer_id)); if (!$customer) { $this->error('مشتری مشخص نشد.'); return null; } return [ 'customer' => $customer, 'order' => null, 'txs' => $txs, 'orphan_order_id' => $orderId, 'label' => "سفارش حذف‌شده #{$orderId}", ]; } $customer = Customer::find($order->customer_id); if (!$customer) { $this->error('مشتری سفارش یافت نشد.'); return null; } return [ 'customer' => $customer, 'order' => $order, 'txs' => Transaction::query()->where('order_id', $order->id)->orderBy('id')->get(), 'orphan_order_id' => null, 'label' => "سفارش #{$order->id}", ]; } // ۲) تراکنش مشخص if ($transactionId > 0) { $tx = Transaction::find($transactionId); if (!$tx) { $this->error("تراکنش #{$transactionId} یافت نشد."); return null; } $customer = Customer::find((int) $tx->customer_id); if (!$customer) { $this->error('مشتری تراکنش یافت نشد.'); return null; } $txs = collect([$tx]); if ($tx->order_id) { $siblings = Transaction::query() ->where('order_id', $tx->order_id) ->where('id', '!=', $tx->id) ->get(); $txs = $txs->merge($siblings)->sortBy('id')->values(); } return [ 'customer' => $customer, 'order' => $tx->order_id ? Order::find($tx->order_id) : null, 'txs' => $txs, 'orphan_order_id' => $tx->order_id && ! Order::find($tx->order_id) ? (int) $tx->order_id : null, 'label' => "تراکنش #{$transactionId}", ]; } if ($customerId <= 0) { $this->error('--order یا --customer یا --transaction الزامی است.'); return null; } $customer = Customer::find($customerId); if (!$customer) { $this->error("مشتری {$customerId} یافت نشد."); return null; } // ۳) سفارش شبیه ۱۰ میلیون / ۰٫۱۰۵ $order = Order::query() ->where('customer_id', $customerId) ->where('direction', 1) ->where('sts', 2) ->whereBetween('amount', [0.09, 0.12]) ->where(function ($q) { $q->whereBetween('payment_amount', [90_000_000, 110_000_000]) ->orWhereBetween('total_price', [90_000_000, 110_000_000]); }) ->orderByDesc('id') ->first(); if ($order) { return [ 'customer' => $customer, 'order' => $order, 'txs' => Transaction::query()->where('order_id', $order->id)->orderBy('id')->get(), 'orphan_order_id' => null, 'label' => "سفارش #{$order->id}", ]; } // ۴) تراکنش یتیم: طلا ~۰٫۱۰۵ و/یا cash ~−۱۰۰ میلیون $orphanTxs = Transaction::query() ->where('customer_id', $customerId) ->where(function ($q) { $q->whereBetween('gold', [0.09, 0.12]) ->orWhereBetween('cash', [-110_000_000, -90_000_000]); }) ->orderByDesc('id') ->limit(20) ->get(); // گروه‌بندی با order_id (حتی اگر سفارش پاک شده) $byOrder = $orphanTxs->groupBy(fn ($t) => $t->order_id ?: ('tx:' . $t->id)); $best = null; $bestScore = -1; foreach ($byOrder as $group) { $g = (float) $group->sum('gold'); $c = (float) $group->sum(fn ($t) => min(0, (float) $t->cash)); $score = 0; if ($g >= 0.09 && $g <= 0.12) { $score += 2; } if ($c <= -90_000_000 && $c >= -110_000_000) { $score += 2; } if ($score > $bestScore) { $bestScore = $score; $best = $group; } } if ($best && $bestScore > 0) { $oid = $best->first()->order_id; return [ 'customer' => $customer, 'order' => $oid ? Order::find($oid) : null, 'txs' => $best->sortBy('id')->values(), 'orphan_order_id' => $oid && ! Order::find($oid) ? (int) $oid : null, 'label' => $oid ? "تراکنش‌های order_id={$oid} (سفارش حذف‌شده؟)" : 'تراکنش یتیم طلا/ریال', ]; } // ۵) فقط --gold: موجودی را کم کن، تراکنشی پاک نکن مگر پیدا شود if ($goldForced !== null && $goldForced > 0) { $this->warn("سفارش/تراکنش مشابه پیدا نشد — فقط --gold={$goldForced} روی کیف اعمال می‌شود."); return [ 'customer' => $customer, 'order' => null, 'txs' => collect(), 'orphan_order_id' => null, 'label' => "کسر دستی طلا {$goldForced} گرم", ]; } $this->error("برای مشتری {$customerId} نه سفارش مانده نه تراکنش شبیه ۱۰ میلیون / ۰٫۱۰۵ گرم."); $this->line('آخرین خریدهای orders:'); Order::query() ->where('customer_id', $customerId) ->where('direction', 1) ->orderByDesc('id') ->limit(8) ->get(['id', 'amount', 'price', 'payment_amount', 'total_price', 'created_at']) ->each(function (Order $o) { $this->line(" #{$o->id} amount={$o->amount} price={$o->price} pay={$o->payment_amount} at={$o->created_at}"); }); $this->line('آخرین تراکنش‌های طلای مثبت:'); Transaction::query() ->where('customer_id', $customerId) ->where('gold', '>', 0) ->orderByDesc('id') ->limit(10) ->get(['id', 'order_id', 'type', 'gold', 'cash', 'title', 'created_at']) ->each(function (Transaction $t) { $this->line(" tx#{$t->id} order_id={$t->order_id} type={$t->type} gold={$t->gold} cash={$t->cash} at={$t->created_at}"); }); $this->newLine(); $this->warn('اگر فقط طلا مانده و تراکنش را هم پاک کرده‌اید:'); $this->line(" php artisan wallet:erase-bad-buy-recharge --customer={$customerId} --gold=0.105 --execute"); return null; } private function mainWallet(int $customerId, int $ownerType): ?Wallet { return Wallet::query() ->where('owner', $customerId) ->where('owner_type', $ownerType) ->whereNull('fund_id') ->whereNull('product_id') ->first(); } }