= 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( 'همین سند لحظاتی پیش برای این مشتری ثبت شده است. اگر تکرارش عمدی است یک دقیقه صبر کنید.' ); } } public function charge2(Request $request) { $data = $request->all(); $customer = Customer::findOrFail($data['customer_id']); $deposit = false; if ($data['value'] > 0) { $deposit = true; } $wallet_interface = new WalletInterface($data['customer_id'], (int) $customer->type); $wallet = $wallet_interface->wallet; 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); try { app(PargarSyncService::class)->queueWalletDocument( $customer, 'cash', (float) $data['value'], [ 'withdraw_id' => $Withdraw->id, 'source' => 'admin_charge2', ] ); } catch (\Throwable $e) { report($e); } $message = 'کیف پول شما به مبلغ ' . self::formatSmsRialAmount($data['value']) . " ریال در تاریخ " . verta()->format('Y/n/j H:i:s') . " شارژ شد " . "\n" . $this->Config('name_fa'); $this->Send($customer->mobile, $message); try { app(\App\Services\CustomerAppNotifyService::class)->notifyCustomer( (int) $customer->id, 'واریز به کیف پول', 'کیف پول شما به مبلغ ' . self::formatSmsRialAmount($data['value']) . ' ریال شارژ شد.', 'success' ); } catch (\Throwable $e) { report($e); } 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; } $wallet_interface = new WalletInterface($data['customer_id'], (int) $customer->type); $wallet = $wallet_interface->wallet; if (is_null($wallet) && !$deposit) { throw new UnprocessableEntityHttpException('مقدار موجودی کمتر از درخواست شماست'); } if (!is_null($wallet) && $wallet->cash < abs($data['value']) && !$deposit) { throw new UnprocessableEntityHttpException('مقدار موجودی کمتر از درخواست شماست'); } $this->assertNotDuplicateCharge((int) $customer->id, (float) $data['value'], 'cash'); $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, ]); $type = $deposit ? 3 : 4; new TransactionInterface($wallet_interface, $_inp = [ 'gold' => 0, 'cash' => $data['value'], 'expire_at' => now(), "note" => $data['note'], 'withdraw_id' => $Withdraw->id, 'active' => true ], $type, true, $deposit, $gold_block); $this->recordAccountingTransaction($customer, $Withdraw, (int) $user->id, (float) $data['value'], 0, $data['note'] ?? null); if (app(ConfigService::class)->isTahesabAccounting()) { try { $data['direction'] = $deposit ? 0 : 1; $data['withdraw_id'] = $Withdraw->id; app(TahesabController::class)->wallet($data); } catch (\Exception $e) { report($e); } } 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); } } 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); } //send sms to users and customer $status = $data['value'] > 0 ?" شارژ شد ": " کسر شد "; if (self::isSmsNotifEnabled('charge_notif')) { $message = 'کیف پول کاربر ' . $customer->name . ' به مقدار ' . self::formatSmsRialAmount($data['value']) . " ریال در تاریخ " . 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, $data) { $this->send($admin->mobile, $message); }); $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); } 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); } $this->broadcastCustomerWalletAssets($customer); 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; } $wallet_interface = new WalletInterface($data['customer_id'], (int) $customer->type); $wallet = $wallet_interface->wallet; // کسر دستی نباید موجودی طلا را منفی کند؛ شارژ مثبت بدون هیچ شرطی باز است if (!$deposit) { $available = is_null($wallet) ? 0.0 : (float) $wallet->gold; $requested = abs((float) $data['value']); // ادمین عدد را از نمایش سه‌رقمی می‌خواند؛ اختلاف تا نصف پلهٔ نمایش یعنی «کل موجودی» if ($requested > $available && $requested - $available <= self::GOLD_DISPLAY_STEP) { $requested = $available; $data['value'] = -$available; } if ($requested <= 0 || $requested > $available) { throw new UnprocessableEntityHttpException('مقدار موجودی طلا کمتر از درخواست شماست'); } } $this->assertNotDuplicateCharge((int) $customer->id, (float) $data['value'], 'gold'); $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, ]); $representative = app(ConfigService::class)->mainWalletRepresentativeProduct(); new TransactionInterface($wallet_interface, $_inp = [ 'cash' => 0, 'gold' => $data['value'], 'expire_at' => now(), "note" => $data['note'], 'withdraw_id' => $Withdraw->id, 'product_id' => $representative?->id, 'active' => true ], 7, $deposit, false, $gold_block); $this->recordAccountingTransaction($customer, $Withdraw, (int) $user->id, 0, (float) $data['value'], $data['note'] ?? null); //send to tahesab if (app(ConfigService::class)->isTahesabAccounting()) { try { $data['direction'] = $deposit ? 1 : 0; $data['withdraw_id'] = $Withdraw->id; app(TahesabController::class)->walletGold($data); } catch (\Exception $e) { report($e); } } if (app(ConfigService::class)->isZarinAccounting()) { try { $data['direction'] = $deposit ? 1 : 0; $data['withdraw_id'] = $Withdraw->id; app(\App\Http\Controllers\Admin\ZarinController::class)->walletGold($data); } catch (\Exception $e) { report($e); } } 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); } //send sms to users and customer $status = $data['value'] > 0 ?" شارژ شد ": " کسر شد "; $weightUnit = self::tradeWeightUnit(); if (self::isSmsNotifEnabled('charge_notif')) { $message = 'کیف پول کاربر ' . $customer->name . ' به مقدار ' . self::formatSmsGoldWeight($data['value']) . " {$weightUnit} در تاریخ " . 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, $data) { $this->send($admin->mobile, $message); }); $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); } 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); } $this->broadcastCustomerWalletAssets($customer); return response()->json(true); } /** گزینه‌های شارژ کیف پول — نقدی + همهٔ محصولات (فعال و غیرفعال) */ public function chargeTargets() { $configService = app(ConfigService::class); $mainWalletProductIds = $configService->mainWalletProductIds(); $balanceReport = app(BalanceReportService::class); $targets = [ [ 'key' => 'money', 'kind' => 'money', 'label' => 'نقدی (ریال)', 'unit' => 'ریال', 'active' => true, ], ]; $products = Product::query() ->orderByDesc('active') ->orderBy('metal_id') ->orderBy('id') ->get(['id', 'title', 'unit', 'metal_id', 'group_type', 'trade_by_quantity', 'amount', 'direct_price', 'active']); foreach ($products as $product) { $usesMainWallet = in_array((int) $product->id, $mainWalletProductIds, true); $label = (string) $product->title; if ($usesMainWallet) { $label .= ' (کیف اصلی)'; } if (!(bool) $product->active) { $label .= ' (غیرفعال)'; } $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, 'group_type' => (string) ($product->group_type ?? ''), 'active' => (bool) $product->active, ]; } 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']); $deposit = ((float) ($data['value'] ?? 0)) > 0; $effectiveProductId = Controller::effectiveProductWalletId($productId); $wallet_interface = new WalletInterface( (int) $data['customer_id'], (int) $customer->type, null, $effectiveProductId ); $wallet = $wallet_interface->wallet; if (!$deposit && $wallet !== null && (float) $wallet->gold < abs((float) $data['value'])) { throw new UnprocessableEntityHttpException('مقدار موجودی کمتر از درخواست شماست'); } $this->assertNotDuplicateCharge((int) $customer->id, (float) $data['value'], 'gold'); $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); $this->recordAccountingTransaction( $customer, $Withdraw, (int) $user->id, 0, (float) $data['value'], ($data['note'] ?? null) ?: ('شارژ ' . $product->title) ); if (app(ConfigService::class)->isTahesabAccounting()) { try { $data['direction'] = $deposit ? 1 : 0; $data['withdraw_id'] = $Withdraw->id; app(TahesabController::class)->walletGold($data); } catch (\Exception $e) { report($e); } } if (app(ConfigService::class)->isZarinAccounting()) { try { $data['direction'] = $deposit ? 1 : 0; $data['withdraw_id'] = $Withdraw->id; app(\App\Http\Controllers\Admin\ZarinController::class)->walletGold($data); } catch (\Exception $e) { report($e); } } 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); } $status = $deposit ? ' شارژ شد ' : ' کسر شد '; $formatted = self::formatSmsGoldWeight($data['value']); if (self::isSmsNotifEnabled('charge_notif')) { $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); }); $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); } 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); } $this->broadcastCustomerWalletAssets($customer); return response()->json(true); } private function resolveProductChargeUnit(Product $product): string { $payload = Controller::productUnitPayload($product); if ( Controller::usesQuantityTrade($payload) && !Controller::isWeightBasedProductUnit($payload['unit'] ?? null) ) { $unit = trim((string) ($payload['unit'] ?? '')); return $unit !== '' ? $unit : 'عدد'; } if (Controller::isWeightBasedProductUnit($payload['unit'] ?? null)) { return trim((string) $payload['unit']); } return Controller::productStorageWeightUnit($product); } public function index(Request $request) { $balanceReport = app(BalanceReportService::class); $balanceService = app(WalletBalanceService::class); $catalogMetalIds = $balanceService->catalogMetalIds(); $records = Transaction::with([ 'customer', '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']); }, ])->whereNull('fund_id'); 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); if (isset($records['rows'])) { $records['rows'] = collect($records['rows'])->map(function ($row) use ($balanceReport, $catalogMetalIds) { $balanceReport->enrichTransactionTableRow($row, $catalogMetalIds); 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(); } $records['catalogMetals'] = $balanceReport->catalogMetalsMeta(); 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 = 'تراکنش با موفقیت به‌روزرسانی شد.'; $customer = Customer::find($transaction->customer_id); if ($customer) { $this->broadcastCustomerWalletAssets($customer); } return response()->json([ 'message' => $message, 'transaction' => $transaction, // 'previous_amount' => $previousAmount, // 'new_amount' => $newAmount, 'difference' => $difference, 'wallet' => $wallet_interface->wallet, ]); } public function indexWallet(Request $request) { $lastPrice = (float) (Price::latest()->first()?->price ?? 0); $balanceService = app(WalletBalanceService::class); $catalogMetalIds = $balanceService->catalogMetalIds(); $mainWalletMetalId = $balanceService->resolveMainWalletMetalId(); // همان فیلتر/جستجوی جدول — بدون صفحه‌بندی — برای مجموع ردیف‌های قابل مشاهده $sumQuery = Wallet::query(); TableEngine::create($sumQuery, $request->all()) ->initSearch(['customer.name']); $walletTableWith = [ 'customer:id,name,mobile', 'fund:id,title', 'product' => static function ($q) { $q->withTrashed()->select(['id', 'metal_id', 'unit', 'amount', 'group_type', 'trade_by_quantity', 'direct_price', 'title']); }, ]; $sumRows = (clone $sumQuery)->with($walletTableWith)->get(); $metalBalances = $balanceService->buildWalletMetalSums($sumRows, $catalogMetalIds); $totalGold = array_sum(array_column($metalBalances, 'gold')); $totalGoldToCash = array_sum(array_column($metalBalances, 'goldToCash')); $totalCash = (float) (clone $sumQuery)->sum('cash'); $sums = [ 'gold' => round($totalGold, 2), 'goldToCash' => round($totalGoldToCash, 2), 'cash' => round($totalCash, 2), 'cashToGold' => $lastPrice > 0 ? round($totalCash / $lastPrice, 2) : 0, 'metalBalances' => $metalBalances, ]; $records = Wallet::with($walletTableWith); TableEngine::create($records, $request->all()) ->initSearch(['customer.name', 'customer.mobile', 'product.title', 'fund.title']) ->select(['*']) ->export($records); if (isset($records['rows'])) { foreach ($records['rows'] as &$row) { $balanceService->enrichWalletTableRow($row, $catalogMetalIds, $mainWalletMetalId); } unset($row); } $records['sums'] = $sums; return response()->json($records); } public function indexWithdraw(Request $request) { $records = Withdraw::with(['customer', 'bank_account', 'address', "Transaction"]); Withdraw::applyFlowFilter($records, $request->input('flow_type', 'all')); TableEngine::create($records, $request->all()) ->initSearch(['customer.name']) ->select([ '*', Withdraw::$SELECT_STS_STR, Withdraw::$SELECT_DAJ ])->export($records); if (app(ConfigService::class)->isTahesabAccounting() && isset($records['rows']) && is_array($records['rows'])) { $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); } } 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); } } 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'); } public function updateWithdraw(Request $request, $id) { $withdraw = Withdraw::with(['Transaction.product', 'customer'])->find($id); if (!$withdraw) { return response()->json(['error' => 'Withdraw not found'], 404); } $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; $withdraw->save(); if ($withdraw->customer) { $this->broadcastCustomerWalletAssets($withdraw->customer); } return response()->json(['message' => 'Withdraw updated successfully', 'withdraw' => $withdraw]); } /** * برگشت موجودی کسرشدهٔ برداشت فیزیکی هنگام لغو (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); } } public function getWithdraw(Request $request, $id) { $withdraw = Withdraw::with(['customer', 'bank_account', 'address', "Transaction"])->find($id); if (!$withdraw) { return response()->json(['error' => 'Withdraw not found'], 404); } return response()->json($withdraw); } public function indexPhysicalWithdraw(Request $request) { $records = Withdraw::with(['customer', 'address', 'Transaction.product']); 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, 'unit' => self::productQuantityUnit( self::productUnitPayload($product) ), '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; }); $this->broadcastCustomerWalletAssets($customer); return response()->json(['id' => $withdraw->id]); } public function adjustPhysicalWithdraw(Request $request, $id, AssetSummaryService $assetSummaryService) { $payload = $request->validate([ 'items' => 'required|array|min:1', 'items.*.transaction_id' => 'required|integer|exists:transactions,id', 'items.*.value' => 'required|numeric|min:0.0001', ]); $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 = Customer::findOrFail($withdraw->customer_id); $transactionsById = $withdraw->Transaction->keyBy('id'); 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; } $isCountBased = self::isCountBasedProduct(self::productUnitPayload($product)); $minValue = $isCountBased ? 1.0 : 0.001; if ($newValue < $minValue) { throw new UnprocessableEntityHttpException( "حداقل مقدار برای {$product->title} برابر {$minValue} است" ); } if ($isCountBased && abs($newValue - round($newValue)) > 0.000001) { 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(); } }); $this->broadcastCustomerWalletAssets($customer); return response()->json([ '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; $isCountBased = $product ? self::isCountBasedProduct(self::productUnitPayload($product)) : false; 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), 'min_value' => $isCountBased ? 1 : 0.001, 'allows_decimal' => !$isCountBased, 'unit' => $product ? self::productQuantityUnit(self::productUnitPayload($product)) : 'گرم', ]; })->values(); return response()->json([ 'withdraw_id' => (int) $withdraw->id, 'customer_name' => (string) ($customer->name ?? ''), 'customer_mobile' => (string) ($customer->mobile ?? ''), 'lines' => $lines, ]); } public function completeWithdraw(Request $request) { $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']); $data['user_id'] = auth('user')->id(); $data['sts'] = 2; $data['tracked_number'] = (string) $data['tracked_number']; if (empty($data['date_at'])) { $data['date_at'] = now(); } $withdraw = Withdraw::findOrFail($request->id); $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); } 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, ]); } private function broadcastCustomerWalletAssets(Customer $customer): void { app(WalletBroadcastService::class)->broadcastForCustomer( (int) $customer->id, (int) $customer->type ); } }