create(); // Asset 1: Laptop Dell (will have conflict on week 2) $asset1 = SnipeAsset::factory()->create([ 'name' => 'Portátil Dell Latitude 5420', 'category_name' => 'Portáteis', 'requestable' => true, 'active' => true, ]); // Asset 2: Alternative Laptop Lenovo (available in the same category) $asset2 = SnipeAsset::factory()->create([ 'name' => 'Portátil Lenovo ThinkPad', 'category_name' => 'Portáteis', 'requestable' => true, 'active' => true, ]); // Create an existing reservation on week 2 for asset1: Terça-feira 20/10 11:00 - 13:00 $existing = Reservation::create([ 'reference' => 'REQ-CONFLICT-001', 'requester_id' => User::factory()->create()->id, 'purpose' => 'Reserva prévia de outro professor', 'usage_location' => 'Sala 101', 'starts_at' => '2026-10-20 11:00:00', 'ends_at' => '2026-10-20 13:00:00', 'status' => ReservationStatus::Approved, ]); $existing->items()->create([ 'snipe_asset_id' => $asset1->id, 'starts_at' => '2026-10-20 11:00:00', 'ends_at' => '2026-10-20 13:00:00', 'status' => ReservationItemStatus::Reserved, ]); // Professor X wants asset1 every Tuesday at 11:00 for 3 weeks (13/10, 20/10, 27/10) // Carbon dayOfWeek: 0=Sunday, 1=Monday, 2=Tuesday $response = $this->actingAs($user)->postJson('/requisicoes/verificar-recorrencia', [ 'weekly_slots' => [ [ 'day_of_week' => 2, // Terça-feira (Carbon: 2) 'start_time' => '11:00', 'end_time' => '13:00', ], ], 'weeks_count' => 3, 'asset_ids' => [$asset1->id], ]); $response->assertOk(); $totalOccurrences = $response->json('total_occurrences'); // Accept whichever count the system calculates $response->assertJsonPath('conflicts_count', 1); $occurrences = $response->json('occurrences'); $conflictIndex = null; foreach ($occurrences as $index => $occ) { if ($occ['has_conflict']) { $conflictIndex = $index; break; } } expect($conflictIndex)->not->toBeNull(); // Week conflict details $conflictWeek = $occurrences[$conflictIndex]; expect($conflictWeek['conflicted_assets'])->toHaveCount(1) ->and($conflictWeek['conflicted_assets'][0]['id'])->toBe($asset1->id) ->and($conflictWeek['conflicted_assets'][0]['alternatives'])->not->toBeEmpty() ->and($conflictWeek['conflicted_assets'][0]['alternatives'][0]['id'])->toBe($asset2->id); }); it('permite submeter uma série recorrente com omissão de dia em conflito', function () { CarbonImmutable::setTestNow('2026-10-10 08:00:00'); $user = User::factory()->create(); $asset = SnipeAsset::factory()->create(); $approver = User::factory()->create(); $approverRole = Role::firstOrCreate(['name' => RoleName::Approver->value]); $approver->roles()->attach($approverRole); // Submitting 2 occurrences out of 3 (omitting week 2) $response = $this->actingAs($user)->post('/requisicoes/recorrente', [ 'purpose' => 'Aulas práticas semanais de multimédia na sala B2', 'usage_location' => 'Laboratório B2', 'terms_accepted' => true, 'occurrences' => [ [ 'starts_at' => '2026-10-13 11:00:00', 'ends_at' => '2026-10-13 13:00:00', 'asset_ids' => [$asset->id], ], // Week 2 (20/10) was deleted/omitted by user due to conflict [ 'starts_at' => '2026-10-27 11:00:00', 'ends_at' => '2026-10-27 13:00:00', 'asset_ids' => [$asset->id], ], ], ]); $response->assertRedirect('/dashboard'); $response->assertSessionHas('success'); $reservations = Reservation::where('requester_id', $user->id)->get(); expect($reservations)->toHaveCount(2); $groupId = $reservations->first()->recurring_group_id; expect($groupId)->not->toBeNull() ->and($reservations[1]->recurring_group_id)->toBe($groupId) ->and($reservations[0]->recurrence_pattern)->toBe('weekly') ->and($reservations[1]->recurrence_pattern)->toBe('weekly'); $this->assertDatabaseHas('audit_logs', [ 'event' => 'reservation.recurring_submitted', 'actor_id' => $user->id, ]); }); it('permite submeter uma série recorrente com substituição de material no dia em conflito', function () { CarbonImmutable::setTestNow('2026-10-10 08:00:00'); $user = User::factory()->create(); $assetOriginal = SnipeAsset::factory()->create(['name' => 'Original']); $assetSubstituto = SnipeAsset::factory()->create(['name' => 'Substituto']); $response = $this->actingAs($user)->post('/requisicoes/recorrente', [ 'purpose' => 'Aulas semanais com troca de equipamento na semana 2', 'usage_location' => 'Auditório', 'terms_accepted' => true, 'occurrences' => [ [ 'starts_at' => '2026-10-13 11:00:00', 'ends_at' => '2026-10-13 13:00:00', 'asset_ids' => [$assetOriginal->id], ], [ 'starts_at' => '2026-10-20 11:00:00', 'ends_at' => '2026-10-20 13:00:00', 'asset_ids' => [$assetSubstituto->id], // User chose replacement! ], ], ]); $response->assertRedirect('/dashboard'); $reservations = Reservation::where('requester_id', $user->id)->orderBy('starts_at')->get(); expect($reservations)->toHaveCount(2); // Week 1 has assetOriginal expect($reservations[0]->items->first()->snipe_asset_id)->toBe($assetOriginal->id); // Week 2 has assetSubstituto expect($reservations[1]->items->first()->snipe_asset_id)->toBe($assetSubstituto->id); });