Files
Investidor-V2/app/Services/DemographicEvidence.php
T

134 lines
8.8 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
final class DemographicEvidence
{
public function territories(): Builder
{
$public = app(InvestmentGeography::class)->publicReferences();
return DB::table('geo_locations')->whereNull('deleted_at')->whereIn('location_type', ['province', 'district'])
->where(function ($q) use ($public) {
$q->whereIn('id', (clone $public)->select('id'));
if ($this->installed() && str_ends_with(DB::connection()->getDatabaseName(), '_test')) {
// Macate/Vanduzi: expressly authorised QA references, never active registry changes.
$q->orWhere(fn ($qa) => $qa->where('source_system', 'ine-demography-qa')->where('data_classification', 'public')->whereIn('parent_id', (clone $public)->select('id')));
}
});
}
public function installed(): bool
{
return (bool) config('demography.enabled', app()->environment('local', 'testing'))
&& Schema::hasTable('demographic_imports') && Schema::hasTable('demographic_observations');
}
/** @return array<string,string> */
public static function sectors(): array
{
return ['commerce' => 'Comércio e serviços', 'industry' => 'Indústria', 'agriculture' => 'Agricultura e agro-indústria', 'education' => 'Educação', 'health' => 'Saúde', 'general' => 'Contexto geral'];
}
/** @param list<string> $territories
* @return array<string,mixed> */
public function analyse(array $territories, int $start, int $end, string $sector = 'general', ?string $edition = null): array
{
$result = ['state' => 'unavailable', 'start_year' => $start, 'end_year' => $end, 'sector' => $sector, 'territories' => [], 'differences' => [], 'comparisons' => [],
'limitations' => ['População residente não mede procura efectiva ou poder de compra.', 'Idade activa não demonstra disponibilidade ou qualificação de mão-de-obra.',
'Sem extrapolação: anos sem série oficial permanecem sem dados.', 'A demografia não altera impostos, incentivos ou probabilidades de sucesso.']];
if (! $this->installed() || $territories === []) {
return $result;
}
$locations = $this->territories()->whereIn('id', $territories)->get(['id', 'name', 'location_type']);
$records = DB::table('demographic_observations as o')->join('demographic_imports as b', 'b.id', '=', 'o.import_id')
->where('b.status', 'approved')->whereIn('o.geo_location_id', $locations->pluck('id'))->whereBetween('o.year', [$start, $end])
->where('o.sex', 'all')->when($edition, fn ($q) => $q->where('b.id', $edition))
->orderBy('o.year')->orderBy('o.import_id')->orderBy('o.id')
->get(['o.*', 'b.source', 'b.publication_version', 'b.sha256', 'b.file_name']);
$issuesByImport = DB::table('demographic_imports')->whereIn('id', $records->pluck('import_id')->unique()->all())
->get(['id', DB::raw("report->'issue_counts' AS issue_counts")])->keyBy('id');
foreach ($locations as $location) {
$series = [];
foreach ($records->where('geo_location_id', $location->id)->groupBy('import_id') as $importId => $rows) {
$first = $rows->first();
$issueCounts = json_decode($issuesByImport->get($importId)->issue_counts ?? '{}', true, 512, JSON_THROW_ON_ERROR);
$values = $rows->filter(function ($r) use ($sector) {
if ($r->indicator !== 'population') {
return in_array($sector, ['health', 'general'], true);
}
return $r->age_group === 'all' || in_array($sector, ['education', 'health', 'industry', 'agriculture', 'general'], true);
})->map(fn ($r) => ['year' => $r->year, 'indicator' => $r->indicator, 'age_group' => $r->age_group, 'residence' => $r->residence,
'value' => $r->numeric_value, 'original_value' => $r->original_value, 'unit' => $r->unit, 'data_kind' => $r->data_kind,
'sheet' => $r->sheet, 'cell' => $r->cell, 'validation_status' => $r->validation_status])->values()->all();
$years = array_values(array_unique(array_column($values, 'year')));
$series[] = ['import_id' => $importId, 'source' => $first->source, 'version' => $first->publication_version, 'sha256' => $first->sha256, 'file_name' => $first->file_name,
'values' => $values, 'missing_years' => array_values(array_diff(range($start, $end), $years)), 'issue_counts' => $issueCounts];
}
$result['territories'][] = ['id' => $location->id, 'name' => $location->name, 'level' => $location->location_type, 'series' => $series,
'factors_to_assess' => match ($sector) {
'industry' => ['Estrutura etária e evolução da população; verificar qualificações, mobilidade e emprego.'],
'agriculture' => ['População rural e mercados locais; relacionar com os clusters documentados.'],
'education' => ['Grupos etários da população-alvo; verificar cobertura e capacidade dos serviços.'],
'health' => ['Estrutura etária e estatísticas vitais com unidades conhecidas; verificar cobertura dos serviços.'],
default => ['Dimensão e evolução da população; confrontar com rendimento, acesso e condições sectoriais.'],
}];
}
// Identify concurrent values without summing editions or choosing a supposedly newer one.
foreach ($result['territories'] as $territory) {
$byDimension = [];
foreach ($territory['series'] as $series) {
foreach ($series['values'] as $row) {
$key = implode('|', [$row['year'], $row['indicator'], $row['age_group'], $row['residence'], $row['unit']]);
$byDimension[$key][] = ['import_id' => $series['import_id'], 'value' => $row['value'], 'data_kind' => $row['data_kind']];
}
}
foreach ($byDimension as $key => $values) {
if (count($values) > 1) {
$result['differences'][] = ['territory_id' => $territory['id'], 'dimension' => $key, 'state' => count(array_unique(array_column($values, 'value'))) === 1 ? 'matching_values' : 'divergent_values', 'values' => $values];
}
}
}
$result['state'] = $records->isEmpty() ? 'no_data' : 'available';
foreach ($result['territories'] as $i => $left) {
foreach (array_slice($result['territories'], $i + 1) as $right) {
foreach ($left['series'] as $a) {
foreach ($right['series'] as $b) {
foreach (array_unique([$start, $end]) as $year) {
$isTotal = fn ($r) => $r['year'] === $year && $r['indicator'] === 'population' && $r['age_group'] === 'all' && $r['residence'] === 'all';
$x = array_values(array_filter($a['values'], $isTotal))[0] ?? null;
$y = array_values(array_filter($b['values'], $isTotal))[0] ?? null;
if (! $x || ! $y) {
continue;
}
$comparable = $left['level'] === $right['level'] && $x['unit'] === $y['unit'] && $x['data_kind'] === $y['data_kind'] && $a['version'] === $b['version'];
$delta = $comparable ? DB::selectOne('SELECT (?::numeric - ?::numeric)::text AS value', [$x['value'], $y['value']])->value : null;
$result['comparisons'][] = ['year' => $year, 'left' => $left['name'], 'right' => $right['name'], 'left_value' => $x['value'], 'right_value' => $y['value'],
'left_import' => $a['import_id'], 'right_import' => $b['import_id'], 'difference' => $delta,
'explanation' => $comparable ? 'Diferença dos totais de população no mesmo ano, nível, tipo de dado, unidade e identificação de edição. Não mede procura efectiva.' : 'Âmbitos ou metadados distintos; não se calculou uma diferença como se fossem equivalentes.'];
}
}
}
}
}
return $result;
}
/** @param array<string,mixed> $context
* @return array<string,mixed> */
public function forContext(array $context): array
{
$fields = $context['fields'] ?? [];
$id = ($fields['district_id'] ?? null) ?: ($fields['province_id'] ?? null);
// No inference from an influence area to a project's implantation territory.
return $this->analyse($id ? [$id] : [], (int) date('Y'), (int) date('Y'), 'general');
}
}