id(); $records = Withdraw::query() ->with(['transaction']) ->where('customer_id', $customerId); Withdraw::scopeDeposits($records); $records->where(function ($q) { $q->where('cash', '>', 0) ->orWhere('gold', '>', 0); })->orderByDesc('id'); TableEngine::create($records, $request->all()) ->initSearch(['customer.name', 'note']) ->select([ '*', Withdraw::$SELECT_STS_STR, Withdraw::$SELECT_DAJ, ])->export($records); foreach ($records['rows'] as &$record) { $this->applyWithdrawDepositRowPresentation($record); } unset($record); return response()->json($records); } private function applyWithdrawDepositRowPresentation($record): void { $record->title = $this->resolveWithdrawDepositListTitle($record); $record->expire_at = $record->date_at ?? $record->created_at; $record->type = 3; $record->unit = 'ریال'; $sts = (int) ($record->sts ?? 0); $record->sts_str = Withdraw::GetSts($sts) ?? (string) $sts; } private function resolveWithdrawDepositListTitle($record): string { $note = trim((string) ($record->note ?? '')); if ($note !== '') { return $note; } return match ((int) ($record->sts ?? 0)) { 4 => self::rialDepositTitle(), 5 => 'پاداش معرفی', 9 => 'سود زیر مجموعه', default => self::rialDepositTitle(), }; } public function walletMe(WalletBalanceService $balanceService) { $customer = auth('customer')->user(); $customerId = (int) $customer->id; $ownerType = (int) $customer->type; $wallets = Wallet::query() ->where('owner', $customerId) ->where('owner_type', $ownerType) ->get(); $mainWallet = $wallets->first( fn (Wallet $row) => $row->fund_id === null && $row->product_id === null ); return response()->json([ 'owner' => $customerId, 'id' => $mainWallet?->id, 'gold' => $balanceService->mainWalletGold($wallets), 'cash' => $balanceService->mainWalletCash($wallets), 'gold_sum' => round($balanceService->totalGoldWeight($wallets), 2), ]); } public function assetsSummary(AssetSummaryService $assetSummaryService) { $user = auth('customer')->user(); return response()->json( $assetSummaryService->buildForCustomer((int) $user->id, (int) $user->type) ); } public function priceData(Request $request, ZaarboxPriceService $zaarboxPriceService) { return response()->json([ 'code' => 0, 'message' => 'عملیات با موفقیت انجام شد.', 'data' => [ 'prices' => $zaarboxPriceService->getAllPrices(), ], ]); } public function transactionTable(Request $request) { $customerId = (int) auth('customer')->id(); $this->repairMisclassifiedRialTransactions($customerId); $mainWalletProductIds = app(ConfigService::class)->mainWalletProductIds(); $walletIds = $this->resolveTransactionTableWalletIds($customerId, $request, $mainWalletProductIds); if ($walletIds->isEmpty()) { $records = ['query' => ['total' => 0], 'rows' => []]; } else { $records = Transaction::with([ 'order' => function ($q) { $q->select(['*', Order::$SELECT_STS_STR]); }, 'withdraw' => function ($q) { $q->select(['*', Withdraw::$SELECT_STS_STR]); }, 'wallet.product', 'product', ])->whereIn('wallet', $walletIds)->whereNull('fund_id'); TableEngine::create($records, $request->all()) ->initSearch(['customer.name']) ->select([ '*', Transaction::$SELECT_CAJ, Transaction::$SELECT_TYPE_STR, Transaction::$SELECT_SAJ, Transaction::$SELECT_EAJ ])->export($records); } foreach ($records['rows'] as &$record) { if ($this->isShopGoldTransaction($record)) { $this->applyShopTransactionPresentation($record); $this->applyAbshodeProductTransactionTitle($record); $this->applyMetalTypeTransactionTitle($record); $this->applyProductTransactionTitle($record); $this->applyProductOrderDisplayFields($record); $this->applyProductTransactionUnit($record); self::normalizeRialTransactionPresentation($record); continue; } // بقیه رکوردها فقط اگر title ندارند یا تعریف نشده هستند اصلاح شوند if (empty($record->title) || $record->title == "تعریف نشده") { $record->title = $record->type_str; $record->unit = "گرم"; if ((int) ($record->type ?? 0) === 22) { $record->title = 'تعدیل حسابداری پرگار'; $record->unit = 'گرم'; } elseif ((int) ($record->type ?? 0) === 12) { $record->title = 'برداشت وجه نقد'; $record->unit = 'ریال'; } elseif (isset($record->wallet)) { $wallet = Wallet::with('product')->where('id', $record->wallet)->first(); if (isset($wallet->product)) { $direction = $record->order->direction ?? null; if ((int) $direction === 1) { $record->title = self::productBuyTitle($wallet->product->title); } elseif ((int) $direction === 2) { $record->title = self::productSellTitle($wallet->product->title); } else { $record->title = ($record->gold > 0 ? self::productBuyTitle($wallet->product->title) : self::productSellTitle($wallet->product->title)); } $record->unit = self::productQuantityUnit(self::productUnitPayload($wallet->product)); } else { $record->title = $this->resolveMainWalletTransactionTitle($record); } } else { $record->title = $record->type_str; } if (!empty($record->title) && $record->title !== 'تعریف نشده') { $record->save(); } } $this->applyAbshodeProductTransactionTitle($record); $this->applyMetalTypeTransactionTitle($record); $this->applyProductTransactionTitle($record); $this->applyProductOrderDisplayFields($record); $this->applyProductTransactionUnit($record); self::normalizeRialTransactionPresentation($record); } $records['rows'] = $this->serializeTransactionRowsForApi($records['rows']); return response()->json($records); } /** * کیف(های) مؤثر برای جدول تراکنش. * محصولات main_wallet_products: تراکنش‌ها روی کیف اصلی (+ کیف legacy محصول) ثبت شده‌اند. * * @return \Illuminate\Support\Collection */ private function resolveTransactionTableWalletIds( int $customerId, Request $request, array $mainWalletProductIds ) { $requestedWalletId = $request->input('wallet_id'); $requestedProductId = (int) $request->input('product_id'); if ($requestedWalletId) { $requestedWallet = Wallet::query() ->where('id', $requestedWalletId) ->where('owner', $customerId) ->first(); if (!$requestedWallet) { return collect(); } if ( $mainWalletProductIds !== [] && ( ($requestedWallet->product_id && self::isMainWalletProduct((int) $requestedWallet->product_id)) || ( $requestedWallet->product_id === null && $requestedWallet->fund_id === null && $requestedProductId > 0 && self::isMainWalletProduct($requestedProductId) ) ) ) { return $this->mainWalletProductTransactionWalletIds($customerId, $mainWalletProductIds); } return collect([(int) $requestedWallet->id]); } if ( $mainWalletProductIds !== [] && $requestedProductId > 0 && self::isMainWalletProduct($requestedProductId) ) { return $this->mainWalletProductTransactionWalletIds($customerId, $mainWalletProductIds); } return Wallet::query() ->where('owner', $customerId) ->pluck('id'); } /** * کیف اصلی + کیف‌های legacy محصولات متصل به کیف اصلی. * * @return \Illuminate\Support\Collection */ private function mainWalletProductTransactionWalletIds(int $customerId, array $mainWalletProductIds) { return Wallet::query() ->where('owner', $customerId) ->whereNull('fund_id') ->where(function ($query) use ($mainWalletProductIds) { $query->whereNull('product_id') ->orWhereIn('product_id', $mainWalletProductIds); }) ->pluck('id') ->map(fn ($id) => (int) $id) ->unique() ->values(); } /** * دادهٔ قدیمی: type=4 با cash مثبت (واریز ادمین) — قبل از فیلتر جدول اصلاح می‌شود. */ private function repairMisclassifiedRialTransactions(int $customerId): void { if ($customerId <= 0) { return; } Transaction::query() ->where('customer_id', $customerId) ->whereNull('fund_id') ->where('type', 4) ->where('cash', '>', 0) ->update([ 'type' => 3, 'title' => self::rialDepositTitle(), ]); Transaction::query() ->where('customer_id', $customerId) ->whereNull('fund_id') ->where('type', 3) ->where('cash', '<', 0) ->update([ 'type' => 4, 'title' => self::rialWithdrawTitle(), ]); } public function transactionTableb(Request $request) { $wallet = Wallet::with('product')->where('owner', auth('customer')->id())->whereNull('fund_id')->get(); if (is_null($wallet)) { $records = ['query' => ['total' => 0], 'rows' => []]; } else { $records = Transaction::with(['order' => function ($q) { return $q->select(['*', Order::$SELECT_STS_STR]); }, 'withdraw' => function ($q) { return $q->select(['*', Order::$SELECT_STS_STR]); }, 'wallet.product'])->whereIn('wallet', $wallet->pluck('id'))->whereNull('fund_id'); TableEngine::create($records, $request->all()) ->initSearch(['customer.name']) ->select([ '*', Transaction::$SELECT_CAJ, Transaction::$SELECT_TYPE_STR, Transaction::$SELECT_SAJ, Transaction::$SELECT_EAJ ])->export($records); } foreach ($records['rows'] as &$record) { return $record['wallet']; if (isset($record->wallet->product->title)) { $record->title = $record->wallet->product->title."22"; $record->save(); } else { $record->title = $record->type_str."22"; $record->save(); } } return response()->json($records); } public function withdraw(Request $request) { $customer = auth('customer')->user(); $data = $request->all(); if (!isset($data['card_id'])) { return response()->json(['message' => 'لطفا کارت بانکی را وارد کنید'], 422); } $bCart = BankAccount::where('id', $data['card_id'])->first(); if ($bCart->sts != 2) { return response()->json(['message' => 'کارت انتخاب شده نا معتبر است'], 422); } $wallet_interface_fund = new WalletInterface(auth('customer')->id(), auth('customer')->user()->type); if ($data['value'] < 0) { throw new UnprocessableEntityHttpException('مقدار برداشت باید مثبت باشد'); } $this->validateWithdrawAmount((int) $data['value']); $extraNote = trim((string) ($data['extra_note'] ?? '')); if (strlen($extraNote) > 500) { return response()->json(['message' => 'توضیحات اضافی حداکثر ۵۰۰ کاراکتر است'], 422); } $Withdraw = Withdraw::create([ 'customer_id' => auth('customer')->id(), "cash" => $data['value'], "note" => "برداشت ریالی", 'extra_note' => $extraNote !== '' ? $extraNote : null, "bank_account_id" => $data['card_id'], 'sts' => 1, 'type' => 2 ]); $transaction = new TransactionInterface($wallet_interface_fund, $_inp = [ "cash" => $data['value'], 'expire_at' => now(), 'withdraw_id' => $Withdraw->id, 'active' => true ], 4, false, false); $userMobile = $customer->mobile; // User's mobile number $userName = $customer->name; // User's name $withdrawAmount = $data['value']; // amount if (app(ConfigService::class)->isTahesabAccounting()) { try { $result = app(TahesabController::class)->wallet([ "customer_id" => auth('customer')->id(), "value" => $data['value'], "direction" => 1, ]); \Log::info('Tahesab withdraw result', [ 'customer_id' => auth('customer')->id(), 'value' => $data['value'], 'result' => $result, ]); } catch (\Exception $e) { report($e); } } $companyName = $this->Config('name_fa'); // Company name from configuration $message = "کاربر $userName با شماره موبایل $userMobile درخواست برداشت مبلغ " . number_format($withdrawAmount) . " ریال را دارد. با تشکر، $companyName"; if (isset($request->otp)) { if (Customer::where("id", $customer->id)->first()->token_sms != $request->otp) { return response()->json(['message' => 'رمز دوم اشتباه میباشد'], 422); } } User::where('send_sms', 1)->get()->each(function ($admin) use ($message) { if (self::isSmsNotifEnabled('sms_notif_withdraw_admin')) { $this->Send($admin->mobile, $message . ' ' . now()->format('H:i:s')); } }); return response()->json($Withdraw); } private function formatDeliveryQuantity(float $value): string { $rounded = round($value, 3); if (fmod($rounded, 1.0) === 0.0) { return number_format($rounded, 0); } return rtrim(rtrim(number_format($rounded, 3, '.', ''), '0'), '.'); } public function withdraw_physical(Request $request) { $data = $request->all(); $data['customer'] = Customer::with([/*"wallet"*/])->where('id', auth('customer')->id())->first(); $data['customer']['wallet_interface'] = new WalletInterface(auth('customer')->id(), auth('customer')->user()->type); if (!isset($data['address_id'])) { return response()->json(['message' => 'آدرس انتخاب نشده است'], 422); } $data['adress'] = Address::find($data['address_id']); if (!$data['adress'] || $data['adress']->customer_id != $data['customer']->id) { return response()->json(['message' => 'آدرس معتبر نیست یا متعلق به شما نمی‌باشد'], 403); } $assetSummaryService = app(AssetSummaryService::class); foreach ($data['wallets'] as $key => $wallet) { $data['wallets'][$key]['wallet'] = Wallet::with(["product"])->find($wallet['wallet_id']); if (!$data['wallets'][$key]['wallet']) { return response()->json(['message' => 'ولت مورد نظر یافت نشد.'], 422); } if ($data['wallets'][$key]['wallet']->owner != $data['customer']['id']) { return response()->json(['message' => 'ولت انتخاب شده متعلق به شما نمی‌باشد.'], 422); } // بررسی وجود محصول در ولت if (!$data['wallets'][$key]['wallet']['product']) { return response()->json(['message' => 'محصولی در ولت مورد نظر یافت نشد.'], 422); } $productId = (int) $data['wallets'][$key]['wallet']['product_id']; $customerId = (int) $data['customer']['id']; $ownerType = (int) auth('customer')->user()->type; // موجودی و کیف مؤثر — همسان با assets-summary و صفحهٔ اصلی $effectiveProductId = $assetSummaryService ->effectiveProductWalletIdForCustomer($customerId, $ownerType, $productId); $availableGold = $assetSummaryService ->productDisplayQuantity($customerId, $ownerType, $productId); // گیت تحویل فیزیکی per-product — سوییچ محصول و حداقل مقدار (به واحد خود محصول) $walletProduct = $data['wallets'][$key]['wallet']['product']; if (Product::hasPhysicalDeliveryColumns() && !$walletProduct->physicalDeliveryEnabled()) { return response()->json([ 'message' => "امکان تحویل فیزیکی برای {$walletProduct['title']} وجود ندارد" ], 422); } $minDeliveryQuantity = Product::hasPhysicalDeliveryColumns() ? $walletProduct->minPhysicalDeliveryQuantity() : 0.0; if ($minDeliveryQuantity > 0) { // موجودی و حداقل هر دو با واحد خود محصول سنجیده می‌شوند: // تعدادی → عدد، وزنی → گرم/کیلوگرم (همسان با inventory_cards) $card = $assetSummaryService->productInventoryCard($customerId, $ownerType, $productId); $unitLabel = $card['unit'] ?? Controller::productQuantityUnit( Controller::productUnitPayload($walletProduct) ); if ($availableGold + 0.0001 < $minDeliveryQuantity) { $remaining = max(0, $minDeliveryQuantity - $availableGold); return response()->json([ 'message' => $this->formatDeliveryQuantity($remaining) . " {$unitLabel} مانده تا رسیدن به حداقل تحویل فیزیکی " . "{$walletProduct['title']}" ], 422); } } if ($data['wallets'][$key]['value'] > $availableGold){ return response()->json([ 'message' => "شما به اندازه کافی از {$data['wallets'][$key]['wallet']['product']['title']} ندارید" ], 422); } if ($data['wallets'][$key]['value'] == 0) { return response()->json([ 'message' => "مقدار انتخابی نمی‌تواند صفر باشد. لطفاً مقدار صحیحی را برای {$data['wallets'][$key]['wallet']['product']['title']} وارد کنید." ], 422); } $requestValue = (float) $data['wallets'][$key]['value']; if (Controller::isCountBasedProduct(Controller::productUnitPayload($walletProduct))) { if (abs($requestValue - round($requestValue)) > 0.000001) { return response()->json([ 'message' => "برای {$walletProduct['title']} فقط تعداد صحیح مجاز است" ], 422); } $data['wallets'][$key]['value'] = (int) round($requestValue); } $data['wallets'][$key]['wallet_interface'] = new WalletInterface(auth('customer')->id(), auth('customer')->user()->type, null, $effectiveProductId); } $data['withdraw'] = new Withdraw(); // Create a new instance of the Withdraw model $data['withdraw']->note = "برداشت فیزیکی"; // 50 for phisicals $data['withdraw']->address_id = $data['address_id']; $data['withdraw']->customer_id = $data['customer']['id']; $data['withdraw']->sts = 50; $data['withdraw']->save(); foreach ($data['wallets'] as $key => $wallet) { $data['wallets'][$key]['transaction'][] = new TransactionInterface($data['wallets'][$key]['wallet_interface'], $_inp = [ "gold" => $data['wallets'][$key]['value'], "number" => $data['wallets'][$key]['wallet']['product']['amount']*$data['wallets'][$key]['value'], "product_id" => $data['wallets'][$key]['wallet']['product']['id'], 'expire_at' => now(), 'active' => true, 'title' => " برداشت {$data['wallets'][$key]['wallet']['product']['title']}", 'note' => " برداشت محصول {$data['wallets'][$key]['wallet']['product']['title']} شماره سند {$data['withdraw']->id}", 'withdraw_id' => $data['withdraw']->id, ], 6, false); } return response()->json($data['withdraw']['id']); } public function indexWithdraw(Request $request){ $records = Withdraw::with(['customer','bank_account','Address',"Transaction"])->where("customer_id",auth('customer')->id()); TableEngine::create($records, $request->all()) ->initSearch(['customer.name']) ->select([ '*',Withdraw::$SELECT_STS_STR,Withdraw::$SELECT_DAJ ])->export($records); return response()->json($records); } public function index_withdraw_physical(Request $request){ $records = Withdraw::with(['customer','bank_account','Address',"Transaction.product.file"])->where("customer_id",auth('customer')->id())->whereBetween('sts', [49, 70]); TableEngine::create($records, $request->all()) ->initSearch(['customer.name']) ->select([ '*',Withdraw::$SELECT_STS_STR,Withdraw::$SELECT_DAJ ])->export($records); return response()->json($records); } public function withdrawOtp(Request $request) { $data = $request->all(); if (!isset($data['value'])) { return response()->json(['message' => 'مبلغ برداشت مشخص نشده است'], 422); } if ($data['value'] < 0) { throw new UnprocessableEntityHttpException('مقدار برداشت باید مثبت باشد'); } $this->validateWithdrawAmount((int) $data['value']); $customer = auth('customer')->user(); $otp = rand(1000, 9999); $customer->token_sms = $otp; $customer->save(); if (self::isSmsNotifEnabled('sms_notif_withdraw_otp')) { $this->Send( $customer->mobile, 'رمز عبور یکبار مصرف شما جهت برداشت پول از اپلیکیشن ' . $this->Config('name_fa') . ' می باشد: ' . $otp ); } return response()->json(true); } private function validateWithdrawAmount(int $value): void { $wallet_interface_fund = new WalletInterface(auth('customer')->id(), auth('customer')->user()->type); $wallet = $wallet_interface_fund->wallet; if ($value > $wallet->cash) { throw new UnprocessableEntityHttpException('مقدار برداشت بیشتر از موجودی است'); } $min_withdraw = Setting::where('is_active', 1) ->where('key', 'min_withdraw') ->first(); if ($min_withdraw && (int) $min_withdraw->value > $value) { throw new UnprocessableEntityHttpException('حداقل مقدار برداشت ' . number_format((int) $min_withdraw->value) . ' ریال است'); } } private function applyAbshodeProductTransactionTitle($record): void { if (self::isCashTransactionTitle($record->title ?? null)) { return; } $product = $this->resolveTransactionProductForLedgerTitle($record); if (!$product || trim((string) $product->title) === '') { return; } $productTitle = trim((string) $product->title); $direction = (int) ($record->order->direction ?? 0); if ($direction === 1 || ($direction === 0 && (float) ($record->gold ?? 0) > 0)) { $record->title = self::productBuyTitle($productTitle); return; } if ($direction === 2 || ($direction === 0 && (float) ($record->gold ?? 0) < 0)) { $record->title = self::productSellTitle($productTitle); } } private function resolveAbshodeProductId($record): ?int { if (!empty($record->product_id)) { return (int) $record->product_id; } $noteProductId = $this->productIdFromTransactionNote($record->note ?? null); if ($noteProductId) { return $noteProductId; } $wallet = $this->resolveTransactionWallet($record); if ($wallet && !empty($wallet->product_id)) { return (int) $wallet->product_id; } if (empty($record->order_id)) { return null; } $siblingProductId = Transaction::query() ->where('order_id', $record->order_id) ->whereNotNull('product_id') ->where('product_id', '>', 0) ->value('product_id'); if ($siblingProductId) { return (int) $siblingProductId; } $siblingNotes = Transaction::query() ->where('order_id', $record->order_id) ->whereNotNull('note') ->pluck('note'); foreach ($siblingNotes as $note) { $productId = $this->productIdFromTransactionNote($note); if ($productId) { return $productId; } } return null; } private function productIdFromTransactionNote(?string $note): ?int { $note = (string) ($note ?? ''); if ($note === '' || !preg_match('/(?:^|;)product_id:(\d+)/', $note, $matches)) { return null; } return (int) $matches[1]; } private function applyMetalTypeTransactionTitle($record): void { if ($this->resolveAbshodeProductId($record)) { return; } if (!empty($record->product_id)) { return; } $wallet = $this->resolveTransactionWallet($record); if ($wallet && !empty($wallet->product)) { return; } if (self::isCashTransactionTitle($record->title)) { return; } if (!self::isGenericMetalTransactionTitle($record->title)) { return; } if (preg_match('/^(موجودی|واریز)/u', (string) $record->title)) { if ((int) ($record->type ?? 0) === 7) { $record->title = self::mainMetalDepositTitle(); } else { $record->title = self::meltedGoldDepositTitle(); } } elseif (preg_match('/^برداشت /u', (string) $record->title)) { if ((int) ($record->type ?? 0) === 10) { $record->title = self::mainMetalWithdrawTitle(); } else { $record->title = self::meltedGoldWithdrawTitle(); } } elseif (preg_match('/^خرید /u', (string) $record->title)) { $record->title = self::meltedGoldBuyTitle(); } elseif (preg_match('/^فروش /u', (string) $record->title)) { $record->title = self::meltedGoldSellTitle(); } } private function applyProductTransactionTitle($record): void { if (self::isCashTransactionTitle($record->title)) { return; } $product = $this->resolveTransactionProductForLedgerTitle($record); if (!$product || trim((string) $product->title) === '') { return; } $productTitle = trim((string) $product->title); $expectedBuy = self::productBuyTitle($productTitle); $expectedSell = self::productSellTitle($productTitle); if (in_array($record->title, [$expectedBuy, $expectedSell], true)) { return; } $direction = (int) ($record->order->direction ?? 0); if ($direction === 1 || ($direction === 0 && (float) ($record->gold ?? 0) > 0)) { $record->title = $expectedBuy; return; } if ($direction === 2 || ($direction === 0 && (float) ($record->gold ?? 0) < 0)) { $record->title = $expectedSell; return; } if (self::isGenericMetalTransactionTitle($record->title)) { $record->title = $expectedBuy; } } private function resolveMainWalletTransactionTitle($record): string { $product = $this->resolveTransactionProduct($record); if ($product && trim((string) $product->title) !== '') { $productTitle = trim((string) $product->title); if ((int) $record->type === 1) { return self::productBuyTitle($productTitle); } if ((int) $record->type === 2) { return self::productSellTitle($productTitle); } } if ((int) $record->type === 1) { return self::mainMetalBuyTitle(); } if ((int) $record->type === 2) { return self::mainMetalSellTitle(); } if ((int) $record->type === 7) { return self::mainMetalDepositTitle(); } if ((int) $record->type === 10) { return self::mainMetalWithdrawTitle(); } if ((int) $record->type === 12) { return 'برداشت وجه نقد'; } if ((int) $record->type === 22) { return 'تعدیل حسابداری پرگار'; } return $record->type_str; } private function applyProductOrderDisplayFields($record): void { if (empty($record->order_id) || empty($record->order) || empty($record->product_id)) { return; } $order = $record->order; $totalPrice = (int) ($order->total_price ?? $order->payment_amount ?? 0); if ($totalPrice <= 0) { return; } if ((int) ($record->cash ?? 0) === 0) { $direction = (int) ($order->direction ?? 0); $record->cash = $direction === 1 ? -$totalPrice : $totalPrice; } if ($this->isProductWalletTransaction($record)) { $mainBalanceCash = $this->resolveMainWalletBalanceCashForOrder($record); if ($mainBalanceCash !== null) { $record->balance_cash = $mainBalanceCash; } } if (!empty($record->wallet?->product)) { $this->applyProductTransactionUnit($record); } } private function resolveTransactionWallet($record): ?Wallet { if ($record instanceof Transaction) { $relation = $record->getRelationValue('wallet'); if ($relation instanceof Wallet) { return $relation; } } $walletId = (int) ($record->getAttributes()['wallet'] ?? 0); if ($walletId <= 0) { return null; } return Wallet::with('product')->find($walletId); } /** * عنوان هر سطر دفترکل — محصول همان کیف پول (مثلاً کاتد در سطر برداشت کاتد برای خرید مفتول). */ private function resolveTransactionProductForLedgerTitle($record): ?Product { $wallet = $this->resolveTransactionWallet($record); if ($wallet?->product instanceof Product && trim((string) $wallet->product->title) !== '') { return $wallet->product; } return $this->resolveTransactionProduct($record); } private function resolveTransactionProduct($record): ?Product { $productId = $this->resolveAbshodeProductId($record) ?? 0; $wallet = $this->resolveTransactionWallet($record); if ($productId <= 0) { $partial = $wallet?->product ?? $record->product ?? null; return $partial instanceof Product ? $partial : null; } $partial = $record->product ?? $wallet?->product ?? null; if ($partial instanceof Product && (int) $partial->id === $productId && trim((string) $partial->title) !== '') { return $partial; } return Product::query() ->select([ 'id', 'title', 'group_type', 'unit', 'direct_price', 'amount', 'trade_by_quantity', 'metal_id', ]) ->find($productId) ?? ($partial instanceof Product ? $partial : null); } private function applyProductTransactionUnit($record): void { $product = $this->resolveTransactionProduct($record); if (!$product) { if (!empty($record->gold)) { $record->unit = self::tradeWeightUnit(); } return; } $payload = self::productUnitPayload($product); $onMainWallet = empty($record->wallet?->product_id); [$displayGold, $unit] = self::resolveTransactionDisplayGoldAndUnit( $payload, (float) ($record->gold ?? 0), $onMainWallet ); $record->gold = $displayGold; $record->unit = $unit; } private function isProductWalletTransaction($record): bool { $wallet = $this->resolveTransactionWallet($record); return !empty($wallet?->product); } /** * عنوان/وضعیت تراکنش‌های API فروشگاه — فقط نمایش؛ واریز/برداشت عادی دست نخورده. */ private function applyShopTransactionPresentation($record): void { if ($this->isShopGoldTransaction($record)) { if (empty($record->title) || $record->title === 'تعریف نشده') { $record->title = 'برداشت طلایی'; } } } /** * خروجی JSON — withdraw/order را صریحاً می‌گذارد (setRelation گاهی در JSON نمی‌آید). */ private function serializeTransactionRowsForApi($rows): array { $out = []; foreach ($rows as $record) { $row = $record instanceof Transaction ? $record->toArray() : (array) $record; $this->attachMissingOrderForApi($row); if ($this->isShopRialTransaction($record)) { $row['withdraw'] = ['sts_str' => 'برداشت ریالی توسط فروشگاه']; } if ($this->isShopGoldTransaction($record)) { $order = $row['order'] ?? []; if (!is_array($order)) { $order = $order instanceof Order ? $order->toArray() : []; } $order['sts_str'] = 'برداشت طلایی توسط فروشگاه'; if (empty($order['id']) && !empty($record->order_id)) { $order['id'] = $record->order_id; } $row['order'] = $order; $row['title'] = 'برداشت طلایی'; $row['type_str'] = 'برداشت طلایی'; } $this->normalizeApiRelationStsStr($row, 'order', Order::$STS); $this->normalizeApiRelationStsStr($row, 'withdraw', Withdraw::$STS); $out[] = $row; } return $out; } /** برداشت ریال API فروشگاه — type=4، بدون withdraw_id، مبلغ منفی */ private function isShopRialTransaction($record): bool { if ((int) $record->type !== 4 || !empty($record->withdraw_id)) { return false; } return (int) ($record->cash ?? 0) < 0; } /** برداشت طلا API فروشگاه — type=10 + order_id، بدون withdraw */ private function isShopGoldTransaction($record): bool { if ((int) $record->type !== 10 || empty($record->order_id) || !empty($record->withdraw_id)) { return false; } $order = $this->resolveOrderForTransaction($record); if (!$order) { return true; } if ((int) ($order->direction ?? 0) !== 2) { return false; } $montify = trim((string) $this->Config('montify_name')); if ($montify !== '' && str_contains((string) ($order->description ?? ''), $montify)) { return true; } return (int) ($order->sts ?? 0) === 2 && (int) ($order->payment_sts ?? 0) === 4; } private function resolveOrderForTransaction($record): ?Order { if ($record->order instanceof Order) { return $record->order; } if (empty($record->order_id)) { return null; } return Order::query() ->select(['*', Order::$SELECT_STS_STR]) ->find($record->order_id); } private function resolveMainWalletBalanceCashForOrder($record): ?int { $customerId = $record->customer_id ?? auth('customer')->id(); $mainWalletIds = Wallet::where('owner', $customerId) ->whereNull('product_id') ->whereNull('fund_id') ->pluck('id'); if ($mainWalletIds->isEmpty()) { return null; } $mainTx = Transaction::where('order_id', $record->order_id) ->whereIn('wallet', $mainWalletIds) ->whereNotNull('balance_cash') ->orderBy('id') ->first(); if ($mainTx) { return (int) $mainTx->balance_cash; } $previousTx = Transaction::whereIn('wallet', $mainWalletIds) ->where('id', '<', $record->id) ->whereNotNull('balance_cash') ->orderByDesc('id') ->first(); return $previousTx ? (int) $previousTx->balance_cash : null; } private function normalizeApiRelationStsStr(array &$row, string $key, array $stsMap): void { if (empty($row[$key]) || !is_array($row[$key])) { return; } $relation = &$row[$key]; $sts = $relation['sts'] ?? null; if ($sts === null && isset($relation['sts_str']) && is_numeric($relation['sts_str'])) { $sts = (int) $relation['sts_str']; } if ($sts !== null && isset($stsMap[$sts])) { $relation['sts_str'] = $stsMap[$sts]; return; } $current = trim((string) ($relation['sts_str'] ?? '')); if ($current !== '' && $current !== 'بدون مقدار') { return; } if ($key === 'order' && (int) ($relation['payment_sts'] ?? 0) === 4) { $relation['sts_str'] = 'موفق'; } } /** * بعضی تراکنش‌های قدیمی order_id دارند ولی ردیف orders حذف شده — برای UI وضعیت/قیمت را بازسازی می‌کنیم. */ private function attachMissingOrderForApi(array &$row): void { $orderId = (int) ($row['order_id'] ?? 0); if ($orderId <= 0) { return; } $order = $row['order'] ?? null; if (is_array($order) && !empty($order['id'])) { return; } $loaded = Order::query()->find($orderId); if ($loaded) { $row['order'] = $loaded->toArray(); return; } $type = (int) ($row['type'] ?? 0); if (!in_array($type, [1, 2, 10], true)) { return; } $row['order'] = [ 'id' => $orderId, 'sts' => 2, 'payment_sts' => 4, 'sts_str' => 'موفق', 'price' => $this->inferTradePriceFromTransactionRow($row), 'amount' => abs((float) ($row['gold'] ?? 0)), ]; } private function inferTradePriceFromTransactionRow(array $row): int { $cash = abs((int) ($row['cash'] ?? 0)); $gold = abs((float) ($row['gold'] ?? 0)); if ($gold > 0 && $cash > 0) { return (int) round($cash / $gold); } return 0; } }