Files
boursecu_server/app/Console/Commands/EraseBadBuyAndRechargeCommand.php
T

528 lines
21 KiB
PHP
Raw Normal View History

2026-08-29 14:04:13 +03:30
<?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\Collection;
2026-08-29 14:04:13 +03:30
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;
/**
* پاک‌کردن اثر خرید اشتباه و شارژ مجدد ریالی.
2026-08-29 14:04:13 +03:30
*
* اگر ردیف orders دستی پاک شده باشد هم کار می‌کند:
* تراکنش‌های باقی‌مانده (یا --gold/--transaction) را پیدا می‌کند.
2026-08-29 14:04:13 +03:30
*/
class EraseBadBuyAndRechargeCommand extends Command
{
protected $signature = 'wallet:erase-bad-buy-recharge
{--order= : شناسه سفارش (اگر هنوز هست)}
{--customer= : شناسه مشتری — برای پیدا کردن سفارش یا تراکنش یتیم}
{--transaction= : شناسه تراکنش خرید (وقتی سفارش پاک شده)}
{--gold= : گرم طلایی که باید از کیف کم شود (مثلاً 0.105) اگر تراکنش هم نیست}
2026-08-29 14:04:13 +03:30
{--recharge-rial=100000000 : مبلغ شارژ مجدد به ریال (پیش‌فرض ۱۰ میلیون تومان)}
{--user= : شناسه ادمین برای withdraw شارژ}
{--skip-recharge : فقط پاک‌کردن؛ شارژ نکن}
{--execute : اجرای واقعی}';
2026-08-29 14:04:13 +03:30
protected $description = 'پاک‌کردن اثر خرید اشتباه و شارژ مجدد ریالی (حتی اگر orders حذف شده باشد)';
2026-08-29 14:04:13 +03:30
public function handle(): int
{
$orderId = (int) $this->option('order');
$customerId = (int) $this->option('customer');
$transactionId = (int) $this->option('transaction');
$goldOpt = $this->option('gold');
$goldForced = $goldOpt !== null && $goldOpt !== '' ? (float) $goldOpt : null;
2026-08-29 14:04:13 +03:30
$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) {
2026-08-29 14:04:13 +03:30
return self::FAILURE;
}
/** @var Customer $customer */
$customer = $resolved['customer'];
/** @var Order|null $order */
$order = $resolved['order'];
/** @var Collection<int, Transaction> $txs */
$txs = $resolved['txs'];
$orphanOrderId = $resolved['orphan_order_id'];
$label = $resolved['label'];
2026-08-29 14:04:13 +03:30
$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 === []) {
2026-08-29 14:04:13 +03:30
$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} گرم روی کیف اصلی استفاده می‌شود.");
2026-08-29 14:04:13 +03:30
}
}
if ($goldToStrip === []) {
$this->error('مقداری برای کسر طلا مشخص نشد. --gold=0.105 بدهید.');
return self::FAILURE;
}
$refOrderId = $order?->id ?? $orphanOrderId;
$adminRows = $refOrderId
? TransactionsAdmin::withTrashed()->where('order_id', $refOrderId)->get()
2026-08-29 14:04:13 +03:30
: 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()
2026-08-29 14:04:13 +03:30
: 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;
}
2026-08-29 14:04:13 +03:30
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('بدون ردیف سفارش — فقط تراکنش/موجودی اصلاح می‌شود.');
2026-08-29 14:04:13 +03:30
}
$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());
2026-08-29 14:04:13 +03:30
$this->newLine();
$this->info('--- کسر طلا از کیف (بدون تراکنش برگشتی) ---');
2026-08-29 14:04:13 +03:30
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('بعد از کسر طلا منفی می‌شود — متوقف شد.');
2026-08-29 14:04:13 +03:30
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 / زرین راه دور لغو نمی‌شود.');
2026-08-29 14:04:13 +03:30
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');
}
2026-08-29 14:04:13 +03:30
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 بدهید.');
2026-08-29 14:04:13 +03:30
return self::FAILURE;
}
$noteTag = $order?->id ?? $orphanOrderId ?? ($txIds[0] ?? 'manual');
2026-08-29 14:04:13 +03:30
DB::transaction(function () use (
$order,
$customer,
$txs,
$goldToStrip,
$adminRows,
$zarinRows,
$tahesabRows,
$skipRecharge,
$rechargeRial,
$adminUserId,
$noteTag,
$refOrderId
2026-08-29 14:04:13 +03:30
) {
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 !== []) {
2026-08-29 14:04:13 +03:30
if (Schema::hasTable('transaction_metas')) {
TransactionMeta::query()->whereIn('transaction', $ids)->delete();
2026-08-29 14:04:13 +03:30
}
Transaction::query()->whereIn('id', $ids)->delete();
2026-08-29 14:04:13 +03:30
}
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();
2026-08-29 14:04:13 +03:30
}
if ($order) {
Order::query()->where('id', $order->id)->delete();
}
2026-08-29 14:04:13 +03:30
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,
2026-08-29 14:04:13 +03:30
'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 ? '.' : ' — شارژ ریالی هم ثبت شد.'));
2026-08-29 14:04:13 +03:30
$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));
2026-08-29 14:04:13 +03:30
}
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
2026-08-29 14:04:13 +03:30
{
// ۱) سفارش هنوز هست
2026-08-29 14:04:13 +03:30
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('مشتری سفارش یافت نشد.');
2026-08-29 14:04:13 +03:30
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}",
];
2026-08-29 14:04:13 +03:30
}
if ($customerId <= 0) {
$this->error('--order یا --customer یا --transaction الزامی است.');
2026-08-29 14:04:13 +03:30
return null;
}
$customer = Customer::find($customerId);
if (!$customer) {
$this->error("مشتری {$customerId} یافت نشد.");
return null;
}
// ۳) سفارش شبیه ۱۰ میلیون / ۰٫۱۰۵
$order = Order::query()
2026-08-29 14:04:13 +03:30
->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();
2026-08-29 14:04:13 +03:30
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}",
];
2026-08-29 14:04:13 +03:30
}
// ۴) تراکنش یتیم: طلا ~۰٫۱۰۵ و/یا 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;
2026-08-29 14:04:13 +03:30
}
}
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;
2026-08-29 14:04:13 +03:30
}
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();
}
}