service = self::getContainer()->get(PartLotWithdrawAddHelper::class); $this->fillTestData(); } private function fillTestData(): void { $this->part = new Part(); $this->storageLocation = new StorageLocation(); $this->full_storageLocation = new StorageLocation(); $this->full_storageLocation->setIsFull(true); $this->partLot1 = new TestPartLot(); $this->partLot1->setPart($this->part); $this->partLot1->setAmount(10); $this->partLot2 = new TestPartLot(); $this->partLot2->setPart($this->part); $this->partLot2->setStorageLocation($this->storageLocation); $this->partLot2->setAmount(2); $this->partLot3 = new TestPartLot(); $this->partLot3->setPart($this->part); $this->partLot3->setAmount(0); $this->fullLot = new TestPartLot(); $this->fullLot->setPart($this->part); $this->fullLot->setAmount(45); $this->fullLot->setStorageLocation($this->full_storageLocation); $this->lotWithUnknownInstock = new TestPartLot(); $this->lotWithUnknownInstock->setPart($this->part); $this->lotWithUnknownInstock->setAmount(5); $this->lotWithUnknownInstock->setInstockUnknown(true); $this->lotWithUnknownInstock->setStorageLocation($this->storageLocation); } public function testCanWithdraw(): void { //Normal lots should be withdrawable $this->assertTrue($this->service->canWithdraw($this->partLot1)); $this->assertTrue($this->service->canWithdraw($this->partLot2)); //Empty lots should not be withdrawable $this->assertFalse($this->service->canWithdraw($this->partLot3)); //Full lots should be withdrawable $this->assertTrue($this->service->canWithdraw($this->fullLot)); //Lots with unknown instock should not be withdrawable $this->assertFalse($this->service->canWithdraw($this->lotWithUnknownInstock)); } public function testCanAdd(): void { //Normal lots should be addable $this->assertTrue($this->service->canAdd($this->partLot1)); $this->assertTrue($this->service->canAdd($this->partLot2)); $this->assertTrue($this->service->canAdd($this->partLot3)); //Full lots should not be addable $this->assertFalse($this->service->canAdd($this->fullLot)); //Lots with unknown instock should not be addable $this->assertFalse($this->service->canAdd($this->lotWithUnknownInstock)); } public function testAdd(): void { //Add 5 to lot 1 $this->service->add($this->partLot1, 5, "Test"); $this->assertEqualsWithDelta(15.0, $this->partLot1->getAmount(), PHP_FLOAT_EPSILON); //Add 3.2 to lot 2 $this->service->add($this->partLot2, 3.2, "Test"); $this->assertEqualsWithDelta(5.0, $this->partLot2->getAmount(), PHP_FLOAT_EPSILON); //Add 1.5 to lot 3 $this->service->add($this->partLot3, 1.5, "Test"); $this->assertEqualsWithDelta(2.0, $this->partLot3->getAmount(), PHP_FLOAT_EPSILON); } public function testWithdraw(): void { //Withdraw 5 from lot 1 $this->service->withdraw($this->partLot1, 5, "Test"); $this->assertEqualsWithDelta(5.0, $this->partLot1->getAmount(), PHP_FLOAT_EPSILON); //Withdraw 2.2 from lot 2 $this->service->withdraw($this->partLot2, 2.2, "Test"); $this->assertEqualsWithDelta(0.0, $this->partLot2->getAmount(), PHP_FLOAT_EPSILON); } public function testMove(): void { //Move 5 from lot 1 to lot 2 $this->service->move($this->partLot1, $this->partLot2, 5, "Test"); $this->assertEqualsWithDelta(5.0, $this->partLot1->getAmount(), PHP_FLOAT_EPSILON); $this->assertEqualsWithDelta(7.0, $this->partLot2->getAmount(), PHP_FLOAT_EPSILON); //Move 2.2 from lot 2 to lot 3 $this->service->move($this->partLot2, $this->partLot3, 2.2, "Test"); $this->assertEqualsWithDelta(5.0, $this->partLot2->getAmount(), PHP_FLOAT_EPSILON); $this->assertEqualsWithDelta(2.0, $this->partLot3->getAmount(), PHP_FLOAT_EPSILON); } public function testStocktake(): void { //Stocktake lot 1 to 20 $this->service->stocktake($this->partLot1, 20, "Test"); $this->assertEqualsWithDelta(20.0, $this->partLot1->getAmount(), PHP_FLOAT_EPSILON); $this->assertNotNull($this->partLot1->getLastStocktakeAt()); //Stocktake date should be set //Stocktake lot 2 to 5 $this->partLot2->setInstockUnknown(true); $this->service->stocktake($this->partLot2, 0, "Test"); $this->assertEqualsWithDelta(0.0, $this->partLot2->getAmount(), PHP_FLOAT_EPSILON); $this->assertFalse($this->partLot2->isInstockUnknown()); //Instock unknown should be cleared } // --- withdraw() error paths --- public function testWithdrawZeroAmountThrows(): void { $this->expectException(\InvalidArgumentException::class); $this->service->withdraw($this->partLot1, 0, "Test"); } public function testWithdrawNegativeAmountThrows(): void { $this->expectException(\InvalidArgumentException::class); $this->service->withdraw($this->partLot1, -5, "Test"); } public function testWithdrawMoreThanStockThrows(): void { $this->expectException(\RuntimeException::class); $this->service->withdraw($this->partLot1, 999, "Test"); } public function testWithdrawFromUnknownInstockLotThrows(): void { $this->expectException(\RuntimeException::class); $this->service->withdraw($this->lotWithUnknownInstock, 1, "Test"); } // --- add() error paths --- public function testAddZeroAmountThrows(): void { $this->expectException(\InvalidArgumentException::class); $this->service->add($this->partLot1, 0, "Test"); } public function testAddNegativeAmountThrows(): void { $this->expectException(\InvalidArgumentException::class); $this->service->add($this->partLot1, -3, "Test"); } public function testAddToFullLotThrows(): void { $this->expectException(\RuntimeException::class); $this->service->add($this->fullLot, 1, "Test"); } public function testAddToUnknownInstockLotThrows(): void { $this->expectException(\RuntimeException::class); $this->service->add($this->lotWithUnknownInstock, 1, "Test"); } // --- move() error paths --- public function testMoveZeroAmountThrows(): void { $this->expectException(\InvalidArgumentException::class); $this->service->move($this->partLot1, $this->partLot2, 0, "Test"); } public function testMoveBetweenDifferentPartsThrows(): void { $otherPart = new Part(); $otherLot = new TestPartLot(); $otherLot->setPart($otherPart); $otherLot->setAmount(5); $this->expectException(\RuntimeException::class); $this->service->move($this->partLot1, $otherLot, 5, "Test"); } public function testMoveMoreThanOriginStockThrows(): void { $this->expectException(\RuntimeException::class); $this->service->move($this->partLot1, $this->partLot2, 999, "Test"); } public function testMoveFromUnwithdrawableLotThrows(): void { $this->expectException(\RuntimeException::class); $this->service->move($this->lotWithUnknownInstock, $this->partLot2, 1, "Test"); } public function testMoveToUnavailableLotThrows(): void { $this->expectException(\RuntimeException::class); $this->service->move($this->partLot1, $this->fullLot, 1, "Test"); } // --- stocktake() error paths --- public function testStocktakeNegativeAmountThrows(): void { $this->expectException(\InvalidArgumentException::class); $this->service->stocktake($this->partLot1, -1, "Test"); } // --- integer-rounding (useFloatAmount() = false, no unit set) --- public function testWithdrawRoundsAmountForIntegerPart(): void { // No unit → useFloatAmount() = false → fractional amounts are rounded $this->assertFalse($this->part->useFloatAmount()); $this->service->withdraw($this->partLot1, 1.7, "Test"); // rounds to 2 $this->assertEqualsWithDelta(8.0, $this->partLot1->getAmount(), PHP_FLOAT_EPSILON); } public function testAddRoundsAmountForIntegerPart(): void { $this->assertFalse($this->part->useFloatAmount()); $this->service->add($this->partLot3, 1.7, "Test"); // rounds to 2 $this->assertEqualsWithDelta(2.0, $this->partLot3->getAmount(), PHP_FLOAT_EPSILON); } public function testStocktakeRoundsAmountForIntegerPart(): void { $this->assertFalse($this->part->useFloatAmount()); $this->service->stocktake($this->partLot1, 7.6, "Test"); // rounds to 8 $this->assertEqualsWithDelta(8.0, $this->partLot1->getAmount(), PHP_FLOAT_EPSILON); } // --- float amounts are preserved when the unit allows floats --- public function testAddPreservesFloatAmountForFloatUnit(): void { $unit = new MeasurementUnit(); $unit->setIsInteger(false); $floatPart = new Part(); $floatPart->setPartUnit($unit); $this->assertTrue($floatPart->useFloatAmount()); $lot = new TestPartLot(); $lot->setPart($floatPart); $lot->setAmount(1.0); $this->service->add($lot, 1.3, "Test"); $this->assertEqualsWithDelta(2.3, $lot->getAmount(), PHP_FLOAT_EPSILON); } public function testWithdrawPreservesFloatAmountForFloatUnit(): void { $unit = new MeasurementUnit(); $unit->setIsInteger(false); $floatPart = new Part(); $floatPart->setPartUnit($unit); $lot = new TestPartLot(); $lot->setPart($floatPart); $lot->setAmount(5.0); $this->service->withdraw($lot, 1.3, "Test"); $this->assertEqualsWithDelta(3.7, $lot->getAmount(), PHP_FLOAT_EPSILON); } // --- delete_lot_if_empty --- /** * Creates a PartLot that looks like a managed, persisted entity to Doctrine: * - has a non-null ID (required by AbstractLogEntry when creating stock-change log entries) * - is registered in the UnitOfWork as managed (required so EntityManager::remove() accepts it) */ private function makeManagedLot(float $amount, int $fakeId = 42): PartLot { $lot = new PartLot(); $lot->setPart($this->part); $lot->setAmount($amount); $ref = new \ReflectionProperty($lot, 'id'); $ref->setValue($lot, $fakeId); $em = self::getContainer()->get('doctrine.orm.entity_manager'); $em->getUnitOfWork()->registerManaged($lot, ['id' => $fakeId], []); return $lot; } public function testWithdrawDeletesLotWhenEmptyAndFlagSet(): void { $lot = $this->makeManagedLot(10); $this->service->withdraw($lot, 10, "Test", null, true); $this->assertEqualsWithDelta(0.0, $lot->getAmount(), PHP_FLOAT_EPSILON); $em = self::getContainer()->get('doctrine.orm.entity_manager'); $scheduled = $em->getUnitOfWork()->getScheduledEntityDeletions(); $this->assertContains($lot, $scheduled); } public function testWithdrawDoesNotDeleteLotWhenNotEmptyAndFlagSet(): void { $lot = $this->makeManagedLot(10); $this->service->withdraw($lot, 5, "Test", null, true); $this->assertEqualsWithDelta(5.0, $lot->getAmount(), PHP_FLOAT_EPSILON); $em = self::getContainer()->get('doctrine.orm.entity_manager'); $scheduled = $em->getUnitOfWork()->getScheduledEntityDeletions(); $this->assertNotContains($lot, $scheduled); } public function testMoveDeletesOriginLotWhenEmptyAndFlagSet(): void { $origin = $this->makeManagedLot(10, 43); $target = $this->makeManagedLot(0, 44); $this->service->move($origin, $target, 10, "Test", null, true); $this->assertEqualsWithDelta(0.0, $origin->getAmount(), PHP_FLOAT_EPSILON); $em = self::getContainer()->get('doctrine.orm.entity_manager'); $scheduled = $em->getUnitOfWork()->getScheduledEntityDeletions(); $this->assertContains($origin, $scheduled); } }