script added
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user