Files
Investidor-V2/tests/Feature/DeskServiceEffectiveDatesTest.php
T

199 lines
13 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\User;
use App\Services\DeskServiceAvailability;
use Carbon\CarbonImmutable;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Tests\TestCase;
final class DeskServiceEffectiveDatesTest extends TestCase
{
use RefreshDatabase;
private User $manager;
private User $investor;
private string $url = '/management/balcao/servicos/project-change';
protected function setUp(): void
{
parent::setUp();
$this->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);
}
}