header('Idempotency-Key'); abort_unless(is_string($key) && strlen($key) >= 16 && strlen($key) <= 128, 428, 'Idempotency-Key é obrigatório.'); $result = $drafts->create($request->snapshots(), $request, $key); $draft = $result['draft']; return response()->json(['data' => ['id' => $draft->id, 'version_no' => $draft->version_no, 'replayed' => $result['replayed']], 'expires_at' => $draft->expires_at?->toISOString()], $result['replayed'] ? 200 : 201) ->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private'); } public function index(Request $request, SimulationFormDrafts $drafts): JsonResponse { abort_unless($request->user() !== null, 401); return response()->json(['data' => $drafts->ownedDrafts($request->user())])->header('Cache-Control', 'no-store, private'); } public function claimPreview(Request $request, SimulationFormDrafts $drafts): JsonResponse { abort_unless($request->user() !== null, 401); return response()->json(['data' => $drafts->claimPreview($request)])->header('Cache-Control', 'no-store, private'); } public function claim(ClaimSimulationFormDraftRequest $request, SimulationFormDrafts $drafts): JsonResponse { $result = $drafts->claim($request, $request->validated('choice'), $request->validated('existing_draft_id')); $draft = $result['draft']; return response()->json(['data' => $drafts->present($draft, true), 'choice' => $result['choice'], 'replayed' => $result['replayed'] ?? false, 'expires_at' => $draft->expires_at?->toISOString()]) ->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private'); } public function linkableBusinesses(Request $request, SimulationFormDrafts $drafts): JsonResponse { abort_unless($request->user()?->hasVerifiedEmail(), 403); return response()->json(['data' => $drafts->linkableBusinesses($request->user())])->header('Cache-Control', 'no-store, private'); } public function businessLink(Request $request, string $id, SimulationFormDrafts $drafts): JsonResponse { abort_unless(Str::isUuid($id) && $request->user()?->hasVerifiedEmail(), 404); return response()->json(['data' => $drafts->activeBusinessLink($request->user(), $id)])->header('Cache-Control', 'no-store, private'); } public function linkBusiness(LinkSimulationDraftBusinessRequest $request, string $id, SimulationFormDrafts $drafts): JsonResponse { abort_unless(Str::isUuid($id), 404); $result = $drafts->linkBusiness($request->user(), $id, $request->validated('business_id')); return response()->json(['data' => $result], isset($result['replayed']) ? 200 : 201)->header('Cache-Control', 'no-store, private'); } public function unlinkBusiness(Request $request, string $id, SimulationFormDrafts $drafts): JsonResponse { abort_unless(Str::isUuid($id) && $request->user()?->hasVerifiedEmail(), 404); return response()->json(['data' => $drafts->unlinkBusiness($request->user(), $id)])->header('Cache-Control', 'no-store, private'); } public function current(Request $request, SimulationFormDrafts $drafts): JsonResponse { $draft = $drafts->current($request); if (! $draft) { return response()->json(['data' => null], 200)->header('Cache-Control', 'no-store, private'); } return response()->json(['data' => $drafts->present($draft, $request->user() !== null)])->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private'); } public function forgetCurrent(Request $request, SimulationFormDrafts $drafts): JsonResponse { $drafts->forgetCurrent($request); return response()->json([], 204)->header('Cache-Control', 'no-store, private'); } public function show(Request $request, string $id, SimulationFormDrafts $drafts): JsonResponse { abort_unless(Str::isUuid($id), 404); $draft = $drafts->find($id, $request); return response()->json(['data' => $drafts->present($draft, $request->user() !== null)])->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private'); } public function update(StoreSimulationFormDraftRequest $request, string $id, SimulationFormDrafts $drafts): JsonResponse { abort_unless(Str::isUuid($id), 404); $draft = $drafts->update($id, $request->snapshots(), $request); return response()->json(['data' => ['id' => $draft->id, 'version_no' => $draft->version_no], 'expires_at' => $draft->expires_at?->toISOString()]) ->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private'); } public function renew(Request $request, string $id, SimulationFormDrafts $drafts): JsonResponse { abort_unless(Str::isUuid($id), 404); $draft = $drafts->renew($id, $request); return response()->json(['data' => ['id' => $draft->id, 'version_no' => $draft->version_no], 'expires_at' => $draft->expires_at?->toISOString()]) ->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private'); } public function attachSimulation(AttachSimulationToDraftRequest $request, string $id, SimulationFormDrafts $drafts, SimulationExecution $execution): JsonResponse { abort_unless(Str::isUuid($id), 404); $validated = $request->validated(); $simulation = $execution->find($validated['simulation_id'], $request); $tokenOwned = $simulation->anonymous_token_hash !== null && hash_equals($simulation->anonymous_token_hash, (string) $execution->tokenHash($request)); if ($request->user()) { $policy = app(SimulationPolicy::class); $canTransfer = $simulation->organization_id === null ? $policy->view($request->user(), $simulation) : $policy->update($request->user(), $simulation); abort_unless($canTransfer || $tokenOwned, 404); } else { abort_unless($tokenOwned, 404); } abort_unless($simulation->status === 'completed' && is_array($simulation->result_snapshot), 409, 'Só é possível reutilizar uma simulação concluída.'); $report = $simulation->reports->where('status', 'ready')->sortByDesc('created_at')->first(); abort_unless($report !== null && is_array($report->report_snapshot), 409, 'O relatório versionado da simulação ainda não está disponível.'); abort_unless(preg_match('/^[a-f0-9]{64}$/D', $report->report_sha256) === 1, 409, 'O relatório da simulação não tem um hash verificável.'); $canonicalSnapshots = DB::selectOne('select CAST(? AS jsonb)::text as report_canonical, CAST(? AS jsonb)::text as result_canonical', [ json_encode($report->report_snapshot, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE), json_encode($simulation->result_snapshot, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE), ]); abort_unless(hash_equals($report->report_sha256, hash('sha256', $canonicalSnapshots->report_canonical)) && hash_equals($canonicalSnapshots->report_canonical, $canonicalSnapshots->result_canonical), 409, 'O relatório não corresponde ao resultado versionado da simulação.'); $inputSnapshot = array_intersect_key($simulation->input_snapshot, array_flip(['base', 'rate', 'reduction'])); $sourceSnapshot = [ 'simulation_id' => $simulation->id, 'source_version_no' => $simulation->version_no, 'source_created_at' => $simulation->created_at?->toISOString(), 'source_updated_at' => $simulation->updated_at?->toISOString(), 'input_snapshot' => $inputSnapshot, 'calculation_snapshot' => $simulation->calculation_snapshot, 'result_snapshot' => $simulation->result_snapshot, 'result_sha256' => $report->report_sha256, 'report_id' => $report->id, 'report_version_no' => $report->version_no, 'report_sha256' => $report->report_sha256, 'report_snapshot' => $report->report_snapshot, 'consented_at' => now()->toISOString(), ]; $attached = $drafts->attachSimulation($id, $sourceSnapshot, $request); $draft = $attached['draft']; return response()->json([ 'data' => $drafts->present($draft, $request->user() !== null), 'replayed' => $attached['replayed'], 'expires_at' => $draft->expires_at?->toISOString(), ])->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private'); } public function destroy(Request $request, string $id, SimulationFormDrafts $drafts): JsonResponse { abort_unless(Str::isUuid($id), 404); $drafts->delete($id, $request); return response()->json([], 204)->header('Cache-Control', 'no-store, private'); } }