Files
boursecu_server/app/Console/Commands/EraseBadBuyAndRechargeCommand.php
T
2026-08-29 14:04:13 +03:30

348 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Console\Commands;
use App\Models\Customer;
use App\Models\Order;
use App\Models\Tahesab;
use App\Models\TransactionsAdmin;
use App\Models\User;
use App\Models\Withdraw;
use App\Models\Zarin;
use App\Services\WalletBroadcastService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Rooyesh\Wallet\interfaces\TransactionInterface;
use Rooyesh\Wallet\interfaces\WalletInterface;
use Rooyesh\Wallet\models\Transaction;
use Rooyesh\Wallet\models\TransactionMeta;
use Rooyesh\Wallet\models\Wallet;
/**
* پاک‌کردن کامل یک خرید اشتباه (مثل ۱۰ میلیون → ۱۰۵ سوت) و شارژ مجدد ریالی.
*
* منطق:
* ۱) طلای همان سفارش از کیف کم می‌شود (بدون ثبت تراکنش برگشتی)
* ۲) سفارش + تراکنش‌ها + اسناد محلی مرتبط hard-delete می‌شوند
* ۳) مبلغ ریالی (--recharge-rial، پیش‌فرض ۱۰۰٬۰۰۰٬۰۰۰ = ۱۰ میلیون تومان) شارژ می‌شود
*
* توجه: سند همکار (Montzar) و سند راه دور زرین/ته‌حساب با این دستور لغو نمی‌شود.
*/
class EraseBadBuyAndRechargeCommand extends Command
{
protected $signature = 'wallet:erase-bad-buy-recharge
{--order= : شناسه سفارش خرید اشتباه}
{--customer= : شناسه مشتری (اگر order ندادید؛ نزدیک‌ترین خرید ۱۰ میلیون / ~۰٫۱۰۵ گرم را پیدا می‌کند)}
{--recharge-rial=100000000 : مبلغ شارژ مجدد به ریال (پیش‌فرض ۱۰ میلیون تومان)}
{--user= : شناسه ادمین برای ردیف withdraw شارژ (پیش‌فرض اولین کاربر)}
{--skip-recharge : فقط پاک‌کردن خرید؛ شارژ نکن}
{--execute : اجرای واقعی (بدون این فلگ فقط پیش‌نمایش)}';
protected $description = 'پاک‌کردن اثر خرید اشتباه و شارژ مجدد ریالی (بدون ردپای سفارش)';
public function handle(): int
{
$orderId = (int) $this->option('order');
$customerId = (int) $this->option('customer');
$rechargeRial = (int) round((float) $this->option('recharge-rial'));
$skipRecharge = (bool) $this->option('skip-recharge');
$execute = (bool) $this->option('execute');
$order = $this->resolveOrder($orderId, $customerId);
if (!$order) {
return self::FAILURE;
}
$customer = Customer::find($order->customer_id);
if (!$customer) {
$this->error("مشتری سفارش #{$order->id} یافت نشد.");
return self::FAILURE;
}
if ((int) $order->direction !== 1) {
$this->error("سفارش #{$order->id} خرید نیست (direction={$order->direction}).");
return self::FAILURE;
}
$txs = Transaction::query()
->where('order_id', $order->id)
->orderBy('id')
->get();
if ($txs->isEmpty()) {
$this->warn("هیچ تراکنشی با order_id={$order->id} پیدا نشد — فقط سفارش حذف می‌شود.");
}
$goldByWallet = [];
$cashByWallet = [];
foreach ($txs as $tx) {
$wid = (int) $tx->wallet;
$goldByWallet[$wid] = ($goldByWallet[$wid] ?? 0.0) + (float) $tx->gold;
$cashByWallet[$wid] = ($cashByWallet[$wid] ?? 0.0) + (float) $tx->cash;
}
// فقط طلای واریزشده را برمی‌داریم؛ ریال کسرشده را با شارژ جدا برمی‌گردانیم تا «شارژ ۱۰ میلیون» در تاریخچه بماند.
$goldToStrip = [];
foreach ($goldByWallet as $wid => $delta) {
if ($delta > 1e-12) {
$goldToStrip[$wid] = $delta;
}
}
if ($goldToStrip === [] && (float) $order->amount > 0) {
$main = $this->mainWallet((int) $customer->id, (int) $customer->type);
if ($main) {
$goldToStrip[(int) $main->id] = (float) $order->amount;
}
}
$adminRows = TransactionsAdmin::withTrashed()->where('order_id', $order->id)->get();
$zarinRows = Schema::hasTable('zarin')
? Zarin::query()->where('order_id', $order->id)->get()
: collect();
$tahesabRows = Schema::hasTable('tahesab')
? Tahesab::query()->where('order_id', $order->id)->get()
: collect();
$payment = (float) ($order->payment_amount ?: $order->total_price);
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("سفارش: #{$order->id} | amount={$order->amount} گرم | price={$order->price} | payment={$payment} | provider_id=" . ($order->provider_id ?? '—'));
$this->line('تراکنش‌های سفارش: ' . $txs->pluck('id')->implode(', '));
foreach ($txs as $tx) {
$this->line(" tx#{$tx->id} wallet={$tx->wallet} type={$tx->type} gold={$tx->gold} cash={$tx->cash} title=" . ($tx->title ?? ''));
}
$this->line('دفترکل محلی (transactions_admins): ' . $adminRows->count());
$this->line('اسناد zarin محلی: ' . $zarinRows->count());
$this->line('اسناد 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->warn('سند حسابداری راه دور برای این شارژ ساخته نمی‌شود (فقط کیف + withdraw + تراکنش).');
}
$this->newLine();
$this->warn('Montzar / زرین راه دور: این دستور سفارش همکار و سند آنلاین را لغو نمی‌کند.');
if (!$execute) {
$this->newLine();
$this->warn('حالت پیش‌نمایش — برای اجرا:');
$this->line(' php artisan wallet:erase-bad-buy-recharge --order=' . $order->id . ' --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('کاربر ادمین برای withdraw پیدا نشد؛ --user بدهید.');
return self::FAILURE;
}
DB::transaction(function () use (
$order,
$customer,
$txs,
$goldToStrip,
$adminRows,
$zarinRows,
$tahesabRows,
$skipRecharge,
$rechargeRial,
$adminUserId
) {
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();
}
$txIds = $txs->pluck('id')->all();
if ($txIds !== []) {
if (Schema::hasTable('transaction_metas')) {
TransactionMeta::query()->whereIn('transaction', $txIds)->delete();
}
Transaction::query()->whereIn('id', $txIds)->delete();
}
foreach ($adminRows as $row) {
$row->forceDelete();
}
foreach ($zarinRows as $row) {
$row->delete();
}
foreach ($tahesabRows as $row) {
$row->delete();
}
// پاداش معرفی / دفترکل وابسته به همین سفارش
if (Schema::hasColumn('transactions_admins', 'order_id')) {
TransactionsAdmin::withTrashed()->where('order_id', $order->id)->forceDelete();
}
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' => 'شارژ اصلاحی پس از حذف خرید اشتباه #' . $order->id,
'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;
}
private function resolveOrder(int $orderId, int $customerId): ?Order
{
if ($orderId > 0) {
$order = Order::find($orderId);
if (!$order) {
$this->error("سفارش #{$orderId} یافت نشد.");
return null;
}
return $order;
}
if ($customerId <= 0) {
$this->error('--order یا --customer الزامی است.');
return null;
}
// نزدیک به کیس ۱۰ میلیون تومان / ~۰٫۱۰۵ گرم
$candidates = 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')
->get();
if ($candidates->isEmpty()) {
$this->error("برای مشتری {$customerId} سفارشی شبیه ۱۰ میلیون / ~۰٫۱۰۵ گرم پیدا نشد. --order بدهید.");
$this->line('آخرین خریدها:');
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} total={$o->total_price} at={$o->created_at}");
});
return null;
}
if ($candidates->count() > 1) {
$this->warn('چند نامزد پیدا شد؛ اولی (جدیدتر) انتخاب می‌شود. در صورت نیاز --order بدهید:');
foreach ($candidates as $o) {
$this->line(" #{$o->id} amount={$o->amount} pay={$o->payment_amount} price={$o->price}");
}
}
return $candidates->first();
}
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();
}
}