2026-06-15 14:38:58 +03:30
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
2026-06-23 21:04:29 +03:30
|
|
|
use App\Models\Price;
|
2026-08-27 14:26:01 +03:30
|
|
|
use App\Models\Product;
|
2026-06-15 14:38:58 +03:30
|
|
|
use Illuminate\Support\Collection;
|
2026-08-27 14:01:34 +03:30
|
|
|
use Illuminate\Support\Facades\DB;
|
2026-07-18 15:04:08 +03:30
|
|
|
use Rooyesh\Wallet\models\Wallet;
|
2026-06-15 14:38:58 +03:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* محاسبهٔ موجودی کیف — منبع واحد برای ادمین و APIها.
|
|
|
|
|
*
|
|
|
|
|
* کیف اصلی: fund_id=null و product_id=null
|
|
|
|
|
* main_wallet_products: طلای این محصولات روی کیف اصلی است؛ کیف legacy محصول نادیده گرفته میشود.
|
|
|
|
|
*/
|
|
|
|
|
class WalletBalanceService
|
|
|
|
|
{
|
2026-06-23 21:04:29 +03:30
|
|
|
public function __construct(
|
|
|
|
|
private ConfigService $configService,
|
|
|
|
|
private BalanceReportService $balanceReportService
|
|
|
|
|
) {
|
2026-06-15 14:38:58 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function mainWalletProductIds(): array
|
|
|
|
|
{
|
|
|
|
|
return $this->configService->mainWalletProductIds();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @param Collection|array<int, mixed>|null $wallets */
|
2026-07-14 12:35:03 +03:30
|
|
|
public function mainWalletCash(Collection|array|null $wallets, ?int $ownerType = null): float
|
2026-06-15 14:38:58 +03:30
|
|
|
{
|
2026-07-14 12:35:03 +03:30
|
|
|
$line = $this->mainWalletLine($wallets, $ownerType);
|
2026-06-15 14:38:58 +03:30
|
|
|
|
|
|
|
|
return (float) ($this->lineValue($line, 'cash') ?? 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @param Collection|array<int, mixed>|null $wallets */
|
2026-07-14 12:35:03 +03:30
|
|
|
public function mainWalletGold(Collection|array|null $wallets, ?int $ownerType = null): float
|
2026-06-15 14:38:58 +03:30
|
|
|
{
|
2026-07-14 12:35:03 +03:30
|
|
|
$line = $this->mainWalletLine($wallets, $ownerType);
|
2026-06-15 14:38:58 +03:30
|
|
|
|
|
|
|
|
return (float) ($this->lineValue($line, 'gold') ?? 0);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 15:04:08 +03:30
|
|
|
/**
|
|
|
|
|
* موجودی مؤثر کیف اصلی برای نمایش آبشده =
|
|
|
|
|
* gold کیف اصلی + gold کیفهای legacy محصولات main_wallet_products.
|
2026-07-18 15:30:59 +03:30
|
|
|
*
|
|
|
|
|
* @param int|null $ownerType اگر داده شود فقط همان owner_type جمع میشود (همتراز wallet/me).
|
2026-07-18 15:04:08 +03:30
|
|
|
*/
|
2026-07-18 15:30:59 +03:30
|
|
|
public function effectiveMainWalletGold(int $ownerId, ?int $ownerType = null): float
|
2026-07-18 15:04:08 +03:30
|
|
|
{
|
2026-07-18 15:30:59 +03:30
|
|
|
$mainQuery = Wallet::query()
|
2026-07-18 15:04:08 +03:30
|
|
|
->where('owner', $ownerId)
|
|
|
|
|
->whereNull('fund_id')
|
2026-07-18 15:30:59 +03:30
|
|
|
->whereNull('product_id');
|
|
|
|
|
if ($ownerType !== null) {
|
|
|
|
|
$mainQuery->where('owner_type', $ownerType);
|
|
|
|
|
}
|
|
|
|
|
$mainGold = (float) $mainQuery->sum('gold');
|
2026-07-18 15:04:08 +03:30
|
|
|
|
|
|
|
|
$productIds = $this->mainWalletProductIds();
|
|
|
|
|
if ($productIds === []) {
|
|
|
|
|
return round($mainGold, 3);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 15:30:59 +03:30
|
|
|
$orphanedQuery = Wallet::query()
|
2026-07-18 15:04:08 +03:30
|
|
|
->where('owner', $ownerId)
|
|
|
|
|
->whereNull('fund_id')
|
2026-07-18 15:30:59 +03:30
|
|
|
->whereIn('product_id', $productIds);
|
|
|
|
|
if ($ownerType !== null) {
|
|
|
|
|
$orphanedQuery->where('owner_type', $ownerType);
|
|
|
|
|
}
|
|
|
|
|
$orphanedGold = (float) $orphanedQuery->sum('gold');
|
2026-07-18 15:04:08 +03:30
|
|
|
|
|
|
|
|
return round($mainGold + $orphanedGold, 3);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-27 14:01:34 +03:30
|
|
|
/**
|
|
|
|
|
* موجودی ریالی مؤثر کیف اصلی =
|
|
|
|
|
* cash کیف اصلی + cash کیفهای legacy محصولات main_wallet_products.
|
|
|
|
|
*/
|
|
|
|
|
public function effectiveMainWalletCash(int $ownerId, ?int $ownerType = null): float
|
|
|
|
|
{
|
|
|
|
|
$mainQuery = Wallet::query()
|
|
|
|
|
->where('owner', $ownerId)
|
|
|
|
|
->whereNull('fund_id')
|
|
|
|
|
->whereNull('product_id');
|
|
|
|
|
if ($ownerType !== null) {
|
|
|
|
|
$mainQuery->where('owner_type', $ownerType);
|
|
|
|
|
}
|
|
|
|
|
$mainCash = (float) $mainQuery->sum('cash');
|
|
|
|
|
|
|
|
|
|
$productIds = $this->mainWalletProductIds();
|
|
|
|
|
if ($productIds === []) {
|
|
|
|
|
return $mainCash;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$orphanedQuery = Wallet::query()
|
|
|
|
|
->where('owner', $ownerId)
|
|
|
|
|
->whereNull('fund_id')
|
|
|
|
|
->whereIn('product_id', $productIds);
|
|
|
|
|
if ($ownerType !== null) {
|
|
|
|
|
$orphanedQuery->where('owner_type', $ownerType);
|
|
|
|
|
}
|
|
|
|
|
$orphanedCash = (float) $orphanedQuery->sum('cash');
|
|
|
|
|
|
|
|
|
|
return $mainCash + $orphanedCash;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* انتقال موجودی کیف legacy یک محصول متصل به کیف اصلی — مثل goldali (idempotent).
|
|
|
|
|
* خرید/فروش بعد از این فقط روی یک کیف (اصلی) کسر/واریز میشود.
|
|
|
|
|
*/
|
|
|
|
|
public function consolidateOrphanedForOwner(int $ownerId, int $ownerType, int $productId): bool
|
|
|
|
|
{
|
|
|
|
|
if (!$this->configService->isMainWalletProduct($productId)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$productWallet = Wallet::query()
|
|
|
|
|
->where('owner', $ownerId)
|
|
|
|
|
->where('owner_type', $ownerType)
|
|
|
|
|
->where('product_id', $productId)
|
|
|
|
|
->whereNull('fund_id')
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if (!$productWallet) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$goldToMove = (float) $productWallet->gold;
|
|
|
|
|
$cashToMove = (float) $productWallet->cash;
|
|
|
|
|
|
|
|
|
|
if (abs($goldToMove) < 1e-12 && abs($cashToMove) < 1e-12) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DB::transaction(function () use ($ownerId, $ownerType, $productWallet, $goldToMove, $cashToMove) {
|
|
|
|
|
$mainWallet = Wallet::query()->firstOrCreate(
|
|
|
|
|
[
|
|
|
|
|
'owner' => $ownerId,
|
|
|
|
|
'owner_type' => $ownerType,
|
|
|
|
|
'fund_id' => null,
|
|
|
|
|
'product_id' => null,
|
|
|
|
|
],
|
|
|
|
|
['gold' => 0, 'cash' => 0]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$mainWallet->gold = (float) $mainWallet->gold + $goldToMove;
|
|
|
|
|
$mainWallet->cash = (float) $mainWallet->cash + $cashToMove;
|
|
|
|
|
$mainWallet->save();
|
|
|
|
|
|
|
|
|
|
$productWallet->gold = 0;
|
|
|
|
|
$productWallet->cash = 0;
|
|
|
|
|
$productWallet->save();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** ادغام همهٔ کیفهای legacy محصولات main_wallet_products برای یک کاربر */
|
|
|
|
|
public function consolidateAllMainWalletProductsForOwner(int $ownerId, int $ownerType): void
|
|
|
|
|
{
|
|
|
|
|
foreach ($this->mainWalletProductIds() as $productId) {
|
|
|
|
|
$this->consolidateOrphanedForOwner($ownerId, $ownerType, (int) $productId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 17:30:14 +03:30
|
|
|
/**
|
|
|
|
|
* طلای داخل قلکهای یک محصول (یا همهٔ محصولات main_wallet اگر $productId=null و فقط main).
|
|
|
|
|
*/
|
|
|
|
|
public function fundGoldForProduct(int $ownerId, ?int $productId, ?int $ownerType = null): float
|
|
|
|
|
{
|
|
|
|
|
$query = Wallet::query()
|
|
|
|
|
->where('owner', $ownerId)
|
|
|
|
|
->whereNotNull('fund_id');
|
|
|
|
|
if ($ownerType !== null) {
|
|
|
|
|
$query->where('owner_type', $ownerType);
|
|
|
|
|
}
|
|
|
|
|
if ($productId === null) {
|
|
|
|
|
$query->whereNull('product_id');
|
|
|
|
|
} else {
|
|
|
|
|
$query->where('product_id', $productId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return round((float) $query->sum('gold'), 3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* جمع طلای قلکهای محصولات متصل به کیف اصلی (+ قلکهای legacy بدون product_id).
|
|
|
|
|
*/
|
|
|
|
|
public function fundGoldForMainWalletProducts(int $ownerId, ?int $ownerType = null): float
|
|
|
|
|
{
|
|
|
|
|
$productIds = $this->mainWalletProductIds();
|
|
|
|
|
$query = Wallet::query()
|
|
|
|
|
->where('owner', $ownerId)
|
|
|
|
|
->whereNotNull('fund_id');
|
|
|
|
|
if ($ownerType !== null) {
|
|
|
|
|
$query->where('owner_type', $ownerType);
|
|
|
|
|
}
|
|
|
|
|
if ($productIds === []) {
|
|
|
|
|
$query->whereNull('product_id');
|
|
|
|
|
} else {
|
|
|
|
|
$query->where(function ($q) use ($productIds) {
|
|
|
|
|
$q->whereIn('product_id', $productIds)->orWhereNull('product_id');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return round((float) $query->sum('gold'), 3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* موجودی قابلمقایسه با حسابداری برای طلای کیف اصلی =
|
|
|
|
|
* آزاد (effectiveMain) + قلکهای همان محصولات.
|
|
|
|
|
* برای معامله استفاده نشود — فقط sync/مانده.
|
|
|
|
|
*/
|
|
|
|
|
public function accountingVisibleMainWalletGold(int $ownerId, ?int $ownerType = null): float
|
|
|
|
|
{
|
|
|
|
|
return round(
|
|
|
|
|
$this->effectiveMainWalletGold($ownerId, $ownerType)
|
|
|
|
|
+ $this->fundGoldForMainWalletProducts($ownerId, $ownerType),
|
|
|
|
|
3
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* موجودی آزاد یک محصول (بدون قلک) — برای معامله و واریز به قلک.
|
|
|
|
|
*/
|
|
|
|
|
public function freeProductGold(int $ownerId, int $productId, ?int $ownerType = null): float
|
|
|
|
|
{
|
|
|
|
|
if ($this->configService->isMainWalletProduct($productId)) {
|
|
|
|
|
return $this->effectiveMainWalletGold($ownerId, $ownerType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$query = Wallet::query()
|
|
|
|
|
->where('owner', $ownerId)
|
|
|
|
|
->whereNull('fund_id')
|
|
|
|
|
->where('product_id', $productId);
|
|
|
|
|
if ($ownerType !== null) {
|
|
|
|
|
$query->where('owner_type', $ownerType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return round((float) $query->sum('gold'), 3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* جمع نمایشی دارایی یک محصول = آزاد + قلکهای همان محصول.
|
|
|
|
|
*/
|
|
|
|
|
public function totalProductGold(int $ownerId, int $productId, ?int $ownerType = null): float
|
|
|
|
|
{
|
|
|
|
|
return round(
|
|
|
|
|
$this->freeProductGold($ownerId, $productId, $ownerType)
|
|
|
|
|
+ $this->fundGoldForProduct($ownerId, $productId, $ownerType),
|
|
|
|
|
3
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 14:38:58 +03:30
|
|
|
/**
|
|
|
|
|
* جمع طلای قابلنمایش: کیف اصلی + کیف محصولات + قلکها.
|
|
|
|
|
* کیفهای legacy محصولات main_wallet_products شمرده نمیشوند.
|
|
|
|
|
*
|
|
|
|
|
* @param Collection|array<int, mixed>|null $wallets
|
|
|
|
|
*/
|
|
|
|
|
public function totalGoldWeight(
|
|
|
|
|
Collection|array|null $wallets,
|
|
|
|
|
?array $mainWalletProductIds = null
|
|
|
|
|
): float {
|
|
|
|
|
$mainWalletProductIds ??= $this->mainWalletProductIds();
|
|
|
|
|
$sum = 0.0;
|
|
|
|
|
|
|
|
|
|
foreach ($this->walletLines($wallets) as $line) {
|
|
|
|
|
$fundId = $this->lineValue($line, 'fund_id');
|
|
|
|
|
$productId = (int) ($this->lineValue($line, 'product_id') ?? 0);
|
|
|
|
|
$gold = (float) ($this->lineValue($line, 'gold') ?? 0);
|
|
|
|
|
|
2026-07-21 21:44:06 +03:30
|
|
|
if (abs($gold) < 0.0000001) {
|
2026-06-15 14:38:58 +03:30
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($fundId !== null) {
|
|
|
|
|
$sum += $gold;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($productId === 0) {
|
|
|
|
|
$sum += $gold;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($mainWalletProductIds !== [] && in_array($productId, $mainWalletProductIds, true)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$sum += $gold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $sum;
|
|
|
|
|
}
|
2026-06-23 21:04:29 +03:30
|
|
|
|
|
|
|
|
/** @return list<int> */
|
|
|
|
|
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<int, mixed>|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<int> $catalogMetalIds
|
|
|
|
|
* @return array<string, float>
|
|
|
|
|
*/
|
|
|
|
|
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);
|
2026-07-21 21:44:06 +03:30
|
|
|
if (abs($goldGrams) < 0.0000001) {
|
2026-06-23 21:04:29 +03:30
|
|
|
return $map;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$metalId = $this->resolveWalletLineMetalId($line, $mainWalletMetalId);
|
|
|
|
|
$map[(string) $metalId] = round(
|
|
|
|
|
$this->balanceReportService->gramsToMetalDisplay($goldGrams, $metalId),
|
|
|
|
|
3
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return $map;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param Collection|array<int, mixed> $walletLines
|
|
|
|
|
* @param list<int> $catalogMetalIds
|
|
|
|
|
* @return array<string, float> جمع به گرم
|
|
|
|
|
*/
|
|
|
|
|
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);
|
2026-07-21 21:44:06 +03:30
|
|
|
if (abs($goldGrams) < 0.0000001) {
|
2026-06-23 21:04:29 +03:30
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$metalId = (string) $this->resolveWalletLineMetalId($line, $mainWalletMetalId);
|
|
|
|
|
$buckets[$metalId] = ($buckets[$metalId] ?? 0.0) + $goldGrams;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $buckets;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param Collection|array<int, mixed> $walletLines
|
|
|
|
|
* @param list<int> $catalogMetalIds
|
|
|
|
|
* @return list<array{metal_id: int, label: string, gold: float, display_unit: string, goldToCash: float}>
|
|
|
|
|
*/
|
|
|
|
|
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<int, mixed> $walletLines
|
|
|
|
|
* @param list<int> $catalogMetalIds
|
|
|
|
|
* @return array<string, float> مقادیر نمایشی
|
|
|
|
|
*/
|
|
|
|
|
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<int> $catalogMetalIds
|
|
|
|
|
*/
|
|
|
|
|
public function enrichWalletTableRow(mixed &$row, array $catalogMetalIds, int $mainWalletMetalId): void
|
|
|
|
|
{
|
|
|
|
|
$metalMap = $this->walletLineMetalGoldMap($row, $catalogMetalIds, $mainWalletMetalId);
|
2026-07-01 20:59:24 +03:30
|
|
|
$meta = $this->buildWalletTableRowMeta($row, $mainWalletMetalId);
|
2026-06-23 21:04:29 +03:30
|
|
|
|
|
|
|
|
if (is_array($row)) {
|
|
|
|
|
$row['metal_gold'] = $metalMap;
|
|
|
|
|
foreach ($catalogMetalIds as $metalId) {
|
|
|
|
|
$row['metal_gold_' . $metalId] = $metalMap[(string) $metalId] ?? 0.0;
|
|
|
|
|
}
|
2026-07-01 20:59:24 +03:30
|
|
|
foreach ($meta as $key => $value) {
|
|
|
|
|
$row[$key] = $value;
|
|
|
|
|
}
|
2026-06-23 21:04:29 +03:30
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (is_object($row)) {
|
|
|
|
|
$row->metal_gold = $metalMap;
|
|
|
|
|
foreach ($catalogMetalIds as $metalId) {
|
|
|
|
|
$row->{'metal_gold_' . $metalId} = $metalMap[(string) $metalId] ?? 0.0;
|
|
|
|
|
}
|
2026-07-01 20:59:24 +03:30
|
|
|
foreach ($meta as $key => $value) {
|
|
|
|
|
$row->{$key} = $value;
|
|
|
|
|
}
|
2026-06-23 21:04:29 +03:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 20:59:24 +03:30
|
|
|
/**
|
|
|
|
|
* @return array<string, mixed>
|
|
|
|
|
*/
|
|
|
|
|
private function buildWalletTableRowMeta(mixed $row, int $mainWalletMetalId): array
|
|
|
|
|
{
|
|
|
|
|
$fundId = $this->lineValue($row, 'fund_id');
|
|
|
|
|
$productId = $this->lineValue($row, 'product_id');
|
|
|
|
|
$product = $this->lineProduct($row);
|
|
|
|
|
$fund = $this->lineFund($row);
|
|
|
|
|
$cash = (float) ($this->lineValue($row, 'cash') ?? 0);
|
|
|
|
|
|
|
|
|
|
if ($fundId !== null) {
|
|
|
|
|
$kind = 'fund';
|
|
|
|
|
$kindLabel = 'قلک (صندوق)';
|
|
|
|
|
$fundTitle = trim((string) ($fund?->title ?? ''));
|
|
|
|
|
$subject = $fundTitle !== '' ? "قلک «{$fundTitle}»" : 'قلک بدون عنوان';
|
|
|
|
|
} elseif ($productId === null) {
|
|
|
|
|
$kind = 'main';
|
|
|
|
|
$kindLabel = 'کیف اصلی';
|
|
|
|
|
$subject = 'کیف اصلی — ریال و طلای آبشده';
|
|
|
|
|
} else {
|
|
|
|
|
$kind = 'product';
|
|
|
|
|
$kindLabel = 'کیف محصول';
|
|
|
|
|
$title = trim((string) ($product?->title ?? ''));
|
|
|
|
|
$subject = $title !== '' ? "محصول «{$title}»" : "محصول کد {$productId}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$metalId = $this->resolveWalletLineMetalId($row, $mainWalletMetalId);
|
|
|
|
|
$goldGrams = $this->walletLineGoldGrams($row);
|
|
|
|
|
$goldToCash = $goldGrams > 0 ? (int) round($goldGrams * $this->livePricePerGram($metalId)) : 0;
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'wallet_kind' => $kind,
|
|
|
|
|
'wallet_kind_label' => $kindLabel,
|
|
|
|
|
'wallet_subject' => $subject,
|
|
|
|
|
'product_title' => $product?->title ?? null,
|
|
|
|
|
'product_group_type' => $product?->group_type ?? null,
|
|
|
|
|
'product_unit' => $product?->unit ?? null,
|
|
|
|
|
'product_amount' => $product?->amount ?? null,
|
|
|
|
|
'fund_title' => $fund?->title ?? null,
|
|
|
|
|
'wallet_metal_id' => $metalId,
|
|
|
|
|
'wallet_metal_label' => $this->balanceReportService->metalLabel($metalId),
|
|
|
|
|
'gold_to_cash' => $goldToCash,
|
|
|
|
|
'gold_grams' => round($goldGrams, 3),
|
|
|
|
|
'total_rial_value' => (int) round($cash + $goldToCash),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 21:04:29 +03:30
|
|
|
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
|
|
|
|
|
{
|
2026-07-21 22:28:04 +03:30
|
|
|
if (is_object($line) && isset($line->product) && $line->product) {
|
2026-06-23 21:04:29 +03:30
|
|
|
return $line->product;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 22:28:04 +03:30
|
|
|
if (is_array($line) && isset($line['product']) && $line['product']) {
|
2026-06-23 21:04:29 +03:30
|
|
|
$product = $line['product'];
|
|
|
|
|
|
|
|
|
|
return is_object($product) ? $product : (object) $product;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 22:28:04 +03:30
|
|
|
$productId = (int) ($this->lineValue($line, 'product_id') ?? 0);
|
|
|
|
|
if ($productId > 0) {
|
|
|
|
|
return Product::withTrashed()
|
|
|
|
|
->select(['id', 'title', 'metal_id', 'unit', 'amount', 'group_type', 'trade_by_quantity', 'direct_price'])
|
|
|
|
|
->find($productId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 21:04:29 +03:30
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 20:59:24 +03:30
|
|
|
private function lineFund(mixed $line): ?object
|
|
|
|
|
{
|
|
|
|
|
if (is_object($line) && isset($line->fund)) {
|
|
|
|
|
return $line->fund;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (is_array($line) && isset($line['fund'])) {
|
|
|
|
|
$fund = $line['fund'];
|
|
|
|
|
|
|
|
|
|
return is_object($fund) ? $fund : (object) $fund;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 21:04:29 +03:30
|
|
|
private function walletLineGoldGrams(mixed $line): float
|
|
|
|
|
{
|
|
|
|
|
$gold = (float) ($this->lineValue($line, 'gold') ?? 0);
|
2026-07-21 21:44:06 +03:30
|
|
|
if (abs($gold) < 0.0000001) {
|
2026-06-23 21:04:29 +03:30
|
|
|
return 0.0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$product = $this->lineProduct($line);
|
|
|
|
|
if ($product !== null) {
|
|
|
|
|
return Controller::productLineWeightToGrams($product, $gold);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Controller::weightToGrams($gold);
|
|
|
|
|
}
|
2026-06-15 14:38:58 +03:30
|
|
|
|
|
|
|
|
/** @param Collection|array<int, mixed>|null $wallets */
|
2026-07-14 12:35:03 +03:30
|
|
|
private function mainWalletLine(Collection|array|null $wallets, ?int $ownerType = null): mixed
|
2026-06-15 14:38:58 +03:30
|
|
|
{
|
|
|
|
|
foreach ($this->walletLines($wallets) as $line) {
|
2026-07-14 12:35:03 +03:30
|
|
|
if ($this->lineValue($line, 'fund_id') !== null || $this->lineValue($line, 'product_id') !== null) {
|
|
|
|
|
continue;
|
2026-06-15 14:38:58 +03:30
|
|
|
}
|
2026-07-14 12:35:03 +03:30
|
|
|
|
|
|
|
|
if ($ownerType !== null) {
|
|
|
|
|
$lineOwnerType = $this->lineValue($line, 'owner_type');
|
|
|
|
|
if ($lineOwnerType !== null && (int) $lineOwnerType !== $ownerType) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $line;
|
2026-06-15 14:38:58 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @param Collection|array<int, mixed>|null $wallets */
|
|
|
|
|
private function walletLines(Collection|array|null $wallets): array
|
|
|
|
|
{
|
|
|
|
|
if ($wallets === null) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($wallets instanceof Collection) {
|
|
|
|
|
return $wallets->all();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return is_array($wallets) ? $wallets : [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|