154 lines
6.0 KiB
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');
|
|
}
|
|
}
|