*/ 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 $input * @return array */ 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']); }); } }