feat(allocation): implement resource allocation feature

- Add AllocationController with CRUD + bulk endpoints
- Add AllocationValidationService for capacity/estimate validation
- Add AllocationMatrixService for optimized matrix queries
- Add AllocationPolicy for authorization
- Add AllocationResource for API responses
- Add frontend allocationService and matrix UI
- Add E2E tests for allocation matrix (20 tests)
- Add unit tests for validation service and policies
- Fix month format conversion (YYYY-MM to YYYY-MM-01)
This commit is contained in:
2026-02-25 16:28:47 -05:00
parent fedfc21425
commit 3324c4f156
35 changed files with 3337 additions and 67 deletions

View File

@@ -324,6 +324,103 @@ test('4.1.20 DELETE /api/ptos/{id} removes PTO and refreshes capacity', function
])->assertStatus(404);
});
test('1.1 POST /api/capacity/availability/batch saves multiple updates and returns 200 with saved count', function () {
$token = loginAsManager($this);
$role = Role::factory()->create();
$member1 = TeamMember::factory()->create(['role_id' => $role->id]);
$member2 = TeamMember::factory()->create(['role_id' => $role->id]);
$payload = [
'month' => '2026-02',
'updates' => [
['team_member_id' => $member1->id, 'date' => '2026-02-03', 'availability' => 0.5],
['team_member_id' => $member1->id, 'date' => '2026-02-04', 'availability' => 0],
['team_member_id' => $member2->id, 'date' => '2026-02-05', 'availability' => 1],
],
];
$response = $this->postJson('/api/capacity/availability/batch', $payload, [
'Authorization' => "Bearer {$token}",
]);
$response->assertStatus(200);
$response->assertJsonPath('data.saved', 3);
$response->assertJsonPath('data.month', '2026-02');
assertDatabaseHas('team_member_daily_availabilities', [
'team_member_id' => $member1->id,
'date' => '2026-02-03 00:00:00',
'availability' => 0.5,
]);
assertDatabaseHas('team_member_daily_availabilities', [
'team_member_id' => $member1->id,
'date' => '2026-02-04 00:00:00',
'availability' => 0,
]);
assertDatabaseHas('team_member_daily_availabilities', [
'team_member_id' => $member2->id,
'date' => '2026-02-05 00:00:00',
'availability' => 1,
]);
});
test('1.2 batch endpoint returns 422 when availability value is not in [0, 0.5, 1]', function () {
$token = loginAsManager($this);
$role = Role::factory()->create();
$member = TeamMember::factory()->create(['role_id' => $role->id]);
$payload = [
'month' => '2026-02',
'updates' => [
['team_member_id' => $member->id, 'date' => '2026-02-03', 'availability' => 0.75],
],
];
$response = $this->postJson('/api/capacity/availability/batch', $payload, [
'Authorization' => "Bearer {$token}",
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors(['updates.0.availability']);
});
test('1.3 batch endpoint returns 422 when team_member_id does not exist', function () {
$token = loginAsManager($this);
$payload = [
'month' => '2026-02',
'updates' => [
['team_member_id' => 'non-existent-uuid', 'date' => '2026-02-03', 'availability' => 1],
],
];
$response = $this->postJson('/api/capacity/availability/batch', $payload, [
'Authorization' => "Bearer {$token}",
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors(['updates.0.team_member_id']);
});
test('1.4 empty updates array returns 200 with saved count 0', function () {
$token = loginAsManager($this);
$payload = [
'month' => '2026-02',
'updates' => [],
];
$response = $this->postJson('/api/capacity/availability/batch', $payload, [
'Authorization' => "Bearer {$token}",
]);
$response->assertStatus(200);
$response->assertJsonPath('data.saved', 0);
$response->assertJsonPath('data.month', '2026-02');
});
function loginAsManager(TestCase $test): string
{
$user = User::factory()->create([