*/ public readonly array $items; /** * @param list $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); } /** * Единственная позиция на всю сумму заказа — доступ к курсу или иной * оплачиваемый объект Moodle; признаки расчёта — из настроек шлюза. */ public static function forCourse( string $courseName, Money $amount, ?Client $client, Vat $vat, PaymentMethod $paymentMethod = PaymentMethod::FullPayment, PaymentObject $paymentObject = PaymentObject::Service, ): self { return new self($client, [ new ReceiptItem( name: $courseName, price: $amount, quantity: 1, vat: $vat, paymentMethod: $paymentMethod, paymentObject: $paymentObject, ), ]); } 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; без покупателя `client` — пустой * объект `{}`, как требует схема. * * @return array{client: array|\stdClass, items: list>} */ public function toCheckReceipt(): array { return [ 'client' => $this->client?->toArray() ?? new \stdClass(), 'items' => array_map(static fn(ReceiptItem $item): array => $item->toCheckItem(), $this->items), ]; } /** * `MNT_ATTRIBUTES` XML-ответа на Check URL и Pay URL: `INVENTORY`, `CLIENT` * и дубли покупателя `CUSTOMER`/`PHONE`. Без фискализации позиции идут без * `vatTag`/`pm`/`po`. * * @return array * @throws \JsonException */ public function toResponseAttributes(bool $fiscalization): array { $attributes = [ 'INVENTORY' => $this->toInventoryAttribute($fiscalization), 'CLIENT' => $this->toClientAttribute(), 'CUSTOMER' => $this->toCustomerAttribute(), 'PHONE' => $this->toPhoneAttribute(), ]; return array_filter($attributes, static fn(?string $value): bool => $value !== null); } /** * Значение атрибута `INVENTORY` — JSON-массив позиций. * * @throws \JsonException */ public function toInventoryAttribute(bool $fiscalization): string { return json_encode( array_map( static fn(ReceiptItem $item): array => $item->toInventoryPosition($fiscalization), $this->items, ), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, ); } /** * Значение атрибута `CLIENT` — JSON-массив из одного объекта. `null` — * покупателя нет. * * @throws \JsonException */ public function toClientAttribute(): ?string { if ($this->client === null) { return null; } 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, items: list>} */ 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, items?: list>} $data */ public static function fromArray(array $data): self { $client = is_array($data['client'] ?? null) ? Client::fromArray($data['client']) : null; $items = array_map(static fn(array $item): ReceiptItem => ReceiptItem::fromArray($item), $data['items'] ?? []); return new self($client, $items); } }