buckets/tests/ApiResource/ProjectionPreviewOutputTest.php

124 lines
4.3 KiB
PHP

<?php
namespace App\Tests\ApiResource;
use App\ApiResource\ProjectionPreviewOutput;
use App\Entity\Bucket;
use App\Entity\Scenario;
use App\Enum\BucketAllocationType;
use App\Enum\BucketType;
use App\Service\Allocation\Result;
use PHPUnit\Framework\TestCase;
/**
* Pure-unit test for `ProjectionPreviewOutput::fromResult()` — the named constructor
* that shapes a `Result` (allocation engine output) into the API-facing output DTO.
*
* Contract pinned here:
* - allocations: list of ['bucket_id' => string (uuid, RFC4122), 'bucket_name' =>
* string, 'bucket_type' => string (enum backing value), 'allocated_amount' => int,
* 'remaining_capacity' => null]
* - totalAllocated: int (Result::getTotalAllocated())
* - unallocated: int (Result::getUnallocated())
*
* `remaining_capacity` is ALWAYS null in this contract — a deliberate simplification.
* The anemic Bucket entity has no getEffectiveCapacity(); computing real remaining
* capacity here would couple this DTO to BucketRoomCalculator/FillStage/income, which
* doesn't belong in a pure mapping function. Real capacity is a follow-up, not this story.
*/
final class ProjectionPreviewOutputTest extends TestCase
{
public function testFromResultMapsAllocationsToTheOutputShape(): void
{
$scenario = $this->makeScenario();
$need = $this->makeBucket(
$scenario,
BucketType::NEED,
BucketAllocationType::FIXED_LIMIT,
priority: 1,
allocationValue: 5000,
name: 'Groceries',
);
$overflow = $this->makeBucket(
$scenario,
BucketType::OVERFLOW,
BucketAllocationType::UNLIMITED,
priority: 2,
allocationValue: null,
name: 'Overflow',
);
$result = new Result(
[
['bucket' => $need, 'amount' => 5000],
['bucket' => $overflow, 'amount' => 3000],
],
8000,
0,
);
$output = ProjectionPreviewOutput::fromResult($result);
self::assertCount(2, $output->allocations);
self::assertSame((string) $need->getId(), $output->allocations[0]['bucket_id']);
self::assertSame('Groceries', $output->allocations[0]['bucket_name']);
self::assertSame('need', $output->allocations[0]['bucket_type']);
self::assertSame(5000, $output->allocations[0]['allocated_amount']);
self::assertNull($output->allocations[0]['remaining_capacity']);
self::assertSame((string) $overflow->getId(), $output->allocations[1]['bucket_id']);
self::assertSame('Overflow', $output->allocations[1]['bucket_name']);
self::assertSame('overflow', $output->allocations[1]['bucket_type']);
self::assertSame(3000, $output->allocations[1]['allocated_amount']);
self::assertNull($output->allocations[1]['remaining_capacity']);
self::assertSame(8000, $output->totalAllocated);
self::assertSame(0, $output->unallocated);
}
public function testFromResultWithNoAllocationsProducesEmptyAllocationsList(): void
{
$result = new Result([], 0, 5000);
$output = ProjectionPreviewOutput::fromResult($result);
self::assertSame([], $output->allocations);
self::assertSame(0, $output->totalAllocated);
self::assertSame(5000, $output->unallocated);
}
public function testFromResultReturnsAProjectionPreviewOutputInstance(): void
{
$result = new Result([], 0, 1000);
$output = ProjectionPreviewOutput::fromResult($result);
self::assertInstanceOf(ProjectionPreviewOutput::class, $output);
}
private function makeScenario(): Scenario
{
return (new Scenario())->setName('Household Budget');
}
private function makeBucket(
Scenario $scenario,
BucketType $type,
BucketAllocationType $allocationType,
int $priority,
?int $allocationValue,
string $name,
): Bucket {
return (new Bucket())
->setScenario($scenario)
->setType($type)
->setName($name)
->setPriority($priority)
->setAllocationType($allocationType)
->setAllocationValue($allocationValue)
->setStartingAmount(0)
->setBufferMultiplier('0.00');
}
}