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

157 lines
4.9 KiB
PHP

<?php
declare(strict_types=1);
namespace paygw_moneta\local\receipt;
use InvalidArgumentException;
use paygw_moneta\local\Money;
/**
* Чек 54-ФЗ: покупатель и позиции. Сумма позиций обязана совпадать с суммой
* операции до копейки — иначе номенклатура не пройдёт валидацию кассы.
*
* @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 Receipt
{
/** @var list<ReceiptItem> */
public readonly array $items;
/**
* @param list<ReceiptItem> $items
*/
public function __construct(
public readonly Client $client,
array $items,
) {
if ($items === []) {
throw new InvalidArgumentException('Чек без позиций.');
}
foreach ($items as $item) {
if (!$item instanceof ReceiptItem) {
throw new InvalidArgumentException('Позиция чека должна быть ReceiptItem.');
}
}
$this->items = array_values($items);
}
/**
* Единственная позиция «доступ к курсу» на всю сумму заказа.
*/
public static function forCourse(string $courseName, Money $amount, Client $client, Vat $vat): self
{
return new self($client, [
new ReceiptItem(
name: $courseName,
price: $amount,
quantity: 1,
vat: $vat,
paymentMethod: PaymentMethod::FullPayment,
paymentObject: PaymentObject::Service,
),
]);
}
public function total(): Money
{
$total = Money::fromMinorUnits(0);
foreach ($this->items as $item) {
$total = $total->add($item->total());
}
return $total;
}
public function matchesAmount(Money $amount): bool
{
return $this->total()->equals($amount);
}
/**
* Объект `receipt` JSON-ответа Check URL.
*
* @return array{client: array<string, string>, items: list<array<string, mixed>>}
*/
public function toCheckReceipt(): array
{
return [
'client' => $this->client->toArray(),
'items' => array_map(static fn(ReceiptItem $item): array => $item->toCheckItem(), $this->items),
];
}
/**
* Значение атрибута `INVENTORY` — JSON-массив позиций.
*
* @throws \JsonException
*/
public function toInventoryAttribute(): string
{
return json_encode(
array_map(static fn(ReceiptItem $item): array => $item->toInventoryPosition(), $this->items),
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
);
}
/**
* Значение атрибута `CLIENT` — JSON-массив из одного объекта.
*
* @throws \JsonException
*/
public function toClientAttribute(): string
{
return json_encode(
[$this->client->toArray()],
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
);
}
/**
* Значение атрибута `CUSTOMER` — e-mail покупателя строкой, по
* `cmsspecification.pdf`; дублирует `CLIENT.email`, чтобы чек принимался при
* любой трактовке документов. `null` — у покупателя
* только телефон.
*/
public function toCustomerAttribute(): ?string
{
return $this->client->email;
}
/**
* Значение атрибута `PHONE` — телефон покупателя без `+`, как в примере
* `cmsspecification.pdf`; дублирует `CLIENT.phone`. `null` — телефона нет.
*/
public function toPhoneAttribute(): ?string
{
$phone = $this->client->phone;
return $phone === null ? null : ltrim($phone, '+');
}
/**
* @return array{client: array<string, string>, items: list<array<string, string|int>>}
*/
public function toArray(): array
{
return [
'client' => $this->client->toArray(),
'items' => array_map(static fn(ReceiptItem $item): array => $item->toArray(), $this->items),
];
}
/**
* @param array{client?: array<string, ?string>, items?: list<array<string, mixed>>} $data
*/
public static function fromArray(array $data): self
{
$client = Client::fromArray(is_array($data['client'] ?? null) ? $data['client'] : []);
$items = array_map(static fn(array $item): ReceiptItem => ReceiptItem::fromArray($item), $data['items'] ?? []);
return new self($client, $items);
}
}