- 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.
123 lines
3.4 KiB
PHP
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);
|
|
}
|
|
}
|