oportunidades 4
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-10 07:03:59 +02:00
parent e662230df7
commit 6ddafc95ba
179 changed files with 22750 additions and 933 deletions
+60 -7
View File
@@ -23,13 +23,18 @@ final class SimulationFormDrafts
public function create(array $data, Request $request, string $key): array
{
[$actorId, $tokenHash] = $this->context($request);
$caseId = $request->input('context');
if ($caseId !== null) {
abort_unless(config('investor-path.enabled') && is_string($caseId) && Str::isUuid($caseId), 422, 'Contexto de investimento indisponível.');
abort_unless($request->user()?->hasVerifiedEmail(), 403);
}
$data = SimulationFormDraftPrivacyPolicy::apply($data, $actorId !== null);
$scope = $actorId === null ? 'anonymous:'.$tokenHash : 'user:'.$actorId;
$scope = ($actorId === null ? 'anonymous:'.$tokenHash : 'user:'.$actorId).($caseId ? ':case:'.$caseId : '');
$keyHash = hash_hmac('sha256', strtolower($key), (string) config('app.key'));
$canonicalPayload = json_encode($this->canonical($data), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
$payloadHash = hash('sha256', $canonicalPayload);
return DB::transaction(function () use ($actorId, $tokenHash, $scope, $keyHash, $payloadHash, $data): array {
return DB::transaction(function () use ($actorId, $tokenHash, $scope, $keyHash, $payloadHash, $data, $caseId, $request): array {
$this->setContext($actorId, $tokenHash);
DB::select('select pg_advisory_xact_lock(261010, hashtext(?))', [$scope]);
@@ -39,7 +44,13 @@ final class SimulationFormDrafts
->update(['deleted_at' => now()]);
}
$draft = $this->ownedQuery($actorId, $tokenHash)->lockForUpdate()->first();
$case = $caseId ? app(VirtualDesk::class)->case($request->user(), $caseId, true, true) : null;
// Shared case access never grants ownership of a private simulator snapshot.
abort_if($case && ((int) $case->user_id !== $actorId || $case->closed_at), 403);
$query = $this->ownedQuery($actorId, $tokenHash);
$draft = $case
? ($case->simulation_id ? $query->whereKey($case->simulation_id)->lockForUpdate()->firstOrFail() : null)
: $this->unscoped($query, $actorId)->lockForUpdate()->first();
if ($draft) {
if (hash_equals($draft->create_key_hash, $keyHash)) {
abort_unless(hash_equals($draft->create_payload_hash, $payloadHash), 409, 'Idempotency-Key já foi utilizada com outros dados.');
@@ -57,6 +68,11 @@ final class SimulationFormDrafts
'expires_at' => $actorId === null ? now()->addDays(30) : null,
]);
if ($case) {
DB::table('desk_cases')->where('id', $case->id)->update(['simulation_id' => $draft->id, 'version' => $case->version + 1, 'updated_at' => now()]);
app(VirtualDesk::class)->event(app(VirtualDesk::class)->case($request->user(), $case->id), $request->user(), 'context_draft_created', ['draft_id' => $draft->id]);
}
return ['draft' => $draft, 'replayed' => false];
}, 3);
}
@@ -70,6 +86,7 @@ final class SimulationFormDrafts
return DB::transaction(function () use ($id, $data, $request, $actorId, $tokenHash): SimulationFormDraft {
$this->setContext($actorId, $tokenHash);
$draft = $this->ownedQuery($actorId, $tokenHash)->lockForUpdate()->findOrFail($id);
$this->checkContext($request, $draft);
$this->requireMatch($request, $draft);
$previousEconomic = SimulationFormDraft::decryptSnapshot($draft->economic_snapshot);
if (($data['economic_snapshot']['reuse_simulation'] ?? null) === 'no') {
@@ -162,10 +179,19 @@ final class SimulationFormDrafts
{
[$actorId, $tokenHash] = $this->context($request);
return DB::transaction(function () use ($actorId, $tokenHash): ?SimulationFormDraft {
return DB::transaction(function () use ($actorId, $tokenHash, $request): ?SimulationFormDraft {
$this->setContext($actorId, $tokenHash);
$query = $this->ownedQuery($actorId, $tokenHash);
if ($request->query('context') !== null) {
$id = $request->query('context');
abort_unless(config('investor-path.enabled') && $request->user()?->hasVerifiedEmail() && is_string($id) && Str::isUuid($id), 403);
$case = app(VirtualDesk::class)->case($request->user(), $id, true);
abort_unless((int) $case->user_id === $actorId, 403);
return $this->ownedQuery($actorId, $tokenHash)->orderByDesc('updated_at')->first();
return $case->simulation_id ? $query->whereKey($case->simulation_id)->firstOrFail() : null;
}
return $this->unscoped($query, $actorId)->orderByDesc('updated_at')->first();
});
}
@@ -327,10 +353,12 @@ final class SimulationFormDrafts
{
[$actorId, $tokenHash] = $this->context($request);
return DB::transaction(function () use ($id, $actorId, $tokenHash): SimulationFormDraft {
return DB::transaction(function () use ($id, $actorId, $tokenHash, $request): SimulationFormDraft {
$this->setContext($actorId, $tokenHash);
$draft = $this->ownedQuery($actorId, $tokenHash)->findOrFail($id);
$this->checkContext($request, $draft);
return $this->ownedQuery($actorId, $tokenHash)->findOrFail($id);
return $draft;
});
}
@@ -414,6 +442,31 @@ final class SimulationFormDrafts
: $query->where('owner_user_id', $actorId)->whereNull('anonymous_token_hash');
}
/** @param Builder<SimulationFormDraft> $query
* @return Builder<SimulationFormDraft>
*/
private function unscoped(Builder $query, ?int $actorId): Builder
{
if ($actorId !== null && config('investor-path.enabled') && app(VirtualDesk::class)->installed()) {
// The direct tool has its own current draft; context drafts are opened explicitly by ID/context.
$query->whereNotExists(fn ($q) => $q->selectRaw('1')->from('desk_cases')
->whereColumn('desk_cases.simulation_id', 'simulation_form_drafts.id'));
}
return $query;
}
private function checkContext(Request $request, SimulationFormDraft $draft): void
{
$context = $request->input('context');
if ($context === null) {
return;
}
abort_unless(config('investor-path.enabled') && $request->user()?->hasVerifiedEmail() && is_string($context) && Str::isUuid($context), 403);
$case = app(VirtualDesk::class)->case($request->user(), $context, true);
abort_unless((int) $case->user_id === $request->user()->id && $case->simulation_id === $draft->id, 403, 'Rascunho pertencente a outro contexto.');
}
/** @return array{id:string,title:string,updated_at:string|null,expires_at:string|null,version_no:int} */
private function summary(SimulationFormDraft $draft): array
{