From fa1e5549f0c764107e59b452c69fa0758e039a01 Mon Sep 17 00:00:00 2001 From: MayNiklas Date: Wed, 15 Apr 2026 13:11:06 +0200 Subject: [PATCH] Deduplicate BOM entry price logic into ProjectBuildHelper The private getBomEntryUnitPrice() in ProjectBomEntriesDataTable was identical to the one in ProjectBuildHelper. Replaced it with a new public getEntryUnitPrice() on ProjectBuildHelper (returns BigDecimal, never null) and delegate to it from the DataTable. This eliminates the duplicate code and brings the DataTable lines under the existing ProjectBuildHelper test coverage. Added three tests for getEntryUnitPrice() covering the no-pricing, non-part, and part cases. --- src/DataTables/ProjectBomEntriesDataTable.php | 24 +++----------- .../ProjectSystem/ProjectBuildHelper.php | 10 ++++++ .../ProjectSystem/ProjectBuildHelperTest.php | 31 +++++++++++++++++++ 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index 6ee726e8..2d5c4ebc 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -36,8 +36,7 @@ use App\Services\ElementTypeNameGenerator; use App\Services\EntityURLGenerator; use App\Services\Formatters\AmountFormatter; use App\Services\Formatters\MoneyFormatter; -use App\Services\Parts\PricedetailHelper; -use Brick\Math\BigDecimal; +use App\Services\ProjectSystem\ProjectBuildHelper; use Brick\Math\RoundingMode; use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\Query; @@ -55,7 +54,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface protected TranslatorInterface $translator, protected AmountFormatter $amountFormatter, protected PartDataTableHelper $partDataTableHelper, - protected PricedetailHelper $pricedetailHelper, + protected ProjectBuildHelper $projectBuildHelper, protected MoneyFormatter $moneyFormatter, ) { } @@ -212,7 +211,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface 'label' => 'project.bom.price', 'visible' => false, 'render' => function ($value, ProjectBOMEntry $context) { - $price = $this->getBomEntryUnitPrice($context); + $price = $this->projectBuildHelper->getEntryUnitPrice($context); return $this->moneyFormatter->format($price->toScale(2, RoundingMode::UP)->toFloat(), null, 2, true); }, ]) @@ -220,7 +219,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface 'label' => 'project.bom.ext_price', 'visible' => false, 'render' => function ($value, ProjectBOMEntry $context) { - $price = $this->getBomEntryUnitPrice($context); + $price = $this->projectBuildHelper->getEntryUnitPrice($context); return $this->moneyFormatter->format( $price->multipliedBy($context->getQuantity())->toScale(2, RoundingMode::UP)->toFloat(), null, @@ -258,21 +257,6 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface ]); } - private function getBomEntryUnitPrice(ProjectBOMEntry $entry): BigDecimal - { - if ($entry->getPart() instanceof Part) { - $amount = $entry->getQuantity(); - // If the BOM quantity is below the minimum order amount, use the minimum order amount - // for the price lookup — otherwise calculateAvgPrice returns null (no price tier matches). - $minOrderAmount = $this->pricedetailHelper->getMinOrderAmount($entry->getPart()); - if ($minOrderAmount !== null) { - $amount = max($amount, $minOrderAmount); - } - return $this->pricedetailHelper->calculateAvgPrice($entry->getPart(), $amount) ?? BigDecimal::zero(); - } - return $entry->getPrice() ?? BigDecimal::zero(); - } - private function getFilterQuery(QueryBuilder $builder, array $options): void { $builder diff --git a/src/Services/ProjectSystem/ProjectBuildHelper.php b/src/Services/ProjectSystem/ProjectBuildHelper.php index 9d643a87..ee5b8c68 100644 --- a/src/Services/ProjectSystem/ProjectBuildHelper.php +++ b/src/Services/ProjectSystem/ProjectBuildHelper.php @@ -227,6 +227,16 @@ final readonly class ProjectBuildHelper ?->toScale(2, RoundingMode::UP); } + /** + * Returns the effective unit price for a single piece of the given BOM entry, + * taking bulk pricing and minimum order amounts into account for N builds. + * Returns BigDecimal::zero() when no pricing data is available. + */ + public function getEntryUnitPrice(ProjectBOMEntry $entry, int $number_of_builds = 1, ?Currency $currency = null): BigDecimal + { + return $this->getBomEntryUnitPrice($entry, $number_of_builds, $currency) ?? BigDecimal::zero(); + } + /** * Returns the effective unit price for a single piece of the given BOM entry, * taking bulk pricing into account for N builds. diff --git a/tests/Services/ProjectSystem/ProjectBuildHelperTest.php b/tests/Services/ProjectSystem/ProjectBuildHelperTest.php index cf36b030..de9f9406 100644 --- a/tests/Services/ProjectSystem/ProjectBuildHelperTest.php +++ b/tests/Services/ProjectSystem/ProjectBuildHelperTest.php @@ -264,6 +264,37 @@ final class ProjectBuildHelperTest extends WebTestCase $this->assertTrue(BigDecimal::of('11.00')->isEqualTo($result)); } + public function testGetEntryUnitPriceReturnsZeroForNoPricingData(): void + { + $entry = new ProjectBOMEntry(); + $entry->setPart(new Part()); // part with no orderdetails + $entry->setQuantity(5); + + $result = $this->service->getEntryUnitPrice($entry); + $this->assertTrue(BigDecimal::zero()->isEqualTo($result)); + } + + public function testGetEntryUnitPriceNonPartEntry(): void + { + $entry = new ProjectBOMEntry(); + $entry->setName('Wire'); + $entry->setQuantity(2); + $entry->setPrice(BigDecimal::of('1.25')); + + $result = $this->service->getEntryUnitPrice($entry); + $this->assertTrue(BigDecimal::of('1.25')->isEqualTo($result)); + } + + public function testGetEntryUnitPriceWithPart(): void + { + $entry = new ProjectBOMEntry(); + $entry->setPart($this->makePartWithPrice(2.00)); + $entry->setQuantity(3); + + $result = $this->service->getEntryUnitPrice($entry); + $this->assertTrue(BigDecimal::of('2.00')->isEqualTo($result)); + } + public function testCalculateTotalBuildPriceRespectsMinOrderAmount(): void { $project = new Project();