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'); } }