583 lines
21 KiB
PHP
583 lines
21 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
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
|
|
) {
|
|
}
|
|
|
|
public function buildForCustomer(int $customerId, int $ownerType): array
|
|
{
|
|
$catalog = Product::query()
|
|
->where('active', 1)
|
|
->with('file')
|
|
->get();
|
|
|
|
$userWallets = Wallet::query()
|
|
->with(['product.file'])
|
|
->where('owner', $customerId)
|
|
->where('owner_type', $ownerType)
|
|
->get();
|
|
|
|
$mainWallet = $this->resolveMainWallet($userWallets);
|
|
$metalIds = $this->catalogMetalIds($catalog);
|
|
$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);
|
|
$inventoryCards = array_merge(
|
|
$this->buildMeltedInventoryCards($catalog, $catalogById, $mainWallet, $metalIds, $metalsWithProductWallet, $mainWalletProductIds),
|
|
$this->buildInventoryCards($userWallets, $catalogById, $mainWalletProductIds)
|
|
);
|
|
$metalTotals = $this->buildMetalTotals($userWallets, $mainWallet, $metalIds, $weightUnit, $catalogById, $metalsWithProductWallet, $mainWalletProductIds);
|
|
$cashWallet = (float) ($mainWallet?->cash ?? 0);
|
|
$assetsRialValue = $this->sumAssetsRialValue(
|
|
$metalTotals,
|
|
$userWallets,
|
|
$catalogById,
|
|
$mainWalletProductIds
|
|
);
|
|
$totalRialValue = $cashWallet + $assetsRialValue;
|
|
|
|
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),
|
|
];
|
|
}
|
|
|
|
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 = []
|
|
): array {
|
|
$cards = [];
|
|
$mainWalletGrams = Controller::weightToGrams((float) ($mainWallet?->gold ?? 0));
|
|
$assignedMetalIds = [];
|
|
$mainProductId = $this->configService->mainProductId();
|
|
|
|
// حالت صریح: محصولات متصل به کیف اصلی — یک کارت واحد (نماینده = اولین محصول لیست)
|
|
if ($mainWalletProductIds !== []) {
|
|
$representative = $this->resolveMainWalletRepresentative($catalogById, $mainWalletProductIds);
|
|
if (!$representative) {
|
|
return [];
|
|
}
|
|
|
|
$mainWalletWeight = (float) ($mainWallet?->gold ?? 0);
|
|
if ($mainWalletWeight <= 0) {
|
|
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,
|
|
'file' => $this->filePayload($catalogProduct),
|
|
];
|
|
}
|
|
|
|
return $cards;
|
|
}
|
|
|
|
private function resolveMainWallet(Collection $userWallets): ?Wallet
|
|
{
|
|
return $userWallets->first(
|
|
fn (Wallet $w) => $w->fund_id === null && $w->product_id === null
|
|
);
|
|
}
|
|
|
|
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 که در کاتالوگ فعال است. */
|
|
private function resolveMainWalletRepresentative(Collection $catalogById, array $mainWalletProductIds): ?Product
|
|
{
|
|
foreach ($mainWalletProductIds as $productId) {
|
|
$product = $catalogById->get((int) $productId);
|
|
if ($product) {
|
|
return $product;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** فلزهایی که کاربر روی کیف محصول موجودی دارد — برای جلوگیری از کارت آبشدهٔ تکراری. */
|
|
private function metalIdsWithProductWallet(Collection $userWallets, Collection $catalogById, array $mainWalletProductIds = []): array
|
|
{
|
|
$metalIds = [];
|
|
|
|
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;
|
|
}
|
|
|
|
$metalId = (int) ($this->catalogProduct($product, $catalogById)->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 = []): array
|
|
{
|
|
$cards = [];
|
|
|
|
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);
|
|
$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 = []
|
|
): array {
|
|
if ($metalIds === []) {
|
|
return [];
|
|
}
|
|
|
|
$mainWalletGrams = Controller::weightToGrams((float) ($mainWallet?->gold ?? 0));
|
|
$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 ((int) ($catalogProduct->metal_id ?? 0) !== (int) $metalId) {
|
|
continue;
|
|
}
|
|
|
|
if ((string) ($catalogProduct->group_type ?? '') === 'سکه') {
|
|
continue;
|
|
}
|
|
|
|
$productGrams += Controller::productLineWeightToGrams(
|
|
$catalogProduct,
|
|
$walletLine->gold
|
|
);
|
|
}
|
|
|
|
if ($mainWalletProductIds !== []) {
|
|
// حالت صریح: کیف اصلی فقط یکبار، زیر فلزِ محصول نماینده
|
|
$meltedGrams = ($mainWalletMetalId !== null && (int) $metalId === $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);
|
|
|
|
$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' => $weightUnit,
|
|
];
|
|
}
|
|
|
|
return $totals;
|
|
}
|
|
|
|
/** جمع واقعی کیف اصلی برای هر فلز — مستقل از show_in_assets. */
|
|
private function meltedGramsForMetalTotal(
|
|
float $mainWalletGrams,
|
|
int $metalId,
|
|
array $catalogMetalIds
|
|
): float {
|
|
if ($mainWalletGrams <= 0) {
|
|
return 0.0;
|
|
}
|
|
|
|
if (count($catalogMetalIds) === 1) {
|
|
return (int) $catalogMetalIds[0] === (int) $metalId ? $mainWalletGrams : 0.0;
|
|
}
|
|
|
|
return (int) $metalId === 1 ? $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
|
|
{
|
|
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 = $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);
|
|
}
|
|
|
|
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,
|
|
];
|
|
}
|
|
}
|