['cluster_projects', 'cluster_id', 'project_id', 'economic_clusters', 'projects'], 'corridor-project' => ['corridor_projects', 'corridor_id', 'project_id', 'development_corridors', 'projects'], 'corridor-cluster' => ['corridor_clusters', 'corridor_id', 'cluster_id', 'development_corridors', 'economic_clusters'], ]; public function store(Request $request, string $kind): RedirectResponse { [$table, $left, $right, $leftTable, $rightTable] = $this->definition($kind); $data = $request->validate([ $left => ['required', 'uuid', Rule::exists($leftTable, 'id')->whereNull('deleted_at')], $right => ['required', 'uuid', Rule::exists($rightTable, 'id')->whereNull('deleted_at')], 'association_type' => ['required', Rule::in(['official', 'influence', 'proximity', 'potential', 'historical'])], 'source_reference' => ['required', 'string', 'min:10', 'max:2000'], 'evidence_reference' => ['required', 'string', 'min:3', 'max:2000'], 'valid_from' => ['required', 'date_format:Y-m-d'], 'valid_until' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:valid_from'], ...($kind === 'cluster-project' ? ['role' => ['required', Rule::in(['anchor', 'complementary', 'supporting'])]] : []), ]); DB::transaction(function () use ($data, $table, $left, $right, $leftTable, $request): void { DB::table($leftTable)->where('id', $data[$left])->lockForUpdate()->firstOrFail(); $existing = DB::table($table)->where($left, $data[$left])->where($right, $data[$right])->whereNull('deleted_at')->lockForUpdate()->first(); abort_if($existing !== null, 409, 'Associacao ja registada.'); $values = [...$data, 'association_type' => $data['association_type'], 'validation_status' => 'pending', 'editor_id' => $request->user()->id, 'validator_id' => null, 'version_no' => 1, 'deleted_at' => null, 'updated_at' => now()]; $id = (string) Str::uuid(); DB::table($table)->insert([...$values, 'id' => $id, 'source_system' => 'portal-management']); $this->audit($request, $table, $id, 'submitted', 1); }); return back()->with('success', 'Associacao submetida para revisao independente.'); } public function review(Request $request, string $kind, string $id, string $action): RedirectResponse { abort_unless(in_array($action, ['validate', 'revoke'], true), 404); [$table] = $this->definition($kind); $data = $request->validate(['version_no' => ['required', 'integer', 'min:1']]); DB::transaction(function () use ($request, $table, $id, $action, $data): void { $relation = DB::table($table)->where('id', $id)->lockForUpdate()->first(); abort_unless($relation !== null, 404); abort_if((int) $relation->version_no !== (int) $data['version_no'], 409, 'Versao da associacao alterada.'); if ($action === 'validate') { abort_if((int) $relation->editor_id === (int) $request->user()->id, 403, 'A associacao exige revisao independente.'); abort_unless($relation->validation_status === 'pending' && $relation->source_reference && $relation->evidence_reference && $relation->valid_from, 422, 'Fonte, evidencia ou vigencia em falta.'); } $nextVersion = (int) $relation->version_no + 1; DB::table($table)->where('id', $id)->update(['validation_status' => $action === 'validate' ? 'validated' : 'revoked', 'validator_id' => $request->user()->id, 'version_no' => $nextVersion, 'updated_at' => now()]); $this->audit($request, $table, $id, $action, $nextVersion); }); return back()->with('success', $action === 'validate' ? 'Associacao validada.' : 'Associacao revogada.'); } /** @return array{string,string,string,string,string} */ private function definition(string $kind): array { abort_unless(isset(self::TABLES[$kind]), 404); return self::TABLES[$kind]; } private function audit(Request $request, string $table, string $id, string $action, int $version): void { DB::table('audit_events')->insert(['source_system' => 'investment-structure-relations', 'entity_type' => $table, 'entity_id' => $id, 'actor_user_id' => $request->user()->id, 'action' => $action, 'after_data' => json_encode(['version_no' => $version, 'validation_status' => $action === 'validate' ? 'validated' : null], JSON_THROW_ON_ERROR)]); } }