31 - Add buffer multiplier validation

This commit is contained in:
myrmidex 2026-06-20 00:53:29 +02:00
parent 3163a95f37
commit a2663be938
5 changed files with 274 additions and 0 deletions

View file

@ -5,16 +5,19 @@ namespace App\Entity;
use App\Enum\BucketAllocationType;
use App\Enum\BucketType;
use App\Repository\BucketRepository;
use App\Validator\BufferOnlyForFixedLimit;
use App\Validator\CompatibleAllocationType;
use App\Validator\SingleOverflowPerScenario;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\UuidV7;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: BucketRepository::class)]
#[ORM\Table(name: 'bucket')]
#[ORM\UniqueConstraint(name: 'unique_scenario_priority', columns: ['scenario_id', 'priority'])]
#[CompatibleAllocationType]
#[SingleOverflowPerScenario]
#[BufferOnlyForFixedLimit]
class Bucket
{
#[ORM\Id]
@ -47,6 +50,7 @@ class Bucket
private int $startingAmount = 0;
#[ORM\Column(type: 'decimal', precision: 5, scale: 2, options: ['default' => '0.00'])]
#[Assert\GreaterThanOrEqual(0)]
private string $bufferMultiplier = '0.00';
public function __construct()

View file

@ -0,0 +1,18 @@
<?php
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
#[\Attribute(\Attribute::TARGET_CLASS)]
final class BufferOnlyForFixedLimit extends Constraint
{
public string $message = 'The buffer multiplier can only be set for fixed-limit buckets.';
public const string BUFFER_NOT_ALLOWED_ERROR = 'BUFFER_NOT_ALLOWED_ERROR';
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Validator;
use App\Entity\Bucket;
use App\Enum\BucketAllocationType;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class BufferOnlyForFixedLimitValidator extends ConstraintValidator
{
public function validate(mixed $value, Constraint $constraint): void
{
if (!$constraint instanceof BufferOnlyForFixedLimit) {
throw new UnexpectedTypeException($constraint, BufferOnlyForFixedLimit::class);
}
if (!$value instanceof Bucket) {
return;
}
if (BucketAllocationType::FIXED_LIMIT === $value->getAllocationType()) {
return; // buffer allowed
}
if (0.0 !== (float) $value->getBufferMultiplier()) {
$this->context->buildViolation($constraint->message)
->atPath('bufferMultiplier')
->setCode(BufferOnlyForFixedLimit::BUFFER_NOT_ALLOWED_ERROR)
->addViolation();
}
}
}

View file

@ -0,0 +1,116 @@
<?php
namespace App\Tests\Validator;
use App\Entity\Bucket;
use App\Entity\Scenario;
use App\Enum\BucketAllocationType;
use App\Enum\BucketType;
use App\Validator\BufferOnlyForFixedLimit;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
final class BufferOnlyForFixedLimitTest extends KernelTestCase
{
private ValidatorInterface $validator;
protected function setUp(): void
{
self::bootKernel();
$this->validator = self::getContainer()->get(ValidatorInterface::class);
}
public function testFixedLimitBucketWithNonZeroBufferIsValid(): void
{
$bucket = $this->makeBucket(BucketType::NEED, BucketAllocationType::FIXED_LIMIT, 50000, '1.50');
$violations = $this->validator->validate($bucket);
$matching = $this->violationsAt($violations, 'bufferMultiplier', BufferOnlyForFixedLimit::class);
self::assertCount(
0,
$matching,
'FIXED_LIMIT bucket with a non-zero buffer must not raise BufferOnlyForFixedLimit.',
);
}
public function testUnlimitedBucketWithNonZeroBufferIsInvalid(): void
{
$bucket = $this->makeBucket(BucketType::OVERFLOW, BucketAllocationType::UNLIMITED, null, '2.00');
$violations = $this->validator->validate($bucket);
$matching = $this->violationsAt($violations, 'bufferMultiplier', BufferOnlyForFixedLimit::class);
self::assertNotCount(
0,
$matching,
'UNLIMITED bucket with a non-zero buffer must raise a violation on bufferMultiplier.',
);
}
public function testNegativeBufferIsInvalid(): void
{
$bucket = $this->makeBucket(BucketType::NEED, BucketAllocationType::FIXED_LIMIT, 50000, '-1.00');
$violations = $this->validator->validate($bucket);
$matching = $this->violationsAt($violations, 'bufferMultiplier');
self::assertNotCount(
0,
$matching,
'A negative bufferMultiplier must raise a violation on bufferMultiplier (GreaterThanOrEqual(0)).',
);
}
private function makeBucket(
BucketType $type,
BucketAllocationType $allocationType,
?int $allocationValue,
string $bufferMultiplier,
): Bucket {
$scenario = new Scenario();
$scenario->setName('Household Budget');
$bucket = new Bucket();
$bucket->setScenario($scenario);
$bucket->setType($type);
$bucket->setName('Test Bucket');
$bucket->setPriority(1);
$bucket->setSortOrder(0);
$bucket->setAllocationType($allocationType);
$bucket->setAllocationValue($allocationValue);
$bucket->setStartingAmount(0);
$bucket->setBufferMultiplier($bufferMultiplier);
return $bucket;
}
/**
* @return list<\Symfony\Component\Validator\ConstraintViolationInterface>
*/
private function violationsAt(
ConstraintViolationListInterface $violations,
string $propertyPath,
?string $constraintClass = null,
): array {
$matching = [];
foreach ($violations as $violation) {
if ($violation->getPropertyPath() !== $propertyPath) {
continue;
}
if (null !== $constraintClass && !$violation->getConstraint() instanceof $constraintClass) {
continue;
}
$matching[] = $violation;
}
return $matching;
}
}

View file

@ -0,0 +1,102 @@
<?php
namespace App\Tests\Validator;
use App\Entity\Bucket;
use App\Entity\Scenario;
use App\Enum\BucketAllocationType;
use App\Enum\BucketType;
use App\Validator\BufferOnlyForFixedLimit;
use App\Validator\BufferOnlyForFixedLimitValidator;
use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
/**
* @extends ConstraintValidatorTestCase<BufferOnlyForFixedLimitValidator>
*/
final class BufferOnlyForFixedLimitValidatorTest extends ConstraintValidatorTestCase
{
protected function createValidator(): ConstraintValidatorInterface
{
return new BufferOnlyForFixedLimitValidator();
}
public function testFixedLimitWithNonZeroBufferIsValid(): void
{
$bucket = $this->makeBucket(BucketAllocationType::FIXED_LIMIT, '1.50');
$this->validator->validate($bucket, new BufferOnlyForFixedLimit());
$this->assertNoViolation();
}
public function testFixedLimitWithZeroBufferIsValid(): void
{
$bucket = $this->makeBucket(BucketAllocationType::FIXED_LIMIT, '0.00');
$this->validator->validate($bucket, new BufferOnlyForFixedLimit());
$this->assertNoViolation();
}
public function testPercentageWithNonZeroBufferIsInvalid(): void
{
$bucket = $this->makeBucket(BucketAllocationType::PERCENTAGE, '1.50');
$this->validator->validate($bucket, new BufferOnlyForFixedLimit());
$this->buildViolation('The buffer multiplier can only be set for fixed-limit buckets.')
->atPath('property.path.bufferMultiplier')
->setCode(BufferOnlyForFixedLimit::BUFFER_NOT_ALLOWED_ERROR)
->assertRaised();
}
public function testUnlimitedWithNonZeroBufferIsInvalid(): void
{
$bucket = $this->makeBucket(BucketAllocationType::UNLIMITED, '2.00');
$this->validator->validate($bucket, new BufferOnlyForFixedLimit());
$this->buildViolation('The buffer multiplier can only be set for fixed-limit buckets.')
->atPath('property.path.bufferMultiplier')
->setCode(BufferOnlyForFixedLimit::BUFFER_NOT_ALLOWED_ERROR)
->assertRaised();
}
public function testPercentageWithZeroBufferIsValid(): void
{
$bucket = $this->makeBucket(BucketAllocationType::PERCENTAGE, '0.00');
$this->validator->validate($bucket, new BufferOnlyForFixedLimit());
$this->assertNoViolation();
}
public function testPercentageWithZeroBufferWithoutDecimalsIsValid(): void
{
$bucket = $this->makeBucket(BucketAllocationType::PERCENTAGE, '0');
$this->validator->validate($bucket, new BufferOnlyForFixedLimit());
$this->assertNoViolation();
}
private function makeBucket(BucketAllocationType $allocationType, string $bufferMultiplier): Bucket
{
$scenario = new Scenario();
$scenario->setName('Household Budget');
$bucket = new Bucket();
$bucket->setScenario($scenario);
$bucket->setType(BucketType::NEED);
$bucket->setName('Test Bucket');
$bucket->setPriority(1);
$bucket->setSortOrder(0);
$bucket->setAllocationType($allocationType);
$bucket->setAllocationValue(BucketAllocationType::FIXED_LIMIT === $allocationType ? 50000 : null);
$bucket->setStartingAmount(0);
$bucket->setBufferMultiplier($bufferMultiplier);
return $bucket;
}
}