199 lines
13 KiB
PHP
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);
|
|
}
|
|
}
|