Files
yookassa-sdk-php/lib/Model/Receipt/Receipt.php
T
2025-01-17 16:04:18 +03:00

545 lines
21 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/*
* The MIT License
*
* Copyright (c) 2025 "YooMoney", NBСO LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
namespace YooKassa\Model\Receipt;
use ReturnTypeWillChange;
use YooKassa\Common\AbstractObject;
use YooKassa\Common\ListObject;
use YooKassa\Common\ListObjectInterface;
use YooKassa\Validator\Constraints as Assert;
use YooKassa\Validator\Exceptions\EmptyPropertyValueException;
use YooKassa\Validator\Exceptions\InvalidPropertyValueTypeException;
use YooKassa\Model\AmountInterface;
/**
* Класс данных для формирования чека в онлайн-кассе (для соблюдения 54-ФЗ).
*
* @category Class
* @package YooKassa\Model
* @author cms@yoomoney.ru
* @link https://yookassa.ru/developers/api
*
* @property ReceiptCustomer $customer Информация о плательщике
* @property ListObjectInterface|ReceiptItemInterface[] $items Список товаров в заказе
* @property ListObjectInterface|SettlementInterface[] $settlements Массив оплат, обеспечивающих выдачу товара
* @property ListObjectInterface|ReceiptItemInterface[] $shippingItems Список товаров в заказе, являющихся доставкой
* @property ListObjectInterface|ReceiptItemInterface[] $shipping_items Список товаров в заказе, являющихся доставкой
* @property int $taxSystemCode Код системы налогообложения. Число 1-6.
* @property int $tax_system_code Код системы налогообложения. Число 1-6.
* @property AdditionalUserProps $additionalUserProps Дополнительный реквизит пользователя (тег в 54 ФЗ — 1084)
* @property AdditionalUserProps $additional_user_props Дополнительный реквизит пользователя (тег в 54 ФЗ — 1084)
* @property ListObjectInterface|IndustryDetails[] $receiptIndustryDetails Отраслевой реквизит чека (тег в 54 ФЗ — 1261)
* @property ListObjectInterface|IndustryDetails[] $receipt_industry_details Отраслевой реквизит чека (тег в 54 ФЗ — 1261)
* @property OperationalDetails $receiptOperationalDetails Операционный реквизит чека (тег в 54 ФЗ — 1270)
* @property OperationalDetails $receipt_operational_details Операционный реквизит чека (тег в 54 ФЗ — 1270)
*/
class Receipt extends AbstractObject implements ReceiptInterface
{
/**
* @var ReceiptCustomer|null Информация о плательщике
*/
#[Assert\Type(ReceiptCustomer::class)]
#[Assert\Valid]
private ?ReceiptCustomer $_customer = null;
/**
* @var ReceiptItemInterface[]|ListObjectInterface Список товаров в заказе
*/
#[Assert\NotBlank]
#[Assert\Valid]
#[Assert\AllType(ReceiptItem::class)]
#[Assert\Type(ListObject::class)]
private ?ListObject $_items = null;
/**
* @var SettlementInterface[]|ListObjectInterface|null Массив оплат, обеспечивающих выдачу товара
*/
#[Assert\Valid]
#[Assert\AllType(Settlement::class)]
#[Assert\Type(ListObject::class)]
#[Assert\Count(min: 1)]
private ?ListObject $_settlements = null;
/**
* @var ReceiptItemInterface[]|ListObjectInterface Список товаров в заказе, являющихся доставкой
*/
#[Assert\Type(ListObject::class)]
#[Assert\Valid]
#[Assert\AllType(ReceiptItem::class)]
private ?ListObject $_shippingItems = null;
/**
* @var int|null Код системы налогообложения. Число 1-6.
*/
#[Assert\Type('int')]
#[Assert\GreaterThanOrEqual(1)]
#[Assert\LessThanOrEqual(6)]
private ?int $_tax_system_code = null;
/** @var AdditionalUserProps|null Дополнительный реквизит пользователя (тег в 54 ФЗ — 1084) */
#[Assert\Type(AdditionalUserProps::class)]
#[Assert\Valid]
private ?AdditionalUserProps $_additional_user_props = null;
/**
* @var IndustryDetails[]|ListObjectInterface|null Отраслевой реквизит чека (тег в 54 ФЗ — 1261)
*/
#[Assert\Valid]
#[Assert\AllType(IndustryDetails::class)]
#[Assert\Type(ListObject::class)]
private ?ListObject $_receipt_industry_details = null;
/**
* @var OperationalDetails|null Операционный реквизит чека (тег в 54 ФЗ — 1270)
*/
#[Assert\Type(OperationalDetails::class)]
#[Assert\Valid]
private ?OperationalDetails $_receipt_operational_details = null;
/**
* Возвращает информацию о плательщике.
*
* @return ReceiptCustomer Информация о плательщике
*/
public function getCustomer(): ReceiptCustomer
{
if (empty($this->_customer)) {
$this->_customer = new ReceiptCustomer();
}
return $this->_customer;
}
/**
* Устанавливает информацию о плательщике.
*
* @param ReceiptCustomer|array|null $customer
*
* @return self
*/
public function setCustomer(mixed $customer = null): self
{
$this->_customer = $this->validatePropertyValue('_customer', $customer);
return $this;
}
/**
* Возвращает список позиций в текущем чеке.
*
* @return ReceiptItemInterface[]|ListObjectInterface Список товаров в заказе
*/
public function getItems(): ListObjectInterface
{
if ($this->_items === null) {
$this->_items = new ListObject(ReceiptItem::class);
}
return $this->_items;
}
/**
* Устанавливает список позиций в чеке.
*
* Если до этого в чеке уже были установлены значения, они удаляются и полностью заменяются переданным списком
* позиций. Все передаваемые значения в массиве позиций должны быть объектами класса, реализующего интерфейс
* ReceiptItemInterface, в противном случае будет выброшено исключение InvalidPropertyValueTypeException.
*
* @param array|ListObjectInterface $items Список товаров в заказе
*
* @throws EmptyPropertyValueException Выбрасывается если передали пустой массив значений
* @throws InvalidPropertyValueTypeException Выбрасывается если в качестве значения был передан не массив и не
* итератор, либо если одно из переданных значений не реализует интерфейс ReceiptItemInterface
*
* @return self
*/
public function setItems(mixed $items): self
{
$items = $this->validatePropertyValue('_items', $items);
$this->_items = null;
$this->_shippingItems = null;
foreach ($items->getItems() as $val) {
$this->addItem($val);
}
return $this;
}
/**
* Обнуляет список позиций в чеке.
*
* Если до этого в чеке уже были установлены значения, они удаляются.
* @return self
*/
public function removeItems(): self
{
$this->getItems()->clear();
return $this;
}
/**
* Добавляет товар в чек.
*
* @param ReceiptItemInterface[]|ListObjectInterface $value Объект добавляемой в чек позиции
*/
public function addItem(mixed $value): void
{
if ($value->isShipping()) {
$this->getShippingItems()->add($value);
}
$this->getItems()->add($value);
}
/**
* Возвращает массив оплат, обеспечивающих выдачу товара.
*
* @return SettlementInterface[]|ListObjectInterface Массив оплат, обеспечивающих выдачу товара
*/
public function getSettlements(): ListObjectInterface
{
if ($this->_settlements === null) {
$this->_settlements = new ListObject(Settlement::class);
}
return $this->_settlements;
}
/**
* Возвращает массив оплат, обеспечивающих выдачу товара.
*
* @param ListObjectInterface|array|null $settlements
*
* @return self
*/
public function setSettlements(mixed $settlements = null): self
{
$this->_settlements = $this->validatePropertyValue('_settlements', $settlements);
return $this;
}
/**
* Добавляет оплату в чек.
*
* @param SettlementInterface $value Оплата
*
* @return self
*/
public function addSettlement(SettlementInterface $value): self
{
$this->getSettlements()->add($value);
return $this;
}
/**
* Возвращает список товаров в заказе, являющихся доставкой
*
* @return ReceiptItemInterface[]|ListObjectInterface Список товаров в заказе, являющихся доставкой
*/
public function getShippingItems(): ListObjectInterface
{
if ($this->_shippingItems === null) {
$this->_shippingItems = new ListObject(ReceiptItem::class);
}
return $this->_shippingItems;
}
/**
* Возвращает код системы налогообложения.
*
* @return int|null Код системы налогообложения. Число 1-6.
*/
public function getTaxSystemCode(): ?int
{
return $this->_tax_system_code;
}
/**
* Устанавливает код системы налогообложения.
*
* @param int|null $tax_system_code Код системы налогообложения. Число 1-6
*
* @return self
*/
public function setTaxSystemCode(?int $tax_system_code): self
{
$this->_tax_system_code = $this->validatePropertyValue('_tax_system_code', $tax_system_code);
return $this;
}
/**
* Возвращает дополнительный реквизит пользователя.
*
* @return AdditionalUserProps|null Дополнительный реквизит пользователя
*/
public function getAdditionalUserProps(): ?AdditionalUserProps
{
return $this->_additional_user_props;
}
/**
* Устанавливает дополнительный реквизит пользователя.
*
* @param AdditionalUserProps|array $additional_user_props Дополнительный реквизит пользователя
*
* @return self
*/
public function setAdditionalUserProps(mixed $additional_user_props = null): self
{
$this->_additional_user_props = $this->validatePropertyValue('_additional_user_props', $additional_user_props);
return $this;
}
/**
* Возвращает отраслевой реквизит чека.
*
* @return IndustryDetails[]|ListObjectInterface Отраслевой реквизит чека
*/
public function getReceiptIndustryDetails(): ListObjectInterface
{
if ($this->_receipt_industry_details === null) {
$this->_receipt_industry_details = new ListObject(IndustryDetails::class);
}
return $this->_receipt_industry_details;
}
/**
* Устанавливает отраслевой реквизит чека.
*
* @param array|ListObjectInterface|null $receipt_industry_details Отраслевой реквизит чека
*
* @throws InvalidPropertyValueTypeException Выбрасывается если переданный аргумент - не массив
*
* @return self
*/
public function setReceiptIndustryDetails(mixed $receipt_industry_details = null): self
{
$this->_receipt_industry_details = $this->validatePropertyValue('_receipt_industry_details', $receipt_industry_details);
return $this;
}
/**
* Добавляет отраслевой реквизит чека.
*
* @param IndustryDetails|array $value Отраслевой реквизит чека.
*
* @return self
*/
public function addReceiptIndustryDetails(mixed $value): self
{
$this->getReceiptIndustryDetails()->add($value);
return $this;
}
/**
* Возвращает операционный реквизит чека.
*
* @return OperationalDetails|null Операционный реквизит чека
*/
public function getReceiptOperationalDetails(): ?OperationalDetails
{
return $this->_receipt_operational_details;
}
/**
* Устанавливает операционный реквизит чека.
*
* @param array|OperationalDetails|null $receipt_operational_details Операционный реквизит чека
*
* @throws InvalidPropertyValueTypeException Выбрасывается если переданный аргумент - не массив
*
* @return self
*/
public function setReceiptOperationalDetails(mixed $receipt_operational_details = null): self
{
$this->_receipt_operational_details = $this->validatePropertyValue('_receipt_operational_details', $receipt_operational_details);
return $this;
}
/**
* Проверяет есть ли в чеке хотя бы одна позиция.
*
* @return bool True если чек не пуст, false если в чеке нет ни одной позиции
*/
public function notEmpty(): bool
{
return !$this->getItems()->isEmpty();
}
/**
* Возвращает стоимость заказа исходя из состава чека.
*
* @param bool $withShipping Добавить ли к стоимости заказа стоимость доставки
*
* @return int Общая стоимость заказа в центах/копейках
*/
public function getAmountValue(bool $withShipping = true): int
{
$result = 0;
foreach ($this->getItems() as $item) {
if ($withShipping || !$item->isShipping()) {
$result += $item->getAmount();
}
}
return $result;
}
/**
* Возвращает стоимость доставки исходя из состава чека.
*
* @return int Стоимость доставки из состава чека в центах/копейках
*/
public function getShippingAmountValue(): int
{
$result = 0;
foreach ($this->getItems() as $item) {
if ($item->isShipping()) {
$result += $item->getAmount();
}
}
return $result;
}
/**
* Подгоняет стоимость товаров в чеке к общей цене заказа.
*
* @param AmountInterface $orderAmount Общая стоимость заказа
* @param bool $withShipping Поменять ли заодно и цену доставки
*/
public function normalize(AmountInterface $orderAmount, bool $withShipping = false): void
{
$amount = $orderAmount->getIntegerValue();
if (!$withShipping && !$this->getShippingItems()->isEmpty()) {
if ($amount > $this->getShippingAmountValue()) {
$amount -= $this->getShippingAmountValue();
} else {
$withShipping = true;
}
}
$realAmount = $this->getAmountValue($withShipping);
if ($realAmount !== $amount) {
$coefficient = (float) $amount / (float) $realAmount;
$items = [];
$realAmount = 0;
foreach ($this->getItems() as $item) {
if ($withShipping || !$item->isShipping()) {
$price = round($coefficient * $item->getPrice()->getIntegerValue());
if ($price < 1.0) {
if ($item->getPrice()->getIntegerValue() > 1) {
$item->getPrice()->setValue(0.01);
}
$amount -= $item->getAmount();
} else {
$items[] = $item;
$realAmount += $item->getAmount();
}
}
}
uasort($items, static function (ReceiptItemInterface $a, ReceiptItemInterface $b) {
if ($a->getPrice()->getIntegerValue() > $b->getPrice()->getIntegerValue()) {
return -1;
}
if ($a->getPrice()->getIntegerValue() < $b->getPrice()->getIntegerValue()) {
return 1;
}
return 0;
});
$coefficient = (float) $amount / (float) $realAmount;
$realAmount = 0;
$aloneId = null;
foreach ($items as $index => $item) {
if ($withShipping || !$item->isShipping()) {
$item->applyDiscountCoefficient($coefficient);
$realAmount += $item->getAmount();
if (null === $aloneId && 1.0 === $item->getQuantity() && !$item->isShipping()) {
$aloneId = $index;
}
}
}
if (null === $aloneId) {
foreach ($this->getItems() as $index => $item) {
if (!$item->isShipping()) {
$aloneId = $index;
break;
}
}
}
if (null === $aloneId) {
$aloneId = 0;
}
$diff = $amount - $realAmount;
if (abs($diff) >= 0.1) {
if (1.0 === $this->_items[$aloneId]->getQuantity()) {
$this->_items[$aloneId]->increasePrice($diff / 100.0);
} elseif ($this->_items[$aloneId]->getQuantity() > 1.0) {
$item = $this->_items[$aloneId]->fetchItem(1);
$item->increasePrice($diff / 100.0);
$items = $this->getItems()->toArray();
array_splice($items, $aloneId + 1, 0, [$item]);
$this->_items = new ListObject(ReceiptItem::class, $items);
} else {
$item = $this->_items[$aloneId]->fetchItem($this->_items[$aloneId]->getQuantity() / 2);
$item->increasePrice($diff / 100.0);
$items = $this->getItems()->toArray();
array_splice($items, $aloneId + 1, 0, [$item]);
$this->_items = new ListObject(ReceiptItem::class, $items);
}
}
}
}
/**
* Возвращает Id объекта чека.
*
* @return null|string Id объекта чека
*/
public function getObjectId(): ?string
{
return null;
}
#[ReturnTypeWillChange]
/**
* Возвращает ассоциативный массив со свойствами текущего объекта для его дальнейшей JSON сериализации.
*
* @return array Ассоциативный массив со свойствами текущего объекта
*/
public function jsonSerialize(): array
{
$data = parent::jsonSerialize();
unset($data['amount_value'], $data['shipping_amount_value']);
return $data;
}
}