Files
Investidor-V2/app/Http/Controllers/InvestmentTerritoryController.php

142 lines
7.8 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Opportunity;
use App\Services\InvestmentZones;
use App\Services\PortalProfileAccess;
use App\Services\TerritoryGeometry;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\ValidationException;
class InvestmentTerritoryController extends Controller
{
public function geometries(Request $request): JsonResponse
{
$data = $request->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]);
}
}