withoutVite(); $this->travelTo(CarbonImmutable::parse('2026-11-02 10:00:00', 'UTC')); $this->manager = User::factory()->create(); $this->investor = User::factory()->create(); config(['portal.managers' => [$this->manager->email]]); } protected function tearDown(): void { $this->travelBack(); parent::tearDown(); } private function input(int $base, string $title, ?string $date = null): array { $definition = config('virtual-desk.services.project-change'); $definition['title'] = $title; return ['base_version' => $base, 'definition' => $definition, 'confirm' => true, 'reason' => 'SYNTHETIC QA edition date; not an actual service publication.', 'effective_from' => $date]; } private function publish(int $base, string $title, ?string $date = null): string { return $this->actingAs($this->manager)->putJson($this->url, $this->input($base, $title, $date))->assertCreated()->json('id'); } private function dossier(): string { return $this->actingAs($this->investor)->postJson('/balcao-virtual/dossiers', ['title' => 'SYNTHETIC dated forms', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); } public function test_only_started_edition_is_offered_and_forged_stale_or_future_selection_never_creates_a_request(): void { $v1 = $this->publish(0, 'SYNTHETIC current edition'); $case = $this->dossier(); $key = (string) Str::uuid(); $request = $this->postJson('/balcao-virtual/dossiers/'.$case.'/pedidos', ['service_version_id' => $v1, 'client_key' => $key])->assertCreated()->json('id'); $before = DB::table('desk_requests')->where('id', $request)->first(); $v2 = $this->publish(1, 'SYNTHETIC future edition', '2026-11-03'); $this->actingAs($this->investor)->getJson('/balcao-virtual/servicos')->assertOk()->assertJsonPath('services.0.id', $v1)->assertDontSee('SYNTHETIC future edition'); $this->getJson('/balcao-virtual/dossiers/'.$case)->assertOk()->assertJsonPath('services.0.id', $v1); $this->postJson('/balcao-virtual/dossiers/'.$case.'/pedidos', ['service_version_id' => $v2, 'client_key' => (string) Str::uuid()])->assertUnprocessable()->assertJsonValidationErrors('service_version_id'); $this->assertDatabaseCount('desk_requests', 1); $this->travelTo(CarbonImmutable::parse('2026-11-03 00:00:00', 'UTC')); $this->getJson('/balcao-virtual/servicos')->assertOk()->assertJsonPath('services.0.id', $v2)->assertJsonPath('services.0.effective_from', '2026-11-03'); $this->getJson('/balcao-virtual/dossiers/'.$case)->assertOk()->assertJsonPath('services.0.id', $v2); $this->postJson('/balcao-virtual/dossiers/'.$case.'/pedidos', ['service_version_id' => $v1, 'client_key' => $key])->assertCreated()->assertJsonPath('id', $request); $this->postJson('/balcao-virtual/dossiers/'.$case.'/pedidos', ['service_version_id' => $v1, 'client_key' => (string) Str::uuid()])->assertUnprocessable()->assertJsonValidationErrors('service_version_id'); $this->getJson('/balcao-virtual/pedidos/'.$request)->assertOk()->assertJsonPath('definition.title', 'SYNTHETIC current edition')->assertJsonPath('serviceEffectiveFrom', '2026-11-02'); $this->assertEquals($before, DB::table('desk_requests')->where('id', $request)->first()); $this->postJson('/balcao-virtual/dossiers/'.$case.'/pedidos', ['service_version_id' => $v2, 'client_key' => (string) Str::uuid()])->assertCreated(); } public function test_immediate_correction_does_not_cancel_later_schedule_and_same_date_uses_highest_version(): void { $this->publish(0, 'SYNTHETIC initial'); $v2 = $this->publish(1, 'SYNTHETIC scheduled', '2026-11-12'); $v3 = $this->publish(2, 'SYNTHETIC correction', '2026-11-02'); $this->assertSame($v3, app(DeskServiceAvailability::class)->current()->first()->id); $this->getJson('/management/balcao/servicos')->assertOk()->assertJsonPath('services.1.upcoming.0.version', 2); $v4 = $this->publish(3, 'SYNTHETIC scheduled correction', '2026-11-12'); $this->getJson('/management/balcao/servicos')->assertOk()->assertJsonPath('services.1.activeVersion', 3)->assertJsonCount(1, 'services.1.upcoming')->assertJsonPath('services.1.upcoming.0.version', 4); $this->travelTo(CarbonImmutable::parse('2026-11-12 00:00:00', 'UTC')); $this->assertSame($v4, app(DeskServiceAvailability::class)->current()->first()->id); $this->assertNotSame($v2, $v4); $this->getJson('/management/balcao/servicos/project-change/versoes')->assertOk()->assertJsonPath('data.0.effective_from', '2026-11-12'); $this->assertDatabaseCount('desk_service_versions', 4); } public function test_scheduling_without_question_changes_is_a_new_edition_and_retries_keep_original_authorship_after_date_passes(): void { $this->publish(0, 'SYNTHETIC unchanged definition'); $v2 = $this->publish(1, 'SYNTHETIC unchanged definition', '2026-11-05'); $before = DB::table('desk_service_versions')->where('id', $v2)->first(); $this->actingAs($this->manager)->putJson($this->url, $this->input(1, 'SYNTHETIC unchanged definition'))->assertConflict(); $this->travelTo(CarbonImmutable::parse('2026-11-06 10:00:00', 'UTC')); $this->actingAs($this->manager)->putJson($this->url, $this->input(1, 'SYNTHETIC unchanged definition', '2026-11-05'))->assertCreated()->assertJsonPath('id', $v2); $this->assertEquals($before, DB::table('desk_service_versions')->where('id', $v2)->first()); $this->assertDatabaseCount('desk_service_versions', 2); $this->assertSame(2, DB::table('audit_events')->where('action', 'service_definition_version_published')->count()); $this->putJson($this->url, $this->input(2, 'SYNTHETIC new backdated edition', '2026-11-05'))->assertUnprocessable()->assertJsonValidationErrors('effective_from'); } public function test_invalid_dates_and_unprivileged_publication_are_rejected_without_changing_versions(): void { $this->actingAs($this->investor)->putJson($this->url, $this->input(0, 'SYNTHETIC denied', '2026-11-03'))->assertForbidden(); foreach (['2026-11-01', '2026-02-30', 'tomorrow', '2026-11-03T10:00:00Z'] as $date) { $this->actingAs($this->manager)->putJson($this->url, $this->input(0, 'SYNTHETIC invalid', $date))->assertUnprocessable()->assertJsonValidationErrors('effective_from'); } $this->assertDatabaseCount('desk_service_versions', 0); } public function test_existing_submitted_answers_and_definition_remain_intact_when_new_date_starts(): void { $v1 = $this->publish(0, 'SYNTHETIC submission edition'); $case = $this->dossier(); $request = $this->postJson('/balcao-virtual/dossiers/'.$case.'/pedidos', ['service_version_id' => $v1, 'client_key' => (string) Str::uuid()])->assertCreated()->json('id'); $answers = ['title' => 'SYNTHETIC actual answers', 'description' => 'SYNTHETIC reserved preparation description', 'original_reference' => 'SYNTHETIC-REF', 'change_reason' => 'SYNTHETIC change']; $this->putJson('/balcao-virtual/pedidos/'.$request, ['version' => 1, 'payload' => $answers, 'submit' => true, 'consent' => true])->assertOk(); $before = DB::table('desk_requests')->where('id', $request)->first(); $input = $this->input(1, 'SYNTHETIC new required question', '2026-11-03'); $input['definition']['fields']['new_evidence'] = ['label' => 'SYNTHETIC new evidence', 'type' => 'text', 'required' => true]; $v2 = $this->actingAs($this->manager)->putJson($this->url, $input)->assertCreated()->json('id'); $this->travelTo(CarbonImmutable::parse('2026-11-03 10:00:00', 'UTC')); $this->actingAs($this->investor)->getJson('/balcao-virtual/pedidos/'.$request)->assertOk()->assertJsonMissingPath('definition.fields.new_evidence'); $this->assertEquals($before, DB::table('desk_requests')->where('id', $request)->first()); $next = $this->postJson('/balcao-virtual/dossiers/'.$case.'/pedidos', ['service_version_id' => $v2, 'client_key' => (string) Str::uuid()])->assertCreated()->json('id'); $this->putJson('/balcao-virtual/pedidos/'.$next, ['version' => 1, 'payload' => $answers, 'submit' => true, 'consent' => true])->assertUnprocessable()->assertJsonValidationErrors('payload.new_evidence'); $this->getJson('/balcao-virtual/pedidos/'.$request.'/recibo')->assertOk(); } public function test_legacy_undated_versions_still_work_and_scheduling_requires_migration_without_rewriting_legacy_rows(): void { $migration = require database_path('migrations/2026_11_03_010000_desk_service_effective_dates.php'); $migration->down(); $this->assertFalse(Schema::hasColumn('desk_service_versions', 'effective_from')); $v1 = $this->publish(0, 'SYNTHETIC legacy edition'); $before = DB::table('desk_service_versions')->where('id', $v1)->first(); $this->putJson($this->url, $this->input(1, 'SYNTHETIC unsupported scheduling', '2026-11-03'))->assertUnprocessable()->assertJsonValidationErrors('effective_from'); $this->assertSame($v1, app(DeskServiceAvailability::class)->current()->first()->id); $migration->up(); $this->assertSame($before->definition, DB::table('desk_service_versions')->where('id', $v1)->value('definition')); $this->assertNull(DB::table('desk_service_versions')->where('id', $v1)->value('effective_from')); $this->publish(1, 'SYNTHETIC scheduled after legacy', '2026-11-03'); $this->assertSame($v1, app(DeskServiceAvailability::class)->current()->first()->id); $this->expectException(\RuntimeException::class); $migration->down(); } public function test_investor_path_uses_the_same_dated_edition_as_the_catalogue(): void { config(['investor-path.enabled' => true, 'investmoz_ai.enabled' => false, 'suitecrm.enabled' => false]); $v1 = $this->publish(0, 'SYNTHETIC path current'); $v2 = $this->publish(1, 'SYNTHETIC path future', '2026-11-03'); $id = $this->actingAs($this->investor)->postJson('/investimentos', ['title' => 'SYNTHETIC dated path', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); $this->getJson('/investimentos/'.$id)->assertOk()->assertJsonPath('services.0.id', $v1); $this->travelTo(CarbonImmutable::parse('2026-11-03 10:00:00', 'UTC')); $this->getJson('/investimentos/'.$id)->assertOk()->assertJsonPath('services.0.id', $v2)->assertJsonPath('services.0.effective_from', '2026-11-03'); } public function test_immediate_retry_retains_original_start_date_after_midnight(): void { $v1 = $this->publish(0, 'SYNTHETIC immediate retry'); $this->travelTo(CarbonImmutable::parse('2026-11-03 10:00:00', 'UTC')); $this->actingAs($this->manager)->putJson($this->url, $this->input(0, 'SYNTHETIC immediate retry'))->assertCreated() ->assertJsonPath('id', $v1)->assertJsonPath('effective_from', '2026-11-02'); $this->assertDatabaseCount('desk_service_versions', 1); $this->assertSame(1, DB::table('audit_events')->where('action', 'service_definition_version_published')->count()); } public function test_initial_template_publication_also_records_the_immediate_date_and_retains_legacy_retry(): void { $input = ['code' => 'project-preparation', 'confirm' => true]; $id = $this->actingAs($this->manager)->postJson('/management/balcao/servicos', $input)->assertCreated()->json('id'); $this->assertSame('2026-11-02', DB::table('desk_service_versions')->where('id', $id)->value('effective_from')); $this->assertSame($id, app(DeskServiceAvailability::class)->current()->first()->id); $this->travelTo(CarbonImmutable::parse('2026-11-03 10:00:00', 'UTC')); $this->postJson('/management/balcao/servicos', $input)->assertCreated()->assertJsonPath('id', $id); $this->assertDatabaseCount('desk_service_versions', 1); $this->assertSame('2026-11-02', DB::table('desk_service_versions')->where('id', $id)->value('effective_from')); } public function test_calendar_change_alone_does_not_create_a_duplicate_unchanged_publication(): void { $this->publish(0, 'SYNTHETIC same form'); $this->travelTo(CarbonImmutable::parse('2026-11-03 10:00:00', 'UTC')); $this->actingAs($this->manager)->putJson($this->url, $this->input(1, 'SYNTHETIC same form'))->assertUnprocessable()->assertJsonValidationErrors('definition'); $this->assertDatabaseCount('desk_service_versions', 1); $this->putJson($this->url, $this->input(1, 'SYNTHETIC same form', '2026-11-03'))->assertCreated()->assertJsonPath('version', 2); } }