validate(['opportunity_id' => ['required', 'integer', Rule::exists('opportunities', '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(); $opportunity = Opportunity::whereKey($data['opportunity_id'])->lockForUpdate()->firstOrFail(); $itemId = app(InvestmentIdentity::class)->forOpportunity($opportunity->id); abort_if(DB::table('investment_zone_links')->where('zone_id', $id)->where('item_id', $itemId)->exists(), 409, 'Associação já registada.'); unset($data['opportunity_id']); $linkId = (string) Str::uuid(); DB::table('investment_zone_links')->insert([...$data, 'id' => $linkId, 'item_id' => $itemId, 'zone_id' => $id, 'editor_id' => $request->user()->id, 'created_at' => now(), 'updated_at' => now()]); $this->audit($linkId, $request->user()->id, 'created', 1); return response()->json(DB::table('investment_zone_links')->where('id', $linkId)->first(), 201); }); } public function transition(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) { $link = DB::table('investment_zone_links')->where('id', $id)->lockForUpdate()->first(); abort_unless($link !== null, 404); abort_if((int) $link->version_no !== (int) $data['version_no'], 409, 'Versão alterada.'); if ($action === 'validate') { abort_if((int) $link->editor_id === $request->user()->id, 403, 'Outra pessoa deve validar a associação.'); abort_unless($link->validation_status === 'pending', 422, 'A associação já foi analisada.'); if ($link->legal_source_id) { $source = DB::table('investment_legal_sources')->where('id', $link->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 jurídica inválida.'); } } DB::table('investment_zone_links')->where('id', $id)->update(['version_no' => $link->version_no + 1, 'validation_status' => $action === 'validate' ? 'validated' : 'revoked', 'validator_id' => $action === 'validate' ? $request->user()->id : $link->validator_id, 'updated_at' => now()]); $this->audit($id, $request->user()->id, $action, $link->version_no + 1); return response()->json(DB::table('investment_zone_links')->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_links', 'entity_id' => $id, 'actor_user_id' => $actor, 'action' => $action, 'after_data' => json_encode(['version_no' => $version], JSON_THROW_ON_ERROR)]); } }