diff --git a/tests/Services/ProjectSystem/ProjectBuildHelperTest.php b/tests/Services/ProjectSystem/ProjectBuildHelperTest.php index fb31b51e..cf36b030 100644 --- a/tests/Services/ProjectSystem/ProjectBuildHelperTest.php +++ b/tests/Services/ProjectSystem/ProjectBuildHelperTest.php @@ -26,7 +26,10 @@ use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Entity\PriceInformations\Orderdetail; +use App\Entity\PriceInformations\Pricedetail; use App\Services\ProjectSystem\ProjectBuildHelper; +use Brick\Math\BigDecimal; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; final class ProjectBuildHelperTest extends WebTestCase @@ -130,6 +133,149 @@ final class ProjectBuildHelperTest extends WebTestCase $project->addBomEntry($bom_entry1); $this->assertSame('∞', $this->service->getMaximumBuildableCountAsString($project)); + } + // --- Build price tests --- + + private function makePartWithPrice(float $pricePerPiece, float $minQty = 1.0): Part + { + $part = new Part(); + $orderdetail = new Orderdetail(); + $pricedetail = (new Pricedetail()) + ->setMinDiscountQuantity($minQty) + ->setPrice(BigDecimal::of((string) $pricePerPiece)); + $orderdetail->addPricedetail($pricedetail); + $part->addOrderdetail($orderdetail); + return $part; + } + + public function testCalculateTotalBuildPriceEmptyProject(): void + { + $project = new Project(); + $this->assertNull($this->service->calculateTotalBuildPrice($project)); + } + + public function testCalculateTotalBuildPriceNoPricingData(): void + { + $project = new Project(); + // Part with no orderdetails — no pricing + $entry = (new ProjectBOMEntry())->setPart(new Part())->setQuantity(2); + $project->addBomEntry($entry); + + $this->assertNull($this->service->calculateTotalBuildPrice($project)); + } + + public function testCalculateTotalBuildPriceNonPartEntry(): void + { + $project = new Project(); + $entry = new ProjectBOMEntry(); + $entry->setName('Custom wire'); + $entry->setQuantity(3); + $entry->setPrice(BigDecimal::of('2.00')); + $project->addBomEntry($entry); + + // 3 × 2.00 = 6.00 for 1 build + $result = $this->service->calculateTotalBuildPrice($project, 1); + $this->assertNotNull($result); + $this->assertTrue(BigDecimal::of('6.00')->isEqualTo($result)); + } + + public function testCalculateTotalBuildPriceNonPartEntryMultipleBuilds(): void + { + $project = new Project(); + $entry = new ProjectBOMEntry(); + $entry->setName('Custom wire'); + $entry->setQuantity(3); + $entry->setPrice(BigDecimal::of('2.00')); + $project->addBomEntry($entry); + + // 3 × 2.00 × 5 = 30.00 for 5 builds + $result = $this->service->calculateTotalBuildPrice($project, 5); + $this->assertNotNull($result); + $this->assertTrue(BigDecimal::of('30.00')->isEqualTo($result)); + } + + public function testCalculateTotalBuildPriceWithPart(): void + { + $project = new Project(); + $entry = new ProjectBOMEntry(); + $entry->setPart($this->makePartWithPrice(1.50)); + $entry->setQuantity(4); + $project->addBomEntry($entry); + + // 4 × 1.50 = 6.00 for 1 build + $result = $this->service->calculateTotalBuildPrice($project, 1); + $this->assertNotNull($result); + $this->assertTrue(BigDecimal::of('6.00')->isEqualTo($result)); + } + + public function testCalculateUnitBuildPriceEqualsTotal(): void + { + $project = new Project(); + $entry = new ProjectBOMEntry(); + $entry->setName('Screw'); + $entry->setQuantity(10); + $entry->setPrice(BigDecimal::of('0.10')); + $project->addBomEntry($entry); + + // unit = 10 × 0.10 = 1.00; total for 3 builds = 3.00 + $unit = $this->service->calculateUnitBuildPrice($project, 3); + $total = $this->service->calculateTotalBuildPrice($project, 3); + $this->assertNotNull($unit); + $this->assertNotNull($total); + $this->assertTrue($total->isEqualTo($unit->multipliedBy(3))); + } + + public function testRoundedTotalBuildPriceRoundsUp(): void + { + $project = new Project(); + $entry = new ProjectBOMEntry(); + $entry->setName('Tiny part'); + $entry->setQuantity(1); + $entry->setPrice(BigDecimal::of('0.001')); + $project->addBomEntry($entry); + + // 0.001 rounded up to 2dp = 0.01 + $result = $this->service->roundedTotalBuildPrice($project, 1); + $this->assertNotNull($result); + $this->assertTrue(BigDecimal::of('0.01')->isEqualTo($result)); + } + + public function testCalculateTotalBuildPriceMixedEntries(): void + { + $project = new Project(); + + // Part entry: 2 × 3.00 = 6.00 + $partEntry = new ProjectBOMEntry(); + $partEntry->setPart($this->makePartWithPrice(3.00)); + $partEntry->setQuantity(2); + $project->addBomEntry($partEntry); + + // Non-part entry with price: 5 × 1.00 = 5.00 + $nonPartEntry = new ProjectBOMEntry(); + $nonPartEntry->setName('Solder'); + $nonPartEntry->setQuantity(5); + $nonPartEntry->setPrice(BigDecimal::of('1.00')); + $project->addBomEntry($nonPartEntry); + + // Total = 11.00 + $result = $this->service->calculateTotalBuildPrice($project, 1); + $this->assertNotNull($result); + $this->assertTrue(BigDecimal::of('11.00')->isEqualTo($result)); + } + + public function testCalculateTotalBuildPriceRespectsMinOrderAmount(): void + { + $project = new Project(); + // Part has a minimum order quantity of 10 at 0.50/piece + $entry = new ProjectBOMEntry(); + $entry->setPart($this->makePartWithPrice(0.50, 10.0)); + $entry->setQuantity(1); // BOM only needs 1, but MOQ is 10 + $project->addBomEntry($entry); + + // Price lookup uses qty=10 (MOQ), returns 0.50. Cost = 1 × 0.50 = 0.50 + $result = $this->service->calculateTotalBuildPrice($project, 1); + $this->assertNotNull($result); + $this->assertTrue(BigDecimal::of('0.50')->isEqualTo($result)); } }