Files
Investidor-V2/app/Http/Controllers/Api/V1/SimulationController.php
T
nala e65cb26901
tests / ci (push) Canceled after 0s
varias alteracoes
2026-10-06 14:50:12 +02:00

171 lines
9.0 KiB
PHP

<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreSimulationRequest;
use App\Http\Requests\UpdateSimulationScenarioRequest;
use App\Models\QuestionnaireVersion;
use App\Models\Simulation;
use App\Models\SimulationJob;
use App\Policies\SimulationPolicy;
use App\Services\SimulationExecution;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class SimulationController extends Controller
{
public function questionnaire(string $code): JsonResponse
{
abort_unless(preg_match('/^[a-z0-9][a-z0-9-]{0,79}$/', $code) === 1, 404);
$version = QuestionnaireVersion::query()->where('code', $code)->where('locale', 'pt-MZ')->where('status', 'published')
->where(fn ($query) => $query->whereNull('valid_from')->orWhereDate('valid_from', '<=', today()))
->where(fn ($query) => $query->whereNull('valid_to')->orWhereDate('valid_to', '>', today()))
->orderByDesc('version_no')->firstOrFail();
$etag = '"questionnaire-'.$version->id.'-v'.$version->version_no.'"';
return response()->json(['data' => $version])->header('ETag', $etag)->header('Cache-Control', 'private, max-age=60');
}
public function store(StoreSimulationRequest $request, SimulationExecution $execution): JsonResponse
{
$key = $request->header('Idempotency-Key');
abort_unless(is_string($key) && strlen($key) >= 16 && strlen($key) <= 128, 428, 'Idempotency-Key é obrigatório.');
$actorId = $request->user()?->id;
$token = $actorId === null ? $request->header('X-Simulation-Token') : null;
abort_if($actorId === null && (! is_string($token) || strlen($token) < 32 || strlen($token) > 256), 422, 'X-Simulation-Token opaco gerado pelo cliente é obrigatório para execução anónima.');
$validated = $request->validated();
$data = ['base' => (string) $validated['base'], 'rate' => (string) $validated['rate'],
'reduction' => (string) ($validated['reduction'] ?? '0'), 'organization_id' => $validated['organization_id'] ?? null];
$organizationId = $data['organization_id'] ?? null;
$execution->authorizeOrganization($actorId, $organizationId);
$result = $execution->create($data, $actorId, $organizationId, $token, $key);
$simulation = $result['simulation'];
$body = ['data' => $simulation, 'replayed' => $result['replayed']];
if ($actorId === null) {
$body['access_token'] = $token;
}
return response()->json($body, $result['replayed'] ? 200 : 202)
->header('ETag', $execution->etag($simulation))
->header('Location', url('/api/v1/simulations/'.$simulation->id));
}
public function show(Request $request, string $id, SimulationExecution $execution): JsonResponse
{
abort_unless(Str::isUuid($id), 404);
$simulation = $execution->find($id, $request);
if ($request->user()) {
abort_unless(app(SimulationPolicy::class)->view($request->user(), $simulation), 404);
} else {
abort_unless($simulation->anonymous_token_hash !== null && hash_equals($simulation->anonymous_token_hash, (string) $execution->tokenHash($request)), 404);
}
return response()->json(['data' => $simulation])->header('ETag', $execution->etag($simulation));
}
public function history(Request $request, SimulationExecution $execution): JsonResponse
{
$user = $request->user();
abort_unless($user !== null, 401);
abort_unless($user->hasVerifiedEmail(), 403, 'Verifique o correio electrónico para consultar o histórico.');
$perPage = min(50, max(1, $request->integer('per_page', 20)));
$page = max(1, $request->integer('page', 1));
$history = DB::transaction(function () use ($execution, $user, $perPage, $page) {
$execution->setContext($user->id, null);
return Simulation::query()
->select([
'simulations.id', 'simulations.organization_id', 'visible_organization.name as organization_name',
'simulations.status', 'simulations.version_no', 'simulations.created_at', 'simulations.updated_at',
])
->leftJoin('organizations as visible_organization', 'visible_organization.id', '=', 'simulations.organization_id')
->where(function ($scope) use ($user) {
$scope->where(fn ($personal) => $personal->whereNull('simulations.organization_id')->where('simulations.owner_user_id', $user->id))
->orWhere(fn ($organization) => $organization->whereNotNull('simulations.organization_id')->whereExists(
fn ($membership) => $membership->selectRaw('1')->from('memberships')
->whereColumn('memberships.organization_id', 'simulations.organization_id')
->where('memberships.user_id', $user->id)->whereNull('memberships.deleted_at'),
));
})
->withExists(['reports as has_ready_report' => fn ($query) => $query->where('status', 'ready')])
->orderByDesc('simulations.created_at')->orderByDesc('simulations.id')
->paginate($perPage, ['*'], 'page', $page);
});
return response()->json([
'data' => $history->getCollection()->map(fn (Simulation $simulation): array => [
'id' => $simulation->id,
'scope' => $simulation->organization_id === null ? 'personal' : 'organization',
'organization_name' => $simulation->getAttribute('organization_name'),
'status' => $simulation->status,
'version_no' => $simulation->version_no,
'created_at' => $simulation->created_at?->toISOString(),
'updated_at' => $simulation->updated_at?->toISOString(),
'has_ready_report' => (bool) $simulation->getAttribute('has_ready_report'),
]),
'meta' => [
'current_page' => $history->currentPage(),
'last_page' => $history->lastPage(),
'per_page' => $history->perPage(),
'total' => $history->total(),
],
])->header('Cache-Control', 'no-store, private');
}
public function updateScenario(UpdateSimulationScenarioRequest $request, string $id, string $scenarioId, SimulationExecution $execution): JsonResponse
{
abort_unless(Str::isUuid($id) && Str::isUuid($scenarioId), 404);
$simulation = $execution->find($id, $request);
if ($request->user()) {
abort_unless(app(SimulationPolicy::class)->update($request->user(), $simulation), 403);
} else {
abort_unless($simulation->anonymous_token_hash !== null && hash_equals($simulation->anonymous_token_hash, (string) $execution->tokenHash($request)), 404);
}
$validated = $request->validated();
$data = ['label' => (string) $validated['label'], 'base' => (string) $validated['base'], 'rate' => (string) $validated['rate'],
'reduction' => (string) ($validated['reduction'] ?? '0')];
$updated = $execution->updateScenario($id, $scenarioId, $data, $request);
return response()->json(['data' => $updated], 202)->header('ETag', $execution->etag($updated));
}
public function report(Request $request, string $id, string $reportId, SimulationExecution $execution): JsonResponse
{
abort_unless(Str::isUuid($id) && Str::isUuid($reportId), 404);
$simulation = $execution->find($id, $request);
if ($request->user()) {
abort_unless(app(SimulationPolicy::class)->view($request->user(), $simulation), 404);
} else {
abort_unless($simulation->anonymous_token_hash !== null && hash_equals($simulation->anonymous_token_hash, (string) $execution->tokenHash($request)), 404);
}
$report = $simulation->reports->firstWhere('id', $reportId);
abort_if($report === null, 404);
return response()->json(['data' => $report]);
}
public function jobs(Request $request, string $id, SimulationExecution $execution): JsonResponse
{
abort_unless(Str::isUuid($id), 404);
$simulation = $execution->find($id, $request);
if ($request->user()) {
abort_unless(app(SimulationPolicy::class)->view($request->user(), $simulation), 404);
} else {
abort_unless($simulation->anonymous_token_hash !== null && hash_equals($simulation->anonymous_token_hash, (string) $execution->tokenHash($request)), 404);
}
$jobs = DB::transaction(function () use ($request, $id, $execution) {
$execution->setContext($request->user()?->id, $execution->tokenHash($request));
return SimulationJob::query()->where('simulation_id', $id)
->orderBy('created_at')->orderBy('id')->get();
});
return response()->json(['data' => $jobs]);
}
}