01
Начните с одной завершённой продажи
Фитнес-клуб продаёт разные по смыслу позиции: абонемент, персональную тренировку, аренду шкафчика, товар бара, подарочную карту и дополнительную услугу. Для клиента это одна корзина, но бухгалтерский и налоговый смысл строк различается. Поэтому фискализация должна получать уже рассчитанную продажу, а не заново собирать её из интерфейса кассира.
Источником суммы служит завершённая sale в кассовом контуре. В ней зафиксированы строки, количество, цена, скидка, валюта, способы оплаты и касса. Чек ссылается на идентификатор этой продажи. Сотрудник может перейти от чека к операции и проверить, почему итог равен конкретной сумме.
Полезный приёмочный сценарий начинается с корзины, где есть абонемент и товар бара. Добавьте скидку, разделите оплату между наличными и сертификатом, завершите продажу и сравните итог чека со строками. Повтор команды завершения обязан вернуть прежнюю sale и прежний чек, без второй фискальной операции.
- одна sale создаёт одно фискальное обязательство;
- сумма чека совпадает с суммой продажи;
- каждая строка имеет понятный источник;
- повтор запроса не создаёт дубль;
- связь sale и receipt доступна для сверки.
02
Создавайте чек в той же транзакции
Между фиксацией оплаты и постановкой чека в очередь не должно быть окна потери. Если sale стала paid, а процесс остановился до создания receipt, бухгалтер увидит деньги без фискального обязательства. Обратная ситуация тоже опасна: чек появился, хотя продажа откатилась из-за ошибки склада или членства.
В ProGym завершение продажи и создание фискального обязательства проходят в одной PostgreSQL-транзакции. Ошибка критичной проверки отменяет весь результат. Успешная транзакция сохраняет paid sale и pending receipt вместе. Внешний OFD вызывается позже через контролируемую очередь, поэтому медленная сеть провайдера не удерживает кассовую блокировку.
Идемпотентный ключ строится из типа источника, идентификатора sale и операции. Повтор после таймаута на кассе находит существующий receipt. Если клиент пытается использовать тот же ключ для другой продажи, Core возвращает conflict. Такая защита нужна и в интерфейсе, и в адаптере терминала.
03
Фиксируйте фискальные реквизиты до отправки
Название услуги, единица измерения, IKPU и налоговая ставка могут измениться в каталоге после продажи. Чек должен отражать состояние на момент расчёта. Поэтому ProGym сохраняет серверный fiscal snapshot в каждой строке sale и использует его при создании receipt.
Для абонемента и услуги фискальные атрибуты берутся из правил каталога. Для товара используются карточка номенклатуры, единица, IKPU и ставка налога. Клиентский запрос не может подменить зарезервированный snapshot. Это важно для кассовых интеграций, браузера и white-label приложения, где входные данные всегда считаются недоверенными.
Перед запуском бухгалтер составляет таблицу соответствий по реальному ассортименту клуба. В ней указываются категория, название в чеке, IKPU, единица и налоговая ставка. Сначала проверяются самые частые продажи, затем персональные услуги, аренда, сертификаты и редкие позиции. Ошибка каталога исправляется до production-отправки.
- название позиции;
- количество и единица;
- цена и скидка;
- IKPU и налоговая ставка;
- тип оплаты и итоговая сумма.
04
Распределяйте общую скидку точно
Промокод или ручная скидка часто применяется ко всей корзине. OFD при этом получает сумму по каждой строке. Простое округление долей может дать расхождение в один сум или тийин. Провайдер отклонит чек либо сохранит итог, который отличается от кассовой продажи.
ProGym распределяет общую скидку пропорционально сумме строк. Каждая промежуточная доля округляется до денежной точности, а остаток применяется к последней строке. После расчёта сумма line amount обязана точно совпасть с sale total. Этот инвариант проверяется автоматическим PostgreSQL-тестом.
На приёмке используйте суммы, которые дают неудобные дроби: три позиции разной стоимости и процентную скидку. Сравните gross, line discount, распределённую global discount и итог. Отдельно проверьте корзину из одной позиции, нулевую скидку и максимально разрешённую скидку.
05
Сохраняйте способы оплаты без лишних данных
Одна продажа может быть оплачена наличными, банковской картой, Click, Payme, подарочным сертификатом или комбинацией способов. Фискальный payload хранит тип и сумму каждой части. Их общий итог сверяется с sale total и позволяет объяснить расчёт кассовой смены.
В payload не должны попадать полный номер карты, secret сертификата, промокод, customer ID или ключ провайдера. Для фискализации достаточно денежных и товарных реквизитов. Секретные значения остаются в своих доменах, а payload чека хранится зашифрованным на сервере.
При раздельной оплате проверьте сумму каждого метода, итог receipt и отчёт смены. Если сертификат покрывает часть покупки, его внутренний код не нужен OFD. Если Click или Payme ещё не подтвердили intent, sale не должна переходить в paid и создавать успешный чек.
06
Разделяйте обязательство и ответ OFD
Статус pending означает, что клуб создал обязательство и ждёт отправки. Processing показывает взятую worker-операцию. Fiscalized подтверждает ответ провайдера и содержит безопасные внешние реквизиты. Failed сообщает об ошибке с возможностью повторить, dead требует ручного разбора, cancelled фиксирует осознанную отмену.
Пользовательский интерфейс обязан показывать эти состояния текстом. Один цвет не подходит для доступности и создаёт риск неверной трактовки. В журнале ProGym видны сумма, дата, provider, число попыток и текущий статус. Карточка раскрывает строки, способы оплаты и историю доставки.
Система не показывает успешную фискализацию до ответа OFD. Если connection отсутствует, receipt остаётся в ожидании с пометкой о настройке. Такой режим позволяет заранее включить политику кассы и увидеть накопленные обязательства, сохраняя честный статус каждой операции.
- pending: создано и ждёт отправки;
- processing: взято адаптером;
- fiscalized: подтверждено OFD;
- failed или dead: требуется повтор или разбор;
- cancelled: отменено с причиной.
07
Настраивайте OFD отдельно для каждой кассы
Сеть может использовать разные юридические лица, кассы и договоры OFD по филиалам. Политика хранится в карточке register: обязательна ли фискализация и какой provider выбран. Изменение проходит с проверкой версии, чтобы два администратора не перезаписали настройки друг друга.
Если provider указан, Core ищет активное connection с точным ключом. Случайное переключение на первый доступный OFD запрещено. При повторе receipt сохраняет исходный provider key и снова ищет именно его. Это защищает юридическую принадлежность чека после добавления нового подключения в сеть.
В staff-интерфейсе руководитель видит все кассы, флаг создания чеков и список разрешённых fiscal connections. Изменение требует права управления продажами. Provider secrets в браузер не передаются, сотрудник выбирает только публичное имя подключения.
08
Повторяйте ошибки управляемо
Внешний OFD может вернуть временную ошибку, ограничение частоты, недоступность сети или бизнес-отказ по реквизитам. Техническая ошибка получает backoff и следующую попытку. Ошибка данных переводится в состояние для ручного разбора. Бесконечный быстрый retry создаёт нагрузку и мешает увидеть первопричину.
Ручная кнопка повторения использует текущую версию receipt. Если worker уже изменил состояние, сервер вернёт conflict и предложит обновить карточку. Успешный, отменённый или находящийся в обработке чек нельзя отправить повторно. Это исключает конкурирующие команды кассира и фоновой очереди.
На FIELD-приёмке отключите sandbox OFD после создания продажи, дождитесь failed, восстановите provider и выполните retry. Проверьте число попыток, отсутствие второго receipt и финальную ссылку. Затем повторите с неверным IKPU и убедитесь, что сотрудник видит понятный отказ без ложного успеха.
09
Отмена и возврат требуют разных операций
Отмена фискального обязательства подходит для ситуации, когда receipt создан ошибочно и ещё не подтверждён провайдером. Сотрудник подтверждает действие, вводит причину, а Core сохраняет audit event. Тихое удаление строки запрещено, потому что оно разрушает связь с продажей и сверкой.
Возврат оплаченной продажи имеет другой экономический смысл. Он ссылается на исходную sale, корректирует товар, членство, ledger и платёж по согласованному правилу. Для OFD формируется отдельная операция возврата с собственным idempotency key. Успешный sale receipt не превращается задним числом в cancelled.
Пилот должен включать полный возврат, частичный возврат и отказ provider. Сумма возвратного чека сверяется с разрешёнными строками исходной продажи. Права на возврат и отмену фискального обязательства лучше разделить, а крупные суммы провести через approval.
10
Связывайте чек с кассовой сменой и сверкой
Кассовая смена отвечает за ожидаемую и фактическую сумму по методам оплаты. Fiscal journal отвечает за доставку чеков. Эти реестры связаны через sale, но имеют разные статусы. Закрытая смена не означает, что каждый provider уже ответил, а fiscalized receipt не заменяет подтверждение денег банком.
Перед закрытием дня сотрудник проверяет paid sales, возвраты, наличные движения и чеки с проблемным статусом. Бухгалтер сверяет provider reports с payment records и fiscal receipts. Drill-down позволяет перейти от итоговой суммы к конкретной sale и её строкам.
Для операционного закрытия месяца полезен автоматический чек-лист. Он показывает открытые смены, незавершённые продажи, failed receipts, неполные EDO-пакеты и расхождения склада. Каждый пункт содержит источник и количество, поэтому команда исправляет конкретные операции, а не отмечает абстрактную готовность.
11
Ограничивайте доступ и защищайте payload
Просмотр журнала, чтение payload, retry, cancel и настройка register являются разными действиями. Product manifest ProGym подключает общий fiscal operator и точные scope. Соседние продукты Taska Core не получают новые возможности автоматически. Такое разделение сохраняет совместимость OneSwim, Cline, Restora и Oborot.
Организация и tenant применяются на сервере. BFF пропускает только разрешённый root fiscal и блокирует произвольный путь к raw provider. CSRF защищает изменения из браузера, If-Match защищает конкурентное редактирование, audit фиксирует автора. Frontend не получает connection credentials и зашифрованный storage value.
Логи не должны печатать payload целиком. Для диагностики достаточно request ID, receipt ID, provider, error code и безопасного сообщения. Экспорт журнала подчиняется тем же правам, что экран. Срок хранения согласуется с бухгалтером и юридической политикой клуба.
12
Проведите sandbox и FIELD-приёмку
Программный контракт можно проверить локально с mock provider и реальной PostgreSQL. Такой тест подтверждает атомарность, точную сумму, распределение скидки, provider binding, шифрование payload, replay и отсутствие секретов. Он не подтверждает юридическую регистрацию чека у выбранного OFD.
Для FIELD-этапа клуб предоставляет договор, данные кассы, sandbox credentials и требования к чеку. Команда подключает connection, настраивает одну register и выполняет продажу услуги, товара и смешанной корзины. Проверяются успешный ответ, QR или URL чека, номер, fiscal sign и отображение в кабинете провайдера.
Затем выполняются отрицательные сценарии: неверный реквизит, недоступность provider, повтор webhook, retry после восстановления, возврат и сверка дня. Результат фиксируется по идентификаторам sale и receipt, без публикации ключей. Только после этого конкретное подключение получает production-статус.
ProGym поставляется с field acceptance runner. Он проверяет доступность канонических контуров СКУД, оплат, fiscal, EDO и mobile distribution, а также отрицательные webhook и BFF gates. Provider-specific доказательства добавляются к отчёту пилота после подключения реальных аккаунтов.
- успешная продажа и чек в OFD;
- смешанная корзина и раздельная оплата;
- ошибка provider и контролируемый retry;
- полный и частичный возврат;
- сверка sale, payment, shift и receipt.