Files
Moodle/classes/local/receipt/ReceiptItem.php
T

128 lines
4.4 KiB
PHP

<?php
declare(strict_types=1);
namespace paygw_moneta\local\receipt;
use InvalidArgumentException;
use paygw_moneta\local\Money;
/**
* Позиция чека — объект `inventPositions` (`kassaspecification.pdf`) и элемент
* `items` JSON-ответа Check URL (`cmsspecification.pdf`).
*
* @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 ReceiptItem
{
public const NAME_MAX_LENGTH = 128;
/** Максимальная цена позиции и произведения `price * quantity`. */
public const PRICE_MAX = '42949672.95';
/** Пустое имя касса не примет; остаётся, если после чистки ничего нет. */
private const FALLBACK_NAME = 'Услуга';
/** Единица измерения из перечня кассы; для доступа к курсу иной не бывает. */
private const MEASURE = 'unit';
public readonly string $name;
public function __construct(
string $name,
public readonly Money $price,
public readonly int $quantity,
public readonly Vat $vat,
public readonly PaymentMethod $paymentMethod,
public readonly PaymentObject $paymentObject,
) {
$name = ReceiptText::validate($name, self::NAME_MAX_LENGTH);
$this->name = ($name === '') ? self::FALLBACK_NAME : $name;
if ($quantity < 1 || $quantity > 99999) {
throw new InvalidArgumentException('Количество позиции вне допустимых границ.');
}
$max = Money::fromDecimal(self::PRICE_MAX)->toMinorUnits();
if ($price->toMinorUnits() > $max || $this->total()->toMinorUnits() > $max) {
throw new InvalidArgumentException('Цена позиции превышает потолок кассы.');
}
}
public function total(): Money
{
return $this->price->multiply($this->quantity);
}
/**
* Элемент `items` для JSON-ответа Check URL: числа — числа, ставка — строкой `vat`.
* Цена подставляется меткой, чтобы в JSON осталось ровно два знака (`100.00`).
*
* @return array<string, mixed>
*/
public function toCheckItem(): array
{
return [
'name' => $this->name,
'price' => JsonNumber::placeholder($this->price),
'quantity' => $this->quantity,
'measure' => self::MEASURE,
'paymentMethod' => $this->paymentMethod->value,
'paymentObject' => $this->paymentObject->value,
'vat' => $this->vat->value,
];
}
/**
* Объект `inventPositions` для атрибута `INVENTORY`: цена и количество —
* строками, как в примере спецификации Moneta.
*
* @return array<string, string>
*/
public function toInventoryPosition(): array
{
return [
'name' => $this->name,
'price' => $this->price->toDecimal(),
'quantity' => (string) $this->quantity,
'measure' => self::MEASURE,
'vatTag' => $this->vat->tag(),
'pm' => $this->paymentMethod->value,
'po' => $this->paymentObject->value,
];
}
/**
* @return array<string, string|int>
*/
public function toArray(): array
{
return [
'name' => $this->name,
'price' => $this->price->toDecimal(),
'quantity' => $this->quantity,
'vat' => $this->vat->value,
'paymentMethod' => $this->paymentMethod->value,
'paymentObject' => $this->paymentObject->value,
];
}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(
name: (string) ($data['name'] ?? ''),
price: Money::fromDecimal((string) ($data['price'] ?? '')),
quantity: (int) ($data['quantity'] ?? 0),
vat: Vat::from((string) ($data['vat'] ?? '')),
paymentMethod: PaymentMethod::from((string) ($data['paymentMethod'] ?? '')),
paymentObject: PaymentObject::from((string) ($data['paymentObject'] ?? '')),
);
}
}