em = self::getContainer()->get(EntityManagerInterface::class); } public function testItPersistsABucketLinkedToItsScenario(): void { $scenario = new Scenario(); $scenario->setName('Household Budget'); $this->em->persist($scenario); $this->em->flush(); $scenarioId = $scenario->getId(); $bucket = new Bucket(); $bucket->setScenario($scenario); $bucket->setType(BucketType::NEED); $bucket->setName('Rent'); $bucket->setPriority(3); $bucket->setSortOrder(2); $bucket->setAllocationType(BucketAllocationType::FIXED_LIMIT); $bucket->setAllocationValue(150000); $bucket->setStartingAmount(5000); $bucket->setBufferMultiplier('1.50'); $this->em->persist($bucket); $this->em->flush(); $bucketId = $bucket->getId(); $this->em->clear(); $reloaded = $this->em->find(Bucket::class, $bucketId); self::assertNotNull($reloaded, 'Bucket must be retrievable from the database after an identity-map clear.'); self::assertInstanceOf(Scenario::class, $reloaded->getScenario()); self::assertSame( (string) $scenarioId, (string) $reloaded->getScenario()->getId(), 'Reloaded bucket must be linked to the same scenario it was persisted with.', ); self::assertInstanceOf( BucketType::class, $reloaded->getType(), 'type must hydrate as a BucketType enum instance, not a raw string.', ); self::assertSame(BucketType::NEED, $reloaded->getType()); self::assertSame('Rent', $reloaded->getName()); self::assertSame(3, $reloaded->getPriority()); self::assertSame(2, $reloaded->getSortOrder()); self::assertInstanceOf( BucketAllocationType::class, $reloaded->getAllocationType(), 'allocation_type must hydrate as a BucketAllocationType enum instance, not a raw string.', ); self::assertSame(BucketAllocationType::FIXED_LIMIT, $reloaded->getAllocationType()); self::assertSame( 150000, $reloaded->getAllocationValue(), 'allocation_value must round-trip as an int (cents), not a string or float.', ); self::assertSame(5000, $reloaded->getStartingAmount()); self::assertSame( '1.50', $reloaded->getBufferMultiplier(), 'buffer_multiplier is a Doctrine decimal column — it hydrates as a numeric string, not a float (precision-safe).', ); } public function testAllocationTypeDefaultsToFixedLimitWhenNotSet(): void { $scenario = new Scenario(); $scenario->setName('Household Budget'); $this->em->persist($scenario); $this->em->flush(); $bucket = new Bucket(); $bucket->setScenario($scenario); $bucket->setType(BucketType::NEED); $bucket->setName('Rent'); $bucket->setPriority(1); $bucket->setSortOrder(0); // setAllocationType() deliberately NOT called — pins the default. $bucket->setAllocationValue(150000); $bucket->setStartingAmount(5000); $bucket->setBufferMultiplier('0.00'); $this->em->persist($bucket); $this->em->flush(); $bucketId = $bucket->getId(); $this->em->clear(); $reloaded = $this->em->find(Bucket::class, $bucketId); self::assertNotNull($reloaded, 'Bucket must be retrievable from the database after an identity-map clear.'); self::assertSame( BucketAllocationType::FIXED_LIMIT, $reloaded->getAllocationType(), 'allocation_type must default to FIXED_LIMIT at the entity (PHP) level when not explicitly set. ' .'This does not exercise the DB column default, since Doctrine always includes allocation_type ' .'in its INSERT once the property holds a value — see testAllocationTypeDbColumnHasFixedLimitDefaultWhenOmittedFromInsert ' .'for the DB-level proof.', ); } public function testAllocationTypeDbColumnHasFixedLimitDefaultWhenOmittedFromInsert(): void { $scenario = new Scenario(); $scenario->setName('Household Budget'); $this->em->persist($scenario); $this->em->flush(); $scenarioId = $scenario->getId(); $bucketId = new UuidV7(); $connection = $this->em->getConnection(); // Raw INSERT deliberately omits allocation_type — the DB column default must supply it. // All other NOT NULL columns without a DB default (id, name, type, priority, scenario_id) // are provided explicitly. $connection->executeStatement( 'INSERT INTO bucket (id, name, type, priority, scenario_id) VALUES (:id, :name, :type, :priority, :scenarioId)', [ 'id' => $bucketId->toRfc4122(), 'name' => 'Rent', 'type' => BucketType::NEED->value, 'priority' => 1, 'scenarioId' => $scenarioId->toRfc4122(), ], ); $allocationType = $connection->executeQuery( 'SELECT allocation_type FROM bucket WHERE id = :id', ['id' => $bucketId->toRfc4122()], )->fetchOne(); self::assertSame( 'fixed_limit', $allocationType, 'When allocation_type is omitted from a raw INSERT, the DB column default must supply ' .'\'fixed_limit\'. If this fails, the migration setting the column DEFAULT has regressed ' .'(the insert would fail outright with a not-null violation instead of defaulting).', ); } public function testScenarioPriorityUniquenessIsEnforced(): void { $scenario = new Scenario(); $scenario->setName('Household Budget'); $this->em->persist($scenario); $this->em->flush(); $first = new Bucket(); $first->setScenario($scenario); $first->setType(BucketType::NEED); $first->setName('Rent'); $first->setPriority(3); $first->setSortOrder(1); $first->setAllocationType(BucketAllocationType::FIXED_LIMIT); $first->setAllocationValue(150000); $first->setStartingAmount(5000); $first->setBufferMultiplier('0.00'); $this->em->persist($first); $this->em->flush(); $second = new Bucket(); $second->setScenario($scenario); $second->setType(BucketType::NEED); $second->setName('Utilities'); $second->setPriority(3); $second->setSortOrder(2); $second->setAllocationType(BucketAllocationType::FIXED_LIMIT); $second->setAllocationValue(50000); $second->setStartingAmount(3000); $second->setBufferMultiplier('0.00'); $this->em->persist($second); $this->expectException(UniqueConstraintViolationException::class); $this->em->flush(); } }