diff --git a/app/Http/Controllers/Admin/CustomerController.php b/app/Http/Controllers/Admin/CustomerController.php index 4007671..6a70fca 100644 --- a/app/Http/Controllers/Admin/CustomerController.php +++ b/app/Http/Controllers/Admin/CustomerController.php @@ -16,6 +16,7 @@ use Rooyesh\Wallet\interfaces\WalletInterface; use Rooyesh\Wallet\models\Transaction; use Rooyesh\Wallet\models\Wallet; use App\Models\Setting; +use App\Services\BalanceReportService; use App\Services\ConfigService; use App\Services\WalletBalanceService; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; @@ -133,19 +134,27 @@ class CustomerController extends SuperController return response()->json(true); } public function tableWallets(Request $request){ - $query_table = Customer::with(['wallet','referral'])->where("is_archive",false)/*->where('type',1)*/; + $query_table = Customer::with(['wallet.product:id,metal_id', 'referral'])->where("is_archive",false)/*->where('type',1)*/; TableEngine::create($query_table, $request) ->initSearch($this->search) ->select(['*',Customer::$SELECT_TYPE_STR]) ->export($query_table); $balanceService = app(WalletBalanceService::class); - $mainWalletProductIds = app(ConfigService::class)->mainWalletProductIds(); + $balanceReport = app(BalanceReportService::class); + $catalogMetalIds = $balanceService->catalogMetalIds(); + $query_table['catalogMetals'] = $balanceReport->catalogMetalsMeta(); + if (isset($query_table['rows'])) { - $query_table['rows'] = collect($query_table['rows'])->map(function ($row) use ($balanceService, $mainWalletProductIds) { + $query_table['rows'] = collect($query_table['rows'])->map(function ($row) use ($balanceService, $catalogMetalIds) { $wallets = collect($row['wallet'] ?? []); $row['wallet_cash'] = $balanceService->mainWalletCash($wallets); - $row['wallet_gold'] = $balanceService->totalGoldWeight($wallets, $mainWalletProductIds); + $metalMap = $balanceService->buildCustomerWalletMetalDisplayMap($wallets, $catalogMetalIds); + $row['wallet_metal_gold'] = $metalMap; + foreach ($catalogMetalIds as $metalId) { + $row['wallet_metal_gold_' . $metalId] = $metalMap[(string) $metalId] ?? 0.0; + } + $row['wallet_gold'] = array_sum($metalMap); return $row; })->values(); diff --git a/app/Http/Controllers/Admin/DashboardController.php b/app/Http/Controllers/Admin/DashboardController.php index 2a65961..028ad83 100644 --- a/app/Http/Controllers/Admin/DashboardController.php +++ b/app/Http/Controllers/Admin/DashboardController.php @@ -10,6 +10,7 @@ use App\Models\Price; use App\Models\Product; use App\Models\TransactionsAdmin; use App\Models\Withdraw; +use App\Services\BalanceReportService; use App\Services\ConfigService; use Carbon\Carbon; use Hekmatinasser\Verta\Verta; @@ -149,7 +150,7 @@ class DashboardController extends Controller return $cnt; } - public function getBalance(Request $request) + public function getBalance(Request $request, BalanceReportService $balanceReportService) { $startDate = Carbon::parse($request->startDate)->startOfDay(); $endDate = Carbon::parse($request->endDate)->endOfDay(); @@ -158,6 +159,12 @@ class DashboardController extends Controller $response["kalhor"]=[]; $excludeCustomerId = (int) app(ConfigService::class)->getInt('balance_report_exclude_customer_id', 4); + $catalogMetalIds = $balanceReportService->catalogMetalIds(); + if ($catalogMetalIds === []) { + $catalogMetalIds = [1]; + } + $limanMetalBuckets = $balanceReportService->initMetalBalanceBuckets($catalogMetalIds); + $kalhorMetalBuckets = $balanceReportService->initMetalBalanceBuckets($catalogMetalIds); $response["pay_at_update"] = Order::whereNull('pay_at')->update(['pay_at' => DB::raw('created_at')]); //liman @@ -171,6 +178,8 @@ class DashboardController extends Controller $response["liman"]["balance_abs"]["gold"] = 0; $response["liman"]["balance_abs"]["cash"] = 0; foreach ($response["liman"]["orders"] as $OrderGoldBalanceRow) { + $balanceReportService->applyOrderToBuckets($limanMetalBuckets, $OrderGoldBalanceRow); + if ($OrderGoldBalanceRow->direction == 1){ $response["liman"]["balance"]["gold"] += $OrderGoldBalanceRow->amount; $response["liman"]["balance"]["cash"] -= $OrderGoldBalanceRow->total_price; @@ -212,10 +221,22 @@ class DashboardController extends Controller $response["kalhor"]["balance"]['gold'] += $data['Count']; $response["kalhor"]["balance"]['cash'] -= $data['TotalPrice']; $direction =2; + $balanceReportService->applyKalhorSlice( + $kalhorMetalBuckets, + (float) $data['Count'], + (float) $data['TotalPrice'], + 2 + ); } elseif ($data['DealTypeStr'] == "فروش" or $data['DealTypeStr'] == "فروش _ دستی") { $response["kalhor"]["balance"]['gold'] -= $data['Count']; $response["kalhor"]["balance"]['cash'] += $data['TotalPrice']; $direction =1; + $balanceReportService->applyKalhorSlice( + $kalhorMetalBuckets, + (float) $data['Count'], + (float) $data['TotalPrice'], + 1 + ); } $response["kalhor"]["balance_abs"]['gold'] += $data['Count']; $response["kalhor"]["balance_abs"]['cash'] += $data['TotalPrice']; @@ -244,6 +265,12 @@ class DashboardController extends Controller //main $response['cashBalance']= -$response["liman"]["balance"]["cash"]+$response["kalhor"]["balance"]["cash"]; $response['goldBalance']= -$response["liman"]["balance"]["gold"]+$response["kalhor"]["balance"]["gold"]; + + $metalBalances = $balanceReportService->buildMetalBalancesResponse($limanMetalBuckets, $kalhorMetalBuckets); + $response['metalBalances'] = $metalBalances['items']; + if (!empty($metalBalances['items'])) { + $response['goldBalance'] = $metalBalances['totalGoldBalance']; + } //mazaneh $changeMesghal = config('change_mesghal'); diff --git a/app/Http/Controllers/Admin/FileController.php b/app/Http/Controllers/Admin/FileController.php index c4676c8..2e0088b 100644 --- a/app/Http/Controllers/Admin/FileController.php +++ b/app/Http/Controllers/Admin/FileController.php @@ -14,16 +14,20 @@ class FileController extends Controller * * @return string */ - protected function createDir() + protected function createDir(): string { $now = Verta(); $path = preg_replace('/-/', '/', $now->formatDate()); - if (!Storage::exists($path)) { - Storage::makeDirectory($path); - Storage::makeDirectory($path . '/' . 'thumb'); + $disk = Storage::disk('public'); + + if (! $disk->exists($path)) { + $disk->makeDirectory($path); + $disk->makeDirectory($path.'/thumb'); } + return $path; } + public function store(Request $request) { // Store FIle in Public Storage By Date @@ -31,17 +35,17 @@ class FileController extends Controller $data = $request->all(); $path = $this->createDir(); - $data['original_name'] = $request->file('file')->getClientOriginalName(); - $data['ext'] = $request->file('file')->getClientOriginalExtension(); + $uploaded = $request->file('file'); + $data['original_name'] = $uploaded->getClientOriginalName(); + $data['ext'] = $uploaded->getClientOriginalExtension(); if (!isset($data['title']) || is_null($data['title'])) { $data['title'] = $data['original_name']; } $file_name_src = md5(time() . $data['original_name']); $file_name = $file_name_src . '.' . $data['ext']; - $file = $request->file('file'); $data['has_thumb'] = false; - $data['size'] = $file->getSize(); - $file->move(public_path('storage/' . $path . '/'), $file_name); + $data['size'] = $uploaded->getSize(); + Storage::disk('public')->putFileAs($path, $uploaded, $file_name); $data['src'] = 'storage/' . $path . '/' . $file_name; // $data['size'] = $file->getSize(); $data['user_id'] = auth()->id(); diff --git a/app/Http/Controllers/Admin/OrderController.php b/app/Http/Controllers/Admin/OrderController.php index e90db91..280d70c 100644 --- a/app/Http/Controllers/Admin/OrderController.php +++ b/app/Http/Controllers/Admin/OrderController.php @@ -5,26 +5,61 @@ namespace App\Http\Controllers\Admin; use App\DB\Te\TableEngine; use App\Http\Controllers\Controller; use App\Models\Order; +use App\Services\BalanceReportService; +use App\Services\WalletBalanceService; use Illuminate\Http\Request; class OrderController extends Controller { public function table(Request $request){ - $records = Order::with(['customer','transactions']); + $balanceReport = app(BalanceReportService::class); + $catalogMetalIds = app(WalletBalanceService::class)->catalogMetalIds(); + + $records = Order::with([ + 'customer', + 'transactions.product:id,title,unit,metal_id,group_type,amount,trade_by_quantity,direct_price', + ]); TableEngine::create($records, $request->all()) ->initSearch(['customer.name']) ->select([ '*',Order::$SELECT_STS_STR ])->export($records); + + if (isset($records['rows'])) { + foreach ($records['rows'] as &$row) { + $balanceReport->enrichOrderTableRow($row, $catalogMetalIds); + } + unset($row); + } + + $records['catalogMetals'] = $balanceReport->catalogMetalsMeta(); + return response()->json($records); } + public function singleOrderTable(Request $request){ - $records = Order::with(['customer'])->where('type',2); + $balanceReport = app(BalanceReportService::class); + $catalogMetalIds = app(WalletBalanceService::class)->catalogMetalIds(); + + $records = Order::with([ + 'customer', + 'transactions.product:id,title,unit,metal_id,group_type,amount,trade_by_quantity,direct_price', + ])->where('type',2); TableEngine::create($records, $request->all()) ->initSearch(['customer.name']) ->select([ '*',Order::$SELECT_STS_STR ])->export($records); + + if (isset($records['rows'])) { + foreach ($records['rows'] as &$row) { + $balanceReport->enrichOrderTableRow($row, $catalogMetalIds); + } + unset($row); + } + + $records['catalogMetals'] = $balanceReport->catalogMetalsMeta(); + return response()->json($records); } } diff --git a/app/Http/Controllers/Admin/ProductController.php b/app/Http/Controllers/Admin/ProductController.php index 42d90b6..5526f4c 100644 --- a/app/Http/Controllers/Admin/ProductController.php +++ b/app/Http/Controllers/Admin/ProductController.php @@ -612,6 +612,10 @@ class ProductController extends SuperController unset($data['direct_price']); } + if (!Product::hasInventoryColumn()) { + unset($data['inventory']); + } + return $data; } } diff --git a/app/Http/Controllers/Admin/WalletController.php b/app/Http/Controllers/Admin/WalletController.php index 784d79b..8b97fac 100644 --- a/app/Http/Controllers/Admin/WalletController.php +++ b/app/Http/Controllers/Admin/WalletController.php @@ -20,8 +20,9 @@ use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; use App\Http\Controllers\Admin\TahesabController; use App\Models\Setting; use App\Models\User; +use App\Services\BalanceReportService; use App\Services\ConfigService; -use App\Services\TransactionMetalService; +use App\Services\WalletBalanceService; use Hekmatinasser\Verta\Verta; @@ -221,10 +222,172 @@ class WalletController extends Controller return response()->json(true); } - public function index(Request $request, TransactionMetalService $transactionMetalService) + /** گزینه‌های شارژ کیف پول — نقدی + محصولات فعال */ + public function chargeTargets() { - $records = Transaction::with(['customer', 'wallet.product:id,metal_id', 'product:id,metal_id']) - ->whereNull('fund_id'); + $configService = app(ConfigService::class); + $mainWalletProductIds = $configService->mainWalletProductIds(); + $balanceReport = app(BalanceReportService::class); + + $targets = [ + [ + 'key' => 'money', + 'kind' => 'money', + 'label' => 'نقدی (ریال)', + 'unit' => 'ریال', + ], + ]; + + $products = Product::query() + ->where('active', 1) + ->orderBy('metal_id') + ->orderBy('id') + ->get(['id', 'title', 'unit', 'metal_id', 'group_type', 'trade_by_quantity', 'amount', 'direct_price']); + + foreach ($products as $product) { + $usesMainWallet = in_array((int) $product->id, $mainWalletProductIds, true); + $label = (string) $product->title; + if ($usesMainWallet) { + $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 ?? ''), + ]; + } + + 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('مقدار موجودی کمتر از درخواست شماست'); + } + + $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); + + if ($this->Config('send_to_tahesab') == 'true') { + try { + $data['direction'] = $deposit ? 1 : 0; + $tahesab = new TahesabController(); + $tahesab->walletGold($data); + } catch (\Exception $e) { + return response($e->getMessage(), 500); + } + } + + $status = $deposit ? ' شارژ شد ' : ' کسر شد '; + $formatted = self::formatSmsGoldWeight($data['value']); + + if ($this->Config('charge_notif') == 'true') { + $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); + + 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:id,metal_id', + 'product: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([ @@ -232,9 +395,13 @@ class WalletController extends Controller ])->export($records); if (isset($records['rows'])) { - $records['rows'] = $transactionMetalService->enrichTransactionRows($records['rows']); + foreach ($records['rows'] as &$row) { + $balanceReport->enrichTransactionTableRow($row, $catalogMetalIds); + } + unset($row); } - $records['catalogMetals'] = $transactionMetalService->catalogMetals(); + + $records['catalogMetals'] = $balanceReport->catalogMetalsMeta(); return response()->json($records); } @@ -289,28 +456,42 @@ class WalletController extends Controller 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']); - $totalGold = (float) (clone $sumQuery)->sum('gold'); + $sumRows = (clone $sumQuery)->with('product:id,metal_id')->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($totalGold * $lastPrice, 2), + 'goldToCash' => round($totalGoldToCash, 2), 'cash' => round($totalCash, 2), 'cashToGold' => $lastPrice > 0 ? round($totalCash / $lastPrice, 2) : 0, + 'metalBalances' => $metalBalances, ]; - $records = Wallet::with('customer'); + $records = Wallet::with(['customer', 'product:id,metal_id']); TableEngine::create($records, $request->all()) ->initSearch(['customer.name']) ->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); diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 578b1c0..125ff2f 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -73,6 +73,26 @@ class Controller extends BaseController return $formatted !== '' ? $formatted : '0'; } + /** + * گزینه‌های Guzzle/cURL — فقط وقتی افزونه curl و ثابت‌ها موجود باشند. + * + * @param array $curlOptions + * @return array> + */ + public static function httpClientOptions(array $curlOptions = []): array + { + $curl = $curlOptions; + + if (defined('CURLOPT_IPRESOLVE') && defined('CURL_IPRESOLVE_V4')) { + $resolveOpt = (int) constant('CURLOPT_IPRESOLVE'); + if (! array_key_exists($resolveOpt, $curl)) { + $curl[$resolveOpt] = (int) constant('CURL_IPRESOLVE_V4'); + } + } + + return $curl === [] ? [] : ['curl' => $curl]; + } + public static function Send($to , $msg, $is_customer = true) { $r = self::SendMessage($to , $msg, $is_customer); @@ -139,26 +159,32 @@ class Controller extends BaseController ]); $platforms[] = "0098:{$to}";*/ - $response = Http::timeout(15) - ->withHeaders([ - 'Content-Type' => 'application/json', - 'secret_key' => 'f21debfb-83ac-4624-a2f4-f08375043181', - ]) - ->withOptions([ - 'curl' => [ - \CURLOPT_IPRESOLVE => \CURL_IPRESOLVE_V4, - ] - ]) - ->post('http://185.231.112.101/api/createsms', [ + try { + $http = Http::timeout(15) + ->withHeaders([ + 'Content-Type' => 'application/json', + 'secret_key' => 'f21debfb-83ac-4624-a2f4-f08375043181', + ]); + + $curlOptions = self::httpClientOptions(); + if ($curlOptions !== []) { + $http = $http->withOptions($curlOptions); + } + + $response = $http->post('http://185.231.112.101/api/createsms', [ 'phone' => $to, 'host' => $config->Config('name_fa') ?? gethostname(), 'text' => trim($msg), 'platform' => 'rayegansms', ]); - $platforms[] = $response->successful() - ? "rayegansms:{$to}" - : "rayegansms_failed:{$to}:{$response->status()}"; + $platforms[] = $response->successful() + ? "rayegansms:{$to}" + : "rayegansms_failed:{$to}:{$response->status()}"; + } catch (\Throwable $e) { + report($e); + $platforms[] = "rayegansms_error:{$to}"; + } } @@ -435,6 +461,10 @@ class Controller extends BaseController return false; } + if (self::usesQuantityTrade($product)) { + return false; + } + if (self::isWeightBasedProductUnit($product['unit'] ?? null)) { return false; } @@ -451,6 +481,86 @@ class Controller extends BaseController return in_array($group, ['سکه', 'گوی', 'مکعب', 'شمش', 'سکه پارسیان'], true); } + public static function productUnitPayload($product): array + { + if (!$product) { + return []; + } + + if (is_array($product)) { + return $product; + } + + return [ + 'unit' => $product->unit ?? null, + 'direct_price' => $product->direct_price ?? null, + 'group_type' => $product->group_type ?? null, + 'amount' => $product->amount ?? null, + 'trade_by_quantity' => $product->trade_by_quantity ?? null, + ]; + } + + public static function convertWeight(float $value, ?string $fromUnit, ?string $toUnit): float + { + $from = trim((string) ($fromUnit ?? '')); + $to = trim((string) ($toUnit ?? '')); + + if ($from === '' || $to === '' || $from === $to) { + return $value; + } + + $grams = self::weightToGrams($value, $from); + + return $to === 'کیلوگرم' ? $grams / 1000 : $grams; + } + + /** + * مقدار و واحد نمایشی تراکنش — منبع حقیقت: unit محصول در ادمین. + * معامله مقداری + وزن دارد: تعداد ذخیره‌شده × وزن هر واحد. + */ + public static function resolveTransactionDisplayGoldAndUnit(?array $payload, float $gold, bool $onMainWallet = false): array + { + if ($payload === []) { + return [$gold, self::tradeWeightUnit()]; + } + + $sign = $gold < 0 ? -1 : 1; + $absGold = abs($gold); + + if ( + self::usesQuantityTrade($payload) + && self::hasProductUnitWeight($payload) + && self::isWeightBasedProductUnit($payload['unit'] ?? null) + ) { + $factor = (float) ($payload['amount'] ?? 1); + + return [ + round($absGold * $factor, 3) * $sign, + trim((string) $payload['unit']), + ]; + } + + $unit = self::productQuantityUnit($payload); + $productWeightUnit = self::isWeightBasedProductUnit($payload['unit'] ?? null) + ? trim((string) $payload['unit']) + : null; + + if ($onMainWallet && $productWeightUnit) { + $tradeUnit = self::tradeWeightUnit(); + + if ($productWeightUnit !== $tradeUnit) { + return [ + round(self::convertWeight($absGold, $tradeUnit, $productWeightUnit), 3) * $sign, + $productWeightUnit, + ]; + } + + return [$gold, $productWeightUnit]; + } + + return [$gold, $unit]; + } + public static function productQuantityUnit(?array $product): string { $unit = trim((string) ($product['unit'] ?? '')); @@ -462,7 +572,7 @@ class Controller extends BaseController return 'عدد'; } - return 'گرم'; + return self::tradeWeightUnit(); } /** مطابق ادمین: وزن «دارد» => amount > 0 ؛ «ندارد» => amount = 0 */ diff --git a/app/Http/Controllers/Customer/WalletController.php b/app/Http/Controllers/Customer/WalletController.php index 81a38ef..4236508 100644 --- a/app/Http/Controllers/Customer/WalletController.php +++ b/app/Http/Controllers/Customer/WalletController.php @@ -137,11 +137,7 @@ class WalletController extends Controller } else { $record->title = ($record->gold > 0 ? self::productBuyTitle($wallet->product->title) : self::productSellTitle($wallet->product->title)); } - $record->unit = self::productQuantityUnit([ - 'unit' => $wallet->product->unit ?? null, - 'direct_price' => $wallet->product->direct_price ?? null, - 'group_type' => $wallet->product->group_type ?? null, - ]); + $record->unit = self::productQuantityUnit(self::productUnitPayload($wallet->product)); } else { $record->title = $this->resolveMainWalletTransactionTitle($record); } @@ -406,17 +402,12 @@ class WalletController extends Controller return; } - $productId = $this->resolveAbshodeProductId($record); - if (!$productId) { - return; - } - - $productTitle = $record->product->title - ?? Product::query()->whereKey($productId)->value('title'); - if (!$productTitle) { + $product = $this->resolveTransactionProduct($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); @@ -434,9 +425,14 @@ class WalletController extends Controller return (int) $record->product_id; } - $note = (string) ($record->note ?? ''); - if ($note !== '' && preg_match('/(?:^|;)product_id:(\d+)/', $note, $matches)) { - return (int) $matches[1]; + $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)) { @@ -449,7 +445,33 @@ class WalletController extends Controller ->where('product_id', '>', 0) ->value('product_id'); - return $siblingProductId ? (int) $siblingProductId : null; + 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 @@ -462,7 +484,7 @@ class WalletController extends Controller return; } - $wallet = $record->wallet ?? null; + $wallet = $this->resolveTransactionWallet($record); if ($wallet && !empty($wallet->product)) { return; } @@ -500,18 +522,12 @@ class WalletController extends Controller return; } - $productId = $this->resolveAbshodeProductId($record); - if (!$productId) { - return; - } - - $productTitle = $record->product->title - ?? $record->wallet->product->title - ?? Product::query()->whereKey($productId)->value('title'); - if (!$productTitle) { + $product = $this->resolveTransactionProduct($record); + if (!$product || trim((string) $product->title) === '') { return; } + $productTitle = trim((string) $product->title); $expectedBuy = self::productBuyTitle($productTitle); $expectedSell = self::productSellTitle($productTitle); @@ -527,11 +543,28 @@ class WalletController extends Controller 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(); } @@ -576,35 +609,86 @@ class WalletController extends Controller } if (!empty($record->wallet?->product)) { - $record->unit = self::productQuantityUnit([ - 'unit' => $record->wallet->product->unit ?? null, - 'direct_price' => $record->wallet->product->direct_price ?? null, - 'group_type' => $record->wallet->product->group_type ?? null, - ]); + $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 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 = $record->wallet->product ?? $record->product ?? null; + $product = $this->resolveTransactionProduct($record); + if (!$product) { - if (!empty($record->gold) && empty($record->wallet?->product)) { + if (!empty($record->gold)) { $record->unit = self::tradeWeightUnit(); } return; } - $record->unit = self::productQuantityUnit([ - 'unit' => $product->unit ?? null, - 'direct_price' => $product->direct_price ?? null, - 'group_type' => $product->group_type ?? null, - ]); + $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 { - return !empty($record->wallet?->product); + $wallet = $this->resolveTransactionWallet($record); + + return !empty($wallet?->product); } /** diff --git a/app/Http/Controllers/Single/SingleOrderController.php b/app/Http/Controllers/Single/SingleOrderController.php index 8c325f6..c91244f 100644 --- a/app/Http/Controllers/Single/SingleOrderController.php +++ b/app/Http/Controllers/Single/SingleOrderController.php @@ -32,6 +32,10 @@ class SingleOrderController extends Controller public function store(Request $request) { $data = $request->all(); + $data['payment_product_id'] = $this->resolveGoldPaymentProductId($data); + if (preg_match('/^gold:\d+$/', (string) ($data['method'] ?? ''))) { + $data['method'] = 'gold'; + } $data['is_katod'] = ($data['method'] ?? null) === 'katod'; if ($data['is_katod']) { $data['method'] = 'gold'; @@ -245,6 +249,7 @@ class SingleOrderController extends Controller } $data['order'] = $order = Order::create($data); $abshode = $this->resolveAbshodeProductContext($data); + $productId = (int) ($data['product_id'] ?? $abshode['id'] ?? 0); $buyTransaction = [ "cash" => $data['payment_amount'], "gold" => $data['amount'], @@ -252,13 +257,11 @@ class SingleOrderController extends Controller 'active' => true, 'order_id' => $order->id, 'start_at' => now(), - 'title' => $abshode['title'] !== '' - ? self::productBuyTitle($abshode['title']) - : self::meltedGoldBuyTitle(), + 'title' => $this->meltedOrProductBuyTitle($data), ]; - if ($abshode['id'] > 0) { - $buyTransaction['product_id'] = $abshode['id']; - $buyTransaction['note'] = 'product_id:' . $abshode['id']; + if ($productId > 0) { + $buyTransaction['product_id'] = $productId; + $buyTransaction['note'] = 'product_id:' . $productId; } new TransactionInterface($wallet_interface, $buyTransaction, 1, true, false); $response['1'] = 2; @@ -312,7 +315,7 @@ class SingleOrderController extends Controller 'active' => true, 'order_id' => $order->id, 'start_at' => now(), - 'title' => self::productBuyTitle($data['product']['title'] ?? ''), + 'title' => $this->meltedOrProductBuyTitle($data), ], 1, true, false); @@ -332,22 +335,69 @@ class SingleOrderController extends Controller $data['total_fee'] = 0; $data['service_charge_price'] = $data['service_charge'] * $data['price_original']; $data['wallet'] = $wallet; - if ($wallet->gold < $data['total_amount']) { + + $paymentProductId = (int) ($data['payment_product_id'] ?? 0); + $paymentWalletInterface = $wallet_interface; + $paymentProduct = null; + $paymentWallet = $wallet; + + if ($paymentProductId > 0) { + $paymentProduct = Product::find($paymentProductId); + if ( + !$paymentProduct + || (int) ($paymentProduct->metal_id ?? 0) !== (int) ($data['product']['metal_id'] ?? 0) + ) { + throw new UnprocessableEntityHttpException('روش پرداخت نامعتبر است.'); + } + + $paymentWalletInterface = $this->productWalletInterface($paymentProductId); + $paymentWallet = $paymentWalletInterface->wallet; + if (!$paymentWallet) { + throw new UnprocessableEntityHttpException('کیف پول یافت نشد.'); + } + } + + if ((float) $paymentWallet->gold < $data['total_amount']) { throw new UnprocessableEntityHttpException($this->insufficientGoldBalanceMessage($data)); } if (!$data['product']['open_buy']) { throw new UnprocessableEntityHttpException('خرید این محصول در این لحظه مقدور نمیباشد'); } $data['order'] = $order = Order::create($data); - $data['transaction'][] = new TransactionInterface($wallet_interface, $_inp = [ + + $paymentSellTitle = $paymentProduct + ? self::productSellTitle($paymentProduct->title ?? '') + : self::mainMetalSellTitle(); + + $paymentTransaction = [ "number" => $data['number'], "gold" => $data['total_amount'], - "product_id" => $data["product_id"], 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, - 'title' => self::mainMetalSellTitle(), - ], 12, false, true); + 'title' => $paymentSellTitle, + ]; + + if ($paymentProductId > 0) { + $paymentTransaction['product_id'] = $paymentProductId; + $this->normalizeProductWalletGold($paymentWalletInterface); + $data['transaction'][] = new TransactionInterface( + $paymentWalletInterface, + $paymentTransaction, + 1, + false, + false + ); + } else { + $data['transaction'][] = new TransactionInterface( + $paymentWalletInterface, + $paymentTransaction, + 12, + false, + true + ); + } + $wallet_interface_product = $this->productWalletInterface((int) $data["product_id"]); $this->normalizeProductWalletGold($wallet_interface_product); new TransactionInterface($wallet_interface_product, [ @@ -357,7 +407,7 @@ class SingleOrderController extends Controller 'active' => true, 'order_id' => $order->id, 'start_at' => now(), - 'title' => self::productBuyTitle($data['product']['title'] ?? ''), + 'title' => $this->meltedOrProductBuyTitle($data), ], 1, true, false); } @@ -378,19 +428,18 @@ class SingleOrderController extends Controller } $data['order'] = $order = Order::create($data); $abshode = $this->resolveAbshodeProductContext($data); + $productId = (int) ($data['product_id'] ?? $abshode['id'] ?? 0); $sellTransaction = [ "gold" => $data['amount'], "cash" => $data['total_price'], 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, - 'title' => $abshode['title'] !== '' - ? self::productSellTitle($abshode['title']) - : self::meltedGoldSellTitle(), + 'title' => $this->meltedOrProductSellTitle($data), ]; - if ($abshode['id'] > 0) { - $sellTransaction['product_id'] = $abshode['id']; - $sellTransaction['note'] = 'product_id:' . $abshode['id']; + if ($productId > 0) { + $sellTransaction['product_id'] = $productId; + $sellTransaction['note'] = 'product_id:' . $productId; } new TransactionInterface($wallet_interface, $sellTransaction, 2, false, true); @@ -428,7 +477,7 @@ class SingleOrderController extends Controller 'active' => true, 'order_id' => $order->id, 'start_at' => now(), - 'title' => self::productSellTitle($data['product']['title'] ?? ''), + 'title' => $this->meltedOrProductSellTitle($data), ], 1, false, false); $data['transaction'][] = new TransactionInterface($wallet_interface, $_inp = [ "number" => $data['number'], @@ -469,7 +518,7 @@ class SingleOrderController extends Controller 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, - 'title' => self::mainMetalBuyTitle(), + 'title' => $this->meltedOrProductSellTitle($data), ], 12, true, false); $this->normalizeProductWalletGold($wallet_interface_product); new TransactionInterface($wallet_interface_product, [ @@ -479,7 +528,7 @@ class SingleOrderController extends Controller 'active' => true, 'order_id' => $order->id, 'start_at' => now(), - 'title' => self::productSellTitle($data['product']['title'] ?? ''), + 'title' => $this->meltedOrProductSellTitle($data), ], 1, false, false); } @@ -794,6 +843,20 @@ class SingleOrderController extends Controller return null; } + private function resolveGoldPaymentProductId(array $data): ?int + { + if (!empty($data['payment_product_id'])) { + return (int) $data['payment_product_id']; + } + + $method = (string) ($data['method'] ?? ''); + if (preg_match('/^gold:(\d+)$/', $method, $matches)) { + return (int) $matches[1]; + } + + return null; + } + private function resolveAbshodeProductContext(array $data): array { $productId = (int) ($data['product_id'] ?? $data['abshode_id'] ?? 0); @@ -825,17 +888,20 @@ class SingleOrderController extends Controller private function productTitleFromData(array $data): ?string { $product = $data['product'] ?? null; - if (!$product) { - return null; - } + $title = null; - if (is_array($product)) { - $title = $product['title'] ?? null; - } else { - $title = $product->title ?? null; + if ($product) { + if (is_array($product)) { + $title = $product['title'] ?? null; + } else { + $title = $product->title ?? null; + } } $title = trim((string) $title); + if ($title === '' && !empty($data['product_id'])) { + $title = trim((string) Product::query()->whereKey((int) $data['product_id'])->value('title')); + } return $title !== '' ? $title : null; } diff --git a/app/Models/Product.php b/app/Models/Product.php index 135c751..56ce3a9 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -22,6 +22,7 @@ class Product extends Model private static ?bool $useLastPriceIfZeroColumnExists = null; private static ?bool $directPriceColumnExists = null; private static ?bool $tradeByQuantityColumnExists = null; + private static ?bool $inventoryColumnExists = null; protected static function booted(): void { @@ -48,6 +49,9 @@ class Product extends Model if (!static::hasTradeByQuantityColumn()) { unset($product->attributes['trade_by_quantity']); } + if (!static::hasInventoryColumn()) { + unset($product->attributes['inventory']); + } }); } @@ -175,6 +179,24 @@ class Product extends Model return static::$tradeByQuantityColumnExists; } + public static function hasInventoryColumn(): bool + { + if (static::$inventoryColumnExists !== null) { + return static::$inventoryColumnExists; + } + + try { + static::$inventoryColumnExists = Schema::hasColumn( + (new static)->getTable(), + 'inventory' + ); + } catch (\Throwable) { + static::$inventoryColumnExists = false; + } + + return static::$inventoryColumnExists; + } + public function orderLimitsEnabled(): bool { if (!static::hasOrderLimitColumns() || !($this->order_limit_enabled ?? false)) { diff --git a/app/Services/AssetSummaryService.php b/app/Services/AssetSummaryService.php index 38fda1b..db2074a 100644 --- a/app/Services/AssetSummaryService.php +++ b/app/Services/AssetSummaryService.php @@ -35,7 +35,7 @@ class AssetSummaryService ->where('owner_type', $ownerType) ->get(); - $mainWallet = $userWallets->first(fn (Wallet $w) => $w->product_id === null); + $mainWallet = $this->resolveMainWallet($userWallets); $metalIds = $this->catalogMetalIds($catalog); $weightUnit = Controller::tradeWeightUnit(); $hasCountBased = $this->catalogHasCountBased($catalog); @@ -46,13 +46,13 @@ class AssetSummaryService // محصولات متصل به کیف اصلی (main_wallet_products) — موجودی‌شان = gold کیف اصلی $mainWalletProductIds = $this->configService->mainWalletProductIds(); - $metalsWithProductWallet = $this->metalIdsWithProductWallet($userWallets, $catalogById, $mainWalletProductIds); + $metalsWithProductWallet = $this->metalIdsWithProductWallet($userWallets, $catalogById, $mainWalletProductIds, $mainWallet); $inventoryCards = array_merge( $this->buildMeltedInventoryCards($catalog, $catalogById, $mainWallet, $metalIds, $metalsWithProductWallet, $mainWalletProductIds), - $this->buildInventoryCards($userWallets, $catalogById, $mainWalletProductIds) + $this->buildInventoryCards($userWallets, $catalogById, $mainWalletProductIds, $mainWallet), ); $metalTotals = $this->buildMetalTotals($userWallets, $mainWallet, $metalIds, $weightUnit, $catalogById, $metalsWithProductWallet, $mainWalletProductIds); - $cashWallet = (float) ($mainWallet->cash ?? 0); + $cashWallet = (float) ($mainWallet?->cash ?? 0); $assetsRialValue = $this->sumAssetsRialValue( $metalTotals, $userWallets, @@ -112,7 +112,7 @@ class AssetSummaryService array $mainWalletProductIds = [] ): array { $cards = []; - $mainWalletGrams = Controller::weightToGrams((float) ($mainWallet->gold ?? 0)); + $mainWalletGrams = Controller::weightToGrams((float) ($mainWallet?->gold ?? 0)); $assignedMetalIds = []; $mainProductId = $this->configService->mainProductId(); @@ -123,7 +123,7 @@ class AssetSummaryService return []; } - $mainWalletWeight = (float) ($mainWallet->gold ?? 0); + $mainWalletWeight = (float) ($mainWallet?->gold ?? 0); if ($mainWalletWeight <= 0) { return []; } @@ -186,6 +186,66 @@ class AssetSummaryService return $cards; } + private function resolveMainWallet(Collection $userWallets): ?Wallet + { + return $userWallets->first( + fn (Wallet $w) => $w->fund_id === null && $w->product_id === null + ); + } + + /** + * کیف legacy محصول آبشده/کاتد — وقتی همان فلز روی کیف اصلی موجودی دارد نباید دوباره شمرده شود. + */ + private function shouldIgnoreLegacyProductWalletLine( + Product $catalogProduct, + ?Wallet $mainWallet, + Collection $catalogById, + array $mainWalletProductIds + ): bool { + if (in_array((int) $catalogProduct->id, $mainWalletProductIds, true)) { + return true; + } + + if ((float) ($mainWallet?->gold ?? 0) <= 0) { + return false; + } + + if (!$this->isMeltedTradeProduct($catalogProduct)) { + return false; + } + + $mainMetalId = $this->resolveMainWalletMetalId($mainWallet, $catalogById, $mainWalletProductIds); + + return $mainMetalId > 0 && (int) ($catalogProduct->metal_id ?? 0) === $mainMetalId; + } + + private function resolveMainWalletMetalId( + ?Wallet $mainWallet, + Collection $catalogById, + array $mainWalletProductIds + ): int { + if ($mainWalletProductIds !== []) { + $representative = $this->resolveMainWalletRepresentative($catalogById, $mainWalletProductIds); + if ($representative) { + return max(1, (int) ($representative->metal_id ?? 1)); + } + } + + if ((float) ($mainWallet?->gold ?? 0) <= 0) { + return 0; + } + + $mainProductId = $this->configService->mainProductId(); + if ($mainProductId) { + $mainProduct = $catalogById->get((int) $mainProductId); + if ($mainProduct) { + return max(1, (int) ($mainProduct->metal_id ?? 1)); + } + } + + return 1; + } + private function catalogProduct(Product $product, Collection $catalogById): Product { return $catalogById->get((int) $product->id) ?? $product; @@ -246,9 +306,14 @@ class AssetSummaryService } /** فلزهایی که کاربر روی کیف محصول موجودی دارد — برای جلوگیری از کارت آبشدهٔ تکراری. */ - private function metalIdsWithProductWallet(Collection $userWallets, Collection $catalogById, array $mainWalletProductIds = []): array - { + private function metalIdsWithProductWallet( + Collection $userWallets, + Collection $catalogById, + array $mainWalletProductIds = [], + ?Wallet $mainWallet = null + ): array { $metalIds = []; + $mainWallet ??= $this->resolveMainWallet($userWallets); foreach ($userWallets as $walletLine) { if ($walletLine->product_id === null || (float) $walletLine->gold <= 0) { @@ -265,7 +330,12 @@ class AssetSummaryService continue; } - $metalId = (int) ($this->catalogProduct($product, $catalogById)->metal_id ?? 0); + $catalogProduct = $this->catalogProduct($product, $catalogById); + if ($this->shouldIgnoreLegacyProductWalletLine($catalogProduct, $mainWallet, $catalogById, $mainWalletProductIds)) { + continue; + } + + $metalId = (int) ($catalogProduct->metal_id ?? 0); if ($metalId > 0) { $metalIds[] = $metalId; } @@ -296,11 +366,20 @@ class AssetSummaryService return $catalog->first(fn (Product $product) => (int) ($product->metal_id ?? 0) === $metalId); } - private function buildInventoryCards(Collection $userWallets, Collection $catalogById, array $mainWalletProductIds = []): array - { + private function buildInventoryCards( + Collection $userWallets, + Collection $catalogById, + array $mainWalletProductIds = [], + ?Wallet $mainWallet = null + ): array { $cards = []; + $mainWallet ??= $this->resolveMainWallet($userWallets); foreach ($userWallets as $walletLine) { + if ($walletLine->fund_id !== null) { + continue; + } + if ($walletLine->product_id === null || (float) $walletLine->gold <= 0) { continue; } @@ -316,6 +395,10 @@ class AssetSummaryService } $catalogProduct = $this->catalogProduct($product, $catalogById); + if ($this->shouldIgnoreLegacyProductWalletLine($catalogProduct, $mainWallet, $catalogById, $mainWalletProductIds)) { + continue; + } + $productPayload = $this->productPayload($catalogProduct); $isCountBased = $this->isCountBasedProduct($catalogProduct); @@ -349,7 +432,7 @@ class AssetSummaryService return []; } - $mainWalletGrams = Controller::weightToGrams((float) ($mainWallet->gold ?? 0)); + $mainWalletGrams = Controller::weightToGrams((float) ($mainWallet?->gold ?? 0)); $totals = []; // فلز نمایندهٔ کیف اصلی در حالت main_wallet_products @@ -373,6 +456,10 @@ class AssetSummaryService } $catalogProduct = $this->catalogProduct($walletLine->product, $catalogById); + if ($this->shouldIgnoreLegacyProductWalletLine($catalogProduct, $mainWallet, $catalogById, $mainWalletProductIds)) { + continue; + } + if ((int) ($catalogProduct->metal_id ?? 0) !== (int) $metalId) { continue; } @@ -532,11 +619,16 @@ class AssetSummaryService 'direct_price' => $product->direct_price ?? null, 'group_type' => $product->group_type ?? null, 'amount' => $product->amount ?? null, + 'trade_by_quantity' => $product->trade_by_quantity ?? null, ]; } private function productUnitWeightLabel(Product $product): ?string { + if (!$this->isCountBasedProduct($product)) { + return null; + } + if (!Controller::hasProductUnitWeight($this->productPayload($product))) { return null; } diff --git a/app/Services/BalanceReportService.php b/app/Services/BalanceReportService.php new file mode 100644 index 0000000..4a79a17 --- /dev/null +++ b/app/Services/BalanceReportService.php @@ -0,0 +1,563 @@ + 'طلا', + 2 => 'نقره', + 3 => 'مس', + ]; + + public function __construct( + private readonly ConfigService $configService + ) { + } + + /** @return list */ + public function catalogMetalIds(): array + { + return Product::query() + ->where('active', 1) + ->whereNotNull('metal_id') + ->where('metal_id', '>', 0) + ->distinct() + ->orderBy('metal_id') + ->pluck('metal_id') + ->map(static fn ($id) => (int) $id) + ->values() + ->all(); + } + + public function metalLabel(int $metalId): string + { + return self::METAL_LABELS[$metalId] ?? "فلز {$metalId}"; + } + + /** واحد نمایش موجودی/تراز هر فلز در ادمین — مس کیلوگرم، بقیه گرم */ + public function metalDisplayUnit(int $metalId): string + { + return (int) $metalId === 3 ? 'کیلوگرم' : 'گرم'; + } + + public function gramsToMetalDisplay(float $grams, int $metalId): float + { + return (int) $metalId === 3 ? $grams / 1000.0 : $grams; + } + + /** + * @return array + */ + public function catalogMetalsMeta(): array + { + $items = []; + foreach ($this->catalogMetalIds() as $metalId) { + $metalId = (int) $metalId; + $items[] = [ + 'metal_id' => $metalId, + 'label' => $this->metalLabel($metalId), + 'display_unit' => $this->metalDisplayUnit($metalId), + ]; + } + + return $items; + } + + /** + * @return array + */ + public function resolveOrderMetalSlices(Order $order): array + { + $order->loadMissing(['transactions.product']); + + $slices = []; + $txWithProduct = $order->transactions->filter( + static fn ($tx) => $tx->product_id && abs((float) ($tx->gold ?? 0)) > 0 + ); + + if ($txWithProduct->isNotEmpty()) { + foreach ($txWithProduct as $tx) { + $metalId = (int) ($tx->product?->metal_id ?? 1); + if ($metalId <= 0) { + $metalId = 1; + } + $gold = abs((float) ($tx->gold ?? 0)); + if ($gold <= 0) { + continue; + } + $slices[$metalId] = ($slices[$metalId] ?? 0.0) + $gold; + } + + $totalGold = array_sum($slices); + $result = []; + foreach ($slices as $metalId => $gold) { + $cashShare = $totalGold > 0 + ? (float) $order->total_price * ($gold / $totalGold) + : 0.0; + $result[(int) $metalId] = ['gold' => $gold, 'cash' => $cashShare]; + } + + return $result; + } + + $amount = (float) ($order->amount ?? 0); + $totalPrice = (float) ($order->total_price ?? 0); + if ($amount <= 0) { + return []; + } + + $metalId = 1; + $tx = $order->transactions->first(static fn ($t) => $t->product_id); + if ($tx?->product) { + $metalId = max(1, (int) ($tx->product->metal_id ?? 1)); + } + + return [$metalId => ['gold' => $amount, 'cash' => $totalPrice]]; + } + + /** + * @param list $metalIds + * @return array + */ + public function initMetalBalanceBuckets(array $metalIds): array + { + $buckets = []; + foreach ($metalIds as $metalId) { + $buckets[(int) $metalId] = [ + 'gold' => 0.0, + 'cash' => 0.0, + 'gold_abs' => 0.0, + 'cash_abs' => 0.0, + ]; + } + + return $buckets; + } + + /** + * @param array $buckets + */ + public function applyOrderToBuckets(array &$buckets, Order $order): void + { + $slices = $this->resolveOrderMetalSlices($order); + foreach ($slices as $metalId => $slice) { + $metalId = (int) $metalId; + if (! isset($buckets[$metalId])) { + $buckets[$metalId] = [ + 'gold' => 0.0, + 'cash' => 0.0, + 'gold_abs' => 0.0, + 'cash_abs' => 0.0, + ]; + } + + $gold = (float) $slice['gold']; + $cash = (float) $slice['cash']; + + if ((int) $order->direction === 1) { + $buckets[$metalId]['gold'] += $gold; + $buckets[$metalId]['cash'] -= $cash; + } elseif ((int) $order->direction === 2) { + $buckets[$metalId]['gold'] -= $gold; + $buckets[$metalId]['cash'] += $cash; + } + + $buckets[$metalId]['gold_abs'] += $gold; + $buckets[$metalId]['cash_abs'] += $cash; + } + } + + /** + * @param array $limanBuckets + * @param array $kalhorBuckets + */ + public function buildMetalBalancesResponse(array $limanBuckets, array $kalhorBuckets): array + { + $metalIds = array_values(array_unique(array_merge( + array_keys($limanBuckets), + array_keys($kalhorBuckets) + ))); + sort($metalIds); + + $items = []; + $totalGoldBalance = 0.0; + + foreach ($metalIds as $metalId) { + $metalId = (int) $metalId; + $liman = $limanBuckets[$metalId] ?? [ + 'gold' => 0.0, + 'cash' => 0.0, + 'gold_abs' => 0.0, + 'cash_abs' => 0.0, + ]; + $kalhor = $kalhorBuckets[$metalId] ?? [ + 'gold' => 0.0, + 'cash' => 0.0, + 'gold_abs' => 0.0, + 'cash_abs' => 0.0, + ]; + + $goldBalance = -$liman['gold'] + $kalhor['gold']; + $cashBalance = -$liman['cash'] + $kalhor['cash']; + $goldBalanceOfLiman = -$liman['gold']; + $goldBalanceOfKalhor = $kalhor['gold']; + + $items[] = [ + 'metal_id' => $metalId, + 'label' => $this->metalLabel($metalId), + 'balance' => round($goldBalance, 4), + 'goldBalance' => round($goldBalance, 4), + 'cashBalance' => round($cashBalance, 2), + 'goldBalanceOfLiman' => round($goldBalanceOfLiman, 4), + 'goldBalanceOfKalhor' => round($goldBalanceOfKalhor, 4), + ]; + + $totalGoldBalance += $goldBalance; + } + + return [ + 'items' => $items, + 'totalGoldBalance' => round($totalGoldBalance, 4), + ]; + } + + /** @param array $buckets */ + public function applyKalhorSlice( + array &$buckets, + float $gold, + float $cash, + int $direction, + int $metalId = 1 + ): void { + if (! isset($buckets[$metalId])) { + $buckets[$metalId] = [ + 'gold' => 0.0, + 'cash' => 0.0, + 'gold_abs' => 0.0, + 'cash_abs' => 0.0, + ]; + } + + if ($direction === 2) { + $buckets[$metalId]['gold'] += $gold; + $buckets[$metalId]['cash'] -= $cash; + } elseif ($direction === 1) { + $buckets[$metalId]['gold'] -= $gold; + $buckets[$metalId]['cash'] += $cash; + } + + $buckets[$metalId]['gold_abs'] += $gold; + $buckets[$metalId]['cash_abs'] += $cash; + } + + public function resolveTransactionMetalId(mixed $transaction): int + { + $product = $this->transactionProduct($transaction); + if ($product !== null) { + $metalId = (int) ($product->metal_id ?? 0); + + return $metalId > 0 ? $metalId : 1; + } + + $walletProduct = $this->transactionWalletProduct($transaction); + if ($walletProduct !== null) { + $metalId = (int) ($walletProduct->metal_id ?? 0); + + return $metalId > 0 ? $metalId : 1; + } + + if ($this->isMainWalletMetalTransaction($transaction)) { + return $this->configService->mainWalletMetalId(); + } + + return 1; + } + + public function resolveTransactionDisplayTypeStr(mixed $transaction, ?int $metalId = null): ?string + { + $type = (int) ($this->lineValue($transaction, 'type') ?? 0); + if (!in_array($type, [1, 2, 7, 10], true)) { + return null; + } + + $metalId ??= $this->resolveTransactionMetalId($transaction); + $label = $this->metalLabel($metalId); + + return match ($type) { + 1 => 'خرید ' . $label, + 2 => 'فروش ' . $label, + 7 => 'واریز ' . $label, + 10 => 'برداشت ' . $label, + default => null, + }; + } + + public function transactionGoldGrams(mixed $transaction): float + { + $gold = (float) ($this->lineValue($transaction, 'gold') ?? 0); + if (abs($gold) < 0.0000001) { + return 0.0; + } + + $product = $this->transactionProduct($transaction); + if ($product !== null) { + return Controller::productLineWeightToGrams($product, $gold); + } + + return Controller::weightToGrams($gold); + } + + /** + * @param list $catalogMetalIds + * @return array + */ + public function buildTransactionMetalDisplayMap(mixed $transaction, array $catalogMetalIds): array + { + $map = []; + foreach ($catalogMetalIds as $metalId) { + $map[(string) (int) $metalId] = 0.0; + } + + $type = (int) ($this->lineValue($transaction, 'type') ?? 0); + if (in_array($type, [3, 4], true)) { + return $map; + } + + $gold = (float) ($this->lineValue($transaction, 'gold') ?? 0); + if (abs($gold) < 0.0000001) { + return $map; + } + + $metalId = $this->resolveTransactionMetalId($transaction); + $grams = abs($this->transactionGoldGrams($transaction)); + $display = $this->gramsToMetalDisplay($grams, $metalId); + if ($gold < 0) { + $display *= -1; + } + + $map[(string) $metalId] = round($display, 3); + + return $map; + } + + /** + * @param list $catalogMetalIds + */ + public function enrichTransactionTableRow(mixed &$row, array $catalogMetalIds): void + { + $metalMap = $this->buildTransactionMetalDisplayMap($row, $catalogMetalIds); + $metalId = $this->resolveTransactionMetalId($row); + $metalLabel = $this->metalLabel($metalId); + $displayType = $this->resolveTransactionDisplayTypeStr($row, $metalId); + + if (is_array($row)) { + $row['metal_gold'] = $metalMap; + $row['transaction_metal_id'] = $metalId; + $row['transaction_metal_label'] = $metalLabel; + if ($displayType !== null) { + $row['type_str'] = $displayType; + } + foreach ($catalogMetalIds as $mid) { + $row['metal_gold_' . $mid] = $metalMap[(string) $mid] ?? 0.0; + } + + return; + } + + if (is_object($row)) { + $row->metal_gold = $metalMap; + $row->transaction_metal_id = $metalId; + $row->transaction_metal_label = $metalLabel; + if ($displayType !== null) { + $row->type_str = $displayType; + } + foreach ($catalogMetalIds as $mid) { + $row->{'metal_gold_' . $mid} = $metalMap[(string) $mid] ?? 0.0; + } + } + } + + private function transactionProduct(mixed $transaction): ?object + { + if (is_object($transaction) && isset($transaction->product)) { + return $transaction->product; + } + + if (is_array($transaction) && isset($transaction['product'])) { + $product = $transaction['product']; + + return is_object($product) ? $product : (object) $product; + } + + return null; + } + + private function transactionWalletProduct(mixed $transaction): ?object + { + $wallet = $this->lineValue($transaction, 'wallet'); + if ($wallet === null) { + return null; + } + + if (is_object($wallet)) { + return isset($wallet->product) ? $wallet->product : null; + } + + if (is_array($wallet) && isset($wallet['product'])) { + $product = $wallet['product']; + + return is_object($product) ? $product : (object) $product; + } + + return null; + } + + private function isMainWalletMetalTransaction(mixed $transaction): bool + { + $type = (int) ($this->lineValue($transaction, 'type') ?? 0); + if (!in_array($type, [1, 2, 7, 10], true)) { + return false; + } + + $wallet = $this->lineValue($transaction, 'wallet'); + if ($wallet === null) { + return true; + } + + if (is_object($wallet)) { + return $wallet->product_id === null && $wallet->fund_id === null; + } + + if (is_array($wallet)) { + return ($wallet['product_id'] ?? null) === null && ($wallet['fund_id'] ?? null) === null; + } + + return true; + } + + public function resolveOrderMetalId(mixed $order): int + { + $product = $this->resolveOrderProduct($order); + if ($product !== null) { + $metalId = (int) ($product->metal_id ?? 0); + + return $metalId > 0 ? $metalId : 1; + } + + return 1; + } + + /** + * @param list $catalogMetalIds + */ + public function enrichOrderTableRow(mixed &$row, array $catalogMetalIds = []): void + { + $product = $this->resolveOrderProduct($row); + $unit = $this->resolveOrderAmountUnit($product); + $amount = (float) ($this->lineValue($row, 'amount') ?? 0); + $displayAmount = $this->resolveOrderDisplayAmount($amount, $product); + $metalId = $this->resolveOrderMetalId($row); + $metalLabel = $this->metalLabel($metalId); + $price = (float) ($this->lineValue($row, 'price') ?? 0); + + $metalPrices = []; + foreach ($catalogMetalIds as $mid) { + $mid = (int) $mid; + $metalPrices[(string) $mid] = $mid === $metalId ? $price : 0.0; + } + + $payload = [ + 'amount_unit' => $unit, + 'amount_display' => $displayAmount, + 'order_metal_id' => $metalId, + 'order_metal_label' => $metalLabel, + 'metal_prices' => $metalPrices, + ]; + + foreach ($catalogMetalIds as $mid) { + $payload['metal_price_' . (int) $mid] = $metalPrices[(string) (int) $mid] ?? 0.0; + } + + if (is_array($row)) { + foreach ($payload as $key => $value) { + $row[$key] = $value; + } + + return; + } + + if (is_object($row)) { + foreach ($payload as $key => $value) { + $row->{$key} = $value; + } + } + } + + public function resolveOrderAmountUnit(?object $product): string + { + if ($product === null) { + return Controller::tradeWeightUnit(); + } + + return Controller::productQuantityUnit(Controller::productUnitPayload($product)); + } + + private function resolveOrderDisplayAmount(float $amount, ?object $product): float + { + if ($product === null || abs($amount) < 0.0000001) { + return round($amount, 3); + } + + $payload = Controller::productUnitPayload($product); + $productUnit = trim((string) ($payload['unit'] ?? '')); + if (!Controller::isWeightBasedProductUnit($productUnit)) { + return round($amount, 3); + } + + $tradeUnit = Controller::tradeWeightUnit(); + if ($productUnit !== $tradeUnit) { + return round(Controller::convertWeight($amount, $tradeUnit, $productUnit), 3); + } + + return round($amount, 3); + } + + private function resolveOrderProduct(mixed $order): ?object + { + $transactions = $this->lineValue($order, 'transactions'); + if ($transactions instanceof \Illuminate\Support\Collection) { + $transactions = $transactions->all(); + } + + if (!is_array($transactions)) { + return null; + } + + foreach ($transactions as $tx) { + $product = is_object($tx) ? ($tx->product ?? null) : ($tx['product'] ?? null); + if ($product) { + return is_object($product) ? $product : (object) $product; + } + } + + return null; + } + + private function lineValue(mixed $line, string $key): mixed + { + if (is_array($line)) { + return $line[$key] ?? null; + } + + if (is_object($line)) { + return $line->{$key} ?? null; + } + + return null; + } +} diff --git a/app/Services/CategoryService.php b/app/Services/CategoryService.php index 6c86ea8..6d5c923 100644 --- a/app/Services/CategoryService.php +++ b/app/Services/CategoryService.php @@ -97,27 +97,22 @@ class CategoryService { $names = $this->collectTargetCategoryNames(); $created = 0; - $order = 0; + $maxOrder = (int) Category::max('sort_order'); foreach ($names as $name) { - $order++; $existing = Category::where('name', $name)->first(); if ($existing) { - if ((int) $existing->sort_order !== $order) { - $existing->sort_order = $order; - $existing->save(); - } - continue; } + $maxOrder++; Category::create([ 'name' => $name, 'active' => 1, 'show_in_filter' => 1, 'show_in_select' => 1, - 'sort_order' => $order, + 'sort_order' => $maxOrder, ]); $created++; } diff --git a/app/Services/MontzarService.php b/app/Services/MontzarService.php index 10b729d..0b0a7eb 100644 --- a/app/Services/MontzarService.php +++ b/app/Services/MontzarService.php @@ -78,12 +78,9 @@ class MontzarService $response = Http::timeout(7) ->acceptJson() - ->withOptions([ - 'curl' => [ - CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, - CURLOPT_TCP_FASTOPEN => 1, - ], - ]) + ->withOptions(self::httpClientOptions( + defined('CURLOPT_TCP_FASTOPEN') ? [(int) constant('CURLOPT_TCP_FASTOPEN') => 1] : [] + )) ->withToken($token) ->get($this->endpoint('/server/customer/product/list')) ->json(); diff --git a/app/Services/ProductDisplayPriceService.php b/app/Services/ProductDisplayPriceService.php index e8060ec..59cab0b 100644 --- a/app/Services/ProductDisplayPriceService.php +++ b/app/Services/ProductDisplayPriceService.php @@ -30,7 +30,22 @@ class ProductDisplayPriceService return false; } - return $this->isMeltedTradeProduct($product); + return $this->isMeltedTradeProduct($product) + || $this->usesQuantityAppPrice($product); + } + + /** معامله مقداری با قیمت اپ — نه قیمت دستی قدیمی با price پر در DB */ + private function usesQuantityAppPrice(Product $product): bool + { + if (! ($product->trade_by_quantity ?? false)) { + return false; + } + + if ($product->reading_method || (int) ($product->direct_price ?? 0) === 1) { + return false; + } + + return (int) ($product->price ?? 0) <= 0; } /** @@ -73,6 +88,19 @@ class ProductDisplayPriceService continue; } + if ($product->reading_method && ! $this->productUsesLastPriceIfZero($product)) { + $product->setAttribute('reference_provider_active', false); + $this->zeroProductPrice($product); + + continue; + } + + if ($product->reading_method) { + $product->setAttribute('reference_provider_active', true); + + continue; + } + if ($this->isWeightBasedStoreProduct($product)) { $this->enrichWeightBasedPrice($product); } @@ -262,25 +290,12 @@ class ProductDisplayPriceService /** @param array> $feed */ private function applyReferenceFeedPrice(Product $product, array $feed, ZaarboxPriceService $zaarbox): void { - if (Product::hasStoredPriceColumns()) { - $stored = (int) ($product->price ?? 0) + (int) ($product->price_profit ?? 0); - if ($stored > 0) { - $product->setAttribute('price', (int) ($product->price ?? 0)); - if (Schema::hasColumn($product->getTable(), 'price_profit')) { - $product->setAttribute('price_profit', (int) ($product->price_profit ?? 0)); - } - - return; - } - } - + $useLast = $this->productUsesLastPriceIfZero($product); $row = $zaarbox->findFeedRow($feed, (string) $product->reading_method); if ($row === null) { - $useLast = $this->productUsesLastPriceIfZero($product); + $product->setAttribute('reference_provider_active', $useLast); $offset = ProductPriceService::offsetPercent($product); - $historical = app(ConfigService::class)->getBool('show_last_price', false) - ? $this->resolveHistoricalBasePrice() - : 0; + $historical = $useLast ? $this->resolveHistoricalBasePrice() : 0; $computed = $historical > 0 ? ProductPriceService::adjustedTotal($historical, 0, $offset) : 0; @@ -289,9 +304,19 @@ class ProductDisplayPriceService return; } + $providerRaw = $zaarbox->resolveReferenceProviderTotal($row); + $providerActive = $useLast || $providerRaw > 0; + $product->setAttribute('reference_provider_active', $providerActive); + + if (! $useLast && $providerRaw <= 0) { + $this->zeroProductPrice($product); + + return; + } + $computed = $zaarbox->computeAdjustedProductPrice($product, $row); if ($computed <= 0) { - if ($this->productUsesLastPriceIfZero($product)) { + if ($useLast) { $fallback = $this->resolveLastPriceFallback($product); if ($fallback > 0) { $product->setAttribute('price', $fallback); @@ -424,7 +449,7 @@ class ProductDisplayPriceService $row = app(ZaarboxPriceService::class)->normalizeFeedRow($row); - return (int) round((float) ($row['price'] ?? 0) + (float) ($row['profit'] ?? 0)); + return app(ZaarboxPriceService::class)->resolveReferenceProviderTotal($row); } private function isMeltedTradeProduct(Product $product): bool diff --git a/app/Services/TransactionMetalService.php b/app/Services/TransactionMetalService.php deleted file mode 100644 index 4143251..0000000 --- a/app/Services/TransactionMetalService.php +++ /dev/null @@ -1,154 +0,0 @@ - 'طلا', - 2 => 'نقره', - 3 => 'مس', - ]; - - public function __construct( - private readonly ConfigService $configService - ) { - } - - public function metalLabel(int $metalId): string - { - return self::METAL_LABELS[$metalId] ?? Controller::metalTypeLabel(); - } - - /** @return array */ - public function catalogMetals(): array - { - return Product::query() - ->where('active', 1) - ->pluck('metal_id') - ->filter(fn ($id) => (int) $id > 0) - ->map(fn ($id) => (int) $id) - ->unique() - ->sort() - ->values() - ->map(fn (int $metalId) => [ - 'metal_id' => $metalId, - 'label' => $this->metalLabel($metalId), - 'display_unit' => Controller::tradeWeightUnit(), - ]) - ->all(); - } - - public function resolveMetalIdForTransaction(object $transaction): int - { - $productId = (int) ($transaction->product_id ?? 0); - if ($productId > 0) { - $metalId = (int) (Product::query()->whereKey($productId)->value('metal_id') ?? 0); - if ($metalId > 0) { - return $metalId; - } - } - - $wallet = $transaction->wallet ?? null; - if (is_numeric($wallet)) { - $wallet = Wallet::with('product:id,metal_id')->find($wallet); - } elseif (is_object($wallet) && !$wallet->relationLoaded('product')) { - $wallet->load('product:id,metal_id'); - } - - if (is_object($wallet) && $wallet->product_id && $wallet->product) { - $metalId = (int) ($wallet->product->metal_id ?? 0); - if ($metalId > 0) { - return $metalId; - } - } - - if ($this->isMainWalletMetalTransaction($transaction, $wallet)) { - return $this->configService->mainWalletMetalId(); - } - - return 1; - } - - private function isMainWalletMetalTransaction(object $transaction, mixed $wallet): bool - { - $type = (int) ($transaction->type ?? 0); - if (!in_array($type, [1, 2, 7, 10], true)) { - return false; - } - - if (!$wallet || !is_object($wallet)) { - return true; - } - - return $wallet->product_id === null && $wallet->fund_id === null; - } - - public function resolveDisplayTypeStr(object $transaction): ?string - { - $type = (int) ($transaction->type ?? 0); - if (!in_array($type, [1, 2, 7, 10], true)) { - return null; - } - - $metalLabel = $this->metalLabel($this->resolveMetalIdForTransaction($transaction)); - - return match ($type) { - 1 => Controller::productBuyTitle($metalLabel), - 2 => Controller::productSellTitle($metalLabel), - 7 => 'واریز ' . $metalLabel, - 10 => 'برداشت ' . $metalLabel, - default => null, - }; - } - - /** @param iterable|object> $rows */ - public function enrichTransactionRows(iterable $rows): array - { - $metalIds = array_column($this->catalogMetals(), 'metal_id'); - $enriched = []; - - foreach ($rows as $row) { - $record = is_array($row) ? (object) $row : $row; - $metalId = $this->resolveMetalIdForTransaction($record); - $displayType = $this->resolveDisplayTypeStr($record); - $type = (int) (is_array($row) ? ($row['type'] ?? 0) : ($row->type ?? 0)); - $gold = (float) (is_array($row) ? ($row['gold'] ?? 0) : ($row->gold ?? 0)); - $isMoney = in_array($type, [3, 4], true); - - if ($displayType !== null) { - if (is_array($row)) { - $row['type_str'] = $displayType; - } else { - $row->type_str = $displayType; - } - } - - foreach ($metalIds as $mid) { - $key = "metal_gold_{$mid}"; - $value = (!$isMoney && $metalId === $mid) ? $gold : 0; - if (is_array($row)) { - $row[$key] = $value; - } else { - $row->{$key} = $value; - } - } - - if (is_array($row)) { - $row['transaction_metal_id'] = $metalId; - $row['transaction_metal_label'] = $this->metalLabel($metalId); - } else { - $row->transaction_metal_id = $metalId; - $row->transaction_metal_label = $this->metalLabel($metalId); - } - - $enriched[] = $row; - } - - return $enriched; - } -} diff --git a/app/Services/WalletBalanceService.php b/app/Services/WalletBalanceService.php index d61db65..202a91e 100644 --- a/app/Services/WalletBalanceService.php +++ b/app/Services/WalletBalanceService.php @@ -2,6 +2,9 @@ namespace App\Services; +use App\Http\Controllers\Controller; +use App\Models\Price; +use App\Models\Product; use Illuminate\Support\Collection; /** @@ -12,8 +15,10 @@ use Illuminate\Support\Collection; */ class WalletBalanceService { - public function __construct(private ConfigService $configService) - { + public function __construct( + private ConfigService $configService, + private BalanceReportService $balanceReportService + ) { } public function mainWalletProductIds(): array @@ -79,6 +84,240 @@ class WalletBalanceService return $sum; } + /** @return list */ + public function catalogMetalIds(): array + { + $ids = $this->balanceReportService->catalogMetalIds(); + + return $ids !== [] ? $ids : [1]; + } + + public function resolveMainWalletMetalId(): int + { + $mainWalletProductIds = $this->mainWalletProductIds(); + if ($mainWalletProductIds !== []) { + $product = Product::query() + ->whereIn('id', $mainWalletProductIds) + ->orderBy('id') + ->first(); + + return max(1, (int) ($product?->metal_id ?? 1)); + } + + $metalIds = $this->catalogMetalIds(); + + return count($metalIds) === 1 ? (int) $metalIds[0] : 1; + } + + /** + * فلز مرتبط با یک ردیف کیف در جدول ادمین. + * + * @param Collection|array|null $wallets + */ + public function resolveWalletLineMetalId(mixed $line, ?int $mainWalletMetalId = null): int + { + $fundId = $this->lineValue($line, 'fund_id'); + $productId = $this->lineValue($line, 'product_id'); + + if ($fundId !== null) { + return 1; + } + + if ($productId !== null) { + $product = $this->lineProduct($line); + $metalId = (int) ($product?->metal_id ?? 0); + + return $metalId > 0 ? $metalId : 1; + } + + return $mainWalletMetalId ?? $this->resolveMainWalletMetalId(); + } + + /** + * @param list $catalogMetalIds + * @return array + */ + public function walletLineMetalGoldMap( + mixed $line, + array $catalogMetalIds, + ?int $mainWalletMetalId = null + ): array { + $map = []; + foreach ($catalogMetalIds as $metalId) { + $map[(string) $metalId] = 0.0; + } + + $goldGrams = $this->walletLineGoldGrams($line); + if ($goldGrams <= 0) { + return $map; + } + + $metalId = $this->resolveWalletLineMetalId($line, $mainWalletMetalId); + $map[(string) $metalId] = round( + $this->balanceReportService->gramsToMetalDisplay($goldGrams, $metalId), + 3 + ); + + return $map; + } + + /** + * @param Collection|array $walletLines + * @param list $catalogMetalIds + * @return array جمع به گرم + */ + public function sumWalletMetalGrams(Collection|array $walletLines, array $catalogMetalIds): array + { + $buckets = []; + foreach ($catalogMetalIds as $metalId) { + $buckets[(string) (int) $metalId] = 0.0; + } + + $mainWalletMetalId = $this->resolveMainWalletMetalId(); + foreach ($this->walletLines($walletLines) as $line) { + $goldGrams = $this->walletLineGoldGrams($line); + if ($goldGrams <= 0) { + continue; + } + + $metalId = (string) $this->resolveWalletLineMetalId($line, $mainWalletMetalId); + $buckets[$metalId] = ($buckets[$metalId] ?? 0.0) + $goldGrams; + } + + return $buckets; + } + + /** + * @param Collection|array $walletLines + * @param list $catalogMetalIds + * @return list + */ + public function buildWalletMetalSums(Collection|array $walletLines, array $catalogMetalIds): array + { + $gramBuckets = $this->sumWalletMetalGrams($walletLines, $catalogMetalIds); + + $items = []; + foreach ($catalogMetalIds as $metalId) { + $metalId = (int) $metalId; + $goldGrams = (float) ($gramBuckets[(string) $metalId] ?? 0.0); + $displayGold = round($this->balanceReportService->gramsToMetalDisplay($goldGrams, $metalId), 3); + $price = $this->livePricePerGram($metalId); + $items[] = [ + 'metal_id' => $metalId, + 'label' => $this->balanceReportService->metalLabel($metalId), + 'gold' => $displayGold, + 'display_unit' => $this->balanceReportService->metalDisplayUnit($metalId), + 'goldToCash' => round($goldGrams * $price, 2), + ]; + } + + return $items; + } + + /** + * @param Collection|array $walletLines + * @param list $catalogMetalIds + * @return array مقادیر نمایشی + */ + public function buildCustomerWalletMetalDisplayMap(Collection|array $walletLines, array $catalogMetalIds): array + { + $gramBuckets = $this->sumWalletMetalGrams($walletLines, $catalogMetalIds); + $map = []; + + foreach ($catalogMetalIds as $metalId) { + $metalId = (int) $metalId; + $goldGrams = (float) ($gramBuckets[(string) $metalId] ?? 0.0); + $map[(string) $metalId] = round( + $this->balanceReportService->gramsToMetalDisplay($goldGrams, $metalId), + 3 + ); + } + + return $map; + } + + /** + * @param list $catalogMetalIds + */ + public function enrichWalletTableRow(mixed &$row, array $catalogMetalIds, int $mainWalletMetalId): void + { + $metalMap = $this->walletLineMetalGoldMap($row, $catalogMetalIds, $mainWalletMetalId); + + if (is_array($row)) { + $row['metal_gold'] = $metalMap; + foreach ($catalogMetalIds as $metalId) { + $row['metal_gold_' . $metalId] = $metalMap[(string) $metalId] ?? 0.0; + } + + return; + } + + if (is_object($row)) { + $row->metal_gold = $metalMap; + foreach ($catalogMetalIds as $metalId) { + $row->{'metal_gold_' . $metalId} = $metalMap[(string) $metalId] ?? 0.0; + } + } + } + + private function livePricePerGram(int $metalId): float + { + if ($metalId === 1) { + $live = (int) app(ProductDisplayPriceService::class)->resolveAppBasePrice(); + if ($live > 0) { + return (float) $live; + } + + return (float) (Price::latest('id')->value('price') ?? 0); + } + + $product = Product::query() + ->where('active', 1) + ->where('metal_id', $metalId) + ->where('price', '>', 0) + ->orderBy('id') + ->first(); + + if (!$product) { + return 0.0; + } + + $unitPrice = (float) $product->price + (float) ($product->price_profit ?? 0); + $amount = (float) ($product->amount ?? 1); + + return $amount > 0 ? $unitPrice * $amount : $unitPrice; + } + + private function lineProduct(mixed $line): ?object + { + if (is_object($line) && isset($line->product)) { + return $line->product; + } + + if (is_array($line) && isset($line['product'])) { + $product = $line['product']; + + return is_object($product) ? $product : (object) $product; + } + + return null; + } + + private function walletLineGoldGrams(mixed $line): float + { + $gold = (float) ($this->lineValue($line, 'gold') ?? 0); + if ($gold <= 0) { + return 0.0; + } + + $product = $this->lineProduct($line); + if ($product !== null) { + return Controller::productLineWeightToGrams($product, $gold); + } + + return Controller::weightToGrams($gold); + } + /** @param Collection|array|null $wallets */ private function mainWalletLine(Collection|array|null $wallets): mixed { diff --git a/app/Services/WebsocketBroadcastService.php b/app/Services/WebsocketBroadcastService.php index 7b35c6d..6a116cb 100644 --- a/app/Services/WebsocketBroadcastService.php +++ b/app/Services/WebsocketBroadcastService.php @@ -84,11 +84,7 @@ class WebsocketBroadcastService try { $response = Http::timeout(3) ->withHeaders(['Content-Type' => 'application/json']) - ->withOptions([ - 'curl' => [ - CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, - ], - ]) + ->withOptions(\App\Http\Controllers\Controller::httpClientOptions()) ->post($url, $body); if (! $response->successful()) { diff --git a/app/Services/ZaarboxPriceService.php b/app/Services/ZaarboxPriceService.php index 1b0be70..9437e3b 100644 --- a/app/Services/ZaarboxPriceService.php +++ b/app/Services/ZaarboxPriceService.php @@ -221,9 +221,7 @@ class ZaarboxPriceService return $empty; } - $base = (float) ($row['price'] ?? 0); - $profit = (float) ($row['profit'] ?? 0); - $orderPrice = (int) round($base + $profit); + $orderPrice = $this->resolveReferenceProviderTotal($row); return ['price' => $orderPrice, 'order_price' => $orderPrice, 'raw' => $row]; } catch (\Throwable $e) { @@ -339,7 +337,7 @@ class ZaarboxPriceService return $empty; } - $orderPrice = (int) round((float) ($row['price'] ?? 0) + (float) ($row['profit'] ?? 0)); + $orderPrice = $this->resolveReferenceProviderTotal($row); return ['order_price' => $orderPrice, 'raw' => $row]; } catch (\Throwable $e) { @@ -386,23 +384,24 @@ class ZaarboxPriceService } $row = $this->normalizeFeedRow($RPrice); + $parts = $this->resolveReferenceBaseProfit($row); if (($row['name'] ?? '') === 'TGJU فلز مس') { - $copper = ($row['price'] ?? 0) + ($row['profit'] ?? 0); + $copper = $parts['price'] + $parts['profit']; } if (($row['name'] ?? '') === 'TGJU دلار') { - $dollar = ($row['price'] ?? 0) + ($row['profit'] ?? 0); + $dollar = $parts['price'] + $parts['profit']; } ProductPriceService::syncFromReference( $product, - (int) $row['price'], - (int) ($row['profit'] ?? 0) + $parts['price'], + $parts['profit'] ); $productPrice = (float) ProductPriceService::adjustedTotal( - (int) $row['price'], - (int) ($row['profit'] ?? 0), + $parts['price'], + $parts['profit'], ProductPriceService::offsetPercent($product) ); @@ -477,22 +476,6 @@ class ZaarboxPriceService return null; } - /** - * قیمت نمایشی محصول از یک ردیف فید + offset درصد — بدون نوشتن DB - * - * @param array $row - */ - public function computeAdjustedProductPrice(Product $product, array $row): int - { - $normalized = $this->normalizeFeedRow($row); - - return ProductPriceService::adjustedTotal( - (int) ($normalized['price'] ?? 0), - (int) ($normalized['profit'] ?? 0), - ProductPriceService::offsetPercent($product) - ); - } - /** @param array $row */ public function normalizeFeedRow(array $row): array { @@ -503,6 +486,55 @@ class ZaarboxPriceService return $row; } + /** + * base/profit مرجع zaarbox — وقتی price صفر است provider قیمت نداده (profit مقدار قدیمی است). + * + * @param array $row + * @return array{price: int, profit: int} + */ + public function resolveReferenceBaseProfit(array $row): array + { + $normalized = $this->normalizeFeedRow($row); + $price = (int) round((float) ($normalized['price'] ?? 0)); + if ($price <= 0) { + return ['price' => 0, 'profit' => 0]; + } + + return [ + 'price' => $price, + 'profit' => (int) round((float) ($normalized['profit'] ?? 0)), + ]; + } + + /** + * @param array $row + */ + public function resolveReferenceProviderTotal(array $row): int + { + $parts = $this->resolveReferenceBaseProfit($row); + + return $parts['price'] + $parts['profit']; + } + + /** + * قیمت نمایشی محصول از یک ردیف فید + offset درصد — بدون نوشتن DB + * + * @param array $row + */ + public function computeAdjustedProductPrice(Product $product, array $row): int + { + $parts = $this->resolveReferenceBaseProfit($row); + if ($parts['price'] <= 0) { + return 0; + } + + return ProductPriceService::adjustedTotal( + $parts['price'], + $parts['profit'], + ProductPriceService::offsetPercent($product) + ); + } + public function syncProductFromApi(Product $product): bool { if (!Product::hasStoredPriceColumns()) { @@ -524,11 +556,12 @@ class ZaarboxPriceService } $row = $this->normalizeFeedRow($RPrice); + $parts = $this->resolveReferenceBaseProfit($row); ProductPriceService::syncFromReference( $product, - (int) $row['price'], - (int) ($row['profit'] ?? 0) + $parts['price'], + $parts['profit'] ); return true; diff --git a/database/sql/deploy_settings_const.sql b/database/sql/deploy_settings_const.sql index f6f3bba..3e3cb03 100644 --- a/database/sql/deploy_settings_const.sql +++ b/database/sql/deploy_settings_const.sql @@ -371,6 +371,10 @@ INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `de SELECT 'metal_type', 'طلای آبشده', 'عنوان فلز در UI', 0, 1, NULL WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'metal_type' LIMIT 1); +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'main_metal', 'طلا', 'نام فلز اصلی (ادمین)', 0, 1, NULL, 'text' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'main_metal' LIMIT 1); + -- درگاه — کلیدهای اختیاری payping INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`) SELECT 'payping_fin_sync_enabled', 'false', 'همگام‌سازی fin', 1, 1, NULL @@ -425,19 +429,19 @@ SELECT 'referral_id', 'required', 'کد معرف اجباری', 1, 1, 'روشن WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'referral_id' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'fee_single', '500000', 'اختلاف پایه نمایش', 1, 1, NULL, 'number' +SELECT 'fee_single', '500000', 'اختلاف پایه نمایش', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_single' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'buy_fee', '', 'سود خرید نمایش', 1, 1, NULL, 'number' +SELECT 'buy_fee', '', 'سود خرید نمایش', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'buy_fee' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'sell_fee', '', 'سود فروش نمایش', 1, 1, NULL, 'number' +SELECT 'sell_fee', '', 'سود فروش نمایش', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'sell_fee' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'fee_cash', '500000', 'کارمزد ریالی سفارش (legacy)', 0, 1, NULL, 'number' +SELECT 'fee_cash', '500000', 'کارمزد ریالی سفارش (legacy)', 0, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_cash' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) @@ -445,15 +449,15 @@ SELECT 'fee_service_charge_percentage', '0.005', 'درصد کارمزد سفار WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_service_charge_percentage' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'min_withdraw', '', 'حداقل برداشت (ریال)', 1, 1, NULL, 'number' +SELECT 'min_withdraw', '', 'حداقل برداشت (ریال)', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'min_withdraw' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'min_first_buy', '', 'حداقل اولین خرید (ریال)', 1, 1, NULL, 'number' +SELECT 'min_first_buy', '', 'حداقل اولین خرید (ریال)', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'min_first_buy' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'minimum_sell', '', 'حداقل فروش', 1, 1, NULL, 'number' +SELECT 'minimum_sell', '', 'حداقل فروش', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'minimum_sell' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) @@ -639,12 +643,18 @@ UPDATE `settings` SET `type` = 'switch' WHERE `key` IN ( 'banner_enabled' ); --- number -UPDATE `settings` SET `type` = 'number' WHERE `key` IN ( - 'fee_single', 'buy_fee', 'sell_fee', 'fee_cash', 'fee_service_charge_percentage', 'fee_free', +-- price (مبالغ ریالی) +UPDATE `settings` SET `type` = 'price' WHERE `key` IN ( + 'fee_single', 'buy_fee', 'sell_fee', 'fee_cash', 'min_withdraw', 'min_first_buy', 'minimum_sell', - 'montzar_product_id', 'gift_for_referrer', - 'gift_for_referral', 'up_price', 'down_price', 'payping_fin_customer_id', 'tahesab_port', + 'up_price', 'down_price' +); + +-- number (شماره، شناسه، درصد، تعداد — بدون جداکننده هزارگان) +UPDATE `settings` SET `type` = 'number' WHERE `key` IN ( + 'fee_service_charge_percentage', 'fee_free', 'service_charge_percentage', + 'montzar_product_id', 'gift_for_referrer', 'gift_for_referral', + 'payping_fin_customer_id', 'tahesab_port', 'referral_limit', 'deposit_sms_support_number' ); @@ -659,8 +669,8 @@ UPDATE `settings` SET `type` = 'text' WHERE `key` IN ( 'payping_fin_token', 'payping_gateway_item', 'tahesab_host', 'tahesab_token', 'tahesab_db', 'node_client_id', 'node_shared_secret', 'sse_path', - 'tahesab_bank_account', 'telegram_bot_token', 'bale_bot_token', 'support_messenger_id', 'referral_limit', - 'referral_bonus_amount', 'balance_report_exclude_customer_id', 'metal_type', 'montify_name', 'price_extreme_date', + 'tahesab_bank_account', 'telegram_bot_token', 'bale_bot_token', 'support_messenger_id', + 'referral_bonus_amount', 'balance_report_exclude_customer_id', 'metal_type', 'main_metal', 'montify_name', 'price_extreme_date', 'default_vc_number', 'liman_token' ); @@ -916,6 +926,7 @@ UPDATE `settings` SET `description` = 'آدرس تصویر لوگو در صفح UPDATE `settings` SET `description` = 'عنوان کوتاه برای نصب PWA روی موبایل. از دیپلوی مدیریت می‌شود.' WHERE `key` = 'title_pwa'; UPDATE `settings` SET `description` = 'عنوان نمایش‌داده‌شده در تب مرورگر (favicon). از دیپلوی مدیریت می‌شود.' WHERE `key` = 'title_fav'; UPDATE `settings` SET `description` = 'عنوان فلز اصلی در رابط کاربر (مثلاً «طلای آبشده» یا «کاتد مس»). در دارایی، کیف پول و برچسب‌های معامله دیده می‌شود. فقط از دیپلوی/SQL تغییر می‌کند.' WHERE `key` = 'metal_type'; +UPDATE `settings` SET `title_fa` = 'نام فلز اصلی (ادمین)', `description` = 'نام کوتاه فلز برای برچسب‌های پنل ادمین (مثلاً «مس» یا «طلا»). «تراز طلایی» می‌شود «تراز مس» — نه «تراز مسی». فقط از دیپلوی/SQL تغییر می‌کند.' WHERE `key` = 'main_metal'; UPDATE `settings` SET `description` = 'واحد نمایش وزن در دارایی‌ها، کیف پول و جزئیات معاملات|گرم,کیلوگرم' WHERE `key` = 'unit'; UPDATE `settings` SET `description` = 'روشن: کاربران جدید می‌توانند ثبت‌نام کنند. خاموش: فقط ورود کاربران موجود مجاز است.' WHERE `key` = 'open_register'; UPDATE `settings` SET `description` = 'روشن: حساب کاربر بلافاصله پس از ثبت‌نام فعال می‌شود. خاموش: نیاز به تأیید دستی ادمین دارد.' WHERE `key` = 'auto_active_customer'; @@ -1060,7 +1071,7 @@ UPDATE `settings` SET `can_edit` = 0 WHERE `key` IN ( 'has_page_earn_money', 'is_store', 'logo_login', 'logo_register', 'title_pwa', 'title_fav', - 'main_product', 'main_wallet_products', 'metal_type', + 'main_product', 'main_wallet_products', 'metal_type', 'main_metal', 'montify_name', 'montzar_base_url', 'montzar_closed', 'montzar_diff', 'montzar_last_product_log', 'montzar_live_price', 'montzar_mazaneh', @@ -1104,6 +1115,19 @@ UPDATE `settings` SET `can_edit` = 1 WHERE `key` IN ( 'text_about', 'text_rule' ); +-- تفکیک نهایی type (بعد از همه batchها — override روی دیتابیس موجود) +UPDATE `settings` SET `type` = 'price' WHERE `key` IN ( + 'fee_single', 'buy_fee', 'sell_fee', 'fee_cash', + 'min_withdraw', 'min_first_buy', 'minimum_sell', + 'up_price', 'down_price' +); +UPDATE `settings` SET `type` = 'number' WHERE `key` IN ( + 'fee_service_charge_percentage', 'fee_free', 'service_charge_percentage', + 'montzar_product_id', 'gift_for_referrer', 'gift_for_referral', + 'payping_fin_customer_id', 'tahesab_port', 'referral_limit', + 'deposit_sms_support_number' +); + COMMIT; SELECT 'deploy_settings_const.sql finished' AS status; diff --git a/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql b/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql index b83f50f..6af90fa 100644 --- a/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql +++ b/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql @@ -351,6 +351,10 @@ INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `de SELECT 'metal_type', 'طلای آبشده', 'عنوان فلز در UI', 0, 1, NULL WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'metal_type' LIMIT 1); +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'main_metal', 'طلا', 'نام فلز اصلی (ادمین)', 0, 1, NULL, 'text' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'main_metal' LIMIT 1); + -- درگاه — کلیدهای اختیاری payping INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`) SELECT 'payping_fin_sync_enabled', 'false', 'همگام‌سازی fin', 1, 1, NULL @@ -405,19 +409,19 @@ SELECT 'referral_id', 'required', 'کد معرف اجباری', 1, 1, 'روشن WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'referral_id' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'fee_single', '500000', 'اختلاف پایه نمایش', 1, 1, NULL, 'number' +SELECT 'fee_single', '500000', 'اختلاف پایه نمایش', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_single' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'buy_fee', '', 'سود خرید نمایش', 1, 1, NULL, 'number' +SELECT 'buy_fee', '', 'سود خرید نمایش', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'buy_fee' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'sell_fee', '', 'سود فروش نمایش', 1, 1, NULL, 'number' +SELECT 'sell_fee', '', 'سود فروش نمایش', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'sell_fee' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'fee_cash', '500000', 'کارمزد ریالی سفارش (legacy)', 0, 1, NULL, 'number' +SELECT 'fee_cash', '500000', 'کارمزد ریالی سفارش (legacy)', 0, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_cash' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) @@ -425,15 +429,15 @@ SELECT 'fee_service_charge_percentage', '0.005', 'درصد کارمزد سفار WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_service_charge_percentage' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'min_withdraw', '', 'حداقل برداشت (ریال)', 1, 1, NULL, 'number' +SELECT 'min_withdraw', '', 'حداقل برداشت (ریال)', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'min_withdraw' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'min_first_buy', '', 'حداقل اولین خرید (ریال)', 1, 1, NULL, 'number' +SELECT 'min_first_buy', '', 'حداقل اولین خرید (ریال)', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'min_first_buy' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'minimum_sell', '', 'حداقل فروش', 1, 1, NULL, 'number' +SELECT 'minimum_sell', '', 'حداقل فروش', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'minimum_sell' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) @@ -615,12 +619,18 @@ UPDATE `settings` SET `type` = 'switch' WHERE `key` IN ( 'register_referral_sms_to_referrer', 'register_referral_bonus_message', 'price_from_nasiryan' ); --- number -UPDATE `settings` SET `type` = 'number' WHERE `key` IN ( - 'fee_single', 'buy_fee', 'sell_fee', 'fee_cash', 'fee_service_charge_percentage', 'fee_free', +-- price (مبالغ ریالی) +UPDATE `settings` SET `type` = 'price' WHERE `key` IN ( + 'fee_single', 'buy_fee', 'sell_fee', 'fee_cash', 'min_withdraw', 'min_first_buy', 'minimum_sell', - 'montzar_product_id', 'gift_for_referrer', - 'gift_for_referral', 'up_price', 'down_price', 'payping_fin_customer_id', 'tahesab_port', + 'up_price', 'down_price' +); + +-- number (شماره، شناسه، درصد، تعداد — بدون جداکننده هزارگان) +UPDATE `settings` SET `type` = 'number' WHERE `key` IN ( + 'fee_service_charge_percentage', 'fee_free', 'service_charge_percentage', + 'montzar_product_id', 'gift_for_referrer', 'gift_for_referral', + 'payping_fin_customer_id', 'tahesab_port', 'referral_limit', 'deposit_sms_support_number' ); @@ -635,8 +645,8 @@ UPDATE `settings` SET `type` = 'text' WHERE `key` IN ( 'payping_fin_token', 'payping_gateway_item', 'tahesab_host', 'tahesab_token', 'tahesab_db', 'node_client_id', 'node_shared_secret', 'sse_path', - 'tahesab_bank_account', 'telegram_bot_token', 'bale_bot_token', 'support_messenger_id', 'referral_limit', - 'referral_bonus_amount', 'balance_report_exclude_customer_id', 'metal_type', 'montify_name', 'price_extreme_date', + 'tahesab_bank_account', 'telegram_bot_token', 'bale_bot_token', 'support_messenger_id', + 'referral_bonus_amount', 'balance_report_exclude_customer_id', 'metal_type', 'main_metal', 'montify_name', 'price_extreme_date', 'default_vc_number', 'liman_token' ); @@ -920,6 +930,7 @@ UPDATE `settings` SET `description` = 'آدرس تصویر لوگو در صفح UPDATE `settings` SET `description` = 'عنوان کوتاه برای نصب PWA روی موبایل. از دیپلوی مدیریت می‌شود.' WHERE `key` = 'title_pwa'; UPDATE `settings` SET `description` = 'عنوان نمایش‌داده‌شده در تب مرورگر (favicon). از دیپلوی مدیریت می‌شود.' WHERE `key` = 'title_fav'; UPDATE `settings` SET `description` = 'عنوان فلز اصلی در رابط کاربر (مثلاً «طلای آبشده» یا «کاتد مس»). در دارایی، کیف پول و برچسب‌های معامله دیده می‌شود. فقط از دیپلوی/SQL تغییر می‌کند.' WHERE `key` = 'metal_type'; +UPDATE `settings` SET `title_fa` = 'نام فلز اصلی (ادمین)', `description` = 'نام کوتاه فلز برای برچسب‌های پنل ادمین (مثلاً «مس» یا «طلا»). «تراز طلایی» می‌شود «تراز مس» — نه «تراز مسی». فقط از دیپلوی/SQL تغییر می‌کند.' WHERE `key` = 'main_metal'; UPDATE `settings` SET `description` = 'واحد نمایش وزن در دارایی‌ها، کیف پول و جزئیات معاملات|گرم,کیلوگرم' WHERE `key` = 'unit'; UPDATE `settings` SET `description` = 'روشن: کاربران جدید می‌توانند ثبت‌نام کنند. خاموش: فقط ورود کاربران موجود مجاز است.' WHERE `key` = 'open_register'; UPDATE `settings` SET `description` = 'روشن: حساب کاربر بلافاصله پس از ثبت‌نام فعال می‌شود. خاموش: نیاز به تأیید دستی ادمین دارد.' WHERE `key` = 'auto_active_customer'; @@ -1158,6 +1169,19 @@ JOIN ( ) d ON d.`key` = s.`key` SET s.`can_edit` = d.`can_edit`; +-- تفکیک نهایی type (بعد از همه batchها — override روی دیتابیس موجود) +UPDATE `settings` SET `type` = 'price' WHERE `key` IN ( + 'fee_single', 'buy_fee', 'sell_fee', 'fee_cash', + 'min_withdraw', 'min_first_buy', 'minimum_sell', + 'up_price', 'down_price' +); +UPDATE `settings` SET `type` = 'number' WHERE `key` IN ( + 'fee_service_charge_percentage', 'fee_free', 'service_charge_percentage', + 'montzar_product_id', 'gift_for_referrer', 'gift_for_referral', + 'payping_fin_customer_id', 'tahesab_port', 'referral_limit', + 'deposit_sms_support_number' +); + COMMIT; SELECT 'deploy_settings_const.sql finished' AS status; diff --git a/packages/rooyesh/wallet/src/models/Transaction.php b/packages/rooyesh/wallet/src/models/Transaction.php index 3fab637..9843a8f 100644 --- a/packages/rooyesh/wallet/src/models/Transaction.php +++ b/packages/rooyesh/wallet/src/models/Transaction.php @@ -43,7 +43,16 @@ class Transaction extends Model } public function product() { - return $this->belongsTo('App\Models\Product','product_id')->select(['id','title','group_type']); + return $this->belongsTo('App\Models\Product', 'product_id')->select([ + 'id', + 'title', + 'group_type', + 'unit', + 'direct_price', + 'amount', + 'trade_by_quantity', + 'metal_id', + ]); } public function order() { diff --git a/packages/rooyesh/wallet/src/models/Wallet.php b/packages/rooyesh/wallet/src/models/Wallet.php index f1d1e88..e74f9fc 100644 --- a/packages/rooyesh/wallet/src/models/Wallet.php +++ b/packages/rooyesh/wallet/src/models/Wallet.php @@ -20,9 +20,9 @@ class Wallet extends Model { return $this->belongsTo('App\Models\Fund','fund_id')->select(['id','title']); } - public function Product() + public function product() { - return $this->belongsTo('App\Models\Product','product_id'); + return $this->belongsTo('App\Models\Product', 'product_id'); } /** کیف اصلی مشتری — بدون fund و بدون product */ diff --git a/routes/admin.php b/routes/admin.php index 19f5d83..201a319 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -104,6 +104,8 @@ Route::middleware('auth:user')->group(function (){ //-------------wallet Route::post('/wallet/charge',[WalletController::class,'charge']); Route::post('/wallet/charge_gold',[WalletController::class,'charge_gold']); + Route::post('/wallet/charge_wallet',[WalletController::class,'charge_wallet']); + Route::get('/wallet/charge-targets',[WalletController::class,'chargeTargets']); Route::post('/wallet/table',[WalletController::class,'indexWallet']); Route::post('/wallet/transaction/table',[WalletController::class,'index']); Route::post('/wallet/transaction/update',[WalletController::class,'updateTransaction']);