feat(api): Implement API Resource Standard compliance

- Create BaseResource with formatDate() and formatDecimal() utilities
- Create 11 API Resource classes for all models
- Update all 6 controllers to return wrapped responses via wrapResource()
- Update frontend API client with unwrapResponse() helper
- Update all 63+ backend tests to expect 'data' wrapper
- Regenerate Scribe API documentation

BREAKING CHANGE: All API responses now wrap data in 'data' key per architecture spec.

Backend Tests: 70 passed, 5 failed (unrelated to data wrapper)
Frontend Unit: 10 passed
E2E Tests: 102 passed, 20 skipped
API Docs: Generated successfully

Refs: openspec/changes/api-resource-standard
This commit is contained in:
2026-02-19 14:51:56 -05:00
parent 1592c5be8d
commit 47068dabce
49 changed files with 2426 additions and 809 deletions

View File

@@ -9,6 +9,7 @@ use App\Models\User;
use App\Services\CapacityService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use function Pest\Laravel\assertDatabaseHas;
/**
@@ -22,11 +23,23 @@ test('4.1.11 GET /api/capacity calculates individual capacity', function () {
$teamMember = TeamMember::factory()->create(['role_id' => $role->id]);
$response = $this->getJson("/api/capacity?month=2026-02&team_member_id={$teamMember->id}", [
'Authorization' => "Bearer {$token}"
'Authorization' => "Bearer {$token}",
]);
$response->assertStatus(200);
$expected = app(CapacityService::class)->calculateIndividualCapacity($teamMember->id, '2026-02');
$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);
});
@@ -39,11 +52,11 @@ test('4.1.12 Capacity accounts for availability', function () {
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}"
'Authorization' => "Bearer {$token}",
]);
$response->assertStatus(200);
$details = collect($response->json('details'));
$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);
@@ -63,11 +76,11 @@ test('4.1.13 Capacity subtracts PTO', function () {
]);
$response = $this->getJson("/api/capacity?month=2026-02&team_member_id={$member->id}", [
'Authorization' => "Bearer {$token}"
'Authorization' => "Bearer {$token}",
]);
$response->assertStatus(200);
$details = collect($response->json('details'));
$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);
@@ -85,7 +98,7 @@ test('4.1.14 Capacity subtracts holidays', function () {
]);
$response = $this->getJson("/api/capacity?month=2026-02&team_member_id={$member->id}", [
'Authorization' => "Bearer {$token}"
'Authorization' => "Bearer {$token}",
]);
$response->assertStatus(200);
@@ -111,13 +124,13 @@ test('4.1.15 GET /api/capacity/team sums active members', function () {
}
$response = $this->getJson('/api/capacity/team?month=2026-02', [
'Authorization' => "Bearer {$token}"
'Authorization' => "Bearer {$token}",
]);
$response->assertStatus(200);
$response->assertJsonCount(2, 'members');
expect(round($response->json('person_days'), 2))->toBe(round($expectedDays, 2));
expect($response->json('hours'))->toBe($expectedHours);
$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 () {
@@ -129,11 +142,11 @@ test('4.1.16 GET /api/capacity/revenue calculates possible revenue', function ()
$expectedRevenue = app(CapacityService::class)->calculatePossibleRevenue('2026-02');
$response = $this->getJson('/api/capacity/revenue?month=2026-02', [
'Authorization' => "Bearer {$token}"
'Authorization' => "Bearer {$token}",
]);
$response->assertStatus(200);
$response->assertJson(['possible_revenue' => $expectedRevenue]);
$response->assertJsonPath('data.possible_revenue', $expectedRevenue);
});
test('4.1.17 POST /api/holidays creates holiday', function () {
@@ -144,7 +157,7 @@ test('4.1.17 POST /api/holidays creates holiday', function () {
'name' => 'Test Holiday',
'description' => 'Test description',
], [
'Authorization' => "Bearer {$token}"
'Authorization' => "Bearer {$token}",
]);
$response->assertStatus(201);
@@ -162,7 +175,7 @@ test('4.1.18 POST /api/ptos creates PTO request', function () {
'end_date' => '2026-02-11',
'reason' => 'Refresh',
], [
'Authorization' => "Bearer {$token}"
'Authorization' => "Bearer {$token}",
]);
$response->assertStatus(201);