32 - Add ProjectionPreviewOutput DTO with fromResult mapping
This commit is contained in:
parent
af467de439
commit
5c5369328a
2 changed files with 165 additions and 0 deletions
41
src/ApiResource/ProjectionPreviewOutput.php
Normal file
41
src/ApiResource/ProjectionPreviewOutput.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace App\ApiResource;
|
||||
|
||||
use App\Service\Allocation\Result;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
|
||||
class ProjectionPreviewOutput
|
||||
{
|
||||
public array $allocations;
|
||||
|
||||
public int $totalAllocated;
|
||||
|
||||
public int $unallocated;
|
||||
|
||||
public function __construct(Result $result)
|
||||
{
|
||||
$this->allocations = $this->setAllocationsFromResult($result);
|
||||
$this->totalAllocated = $result->getTotalAllocated();
|
||||
$this->unallocated = $result->getUnallocated();
|
||||
}
|
||||
|
||||
public static function fromResult(Result $result): static
|
||||
{
|
||||
return new self($result);
|
||||
}
|
||||
|
||||
private function setAllocationsFromResult(Result $result): array
|
||||
{
|
||||
return new ArrayCollection($result->getAllocations())
|
||||
->map(static function (array $allocation) {
|
||||
return [
|
||||
'bucket_id' => (string) $allocation['bucket']->getId(),
|
||||
'bucket_name' => $allocation['bucket']->getName(),
|
||||
'bucket_type' => $allocation['bucket']->getType()->value,
|
||||
'allocated_amount' => $allocation['amount'],
|
||||
'remaining_capacity' => null,
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
}
|
||||
124
tests/ApiResource/ProjectionPreviewOutputTest.php
Normal file
124
tests/ApiResource/ProjectionPreviewOutputTest.php
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<?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');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue