Files
Investidor-V2/app/Services/FundingDossierPreparation.php

98 lines
7.0 KiB
PHP

<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
/** A selected public funding reference in the existing encrypted investment dossier. */
final class FundingDossierPreparation
{
/** @return array<string,mixed> */
public function context(User $actor, string $id, ?string $opportunity): array
{
$case = app(VirtualDesk::class)->case($actor, $id);
abort_unless(app(VirtualDesk::class)->owns($actor, $case) || $actor->can('manage-portal'), 403);
$state = app(InvestorContext::class)->state($case);
$scope = app(InvestmentConsumerScope::class);
$opportunity = $scope->key($state, $opportunity, true);
$specific = $scope->context($state, $opportunity);
$selections = app(VirtualDesk::class)->decode($case->payload)['investor_path']['funding_selections'] ?? [];
return ['id' => $id, 'version' => (int) $case->version, 'closed' => $case->closed_at !== null, 'opportunity' => $opportunity,
'territory' => $specific['selection']['territory']['id'] ?? null,
'selections' => array_values(array_filter($selections, fn ($row) => ($row['opportunity'] ?? null) === $opportunity)),
'href' => '/investimentos/'.$id.($opportunity ? '?opportunity='.urlencode($opportunity) : '').'#financiamento'];
}
/** @param array<string,mixed> $input
* @return array<string,mixed> */
public function save(User $actor, string $programme, array $input): array
{
$data = Validator::make($input, ['context' => 'required|uuid', 'opportunity' => 'nullable|string|max:150', 'version' => 'required|integer|min:1',
'version_id' => 'required|uuid', 'product' => 'nullable|string|max:80', 'need' => ['required', Rule::in(array_keys(FundingInstitutionDetails::NEEDS))],
'territory' => 'nullable|uuid', 'location_id' => 'nullable|string|max:100', 'location_mode' => 'nullable|in:physical,coverage', 'operation_key' => 'required|uuid', 'consent' => 'accepted'])->validate();
$intent = hash('sha256', json_encode(Arr::sortRecursive([...$data, 'actor' => $actor->id, 'programme' => $programme]), JSON_THROW_ON_ERROR));
return DB::transaction(function () use ($actor, $programme, $data, $intent): array {
$desk = app(VirtualDesk::class);
$case = $desk->case($actor, $data['context'], true, true);
abort_if($case->closed_at, 409, 'O dossier está encerrado.');
$context = $this->context($actor, $case->id, $data['opportunity'] ?? null);
$payload = $desk->decode($case->payload);
$selections = $payload['investor_path']['funding_selections'] ?? [];
foreach ($selections as $selection) {
if ($selection['id'] === $data['operation_key']) {
abort_unless(hash_equals($selection['intent'], $intent), 409, 'A intenção já tem outro conteúdo.');
return $context;
}
}
foreach ($selections as $selection) {
if (($selection['opportunity'] ?? null) === $context['opportunity'] && $selection['programme_id'] === $programme && $selection['version_id'] === $data['version_id'] && ($selection['product']['code'] ?? null) === ($data['product'] ?? null) && $selection['need'] === $data['need'] && ($selection['territory'] ?? null) === ($data['territory'] ?? null) && ($selection['location']['id'] ?? null) === ($data['location_id'] ?? null)) {
return $context;
}
}
abort_unless((int) $case->version === (int) $data['version'], 409, 'O dossier mudou. Consulte-o antes de guardar esta escolha.');
$catalogue = app(FundingProgrammes::class);
$record = $catalogue->published($programme);
abort_unless($record['version_id'] === $data['version_id'], 409, 'A ficha pública mudou. Confira a versão actual.');
if (! empty($data['territory'])) {
abort_unless(app(InvestmentGeography::class)->publicReferences()->where('id', $data['territory'])->exists(), 422);
}
$location = null;
if (! empty($data['location_id'])) {
foreach ($record['payload']['locations'] ?? [] as $candidate) {
if (($candidate['id'] ?? null) === $data['location_id']) {
$location = $candidate;
}
}
abort_unless($location, 422, 'O local seleccionado não pertence à versão consultada da instituição.');
}
$product = null;
if ($record['payload']['kind'] === 'institution') {
foreach ($record['payload']['products'] as $candidate) {
if (($candidate['code'] ?? null) === ($data['product'] ?? null)) {
$product = $candidate;
break;
}
}
abort_unless(($record['payload']['products'] === [] && empty($data['product']) && app(FundingInstitutionClassification::class)->permitsUnknownProduct($record['payload'], $data['need'])) || ($product && app(FundingInstitutionDetails::class)->match($product, $data)), 422, 'O serviço ou produto escolhido não corresponde à informação da ficha para esta necessidade/local. Confira as condições ou seleccione outra referência.');
}
abort_if(count($selections) >= 200, 422, 'O dossier atingiu o limite de referências de financiamento.');
$selection = ['id' => $data['operation_key'], 'intent' => $intent, 'actor' => $actor->id, 'opportunity' => $context['opportunity'],
'programme_id' => $programme, 'version_id' => $record['version_id'], 'version' => $record['version'], 'title' => $record['payload']['title'],
'record' => $record['payload'], 'product' => $product, 'need' => $data['need'], 'territory' => $data['territory'] ?? null, 'location' => $location, 'location_mode' => $data['location_mode'] ?? 'coverage', 'categories' => $record['payload']['institution_categories'] ?? [], 'institution_types' => $record['payload']['institution_types'] ?? [], 'institution_key' => $record['payload']['institution_key'] ?? null, 'recorded_at' => now()->toIso8601String(),
'notice' => 'Referência para preparação. Não é candidatura, contacto enviado, aprovação ou garantia de financiamento.'];
$payload['investor_path']['funding_selections'][] = $selection;
DB::table('desk_cases')->where('id', $case->id)->update(['payload' => $desk->encode($payload), 'version' => (int) $case->version + 1, 'updated_at' => now()]);
$desk->event($case, $actor, 'funding_preparation_saved', ['selection_id' => $selection['id'], 'programme_id' => $programme, 'version_id' => $record['version_id'], 'opportunity' => $context['opportunity']]);
return $this->context($actor, $case->id, $context['opportunity']);
});
}
}