payload(); $created = $this->postJson('/api/v1/simulation-drafts', $payload, [ 'Idempotency-Key' => $key, 'X-Simulation-Token' => $token, ])->assertCreated()->assertJsonPath('data.version_no', 1); $id = $created->json('data.id'); $stored = DB::table('simulation_form_drafts')->where('id', $id)->first(); $this->assertNotSame(json_encode($payload['personal_snapshot']), $stored->personal_snapshot); $this->assertNotSame(json_encode($payload['economic_snapshot']), $stored->economic_snapshot); $this->getJson('/api/v1/simulation-drafts/current', ['X-Simulation-Token' => $token]) ->assertOk()->assertJsonPath('data.personal_snapshot.legal_name', 'Entidade de teste') ->assertJsonPath('data.economic_snapshot.base', '1200'); $this->getJson('/api/v1/simulation-drafts/'.$id, ['X-Simulation-Token' => Str::random(48)])->assertNotFound(); } public function test_creation_is_idempotent_and_rejects_key_reuse_with_changed_data(): void { $token = Str::random(48); $key = (string) Str::uuid(); $headers = ['Idempotency-Key' => $key, 'X-Simulation-Token' => $token]; $payload = $this->payload(); $first = $this->postJson('/api/v1/simulation-drafts', $payload, $headers)->assertCreated(); $id = $first->json('data.id'); $this->postJson('/api/v1/simulation-drafts', $payload, $headers) ->assertOk()->assertJsonPath('data.id', $id)->assertJsonPath('data.replayed', true); $payload['economic_snapshot']['base'] = '1300'; $this->postJson('/api/v1/simulation-drafts', $payload, $headers)->assertConflict(); } public function test_updates_require_a_current_etag_and_delete_is_reversible_until_expiry(): void { $token = Str::random(48); $headers = ['Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => $token]; $payload = $this->payload(); $created = $this->postJson('/api/v1/simulation-drafts', $payload, $headers)->assertCreated(); $id = $created->json('data.id'); $url = '/api/v1/simulation-drafts/'.$id; $this->putJson($url, $payload, ['X-Simulation-Token' => $token])->assertStatus(428); $this->putJson($url, $payload, ['X-Simulation-Token' => $token, 'If-Match' => '"sfd-'.$id.'-v99"'])->assertStatus(412); $updated = $this->putJson($url, $payload, ['X-Simulation-Token' => $token, 'If-Match' => $created->headers->get('ETag')]) ->assertOk()->assertJsonPath('data.version_no', 2); $this->deleteJson($url, [], ['X-Simulation-Token' => $token, 'If-Match' => $updated->headers->get('ETag')])->assertNoContent(); $this->assertDatabaseHas('simulation_form_drafts', ['id' => $id]); $this->assertDatabaseMissing('simulation_form_drafts', ['id' => $id, 'deleted_at' => null]); } public function test_server_rejects_invalid_rates_and_unlisted_snapshot_fields(): void { $token = Str::random(48); $payload = $this->payload(); $payload['economic_snapshot']['rate'] = '101'; $this->postJson('/api/v1/simulation-drafts', $payload, [ 'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => $token, ])->assertUnprocessable()->assertJsonValidationErrors('economic_snapshot.rate'); $payload = $this->payload(); $payload['economic_snapshot']['unapproved_field'] = 'not accepted'; $this->postJson('/api/v1/simulation-drafts', $payload, [ 'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => $token, ])->assertUnprocessable()->assertJsonValidationErrors('economic_snapshot'); } /** @return array{personal_snapshot: array, economic_snapshot: array} */ private function payload(): array { return [ 'personal_snapshot' => ['legal_name' => 'Entidade de teste', 'email' => 'teste@example.com'], 'economic_snapshot' => ['purpose' => 'simulate_only', 'tax_calculation' => 'yes', 'base' => '1200', 'rate' => '8', 'reduction' => '0'], ]; } }