134 lines
8.8 KiB
PHP
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');
|
|
}
|
|
}
|