'طلا', 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->resolveTransactionProduct($transaction); if ($product !== null) { $metalId = (int) ($product->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 = $this->resolveTransactionType($transaction); if (in_array($type, [3, 4, 8, 9], true)) { return null; } $productLabel = $this->resolveTransactionProductLabel($transaction); if ($productLabel === null) { return null; } $prefix = $this->resolveTransactionActionPrefix($transaction) ?? $this->inferActionPrefixFromLabel(trim((string) ($this->lineValue($transaction, 'type_str') ?? ''))) ?? $this->inferActionPrefixFromGold($transaction); if ($prefix === null) { return null; } return trim($prefix . ' ' . $productLabel); } private function resolveTransactionType(mixed $transaction): int { $type = $this->lineValue($transaction, 'type'); if ($type !== null && $type !== '' && is_numeric($type)) { return (int) $type; } if (is_object($transaction) && method_exists($transaction, 'getAttributes')) { $attrs = $transaction->getAttributes(); if (isset($attrs['type']) && is_numeric($attrs['type'])) { return (int) $attrs['type']; } if (isset($attrs['type_str']) && is_numeric($attrs['type_str'])) { return (int) $attrs['type_str']; } } return 0; } private function resolveTransactionActionPrefix(mixed $transaction): ?string { $type = $this->resolveTransactionType($transaction); $gold = (float) ($this->lineValue($transaction, 'gold') ?? 0); return match (true) { $type === 1 => 'خرید', $type === 2 => 'فروش', $type === 51 => 'اصلاح', in_array($type, [6, 10], true) => 'برداشت', $type === 5 => 'واریز', $type === 7 => $gold >= 0 ? 'واریز' : 'برداشت', $type === 12 => 'برداشت وجه', abs($gold) > 0 => $gold > 0 ? 'واریز' : 'برداشت', default => null, }; } private function inferActionPrefixFromGold(mixed $transaction): ?string { $gold = (float) ($this->lineValue($transaction, 'gold') ?? 0); if (abs($gold) < 0.0000001) { return null; } return $gold > 0 ? 'واریز' : 'برداشت'; } private function inferActionPrefixFromLabel(string $label): ?string { if (preg_match('/^خرید/u', $label)) { return 'خرید'; } if (preg_match('/^فروش/u', $label)) { return 'فروش'; } if (preg_match('/^برداشت/u', $label)) { return 'برداشت'; } if (preg_match('/^(واریز|موجودی)/u', $label)) { return 'واریز'; } return null; } private function shouldReplaceGenericTransactionLabel(string $label): bool { if ($label === 'تعریف نشده' || $label === '') { return true; } $generic = [ 'واریز', 'برداشت', 'واریز طلایی', 'برداشت طلایی', 'خرید طلا', 'فروش طلا', ]; return in_array($label, $generic, true); } public function resolveTransactionProductLabel(mixed $transaction): ?string { $product = $this->resolveTransactionProduct($transaction); if ($product !== null && !empty($product->title)) { return trim((string) $product->title); } if ($this->isMainWalletMetalTransaction($transaction)) { $labels = $this->configService->mainWalletProductLabels(); return $labels !== '' ? $labels : null; } return $this->parseProductLabelFromNote($transaction); } private function parseProductLabelFromNote(mixed $transaction): ?string { $note = trim((string) ($this->lineValue($transaction, 'note') ?? '')); if ($note === '') { return null; } if (preg_match('/(?:برداشت|واریز)\s+محصول\s+(.+?)\s+شماره\s+سند/u', $note, $matches)) { return trim($matches[1]); } return null; } private function resolveTransactionTitleLabel(mixed $transaction): ?string { $title = trim((string) ($this->lineValue($transaction, 'title') ?? '')); if ($title === '' || $title === 'تعریف نشده') { return null; } return $title; } 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); $currentTypeStr = trim((string) ($this->lineValue($row, 'type_str') ?? '')); $titleLabel = $this->resolveTransactionTitleLabel($row); $productAware = $this->resolveTransactionDisplayTypeStr($row, $metalId); $product = $this->resolveTransactionProduct($row); $productTitle = $this->resolveTransactionProductLabel($row); $productId = (int) ($this->lineValue($row, 'product_id') ?? 0); if ($productId <= 0 && $product !== null) { $productId = (int) ($product->id ?? 0); } // عنوان ژنریک مثل «خرید طلا» را با «خرید {نام محصول}» عوض کن if ($productAware !== null && ( $titleLabel === null || $this->shouldReplaceGenericTransactionLabel($titleLabel) || $this->shouldReplaceGenericTransactionLabel($currentTypeStr) )) { $displayType = $productAware; } else { $displayType = $titleLabel; } $productPayload = $this->productTablePayload($product); if (is_array($row)) { $row['metal_gold'] = $metalMap; $row['transaction_metal_id'] = $metalId; $row['transaction_metal_label'] = $metalLabel; $row['product_id'] = $productId > 0 ? $productId : null; $row['product_title'] = $productTitle; if ($productPayload !== null && empty($row['product'])) { $row['product'] = $productPayload; } 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; $row->product_id = $productId > 0 ? $productId : null; $row->product_title = $productTitle; if ($product instanceof Product && empty($this->lineValue($row, 'product'))) { $row->setRelation('product', $product); } if ($displayType !== null) { $row->setAttribute('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) && $transaction->product) { return $transaction->product; } if (is_array($transaction) && isset($transaction['product']) && $transaction['product']) { $product = $transaction['product']; return is_object($product) ? $product : (object) $product; } return null; } private function resolveTransactionProduct(mixed $transaction): ?object { $product = $this->transactionProduct($transaction); if ($product !== null && ! empty($product->title ?? null)) { return $product; } $walletProduct = $this->transactionWalletProduct($transaction); if ($walletProduct !== null && ! empty($walletProduct->title ?? null)) { return $walletProduct; } $productId = (int) ($this->lineValue($transaction, 'product_id') ?? 0); if ($productId > 0) { return Product::withTrashed() ->select(['id', 'title', 'group_type', 'unit', 'direct_price', 'amount', 'trade_by_quantity', 'metal_id', 'file_id']) ->find($productId); } return $product ?? $walletProduct; } /** * @return array|null */ private function productTablePayload(?object $product): ?array { if ($product === null) { return null; } $id = (int) ($product->id ?? 0); if ($id <= 0) { return null; } return [ 'id' => $id, 'title' => $product->title ?? null, 'group_type' => $product->group_type ?? null, 'unit' => $product->unit ?? null, 'amount' => $product->amount ?? null, 'trade_by_quantity' => (bool) ($product->trade_by_quantity ?? false), 'direct_price' => $product->direct_price ?? null, 'metal_id' => isset($product->metal_id) ? (int) $product->metal_id : 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 { $wallet = $this->lineValue($transaction, 'wallet'); if ($wallet === null) { return true; } if (is_object($wallet)) { if ($wallet->product_id !== null || $wallet->fund_id !== null) { return false; } } elseif (is_array($wallet)) { if (($wallet['product_id'] ?? null) !== null || ($wallet['fund_id'] ?? null) !== null) { return false; } } else { return true; } $type = $this->resolveTransactionType($transaction); $gold = (float) ($this->lineValue($transaction, 'gold') ?? 0); return in_array($type, [1, 2, 5, 6, 7, 10, 51], true) || abs($gold) > 0; } 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); $fee = (float) ($this->lineValue($row, 'fee') ?? 0); $totalFee = (float) ($this->lineValue($row, 'total_fee') ?? 0); $direction = (int) ($this->lineValue($row, 'direction') ?? 0); $productId = (int) ($this->lineValue($row, 'product_id') ?? 0); if ($productId <= 0 && $product !== null) { $productId = (int) ($product->id ?? 0); } $productTitle = $product !== null ? trim((string) ($product->title ?? '')) : ''; $productPayload = $this->productTablePayload($product); $storedSource = trim((string) ($this->lineValue($row, 'fee_source') ?? '')); $storedLabel = trim((string) ($this->lineValue($row, 'fee_source_label') ?? '')); $storedKey = trim((string) ($this->lineValue($row, 'fee_source_key') ?? '')); $storedBreakdown = $this->lineValue($row, 'fee_breakdown'); if (is_string($storedBreakdown) && $storedBreakdown !== '') { $decoded = json_decode($storedBreakdown, true); $storedBreakdown = is_array($decoded) ? $decoded : null; } elseif (! is_array($storedBreakdown)) { $storedBreakdown = null; } $feeSource = $storedSource !== '' ? $storedSource : $this->resolveOrderFeeSourceKey($product); $feeSourceLabel = $storedLabel !== '' ? $storedLabel : $this->resolveOrderFeeSourceLabel($direction, $feeSource); $feeSourceKey = $storedKey !== '' ? $storedKey : null; $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, 'product_id' => $productId > 0 ? $productId : null, 'product_title' => $productTitle !== '' ? $productTitle : null, 'fee_source' => $feeSource, 'fee_source_key' => $feeSourceKey, 'fee_source_label' => $feeSourceLabel, 'fee_breakdown' => $storedBreakdown, 'fee_display' => abs($fee), 'total_fee_display' => abs($totalFee), ]; 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; } if ($productPayload !== null && empty($row['product'])) { $row['product'] = $productPayload; } return; } if (is_object($row)) { foreach ($payload as $key => $value) { $row->{$key} = $value; } if ($productPayload !== null && empty($this->lineValue($row, 'product'))) { if ($product instanceof Product) { $row->setRelation('product', $product); } else { $row->product = $productPayload; } } } } /** settings | product | app+product — هم‌تراز resolveCashPriceDiffMeta در SingleOrderController */ public function resolveOrderFeeSourceKey(?object $product = null): string { if ($product instanceof Product) { if (app(ProductDisplayPriceService::class)->usesAppPrice($product)) { return 'app+product'; } return 'product'; } $provider = $this->configService->priceProvider(); if (in_array($provider, ['zaarbox', 'main_product'], true)) { return 'settings'; } return 'product'; } public function resolveOrderFeeSourceLabel(int $direction, ?string $feeSource = null): string { $source = $feeSource ?? $this->resolveOrderFeeSourceKey(); if ($source === 'app+product') { return $direction === 2 ? 'قیمت صفحه اصلی اپلیکیشن (sell_fee) + تب اختلاف محصول' : 'قیمت صفحه اصلی اپلیکیشن (buy_fee) + تب اختلاف محصول'; } if ($source === 'settings') { return $direction === 2 ? 'تنظیمات سراسری (sell_fee / fee_single)' : 'تنظیمات سراسری (buy_fee / fee_single)'; } return $direction === 2 ? 'هزینه فروش محصول (cash_fee_sell)' : 'هزینه خرید محصول (cash_fee_buy)'; } 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 { $embedded = $this->lineValue($order, 'product'); if (is_object($embedded) && ! empty($embedded->title ?? null)) { return $embedded; } if (is_array($embedded) && ! empty($embedded['title'])) { return (object) $embedded; } $productId = (int) ($this->lineValue($order, 'product_id') ?? 0); if ($productId > 0) { $byId = Product::withTrashed() ->select(['id', 'title', 'unit', 'metal_id', 'group_type', 'amount', 'trade_by_quantity', 'direct_price']) ->find($productId); if ($byId) { return $byId; } } $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; } $txProductId = (int) ($this->lineValue($tx, 'product_id') ?? 0); if ($txProductId > 0) { $byTx = Product::withTrashed() ->select(['id', 'title', 'unit', 'metal_id', 'group_type', 'amount', 'trade_by_quantity', 'direct_price']) ->find($txProductId); if ($byTx) { return $byTx; } } } 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; } }