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