feat: paygw_moneta 1.0.0, MONETA.Assistant payment gateway for Moodle
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user