kopecks * (100 + $percent) / 100)); } public function format(): string { return number_format($this->kopecks / 100, 2, '.', ' ') . ' RUB'; } } final class SupplierOffer { public function __construct( public readonly int $supplierId, public readonly Money $recommendedPrice, public readonly int $quantity, ) { if ($quantity < 0) { throw new InvalidArgumentException('Quantity must be non-negative.'); } } } final class LargeSupplierOffer { public function __construct( public readonly int $supplierId, public readonly Money $recommendedPrice, public readonly Money $calculatedMinPrice, public readonly int $quantity, ) { if ($quantity < 0) { throw new InvalidArgumentException('Quantity must be non-negative.'); } } } final class WarehouseLot { public function __construct( public readonly Money $purchasePrice, public readonly Money $salePrice, public readonly int $quantity, ) { if ($quantity < 0) { throw new InvalidArgumentException('Quantity must be non-negative.'); } } } final class ProductPricingInput { /** * @param SupplierOffer[] $smallSupplierOffers type a * @param LargeSupplierOffer[] $largeSupplierOffers types b and c * @param WarehouseLot[] $warehouseLots own stock lots */ public function __construct( public readonly array $smallSupplierOffers, public readonly array $largeSupplierOffers, public readonly array $warehouseLots, public readonly int $minMarkupPercent, ) { if ($minMarkupPercent < 0) { throw new InvalidArgumentException('Minimal markup must be non-negative.'); } } } final class RetailPriceCalculator { public function calculate(ProductPricingInput $input): Money { $availableOwnLots = array_values(array_filter( $input->warehouseLots, static fn (WarehouseLot $lot): bool => $lot->quantity > 0, )); $hasSupplierData = $this->hasAvailableSupplierData($input); if (!$hasSupplierData) { return $this->validatedWarehouseSalePrice($availableOwnLots, $input->minMarkupPercent); } if ($availableOwnLots !== []) { $minimalOwnPrice = $this->minimalAllowedOwnStockPrice($availableOwnLots, $input->minMarkupPercent); $minimalRecommended = $this->minimalRecommendedSupplierPrice($input); return $minimalRecommended === null ? $minimalOwnPrice : new Money(max($minimalRecommended->kopecks, $minimalOwnPrice->kopecks)); } $minimalCalculatedRemote = $this->minimalCalculatedLargeSupplierPrice($input->largeSupplierOffers); if ($minimalCalculatedRemote !== null) { return $minimalCalculatedRemote; } throw new DomainException('Cannot calculate retail price: no own stock and no calculated supplier prices.'); } /** * @param WarehouseLot[] $availableOwnLots */ private function validatedWarehouseSalePrice(array $availableOwnLots, int $minMarkupPercent): Money { if ($availableOwnLots === []) { throw new DomainException('Cannot use warehouse sale price: product is absent from own stock.'); } $validSalePrices = []; foreach ($availableOwnLots as $lot) { $minimalAllowed = $lot->purchasePrice->multiplyPercent($minMarkupPercent); if ($lot->salePrice->kopecks < $minimalAllowed->kopecks) { throw new DomainException('Warehouse sale price is lower than purchase price plus minimal markup.'); } $validSalePrices[] = $lot->salePrice; } return $this->minMoney($validSalePrices); } /** * @param WarehouseLot[] $availableOwnLots */ private function minimalAllowedOwnStockPrice(array $availableOwnLots, int $minMarkupPercent): Money { return $this->minMoney(array_map( static fn (WarehouseLot $lot): Money => $lot->purchasePrice->multiplyPercent($minMarkupPercent), $availableOwnLots, )); } private function minimalRecommendedSupplierPrice(ProductPricingInput $input): ?Money { $prices = []; foreach ($input->smallSupplierOffers as $offer) { if ($offer->quantity > 0) { $prices[] = $offer->recommendedPrice; } } foreach ($input->largeSupplierOffers as $offer) { if ($offer->quantity > 0) { $prices[] = $offer->recommendedPrice; } } return $prices === [] ? null : $this->minMoney($prices); } /** * @param LargeSupplierOffer[] $offers */ private function minimalCalculatedLargeSupplierPrice(array $offers): ?Money { $prices = []; foreach ($offers as $offer) { if ($offer->quantity > 0) { $prices[] = $offer->calculatedMinPrice; } } return $prices === [] ? null : $this->minMoney($prices); } private function hasAvailableSupplierData(ProductPricingInput $input): bool { foreach ($input->smallSupplierOffers as $offer) { if ($offer->quantity > 0) { return true; } } foreach ($input->largeSupplierOffers as $offer) { if ($offer->quantity > 0) { return true; } } return false; } /** * @param Money[] $prices */ private function minMoney(array $prices): Money { if ($prices === []) { throw new InvalidArgumentException('Cannot get minimal price from an empty list.'); } return array_reduce( $prices, static fn (?Money $carry, Money $price): Money => $carry === null || $price->kopecks < $carry->kopecks ? $price : $carry, ); } } final class SupplierMarkupRange { public function __construct( public readonly Money $fromInclusive, public readonly ?Money $toInclusive, public readonly int $markupPercent, ) { if ($markupPercent < 0) { throw new InvalidArgumentException('Markup must be non-negative.'); } } public function matches(Money $purchasePrice): bool { if ($purchasePrice->kopecks < $this->fromInclusive->kopecks) { return false; } return $this->toInclusive === null || $purchasePrice->kopecks <= $this->toInclusive->kopecks; } } final class SupplierMarkupPolicy { /** * @param SupplierMarkupRange[] $ranges */ public function __construct( public readonly int $supplierId, public readonly array $ranges, public readonly ?int $discountlessMarkupPercent = null, ) { } public function markupFor(Money $purchasePrice, bool $discountless): int { if ($discountless && $this->discountlessMarkupPercent !== null) { return $this->discountlessMarkupPercent; } foreach ($this->ranges as $range) { if ($range->matches($purchasePrice)) { return $range->markupPercent; } } return 25; } } final class SupplierPurchaseOffer { public function __construct( public readonly int $supplierId, public readonly Money $purchasePrice, public readonly int $quantity, ) { if ($quantity < 0) { throw new InvalidArgumentException('Quantity must be non-negative.'); } } } final class SupplierCalculatedSalePrice { public function __construct( public readonly int $supplierId, public readonly Money $purchasePrice, public readonly Money $salePrice, public readonly int $markupPercent, public readonly int $quantity, ) { } } final class SupplierSalePriceCalculator { /** * @param SupplierPurchaseOffer[] $purchaseOffers * @param array $policiesBySupplierId * @return SupplierCalculatedSalePrice[] */ public function calculate(array $purchaseOffers, array $policiesBySupplierId, bool $discountless): array { $result = []; foreach ($purchaseOffers as $offer) { if ($offer->quantity <= 0) { continue; } $policy = $policiesBySupplierId[$offer->supplierId] ?? new SupplierMarkupPolicy($offer->supplierId, []); $markupPercent = $policy->markupFor($offer->purchasePrice, $discountless); $result[] = new SupplierCalculatedSalePrice( supplierId: $offer->supplierId, purchasePrice: $offer->purchasePrice, salePrice: $offer->purchasePrice->multiplyPercent($markupPercent), markupPercent: $markupPercent, quantity: $offer->quantity, ); } return $result; } }