Files
boursecu_server/app/Console/Commands/EraseBadBuyAndRechargeCommand.php
T
mohmdsdgandCursor cecb84bad9 Allow erase-bad-buy when the orders row was already deleted.
Find orphan buy transactions or accept --gold/--transaction so wallet gold can still be stripped and cash recharged.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 14:08:51 +03:30

528 lines
21 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\Collection;
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;
/**
* پاک‌کردن اثر خرید اشتباه و شارژ مجدد ریالی.
*
* اگر ردیف orders دستی پاک شده باشد هم کار می‌کند:
* تراکنش‌های باقی‌مانده (یا --gold/--transaction) را پیدا می‌کند.
*/
class EraseBadBuyAndRechargeCommand extends Command
{
protected $signature = 'wallet:erase-bad-buy-recharge
{--order= : شناسه سفارش (اگر هنوز هست)}
{--customer= : شناسه مشتری — برای پیدا کردن سفارش یا تراکنش یتیم}
{--transaction= : شناسه تراکنش خرید (وقتی سفارش پاک شده)}
{--gold= : گرم طلایی که باید از کیف کم شود (مثلاً 0.105) اگر تراکنش هم نیست}
{--recharge-rial=100000000 : مبلغ شارژ مجدد به ریال (پیش‌فرض ۱۰ میلیون تومان)}
{--user= : شناسه ادمین برای withdraw شارژ}
{--skip-recharge : فقط پاک‌کردن؛ شارژ نکن}
{--execute : اجرای واقعی}';
protected $description = 'پاک‌کردن اثر خرید اشتباه و شارژ مجدد ریالی (حتی اگر orders حذف شده باشد)';
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;
$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<int, Transaction> $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();
}
}