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(); $organizationId = $validated['organization_id'] ?? null; $execution->authorizeOrganization($actorId, $organizationId); if (($validated['scenario_kind'] ?? null) === 'fiscal_rule_published') { $input = array_intersect_key($validated, array_flip(['base', 'currency', 'reference_date', 'operation', 'scope_declared', 'monthly_sales', 'sales_vat_basis'])); $input['scope_declared'] = (bool) $input['scope_declared']; $result = $execution->createPublishedFiscal($input, $actorId, $organizationId, $token, $key); } else { $data = ['base' => (string) $validated['base'], 'rate' => (string) $validated['rate'], 'reduction' => (string) ($validated['reduction'] ?? '0')]; $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]); } }