Files

952 lines
34 KiB
PHP

<?php
namespace App\Services;
use App\Http\Controllers\Controller;
use App\Models\Order;
use App\Models\Price;
use App\Models\Product;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Schema;
use Rooyesh\Wallet\models\Wallet;
class AssetSummaryService
{
private const METAL_LABELS = [
1 => 'طلا',
2 => 'نقره',
3 => 'مس',
];
public function __construct(
private readonly ConfigService $configService,
private readonly WalletBalanceService $walletBalanceService
) {
}
public function buildForCustomer(int $customerId, int $ownerType, bool $includeInactiveProducts = false): array
{
$catalogQuery = Product::query()->with('file');
if (!$includeInactiveProducts) {
$catalogQuery->where('active', 1);
}
$catalog = $catalogQuery->get();
$userWallets = Wallet::query()
->with(['product.file'])
->where('owner', $customerId)
->where('owner_type', $ownerType)
->get();
$mainWallet = $this->resolveMainWallet($userWallets);
$metalIds = $this->catalogMetalIds($catalog);
// موجودی کیف اصلی نباید به‌خاطر inactive بودن محصول نماینده از metal_totals حذف شود
if ($customerId > 0) {
$effectiveGold = $this->walletBalanceService->effectiveMainWalletGold($customerId, $ownerType);
if (abs($effectiveGold) >= 0.000001) {
$mainMetalId = $this->walletBalanceService->resolveMainWalletMetalId();
if ($mainMetalId > 0 && !in_array($mainMetalId, $metalIds, true)) {
$metalIds[] = $mainMetalId;
sort($metalIds);
}
}
}
$weightUnit = Controller::tradeWeightUnit();
$hasCountBased = $this->catalogHasCountBased($catalog);
$hasWeightBased = $this->catalogHasWeightBased($catalog);
$catalogById = $catalog->keyBy('id');
// محصولات متصل به کیف اصلی (main_wallet_products) — موجودی‌شان = gold کیف اصلی
$mainWalletProductIds = $this->configService->mainWalletProductIds();
$metalsWithProductWallet = $this->metalIdsWithProductWallet($userWallets, $catalogById, $mainWalletProductIds, $mainWallet);
$inventoryCards = array_merge(
$this->buildMeltedInventoryCards(
$catalog,
$catalogById,
$mainWallet,
$metalIds,
$metalsWithProductWallet,
$mainWalletProductIds,
$userWallets,
$customerId,
$ownerType
),
$this->buildInventoryCards($userWallets, $catalogById, $mainWalletProductIds, $mainWallet),
);
$metalTotals = $this->buildMetalTotals(
$userWallets,
$mainWallet,
$metalIds,
$weightUnit,
$catalogById,
$metalsWithProductWallet,
$mainWalletProductIds,
$customerId,
$ownerType
);
$cashWallet = $this->walletBalanceService->mainWalletCash($userWallets);
$assetsRialValue = $this->sumAssetsRialValue(
$metalTotals,
$userWallets,
$catalogById,
$mainWalletProductIds
);
$totalRialValue = $cashWallet + $assetsRialValue;
$portfolio = $this->buildPortfolioInsight(
$customerId,
$metalTotals,
$cashWallet,
$assetsRialValue,
$totalRialValue
);
return [
'meta' => [
'metal_ids' => $metalIds,
'metal_count' => count($metalIds),
'has_count_based_products' => $hasCountBased,
'has_weight_based_products' => $hasWeightBased,
'weight_unit' => $weightUnit,
'single_metal' => count($metalIds) === 1,
],
'cash_wallet' => round($cashWallet, 0),
'inventory_cards' => $inventoryCards,
'count_products' => $hasCountBased
? array_values(array_filter($inventoryCards, fn (array $row) => ($row['display_type'] ?? '') === 'count'))
: [],
'metal_totals' => $metalTotals,
'assets_rial_value' => round($assetsRialValue, 0),
'total_rial_value' => round($totalRialValue, 0),
'profit_loss' => $portfolio['unrealized_profit_rial'],
'portfolio' => $portfolio,
];
}
/**
* خلاصه دارایی برای داشبورد — ارزش کل، تفکیک نقد/فلز، سود/زیان تحقق‌نیافته طلا.
*
* @return array{
* total_rial_value: float,
* cash_rial: float,
* metal_rial_value: float,
* gold_weight: float,
* gold_rial_value: float,
* average_buy_price: float|null,
* cost_basis_rial: float|null,
* unrealized_profit_rial: float|null,
* unrealized_profit_percent: float|null,
* unrealized_profit_gold: float|null,
* live_gold_price: float
* }
*/
private function buildPortfolioInsight(
int $customerId,
array $metalTotals,
float $cashWallet,
float $assetsRialValue,
float $totalRialValue
): array {
$mainMetalId = $this->walletBalanceService->resolveMainWalletMetalId();
$primaryMetal = collect($metalTotals)->first(fn (array $row) => (int) ($row['metal_id'] ?? 0) === (int) $mainMetalId)
?? collect($metalTotals)->first(fn (array $row) => (int) ($row['metal_id'] ?? 0) === 3)
?? collect($metalTotals)->first(fn (array $row) => (int) ($row['metal_id'] ?? 0) === 1)
?? collect($metalTotals)->first();
$activeMetalId = (int) ($primaryMetal['metal_id'] ?? $mainMetalId);
$metalWeight = (float) ($primaryMetal['total_weight'] ?? 0);
$livePrice = $this->resolveLivePricePerUnit($activeMetalId);
$metalRialValue = $metalWeight > 0 && $livePrice > 0
? round($metalWeight * $livePrice, 0)
: 0.0;
$averageBuyPrice = null;
$costBasisRial = null;
$unrealizedProfitRial = null;
$unrealizedProfitPercent = null;
$unrealizedProfitGold = null;
if ($metalWeight > 0) {
$buyOrders = Order::query()
->where('customer_id', $customerId)
->where('direction', 1)
->where('sts', 2)
->where('amount', '>', 0)
->get(['amount', 'price']);
$totalBought = (float) $buyOrders->sum('amount');
$weightedSum = (float) $buyOrders->sum(fn (Order $order) => (float) $order->price * (float) $order->amount);
if ($totalBought > 0 && $weightedSum > 0) {
$averageBuyPrice = round($weightedSum / $totalBought, 0);
$costBasisRial = round($averageBuyPrice * $metalWeight, 0);
$unrealizedProfitRial = round($metalRialValue - $costBasisRial, 0);
if ($costBasisRial > 0) {
$unrealizedProfitPercent = round(($unrealizedProfitRial / $costBasisRial) * 100, 2);
}
// سود وزنی: همان سود ریالی تبدیل‌شده به وزن فلز با قیمت زندهٔ امروز
if ($livePrice > 0) {
$unrealizedProfitGold = round($unrealizedProfitRial / $livePrice, 4);
}
}
}
return [
'total_rial_value' => round($totalRialValue, 0),
'cash_rial' => round($cashWallet, 0),
'metal_rial_value' => round($assetsRialValue, 0),
'gold_weight' => round($metalWeight, 3),
'gold_rial_value' => $metalRialValue,
'copper_weight' => round($metalWeight, 3),
'copper_rial_value' => $metalRialValue,
'average_buy_price' => $averageBuyPrice,
'cost_basis_rial' => $costBasisRial,
'unrealized_profit_rial' => $unrealizedProfitRial,
'unrealized_profit_percent' => $unrealizedProfitPercent,
'unrealized_profit_gold' => $unrealizedProfitGold,
'live_gold_price' => round($livePrice, 0),
'live_copper_price' => round($livePrice, 0),
];
}
private function catalogMetalIds(Collection $catalog): array
{
return $catalog
->pluck('metal_id')
->filter(fn ($id) => (int) $id > 0)
->map(fn ($id) => (int) $id)
->unique()
->sort()
->values()
->all();
}
private function catalogHasCountBased(Collection $catalog): bool
{
return $catalog->contains(fn (Product $product) => $this->isCountBasedProduct($product));
}
private function catalogHasWeightBased(Collection $catalog): bool
{
return $catalog->contains(fn (Product $product) => !$this->isCountBasedProduct($product));
}
private function buildMeltedInventoryCards(
Collection $catalog,
Collection $catalogById,
?Wallet $mainWallet,
array $metalIds,
array $metalsWithProductWallet = [],
array $mainWalletProductIds = [],
Collection $userWallets,
int $customerId = 0,
int $ownerType = 0
): array {
$cards = [];
$mainWalletGrams = Controller::weightToGrams(
$customerId > 0
? $this->walletBalanceService->effectiveMainWalletGold($customerId, $ownerType)
: $this->effectiveMainWalletGold($userWallets, $mainWallet, $mainWalletProductIds)
);
$assignedMetalIds = [];
$mainProductId = $this->configService->mainProductId();
// حالت صریح: محصولات متصل به کیف اصلی — یک کارت واحد (نماینده = اولین محصول لیست)
if ($mainWalletProductIds !== []) {
$representative = $this->resolveMainWalletRepresentative($catalogById, $mainWalletProductIds);
if (!$representative) {
return [];
}
$mainWalletWeight = $customerId > 0
? $this->walletBalanceService->effectiveMainWalletGold($customerId, $ownerType)
: $this->effectiveMainWalletGold($userWallets, $mainWallet, $mainWalletProductIds);
// صفر کامل را نشان نده؛ منفی را نشان بده تا اختلاف موجودی دیده شود
if (abs($mainWalletWeight) < 0.000001) {
return [];
}
$productPayload = $this->productPayload($representative);
return [[
'product_id' => (int) $representative->id,
'title' => (string) $representative->title,
'group_type' => $this->meltedGroupTypeLabel($representative),
'metal_id' => (int) ($representative->metal_id ?? 0),
'quantity' => round($mainWalletWeight, 3),
'unit' => Controller::productQuantityUnit($productPayload),
'display_type' => 'weight',
'unit_weight' => $this->productUnitWeightLabel($representative),
'show_in_assets' => $this->shouldShowInAssets($representative),
'is_melted' => true,
'is_main_wallet' => true,
'file' => $this->filePayload($representative),
]];
}
foreach ($metalIds as $metalId) {
if (
$metalId <= 0
|| in_array($metalId, $assignedMetalIds, true)
|| in_array((int) $metalId, $metalsWithProductWallet, true)
) {
continue;
}
$meltedGrams = $this->meltedGramsForMetalTotal($mainWalletGrams, (int) $metalId, $metalIds);
if ($meltedGrams <= 0) {
continue;
}
$catalogProduct = $this->resolveDisplayProductForMetal($catalog, (int) $metalId, $mainProductId);
if (!$catalogProduct) {
continue;
}
$assignedMetalIds[] = (int) $metalId;
$productPayload = $this->productPayload($catalogProduct);
$cards[] = [
'product_id' => (int) $catalogProduct->id,
'title' => (string) $catalogProduct->title,
'group_type' => $this->meltedGroupTypeLabel($catalogProduct),
'metal_id' => (int) $metalId,
'quantity' => round(Controller::gramsToTradeUnit($meltedGrams), 3),
'unit' => Controller::productQuantityUnit($productPayload),
'display_type' => 'weight',
'unit_weight' => $this->productUnitWeightLabel($catalogProduct),
'show_in_assets' => $this->shouldShowInAssets($catalogProduct),
'is_melted' => true,
'is_main_wallet' => true,
'file' => $this->filePayload($catalogProduct),
];
}
return $cards;
}
/**
* کارت موجودی یک محصول از assets-summary — منبع واحد با صفحهٔ اصلی و کیف پول.
*/
public function productInventoryCard(int $customerId, int $ownerType, int $productId): ?array
{
$summary = $this->buildForCustomer($customerId, $ownerType);
foreach ($summary['inventory_cards'] as $card) {
if ((int) ($card['product_id'] ?? 0) === $productId) {
return $card;
}
}
return null;
}
/** موجودی نمایشی محصول — همسان با inventory_cards */
public function productDisplayQuantity(int $customerId, int $ownerType, int $productId): float
{
if ($this->configService->isMainWalletProduct($productId)) {
return $this->walletBalanceService->effectiveMainWalletGold($customerId, $ownerType);
}
$card = $this->productInventoryCard($customerId, $ownerType, $productId);
if ($card !== null) {
return (float) ($card['quantity'] ?? 0);
}
$wallet = Wallet::query()
->where('owner', $customerId)
->where('owner_type', $ownerType)
->where('product_id', $productId)
->whereNull('fund_id')
->first();
return (float) ($wallet->gold ?? 0);
}
/**
* کیف مؤثر برای برداشت/تحویل فیزیکی — null یعنی کیف اصلی.
*/
public function effectiveProductWalletIdForCustomer(int $customerId, int $ownerType, int $productId): ?int
{
if ($this->configService->isMainWalletProduct($productId)) {
return null;
}
$card = $this->productInventoryCard($customerId, $ownerType, $productId);
if ($card !== null && !empty($card['is_main_wallet'])) {
return null;
}
return $productId;
}
private function resolveMainWallet(Collection $userWallets): ?Wallet
{
return $userWallets->first(
fn (Wallet $w) => $w->fund_id === null && $w->product_id === null
);
}
/**
* کیف legacy محصول آبشده/کاتد — فقط همان محصول نماینده یا main_product؛
* محصولات آبشدهٔ جدا (مثل آبشده سرمایه‌گذاری) نباید پنهان شوند.
*/
private function shouldIgnoreLegacyProductWalletLine(
Product $catalogProduct,
?Wallet $mainWallet,
Collection $catalogById,
array $mainWalletProductIds
): bool {
$productId = (int) $catalogProduct->id;
if (in_array($productId, $mainWalletProductIds, true)) {
return true;
}
if ((float) ($mainWallet?->gold ?? 0) <= 0) {
return false;
}
if (!$this->isMeltedTradeProduct($catalogProduct)) {
return false;
}
$representative = $this->resolveMainWalletRepresentative($catalogById, $mainWalletProductIds);
if ($representative && $productId === (int) $representative->id) {
return true;
}
$mainProductId = $this->configService->mainProductId();
if ($mainProductId && $productId === (int) $mainProductId) {
return true;
}
return false;
}
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;
}
/**
* محصولات آبشده: دسته «آبشده»/«کاتد مس» + محصول اصلی تنظیمات (main_product).
*/
private function resolveMeltedProducts(Collection $catalog): Collection
{
$mainProductId = $this->configService->mainProductId();
return $catalog
->filter(fn (Product $product) => $this->isMeltedCatalogProduct($product, $mainProductId))
->sortBy(fn (Product $product) => $this->isMainMeltedProduct($product, $mainProductId) ? 0 : 1)
->values();
}
private function isMeltedCatalogProduct(Product $product, ?int $mainProductId): bool
{
if ($mainProductId && (int) $product->id === (int) $mainProductId) {
return true;
}
return $this->isMeltedTradeProduct($product);
}
private function isMainMeltedProduct(Product $product, ?int $mainProductId): bool
{
return $mainProductId !== null && (int) $product->id === (int) $mainProductId;
}
private function isMeltedTradeProduct(Product $product): bool
{
$group = trim((string) ($product->group_type ?? ''));
return in_array($group, ['آبشده', 'کاتد مس'], true);
}
private function meltedGroupTypeLabel(Product $product): string
{
$group = trim((string) ($product->group_type ?? ''));
return $group !== '' ? $group : 'آبشده';
}
/**
* اولین محصولِ لیست main_wallet_products.
* اگر در کاتالوگ فیلترشده (مثلاً فقط active) نبود، از DB بدون فیلتر active می‌خواند
* تا موجودی کیف اصلی برای اپ مشتری صفر نشود وقتی محصول نماینده غیرفعال است.
*/
private function resolveMainWalletRepresentative(Collection $catalogById, array $mainWalletProductIds): ?Product
{
foreach ($mainWalletProductIds as $productId) {
$product = $catalogById->get((int) $productId);
if ($product) {
return $product;
}
}
if ($mainWalletProductIds === []) {
return null;
}
$byId = Product::query()
->with('file')
->whereIn('id', $mainWalletProductIds)
->get()
->keyBy('id');
foreach ($mainWalletProductIds as $productId) {
$product = $byId->get((int) $productId);
if ($product) {
return $product;
}
}
return null;
}
/** فلزهایی که کاربر روی کیف محصول موجودی دارد — برای جلوگیری از کارت آبشدهٔ تکراری. */
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) {
continue;
}
// کیف محصولاتِ متصل به کیف اصلی نادیده گرفته می‌شود
if (in_array((int) $walletLine->product_id, $mainWalletProductIds, true)) {
continue;
}
$product = $walletLine->product;
if (!$product) {
continue;
}
$catalogProduct = $this->catalogProduct($product, $catalogById);
if ($this->shouldIgnoreLegacyProductWalletLine($catalogProduct, $mainWallet, $catalogById, $mainWalletProductIds)) {
continue;
}
$metalId = (int) ($catalogProduct->metal_id ?? 0);
if ($metalId > 0) {
$metalIds[] = $metalId;
}
}
return array_values(array_unique($metalIds));
}
/** محصول نمایشی برای موجودی کیف اصلی هر فلز. */
private function resolveDisplayProductForMetal(
Collection $catalog,
int $metalId,
?int $mainProductId
): ?Product {
if ($mainProductId) {
$mainProduct = $catalog->firstWhere('id', $mainProductId);
if ($mainProduct && (int) ($mainProduct->metal_id ?? 0) === $metalId) {
return $mainProduct;
}
}
foreach ($this->resolveMeltedProducts($catalog) as $product) {
if ((int) ($product->metal_id ?? 0) === $metalId) {
return $product;
}
}
return $catalog->first(fn (Product $product) => (int) ($product->metal_id ?? 0) === $metalId);
}
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;
}
// محصول متصل به کیف اصلی دارایی جدا ندارد — کارتش از کیف اصلی ساخته می‌شود
if (in_array((int) $walletLine->product_id, $mainWalletProductIds, true)) {
continue;
}
$product = $walletLine->product;
if (!$product) {
continue;
}
$catalogProduct = $this->catalogProduct($product, $catalogById);
if ($this->shouldIgnoreLegacyProductWalletLine($catalogProduct, $mainWallet, $catalogById, $mainWalletProductIds)) {
continue;
}
$productPayload = $this->productPayload($catalogProduct);
$isCountBased = $this->isCountBasedProduct($catalogProduct);
$cards[] = [
'product_id' => (int) $catalogProduct->id,
'title' => (string) $catalogProduct->title,
'group_type' => (string) ($catalogProduct->group_type ?? ''),
'metal_id' => (int) ($catalogProduct->metal_id ?? 0),
'quantity' => round((float) $walletLine->gold, 3),
'unit' => Controller::productQuantityUnit($productPayload),
'display_type' => $isCountBased ? 'count' : 'weight',
'unit_weight' => $this->productUnitWeightLabel($catalogProduct),
'show_in_assets' => $this->shouldShowInAssets($catalogProduct),
'file' => $this->filePayload($catalogProduct),
];
}
return $cards;
}
private function buildMetalTotals(
Collection $userWallets,
?Wallet $mainWallet,
array $metalIds,
string $weightUnit,
Collection $catalogById,
array $metalsWithProductWallet = [],
array $mainWalletProductIds = [],
int $customerId = 0,
int $ownerType = 0
): array {
if ($metalIds === []) {
return [];
}
$mainWalletGrams = Controller::weightToGrams(
$customerId > 0
? $this->walletBalanceService->effectiveMainWalletGold($customerId, $ownerType)
: $this->effectiveMainWalletGold($userWallets, $mainWallet, $mainWalletProductIds)
);
$totals = [];
// فلز نمایندهٔ کیف اصلی در حالت main_wallet_products
$mainWalletMetalId = null;
if ($mainWalletProductIds !== []) {
$representative = $this->resolveMainWalletRepresentative($catalogById, $mainWalletProductIds);
$mainWalletMetalId = $representative ? (int) ($representative->metal_id ?? 0) : null;
}
foreach ($metalIds as $metalId) {
$productGrams = 0.0;
foreach ($userWallets as $walletLine) {
if ($walletLine->product_id === null || !$walletLine->product) {
continue;
}
// کیف محصولاتِ متصل به کیف اصلی جداگانه شمرده نمی‌شود
if (in_array((int) $walletLine->product_id, $mainWalletProductIds, true)) {
continue;
}
$catalogProduct = $this->catalogProduct($walletLine->product, $catalogById);
if ($this->shouldIgnoreLegacyProductWalletLine($catalogProduct, $mainWallet, $catalogById, $mainWalletProductIds)) {
continue;
}
if ((int) ($catalogProduct->metal_id ?? 0) !== (int) $metalId) {
continue;
}
if ((string) ($catalogProduct->group_type ?? '') === 'سکه') {
continue;
}
$productGrams += Controller::productLineWeightToGrams(
$catalogProduct,
$walletLine->gold
);
}
if ($mainWalletProductIds !== []) {
// حالت صریح: کیف اصلی فقط یک‌بار، زیر فلزِ محصول نماینده
// اگر نماینده پیدا نشد (محصول حذف‌شده)، به فلز resolveMainWalletMetalId برگرد
if ($mainWalletMetalId === null || $mainWalletMetalId <= 0) {
$mainWalletMetalId = $this->walletBalanceService->resolveMainWalletMetalId();
}
$meltedGrams = ((int) $metalId === (int) $mainWalletMetalId)
? $mainWalletGrams
: 0.0;
} else {
// کیف اصلی فقط برای فلزی که هیچ کیف محصولی ندارد به مجموع اضافه می‌شود.
$meltedGrams = in_array((int) $metalId, $metalsWithProductWallet, true)
? 0.0
: $this->meltedGramsForMetalTotal($mainWalletGrams, $metalId, $metalIds);
}
$productWeight = round(Controller::gramsToTradeUnit($productGrams), 3);
$meltedWeight = round(Controller::gramsToTradeUnit($meltedGrams), 3);
$totalWeight = round($productWeight + $meltedWeight, 3);
$metalUnit = ((int) $metalId === 3 || $weightUnit === 'کیلوگرم') ? 'کیلوگرم' : $weightUnit;
$totals[] = [
'metal_id' => (int) $metalId,
'metal_label' => self::METAL_LABELS[$metalId] ?? 'فلز',
'label' => 'کل دارایی ' . (self::METAL_LABELS[$metalId] ?? 'فلز'),
'product_weight' => $productWeight,
'melted_weight' => $meltedWeight,
'total_weight' => $totalWeight,
'unit' => $metalUnit,
];
}
return $totals;
}
/** جمع واقعی کیف اصلی برای هر فلز — مستقل از show_in_assets. */
private function meltedGramsForMetalTotal(
float $mainWalletGrams,
int $metalId,
array $catalogMetalIds
): float {
if ($mainWalletGrams <= 0) {
return 0.0;
}
$mainMetalId = $this->walletBalanceService->resolveMainWalletMetalId();
return (int) $metalId === (int) $mainMetalId ? $mainWalletGrams : 0.0;
}
/**
* ارزش ریالی دارایی‌های فلزی:
* • وزن کل هر فلز × قیمت زنده
* • سکه‌ها (خارج از جمع وزن) × قیمت محصول
*/
private function sumAssetsRialValue(
array $metalTotals,
Collection $userWallets,
Collection $catalogById,
array $mainWalletProductIds = []
): float {
$total = 0.0;
foreach ($metalTotals as $metal) {
$weight = (float) ($metal['total_weight'] ?? 0);
if ($weight <= 0) {
continue;
}
$livePrice = $this->resolveLivePricePerUnit((int) ($metal['metal_id'] ?? 0));
if ($livePrice > 0) {
$total += $weight * $livePrice;
}
}
foreach ($userWallets as $walletLine) {
if ($walletLine->product_id === null || !$walletLine->product || (float) $walletLine->gold <= 0) {
continue;
}
if (in_array((int) $walletLine->product_id, $mainWalletProductIds, true)) {
continue;
}
$catalogProduct = $this->catalogProduct($walletLine->product, $catalogById);
if ((string) ($catalogProduct->group_type ?? '') !== 'سکه') {
continue;
}
$total += $this->productDisplayUnitPrice($catalogProduct) * (float) $walletLine->gold;
}
return $total;
}
private function resolveLivePricePerUnit(int $metalId): float
{
$prices = app(ProductDisplayPriceService::class);
$mainMetalId = $this->walletBalanceService->resolveMainWalletMetalId();
if ($metalId === 3 || $metalId === $mainMetalId) {
$copperProduct = Product::query()
->where('active', 1)
->where('metal_id', $metalId)
->orderBy('id')
->first();
if ($copperProduct) {
$base = (float) $prices->resolveOrderBasePrice($copperProduct);
if ($base > 0) {
return $base;
}
}
$live = (float) $prices->resolveAppBasePrice();
if ($live > 0) {
return $live;
}
return (float) (Price::latest('id')->value('price') ?? 0);
}
if ($metalId === 1) {
$live = (int) $prices->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;
}
$base = (float) $prices->resolveOrderBasePrice($product);
if ($base > 0) {
return $base;
}
$unitPrice = $this->productDisplayUnitPrice($product);
$amount = (float) ($product->amount ?? 1);
return $amount > 0 ? $unitPrice * $amount : $unitPrice;
}
private function productDisplayUnitPrice(Product $product): float
{
return (float) ($product->price ?? 0) + (float) ($product->price_profit ?? 0);
}
private function shouldShowInAssets(Product $product): bool
{
if (!Product::hasShowInAssetsColumn()) {
return true;
}
return (bool) ($product->show_in_assets ?? false);
}
/**
* طلای کیف اصلی + کیف legacy محصولات main_wallet_products (خریدهای قبل از اتصال).
*/
private function effectiveMainWalletGold(
Collection $userWallets,
?Wallet $mainWallet,
array $mainWalletProductIds
): float {
$total = (float) ($mainWallet?->gold ?? 0);
if ($mainWalletProductIds === []) {
return $total;
}
foreach ($userWallets as $walletLine) {
$productId = (int) ($walletLine->product_id ?? 0);
if ($productId <= 0 || !in_array($productId, $mainWalletProductIds, true)) {
continue;
}
$total += (float) ($walletLine->gold ?? 0);
}
return $total;
}
private function isCountBasedProduct(Product $product): bool
{
return Controller::isCountBasedProduct($this->productPayload($product));
}
private function productPayload(Product $product): array
{
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,
];
}
private function productUnitWeightLabel(Product $product): ?string
{
if (!$this->isCountBasedProduct($product)) {
return null;
}
if (!Controller::hasProductUnitWeight($this->productPayload($product))) {
return null;
}
$amount = (float) ($product->amount ?? 0);
$unit = Controller::productStorageWeightUnit($product);
return trim($amount . ' ' . $unit);
}
private function filePayload(Product $product): ?array
{
if (!$product->relationLoaded('file') || !$product->file) {
return null;
}
$fileModel = $product->file;
$src = (string) ($fileModel->src ?? '');
return [
'id' => $fileModel->id,
'src' => $src,
'full_src' => $src,
'url' => $fileModel->url ?? null,
];
}
}