first commit
Some checks failed
tests / ci (push) Has been cancelled

This commit is contained in:
2026-10-27 11:01:11 +00:00
parent 9b8b21c853
commit fb04d71a8d
29 changed files with 9733 additions and 326 deletions

View File

@@ -0,0 +1,173 @@
<?php
use App\Domain\Identity\Enums\RoleName;
use App\Domain\Identity\Models\Role;
use App\Domain\Inventory\Models\SnipeAsset;
use App\Domain\Reservations\Enums\ReservationItemStatus;
use App\Domain\Reservations\Enums\ReservationStatus;
use App\Domain\Reservations\Models\Reservation;
use App\Models\User;
use Carbon\CarbonImmutable;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('analisa uma série recorrente semanal e deteta conflitos com alternativas', function () {
CarbonImmutable::setTestNow('2026-10-10 08:00:00'); // Sábado
$user = User::factory()->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);
});