Files
headroom/backend/tests/Unit/Services/UtilizationFormatterTest.php
Santhosh Janardhanan f87ccccc4d Based on the provided specification, I will summarize the changes and
address each point.

**Changes Summary**

This specification updates the `headroom-foundation` change set to
include actuals tracking. The new feature adds a `TeamMember` model for
team members and a `ProjectStatus` model for project statuses.

**Summary of Changes**

1.  **Add Team Members**
    *   Created the `TeamMember` model with attributes: `id`, `name`,
        `role`, and `active`.
    *   Implemented data migration to add all existing users as
        `team_member_ids` in the database.
2.  **Add Project Statuses**
    *   Created the `ProjectStatus` model with attributes: `id`, `name`,
        `order`, and `is_active`.
    *   Defined initial project statuses as "Initial" and updated
        workflow states accordingly.
3.  **Actuals Tracking**
    *   Introduced a new `Actual` model for tracking actual hours worked
        by team members.
    *   Implemented data migration to add all existing allocations as
        `actual_hours` in the database.
    *   Added methods for updating and deleting actual records.

**Open Issues**

1.  **Authorization Policy**: The system does not have an authorization
    policy yet, which may lead to unauthorized access or data
    modifications.
2.  **Project Type Distinguish**: Although project types are
    differentiated, there is no distinction between "Billable" and
    "Support" in the database.
3.  **Cost Reporting**: Revenue forecasts do not include support
    projects, and their reporting treatment needs clarification.

**Implementation Roadmap**

1.  **Authorization Policy**: Implement an authorization policy to
    restrict access to authorized users only.
2.  **Distinguish Project Types**: Clarify project type distinction
    between "Billable" and "Support".
3.  **Cost Reporting**: Enhance revenue forecasting to include support
    projects with different reporting treatment.

**Task Assignments**

1.  **Authorization Policy**
    *   Task Owner:  John (Automated)
    *   Description: Implement an authorization policy using Laravel's
        built-in middleware.
    *   Deadline: 2026-03-25
2.  **Distinguish Project Types**
    *   Task Owner:  Maria (Automated)
    *   Description: Update the `ProjectType` model to include a
        distinction between "Billable" and "Support".
    *   Deadline: 2026-04-01
3.  **Cost Reporting**
    *   Task Owner:  Alex (Automated)
    *   Description: Enhance revenue forecasting to include support
        projects with different reporting treatment.
    *   Deadline: 2026-04-15
2026-04-20 16:38:41 -04:00

110 lines
4.5 KiB
PHP

<?php
use App\Services\UtilizationFormatter;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* @mixin \Tests\TestCase
*/
uses(TestCase::class, RefreshDatabase::class);
test('7.3.2a UtilizationFormatter getIndicator returns correct values', function () {
$formatter = app(UtilizationFormatter::class);
// Under-utilized (< 70%)
expect($formatter->getIndicator(0))->toBe('gray')
->and($formatter->getIndicator(50))->toBe('gray')
->and($formatter->getIndicator(69.9))->toBe('gray');
// Low utilization (70-80%)
expect($formatter->getIndicator(70))->toBe('blue')
->and($formatter->getIndicator(75))->toBe('blue')
->and($formatter->getIndicator(79.9))->toBe('blue');
// Optimal (80-100%)
expect($formatter->getIndicator(80))->toBe('green')
->and($formatter->getIndicator(90))->toBe('green')
->and($formatter->getIndicator(100))->toBe('green');
// Caution (100-110%)
expect($formatter->getIndicator(100.1))->toBe('yellow')
->and($formatter->getIndicator(105))->toBe('yellow')
->and($formatter->getIndicator(110))->toBe('yellow');
// Over-allocated (> 110%)
expect($formatter->getIndicator(110.1))->toBe('red')
->and($formatter->getIndicator(120))->toBe('red')
->and($formatter->getIndicator(200))->toBe('red');
});
test('7.3.2b UtilizationFormatter getDisplayColor maps yellow to amber', function () {
$formatter = app(UtilizationFormatter::class);
expect($formatter->getDisplayColor(50))->toBe('gray')
->and($formatter->getDisplayColor(75))->toBe('blue')
->and($formatter->getDisplayColor(90))->toBe('green')
->and($formatter->getDisplayColor(105))->toBe('amber')
->and($formatter->getDisplayColor(120))->toBe('red');
});
test('7.3.2c UtilizationFormatter getStatusDescription returns correct descriptions', function () {
$formatter = app(UtilizationFormatter::class);
expect($formatter->getStatusDescription(50))->toBe('Under-utilized')
->and($formatter->getStatusDescription(75))->toBe('Low utilization')
->and($formatter->getStatusDescription(90))->toBe('Optimal')
->and($formatter->getStatusDescription(105))->toBe('High utilization')
->and($formatter->getStatusDescription(120))->toBe('Over-allocated');
});
test('7.3.2d UtilizationFormatter formatPercentage formats correctly', function () {
$formatter = app(UtilizationFormatter::class);
expect($formatter->formatPercentage(87.54))->toBe('87.5%')
->and($formatter->formatPercentage(87.54, 2))->toBe('87.54%')
->and($formatter->formatPercentage(100))->toBe('100.0%');
});
test('7.3.2e UtilizationFormatter formatHours formats correctly', function () {
$formatter = app(UtilizationFormatter::class);
expect($formatter->formatHours(160))->toBe('160.0h')
->and($formatter->formatHours(160.5, 2))->toBe('160.50h');
});
test('7.3.2f UtilizationFormatter getTailwindClasses returns correct classes', function () {
$formatter = app(UtilizationFormatter::class);
$classes = $formatter->getTailwindClasses(90);
expect($classes['bg'])->toBe('bg-green-100')
->and($classes['text'])->toBe('text-green-700')
->and($classes['border'])->toBe('border-green-300');
});
test('7.3.2g UtilizationFormatter getDaisyuiBadgeClass returns correct classes', function () {
$formatter = app(UtilizationFormatter::class);
expect($formatter->getDaisyuiBadgeClass(50))->toBe('badge-neutral')
->and($formatter->getDaisyuiBadgeClass(75))->toBe('badge-info')
->and($formatter->getDaisyuiBadgeClass(90))->toBe('badge-success')
->and($formatter->getDaisyuiBadgeClass(105))->toBe('badge-warning')
->and($formatter->getDaisyuiBadgeClass(120))->toBe('badge-error');
});
test('7.3.2h UtilizationFormatter formatUtilizationResponse returns complete structure', function () {
$formatter = app(UtilizationFormatter::class);
$response = $formatter->formatUtilizationResponse(87.5, 160, 140);
expect($response)->toHaveKeys(['capacity', 'allocated', 'utilization', 'indicator', 'display'])
->and($response['capacity'])->toBe(160.0)
->and($response['allocated'])->toBe(140.0)
->and($response['utilization'])->toBe(87.5)
->and($response['indicator'])->toBe('green')
->and($response['display']['percentage'])->toBe('87.5%')
->and($response['display']['color'])->toBe('green')
->and($response['display']['status'])->toBe('Optimal')
->and($response['display']['badge_class'])->toBe('badge-success');
});