validate(['territory_id' => ['required', 'uuid']]); $profile = $request->is('minhas-oportunidades/*'); $management = $request->is('management/*') || $profile; $actor = $request->user(); $access = app(PortalProfileAccess::class); // The general consumers use this same endpoint while authenticated. // Reuse the read access already granted by the private cadastral routes. $profileAccess = $actor && $actor->hasVerifiedEmail() && ($access->grants($actor) !== [] || $access->superuser($actor)); $internal = $profileAccess || ($actor?->hasVerifiedEmail() && $actor->can('manage-portal')); if ($management) { abort_unless($profile ? $profileAccess : $internal, 403); } return response()->json([ ...app(TerritoryGeometry::class)->context($data['territory_id'], $internal), 'geometry_scope' => $internal ? 'authorized_internal' : 'public', ])->header('Cache-Control', 'private, no-store'); } private const TABLES = ['opportunities' => 'opportunities', 'projects' => 'projects', 'clusters' => 'economic_clusters', 'corridors' => 'development_corridors', 'zones' => 'investment_zones']; public function subjects(Request $request): JsonResponse { $data = $request->validate(['kind' => ['required', 'in:'.implode(',', array_keys(self::TABLES))], 'q' => ['nullable', 'string', 'max:100'], 'page' => ['nullable', 'integer', 'min:1']]); $column = $data['kind'] === 'opportunities' ? 'title' : 'name'; $query = DB::table(self::TABLES[$data['kind']])->whereNull('deleted_at'); if ($data['kind'] === 'opportunities' && Schema::hasColumn('opportunities', 'owner_user_id')) { $query->whereIn('id', app(PortalProfileAccess::class)->scope(Opportunity::query(), $request->user())->select('id')); } if (! empty($data['q'])) { $query->whereRaw($column.' ILIKE ?', ['%'.addcslashes($data['q'], '%_\\').'%']); } return response()->json($query->orderBy($column)->orderBy('id')->paginate(50, ['id', $column.' as name'])); } public function options(Request $request): JsonResponse { if ($request->is('minhas-oportunidades/*')) { $access = app(PortalProfileAccess::class); abort_unless($access->grants($request->user()) !== [] || $access->superuser($request->user()), 403); } $data = $request->validate(['type' => ['required', 'in:province,district,admin_post,locality'], 'parent_id' => ['nullable', 'uuid'], 'q' => ['nullable', 'string', 'max:100'], 'page' => ['nullable', 'integer', 'min:1']]); $query = DB::table('geo_locations')->whereNull('deleted_at')->where('location_type', $data['type']) ->whereIn('data_classification', ['public', 'internal']); if ($data['type'] !== 'province') { $parentType = ['district' => 'province', 'admin_post' => 'district', 'locality' => 'admin_post'][$data['type']]; if (! DB::table('geo_locations')->where('id', $data['parent_id'] ?? '00000000-0000-0000-0000-000000000000')->whereNull('deleted_at')->whereIn('data_classification', ['public', 'internal'])->where('location_type', $parentType)->exists()) { throw ValidationException::withMessages(['parent_id' => 'Seleccione um pai activo do nível anterior.']); } $query->where('parent_id', $data['parent_id'] ?? '00000000-0000-0000-0000-000000000000'); } if (! empty($data['q'])) { $term = '%'.addcslashes($data['q'], '%_\\').'%'; $query->where(fn ($q) => $q->whereRaw('name ILIKE ?', [$term])->orWhereRaw('code ILIKE ?', [$term])->orWhereRaw('source_code ILIKE ?', [$term])); } return response()->json($query->orderBy('name')->orderBy('id')->paginate(100, ['id', 'name', 'parent_id', 'location_type', 'source_code'])); } public function show(string $kind, string $id): JsonResponse { $table = self::TABLES[$kind] ?? abort(404); $record = DB::table($table)->where('id', $id)->whereNull('deleted_at')->first(); abort_unless($record !== null, 404); $path = []; $current = $record->primary_territory_id; for ($i = 0; $current && $i < 5; $i++) { $location = DB::table('geo_locations')->where('id', $current)->first(['id', 'name', 'parent_id', 'location_type', 'deleted_at']); if (! $location) { break; } $path[] = $location; $current = $location->parent_id; } return response()->json(['version' => $record->territory_version, 'path' => $path]); } public function save(Request $request, string $kind, string $id): JsonResponse { $table = self::TABLES[$kind] ?? abort(404); if ($kind === 'zones') { abort_unless($request->user()->two_factor_confirmed_at && $request->user()->two_factor_secret, 403); } $data = $request->validate(['version' => ['required', 'integer', 'min:0'], 'path' => ['present', 'array', 'max:4'], 'path.*' => ['required', 'uuid', 'distinct']]); $version = DB::transaction(function () use ($request, $table, $id, $data) { DB::select('SELECT pg_advisory_xact_lock(280926, 150000)'); $record = DB::table($table)->where('id', $id)->whereNull('deleted_at')->lockForUpdate()->first(); abort_unless($record !== null, 404); abort_unless((int) $record->territory_version === $data['version'], 409, 'Localização alterada. Recarregue a ficha.'); $parent = null; $names = []; foreach (array_values($data['path']) as $index => $locationId) { $location = DB::table('geo_locations')->where('id', $locationId)->whereNull('deleted_at')->whereIn('data_classification', ['public', 'internal'])->first(); if (! $location || $location->location_type !== ['province', 'district', 'admin_post', 'locality'][$index] || ($index > 0 && $location->parent_id !== $parent)) { throw ValidationException::withMessages(['path' => 'Seleccione uma hierarquia territorial coerente e activa.']); } $parent = $locationId; $names[] = $location->name; } $version = $data['version'] + 1; $changes = ['primary_territory_id' => $parent, 'territory_version' => $version, 'updated_at' => now()]; if (in_array($table, ['opportunities', 'investment_zones'], true)) { $changes['province'] = $names[0] ?? null; $changes['district'] = $names[1] ?? null; } if ($table === 'investment_zones') { app(InvestmentZones::class)->save($id, [...$changes, 'version_no' => $record->version_no], $request->user()->id); } else { DB::table($table)->where('id', $id)->update($changes); } DB::table('audit_events')->insert(['source_system' => 'investment-territory', 'entity_type' => $table, 'actor_user_id' => $request->user()->id, 'action' => 'primary_territory_saved', 'after_data' => json_encode(['record_id' => $id, 'territory_id' => $parent, 'version' => $version], JSON_THROW_ON_ERROR)]); return $version; }); return response()->json(['version' => $version]); } }