- Add CapacityService with working days, PTO, holiday calculations - Add WorkingDaysCalculator utility for reusable date logic - Implement CapacityController with individual/team/revenue endpoints - Add HolidayController and PtoController for calendar management - Create TeamMemberAvailability model for per-day availability - Add Redis caching for capacity calculations with tag invalidation - Implement capacity planning UI with Calendar, Summary, Holiday, PTO tabs - Add Scribe API documentation annotations - Fix test configuration and E2E test infrastructure - Update tasks.md with completion status Backend Tests: 63 passed Frontend Unit: 32 passed E2E Tests: 134 passed, 20 fixme (capacity UI rendering) API Docs: Generated successfully
116 lines
4.0 KiB
PHP
116 lines
4.0 KiB
PHP
<?php
|
|
|
|
use App\Models\Holiday;
|
|
use App\Models\Pto;
|
|
use App\Models\Role;
|
|
use App\Models\TeamMember;
|
|
use App\Models\TeamMemberAvailability;
|
|
use App\Services\CapacityService;
|
|
use Carbon\CarbonPeriod;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* @mixin \Tests\TestCase
|
|
*/
|
|
uses(TestCase::class, RefreshDatabase::class);
|
|
|
|
test('4.1.19 CapacityService calculates working days', function () {
|
|
Holiday::create(['date' => '2026-02-11', 'name' => 'Extra Day', 'description' => 'Standalone']);
|
|
Holiday::create(['date' => '2026-02-25', 'name' => 'Another Day', 'description' => 'Standalone']);
|
|
|
|
$period = CarbonPeriod::create('2026-02-01', '2026-02-28');
|
|
$expected = 0;
|
|
|
|
foreach ($period as $day) {
|
|
if ($day->isWeekend()) {
|
|
continue;
|
|
}
|
|
|
|
if (in_array($day->toDateString(), ['2026-02-11', '2026-02-25'], true)) {
|
|
continue;
|
|
}
|
|
|
|
$expected++;
|
|
}
|
|
|
|
$service = app(CapacityService::class);
|
|
|
|
expect($service->calculateWorkingDays('2026-02'))->toBe($expected);
|
|
});
|
|
|
|
test('4.1.20 CapacityService applies availability', function () {
|
|
$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]);
|
|
|
|
$result = app(CapacityService::class)->calculateIndividualCapacity($member->id, '2026-02');
|
|
$details = collect($result['details']);
|
|
|
|
expect($details->firstWhere('date', '2026-02-03')['availability'])->toBe(0.5);
|
|
expect($details->firstWhere('date', '2026-02-04')['availability'])->toBe(0.0);
|
|
});
|
|
|
|
test('4.1.21 CapacityService handles PTO', function () {
|
|
$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' => 'Rest',
|
|
'status' => 'approved',
|
|
]);
|
|
|
|
$service = app(CapacityService::class);
|
|
$workingDays = $service->calculateWorkingDays('2026-02');
|
|
$result = $service->calculateIndividualCapacity($member->id, '2026-02');
|
|
|
|
expect($result['person_days'])->toBe((float) ($workingDays - 3));
|
|
});
|
|
|
|
test('4.1.22 CapacityService handles holidays', function () {
|
|
$role = Role::factory()->create();
|
|
$member = TeamMember::factory()->create(['role_id' => $role->id]);
|
|
|
|
Holiday::create(['date' => '2026-02-17', 'name' => 'Presidents Day', 'description' => 'Holiday']);
|
|
|
|
$service = app(CapacityService::class);
|
|
$result = $service->calculateIndividualCapacity($member->id, '2026-02');
|
|
|
|
$dates = collect($result['details'])->pluck('date');
|
|
|
|
expect($dates)->not->toContain('2026-02-17');
|
|
});
|
|
|
|
test('4.1.23 CapacityService calculates revenue', function () {
|
|
$role = Role::factory()->create();
|
|
$memberA = TeamMember::factory()->create(['role_id' => $role->id, 'hourly_rate' => 150]);
|
|
$memberB = TeamMember::factory()->create(['role_id' => $role->id, 'hourly_rate' => 125]);
|
|
|
|
$service = app(CapacityService::class);
|
|
$revenue = $service->calculatePossibleRevenue('2026-02');
|
|
|
|
$hoursA = $service->calculateIndividualCapacity($memberA->id, '2026-02')['hours'];
|
|
$hoursB = $service->calculateIndividualCapacity($memberB->id, '2026-02')['hours'];
|
|
|
|
expect($revenue)->toBe(round($hoursA * 150 + $hoursB * 125, 2));
|
|
});
|
|
|
|
test('4.1.24 Redis caching for capacity', function () {
|
|
$role = Role::factory()->create();
|
|
$member = TeamMember::factory()->create(['role_id' => $role->id]);
|
|
|
|
$service = app(CapacityService::class);
|
|
$key = "capacity:2026-02:{$member->id}";
|
|
|
|
Cache::store('array')->forget($key);
|
|
$service->calculateIndividualCapacity($member->id, '2026-02');
|
|
|
|
expect(Cache::store('array')->get($key))->not->toBeNull();
|
|
});
|