feat(payments): report income to «Мой налог»
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Failing after 24s
CI / ui (pull_request) Successful in 1m17s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Failing after 24s
CI / ui (pull_request) Successful in 1m17s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
The direct rail runs on НПД, where the provider neither files with the tax service nor issues a receipt — so nobody was doing it. This registers each rouble purchase, annuls its receipt on a refund, and hands the buyer the receipt by email. Two properties of the (unofficial) lknpd API shape the design. Registering an income takes no idempotency key, so an error does not mean nothing happened: the service name is frozen before the call and carries a marker from the tail of the order id, and after a failure the taxpayer's income list is searched for that exact name. Found means filed; not found halts the queue for a human, because declaring an income twice is as wrong as not declaring it. And faults are classified rather than logged: a token is renewed silently, a throttle backs off, an outage retries, but three unfixable rejections take the rail out of service — a changed format must not become thousands of requests overnight. The console button and the worker share one RunBatch. Automatic mode is armed from the console, not from configuration, so the operator can watch a run go through by hand first. A daily watchdog runs whether or not it is armed, since the case it exists for is the export being off. An idle queue issues no call at all — not even an authentication. No payment path changed: the purchase letter rides the existing payment-event outbox on its own cursor, the receipt and annulment letters ride the export row. Decisions D53-D60.
This commit is contained in:
@@ -345,9 +345,68 @@ web+PWA / native Android+iOS через Capacitor). Владелец (самоз
|
||||
(тег 1212), способ расчёта `full_payment` (тег 1214), доставка на email-якорь D36,
|
||||
`tax_system_code` не шлём. Возврат к чекам предсказуем: у НПД годовой потолок дохода 2,4 млн ₽, и
|
||||
его превышение возвращает 54-ФЗ — тогда это правка переменной, а не кода.
|
||||
Связка с «Мой налог» — **отдельная задача**. Всё нужное для неё уже хранится (идентификатор заказа
|
||||
и платежа провайдера, сумма, валюта, время зачисления, снимок пакета, возвраты); не хватает только
|
||||
отметки «операция уже отправлена» для идемпотентности выгрузки — её заводит та задача.
|
||||
Связка с «Мой налог» — **отдельная задача**, закрыта решениями D53-D60 ниже.
|
||||
|
||||
## Выгрузка в «Мой налог» (интервью с владельцем, 2026-07-28)
|
||||
|
||||
- **D53. Охват выгрузки — только рубли ЮKassa.** Выгружаем `kind='fund'`, `provider='yookassa'`,
|
||||
валюта `RUB`. VK-Голоса и Telegram Stars вне охвата **не по решению, а по факту**: журнал операций
|
||||
хранит их цену в `VOTE`/`XTR` и рублёвой суммы не знает вообще — она приходит выплатой платформы за
|
||||
вычетом её комиссии. Плюс VK — юрлицо, то есть ставка 6% и ИНН плательщика в чеке, а это другой тип
|
||||
чека. Stars владелец не выводит, поэтому спор о моменте признания дохода по ним не открываем.
|
||||
Доход от rewarded-рекламы (`vk_ads`) денег в журнале не имеет и тоже вне охвата.
|
||||
- **D54. Пароль вводится, хранится только refresh-токен, зашифрованный.** Логин (ИНН) и пароль
|
||||
вводятся в форме `/_gm/mynalog`, используются один раз и **никуда не пишутся**; на диск ложится
|
||||
только выданный refresh-токен, запечатанный AES-GCM на `BACKEND_MYNALOG_KEY`. Ключ — наши
|
||||
собственные случайные 32 байта, к учётным данным налоговой отношения не имеющие; он нужен потому,
|
||||
что refresh-токен — долгоживущий доступ к личному кабинету ФНС, и в дампе базы (а бэкапы уезжают
|
||||
в S3) ему открытым текстом не место. Потеря ключа не катастрофа: токен перестаёт расшифровываться,
|
||||
и консоль просит войти заново. Вариант «логин/пароль в секретах Gitea» отклонён: это пароль от
|
||||
налоговой, а не от магазина, и радиус поражения несопоставим.
|
||||
- **D55. Автоматический режим есть, но включается галочкой в консоли, а не конфигом.** Кнопка и
|
||||
воркер зовут один и тот же `RunBatch` — второй реализации нет. Автомат по умолчанию выключен:
|
||||
владелец сначала прогоняет рельс руками и убеждается, что чек выглядит правильно, и только потом
|
||||
включает. Тик — 15 минут; при пустой очереди прогон **не авторизуется** и наружу не ходит.
|
||||
Ограничение прогона — **бюджет времени** (13 минут у воркера, 100 секунд у кнопки), а не счётчик
|
||||
чеков: счётчик ничего не защищает, защищают пауза 2 секунды между вызовами и предохранители.
|
||||
- **D56. Неустановленный исход останавливает очередь.** Регистрация дохода не идемпотентна: ключа
|
||||
идемпотентности у API нет, поэтому ошибка не означает, что чек не создан. Название услуги
|
||||
замораживается в базе **до** запроса и несёт маркер из **хвоста** идентификатора заказа (старшие
|
||||
разряды UUIDv7 — миллисекундные часы, одинаковые у всех заказов одной минуты, и такой маркер сделал
|
||||
бы две покупки неразличимыми). После сбоя ищем чек по этому точному названию: нашли — записываем,
|
||||
не нашли — статус «неизвестно», и **новые чеки не отправляются**, пока человек не разберёт.
|
||||
Аннулирования при этом продолжают идти: они только убирают доход. Один прогон за раз — advisory-лок
|
||||
Postgres; строка, застрявшая в `sending` после падения процесса, под локом заведомо осиротевшая и
|
||||
переводится в «неизвестно», а не переотправляется.
|
||||
- **D57. Сбои классифицируются на пять классов.** `401` — молча переавторизуемся (запрос отклонён до
|
||||
обработки, поэтому единственный сбой, который можно повторить без зонда); `429` — отступаем; `5xx`
|
||||
и таймаут — мягко, письмо только если сервис не поднялся за сутки; прочие `4xx` — жёстко, и три
|
||||
подряд снимают рельс с эксплуатации с письмом, потому что смена формата API отклоняет всё подряд и
|
||||
без этого превратилась бы в тысячи запросов за ночь; превышение годового лимита НПД выделено
|
||||
отдельным классом (оно подчиняет предыдущий, чтобы проверка «это неисправимо?» его не пропустила) —
|
||||
это уже не техническая ошибка, а потеря режима.
|
||||
- **D58. Сумма — полная, дата — фактическая, пояс — налогоплательщика.** В чек идёт то, что заплатил
|
||||
покупатель: на НПД расходы не вычитаются, комиссия ЮKassa — расход владельца. Время операции — это
|
||||
момент платежа, а не момент выгрузки, и отправляется в поясе `BACKEND_MYNALOG_TZ` (по умолчанию
|
||||
`Europe/Moscow`), потому что смещение решает, в какой налоговый месяц попадёт околополуночный
|
||||
платёж. Название услуги: `Внутриигровая валюта: "Фишка", NN шт. (ID: xxxxxxxx)`, не длиннее 128
|
||||
символов — при усечении жертвуем описанием, маркер неприкосновенен.
|
||||
- **D59. Надзиратель за фискальным периодом работает всегда.** Отдельный суточный цикл, независимый
|
||||
от автоматического режима — именно потому, что случай, ради которого он существует, это выключенный
|
||||
или сломавшийся автомат. Эскалация по незакрытому прошлому месяцу: 1-е число — предупреждение, с
|
||||
5-го — тревога ежедневно, с 9-го — «срок вышел». Правовая рамка: ст. 14 ч. 3 ФЗ-422 даёт отсрочку
|
||||
до 9-го числа только для расчётов, **не связанных** с электронными средствами платежа; оплата
|
||||
картой — ЭСП, поэтому строгое прочтение требует чек в момент расчёта. Автоматический режим
|
||||
удовлетворяет обеим трактовкам, поэтому спор решать не потребовалось.
|
||||
- **D60. Покупателю уходят три письма, и ни одно не трогает платёжный путь.** Подтверждение покупки
|
||||
(сразу, с прямой оговоркой, что это **не** чек и что чек придёт отдельно), фискальный чек ссылкой
|
||||
после регистрации и уведомление об аннулировании после возврата — иначе у покупателя осталась бы
|
||||
ссылка на мёртвый чек. Только по-русски, локализация не нужна. Первое письмо едет по уже
|
||||
существующему outbox'у `payments.payment_events` на собственном курсоре `mailed_at`, два других — по
|
||||
колонкам строки выгрузки; поэтому в коде зачисления и возврата не потребовалось менять ни строки, а
|
||||
сбой релея откладывает письмо, а не теряет его. Адрес — подтверждённый email-якорь D36, который
|
||||
прямая покупка требует и так. Возврат, случившийся раньше выгрузки, закрывается как `not_required`:
|
||||
ни чека, ни аннулирования, ни обращения к сервису.
|
||||
|
||||
## Заметки к оформлению документов
|
||||
|
||||
@@ -363,9 +422,10 @@ web+PWA / native Android+iOS через Capacitor). Владелец (самоз
|
||||
ведёт к пропускам. Текст — только для фиксации решённого и пояснений. Усилить
|
||||
feedback-память `prefer-interview-mode` после plan mode.
|
||||
|
||||
## Все развилки закрыты (D1-D52)
|
||||
## Все развилки закрыты (D1-D60)
|
||||
|
||||
Интервью завершено. Дальше — оформление документов и реализация по релизам.
|
||||
D53-D60 добавлены отдельным интервью 2026-07-28 по выгрузке в «Мой налог».
|
||||
|
||||
**Дополнение 2026-07-14 (интервью с владельцем).** D41 ревизована (владелец переходит на
|
||||
ИП: 54-ФЗ через облачную кассу Robokassa вместо авточека НПД); добавлены D42-D44 — сплит
|
||||
|
||||
Reference in New Issue
Block a user