ldc-boutique-cnc-sable/domain/Models/ValueObjects/DiscountValue.php
Jérémy Munsch e857a58d00
test(Product): add comprehensive tests for Product entity
- Implement tests for product publication status.
- Add tests for product orderability based on various conditions.
- Enhance pricing calculation tests for different discount scenarios.
- Remove outdated PricingServiceTest and refactor ProductAvailabilityServiceTest.
2026-10-29 19:48:38 +01:00

123 lines
3.4 KiB
PHP

<?php
declare(strict_types=1);
namespace Domain\Models\ValueObjects;
use Domain\Enums\DiscountType;
use Domain\Models\Entities\Product;
use Money\Money;
/**
* Represents a product discount (either a percentage or a fixed amount).
*
* Business rule: All discounts apply to the piece (sold unit), regardless of the product's invoicing unit.
*/
final readonly class DiscountValue
{
private function __construct(
public DiscountType $type,
private ?int $percentage = null,
private ?Money $amount = null
) {
if ($type === DiscountType::PERCENT && $percentage === null) {
throw new \DomainException(
'Percentage rate must be provided for percentage discounts.'
);
}
if ($percentage !== null && ($percentage < 0 || $percentage > 100)) {
throw new \DomainException(
'Percentage rate must be between 0 and 100.'
);
}
if ($type === DiscountType::AMOUNT && $amount === null) {
throw new \DomainException(
'Amount must be provided for amount discounts.'
);
}
if ($amount !== null && $amount->isNegative()) {
throw new \DomainException(
'Discount amount cannot be negative.'
);
}
}
public static function fromAmount(Money $amount): self
{
return new self(type: DiscountType::AMOUNT, amount: $amount);
}
public static function fromPercentage(int $percentageRate): self
{
return new self(type: DiscountType::PERCENT, percentage: $percentageRate);
}
public function value(): Money | int
{
if ($this->type === DiscountType::AMOUNT) {
return $this->getAmount();
}
return $this->getPercentage();
}
public function isPercentage(): bool
{
return $this->type === DiscountType::PERCENT;
}
public function getPercentageRate(): float
{
return $this->getPercentage() / 100;
}
public function getPercentage(): int
{
if ($this->type !== DiscountType::PERCENT) {
throw new \DomainException(
'Cannot get percentage from a non-percentage discount.'
);
}
if ($this->percentage === null) {
throw new \DomainException(
'Percentage rate must be set for percentage discounts.'
);
}
return $this->percentage;
}
public function getAmount(): Money
{
if ($this->type !== DiscountType::AMOUNT) {
throw new \DomainException(
'Cannot get amount from a non-amount discount.'
);
}
if ($this->amount === null) {
throw new \DomainException(
'Amount must be set for amount discounts.'
);
}
return $this->amount;
}
public function calculateDiscountAmount(Money $basePrice): Money
{
if ($this->type === DiscountType::AMOUNT) {
$amount = $this->getAmount();
if (! $amount->getCurrency()->equals($basePrice->getCurrency())) {
throw new \DomainException('Currency mismatch.');
}
return $amount;
}
$rate = $this->getPercentageRate();
return $basePrice->multiply((string) $rate, Product::ROUNDING);
}
}