105 lines
2.9 KiB
PHP
105 lines
2.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Domain\Models\ValueObjects;
|
|
|
|
use Money\Money;
|
|
use Webmozart\Assert\Assert;
|
|
|
|
/**
|
|
* Taux de remise exprimé en points de base (1 bp = 0,01 %), pour éviter toute imprécision en virgule flottante.
|
|
*/
|
|
final readonly class DiscountRate implements \Stringable
|
|
{
|
|
public const MAX_BASIS_POINTS = 10000;
|
|
|
|
public function __construct(
|
|
private int $basisPoints
|
|
) {
|
|
Assert::range($basisPoints, 0, self::MAX_BASIS_POINTS, 'Discount rate has to be between 0 and 100 percent.');
|
|
}
|
|
|
|
public static function zero(): self
|
|
{
|
|
return new self(0);
|
|
}
|
|
|
|
public static function fromBasisPoints(int $basisPoints): self
|
|
{
|
|
return new self($basisPoints);
|
|
}
|
|
|
|
/** @param int|float $percentage Percentage between 0 and 100 (ex: 15 for 15 %) */
|
|
public static function fromPercentage(int | float $percentage): self
|
|
{
|
|
return new self((int) round($percentage * 100));
|
|
}
|
|
|
|
/** @param float $ratio Ratio between 0 and 1 (ex: 0.15 for 15 %) */
|
|
public static function fromRatio(float $ratio): self
|
|
{
|
|
return new self((int) round($ratio * self::MAX_BASIS_POINTS));
|
|
}
|
|
|
|
/**
|
|
* Computes the effective rate represented by a fixed discount amount on a base price.
|
|
* The result is capped at 100 %.
|
|
*/
|
|
public static function fromAmounts(Money $discountAmount, Money $basePrice): self
|
|
{
|
|
if (! $basePrice->isPositive() || ! $discountAmount->isPositive()) {
|
|
return self::zero();
|
|
}
|
|
|
|
$basisPoints = intdiv(
|
|
((int) $discountAmount->getAmount() * self::MAX_BASIS_POINTS * 2) + (int) $basePrice->getAmount(),
|
|
(int) $basePrice->getAmount() * 2
|
|
);
|
|
|
|
return new self(min($basisPoints, self::MAX_BASIS_POINTS));
|
|
}
|
|
|
|
public function basisPoints(): int
|
|
{
|
|
return $this->basisPoints;
|
|
}
|
|
|
|
/** Ratio between 0.0 and 1.0 */
|
|
public function ratio(): float
|
|
{
|
|
return $this->basisPoints / self::MAX_BASIS_POINTS;
|
|
}
|
|
|
|
/** Percentage between 0.0 and 100.0 */
|
|
public function percentage(): float
|
|
{
|
|
return $this->basisPoints / 100;
|
|
}
|
|
|
|
public function isZero(): bool
|
|
{
|
|
return $this->basisPoints === 0;
|
|
}
|
|
|
|
public function equals(self $other): bool
|
|
{
|
|
return $this->basisPoints === $other->basisPoints;
|
|
}
|
|
|
|
/**
|
|
* Returns the discount amount produced by this rate on the given amount.
|
|
*
|
|
* @param Money::ROUND_* $roundingMode
|
|
*/
|
|
public function applyTo(Money $amount, int $roundingMode = Money::ROUND_UP): Money
|
|
{
|
|
return $amount->multiply((string) $this->basisPoints)->divide(self::MAX_BASIS_POINTS, $roundingMode);
|
|
}
|
|
|
|
#[\Override]
|
|
public function __toString(): string
|
|
{
|
|
return rtrim(rtrim(number_format($this->percentage(), 2, '.', ''), '0'), '.') . ' %';
|
|
}
|
|
}
|