128 lines
4.4 KiB
PHP
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'] ?? '')),
|
|
);
|
|
}
|
|
|
|
}
|