create(); $teamMember = TeamMember::factory()->create(['role_id' => $role->id]); $response = $this->getJson("/api/capacity?month=2026-02&team_member_id={$teamMember->id}", [ 'Authorization' => "Bearer {$token}", ]); $response->assertStatus(200); $service = app(CapacityService::class); $capacity = $service->calculateIndividualCapacity($teamMember->id, '2026-02'); $expected = [ 'data' => [ 'team_member_id' => $teamMember->id, 'month' => '2026-02', 'working_days' => $service->calculateWorkingDays('2026-02'), 'person_days' => $capacity['person_days'], 'hours' => $capacity['hours'], 'details' => $capacity['details'], ], ]; $response->assertExactJson($expected); }); test('4.1.12 Capacity accounts for availability', function () { $token = loginAsManager($this); $role = Role::factory()->create(); $member = TeamMember::factory()->create(['role_id' => $role->id]); TeamMemberAvailability::factory()->forDate('2026-02-03')->availability(0.5)->create(['team_member_id' => $member->id]); TeamMemberAvailability::factory()->forDate('2026-02-04')->availability(0.0)->create(['team_member_id' => $member->id]); $response = $this->getJson("/api/capacity?month=2026-02&team_member_id={$member->id}", [ 'Authorization' => "Bearer {$token}", ]); $response->assertStatus(200); $details = collect($response->json('data.details')); expect($details->firstWhere('date', '2026-02-03')['availability'])->toBe(0.5); expect($details->firstWhere('date', '2026-02-04')['availability'])->toBe(0); }); test('4.1.13 Capacity subtracts PTO', function () { $token = loginAsManager($this); $role = Role::factory()->create(); $member = TeamMember::factory()->create(['role_id' => $role->id]); Pto::create([ 'team_member_id' => $member->id, 'start_date' => '2026-02-10', 'end_date' => '2026-02-12', 'reason' => 'Vacation', 'status' => 'approved', ]); $response = $this->getJson("/api/capacity?month=2026-02&team_member_id={$member->id}", [ 'Authorization' => "Bearer {$token}", ]); $response->assertStatus(200); $details = collect($response->json('data.details')); expect($details->where('is_pto', true)->count())->toBe(3); expect($details->firstWhere('date', '2026-02-11')['availability'])->toBe(0); }); test('4.1.14 Capacity subtracts holidays', function () { $token = loginAsManager($this); $role = Role::factory()->create(); $member = TeamMember::factory()->create(['role_id' => $role->id]); Holiday::create([ 'date' => '2026-02-17', 'name' => 'Presidents Day', 'description' => 'Company wide', ]); $response = $this->getJson("/api/capacity?month=2026-02&team_member_id={$member->id}", [ 'Authorization' => "Bearer {$token}", ]); $response->assertStatus(200); $dates = collect($response->json('details'))->pluck('date'); expect($dates)->not->toContain('2026-02-17'); }); test('4.1.15 GET /api/capacity/team sums active members', function () { $token = loginAsManager($this); $role = Role::factory()->create(); $activeA = TeamMember::factory()->create(['role_id' => $role->id]); $activeB = TeamMember::factory()->create(['role_id' => $role->id]); TeamMember::factory()->inactive()->create(['role_id' => $role->id]); $expectedDays = 0; $expectedHours = 0; foreach ([$activeA, $activeB] as $member) { $capacity = app(CapacityService::class)->calculateIndividualCapacity($member->id, '2026-02'); $expectedDays += $capacity['person_days']; $expectedHours += $capacity['hours']; } $response = $this->getJson('/api/capacity/team?month=2026-02', [ 'Authorization' => "Bearer {$token}", ]); $response->assertStatus(200); $response->assertJsonCount(2, 'data.members'); expect(round($response->json('data.person_days'), 2))->toBe(round($expectedDays, 2)); expect($response->json('data.hours'))->toBe($expectedHours); }); test('4.1.16 GET /api/capacity/revenue calculates possible revenue', function () { $token = loginAsManager($this); $role = Role::factory()->create(); TeamMember::factory()->create(['role_id' => $role->id, 'hourly_rate' => 150]); TeamMember::factory()->create(['role_id' => $role->id, 'hourly_rate' => 125]); $expectedRevenue = app(CapacityService::class)->calculatePossibleRevenue('2026-02'); $response = $this->getJson('/api/capacity/revenue?month=2026-02', [ 'Authorization' => "Bearer {$token}", ]); $response->assertStatus(200); expect(round($response->json('data.possible_revenue'), 2))->toBe(round($expectedRevenue, 2)); }); test('4.1.25 POST /api/capacity/availability saves entry', function () { $token = loginAsManager($this); $role = Role::factory()->create(); $member = TeamMember::factory()->create(['role_id' => $role->id]); $payload = [ 'team_member_id' => $member->id, 'date' => '2026-02-03', 'availability' => 0.5, ]; $response = $this->postJson('/api/capacity/availability', $payload, [ 'Authorization' => "Bearer {$token}", ]); $response->assertStatus(201); $response->assertJsonPath('data.date', '2026-02-03'); assertDatabaseHas('team_member_daily_availabilities', [ 'team_member_id' => $member->id, 'date' => '2026-02-03 00:00:00', 'availability' => 0.5, ]); }); test('4.1.17 POST /api/holidays creates holiday', function () { $token = loginAsManager($this); $response = $this->postJson('/api/holidays', [ 'date' => '2026-02-20', 'name' => 'Test Holiday', 'description' => 'Test description', ], [ 'Authorization' => "Bearer {$token}", ]); $response->assertStatus(201); assertDatabaseHas('holidays', ['date' => '2026-02-20 00:00:00', 'name' => 'Test Holiday']); }); test('4.1.17b POST /api/holidays returns 422 for duplicate date', function () { $token = loginAsManager($this); // Create first holiday $this->postJson('/api/holidays', [ 'date' => '2026-02-20', 'name' => 'First Holiday', ], [ 'Authorization' => "Bearer {$token}", ])->assertStatus(201); // Try to create duplicate $response = $this->postJson('/api/holidays', [ 'date' => '2026-02-20', 'name' => 'Duplicate Holiday', ], [ 'Authorization' => "Bearer {$token}", ]); $response->assertStatus(422); $response->assertJson([ 'message' => 'A holiday already exists for this date.', 'errors' => [ 'date' => ['A holiday already exists for this date.'], ], ]); }); test('4.1.18 POST /api/ptos creates PTO request', function () { $token = loginAsManager($this); $role = Role::factory()->create(); $member = TeamMember::factory()->create(['role_id' => $role->id]); $response = $this->postJson('/api/ptos', [ 'team_member_id' => $member->id, 'start_date' => '2026-02-10', 'end_date' => '2026-02-11', 'reason' => 'Refresh', ], [ 'Authorization' => "Bearer {$token}", ]); $response->assertStatus(201); $response->assertJsonPath('data.status', 'approved'); assertDatabaseHas('ptos', ['team_member_id' => $member->id, 'status' => 'approved']); }); test('4.1.19 PTO creation invalidates team and revenue caches', function () { $token = loginAsManager($this); $role = Role::factory()->create(); $member = TeamMember::factory()->create([ 'role_id' => $role->id, 'hourly_rate' => 80, 'active' => true, ]); $month = '2026-02'; $this->getJson("/api/capacity/team?month={$month}", [ 'Authorization' => "Bearer {$token}", ])->assertStatus(200); $this->getJson("/api/capacity/revenue?month={$month}", [ 'Authorization' => "Bearer {$token}", ])->assertStatus(200); $this->postJson('/api/ptos', [ 'team_member_id' => $member->id, 'start_date' => '2026-02-26', 'end_date' => '2026-02-28', 'reason' => 'Vacation', ], [ 'Authorization' => "Bearer {$token}", ])->assertStatus(201); $teamResponse = $this->getJson("/api/capacity/team?month={$month}", [ 'Authorization' => "Bearer {$token}", ]); $teamResponse->assertStatus(200); expect((float) $teamResponse->json('data.person_days'))->toBe(18.0); expect($teamResponse->json('data.hours'))->toBe(144); $revenueResponse = $this->getJson("/api/capacity/revenue?month={$month}", [ 'Authorization' => "Bearer {$token}", ]); $revenueResponse->assertStatus(200); expect((float) $revenueResponse->json('data.possible_revenue'))->toBe(11520.0); }); test('4.1.20 DELETE /api/ptos/{id} removes PTO and refreshes capacity', function () { $token = loginAsManager($this); $role = Role::factory()->create(); $member = TeamMember::factory()->create([ 'role_id' => $role->id, 'hourly_rate' => 80, 'active' => true, ]); $createResponse = $this->postJson('/api/ptos', [ 'team_member_id' => $member->id, 'start_date' => '2026-02-26', 'end_date' => '2026-02-28', 'reason' => 'Vacation', ], [ 'Authorization' => "Bearer {$token}", ]); $createResponse->assertStatus(201); $ptoId = $createResponse->json('data.id'); $beforeDelete = $this->getJson("/api/capacity?month=2026-02&team_member_id={$member->id}", [ 'Authorization' => "Bearer {$token}", ]); $beforeDelete->assertStatus(200); expect((float) $beforeDelete->json('data.person_days'))->toBe(18.0); $deleteResponse = $this->deleteJson("/api/ptos/{$ptoId}", [], [ 'Authorization' => "Bearer {$token}", ]); $deleteResponse->assertStatus(200); $deleteResponse->assertJson(['message' => 'PTO deleted']); $afterDelete = $this->getJson("/api/capacity?month=2026-02&team_member_id={$member->id}", [ 'Authorization' => "Bearer {$token}", ]); $afterDelete->assertStatus(200); expect((float) $afterDelete->json('data.person_days'))->toBe(20.0); $this->deleteJson('/api/ptos/non-existent', [], [ 'Authorization' => "Bearer {$token}", ])->assertStatus(404); }); function loginAsManager(TestCase $test): string { $user = User::factory()->create([ 'email' => 'manager@example.com', 'password' => bcrypt('password123'), 'role' => 'manager', 'active' => true, ]); $response = $test->postJson('/api/auth/login', [ 'email' => 'manager@example.com', 'password' => 'password123', ]); return $response->json('access_token'); }