ldc-boutique-cnc-sable/domain/Models/ValueObjects/DiscountRate.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'), '.') . ' %';
}
}