fiscalgate — оплати у фіскальні чеки Checkbox, рівно один раз
Огляд
Незалежний проєкт: сервіс на Go, що перетворює підтверджені оплати Monobank і LiqPay (та накладений платіж) на фіскальні чеки ПРРО Checkbox, оформлює повернення чеком повернення, а зміну відкриває й закриває (Z-звіт) за розкладом.
Результат: 300 замовлень × 4 одночасні копії кожного вебхука дають рівно 300 чеків; загублена відповідь реєстратора ніколи не подвоює чек; передоплата плюс накладений платіж стає одним чеком з двома формами оплати.
Справжня складність
Видати фіскальний чек — це один HTTP-запит. Зробити це правильно — ні: провайдери повторюють вебхуки, доки не побачать 2xx; дві копії можуть летіти одночасно; відповідь реєстратора може загубитися вже після прийняття чека; реєстратор може бути недоступний; частина замовлень оплачується двома способами. Кожен випадок має закінчитися рівно одним чеком — ні нулем, ні двома.
Архітектура

Перевірений вебхук записує платіж під унікальним ключем (провайдер, id платежу); коли замовлення оплачене повністю, одне фіскальне завдання ставиться в чергу в тій самій транзакції. Воркер відкриває зміну на вимогу, надсилає чек із нашим UUID, опитує до статусу DONE й відправляє клієнту посилання на чек.
Ключові інженерні рішення
| Рішення | Чому |
|---|---|
| Унікальний ключ платежу + завдання в транзакції, що завершує замовлення | Дублікати й одночасні вебхуки — no-op за конструкцією, а не за таймінгом |
| Власний UUID як id чека; перед повтором — пошук | Якщо відповідь загубилась, повтор знаходить чек, а не створює другий |
Експоненційний backoff, видимий стан failed, ендпоінт requeue |
Збій відкладає чек, але не губить; завдання, що здалося, видно з причиною |
| Чек лише коли оплачено рівно повністю | Передоплата карткою + накладений платіж = один чек з двома формами оплати; переплата чекає на людину |
| Повернення обмежені продажем, спершу картка, потім готівка, ідемпотентні за ключем | Чек повернення не перевищить проданого й не поверне двічі |
| Зміна закривається після щоденної межі, лише коли немає чеків у роботі | Z-звіт ніколи не потрапляє посеред продажу |
Результати
| Перевірка | Результат |
|---|---|
| Паралельність | 300 замовлень × 4 одночасні копії вебхука → рівно 300 чеків |
| Збій реєстратора | 3 невдалі спроби → повтор із backoff → один чек |
| Загублена відповідь після успішного POST | один POST, один чек |
| Тести | 26 під детектором гонок, покриття 68–82% по пакетах |
Підключення до бойових систем
- Checkbox:
checkbox.base_url,login,password,license_key(${VAR}читає з оточення). - Monobank / LiqPay: токен чи приватний ключ і зареєстровані callback-адреси
/webhooks/monobank,/webhooks/liqpay. - Ваш магазин:
POST /api/ordersпри створенні замовлення,…/cod-paidпри отриманні,…/refundдля повернень. - Інший реєстратор чи еквайринг: один тип, що реалізує
service.Fiscal, або один парсер, що повертаєacquiring.Payment.
Код: github.com/valpere/fiscalgate
Стек
Go · Checkbox API · Monobank · LiqPay · SQLite · Docker