ultimatepos/Modules/Accounting/Services/ConsolidationService.php

154 lines
6.0 KiB
PHP

<?php
namespace Modules\Accounting\Services;
use App\HoldingEntity;
use App\Services\Holding\AssignmentQueryScopeService;
use Illuminate\Support\Facades\DB;
use Modules\Accounting\Entities\AccountingConsolidationRun;
class ConsolidationService
{
public function __construct(
protected HoldingEntityResolutionService $entityResolution,
protected AssignmentQueryScopeService $assignmentScope
) {}
public function run(
int $businessId,
string $periodStart,
string $periodEnd,
int $userId,
?int $parentEntityId = null,
string $targetCurrencyCode = 'IRR',
array $entityExchangeRates = [],
string $rateMode = 'market',
array $officialExchangeRates = []
): AccountingConsolidationRun {
$entityQuery = HoldingEntity::where('business_id', $businessId)->where('is_active', true);
$allowedEntityIds = $this->assignmentScope->allowedEntityIds();
if ($allowedEntityIds !== null) {
$entityQuery->whereIn('id', $allowedEntityIds);
}
$entities = $entityQuery->get();
$entityBalances = [];
$rateComparison = [];
foreach ($entities as $entity) {
$balance = $this->entityNetBalance($businessId, $entity, $periodStart, $periodEnd);
$entityCurrency = (string) data_get($entity->meta, 'currency_code', $targetCurrencyCode);
$exchangeRate = (float) ($entityExchangeRates[$entity->id] ?? 1);
$officialRate = (float) ($officialExchangeRates[$entity->id] ?? $exchangeRate);
if ($exchangeRate <= 0) {
$exchangeRate = 1;
}
if ($officialRate <= 0) {
$officialRate = $exchangeRate;
}
$convertedMarket = round($balance * $exchangeRate, 4);
$convertedOfficial = round($balance * $officialRate, 4);
$activeRate = $rateMode === 'official' ? $officialRate : $exchangeRate;
$entityBalances[] = [
'entity_id' => $entity->id,
'entity_name' => $entity->name,
'entity_currency' => $entityCurrency,
'exchange_rate' => $activeRate,
'market_rate' => $exchangeRate,
'official_rate' => $officialRate,
'net_balance' => $balance,
'converted_net_balance' => round($balance * $activeRate, 4),
'converted_market_balance' => $convertedMarket,
'converted_official_balance' => $convertedOfficial,
'rate_variance' => round($convertedMarket - $convertedOfficial, 4),
];
if ($entityCurrency !== strtoupper($targetCurrencyCode)) {
$rateComparison[] = [
'entity' => $entity->name,
'currency' => $entityCurrency,
'market_rate' => $exchangeRate,
'official_rate' => $officialRate,
'variance_pct' => $officialRate > 0 ? round((($exchangeRate - $officialRate) / $officialRate) * 100, 2) : null,
'balance_variance' => round($convertedMarket - $convertedOfficial, 4),
];
}
}
$elimination = $this->estimateIntercompanyElimination($businessId, $periodStart, $periodEnd);
return AccountingConsolidationRun::create([
'business_id' => $businessId,
'parent_entity_id' => $parentEntityId,
'period_start' => $periodStart,
'period_end' => $periodEnd,
'currency_code' => strtoupper($targetCurrencyCode),
'rate_mode' => $rateMode,
'elimination_amount' => $elimination,
'entity_balances' => $entityBalances,
'rate_comparison' => $rateComparison,
'status' => 'draft',
'created_by' => $userId,
]);
}
public function readiness(int $businessId): array
{
$entities = HoldingEntity::where('business_id', $businessId)->where('is_active', true)->count();
$tagging = $this->entityResolution->assessTagging($businessId);
$fx = app(ExchangeRateService::class)->staleRateSummary($businessId);
$issues = [];
if ($entities === 0) {
$issues[] = 'no_entities';
}
if (! $tagging['ready']) {
$issues[] = 'untagged_gl_lines';
}
if (($fx['total_issues'] ?? 0) > 0) {
$issues[] = 'stale_fx_rates';
}
return [
'entities' => $entities,
'tagging' => $tagging,
'fx' => $fx,
'ready' => empty($issues),
'issues' => $issues,
];
}
public function finalize(AccountingConsolidationRun $run): AccountingConsolidationRun
{
$run->status = 'finalized';
$run->save();
return $run;
}
protected function entityNetBalance(int $businessId, HoldingEntity $entity, string $periodStart, string $periodEnd): float
{
$query = DB::table('accounting_accounts_transactions as aat')
->join('accounting_accounts as aa', 'aa.id', '=', 'aat.accounting_account_id')
->where('aa.business_id', $businessId)
->whereDate('aat.operation_date', '>=', $periodStart)
->whereDate('aat.operation_date', '<=', $periodEnd);
$this->entityResolution->applyEntityScope($query, $businessId, $entity);
return (float) ($query->select(DB::raw("SUM(IF(aat.type='debit', aat.amount, -1*aat.amount)) as net"))->value('net') ?? 0);
}
protected function estimateIntercompanyElimination(int $businessId, string $start, string $end): float
{
return (float) DB::table('accounting_accounts_transactions as aat')
->join('accounting_accounts as aa', 'aa.id', '=', 'aat.accounting_account_id')
->where('aa.business_id', $businessId)
->where('aat.source_type', 'intercompany')
->whereDate('aat.operation_date', '>=', $start)
->whereDate('aat.operation_date', '<=', $end)
->sum('aat.amount');
}
}