Files
Moodle/classes/local/Money.php
T

118 lines
4.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace paygw_moneta\local;
use InvalidArgumentException;
/**
* Денежная сумма в формате MONETA.Assistant: десятичная строка ровно с двумя
* знаками после точки (`120.25`, `100.00`), без знака, без разделителей тысяч.
*
* Внутри — только строка и целые копейки; `float` появляется лишь на границе
* с хостом ({@see self::fromFloat()}), где Moodle отдаёт стоимость числом.
*
* @package paygw_moneta
* @copyright 2026 Moneta Labs {@link https://moneta.ru/}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class Money
{
/** Верхняя граница суммы по JSON-схеме Check URL (`number_amount.maximum`). */
public const MAX = '10000000000000.00';
/** Целая часть — до 14 знаков без ведущих нулей, дробная — ровно два. */
private const PATTERN = '/\A(?:0|[1-9][0-9]{0,13})\.[0-9]{2}\z/';
private function __construct(private string $value) {}
/**
* Строгий разбор десятичной строки; всё, что не `^\d+\.\d{2}$`, отклоняется.
*/
public static function fromDecimal(string $value): self
{
if (preg_match(self::PATTERN, $value) !== 1) {
throw new InvalidArgumentException('Сумма должна быть десятичной строкой с двумя знаками после точки.');
}
$money = new self($value);
if ($money->toMinorUnits() > self::fromMax()->toMinorUnits()) {
throw new InvalidArgumentException('Сумма превышает допустимый максимум.');
}
return $money;
}
/**
* Сумма в копейках; безопасный путь для арифметики.
*/
public static function fromMinorUnits(int $minorUnits): self
{
if ($minorUnits < 0) {
throw new InvalidArgumentException('Сумма не может быть отрицательной.');
}
return self::fromDecimal(sprintf('%d.%02d', intdiv($minorUnits, 100), $minorUnits % 100));
}
/**
* Граница с хостом: Moodle считает стоимость `float` (`helper::get_rounded_cost`).
* Округление до копеек делается здесь один раз, дальше `float` не используется.
*/
public static function fromFloat(float $amount): self
{
if ($amount < 0 || !is_finite($amount)) {
throw new InvalidArgumentException('Сумма хоста должна быть конечным неотрицательным числом.');
}
return self::fromDecimal(number_format($amount, 2, '.', ''));
}
public function toDecimal(): string
{
return $this->value;
}
public function toMinorUnits(): int
{
[$units, $cents] = explode('.', $this->value);
return ((int) $units * 100) + (int) $cents;
}
public function isPositive(): bool
{
return $this->toMinorUnits() > 0;
}
public function equals(self $other): bool
{
return $this->value === $other->value;
}
public function add(self $other): self
{
return self::fromMinorUnits($this->toMinorUnits() + $other->toMinorUnits());
}
public function multiply(int $quantity): self
{
if ($quantity < 0) {
throw new InvalidArgumentException('Количество не может быть отрицательным.');
}
return self::fromMinorUnits($this->toMinorUnits() * $quantity);
}
public function __toString(): string
{
return $this->value;
}
private static function fromMax(): self
{
return new self(self::MAX);
}
}