feat(payments): send no fiscal receipt; keep the code switchable
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 21s
CI / ui (pull_request) Successful in 1m28s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m45s

The merchant accepts payments as a sole proprietor on НПД, which is outside
54-ФЗ: there is no online cash register, YooKassa does not serve receipts for
that regime at all (checked with their support), and each operation is reported
by the merchant to «Мой налог», which issues the чек. So no `receipt` is sent
with a payment or a refund.

This also removes a failure class rather than just code: a malformed receipt
was an API error at payment creation, which broke the purchase outright.

The fiscal code is kept dormant rather than deleted. All of it now sits behind
one switch, `BACKEND_YOOKASSA_VAT_CODE`: unset — the default — builds and sends
nothing; a 54-ФЗ rate code turns «Чеки от ЮKassa» back on unchanged. The return
is foreseeable, which is why the switch exists: НПД carries an annual income
ceiling, and losing the regime puts 54-ФЗ back in force, at which point this is
a deploy-variable edit instead of writing the integration again.

The D36 email anchor still gates a direct purchase. It had two justifications —
a recovery anchor and the receipt address — and only the second is gone; without
an email a paying customer who loses the account loses the chips with it. What
was missing is that the rule was enforced but never communicated: the wallet
showed the packs to a player signed in through VK or Telegram in a browser, and
tapping Buy produced a bare "something went wrong". It now says "add an email in
your profile" in the buy tab instead, linking to the profile; spending
already-earned chips is untouched. The predicate is a pure function so it is
covered by the node-env unit tests rather than needing a browser.

Tests: the receipt-off default is pinned by an integration test asserting a
purchase carries no receipt, and the dormant path by one that switches a VAT
code on and checks the receipt reappears with the right fiscal attributes;
plus unit coverage for the enable predicate and the wallet's email rule.

A note for whoever runs the numbers next: the shared (svelte + i18n) chunk is
now 40 bytes under its 31 KB gzip budget.

