157 lines
4.9 KiB
PHP
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);
|
|
}
|
|
}
|