validate(['cluster_id' => ['nullable', 'required_without_all:corridor_id,project_id', 'prohibits:corridor_id,project_id', 'uuid', Rule::exists('economic_clusters', 'id')->whereNull('deleted_at')], 'corridor_id' => ['nullable', 'required_without_all:cluster_id,project_id', 'prohibits:cluster_id,project_id', 'uuid', Rule::exists('development_corridors', 'id')->whereNull('deleted_at')], 'project_id' => ['nullable', 'required_without_all:cluster_id,corridor_id', 'prohibits:cluster_id,corridor_id', 'uuid', Rule::exists('projects', 'id')->whereNull('deleted_at')], 'association_type' => ['required', 'in:official,influence,proximity,potential,historical'], 'role' => ['required', 'in:primary,beneficiary,served,enabled,dependent,related,potential,historical'], 'source_reference' => ['required', 'string', 'min:10', 'max:2000'], 'justification' => ['required', 'string', 'min:10', 'max:3000'], 'legal_source_id' => ['nullable', 'required_if:association_type,official', 'uuid', 'exists:investment_legal_sources,id'], 'valid_from' => ['required', 'date_format:Y-m-d'], 'valid_until' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:valid_from'], 'priority' => ['sometimes', 'integer', 'between:0,100'], 'is_primary' => ['sometimes', 'boolean']]); return DB::transaction(function () use ($request, $id, $data) { InvestmentZone::whereKey($id)->lockForUpdate()->firstOrFail(); $query = DB::table('investment_zone_relations')->where('zone_id', $id); foreach (['cluster_id', 'corridor_id', 'project_id'] as $field) { if (! empty($data[$field])) { $query->where($field, $data[$field]); } } abort_if($query->exists(), 409, 'Relação já registada.'); $relation = (string) Str::uuid(); DB::table('investment_zone_relations')->insert([...$data, 'id' => $relation, 'zone_id' => $id, 'editor_id' => $request->user()->id, 'created_at' => now(), 'updated_at' => now()]); $this->audit($relation, $request->user()->id, 'created', 1); return response()->json(DB::table('investment_zone_relations')->where('id', $relation)->first(), 201); }); } public function review(Request $request, string $id, string $action): JsonResponse { $data = $request->validate(['version_no' => ['required', 'integer', 'min:1']]); return DB::transaction(function () use ($request, $id, $action, $data) { $relation = DB::table('investment_zone_relations')->where('id', $id)->lockForUpdate()->first(); abort_unless($relation !== null, 404); abort_if((int) $relation->version_no !== (int) $data['version_no'], 409, 'Versão alterada.'); if ($action === 'validate') { abort_if((int) $relation->editor_id === $request->user()->id, 403, 'A relação exige revisão independente.'); abort_unless($relation->validation_status === 'pending', 422, 'Relação já analisada.'); if ($relation->legal_source_id) { $source = DB::table('investment_legal_sources')->where('id', $relation->legal_source_id)->lockForUpdate()->first(); abort_unless($source && $source->validation_status === 'validated' && $source->valid_from <= today()->toDateString() && (! $source->valid_until || $source->valid_until >= today()->toDateString()), 422, 'Fonte válida em falta.'); } } DB::table('investment_zone_relations')->where('id', $id)->update(['validation_status' => $action === 'validate' ? 'validated' : 'revoked', 'validator_id' => $action === 'validate' ? $request->user()->id : $relation->validator_id, 'version_no' => $relation->version_no + 1, 'updated_at' => now()]); $this->audit($id, $request->user()->id, $action, $relation->version_no + 1); return response()->json(DB::table('investment_zone_relations')->where('id', $id)->first()); }); } private function audit(string $id, int $actor, string $action, int $version): void { DB::table('audit_events')->insert(['source_system' => 'investment-zones', 'entity_type' => 'investment_zone_relations', 'entity_id' => $id, 'actor_user_id' => $actor, 'action' => $action, 'after_data' => json_encode(['version_no' => $version], JSON_THROW_ON_ERROR)]); } }