fix(capacity): stabilize PTO flows and calendar consistency
Make PTO creation immediately approved, add PTO deletion, and ensure cache invalidation updates individual/team/revenue capacity consistently. Harden holiday duplicate handling (422), support PTO-day availability overrides without disabling edits, and align tests plus OpenSpec artifacts with the new behavior.
This commit is contained in:
@@ -189,6 +189,34 @@ test('4.1.17 POST /api/holidays creates holiday', function () {
|
||||
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();
|
||||
@@ -204,8 +232,96 @@ test('4.1.18 POST /api/ptos creates PTO request', function () {
|
||||
]);
|
||||
|
||||
$response->assertStatus(201);
|
||||
$response->assertJsonPath('data.status', 'pending');
|
||||
assertDatabaseHas('ptos', ['team_member_id' => $member->id, 'status' => 'pending']);
|
||||
$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
|
||||
|
||||
Reference in New Issue
Block a user