withoutVite(); $this->owner = User::factory()->create(); $this->manager = User::factory()->create(); config(['portal.managers' => [$this->manager->email]]); $definition = config('virtual-desk.services.project-change'); $definition['fields'] = [...$definition['fields'], 'email' => ['label' => 'SYNTHETIC email', 'type' => 'email', 'required' => false], 'amount' => ['label' => 'SYNTHETIC amount', 'type' => 'number', 'required' => false], 'choices' => ['label' => 'SYNTHETIC choices', 'type' => 'multiselect', 'required' => false, 'options' => ['0' => 'Zero', 'cotton' => 'Cotton']], 'declaration' => ['label' => 'SYNTHETIC declaration', 'type' => 'consent', 'required' => false, 'statement' => 'SYNTHETIC statement, no actual authorisation.'], ]; $edition = $this->actingAs($this->manager)->putJson('/management/balcao/servicos/project-change', ['base_version' => 0, 'definition' => $definition, 'confirm' => true, 'reason' => 'SYNTHETIC checkpoint QA, not an actual publication.'])->assertCreated()->json('id'); $case = $this->actingAs($this->owner)->postJson('/balcao-virtual/dossiers', ['title' => 'SYNTHETIC checkpoint', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); $this->id = $this->postJson('/balcao-virtual/dossiers/'.$case.'/pedidos', ['service_version_id' => $edition, 'client_key' => (string) Str::uuid()])->assertCreated()->json('id'); } private function url(): string { return '/balcao-virtual/pedidos/'.$this->id.'/rascunho'; } private function input(array $payload = [], int $version = 0, int $requestVersion = 1): array { return ['version' => $version, 'request_version' => $requestVersion, 'operation_key' => (string) Str::uuid(), 'payload' => $payload, 'edited_keys' => array_keys($payload), 'reason' => 'SYNTHETIC incomplete correction']; } private function answers(): array { return ['title' => 'SYNTHETIC main', 'description' => 'SYNTHETIC complete description', 'original_reference' => 'SYNTHETIC-REF', 'change_reason' => 'SYNTHETIC reason']; } public function test_incomplete_formats_are_recoverable_encrypted_and_never_change_the_authoritative_request_or_notifications(): void { $before = DB::table('desk_requests')->where('id', $this->id)->first(); $events = DB::table('desk_events')->count(); $notifications = DB::table('desk_notifications')->count(); $payload = ['email' => 'incomplete@', 'amount' => '-', 'choices' => ['cotton', '0'], 'declaration' => 'declined']; $this->postJson($this->url(), $this->input($payload))->assertOk()->assertJsonPath('checkpoint.version', 1)->assertJsonPath('checkpoint.payload.email', 'incomplete@'); $this->getJson($this->url())->assertOk()->assertHeader('Cache-Control', 'no-store, private')->assertJsonPath('checkpoint.payload.amount', '-')->assertJsonPath('checkpoint.payload.choices', ['0', 'cotton'])->assertJsonPath('checkpoint.payload.declaration', 'declined'); $row = DB::table('desk_request_checkpoints')->first(); $this->assertStringNotContainsString('incomplete@', $row->snapshot); $this->assertSame(app(VirtualDesk::class)->decode($before->payload), app(VirtualDesk::class)->decode($row->snapshot)['base_payload']); $this->assertEquals($before, DB::table('desk_requests')->where('id', $this->id)->first()); $this->assertSame($events, DB::table('desk_events')->count()); $this->assertSame($notifications, DB::table('desk_notifications')->count()); $this->assertDatabaseCount('desk_revisions', 0); $audit = DB::table('audit_events')->where('action', 'draft_checkpoint_saved')->first(); $this->assertSame(['version' => 1, 'request_version' => 1], json_decode($audit->after_data, true)); $this->putJson('/balcao-virtual/pedidos/'.$this->id, ['version' => 1, 'payload' => [...$this->answers(), ...$payload], 'submit' => true, 'consent' => true])->assertUnprocessable()->assertJsonValidationErrors(['payload.email', 'payload.amount']); } public function test_attempt_retry_is_idempotent_but_changed_body_reused_key_and_stale_version_conflict(): void { $input = $this->input(['title' => 'SYNTHETIC one']); $this->postJson($this->url(), $input)->assertOk(); $row = DB::table('desk_request_checkpoints')->first(); $this->postJson($this->url(), $input)->assertOk()->assertJsonPath('checkpoint.version', 1); $this->assertEquals($row, DB::table('desk_request_checkpoints')->first()); $this->postJson($this->url(), [...$input, 'payload' => ['title' => 'SYNTHETIC changed key']])->assertConflict(); $this->postJson($this->url(), $this->input(['title' => 'SYNTHETIC other tab']))->assertConflict(); $this->postJson($this->url(), $this->input(['title' => 'SYNTHETIC fresh'], 1))->assertOk()->assertJsonPath('checkpoint.version', 2); $this->assertSame(2, DB::table('audit_events')->where('action', 'draft_checkpoint_saved')->count()); } public function test_stale_main_version_is_exposed_for_comparison_and_cannot_be_silently_overwritten(): void { $this->postJson($this->url(), $this->input(['title' => 'SYNTHETIC pending']))->assertOk(); $this->putJson('/balcao-virtual/pedidos/'.$this->id, ['version' => 1, 'payload' => ['title' => 'SYNTHETIC manually changed']])->assertOk(); $this->postJson($this->url(), $this->input(['title' => 'SYNTHETIC stale'], 1))->assertConflict(); $this->getJson($this->url())->assertOk()->assertJsonPath('request.version', 2)->assertJsonPath('request.payload.title', 'SYNTHETIC manually changed')->assertJsonPath('checkpoint.request_version', 1)->assertJsonPath('checkpoint.payload.title', 'SYNTHETIC pending'); $this->postJson($this->url(), $this->input(['title' => 'SYNTHETIC deliberately recovered'], 1, 2))->assertOk()->assertJsonPath('checkpoint.base_payload.title', 'SYNTHETIC manually changed'); $this->assertSame('SYNTHETIC manually changed', app(VirtualDesk::class)->decode(DB::table('desk_requests')->where('id', $this->id)->value('payload'))['title']); } public function test_checkpoint_route_cannot_submit_accept_arbitrary_keys_or_forged_baseline_and_still_bounds_types(): void { foreach ([['submit' => true], ['consent' => true], ['base_payload' => []], ['field_declarations' => []]] as $extra) { $this->postJson($this->url(), [...$this->input(), ...$extra])->assertUnprocessable(); } foreach ([['unknown' => 'SYNTHETIC'], ['choices' => ['absent']], ['choices' => [0]], ['declaration' => true], ['email' => ['nested']], ['amount' => false], ['amount' => str_repeat('1', 1001)]] as $payload) { $this->postJson($this->url(), $this->input($payload))->assertUnprocessable(); } $this->postJson($this->url(), [...$this->input(), 'edited_keys' => ['unknown']])->assertUnprocessable(); $this->assertDatabaseCount('desk_request_checkpoints', 0); $this->postJson($this->url(), $this->input())->assertOk()->assertJsonPath('checkpoint.payload', []); } public function test_server_derives_edit_intent_and_conserves_deliberate_clear_and_zero(): void { $this->postJson($this->url(), [...$this->input(['title' => 'SYNTHETIC changed', 'amount' => 0]), 'edited_keys' => []])->assertOk()->assertJsonPath('checkpoint.edited_keys', ['title', 'description', 'regime', 'amount']); $this->postJson($this->url(), $this->input(['amount' => 0, 'choices' => [], 'declaration' => null], 1))->assertOk()->assertJsonPath('checkpoint.payload.amount', 0)->assertJsonPath('checkpoint.payload.choices', [])->assertJsonPath('checkpoint.payload.declaration', null); } public function test_manager_and_unrelated_user_cannot_read_or_write_private_recovery(): void { $this->postJson($this->url(), $this->input(['title' => 'SYNTHETIC private recovery']))->assertOk(); foreach ([$this->manager, User::factory()->create()] as $actor) { $this->actingAs($actor)->getJson($this->url())->assertForbidden()->assertDontSee('SYNTHETIC private recovery'); $this->postJson($this->url(), $this->input())->assertForbidden(); } $this->assertDatabaseCount('desk_request_checkpoints', 1); } public function test_submitted_and_closed_requests_reject_checkpoint_writes_and_do_not_create_a_declaration_snapshot_from_recovery(): void { $this->postJson($this->url(), $this->input(['declaration' => 'accepted']))->assertOk(); $this->putJson('/balcao-virtual/pedidos/'.$this->id, ['version' => 1, 'payload' => $this->answers(), 'submit' => true, 'consent' => true])->assertOk(); $row = DB::table('desk_requests')->where('id', $this->id)->first(); $this->postJson($this->url(), $this->input(['declaration' => 'accepted'], 1, 2))->assertConflict(); $this->getJson($this->url())->assertOk()->assertJsonPath('request.can_edit', false); $this->assertSame([], app(VirtualDesk::class)->decode($row->submitted_snapshot)['field_declarations']); $this->assertArrayNotHasKey('declaration', app(VirtualDesk::class)->decode($row->submitted_snapshot)['payload']); $this->assertEquals($row, DB::table('desk_requests')->where('id', $this->id)->first()); } public function test_returned_scope_is_enforced_for_both_values_and_claimed_intent(): void { $this->putJson('/balcao-virtual/pedidos/'.$this->id, ['version' => 1, 'payload' => $this->answers(), 'submit' => true, 'consent' => true])->assertOk(); app(VirtualDesk::class)->transition($this->manager, $this->id, ['version' => 2, 'status' => 'returned', 'reason' => 'SYNTHETIC description only', 'editable_fields' => ['description']]); $this->postJson($this->url(), $this->input([...$this->answers(), 'description' => 'SYNTHETIC revised'], 0, 3))->assertUnprocessable(); $data = [...$this->input([...$this->answers(), 'description' => 'SYNTHETIC revised'], 0, 3), 'edited_keys' => ['description']]; $this->postJson($this->url(), $data)->assertOk()->assertJsonPath('checkpoint.edited_keys', ['description']); $this->postJson($this->url(), [...$data, 'version' => 1, 'operation_key' => (string) Str::uuid(), 'payload' => [...$this->answers(), 'title' => 'SYNTHETIC forbidden']])->assertUnprocessable(); } public function test_missing_migration_keeps_manual_route_available_and_rollback_refuses_retained_drafts(): void { Schema::rename('desk_request_checkpoints', 'desk_request_checkpoints_qa_absent'); try { $this->getJson($this->url())->assertOk()->assertJsonPath('installed', false); $this->postJson($this->url(), $this->input())->assertConflict(); $this->putJson('/balcao-virtual/pedidos/'.$this->id, ['version' => 1, 'payload' => []])->assertOk(); } finally { Schema::rename('desk_request_checkpoints_qa_absent', 'desk_request_checkpoints'); } $this->postJson($this->url(), $this->input(['title' => 'SYNTHETIC preserved'], 0, 2))->assertOk(); $migration = require database_path('migrations/2026_11_03_040000_create_desk_request_checkpoints.php'); try { $migration->down(); $this->fail('Rollback must not remove retained drafts.'); } catch (\RuntimeException $error) { $this->assertStringContainsString('Preserve', $error->getMessage()); } $this->assertDatabaseCount('desk_request_checkpoints', 1); } public function test_organisation_recovery_is_private_per_actor_and_revoked_or_read_only_members_have_no_access(): void { $org = (string) Str::uuid(); $officer = User::factory()->create(); $reader = User::factory()->create(); DB::table('organizations')->insert(['id' => $org, 'name' => 'SYNTHETIC checkpoint org', 'organization_type' => 'private']); foreach ([[$this->owner, 'admin'], [$officer, 'officer'], [$reader, 'viewer']] as [$actor,$role]) { DB::table('memberships')->insert(['id' => (string) Str::uuid(), 'organization_id' => $org, 'user_id' => $actor->id, 'role' => $role]); } $caseId = DB::table('desk_requests')->where('id', $this->id)->value('case_id'); DB::table('desk_cases')->where('id', $caseId)->update(['organization_id' => $org]); $this->postJson($this->url(), $this->input(['title' => 'SYNTHETIC owner only']))->assertOk(); $this->actingAs($officer)->getJson($this->url())->assertOk()->assertJsonPath('checkpoint', null)->assertDontSee('SYNTHETIC owner only'); $this->postJson($this->url(), $this->input(['title' => 'SYNTHETIC officer only']))->assertOk(); $this->actingAs($this->owner)->getJson($this->url())->assertOk()->assertJsonPath('checkpoint.payload.title', 'SYNTHETIC owner only'); $this->actingAs($reader)->getJson($this->url())->assertForbidden(); DB::table('memberships')->where('organization_id', $org)->where('user_id', $officer->id)->update(['deleted_at' => now()]); $this->actingAs($officer)->getJson($this->url())->assertForbidden()->assertDontSee('SYNTHETIC officer only'); $this->postJson($this->url(), $this->input([], 1))->assertForbidden(); $this->assertDatabaseCount('desk_request_checkpoints', 2); } public function test_closed_dossier_does_not_accept_checkpoint_even_when_request_is_still_a_draft(): void { $caseId = DB::table('desk_requests')->where('id', $this->id)->value('case_id'); DB::table('desk_cases')->where('id', $caseId)->update(['closed_at' => now()]); $this->getJson($this->url())->assertOk()->assertJsonPath('request.can_edit', false); $this->postJson($this->url(), $this->input(['title' => 'SYNTHETIC forbidden']))->assertConflict(); $this->putJson('/balcao-virtual/pedidos/'.$this->id, ['version' => 1, 'payload' => []])->assertConflict(); $this->assertSame(1, DB::table('desk_requests')->where('id', $this->id)->value('version')); $this->assertDatabaseCount('desk_request_checkpoints', 0); } }