Кассы и чеки
- Выбрать способ работы с чеком
- Связь заказа, оплаты, отгрузки и чека
- Основные классы
- Подготовить данные
- Получить список касс
- Получить типы чеков, которые поддерживает модуль
- Создать чек по оплате
- Сформировать чек по отгрузке
- Найти чеки заказа
- Получить ссылку на чек после печати
- Управлять состоянием чека через CheckManager
- Проверить связь с оплатой или отгрузкой
- Получить данные в обработчике кассы
- Расширить интеграцию с кассовым сервисом
- Использовать события касс и чеков
- Обработать ошибку печати
- Учитывать права и требования законодательства
- Проверить итоговое состояние чека
В модуле Интернет-магазин подсистема касс создает фискальные чеки по оплатам и отгрузкам, которые входят в заказ. Класс Bitrix\Sale\Cashbox\CheckManager формирует чек, подбирает кассу с учетом ограничений и запускает печать. Классы Bitrix\Sale\Cashbox\Check и Bitrix\Sale\Cashbox\Cashbox отвечают за данные чека и обмен с кассовым сервисом.
Оплата и чек фиксируют разные операции. Оплата хранит расчет с покупателем и состояние платежа. Чек хранит данные фискального документа. Он содержит связь с заказом, оплатой или отгрузкой и результат печати.
Выбрать способ работы с чеком
|
Задача и API |
Когда использовать |
Как проверить результат |
|
Сформировать чек автоматически при сохранении заказа |
Штатный сценарий, в котором тип и момент печати определяет модуль |
Найти чек через |
|
Создать чек известного типа через |
Проект сам определяет тип и момент создания. Для фонового кода нужна защита от параллельных повторов |
Проверить |
|
Передать выбор типа модулю через |
Тип зависит от состояния документов, настроек и стандартного сопоставления |
Повторно вызвать |
|
Прочитать чеки документа через |
Нужны чеки конкретной оплаты или отгрузки |
Проверить тип, сумму и статус каждой записи |
|
Повторить печать после ошибки через |
Разработчик устранил причину ошибки, а чек уже находится в состоянии |
Проверить |
Связь заказа, оплаты, отгрузки и чека
Заказ содержит товары, оплаты и отгрузки. Когда изменение состояния оплаты или отгрузки запускает формирование чека, модуль выполняет последовательность:
-
Определяет тип чека по операции и состоянию документов заказа.
-
Собирает товары, доставку, суммы, НДС, данные покупателя и способы расчета.
-
Подбирает кассу с учетом ее активности, обработчика и ограничений.
-
Создает запись чека и связывает ее с заказом, оплатой или отгрузкой.
-
Передает чек обработчику кассы сразу или оставляет его в очереди.
-
Сохраняет результат печати, внешний идентификатор, ссылку на чек или сообщение об ошибке.
Связь с оплатой нужна для чека по платежной операции. Отгрузку связывают с чеком, например, после передачи товаров. Один заказ может содержать несколько чеков, если покупатель платит частями, оформляет возврат или завершает расчет при отгрузке.
Тип чека и момент печати зависят от способа расчета, версии формата фискальных данных и настроек проекта. Не выбирайте тип только по признаку PAID или DEDUCTED. Сначала определите фискальный сценарий проекта.
Основные классы
|
Класс |
Роль |
|
|
Базовый класс для обработки данных кассы. Преобразует данные чека в запрос внешнего сервиса и разбирает ответ |
|
|
Базовый класс товарного чека. Хранит тип, сумму, валюту, связи с документами и данные для печати |
|
|
Возвращает типы чеков, которые поддерживает модуль, создает чеки, читает их данные, повторяет печать и сохраняет результат |
|
|
Загружает настройки касс, создает объекты обработчиков и выбирает доступную кассу для чека |
|
|
Читает настройки касс через ORM |
|
|
Читает записи чеков через ORM. Для создания и повторной печати используйте |
Таблицы CashboxTable и CashboxCheckTable показывают текущее состояние. Они не выполняют бизнес-логику печати. Не добавляйте чек прямым вызовом CashboxCheckTable::add(). Такая запись не пройдет подбор кассы, проверку данных и стандартный запуск обработчика.
Подготовить данные
Перед работой с чеками:
-
Подключите модуль
sale. -
Загрузите заказ через
Bitrix\Sale\Order::load(). -
Получите оплату или отгрузку из коллекции этого заказа.
-
Проверьте, что код сохранил документ и его состояние соответствует типу чека.
-
Проверьте права текущего пользователя или фонового процесса на работу с заказом.
use Bitrix\Main\Loader;
use Bitrix\Sale\Order;
$orderId = 123;
if (!Loader::includeModule('sale'))
{
throw new \RuntimeException('Не удалось подключить модуль sale');
}
if ($orderId <= 0)
{
throw new \InvalidArgumentException(
'Идентификатор заказа должен быть положительным числом'
);
}
$order = Order::load($orderId);
if (!$order)
{
throw new \RuntimeException("Заказ {$orderId} не найден");
}
Не загружайте оплату или отгрузку произвольного заказа отдельно от объекта Order, который код уже проверил. Поиск документа в коллекции одновременно подтверждает его связь с заказом.
Во всех следующих примерах $order — объект, который код загрузил этим способом. Переменные $payment и $shipment обозначают документы, которые код получает из коллекций заказа после его сохранения. Разделы, в которых эти переменные появляются впервые, показывают их получение.
Получить список касс
Чтобы получить список конфигураций, используйте CashboxTable::getList(). Обычно коду проекта нужны идентификатор, название, активность, доступность и обработчик.
use Bitrix\Sale\Cashbox\Internals\CashboxTable;
$cashboxes = CashboxTable::getList([
'select' => [
'ID',
'NAME',
'ACTIVE',
'ENABLED',
'HANDLER',
],
'order' => [
'ID' => 'ASC',
],
]);
while ($cashbox = $cashboxes->fetch())
{
echo sprintf(
"%d: %s, active=%s, enabled=%s\n",
$cashbox['ID'],
$cashbox['NAME'],
$cashbox['ACTIVE'],
$cashbox['ENABLED']
);
}
Поля ACTIVE и ENABLED выполняют разные функции:
-
по
ACTIVEкод определяет активность настройки кассы, -
поле
ENABLEDучаствует в выборе кассы, которая может печатать чек.
Чтобы получить объект обработчика по идентификатору, используйте Manager::getObjectById().
use Bitrix\Sale\Cashbox\Manager;
$cashboxId = 7;
$cashbox = Manager::getObjectById($cashboxId);
if (!$cashbox)
{
throw new \RuntimeException("Касса {$cashboxId} не найдена");
}
echo $cashbox::getName();
Список настроек отличается от перечня касс, которые можно использовать для конкретного документа. Чтобы заранее проверить ограничения для оплаты или отгрузки, используйте Manager::getListWithRestrictions().
use Bitrix\Sale\Cashbox\Manager;
$paymentId = 456;
$payment = $order
->getPaymentCollection()
->getItemById($paymentId)
;
if (!$payment)
{
throw new \RuntimeException(
"Оплата {$paymentId} не найдена в заказе {$order->getId()}"
);
}
$availableCashboxes = Manager::getListWithRestrictions($payment);
foreach ($availableCashboxes as $cashboxId => $cashbox)
{
echo sprintf(
"%d: %s\n",
$cashboxId,
$cashbox['NAME']
);
}
Метод Manager::getListWithRestrictions() возвращает только активные кассы, ограничения которых допускают документ из аргумента. Это предварительная проверка для одной оплаты или отгрузки. При создании чека CheckManager дополнительно вызывает Manager::getAvailableCashboxList(). Этот метод проверяет ограничения каждого основного документа и отдельно учитывает, может ли платежная система печатать чек.
При печати не выбирайте первую активную кассу вручную. Передайте выбор менеджеру чеков.
Ограничение Bitrix\Sale\Cashbox\Restrictions\Company связывает кассу с юридическим лицом продавца. Ограничение Bitrix\Sale\Cashbox\Restrictions\PaySystem учитывает платежную систему.
Если проект использует несколько компаний или вариантов оплаты, настройте ограничения касс и заполните COMPANY_ID в оплате или отгрузке. Подробнее о связи оплаты с компанией читайте в статье Оплаты и платежные системы, о создании компании — в статье Настройки интернет-магазина.
Получить типы чеков, которые поддерживает модуль
Список типов зависит от версии модуля и формата фискальных данных. Пользовательские обработчики могут добавить свои типы. Получите список через CheckManager, а не храните его копию в коде проекта.
use Bitrix\Sale\Cashbox\CheckManager;
foreach (CheckManager::getSalesCheckList() as $checkClass)
{
echo sprintf(
"%s: %s (%s)\n",
$checkClass::getType(),
$checkClass::getName(),
$checkClass
);
}
Метод getSalesCheckList() возвращает классы товарных чеков. Каждый класс сообщает:
-
тип через
getType(), -
название для вывода через
getName(), -
допустимый источник через
getSupportedEntityType().
Стандартный список включает продажу и возврат продажи. Если кассы поддерживают нужную версию формата фискальных данных, список также может включать аванс, предоплату, полную предоплату, кредит, погашение кредита и возвраты. Метод getSalesCheckList() не возвращает чеки коррекции. Их классы наследуют CorrectionCheck, а не Check.
Для базовой продажи используйте тип класса Bitrix\Sale\Cashbox\SellCheck.
use Bitrix\Sale\Cashbox\SellCheck;
$checkType = SellCheck::getType();
Не передавайте строку типа из HTTP-запроса напрямую в CheckManager::addByType(). Сопоставьте сценарий, который разрешает проект, с известным классом чека или проверьте строку по карте CheckManager::getCheckTypeMap().
Создать чек по оплате
Обычно модуль создает чеки автоматически при изменении состояния оплаты или отгрузки. Явный вызов нужен для сценария под контролем проекта, в котором проект самостоятельно определяет момент и тип фискального документа.
Создать чек вручную из сервиса проекта
Для ручного создания вызывайте addByType() из сервиса проекта после успешного Order::save(). Не вызывайте метод напрямую из OnSalePaymentEntitySaved. Событие срабатывает во время сохранения оплаты. После него Order::save() может запустить стандартное создание чеков, поэтому прямой вызов из обработчика создает риск дубля.
Для ручного сценария:
-
Поместите код в сервис проекта, например, в
/local/php_interface/lib/sale/cashbox/paymentcheckservice.php. -
Подключите класс через автозагрузку проекта.
-
Вызывите сервис из обработчика ответа платежного шлюза или фонового задания после успешного
Order::save().
Проект должен явно управлять типом и моментом создания чека. Не запускайте сервис вместе со стандартным автоматическим созданием чека по той же оплате.
Метод addByType() принимает массив основных документов чека и строковый тип чека. Основными документами могут быть объекты Payment или Shipment. В третьем необязательном аргументе передайте оплаты и отгрузки, которые нужно связать с основными документами, если тип чека учитывает предыдущие расчеты.
namespace Local\Sale\Cashbox;
use Bitrix\Main\Application;
use Bitrix\Main\Loader;
use Bitrix\Sale\Cashbox\CheckManager;
use Bitrix\Sale\Cashbox\SellCheck;
use Bitrix\Sale\Order;
use Bitrix\Sale\Payment;
final class PaymentCheckService
{
public static function createSellCheck(
int $orderId,
int $paymentId
): int
{
if (!Loader::includeModule('sale'))
{
throw new \RuntimeException('Не удалось подключить модуль sale');
}
$order = Order::load($orderId);
if (!$order)
{
throw new \RuntimeException("Заказ {$orderId} не найден");
}
$payment = $order
->getPaymentCollection()
->getItemById($paymentId)
;
if (!$payment)
{
throw new \RuntimeException(
"Оплата {$paymentId} не найдена в заказе {$orderId}"
);
}
if (!$payment->isPaid())
{
throw new \RuntimeException('Оплата еще не проведена');
}
$connection = Application::getConnection();
$lockName = sprintf(
'sale-check-%d-%d-%s',
$orderId,
$paymentId,
SellCheck::getType()
);
if (!$connection->lock($lockName, 5))
{
throw new \RuntimeException(
'Не удалось получить блокировку для создания чека'
);
}
try
{
$checkId = self::findSellCheckId($payment);
if ($checkId !== null)
{
return $checkId;
}
$result = CheckManager::addByType(
[$payment],
SellCheck::getType()
);
if (!$result->isSuccess())
{
throw new \RuntimeException(
implode('; ', $result->getErrorMessages())
);
}
$checkId = self::findSellCheckId($payment);
if ($checkId === null)
{
throw new \RuntimeException(
'Чек не создан: проверьте активные кассы и их ограничения'
);
}
return $checkId;
}
finally
{
$connection->unlock($lockName);
}
}
private static function findSellCheckId(Payment $payment): ?int
{
foreach (CheckManager::getCheckInfo($payment) as $id => $check)
{
if ($check['TYPE'] === SellCheck::getType())
{
return (int)$id;
}
}
return null;
}
}
В точке интеграции у $payment уже должен быть признак оплаты. Сохраните весь заказ, а затем передайте сервису идентификаторы объектов.
use Local\Sale\Cashbox\PaymentCheckService;
$saveResult = $order->save();
if (!$saveResult->isSuccess())
{
throw new \RuntimeException(
implode('; ', $saveResult->getErrorMessages())
);
}
$checkId = PaymentCheckService::createSellCheck(
(int)$order->getId(),
(int)$payment->getId()
);
Метод addByType() не всегда записывает идентификатор чека в Result. Если в системе нет активных касс, метод может вернуть успешный результат без новой записи. Поэтому сервис повторно читает чеки оплаты, подтверждает наличие записи и возвращает ее идентификатор.
Метод Connection::lock() базового класса соединения может вернуть true, не установив блокировку. Проверьте, поддерживает ли драйвер подключения к базе данных блокировку. Если блокировка не поддерживается, примените распределенную блокировку или хранилище ключей идемпотентности. Чек со статусом N, P, Y или E уже существует. После устранения ошибки повторите печать через CheckManager::reprint(), а не создавайте запись заново.
Сформировать чек по отгрузке
Если тип чека должен зависеть от состояния заказа, настроек модуля и схемы расчетов, измените состояние отгрузки в объекте заказа и сохраните весь заказ. При переходе DEDUCTED из N в Y модуль передает отгрузку в CheckManager::addChecks(). Метод применяет стандартные правила сопоставления документов или результат события OnCheckCollateDocuments.
use Bitrix\Sale\Cashbox\CheckManager;
$shipmentId = 567;
$shipment = $order
->getShipmentCollection()
->getItemById($shipmentId)
;
if (!$shipment || $shipment->isSystem())
{
throw new \RuntimeException(
"Отгрузка {$shipmentId} не найдена в заказе {$order->getId()}"
);
}
if ($shipment->getField('DEDUCTED') === 'Y')
{
throw new \RuntimeException('Отгрузка уже выполнена');
}
$shipment->setField('DEDUCTED', 'Y');
$saveResult = $order->save();
if (!$saveResult->isSuccess())
{
throw new \RuntimeException(
implode('; ', $saveResult->getErrorMessages())
);
}
$checkInfo = CheckManager::getCheckInfo($shipment);
Для отгрузки результат стандартного сопоставления зависит от оплаты заказа, опции типа чека при оплате и документов, которые модуль уже сформировал. Сопоставление может:
-
создать чек продажи,
-
создать чек кредита,
-
не создать новый чек.
Не вызывайте CheckManager::addChecks() после повторной загрузки уже выполненной отгрузки. Стандартное сопоставление учитывает переход DEDUCTED из N в Y и для сохраненного состояния может вернуть пустой список.
Метод Order::save() не возвращает идентификаторы созданных чеков. После сохранения получите актуальный список через CheckManager::getCheckInfo($shipment).
Найти чеки заказа
В примерах раздела используются два объекта:
-
$order— заказ, который код загрузил ранее, -
$payment— оплата, которую код получает по идентификатору из коллекции заказа после его сохранения, как в разделе Создать чек по оплате.
После загрузки заказа метод getPrintedChecks() возвращает объекты Check, которые относятся к этому заказу.
foreach ($order->getPrintedChecks() as $check)
{
echo sprintf(
"%d: %s, status=%s\n",
$check->getField('ID'),
$check->getField('TYPE'),
$check->getField('STATUS')
);
}
Если нужно найти чеки конкретной оплаты или отгрузки, используйте CheckManager::getCheckInfo().
use Bitrix\Sale\Cashbox\CheckManager;
$checkInfo = CheckManager::getCheckInfo($payment);
foreach ($checkInfo as $checkId => $check)
{
echo sprintf(
"%d: %s, %s %s, status=%s\n",
$checkId,
$check['TYPE_NAME'],
$check['SUM'],
$check['CURRENCY'],
$check['STATUS']
);
}
Метод принимает объект Payment или Shipment, который код получил из заказа после его сохранения. Метод фильтрует чеки по идентификатору документа и типу реестра, поэтому не смешивает чеки заказа и другого типа документов.
Для списка по заказу и собственных отчетов используйте CheckManager::getList() или CashboxCheckTable::getList().
use Bitrix\Sale\Cashbox\CheckManager;
$checks = CheckManager::getList([
'select' => [
'ID',
'ORDER_ID',
'PAYMENT_ID',
'SHIPMENT_ID',
'CASHBOX_ID',
'TYPE',
'STATUS',
'SUM',
'CURRENCY',
'EXTERNAL_UUID',
'DATE_CREATE',
'DATE_PRINT_START',
'DATE_PRINT_END',
'ERROR_MESSAGE',
],
'filter' => [
'=ORDER_ID' => $order->getId(),
],
'order' => [
'ID' => 'ASC',
],
]);
$checkRows = $checks->fetchAll();
Основные поля записи
|
Поле |
Что хранит |
|
|
Идентификатор заказа |
|
|
Идентификатор основной оплаты, если чек относится к ней |
|
|
Идентификатор основной отгрузки, если чек относится к ней |
|
|
Идентификатор кассы, которую модуль выбрал для печати |
|
|
Строковый тип чека |
|
|
Состояние печати |
|
|
Идентификатор чека во внешнем кассовом сервисе |
|
|
Последнее сообщение об ошибке, которое сохранил модуль |
Значения статуса печати
|
Значение |
Состояние |
|
|
Чек ожидает печати |
|
|
Обработчик начал печать или проверку результата |
|
|
Касса подтвердила успешную печать |
|
|
Обработчик вернул ошибку печати |
Для одного чека по идентификатору используйте CheckManager::getObjectById(). Метод возвращает объект нужного класса или null.
use Bitrix\Sale\Cashbox\CheckManager;
$checkId = 789;
$check = CheckManager::getObjectById($checkId);
if (!$check)
{
throw new \RuntimeException("Чек {$checkId} не найден");
}
Получить ссылку на чек после печати
В примере $payment — оплата, которую код получает из коллекции $order после сохранения заказа.
Для оплаты или отгрузки используйте CheckManager::getLastPrintableCheckInfo(). Метод выбирает последний чек со статусом Y и формирует ссылку через обработчик кассы.
use Bitrix\Sale\Cashbox\CheckManager;
$checkInfo = CheckManager::getLastPrintableCheckInfo($payment);
if ($checkInfo === [])
{
throw new \RuntimeException(
'У оплаты нет успешно напечатанного чека'
);
}
$checkUrl = (string)($checkInfo['LINK'] ?? '');
if ($checkUrl === '')
{
throw new \RuntimeException(
'Касса не предоставила ссылку на чек'
);
}
echo $checkUrl;
Пустая ссылка не означает ошибку печати. Обработчик может не сохранить LINK_PARAMS или не поддерживать публичный URL. Если код уже загрузил объект чека, тот же результат возвращает $check->getUrl().
Передавайте ссылку только после проверки права пользователя на заказ. Не собирайте URL самостоятельно из LINK_PARAMS. Их формат определяет обработчик кассы.
Управлять состоянием чека через CheckManager
Используйте CashboxCheckTable для чтения и отчетов. Не изменяйте напрямую поля STATUS, EXTERNAL_UUID, LINK_PARAMS, ERROR_MESSAGE и даты печати. Менеджер чеков сохраняет результат печати вместе со связями, журналом, состоянием заказа и событиями. Прямой update() обходит этот жизненный цикл.
Для операций используйте методы CheckManager:
-
addByType()илиaddChecks()— создать чек, -
reprint()— повторить печать, -
штатные методы обработчика — сохранить результат.
Метод CheckManager::delete($checkId) удаляет локальную запись чека и связи с кассами. Метод не отправляет отмену во внешний кассовый сервис и не возвращает Result. Не используйте его как способ аннулировать фискальный документ. Техническое удаление допустимо только в контролируемом обслуживании после проверки состояния у провайдера и требований учета.
Проверить связь с оплатой или отгрузкой
Проверьте связь одним из способов:
-
вызовите
CheckManager::getCheckInfo($payment)илиgetCheckInfo($shipment), -
прочитайте
PAYMENT_IDиSHIPMENT_IDв записиCashboxCheckTable.
Для закрывающих чеков и сложных расчетов одной пары полей может быть недостаточно. Чек может учитывать другие документы, которые участвуют в расчете: предыдущие оплаты, авансы или пользовательские отгрузки. Эти связи CheckManager передает в третий аргумент addByType() и хранит отдельно от основной оплаты или отгрузки.
Собственную схему сопоставления автоматических чеков задавайте через событие OnCheckCollateDocuments. Обработчик получает документы. Модуль формирует чеки на их основе. Обработчик возвращает массив элементов с ключами:
-
TYPE— тип чека, -
ENTITIES— основные оплаты или отгрузки, -
RELATED_ENTITIES— документы, которые обработчик распределил по типам расчета.
Используйте событие, когда стандартная схема не описывает частичную оплату, аванс или несколько этапов передачи заказа. Обработчик должен учитывать чеки, которые модуль уже создал, иначе одно изменение документов может привести к повторной фискализации.
Получить данные в обработчике кассы
Объект Check собирает данные из заказа и документов, которые участвуют в расчете. Конкретный набор зависит от типа чека, но основные данные одинаковы:
|
Данные |
Источник |
|
Заказ |
Объект |
|
Способы расчета |
Основные объекты |
|
Товары |
Позиции |
|
Доставка |
Пользовательские объекты |
|
Покупатель |
Адрес электронной почты и телефон, которые модуль получил из данных заказа |
|
Итог |
Сумма и валюта чека |
Обработчик кассы получает объект Check в методе buildCheckQuery(Check $check). Для чтения массива данных вызовите $check->getDataForCheck(). Затем преобразуйте данные в формат внешнего сервиса.
Перед сборкой запроса модуль вызывает событие OnSaleCheckPrepareData. Оно позволяет изменить массив, который подготовил модуль:
-
скорректировать название позиции,
-
исключить строку с нулевой стоимостью,
-
дополнить данные по правилам проекта.
Не смешивайте два формата данных:
|
Точка расширения |
Формат и основные ключи |
Где использовать |
|
|
Массив с ключами в нижнем регистре: |
В |
|
|
Внутренний массив с ключами в верхнем регистре: |
В обработчике события, чтобы изменить данные, которые модуль подготовил до сборки запроса |
Обработчик события должен изменить массив и вернуть его. Для способа расчета в элементах PAYMENTS используйте ключ TYPE со значениями cash, cashless, advance или credit. Модуль поддерживает ключ IS_CASH для совместимости. Не ищите тип чека в массиве. Модуль передает его отдельным аргументом обработчика.
Не используйте событие, чтобы изменить заказ в хранилище. Обработчик возвращает данные конкретного чека, а не новые значения оплаты, отгрузки или корзины.
Расширить интеграцию с кассовым сервисом
Платежный шлюз может самостоятельно печатать чеки, а проект — подключать собственную онлайн-кассу. В этих случаях базовых операций создания, поиска и проверки чеков недостаточно.
Печатать чеки через платежную систему
Платежная система может передавать товары во внешний платежный шлюз и печатать чек через кассу этого шлюза. В таком сценарии совместно работают два обработчика:
-
обработчик платежной системы сообщает, какой класс кассы он поддерживает,
-
обработчик кассы формирует запрос, отправляет закрывающий чек и проверяет его состояние.
Разделить ответственность
В сценарии участвуют модуль, обработчик платежной системы и обработчик кассы. Они решают разные задачи.
|
Участник |
Отвечает за |
Не отвечает за |
|
Модуль |
Создает локальную запись чека, связывает ее с оплатой или отгрузкой, выбирает кассу и сохраняет результат печати |
Формат запроса и протокол конкретного провайдера |
|
Обработчик платежной системы |
Создает оплату во внешнем шлюзе, обрабатывает ответ об оплате и добавляет данные будущего чека в запрос платежа |
Самостоятельный выбор фискального типа и сохранение локальной записи чека |
|
Обработчик кассы |
Преобразует объект |
Проведение оплаты и обработка платежного уведомления |
Для первичного чека обработчик платежной системы получает товарные позиции, которые подготовила касса, и отправляет их вместе с запросом оплаты. Для закрывающего чека модуль уже вызывает обработчик кассы. Он отправляет отдельный запрос провайдеру и сохраняет результат через CheckManager.
Пройти сквозной сценарий
Обработчик платежной системы реализует Bitrix\Sale\PaySystem\Cashbox\ISupportPrintCheck и использует Bitrix\Sale\PaySystem\Cashbox\CheckTrait. Метод getCashboxClass() связывает его с классом кассы. Один сценарий проходит от запуска оплаты до окончательного статуса чека.
|
Этап |
Что вызывает проект или модуль |
Результат |
|
1. Подготовить платеж |
В |
Трейт сопоставляет документы, создает временный объект |
|
2. Отправить товары |
|
Платежный шлюз получает данные платежа и первичного чека одним запросом |
|
3. Подтвердить оплату |
Обработчик ответа шлюза переводит |
Модуль создает локальную запись чека и выбирает кассу для этой оплаты. Для первичного чека повторный запрос с товарами не нужен |
|
4. Запросить состояние |
Агент |
Касса вызывает |
|
5. Сохранить результат |
|
Чек получает статус |
|
6. Напечатать закрывающий чек |
Если первичный чек фиксировал аванс или предоплату, модуль вызывает |
Касса снова вызывает |
Если первичный документ был чеком полной оплаты, шестой этап не нужен. Ошибка buildCheckQuery() должна остановить initiatePay(). Не создавайте платеж во внешнем шлюзе без обязательных данных чека.
Не записывайте полный массив чека в журнал. Для диагностики достаточно идентификатора оплаты, класса кассы и текста ошибки.
Реализовать кассу платежной системы
Для собственной кассы платежной системы наследуйте Bitrix\Sale\Cashbox\CashboxPaySystem. Этот класс уже реализует немедленную печать и проверку состояния. Наследник задает URL, отправку запроса, разбор ответа, получение данных для проверки и код аккаунта платежной системы.
|
Метод наследника |
Назначение |
|
|
Возвращает адрес печати закрывающего чека |
|
|
Возвращает адрес проверки состояния чека |
|
Отправляет запрос и возвращает |
|
|
Преобразует ответ печати. При успехе добавляет в результат данные внешнего чека, например, UUID |
|
|
Формирует параметры запроса состояния |
|
|
Преобразует ответ о состоянии чека |
|
|
Сохраняет окончательный результат через |
|
|
Возвращает код бизнес-смысла, по которому модуль связывает кассу с аккаунтом платежной системы |
Настроить HTTP-запрос. В методе send() четвертый аргумент $method задает HTTP-метод запроса. По умолчанию использует POST из константы self::SEND_METHOD_HTTP_POST. Аргумент применяется при печати и проверке состояния. Если API провайдера проверяет чек через GET, переопределите getCheckHttpMethod() и верните self::SEND_METHOD_HTTP_GET.
Обработать ответ провайдера. Разделяйте отправку и обработку ответа:
-
send()выполняет HTTP-запрос, добавляет сетевые и протокольные ошибки вResult, но не меняет состояние локального чека. -
processPrintResult()проверяет бизнес-ответ провайдера и при успехе записывает внешний идентификатор в данные результата под ключомUUID. -
getDataForCheck()формирует параметры, по которым внешний сервис найдет чек. -
processCheckResult()преобразует ответ сервиса в понятное обработчику состояние. -
onAfterProcessCheck()обрабатывает результат проверки. Конкретное поведение определяет наследник. Если провайдер еще печатает чек, наследник может вернуть ошибку без вызоваapplyCheckResult(), чтобы модуль запросил состояние позднее.
Класс CheckManager переносит значение UUID из успешного результата немедленной печати в поле EXTERNAL_UUID локальной записи. Не возвращайте внешний идентификатор под произвольным ключом: последующая проверка может не найти чек.
Реализуйте абстрактный метод buildCheckQuery(Check $check) базового класса. Чтобы сохранить ответ через applyCheckResult(), касса должна реализовать extractCheckData().
Проверить условия печати. Перед печатью CashboxPaySystem находит оплату, которая относится к чеку, проверяет поддержку печати чеков у платежной системы и вызывает canPrintCheckSelf($payment). Отправка начинается, только если canPrintCheckSelf($payment) вернул true. Ошибка любой из этих проверок останавливает отправку и попадает в результат операции.
Базовый CashboxPaySystem не формирует Z-отчеты: buildZReportQuery() возвращает пустой массив. Он объявляет ФФД 1.05 и не требует отдельных настроек ОФД. Если возможности собственного провайдера отличаются, проверьте, подходит ли этот базовый класс для интеграции.
Связать кассу с платежной системой. При включении печати через платежную систему модуль автоматически добавляет кассу и связывает ее с аккаунтом обработчика, а не только с идентификатором настройки платежной системы. Код бизнес-смысла этого аккаунта возвращает getPaySystemCodeForKkm(); обычно это логин или идентификатор магазина. Несколько настроек с одним обработчиком и одним значением аккаунта могут использовать одну кассу. Разные аккаунты требуют разных настроек касс.
О жизненном цикле печати через платежную систему читайте в подразделе Пройти сквозной сценарий. Он охватывает подготовку платежа, передачу товаров, создание локального чека, проверку состояния и сохранение результата.
В обработчике платежной системы реализуйте интерфейс ISupportPrintCheck и подключите трейт CheckTrait. Метод getCashboxClass() возвращает класс кассы, который формирует данные чека.
Подробнее о запуске оплаты и обработке ответа шлюза читайте в статье Оплаты и платежные системы.
Создать обработчик онлайн-кассы
Создайте класс в пространстве имен проекта и унаследуйте его от Bitrix\Sale\Cashbox\Cashbox. Базовый класс требует, чтобы наследник формировал запросы чека и Z-отчета.
Путь обработки чека
Тестовый обработчик TestCashbox не обращается во внешний сервис, но проходит тот же путь, что и синхронная интеграция:
-
buildCheckQuery()преобразует объектCheckв массив запроса провайдера. -
printImmediately()передает этот массив вsendTestRequest(). -
sendTestRequest()имитирует HTTP-ответ провайдера. -
applyCheckResult()вызываетextractCheckData(). -
CheckManager::savePrintResult()переводит локальный чек изPвYилиE.
Реализовать тестовый обработчик
Класс TestCashbox имитирует успешный ответ провайдера или ошибку по признаку окружения.
namespace Local\Sale\Cashbox;
use Bitrix\Sale\Cashbox\Cashbox;
use Bitrix\Sale\Cashbox\Check;
use Bitrix\Sale\Cashbox\Errors\Error as CashboxError;
use Bitrix\Sale\Cashbox\IPrintImmediately;
use Bitrix\Sale\Result;
class TestCashbox extends Cashbox implements IPrintImmediately
{
public function buildCheckQuery(Check $check): array
{
$data = $check->getDataForCheck();
return [
'check_id' => (int)$check->getField('ID'),
'total' => (float)$data['total_sum'],
'currency' => (string)$data['currency'],
'items' => $data['items'],
];
}
public function printImmediately(Check $check): Result
{
$query = $this->buildCheckQuery($check);
$providerResponse = $this->sendTestRequest($query);
return static::applyCheckResult($providerResponse);
}
public function buildZReportQuery($id): array
{
return [];
}
public static function getName(): string
{
return 'Тестовая касса';
}
public static function getFfdVersion(): ?float
{
return 1.05;
}
protected static function extractCheckData(array $data): array
{
$result = [
'ID' => (int)$data['check_id'],
];
if (($data['status'] ?? '') !== 'done')
{
$result['ERROR'] = [
'TYPE' => CashboxError::TYPE,
'MESSAGE' => (string)($data['error'] ?? 'Ошибка тестовой кассы'),
];
return $result;
}
$result['EXTERNAL_UUID'] = (string)$data['external_id'];
$result['LINK_PARAMS'] = $data['link_params'] ?? [];
return $result;
}
private function sendTestRequest(array $query): array
{
// Имитирует ошибочный HTTP-ответ по управляемому признаку окружения
if (getenv('LOCAL_CASHBOX_TEST_FAIL') === '1')
{
return [
'check_id' => $query['check_id'],
'status' => 'error',
'error' => 'Тестовая ошибка провайдера',
];
}
// Имитирует успешный HTTP-ответ
return [
'check_id' => $query['check_id'],
'status' => 'done',
'external_id' => 'test-' . $query['check_id'],
'link_params' => [],
];
}
}
Подготовить рабочую интеграцию
Метод sendTestRequest() заменяет реальный HTTP-клиент во время теста. В рабочем обработчике замените его вызовом API провайдера и сохраните разделение этапов: buildCheckQuery() формирует запрос, транспорт возвращает ответ, а extractCheckData() преобразует ответ в формат модуля.
В рабочую интеграцию также добавьте:
-
авторизацию, подпись, тайм-ауты и правила повторов провайдера,
-
проверку HTTP-кода, структуры ответа и обязательных полей,
-
ICheckable::check(), если провайдер завершает фискализацию асинхронно, -
реальные параметры ссылки на чек в
LINK_PARAMS, -
настройки подключения и безопасное хранение реквизитов.
Версию ФФД задает метод Cashbox::getFfdVersion(): ?float. Укажите версию, которую поддерживает провайдер и реализует обработчик. Пример использует версию 1.05. Не копируйте ее без проверки API сервиса.
Если кассовый сервис не печатает Z-отчеты, верните из buildZReportQuery() пустой массив, как в примере. Для асинхронного Z-отчета дополнительно разберите ответ в extractZReportData() и передайте данные провайдера в applyZReportResult().
Если сервис отправляет результат асинхронно, передайте его в ProviderCashbox::applyCheckResult($data), где ProviderCashbox — класс рабочей кассы. Базовый метод вызовет extractCheckData(), который нормализует результат, а затем передаст его в CheckManager::savePrintResult().
Метод extractCheckData() должен вернуть идентификатор локального чека в ключе ID. При успехе добавьте внешний идентификатор и параметры ссылки на чек, если их возвращает сервис. Ошибку провайдера преобразуйте в структуру ошибки модуля, иначе чек может остаться в состоянии ожидания печати без понятной причины.
Добавьте интерфейсы в зависимости от функций сервиса:
|
Интерфейс |
Когда нужен |
|
|
Обработчик отправляет чек сразу после его создания. Реализуйте |
|
|
Сервис позволяет запрашивать состояние чека, который касса отправила ранее. Реализуйте |
Не добавляйте интерфейс исключительно ради регистрации класса. Если обработчик объявляет IPrintImmediately, модуль вызывает printImmediately() при создании чека. Если объявляет ICheckable, модуль может вызывать check() из агента или административного интерфейса. Каждый метод должен возвращать результат операции и передавать ошибки через объект результата.
Зарегистрировать обработчик
Зарегистрируйте обработчик через событие OnGetCustomCashboxHandlers. Обработчик события возвращает массив. Ключ массива содержит полное имя класса, а значение — путь к файлу.
use Bitrix\Main\EventManager;
use Bitrix\Main\EventResult;
use Local\Sale\Cashbox\TestCashbox;
if (getenv('LOCAL_CASHBOX_TEST_ENABLED') === '1')
{
EventManager::getInstance()->addEventHandler(
'sale',
'OnGetCustomCashboxHandlers',
static function (): EventResult
{
return new EventResult(
EventResult::SUCCESS,
[
TestCashbox::class =>
'/local/php_interface/lib/sale/cashbox/testcashbox.php',
],
'sale'
);
}
);
}
После регистрации создайте настройку кассы в административном интерфейсе и выберите новый обработчик. Код регистрирует класс, но не создает запись кассы и не заполняет реквизиты подключения. Базовые обязательные настройки включают название, адрес электронной почты и обработчик. Поля конкретного провайдера можно объявить в методе getSettings($modelId = 0), который переопределяет обработчик.
Проверить обработчик локально
Класс Cashbox не задает универсальный тестовый режим для произвольного провайдера. Если провайдер предоставляет песочницу, используйте ее URL и отдельные тестовые реквизиты. Для проверки логики без сети используйте тестовый обработчик только на локальной или тестовой установке.
Порядок проверки:
-
Сохраните
TestCashboxв файле, путь к которому обработчик события возвращает при регистрации. -
Задайте
LOCAL_CASHBOX_TEST_ENABLED=1в конфигурации окружения PHP только на тестовой установке. -
Зарегистрируйте и активируйте тестовую кассу. Отключите другие кассы, которые подходят по ограничениям, чтобы модуль выбрал именно тестовый обработчик.
-
Создайте тестовый заказ и оплату. Установите для оплаты признак
PAID = Yи сохраните заказ. -
Сформируйте чек через
CheckManager::addByType()и повторно прочитайте его черезCheckManager::getCheckInfo($payment). -
Для успешного ответа убедитесь, что статус стал
Y,EXTERNAL_UUIDначинается сtest-, аERROR_MESSAGEпуст. -
Задайте
LOCAL_CASHBOX_TEST_FAIL=1, создайте новый тестовый чек и убедитесь, что его статус сталE, аERROR_MESSAGEсодержитТестовая ошибка провайдера.
Не регистрируйте тестовый обработчик на рабочей установке. Он переводит чек в успешное состояние без реальной фискализации. После теста отключите и удалите настройку тестовой кассы, а также сбросьте признаки LOCAL_CASHBOX_TEST_ENABLED и LOCAL_CASHBOX_TEST_FAIL.
Использовать события касс и чеков
События модуля sale изменяют подготовленные данные, регистрируют классы и расширяют жизненный цикл чека.
|
Событие |
Момент вызова и результат |
|
|
После сборки данных конкретного чека. Обработчик получает массив данных и тип чека, изменяет массив и возвращает его |
|
|
При формировании списка обработчиков касс. Возвращает массив: ключ содержит полное имя класса, а значение — путь к файлу |
|
|
При формировании списка типов чеков, которые поддерживает модуль. Результат содержит соответствие класса пользовательского чека и пути к файлу |
|
|
При формировании списка классов ограничений касс. Используйте для собственного ограничения по сайту, компании или другому признаку проекта |
|
|
До автоматического создания чеков. Обработчик получает |
|
|
После сопоставления документов, перед |
|
|
После неуспешного |
|
|
После успешного сохранения результата печати. Позволяет самостоятельно отправить покупателю ссылку или данные чека |
|
|
После сохранения окончательной ошибки печати. Параметры содержат данные результата после нормализации, включая структуру |
Событие OnGetCustomCheckList только регистрирует классы пользовательских типов. Успешный результат события должен содержать массив, как у OnGetCustomCashboxHandlers. Ключ содержит полное имя класса, а значение — путь к файлу. Класс должен наследовать Bitrix\Sale\Cashbox\Check, возвращать уникальный строковый тип через getType() и реализовать статические методы getCalculatedSign() и getName().
Базовый Check поддерживает оплату. Для другого основного документа переопределите getSupportedEntityType(), а для документов, которые относятся к основному, — getSupportedRelatedEntityType().
Не регистрируйте пустой класс ради нового названия. Базовый Check собирает товары и расчеты, но пользовательский тип отвечает за корректный фискальный признак и допустимые документы. Если стандартный класс или событие OnCheckCollateDocuments решают задачу, используйте их.
Событие OnPrintableCheckSend не отправляет чек в кассу. Оно только уведомляет покупателя после успешной фискализации. В актуальном вызове доступны именованные параметры:
-
PAYMENT— объект оплаты илиnull, -
SHIPMENT— объект отгрузки илиnull, -
CHECK— снимок полей записи чека, который модуль загрузил до сохранения окончательного результата печати.
Если обработчику нужны актуальные STATUS и LINK_PARAMS, повторно получите чек через CheckManager::getLastPrintableCheckInfo() по оплате или отгрузке из параметров события.
Если хотя бы один обработчик вернул EventResult::SUCCESS, модуль фиксирует обработку уведомления и не запускает стандартную отправку. Возвращайте успешный результат только после того, как собственный канал действительно принял уведомление.
use Bitrix\Main\Event;
use Bitrix\Main\EventManager;
use Bitrix\Main\EventResult;
$notificationSender = static function (
$payment,
$shipment,
array $check
): bool
{
// Замените на вызов собственного сервиса уведомлений
return false;
};
EventManager::getInstance()->addEventHandler(
'sale',
'OnPrintableCheckSend',
static function (Event $event) use ($notificationSender): EventResult
{
$payment = $event->getParameter('PAYMENT');
$shipment = $event->getParameter('SHIPMENT');
$check = $event->getParameter('CHECK');
$notificationSent = $notificationSender(
$payment,
$shipment,
$check
);
return new EventResult(
$notificationSent
? EventResult::SUCCESS
: EventResult::ERROR
);
}
);
Пример показывает параметры и результат события. Перед успешным результатом проверьте ответ сервиса уведомлений. Если отправка завершилась ошибкой, не подавляйте стандартное уведомление.
Обработать ошибку печати
Проверяйте ошибку на двух уровнях:
-
Resultпри создании или повторной печати сообщает, удалось ли запустить операцию, -
запись чека хранит итоговое состояние и
ERROR_MESSAGE.
use Bitrix\Sale\Cashbox\CheckManager;
$checkId = 789;
$result = CheckManager::reprint($checkId);
if (!$result->isSuccess())
{
foreach ($result->getErrorMessages() as $message)
{
echo $message . "\n";
}
}
Метод reprint() не печатает повторно чек со статусом Y и не запускает второй запрос, пока чек ожидает завершения печати. Для чека коррекции действует отдельный сценарий.
Касса с интерфейсом ICheckable может сообщить результат позже. Состояние таких чеков обновляет агент Bitrix\Sale\Cashbox\Manager::updateChecksStatus(). Менеджер также может запросить состояние чека из административного интерфейса.
Статус P не подтверждает фискализацию. Модуль не задает универсальный срок ожидания для этого статуса. Срок и порядок восстановления зависят от кассового сервиса.
Для чеков в состоянии P:
-
контролируйте работу агента,
-
сверяйте состояние по внешнему идентификатору,
-
задайте в проекте порог оповещения для чеков, которые остаются в этом состоянии дольше допустимого срока.
Не меняйте P на E через ORM и не создавайте новый чек взамен прежнего, пока провайдер не подтвердит, что касса не фискализировала первый запрос.
При окончательной ошибке CheckManager:
-
переводит чек в состояние
E, -
записывает текст в
ERROR_MESSAGE, -
отмечает оплату или отгрузку, к которой относится чек, как проблемную,
-
сохраняет маркер в заказе,
-
добавляет запись в журнал касс,
-
вызывает событие
OnCheckPrintError.
Записать ошибку в журнал
Собственный обработчик может писать диагностические сообщения через Bitrix\Sale\Cashbox\Logger.
use Bitrix\Sale\Cashbox\Logger;
$cashboxId = 7;
Logger::addError(
'Кассовый сервис отклонил запрос',
$cashboxId
);
Не записывайте в журнал полный запрос без фильтрации. В нем могут находиться телефон, адрес электронной почты, реквизиты подключения и фискальные данные.
Для диагностики проверьте:
-
Состояние и
ERROR_MESSAGEзаписи чека. -
Активность и доступность кассы.
-
Соответствие компании, платежной системы и ограничений кассы.
-
Наличие обязательных данных покупателя, товаров, НДС и способа расчета.
-
Журнал касс и ответ внешнего сервиса.
-
Работу агента проверки состояния, если сервис отвечает асинхронно.
Учитывать права и требования законодательства
Методы чтения ORM не проверяют право пользователя на доступ к заказу. Перед выводом чеков в публичном разделе проверьте принадлежность заказа текущему пользователю. В административном и фоновом сценариях проверьте право модуля и полномочия процесса.
API модуля помогает сформировать, отправить и сохранить чек, но не определяет юридическую схему расчета проекта. Типы чеков, обязательные реквизиты, версия формата фискальных данных и момент передачи документа зависят от законодательства и настроек кассового сервиса.
Администратор задает настройки кассы, оператора фискальных данных, налогов и автоматической печати. Код должен получать настройки через Manager и возвращать понятные ошибки, если нет кассы, которая соответствует ограничениям.
Путь к списку чеков и настройкам касс зависит от версии продукта и состава административного меню. Используйте интерфейс для настройки и ручной диагностики. Автоматизацию, интеграцию и отчеты стройте через CheckManager, Manager и ORM-таблицы.
Проверить итоговое состояние чека
В стандартном сценарии проект меняет состояние оплаты или отгрузки и позволяет модулю автоматически сформировать чек. Для ручного сценария загрузите документ из заказа и используйте CheckManager::addByType() для известного типа или CheckManager::addChecks() для стандартного сопоставления документов. Защитите создание от параллельных повторов и не дублируйте автоматическую генерацию.
Завершение чека подтверждает только состояние Y. Успешное создание записи или отправка запроса не подтверждают фискализацию. При ошибке сохраните контекст операции, проверьте ERROR_MESSAGE и журнал касс, затем повторите печать через CheckManager::reprint() после устранения причины.