diff --git a/src/Entity/Bucket.php b/src/Entity/Bucket.php index 4ce047f..ec09345 100644 --- a/src/Entity/Bucket.php +++ b/src/Entity/Bucket.php @@ -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() diff --git a/src/Validator/BufferOnlyForFixedLimit.php b/src/Validator/BufferOnlyForFixedLimit.php new file mode 100644 index 0000000..a4a5db4 --- /dev/null +++ b/src/Validator/BufferOnlyForFixedLimit.php @@ -0,0 +1,18 @@ +getAllocationType()) { + return; // buffer allowed + } + + if (0.0 !== (float) $value->getBufferMultiplier()) { + $this->context->buildViolation($constraint->message) + ->atPath('bufferMultiplier') + ->setCode(BufferOnlyForFixedLimit::BUFFER_NOT_ALLOWED_ERROR) + ->addViolation(); + } + } +} diff --git a/tests/Validator/BufferOnlyForFixedLimitTest.php b/tests/Validator/BufferOnlyForFixedLimitTest.php new file mode 100644 index 0000000..ee75c29 --- /dev/null +++ b/tests/Validator/BufferOnlyForFixedLimitTest.php @@ -0,0 +1,116 @@ +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; + } +} diff --git a/tests/Validator/BufferOnlyForFixedLimitValidatorTest.php b/tests/Validator/BufferOnlyForFixedLimitValidatorTest.php new file mode 100644 index 0000000..0519b96 --- /dev/null +++ b/tests/Validator/BufferOnlyForFixedLimitValidatorTest.php @@ -0,0 +1,102 @@ + + */ +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; + } +}