Files
boursecu_server/app/Http/Controllers/Admin/WalletController.php
T

1490 lines
59 KiB
PHP
Raw Normal View History

2026-01-24 14:03:11 +00:00
<?php
namespace App\Http\Controllers\Admin;
use App\DB\Te\TableEngine;
use App\Http\Controllers\Controller;
2026-07-08 11:25:19 +03:30
use App\Models\Address;
2026-01-24 14:03:11 +00:00
use App\Models\Customer;
use App\Models\Order;
use App\Models\Price;
use App\Models\Product;
use Illuminate\Support\Facades\DB;
use App\Models\Withdraw;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Rooyesh\Wallet\interfaces\TransactionInterface;
use Rooyesh\Wallet\interfaces\WalletInterface;
use Rooyesh\Wallet\models\Transaction;
use Rooyesh\Wallet\models\Wallet;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use App\Http\Controllers\Admin\TahesabController;
use App\Models\Setting;
2026-02-19 16:20:36 +03:30
use App\Models\User;
2026-06-25 23:06:29 +03:30
use App\Models\TransactionsAdmin;
2026-07-08 11:25:19 +03:30
use App\Services\AssetSummaryService;
2026-06-23 21:21:04 +03:30
use App\Services\BalanceReportService;
2026-06-24 01:17:34 +03:30
use App\Services\ConfigService;
2026-07-20 15:52:42 +03:30
use App\Services\PargarSyncService;
2026-06-23 21:04:29 +03:30
use App\Services\WalletBalanceService;
2026-06-27 15:51:44 +03:30
use App\Services\WalletBroadcastService;
use Hekmatinasser\Verta\Verta;
2026-01-24 14:03:11 +00:00
class WalletController extends Controller
{
2026-07-26 14:15:26 +03:30
/** موجودی طلا با سه رقم اعشار نمایش داده می‌شود؛ نصف پلهٔ نمایش = بیشترین اختلاف ممکن با مقدار واقعی */
private const GOLD_DISPLAY_STEP = 0.0005;
/** پنجرهٔ تشخیص سند تکراری — دوبار کلیک یا ارسال مجدد فرم */
private const DUPLICATE_CHARGE_WINDOW_SECONDS = 60;
/**
* جلوگیری از ثبت دوبارهٔ همان سند دستی. فقط روی کسر اعمال می‌شود تا شارژهای
* مثبتِ پشت‌سرهم (مثلاً دو واریز یکسان) بسته نشوند.
*/
private function assertNotDuplicateCharge(int $customerId, float $value, string $column): void
{
if ($value >= 0) {
return;
}
$recent = Withdraw::query()
->where('customer_id', $customerId)
->where('sts', 4)
->where('created_at', '>=', now()->subSeconds(self::DUPLICATE_CHARGE_WINDOW_SECONDS));
if ($column === 'cash') {
$recent->where('cash', (int) $value);
} else {
$recent->whereBetween('gold', [$value - 1e-6, $value + 1e-6]);
}
if ($recent->exists()) {
throw new UnprocessableEntityHttpException(
'همین سند لحظاتی پیش برای این مشتری ثبت شده است. اگر تکرارش عمدی است یک دقیقه صبر کنید.'
);
}
}
/**
* عدد نمایشی موجودی تا ۳ رقم گرد می‌شود؛ اگر ادمین همان عدد را بزند، کل ماندهٔ واقعی کسر شود.
*/
private function resolveGoldDischargeAmount(float $available, float $requested, string $error): float
{
if ($requested <= 0) {
throw new UnprocessableEntityHttpException($error);
}
if ($requested <= $available) {
return $requested;
}
$sameDisplayed = round($requested, 3) <= round($available, 3);
$withinDisplayStep = ($requested - $available) <= self::GOLD_DISPLAY_STEP;
if ($sameDisplayed || $withinDisplayStep) {
return $available;
}
throw new UnprocessableEntityHttpException($error);
}
/**
* طلای مانده روی کیف legacy محصولات کیف اصلی را به کیف اصلی منتقل می‌کند
* تا موجودی نمایشی (effective) با کیف قابل‌کسر یکی شود.
*/
private function consolidateOrphanedMainWalletGold(int $ownerId, int $ownerType): void
{
$productIds = app(ConfigService::class)->mainWalletProductIds();
if ($productIds === []) {
return;
}
$orphans = Wallet::query()
->where('owner', $ownerId)
->where('owner_type', $ownerType)
->whereNull('fund_id')
->whereIn('product_id', $productIds)
->get();
$main = new WalletInterface($ownerId, $ownerType, null, null);
foreach ($orphans as $orphan) {
$amount = (float) $orphan->gold;
if ($amount <= 1e-12) {
continue;
}
$orphanWallet = new WalletInterface($ownerId, $ownerType, null, (int) $orphan->product_id);
new TransactionInterface($orphanWallet, [
'gold' => $amount,
'cash' => 0,
'expire_at' => now(),
'note' => 'انتقال به کیف اصلی',
'active' => true,
], 7, false, false, 0);
new TransactionInterface($main, [
'gold' => $amount,
'cash' => 0,
'expire_at' => now(),
'note' => 'انتقال از کیف محصول',
'active' => true,
], 7, true, false, 0);
$main = new WalletInterface($ownerId, $ownerType, null, null);
}
}
2026-01-24 14:03:11 +00:00
public function charge2(Request $request)
{
$data = $request->all();
$customer = Customer::findOrFail($data['customer_id']);
$deposit = false;
if ($data['value'] > 0) {
$deposit = true;
}
2026-06-15 11:25:10 +03:30
$wallet_interface = new WalletInterface($data['customer_id'], (int) $customer->type);
$wallet = $wallet_interface->wallet;
2026-01-24 14:03:11 +00:00
if (is_null($wallet) && !$deposit) {
throw new UnprocessableEntityHttpException('مقدار موجودی کمتر از درخواست شماست');
}
if (!is_null($wallet) && $wallet->cash < abs($data['value']) && !$deposit) {
throw new UnprocessableEntityHttpException('مقدار موجودی کمتر از درخواست شماست');
}
$orders = Order::where('customer_id', $data['customer_id'])->where('sts', 1)->get();
$gold_block = 0;
if (count($orders) > 0) {
$value = $data['value'];
foreach ($orders as $order) {
if ($value < $order->payment_remind) {
$order->payment_amount += $value;
$order->payment_remind -= $value;
$order->save();
break;
} else {
$value -= $order->payment_remind;
$order->payment_amount = $order->total_price;
$order->payment_remind = 0;
$order->sts = 2;
$gold_block -= $order->amount;
$order->save();
}
}
}
$user = auth()->user();
$customer = Customer::findOrFail($data["customer_id"]);
$Withdraw = Withdraw::create([
'user_id' => $user->id,
'customer_id' => $customer->id,
"cash" => $data['value'],
'sts' => 4,
]);
new TransactionInterface($wallet_interface, $_inp = [
'cash' => $data['value'],
'expire_at' => now(),
'withdraw_id' => $Withdraw->id,
'active' => true
], 3, false, true, $gold_block);
2026-07-20 15:52:42 +03:30
try {
app(PargarSyncService::class)->queueWalletDocument(
$customer,
'cash',
(float) $data['value'],
[
'withdraw_id' => $Withdraw->id,
'source' => 'admin_charge2',
]
);
} catch (\Throwable $e) {
report($e);
}
2026-06-16 20:38:48 +03:30
$message = 'کیف پول شما به مبلغ ' . self::formatSmsRialAmount($data['value']) . " ریال در تاریخ " . verta()->format('Y/n/j H:i:s') . " شارژ شد " . "\n"
2026-01-24 14:03:11 +00:00
. $this->Config('name_fa');
$this->Send($customer->mobile, $message);
2026-07-27 22:09:08 +03:30
try {
app(\App\Services\CustomerAppNotifyService::class)->notifyCustomer(
(int) $customer->id,
'واریز به کیف پول',
'کیف پول شما به مبلغ ' . self::formatSmsRialAmount($data['value']) . ' ریال شارژ شد.',
'success'
);
} catch (\Throwable $e) {
report($e);
}
2026-08-23 14:16:00 +03:30
\App\Services\Automation\AutomationEvents::fire('wallet.rial_charge', (int) $customer->id);
2026-01-24 14:03:11 +00:00
return response()->json(true);
}
public function charge(Request $request)
{
$data = $request->all();
$customer = Customer::findOrFail($data['customer_id']);
$deposit = false;
if ($data['value'] > 0) {
$deposit = true;
}
2026-06-15 11:25:10 +03:30
$wallet_interface = new WalletInterface($data['customer_id'], (int) $customer->type);
$wallet = $wallet_interface->wallet;
2026-01-24 14:03:11 +00:00
if (is_null($wallet) && !$deposit) {
throw new UnprocessableEntityHttpException('مقدار موجودی کمتر از درخواست شماست');
}
if (!is_null($wallet) && $wallet->cash < abs($data['value']) && !$deposit) {
throw new UnprocessableEntityHttpException('مقدار موجودی کمتر از درخواست شماست');
}
2026-07-26 14:15:26 +03:30
$this->assertNotDuplicateCharge((int) $customer->id, (float) $data['value'], 'cash');
2026-01-24 14:03:11 +00:00
$orders = Order::where('customer_id', $data['customer_id'])->where('sts', 1)->get();
$gold_block = 0;
$user = auth()->user();
$customer = Customer::findOrFail($data["customer_id"]);
$Withdraw = Withdraw::create([
'user_id' => $user->id,
'customer_id' => $customer->id,
"cash" => $data['value'],
"note" => $data['note'],
'sts' => 4,
]);
2026-06-07 12:25:32 +03:30
$type = $deposit ? 3 : 4;
2026-01-24 14:03:11 +00:00
new TransactionInterface($wallet_interface, $_inp = [
'gold' => 0,
'cash' => $data['value'],
'expire_at' => now(),
"note" => $data['note'],
'withdraw_id' => $Withdraw->id,
'active' => true
2026-06-07 12:25:32 +03:30
], $type, true, $deposit, $gold_block);
2026-01-24 14:03:11 +00:00
2026-06-25 23:06:29 +03:30
$this->recordAccountingTransaction($customer, $Withdraw, (int) $user->id, (float) $data['value'], 0, $data['note'] ?? null);
2026-07-05 16:43:34 +03:30
if (app(ConfigService::class)->isTahesabAccounting()) {
try {
$data['direction'] = $deposit ? 0 : 1;
2026-06-25 22:20:40 +03:30
$data['withdraw_id'] = $Withdraw->id;
app(TahesabController::class)->wallet($data);
} catch (\Exception $e) {
report($e);
}
}
2026-07-23 20:24:58 +03:30
if (app(ConfigService::class)->isZarinAccounting()) {
try {
$data['direction'] = $deposit ? 0 : 1;
$data['withdraw_id'] = $Withdraw->id;
app(\App\Http\Controllers\Admin\ZarinController::class)->wallet($data);
} catch (\Exception $e) {
report($e);
}
}
2026-07-20 15:52:42 +03:30
try {
app(PargarSyncService::class)->queueWalletDocument(
$customer,
'cash',
(float) $data['value'],
[
'withdraw_id' => $Withdraw->id,
'note' => $data['note'] ?? null,
'source' => 'admin_cash',
]
);
} catch (\Throwable $e) {
report($e);
}
2026-02-19 16:20:36 +03:30
//send sms to users and customer
$status = $data['value'] > 0 ?" شارژ شد ": " کسر شد ";
2026-07-18 22:22:01 +03:30
if (self::isSmsNotifEnabled('charge_notif')) {
2026-06-16 20:38:48 +03:30
$message = 'کیف پول کاربر ' . $customer->name . ' به مقدار ' . self::formatSmsRialAmount($data['value']) . " ریال در تاریخ " . verta()->format('Y/n/j H:i:s') .$status . "بابت" . "\n"
2026-02-19 16:20:36 +03:30
. $data['note']
. "\n"
. $this->Config('name_fa');
User::where('send_sms', 1)->get()->each(function ($admin) use ($message, $data) {
$this->send($admin->mobile, $message);
});
2026-07-18 22:22:01 +03:30
$message = 'کیف پول شما به اندازه ' . self::formatSmsRialAmount($data['value']) . " ریال در تاریخ " . verta()->format('Y/n/j H:i:s') .$status . "بابت" . "\n"
. $data['note']
. "\n"
. $this->Config('name_fa');
$this->Send($customer->mobile, $message);
}
2026-07-27 22:09:08 +03:30
try {
$isDeposit = (float) $data['value'] > 0;
app(\App\Services\CustomerAppNotifyService::class)->notifyCustomer(
(int) $customer->id,
$isDeposit ? 'واریز به کیف پول' : 'برداشت از کیف پول',
'کیف پول شما به اندازه ' . self::formatSmsRialAmount($data['value']) . ' ریال'
. ($isDeposit ? ' شارژ شد.' : ' کسر شد.')
. (trim((string) ($data['note'] ?? '')) !== '' ? "\n" . $data['note'] : ''),
$isDeposit ? 'success' : 'warning'
);
} catch (\Throwable $e) {
report($e);
}
2026-08-23 14:16:00 +03:30
if ((float) $data['value'] > 0) {
\App\Services\Automation\AutomationEvents::fire('wallet.rial_charge', (int) $customer->id);
}
2026-06-27 15:51:44 +03:30
$this->broadcastCustomerWalletAssets($customer);
2026-01-24 14:03:11 +00:00
return response()->json(true);
}
public function charge_gold(Request $request)
{
$data = $request->all();
$customer = Customer::findOrFail($data['customer_id']);
$deposit = false;
if ($data['value'] > 0) {
$deposit = true;
}
if (!$deposit) {
$this->consolidateOrphanedMainWalletGold((int) $customer->id, (int) $customer->type);
}
2026-06-15 11:25:10 +03:30
$wallet_interface = new WalletInterface($data['customer_id'], (int) $customer->type);
$wallet = $wallet_interface->wallet;
2026-07-26 14:15:26 +03:30
// کسر دستی نباید موجودی طلا را منفی کند؛ شارژ مثبت بدون هیچ شرطی باز است
if (!$deposit) {
$available = is_null($wallet) ? 0.0 : (float) $wallet->gold;
$requested = abs((float) $data['value']);
$resolved = $this->resolveGoldDischargeAmount($available, $requested, 'مقدار موجودی طلا کمتر از درخواست شماست');
$data['value'] = -$resolved;
2026-07-26 14:15:26 +03:30
}
$this->assertNotDuplicateCharge((int) $customer->id, (float) $data['value'], 'gold');
2026-01-24 14:03:11 +00:00
$gold_block = 0;
$user = auth()->user();
$customer = Customer::findOrFail($data["customer_id"]);
$Withdraw = Withdraw::create([
'user_id' => $user->id,
'customer_id' => $customer->id,
"gold" => $data['value'],
"note" => $data['note'],
'sts' => 4,
]);
2026-06-24 01:17:34 +03:30
$representative = app(ConfigService::class)->mainWalletRepresentativeProduct();
2026-01-24 14:03:11 +00:00
new TransactionInterface($wallet_interface, $_inp = [
'cash' => 0,
'gold' => $data['value'],
'expire_at' => now(),
"note" => $data['note'],
'withdraw_id' => $Withdraw->id,
2026-06-24 01:17:34 +03:30
'product_id' => $representative?->id,
2026-01-24 14:03:11 +00:00
'active' => true
], 7, $deposit, false, $gold_block);
2026-06-25 23:06:29 +03:30
$this->recordAccountingTransaction($customer, $Withdraw, (int) $user->id, 0, (float) $data['value'], $data['note'] ?? null);
2026-06-09 20:08:06 +03:30
//send to tahesab
2026-07-05 16:43:34 +03:30
if (app(ConfigService::class)->isTahesabAccounting()) {
try {
2026-06-07 14:30:26 +03:30
$data['direction'] = $deposit ? 1 : 0;
2026-06-25 22:20:40 +03:30
$data['withdraw_id'] = $Withdraw->id;
2026-07-28 17:44:24 +03:30
if ($representative) {
$data['product_id'] = $representative->id;
$data['product'] = [
'id' => $representative->id,
'title' => $representative->title,
'group_type' => $representative->group_type,
'metal_id' => $representative->metal_id,
'amount' => $representative->amount,
'unit' => $representative->unit,
'trade_by_quantity' => $representative->trade_by_quantity,
];
}
2026-06-25 22:20:40 +03:30
app(TahesabController::class)->walletGold($data);
} catch (\Exception $e) {
report($e);
}
}
2026-07-20 15:52:42 +03:30
2026-07-23 20:24:58 +03:30
if (app(ConfigService::class)->isZarinAccounting()) {
try {
$data['direction'] = $deposit ? 1 : 0;
$data['withdraw_id'] = $Withdraw->id;
2026-07-28 17:44:24 +03:30
if ($representative) {
$data['product_id'] = $representative->id;
$data['product'] = [
'id' => $representative->id,
'title' => $representative->title,
'group_type' => $representative->group_type,
'metal_id' => $representative->metal_id,
'amount' => $representative->amount,
'unit' => $representative->unit,
'trade_by_quantity' => $representative->trade_by_quantity,
];
}
2026-07-23 20:24:58 +03:30
app(\App\Http\Controllers\Admin\ZarinController::class)->walletGold($data);
} catch (\Exception $e) {
report($e);
}
}
2026-07-20 15:52:42 +03:30
try {
app(PargarSyncService::class)->queueWalletDocument(
$customer,
'gold',
(float) $data['value'],
[
'withdraw_id' => $Withdraw->id,
'note' => $data['note'] ?? null,
'product_id' => $representative?->id,
'source' => 'admin_gold',
]
);
} catch (\Throwable $e) {
report($e);
}
2026-02-19 16:20:36 +03:30
//send sms to users and customer
$status = $data['value'] > 0 ?" شارژ شد ": " کسر شد ";
2026-06-24 01:17:34 +03:30
$weightUnit = self::tradeWeightUnit();
2026-07-18 22:22:01 +03:30
if (self::isSmsNotifEnabled('charge_notif')) {
2026-06-24 01:17:34 +03:30
$message = 'کیف پول کاربر ' . $customer->name . ' به مقدار ' . self::formatSmsGoldWeight($data['value']) . " {$weightUnit} در تاریخ " . verta()->format('Y/n/j H:i:s') .$status . "بابت" . "\n"
2026-02-19 16:20:36 +03:30
. $data['note']
. "\n"
. $this->Config('name_fa');
User::where('send_sms', 1)->get()->each(function ($admin) use ($message, $data) {
$this->send($admin->mobile, $message);
});
2026-07-18 22:22:01 +03:30
$message = 'کیف پول شما به اندازه ' . self::formatSmsGoldWeight($data['value']) . " {$weightUnit} در تاریخ " . verta()->format('Y/n/j H:i:s') . $status . " بابت" . "\n"
. $data['note']
. $this->Config('name_fa');
$this->Send($customer->mobile, $message);
}
2026-07-27 22:09:08 +03:30
try {
$isDeposit = (float) $data['value'] > 0;
$weightUnit = self::tradeWeightUnit();
app(\App\Services\CustomerAppNotifyService::class)->notifyCustomer(
(int) $customer->id,
$isDeposit ? 'واریز طلا' : 'برداشت طلا',
'کیف پول شما به اندازه ' . self::formatSmsGoldWeight($data['value']) . " {$weightUnit}"
. ($isDeposit ? ' شارژ شد.' : ' کسر شد.'),
$isDeposit ? 'success' : 'warning'
);
} catch (\Throwable $e) {
report($e);
}
2026-06-27 15:51:44 +03:30
$this->broadcastCustomerWalletAssets($customer);
2026-01-24 14:03:11 +00:00
return response()->json(true);
}
2026-07-11 17:59:50 +03:30
/** گزینه‌های شارژ کیف پول — نقدی + همهٔ محصولات (فعال و غیرفعال) */
2026-06-23 21:59:38 +03:30
public function chargeTargets()
2026-01-24 14:03:11 +00:00
{
2026-06-23 21:59:38 +03:30
$configService = app(ConfigService::class);
$mainWalletProductIds = $configService->mainWalletProductIds();
$balanceReport = app(BalanceReportService::class);
$targets = [
[
'key' => 'money',
'kind' => 'money',
'label' => 'نقدی (ریال)',
'unit' => 'ریال',
2026-07-11 17:59:50 +03:30
'active' => true,
2026-06-23 21:59:38 +03:30
],
];
$products = Product::query()
2026-07-11 17:59:50 +03:30
->orderByDesc('active')
2026-06-23 21:59:38 +03:30
->orderBy('metal_id')
->orderBy('id')
2026-07-11 17:59:50 +03:30
->get(['id', 'title', 'unit', 'metal_id', 'group_type', 'trade_by_quantity', 'amount', 'direct_price', 'active']);
2026-06-23 21:59:38 +03:30
foreach ($products as $product) {
$usesMainWallet = in_array((int) $product->id, $mainWalletProductIds, true);
$label = (string) $product->title;
if ($usesMainWallet) {
$label .= ' (کیف اصلی)';
}
2026-07-11 17:59:50 +03:30
if (!(bool) $product->active) {
$label .= ' (غیرفعال)';
}
2026-06-23 21:59:38 +03:30
2026-07-28 17:44:24 +03:30
$payload = Controller::productUnitPayload($product);
$isCountBased = Controller::isCountBasedProduct($payload);
2026-06-23 21:59:38 +03:30
$targets[] = [
'key' => 'product:' . $product->id,
'kind' => 'metal',
'product_id' => (int) $product->id,
'label' => $label,
'unit' => $this->resolveProductChargeUnit($product),
'metal_id' => max(1, (int) ($product->metal_id ?? 1)),
'uses_main_wallet' => $usesMainWallet,
'trade_by_quantity' => (bool) $product->trade_by_quantity,
2026-07-28 17:44:24 +03:30
'allows_decimal' => !$isCountBased,
2026-06-23 21:59:38 +03:30
'group_type' => (string) ($product->group_type ?? ''),
2026-07-11 17:59:50 +03:30
'active' => (bool) $product->active,
2026-06-23 21:59:38 +03:30
];
}
return response()->json([
'targets' => $targets,
'catalogMetals' => $balanceReport->catalogMetalsMeta(),
]);
}
/**
* شارژ یکپارچه کیف پول — نقدی یا محصول (با رعایت کیف اصلی برای آبشده).
*/
public function charge_wallet(Request $request)
{
$productId = (int) $request->input('product_id', 0);
if ($productId > 0) {
return $this->chargeProductMetal($request, $productId);
}
return $this->charge($request);
}
private function chargeProductMetal(Request $request, int $productId)
{
$data = $request->all();
$product = Product::findOrFail($productId);
$customer = Customer::findOrFail($data['customer_id']);
2026-07-28 17:44:24 +03:30
$value = (float) ($data['value'] ?? 0);
$deposit = $value > 0;
$payload = Controller::productUnitPayload($product);
if (Controller::isCountBasedProduct($payload) && abs($value - round($value)) > 0.000001) {
throw new UnprocessableEntityHttpException('برای محصول تعدادی فقط عدد صحیح مجاز است');
}
2026-06-23 21:59:38 +03:30
$isMainWalletProduct = Controller::isMainWalletProduct($productId);
if (!$deposit && $isMainWalletProduct) {
$this->consolidateOrphanedMainWalletGold((int) $customer->id, (int) $customer->type);
}
2026-06-23 21:59:38 +03:30
$effectiveProductId = Controller::effectiveProductWalletId($productId);
$wallet_interface = new WalletInterface(
(int) $data['customer_id'],
(int) $customer->type,
null,
$effectiveProductId
);
$wallet = $wallet_interface->wallet;
if (!$deposit) {
$available = is_null($wallet) ? 0.0 : (float) $wallet->gold;
$resolved = $this->resolveGoldDischargeAmount(
$available,
abs($value),
'مقدار موجودی کمتر از درخواست شماست'
);
$value = -$resolved;
$data['value'] = $value;
2026-06-23 21:59:38 +03:30
}
2026-07-26 14:15:26 +03:30
$this->assertNotDuplicateCharge((int) $customer->id, (float) $data['value'], 'gold');
2026-06-23 21:59:38 +03:30
$goldBlock = 0;
$user = auth()->user();
$unitLabel = $this->resolveProductChargeUnit($product);
$Withdraw = Withdraw::create([
'user_id' => $user->id,
'customer_id' => $customer->id,
'gold' => $data['value'],
'note' => $data['note'] ?? null,
'sts' => 4,
]);
new TransactionInterface($wallet_interface, [
'cash' => 0,
'gold' => $data['value'],
'product_id' => $productId,
'expire_at' => now(),
'note' => $data['note'] ?? null,
'withdraw_id' => $Withdraw->id,
'active' => true,
], 7, $deposit, false, $goldBlock);
2026-06-25 23:06:29 +03:30
$this->recordAccountingTransaction(
$customer,
$Withdraw,
(int) $user->id,
0,
(float) $data['value'],
($data['note'] ?? null) ?: ('شارژ ' . $product->title)
);
2026-07-05 16:43:34 +03:30
if (app(ConfigService::class)->isTahesabAccounting()) {
2026-06-23 21:59:38 +03:30
try {
$data['direction'] = $deposit ? 1 : 0;
2026-06-25 22:20:40 +03:30
$data['withdraw_id'] = $Withdraw->id;
2026-07-28 17:44:24 +03:30
$data['product_id'] = $productId;
$data['product'] = [
'id' => $product->id,
'title' => $product->title,
'group_type' => $product->group_type,
'metal_id' => $product->metal_id,
'amount' => $product->amount,
'unit' => $product->unit,
'trade_by_quantity' => $product->trade_by_quantity,
];
2026-06-25 22:20:40 +03:30
app(TahesabController::class)->walletGold($data);
2026-06-23 21:59:38 +03:30
} catch (\Exception $e) {
2026-06-25 22:20:40 +03:30
report($e);
2026-06-23 21:59:38 +03:30
}
}
2026-07-23 20:24:58 +03:30
if (app(ConfigService::class)->isZarinAccounting()) {
try {
$data['direction'] = $deposit ? 1 : 0;
$data['withdraw_id'] = $Withdraw->id;
2026-07-28 17:44:24 +03:30
$data['product_id'] = $productId;
$data['product'] = [
'id' => $product->id,
'title' => $product->title,
'group_type' => $product->group_type,
'metal_id' => $product->metal_id,
'amount' => $product->amount,
'unit' => $product->unit,
'trade_by_quantity' => $product->trade_by_quantity,
];
2026-07-23 20:24:58 +03:30
app(\App\Http\Controllers\Admin\ZarinController::class)->walletGold($data);
} catch (\Exception $e) {
report($e);
}
}
2026-07-20 15:52:42 +03:30
try {
// فقط محصول متصل به کیف اصلی → سند طلای پرگار
app(PargarSyncService::class)->queueWalletDocument(
$customer,
'gold',
(float) $data['value'],
[
'withdraw_id' => $Withdraw->id,
'note' => $data['note'] ?? null,
'product_id' => $productId,
'source' => 'admin_product',
]
);
} catch (\Throwable $e) {
report($e);
}
2026-06-23 21:59:38 +03:30
$status = $deposit ? ' شارژ شد ' : ' کسر شد ';
$formatted = self::formatSmsGoldWeight($data['value']);
2026-07-18 22:22:01 +03:30
if (self::isSmsNotifEnabled('charge_notif')) {
2026-06-23 21:59:38 +03:30
$message = 'کیف پول کاربر ' . $customer->name . ' به مقدار ' . $formatted . ' ' . $unitLabel . ' در تاریخ ' . verta()->format('Y/n/j H:i:s') . $status . 'بابت' . "\n"
. ($data['note'] ?? '')
. "\n"
. $this->Config('name_fa');
User::where('send_sms', 1)->get()->each(function ($admin) use ($message) {
$this->send($admin->mobile, $message);
});
2026-07-18 22:22:01 +03:30
$message = 'کیف پول شما به اندازه ' . $formatted . ' ' . $unitLabel . ' در تاریخ ' . verta()->format('Y/n/j H:i:s') . $status . ' بابت' . "\n"
. ($data['note'] ?? '')
. "\n"
. $this->Config('name_fa');
$this->Send($customer->mobile, $message);
}
2026-07-27 22:09:08 +03:30
try {
$isDeposit = (bool) $deposit;
app(\App\Services\CustomerAppNotifyService::class)->notifyCustomer(
(int) $customer->id,
$isDeposit ? 'واریز محصول' : 'برداشت محصول',
'کیف پول شما به اندازه ' . $formatted . ' ' . $unitLabel
. ($isDeposit ? ' شارژ شد.' : ' کسر شد.'),
$isDeposit ? 'success' : 'warning'
);
} catch (\Throwable $e) {
report($e);
}
2026-06-27 15:51:44 +03:30
$this->broadcastCustomerWalletAssets($customer);
2026-06-23 21:59:38 +03:30
return response()->json(true);
}
private function resolveProductChargeUnit(Product $product): string
{
$payload = Controller::productUnitPayload($product);
2026-07-28 17:44:24 +03:30
// تعدادی: همیشه «عدد» — حتی اگر unit محصول وزن هر قطعه (گرم) باشد
if (Controller::isCountBasedProduct($payload)) {
return 'عدد';
2026-06-23 21:59:38 +03:30
}
if (Controller::isWeightBasedProductUnit($payload['unit'] ?? null)) {
return trim((string) $payload['unit']);
}
2026-07-28 17:44:24 +03:30
$unit = trim((string) ($payload['unit'] ?? ''));
if ($unit !== '') {
return $unit;
}
2026-06-23 21:59:38 +03:30
return Controller::productStorageWeightUnit($product);
}
2026-01-24 14:03:11 +00:00
public function index(Request $request)
{
2026-06-23 21:21:04 +03:30
$balanceReport = app(BalanceReportService::class);
$balanceService = app(WalletBalanceService::class);
$catalogMetalIds = $balanceService->catalogMetalIds();
$records = Transaction::with([
'customer',
2026-07-21 22:23:09 +03:30
'wallet.product' => static function ($q) {
$q->withTrashed()->select(['id', 'title', 'metal_id', 'group_type', 'unit', 'amount', 'trade_by_quantity', 'direct_price']);
},
'product' => static function ($q) {
$q->withTrashed()->select(['id', 'title', 'metal_id', 'group_type', 'unit', 'amount', 'trade_by_quantity', 'direct_price']);
},
2026-06-23 21:21:04 +03:30
])->whereNull('fund_id');
2026-01-24 14:03:11 +00:00
TableEngine::create($records, $request->all())
->initSearch(['order_id', 'customer.name'])
->select([
'*', Transaction::$SELECT_CAJ, Transaction::$SELECT_TYPE_STR, Transaction::$SELECT_SAJ, Transaction::$SELECT_EAJ
])->export($records);
2026-06-24 01:17:34 +03:30
if (isset($records['rows'])) {
2026-06-24 01:44:38 +03:30
$records['rows'] = collect($records['rows'])->map(function ($row) use ($balanceReport, $catalogMetalIds) {
2026-06-23 21:21:04 +03:30
$balanceReport->enrichTransactionTableRow($row, $catalogMetalIds);
2026-06-24 01:44:38 +03:30
if ($row instanceof Transaction) {
$array = $row->toArray();
$rawTypeStr = $row->getAttributes()['type_str'] ?? null;
if (is_string($rawTypeStr) && !is_numeric(trim($rawTypeStr))) {
$array['type_str'] = $rawTypeStr;
}
return $array;
}
return is_array($row) ? $row : (array) $row;
})->values()->all();
2026-06-24 01:17:34 +03:30
}
2026-06-23 21:21:04 +03:30
$records['catalogMetals'] = $balanceReport->catalogMetalsMeta();
2026-01-24 14:03:11 +00:00
return response()->json($records);
}
public function updateTransaction(Request $request)
{
$message = 'تراکنش تغیری نکرده.';
$transaction = Transaction::find($request->id);
$product = Product::find($transaction->product_id);
if (!$transaction) return response()->json(['error' => 'تراکنش یافت نشد.'], 404);
$difference = $request->input('number') - $transaction->number;
if (abs($difference) < 0.0001) {
return response()->json(['message' => $message], 422);
}
$transaction->number = $request->input('number');
$wallet_interface = new WalletInterface($transaction->customer_id, 2);
if ($wallet_interface->wallet->gold < $difference ) {
if (!(isset($request->force) && $request->force === true)){
return response()->json(['message' => "کاربر مورد نظر موجودی ندارد. موجودی فعلی: " . $wallet_interface->wallet->gold . " مقدار مورد نیاز: " . $difference,'type'=>406],422);
}
}
$isDiposit = $difference > 0 ? 0 : 1;
new TransactionInterface($wallet_interface, [
"title" => "اصلاح سند " . $transaction->withdraw_id,
"note" => $product->title,
"product_id" => $product->id,
"number" => 0,
"gold" => $difference,
'expire_at' => now(),
'active' => true,
'withdraw_id' => $transaction->withdraw_id,
'start_at' => now(),
], 51, $isDiposit, false);
$transaction->save();
$message = 'تراکنش با موفقیت به‌روزرسانی شد.';
2026-06-27 15:51:44 +03:30
$customer = Customer::find($transaction->customer_id);
if ($customer) {
$this->broadcastCustomerWalletAssets($customer);
}
2026-01-24 14:03:11 +00:00
return response()->json([
'message' => $message,
'transaction' => $transaction,
// 'previous_amount' => $previousAmount,
// 'new_amount' => $newAmount,
'difference' => $difference,
'wallet' => $wallet_interface->wallet,
]);
}
public function indexWallet(Request $request)
{
2026-06-09 20:32:27 +03:30
$lastPrice = (float) (Price::latest()->first()?->price ?? 0);
2026-06-23 21:04:29 +03:30
$balanceService = app(WalletBalanceService::class);
$catalogMetalIds = $balanceService->catalogMetalIds();
$mainWalletMetalId = $balanceService->resolveMainWalletMetalId();
2026-06-09 20:32:27 +03:30
// همان فیلتر/جستجوی جدول — بدون صفحه‌بندی — برای مجموع ردیف‌های قابل مشاهده
$sumQuery = Wallet::query();
TableEngine::create($sumQuery, $request->all())
->initSearch(['customer.name']);
2026-07-01 20:59:24 +03:30
$walletTableWith = [
'customer:id,name,mobile',
'fund:id,title',
2026-07-21 22:28:04 +03:30
'product' => static function ($q) {
$q->withTrashed()->select(['id', 'metal_id', 'unit', 'amount', 'group_type', 'trade_by_quantity', 'direct_price', 'title']);
},
2026-07-01 20:59:24 +03:30
];
$sumRows = (clone $sumQuery)->with($walletTableWith)->get();
2026-06-23 21:04:29 +03:30
$metalBalances = $balanceService->buildWalletMetalSums($sumRows, $catalogMetalIds);
$totalGold = array_sum(array_column($metalBalances, 'gold'));
$totalGoldToCash = array_sum(array_column($metalBalances, 'goldToCash'));
2026-06-09 20:32:27 +03:30
$totalCash = (float) (clone $sumQuery)->sum('cash');
$sums = [
'gold' => round($totalGold, 2),
2026-06-23 21:04:29 +03:30
'goldToCash' => round($totalGoldToCash, 2),
2026-06-09 20:32:27 +03:30
'cash' => round($totalCash, 2),
'cashToGold' => $lastPrice > 0 ? round($totalCash / $lastPrice, 2) : 0,
2026-06-23 21:04:29 +03:30
'metalBalances' => $metalBalances,
2026-06-09 20:32:27 +03:30
];
2026-07-01 20:59:24 +03:30
$records = Wallet::with($walletTableWith);
2026-01-24 14:03:11 +00:00
TableEngine::create($records, $request->all())
2026-07-01 20:59:24 +03:30
->initSearch(['customer.name', 'customer.mobile', 'product.title', 'fund.title'])
2026-01-24 14:03:11 +00:00
->select(['*'])
->export($records);
2026-06-23 21:04:29 +03:30
if (isset($records['rows'])) {
foreach ($records['rows'] as &$row) {
$balanceService->enrichWalletTableRow($row, $catalogMetalIds, $mainWalletMetalId);
}
unset($row);
}
2026-01-24 14:03:11 +00:00
$records['sums'] = $sums;
return response()->json($records);
}
public function indexWithdraw(Request $request)
{
2026-07-27 16:05:54 +03:30
$records = Withdraw::with(['customer', 'bank_account', 'address', "Transaction"]);
Withdraw::applyFlowFilter($records, $request->input('flow_type', 'all'));
2026-01-24 14:03:11 +00:00
TableEngine::create($records, $request->all())
->initSearch(['customer.name'])
->select([
'*', Withdraw::$SELECT_STS_STR, Withdraw::$SELECT_DAJ
])->export($records);
2026-06-25 22:20:40 +03:30
2026-07-05 16:43:34 +03:30
if (app(ConfigService::class)->isTahesabAccounting() && isset($records['rows']) && is_array($records['rows'])) {
2026-06-25 22:20:40 +03:30
$withdrawIds = collect($records['rows'])->pluck('id')->filter()->values()->all();
if ($withdrawIds !== []) {
$failedByWithdraw = \App\Models\Tahesab::query()
->whereIn('withdraw_id', $withdrawIds)
->where(function ($query) {
$query->whereNull('tahesab_id');
if (\Illuminate\Support\Facades\Schema::hasColumn('tahesab', 'sync_status')) {
$query->orWhere('sync_status', \App\Services\TahesabSyncService::STATUS_FAILED);
}
})
->orderByDesc('id')
->get()
->unique('withdraw_id')
->keyBy('withdraw_id');
foreach ($records['rows'] as &$row) {
$tahesab = $failedByWithdraw->get($row['id'] ?? null);
if ($tahesab) {
$row['tahesab_failed_id'] = $tahesab->id;
}
}
unset($row);
}
}
2026-07-23 20:24:58 +03:30
if (app(ConfigService::class)->isZarinAccounting() && isset($records['rows']) && is_array($records['rows'])) {
$withdrawIds = collect($records['rows'])->pluck('id')->filter()->values()->all();
if ($withdrawIds !== []) {
$failedByWithdraw = \App\Models\Zarin::query()
->whereIn('withdraw_id', $withdrawIds)
->where(function ($query) {
$query->whereNull('zarin_id');
if (\Illuminate\Support\Facades\Schema::hasColumn('zarin', 'sync_status')) {
$query->orWhere('sync_status', \App\Services\ZarinSyncService::STATUS_FAILED);
}
})
->orderByDesc('id')
->get()
->unique('withdraw_id')
->keyBy('withdraw_id');
foreach ($records['rows'] as &$row) {
$zarin = $failedByWithdraw->get($row['id'] ?? null);
if ($zarin) {
$row['zarin_failed_id'] = $zarin->id;
}
}
unset($row);
}
}
2026-01-24 14:03:11 +00:00
return response()->json($records);
}
public function exportWithdrawReport(Request $request)
{
$flowType = $request->input('flow_type', 'all');
$query = Withdraw::with(['customer', 'bank_account']);
Withdraw::applyFlowFilter($query, $flowType);
TableEngine::create($query, $request->all())
->initSearch(['customer.name']);
$rows = $query->orderBy('id', 'desc')->get();
$titles = [
'all' => 'گزارش برداشت و واریز',
'withdraw' => 'گزارش برداشت',
'deposit' => 'گزارش واریز',
];
return response()->view('admin.withdrawReport', [
'rows' => $rows,
'title' => $titles[$flowType] ?? $titles['all'],
'flowType' => $flowType,
'generatedAt' => Verta::now()->format('Y/n/j H:i:s'),
])->header('Content-Type', 'text/html; charset=UTF-8');
}
2026-01-24 14:03:11 +00:00
public function updateWithdraw(Request $request, $id)
{
2026-07-16 17:29:18 +03:30
$withdraw = Withdraw::with(['Transaction.product', 'customer'])->find($id);
2026-01-24 14:03:11 +00:00
if (!$withdraw) {
return response()->json(['error' => 'Withdraw not found'], 404);
}
2026-07-16 17:29:18 +03:30
$newSts = (int) $request->sts;
$oldSts = (int) $withdraw->sts;
if (
Withdraw::isPhysicalWithdrawNote($withdraw->note)
&& $newSts === 52
&& in_array($oldSts, [50, 51], true)
) {
$this->refundPhysicalWithdrawGold($withdraw);
}
$withdraw->sts = $newSts;
2026-01-24 14:03:11 +00:00
$withdraw->save();
2026-07-16 17:29:18 +03:30
if ($withdraw->customer) {
$this->broadcastCustomerWalletAssets($withdraw->customer);
}
2026-01-24 14:03:11 +00:00
return response()->json(['message' => 'Withdraw updated successfully', 'withdraw' => $withdraw]);
}
2026-07-16 17:29:18 +03:30
/**
* برگشت موجودی کسرشدهٔ برداشت فیزیکی هنگام لغو (sts=52).
*/
protected function refundPhysicalWithdrawGold(Withdraw $withdraw): void
{
$alreadyRefunded = Transaction::query()
->where('withdraw_id', $withdraw->id)
->where('note', 'like', '%لغو سند برداشت فیزیکی%')
->exists();
if ($alreadyRefunded) {
return;
}
$customer = $withdraw->customer ?: Customer::find($withdraw->customer_id);
if (!$customer) {
return;
}
$assetSummaryService = app(AssetSummaryService::class);
foreach ($withdraw->Transaction as $transaction) {
// فقط تراکنش‌های کسر (منفی) را برگردان — نه تراکنش‌های برگشت قبلی
$gold = (float) ($transaction->gold ?? 0);
if ($gold >= 0) {
continue;
}
$amount = abs($gold);
if ($amount < 0.000001) {
continue;
}
$product = $transaction->product;
$productId = (int) ($transaction->product_id ?? ($product->id ?? 0));
if ($productId <= 0) {
continue;
}
$effectiveProductId = $assetSummaryService->effectiveProductWalletIdForCustomer(
(int) $customer->id,
(int) $customer->type,
$productId
);
$walletInterface = new WalletInterface(
(int) $customer->id,
(int) $customer->type,
null,
$effectiveProductId
);
$title = $product->title ?? 'محصول';
new TransactionInterface($walletInterface, [
'gold' => $amount,
'number' => $transaction->number ?? null,
'product_id' => $productId,
'expire_at' => now(),
'active' => true,
'title' => "برگشت برداشت فیزیکی {$title}",
'note' => "لغو سند برداشت فیزیکی شماره {$withdraw->id}",
'withdraw_id' => $withdraw->id,
], 6, true);
}
}
2026-01-24 14:03:11 +00:00
public function getWithdraw(Request $request, $id)
{
2026-07-27 16:05:54 +03:30
$withdraw = Withdraw::with(['customer', 'bank_account', 'address', "Transaction"])->find($id);
2026-01-24 14:03:11 +00:00
if (!$withdraw) {
return response()->json(['error' => 'Withdraw not found'], 404);
}
2026-06-09 20:08:06 +03:30
return response()->json($withdraw);
2026-01-24 14:03:11 +00:00
}
2026-07-08 11:25:19 +03:30
public function indexPhysicalWithdraw(Request $request)
{
2026-07-27 16:05:54 +03:30
$records = Withdraw::with(['customer', 'address', 'Transaction.product']);
2026-07-08 11:25:19 +03:30
Withdraw::scopePhysicalWithdrawals($records);
TableEngine::create($records, $request->all())
->initSearch(['customer.name', 'customer.mobile'])
->select([
'*', Withdraw::$SELECT_STS_STR, Withdraw::$SELECT_DAJ
])->export($records);
return response()->json($records);
}
public function physicalWithdrawOptions($customerId, AssetSummaryService $assetSummaryService)
{
$customer = Customer::findOrFail($customerId);
$summary = $assetSummaryService->buildForCustomer((int) $customer->id, (int) $customer->type);
$inventoryByProduct = collect($summary['inventory_cards'] ?? [])->keyBy('product_id');
$configService = app(ConfigService::class);
$mainWalletProductIds = $configService->mainWalletProductIds();
$products = Product::where(function ($query) use ($customer, $mainWalletProductIds) {
$query->whereHas('wallet', function ($q) use ($customer) {
$q->where('owner', $customer->id);
});
if ($mainWalletProductIds !== []) {
$query->orWhereIn('id', $mainWalletProductIds);
}
})->with(['wallet' => function ($query) use ($customer) {
$query->where('owner', $customer->id);
}])->get();
$representativeId = null;
if ($mainWalletProductIds !== []) {
foreach ($mainWalletProductIds as $pid) {
if ($products->firstWhere('id', $pid)) {
$representativeId = (int) $pid;
break;
}
}
$products = $products->filter(function ($product) use ($mainWalletProductIds, $representativeId) {
if (in_array((int) $product->id, array_map('intval', $mainWalletProductIds), true)) {
return (int) $product->id === $representativeId;
}
return true;
})->values();
}
$productRows = $products->map(function ($product) use ($customer, $assetSummaryService, $inventoryByProduct, $representativeId) {
$card = $inventoryByProduct->get((int) $product->id);
$displayQty = $card !== null ? (float) ($card['quantity'] ?? 0) : 0.0;
$walletInterface = new WalletInterface(
(int) $customer->id,
(int) $customer->type,
null,
$assetSummaryService->effectiveProductWalletIdForCustomer(
(int) $customer->id,
(int) $customer->type,
(int) $product->id
)
);
$wallet = $walletInterface->wallet;
if (!$wallet) {
return null;
}
return [
'product_id' => (int) $product->id,
'title' => (string) $product->title,
'wallet_id' => (int) $wallet->id,
'available' => $displayQty,
2026-07-16 17:29:18 +03:30
'unit' => self::productQuantityUnit(
self::productUnitPayload($product)
),
2026-07-08 11:25:19 +03:30
'is_main_wallet' => $representativeId !== null && (int) $product->id === $representativeId,
];
})->filter(function ($row) {
return $row !== null && ($row['available'] ?? 0) > 0 && $row['title'] !== 'آبشده نقدی';
})->values();
$addresses = Address::query()
->where('customer_id', $customer->id)
->orderByDesc('default_address')
->orderByDesc('id')
->get(['id', 'province', 'city', 'address', 'phone', 'zip', 'default_address']);
return response()->json([
'products' => $productRows,
'addresses' => $addresses,
]);
}
public function createPhysicalWithdraw(Request $request, AssetSummaryService $assetSummaryService)
{
$data = $request->validate([
'customer_id' => 'required|integer|exists:customers,id',
'address_id' => 'required|integer|exists:addresses,id',
'wallets' => 'required|array|min:1',
'wallets.*.wallet_id' => 'required|integer|exists:wallets,id',
'wallets.*.value' => 'required|numeric|min:0.0001',
]);
$customer = Customer::findOrFail($data['customer_id']);
$address = Address::find($data['address_id']);
if (!$address || (int) $address->customer_id !== (int) $customer->id) {
return response()->json(['message' => 'آدرس معتبر نیست یا متعلق به این مشتری نمی‌باشد'], 403);
}
$withdraw = DB::transaction(function () use ($data, $customer, $address, $assetSummaryService) {
$withdraw = new Withdraw();
$withdraw->note = 'برداشت فیزیکی';
$withdraw->address_id = $address->id;
$withdraw->customer_id = $customer->id;
$withdraw->user_id = auth('user')->id();
$withdraw->sts = 50;
$withdraw->save();
foreach ($data['wallets'] as $walletRow) {
$wallet = Wallet::with('product')->findOrFail($walletRow['wallet_id']);
if ((int) $wallet->owner !== (int) $customer->id) {
throw new UnprocessableEntityHttpException('کیف پول انتخاب‌شده متعلق به این مشتری نیست');
}
if (!$wallet->product) {
throw new UnprocessableEntityHttpException('محصولی در کیف پول مورد نظر یافت نشد');
}
$productId = (int) $wallet->product_id;
$value = (float) $walletRow['value'];
$effectiveProductId = $assetSummaryService->effectiveProductWalletIdForCustomer(
(int) $customer->id,
(int) $customer->type,
$productId
);
$availableGold = $assetSummaryService->productDisplayQuantity(
(int) $customer->id,
(int) $customer->type,
$productId
);
if ($value > $availableGold) {
throw new UnprocessableEntityHttpException(
"مشتری به اندازه کافی از {$wallet->product->title} ندارد"
);
}
$walletInterface = new WalletInterface(
(int) $customer->id,
(int) $customer->type,
null,
$effectiveProductId
);
new TransactionInterface($walletInterface, [
'gold' => $value,
'number' => (float) $wallet->product->amount * $value,
'product_id' => $wallet->product->id,
'expire_at' => now(),
'active' => true,
'title' => " برداشت {$wallet->product->title}",
'note' => " برداشت محصول {$wallet->product->title} شماره سند {$withdraw->id}",
'withdraw_id' => $withdraw->id,
], 6, false);
}
return $withdraw;
});
2026-07-28 17:44:24 +03:30
foreach ($data['wallets'] as $walletRow) {
$wallet = Wallet::with('product')->find($walletRow['wallet_id'] ?? 0);
$product = $wallet?->product;
if (!$product) {
continue;
}
try {
app(\App\Services\AccountingWalletPush::class)->pushProductWalletGold(
$customer,
$product,
(float) $walletRow['value'],
false,
(int) $withdraw->id,
'برداشت فیزیکی ' . $product->title
);
} catch (\Throwable $e) {
report($e);
}
}
2026-07-08 11:25:19 +03:30
$this->broadcastCustomerWalletAssets($customer);
return response()->json(['id' => $withdraw->id]);
}
public function adjustPhysicalWithdraw(Request $request, $id, AssetSummaryService $assetSummaryService)
{
$payload = $request->validate([
2026-07-08 17:58:55 +03:30
'items' => 'required|array|min:1',
'items.*.transaction_id' => 'required|integer|exists:transactions,id',
'items.*.value' => 'required|numeric|min:0.0001',
2026-07-08 11:25:19 +03:30
]);
2026-07-08 17:58:55 +03:30
$withdraw = Withdraw::with(['Transaction.product', 'customer'])->findOrFail($id);
2026-07-08 11:25:19 +03:30
if (!Withdraw::isPhysicalWithdrawNote($withdraw->note)) {
return response()->json(['message' => 'این سند برداشت فیزیکی نیست'], 422);
}
if ((int) $withdraw->sts !== 50) {
return response()->json(['message' => 'تغییر مقدار فقط در وضعیت «در انتظار ارسال» مجاز است'], 422);
}
$customer = Customer::findOrFail($withdraw->customer_id);
2026-07-08 17:58:55 +03:30
$transactionsById = $withdraw->Transaction->keyBy('id');
2026-07-08 11:25:19 +03:30
2026-07-08 17:58:55 +03:30
DB::transaction(function () use (
$payload,
$withdraw,
$customer,
$transactionsById,
$assetSummaryService
) {
foreach ($payload['items'] as $item) {
$transaction = $transactionsById->get((int) $item['transaction_id']);
if (!$transaction) {
throw new UnprocessableEntityHttpException('تراکنش به این سند تعلق ندارد');
}
$product = $transaction->product;
if (!$product) {
throw new UnprocessableEntityHttpException('محصول تراکنش یافت نشد');
}
$newValue = round((float) $item['value'], 6);
$currentUnits = round(abs((float) $transaction->gold), 6);
if (abs($newValue - $currentUnits) < 0.000001) {
continue;
}
2026-07-16 17:29:18 +03:30
$isCountBased = self::isCountBasedProduct(self::productUnitPayload($product));
$minValue = $isCountBased ? 1.0 : 0.001;
2026-07-08 17:58:55 +03:30
if ($newValue < $minValue) {
throw new UnprocessableEntityHttpException(
"حداقل مقدار برای {$product->title} برابر {$minValue} است"
);
}
2026-07-16 17:29:18 +03:30
if ($isCountBased && abs($newValue - round($newValue)) > 0.000001) {
2026-07-08 17:58:55 +03:30
throw new UnprocessableEntityHttpException(
"مقدار {$product->title} باید عدد صحیح باشد"
);
}
$delta = round($newValue - $currentUnits, 6);
$effectiveProductId = $assetSummaryService->effectiveProductWalletIdForCustomer(
(int) $customer->id,
(int) $customer->type,
(int) $product->id
);
$walletInterface = new WalletInterface(
(int) $customer->id,
(int) $customer->type,
null,
$effectiveProductId
);
$wallet = $walletInterface->wallet;
if (!$wallet) {
throw new UnprocessableEntityHttpException('کیف پول مشتری یافت نشد');
}
if ($delta > 0) {
$available = $assetSummaryService->productDisplayQuantity(
(int) $customer->id,
(int) $customer->type,
(int) $product->id
);
if ($delta > $available + 0.000001) {
throw new UnprocessableEntityHttpException(
"موجودی {$product->title} کافی نیست. حداکثر قابل افزودن: {$available}"
);
}
$wallet->gold -= $delta;
} else {
$wallet->gold += abs($delta);
}
$transaction->gold = -$newValue;
$transaction->number = (float) $product->amount * $newValue;
$transaction->balance_gold = $wallet->gold;
$wallet->save();
$transaction->save();
$walletInterface->set_hash();
2026-07-08 11:25:19 +03:30
}
2026-07-08 17:58:55 +03:30
});
2026-07-08 11:25:19 +03:30
$this->broadcastCustomerWalletAssets($customer);
return response()->json([
2026-07-08 17:58:55 +03:30
'message' => 'مقادیر برداشت به‌روزرسانی شد',
'withdraw' => $withdraw->fresh(['Transaction.product', 'customer']),
]);
}
public function physicalWithdrawAdjustInfo($id, AssetSummaryService $assetSummaryService)
{
$withdraw = Withdraw::with(['Transaction.product', 'customer'])->findOrFail($id);
if (!Withdraw::isPhysicalWithdrawNote($withdraw->note)) {
return response()->json(['message' => 'این سند برداشت فیزیکی نیست'], 422);
}
if ((int) $withdraw->sts !== 50) {
return response()->json(['message' => 'تغییر مقدار فقط در وضعیت «در انتظار ارسال» مجاز است'], 422);
}
$customer = $withdraw->customer;
if (!$customer) {
return response()->json(['message' => 'مشتری یافت نشد'], 422);
}
$lines = $withdraw->Transaction->map(function (Transaction $transaction) use (
$customer,
$assetSummaryService
) {
$product = $transaction->product;
$current = round(abs((float) $transaction->gold), 6);
$availableAdd = $product
? $assetSummaryService->productDisplayQuantity(
(int) $customer->id,
(int) $customer->type,
(int) $product->id
)
: 0.0;
2026-07-16 17:29:18 +03:30
$isCountBased = $product
? self::isCountBasedProduct(self::productUnitPayload($product))
: false;
2026-07-08 17:58:55 +03:30
return [
'transaction_id' => (int) $transaction->id,
'title' => trim((string) $transaction->title) ?: ($product->title ?? 'محصول'),
'current_value' => $current,
'available_add' => round((float) $availableAdd, 6),
'max_value' => round($current + (float) $availableAdd, 6),
2026-07-16 17:29:18 +03:30
'min_value' => $isCountBased ? 1 : 0.001,
'allows_decimal' => !$isCountBased,
2026-07-08 17:58:55 +03:30
'unit' => $product
2026-07-16 17:29:18 +03:30
? self::productQuantityUnit(self::productUnitPayload($product))
2026-07-08 17:58:55 +03:30
: 'گرم',
];
})->values();
return response()->json([
'withdraw_id' => (int) $withdraw->id,
'customer_name' => (string) ($customer->name ?? ''),
'customer_mobile' => (string) ($customer->mobile ?? ''),
'lines' => $lines,
2026-07-08 11:25:19 +03:30
]);
}
2026-01-24 14:03:11 +00:00
public function completeWithdraw(Request $request)
{
2026-06-09 20:08:06 +03:30
$request->validate([
'id' => 'required|integer|exists:withdraws,id',
'tracked_number' => 'required|string|max:50',
'date_at' => 'nullable|date',
]);
$data = $request->only(['tracked_number', 'date_at']);
2026-01-24 14:03:11 +00:00
$data['user_id'] = auth('user')->id();
$data['sts'] = 2;
2026-06-09 20:08:06 +03:30
$data['tracked_number'] = (string) $data['tracked_number'];
if (empty($data['date_at'])) {
$data['date_at'] = now();
}
$withdraw = Withdraw::findOrFail($request->id);
2026-01-24 14:03:11 +00:00
$withdraw->update($data);
// $tahesab = new TahesabController();
// $tahesab->wallet([
// 'customer_id' => $withdraw->customer_id,
// 'value' => $withdraw->cash,
// 'direction' => 1,
// 'note' => 'برداشت از کیف پول شماره ' . $withdraw->id,
// ]);
return response()->json(true);
}
2026-06-25 23:06:29 +03:30
private function recordAccountingTransaction(
Customer $customer,
Withdraw $withdraw,
int $adminUserId,
float $cash = 0,
float $gold = 0,
?string $note = null
): void {
$transaction = Transaction::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' => $transaction?->id,
'cash' => $cash,
'note' => $note ?? $withdraw->note,
'type' => 1,
'gold' => $gold,
'balance_cash' => 0,
'withdraw_id' => $withdraw->id,
]);
}
2026-06-27 15:51:44 +03:30
private function broadcastCustomerWalletAssets(Customer $customer): void
{
app(WalletBroadcastService::class)->broadcastForCustomer(
(int) $customer->id,
(int) $customer->type
);
}
2026-01-24 14:03:11 +00:00
}