Decision D51 revised.
This commit is contained in:
Ilia Denisov
2026-07-28 11:40:59 +02:00
parent 395a307eca
commit 12e616ceae
19 changed files with 288 additions and 90 deletions
+19 -9
View File
@@ -411,15 +411,25 @@ spends, grants, refunds, full history — as an extension of the existing user c
Receipts are automatic **through the provider**, and differ by rail:
- **YooKassa** (direct) — a 54-ФЗ fiscal receipt through **«Чеки от ЮKassa»**: YooKassa owns the
cash register, the fiscal drive and the OFD contract, and files with the tax authority. Unlike the
retired rail's cabinet-side receipts, it registers one **only if the request carries it** (D51), so
every payment and every refund sends a `receipt`: one line (the pack title, quantity 1, the amount)
with the VAT rate code (54-ФЗ tag 1199, a deploy variable — `1` = «Без НДС» for УСН/ПСН), the
settlement subject `service` (tag 1212) and the settlement method `full_payment` (tag 1214).
Delivery is by email only, to the D36 confirmed anchor. `tax_system_code` is not sent — YooKassa
ignores it for this solution. A malformed receipt is an API error at payment creation, so it blocks
the purchase loudly rather than silently skipping the fiscal document.
- **YooKassa** (direct) — **no receipt is sent, by design (D51 rev)**. The merchant operates on
**НПД**, which is outside 54-ФЗ: there is no online cash register and the provider registers
nothing. Each operation is reported by the merchant to **«Мой налог»**, which issues the чек.
YooKassa does not serve receipts for this regime at all.
The fiscal code is **kept dormant**, not deleted: «Чеки от ЮKassa» (YooKassa owning the cash
register, the fiscal drive and the OFD contract) registers a receipt **only if the request carries
it**, and that request-building lives behind one switch — `BACKEND_YOOKASSA_VAT_CODE`. Unset, no
`receipt` is built or sent; set to a 54-ФЗ rate code (tag 1199) it goes back to sending one line
(pack title, quantity 1, amount) with the settlement subject `service` (tag 1212) and method
`full_payment` (tag 1214), delivered by email to the D36 anchor. That switch exists because the
return is foreseeable: НПД carries an annual income ceiling, and losing the regime puts 54-ФЗ back
in force.
Everything an automated «Мой налог» submission needs is already recorded — the order id, the
provider payment id, the amount and currency, the credit time, the sold-pack snapshot, and refunds
as their own ledger rows, all exportable as CSV. The one thing it will additionally need is a
per-operation "already reported" marker for its own idempotency; that belongs to the submission
work, not here.
- **VK** — VK processes Votes through the tax authority itself; nothing to do.
- **TG Stars** — no tax side (for a RU self-employed, Stars are not legally withdrawable = not
НПД income; accepted, no receipt issued).
+19 -10
View File
@@ -323,16 +323,20 @@ web+PWA / native Android+iOS через Capacitor). Владелец (самоз
**Частичный возврат не записываем вовсе** — движок по устройству работает только с полной суммой
заказа, а непроизвольного способа решить, скольких Фишек стоит часть возврата, нет; вместо записи —
громкий лог для оператора.
- **D51. Фискализация — «Чеки от ЮKassa», чек отправляем из кода.** Ревизия D41: у ЮKassa нет
кабинетного «обобщённого чека», как у Robokassa, — чек регистрируется **только если запрос его
несёт**, поэтому itemized-код, от которого отказались в D41, возвращается в объём. Каждый платёж и
возврат шлют `receipt`: одна позиция (название пакета, количество 1, сумма), код ставки НДС
(тег 1199), признак предмета расчёта `service` (тег 1212), признак способа расчёта `full_payment`
(тег 1214); доставка только на email — на подтверждённый якорь D36. Код ставки НДС — **переменная
деплоя** (дефолт `1` = «Без НДС» для УСН/ПСН): это единственный реквизит, который реально меняется
(с 1 января 2026 ставки выросли, появились коды 11/12), и менять его без релиза нужно; признаки
предмета и способа расчёта — константы в коде. `tax_system_code` не шлём: для «Чеков от ЮKassa»
провайдер его игнорирует.
- **D51 (ревизия). Чеки не передаём: владелец на НПД, это вне 54-ФЗ.** Уточнено у поддержки ЮKassa:
при НПД провайдер с чеками не работает. Онлайн-кассы нет, `receipt` в запросах не отправляется, о
каждой операции владелец сообщает в **«Мой налог»**, он и формирует чек. Побочный выигрыш: исчезает
целый класс отказов — некорректный `receipt` был ошибкой API прямо при создании платежа, то есть
ломал покупку.
**Код чеков при этом консервируем, а не удаляем** (решение владельца): вся сборка `receipt` живёт за
одним переключателем `BACKEND_YOOKASSA_VAT_CODE` — пусто (дефолт) значит «не отправлять», код ставки
по 54-ФЗ (тег 1199) возвращает прежнее поведение: одна позиция, признак предмета расчёта `service`
(тег 1212), способ расчёта `full_payment` (тег 1214), доставка на email-якорь D36,
`tax_system_code` не шлём. Возврат к чекам предсказуем: у НПД годовой потолок дохода 2,4 млн ₽, и
его превышение возвращает 54-ФЗ — тогда это правка переменной, а не кода.
Связка с «Мой налог» — **отдельная задача**. Всё нужное для неё уже хранится (идентификатор заказа
и платежа провайдера, сумма, валюта, время зачисления, снимок пакета, возвраты); не хватает только
отметки «операция уже отправлена» для идемпотентности выгрузки — её заводит та задача.
## Заметки к оформлению документов
@@ -371,6 +375,11 @@ D47-D51 — консервация Robokassa с откатом по кредам
добавлена D52 — обработка `refund.succeeded`, потому что кабинет ЮKassa позволяет вернуть деньги
мимо нашего API. Обе правки вошли в тот же PR, что и переход на рельс.
**Уточнение по фискализации (владелец, 2026-07-28).** D51 ревизована: владелец принимает платежи как
ИП на **НПД**, при котором ЮKassa с чеками не работает (подтверждено поддержкой), поэтому `receipt`
не передаётся вовсе, а отчётность идёт через «Мой налог». Код чеков законсервирован за переменной
`BACKEND_YOOKASSA_VAT_CODE` на случай потери режима. Правка вошла в тот же PR.
## План внедрения (черновик PLAN.md — «слоями»)
Владелец выбрал слоёную стратегию: сначала вся механика без реальных денег (обкатка
+17 -9
View File
@@ -405,15 +405,23 @@ in-process кэш сегментов и бенефитов по ключу-ак
Чеки формируются автоматически **на стороне провайдера** и отличаются по каналу:
- **ЮKassa** (direct) — фискальный чек по 54-ФЗ через **«Чеки от ЮKassa»**: касса, фискальный
накопитель, договор с ОФД и отправка в налоговую — на стороне ЮKassa. В отличие от кабинетных чеков
прежнего рельса, чек регистрируется **только если запрос его несёт** (D51), поэтому каждый платёж и
каждый возврат отправляют `receipt`: одна позиция (название пакета, количество 1, сумма) с кодом
ставки НДС (тег 54-ФЗ 1199, переменная деплоя — `1` = «Без НДС» для УСН/ПСН), признаком предмета
расчёта `service` (тег 1212) и признаком способа расчёта `full_payment` (тег 1214). Доставка только
на email — на подтверждённый якорь D36. `tax_system_code` не передаём: для этого решения ЮKassa его
игнорирует. Некорректный чек — ошибка API при создании платежа, то есть покупка ломается громко, а
не тихо остаётся без фискального документа.
- **ЮKassa** (direct) — **чек не передаём, так задумано (ревизия D51)**. Владелец работает на
**НПД**, а это вне 54-ФЗ: онлайн-кассы нет, провайдер ничего не регистрирует. О каждой операции
владелец сообщает в **«Мой налог»**, он и формирует чек. ЮKassa при таком режиме налогообложения с
чеками не работает вовсе.
Фискальный код **законсервирован, а не удалён**: «Чеки от ЮKassa» (касса, фискальный накопитель и
договор с ОФД на стороне ЮKassa) регистрируют чек **только если запрос его несёт**, и вся сборка
такого запроса спрятана за одним переключателем — `BACKEND_YOOKASSA_VAT_CODE`. Пусто — `receipt` не
собирается и не отправляется; задан код ставки по 54-ФЗ (тег 1199) — снова уходит одна позиция
(название пакета, количество 1, сумма) с признаком предмета расчёта `service` (тег 1212) и способа
расчёта `full_payment` (тег 1214), доставка на email-якорь D36. Переключатель нужен потому, что
возврат к чекам предсказуем: у НПД есть годовой потолок дохода, и потеря режима возвращает 54-ФЗ.
Всё, что потребуется автоматической отправке в «Мой налог», уже записано — идентификатор заказа,
идентификатор платежа провайдера, сумма и валюта, время зачисления, снимок проданного пакета и
возвраты отдельными строками журнала, всё выгружается в CSV. Не хватать будет только отметки «эта
операция уже отправлена» для идемпотентности самой выгрузки; ей место в той задаче, а не здесь.
- **VK** — VK сам процессит Голоса через налоговую; делать нечего.
- **TG Stars** — налоговой стороны нет (для РФ-самозанятого Stars легально невыводимы = не
доход НПД; принимаем, чек не формируем).