Prepare supplied financial directory with product coverage and native dossier continuity

This commit is contained in:
2026-11-10 18:51:23 +02:00
parent c8e0a7ee39
commit 5ff9402fd0
25 changed files with 2730 additions and 37 deletions
+19 -2
View File
@@ -18,7 +18,7 @@ use Symfony\Component\HttpFoundation\BinaryFileResponse;
/** Informational funding catalogue, separate from applications, credit decisions and fiscal calculations. */
final class FundingProgrammes
{
public const KINDS = ['fund' => 'Fundo', 'programme' => 'Programa de apoio', 'incentive' => 'Incentivo'];
public const KINDS = ['institution' => 'Instituição financeira', 'fund' => 'Fundo', 'programme' => 'Programa de apoio', 'incentive' => 'Incentivo'];
public const WINDOWS = ['dated' => 'Datas documentadas', 'rolling' => 'Recepção contínua documentada', 'unknown' => 'Janela por confirmar', 'closed' => 'Encerrada segundo a fonte'];
@@ -56,6 +56,7 @@ final class FundingProgrammes
public function catalogueQuery(array $filters): Builder
{
$query = $this->publicQuery();
app(FundingInstitutionDetails::class)->applyFilters($query, $filters);
$includeUnknown = ($filters['certainty'] ?? 'include_unknown') === 'include_unknown';
if (! empty($filters['sector'])) {
$query->where(function (Builder $scope) use ($filters, $includeUnknown): void {
@@ -69,7 +70,7 @@ final class FundingProgrammes
if (! empty($filters['territory'])) {
$path = array_column(app(InvestmentGeography::class)->path($filters['territory']), 'id');
$query->where(function (Builder $scope) use ($filters, $path, $includeUnknown): void {
$scope->whereRaw("v.payload->>'coverage' = 'national'")
$scope->whereRaw("v.payload->>'kind' = 'institution'")->orWhereRaw("v.payload->>'coverage' = 'national'")
->orWhereJsonContains('v.payload->territory_ancestor_ids', $filters['territory']);
foreach ($path as $id) {
$scope->orWhereJsonContains('v.payload->territory_ids', $id);
@@ -102,6 +103,11 @@ final class FundingProgrammes
public function filterExplanation(array $record, array $filters): array
{
$payload = $record['payload'];
if ($payload['kind'] === 'institution') {
$products = array_values(array_filter($payload['products'] ?? [], fn ($p) => app(FundingInstitutionDetails::class)->match($p, $filters)));
return ['institution' => $products === [] ? 'Instituição identificada; produtos, cobertura e disponibilidade por confirmar. Não é uma correspondência confirmada.' : 'Consulte os produtos correspondentes e as condições por confirmar. Sede e balcões não determinam a cobertura do financiamento.'];
}
$result = [];
if (! empty($filters['sector'])) {
$result['sector'] = $payload['sectors_scope'] === 'unknown'
@@ -200,6 +206,13 @@ final class FundingProgrammes
if (! $row && DB::table('funding_programmes')->where('code', $data['code'])->exists()) {
throw ValidationException::withMessages(['code' => 'Este identificador já pertence a uma ficha. Abra-a para actualizar ou acrescentar uma janela.']);
}
if ($draft['kind'] === 'institution') {
DB::select('select pg_advisory_xact_lock(261102, hashtext(?))', ['institution:'.$draft['institution_key']]);
$duplicate = DB::table('funding_programmes')->whereRaw("draft->>'kind' = 'institution'")->whereRaw("draft->>'institution_key' = ?", [$draft['institution_key']])->when($id, fn ($q) => $q->where('id', '<>', $id))->exists();
if ($duplicate) {
throw ValidationException::withMessages(['institution_key' => 'A instituição já tem ficha. Reutilize a identidade existente; não substitua as suas edições.']);
}
}
$id ??= (string) Str::uuid();
DB::table('funding_programmes')->updateOrInsert(['id' => $id], ['code' => $data['code'], 'version' => (int) $data['version'] + 1,
'draft' => json_encode($draft, JSON_THROW_ON_ERROR), 'created_by' => $row->created_by ?? $actor->id,
@@ -225,6 +238,7 @@ final class FundingProgrammes
'sectors_scope' => 'required|in:all,selected,unknown', 'sectors_text' => 'nullable|string|max:2000', 'sector_ids' => 'array|max:40',
'sector_ids.*' => ['required', 'integer', 'distinct', Rule::in(Sector::choices()->whereNotNull('published_at')->pluck('id')->all())],
'resource_ids' => 'array|max:30', 'resource_ids.*' => 'required|integer|distinct|exists:portal_resources,id',
'funding_needs' => 'array|max:6', 'funding_needs.*' => ['required', 'distinct', Rule::in(array_keys(FundingInstitutionDetails::NEEDS))],
'criteria' => 'array|max:30', 'windows' => 'array|max:20',
'windows.*' => 'required|array:code,title,mode,start_on,end_on,coverage,coverage_text,territory_ids,source_url,source_reference,checked_on,application_url,criteria',
'windows.*.code' => 'required|string|regex:/^[a-z0-9][a-z0-9-]{0,79}$/D|distinct', 'windows.*.title' => "$required|string|max:250",
@@ -244,6 +258,9 @@ final class FundingProgrammes
$rules[$prefix.'.*.explanation'] = "$required|string|max:2000";
}
$data = Validator::make($input, $rules)->validate();
if ($data['kind'] === 'institution') {
$data = [...$data, ...app(FundingInstitutionDetails::class)->normalize($input, $publishing)];
}
$data = [...$data, 'territory_ids' => $data['territory_ids'] ?? [], 'sector_ids' => $data['sector_ids'] ?? [], 'resource_ids' => $data['resource_ids'] ?? [],
'criteria' => $data['criteria'] ?? [], 'windows' => $data['windows'] ?? []];
if ($data['coverage'] !== 'territorial' && $data['territory_ids'] !== []) {