Files
boursecu_server/app/Services/WalletBalanceService.php
T
2026-07-01 20:59:24 +03:30

430 lines
13 KiB
PHP

<?php
namespace App\Services;
use App\Http\Controllers\Controller;
use App\Models\Price;
use App\Models\Product;
use Illuminate\Support\Collection;
/**
* محاسبهٔ موجودی کیف — منبع واحد برای ادمین و APIها.
*
* کیف اصلی: fund_id=null و product_id=null
* main_wallet_products: طلای این محصولات روی کیف اصلی است؛ کیف legacy محصول نادیده گرفته می‌شود.
*/
class WalletBalanceService
{
public function __construct(
private ConfigService $configService,
private BalanceReportService $balanceReportService
) {
}
public function mainWalletProductIds(): array
{
return $this->configService->mainWalletProductIds();
}
/** @param Collection|array<int, mixed>|null $wallets */
public function mainWalletCash(Collection|array|null $wallets): float
{
$line = $this->mainWalletLine($wallets);
return (float) ($this->lineValue($line, 'cash') ?? 0);
}
/** @param Collection|array<int, mixed>|null $wallets */
public function mainWalletGold(Collection|array|null $wallets): float
{
$line = $this->mainWalletLine($wallets);
return (float) ($this->lineValue($line, 'gold') ?? 0);
}
/**
* جمع طلای قابل‌نمایش: کیف اصلی + کیف محصولات + قلک‌ها.
* کیف‌های 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);
if ($gold <= 0) {
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;
}
/** @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);
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<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);
if ($goldGrams <= 0) {
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);
$meta = $this->buildWalletTableRowMeta($row, $mainWalletMetalId);
if (is_array($row)) {
$row['metal_gold'] = $metalMap;
foreach ($catalogMetalIds as $metalId) {
$row['metal_gold_' . $metalId] = $metalMap[(string) $metalId] ?? 0.0;
}
foreach ($meta as $key => $value) {
$row[$key] = $value;
}
return;
}
if (is_object($row)) {
$row->metal_gold = $metalMap;
foreach ($catalogMetalIds as $metalId) {
$row->{'metal_gold_' . $metalId} = $metalMap[(string) $metalId] ?? 0.0;
}
foreach ($meta as $key => $value) {
$row->{$key} = $value;
}
}
}
/**
* @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),
];
}
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 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;
}
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<int, mixed>|null $wallets */
private function mainWalletLine(Collection|array|null $wallets): mixed
{
foreach ($this->walletLines($wallets) as $line) {
if ($this->lineValue($line, 'fund_id') === null && $this->lineValue($line, 'product_id') === null) {
return $line;
}
}
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;
}
}