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:
+61
-4
@@ -446,10 +446,7 @@ Stars, общая сумма по ним не значила бы ничего)
|
||||
расчёта `full_payment` (тег 1214), доставка на email-якорь D36. Переключатель нужен потому, что
|
||||
возврат к чекам предсказуем: у НПД есть годовой потолок дохода, и потеря режима возвращает 54-ФЗ.
|
||||
|
||||
Всё, что потребуется автоматической отправке в «Мой налог», уже записано — идентификатор заказа,
|
||||
идентификатор платежа провайдера, сумма и валюта, время зачисления, снимок проданного пакета и
|
||||
возвраты отдельными строками журнала, всё выгружается в CSV. Не хватать будет только отметки «эта
|
||||
операция уже отправлена» для идемпотентности самой выгрузки; ей место в той задаче, а не здесь.
|
||||
Отправка каждой рублёвой операции в «Мой налог» автоматизирована — см. ниже.
|
||||
- **VK** — VK сам процессит Голоса через налоговую; делать нечего.
|
||||
- **TG Stars** — налоговой стороны нет (для РФ-самозанятого Stars легально невыводимы = не
|
||||
доход НПД; принимаем, чек не формируем).
|
||||
@@ -457,6 +454,66 @@ Stars, общая сумма по ним не значила бы ничего)
|
||||
**ОРД** (маркировка рекламы) по VK-рекламе — на стороне VK. (Не юридическая консультация —
|
||||
владелец сверяет точную схему НПД с налоговым консультантом.)
|
||||
|
||||
### Выгрузка в «Мой налог» (D53-D60)
|
||||
|
||||
Рублёвый доход прямого рельса регистрируем в налоговой мы, потому что больше некому: провайдер при
|
||||
этом режиме ни отчёта не подаёт, ни чека не формирует. Охват — `kind='fund'`,
|
||||
`provider='yookassa'`, валюта `RUB`; магазинные рельсы вне охвата, так как журнал операций хранит их
|
||||
цену в валюте магазина и рублёвой суммы там нет вообще.
|
||||
|
||||
**Где живёт.** `backend/internal/mynalog` — клиент API (без базы), `backend/internal/mynalogsync` —
|
||||
оркестрация, `payments.mynalog_receipt` — состояние по каждому приходу, `/_gm/mynalog` — экран
|
||||
оператора. Кнопка в консоли и фоновый воркер зовут один и тот же `RunBatch`; второй реализации той
|
||||
части, которая решает, что значит неудавшаяся регистрация, сознательно не существует. Без
|
||||
`BACKEND_MYNALOG_KEY` рельс спит целиком.
|
||||
|
||||
**API неофициальный** — `lknpd.nalog.ru`, опубликованного контракта нет, тестовой среды нет. Отсюда
|
||||
два следствия, пронизывающих всю конструкцию:
|
||||
|
||||
- **Регистрация дохода не идемпотентна.** Ключа идемпотентности нет, поэтому ошибка не означает, что
|
||||
ничего не произошло. Название услуги замораживается в базе **до** запроса и несёт маркер из
|
||||
**хвоста** идентификатора заказа (старшие разряды UUIDv7 — миллисекундные часы, они одинаковы у
|
||||
всех заказов одной минуты). После сбоя список доходов ищется по этому точному названию: нашли —
|
||||
значит зарегистрировано, не нашли — **исход неизвестен**, и очередь встаёт до решения человека,
|
||||
потому что альтернативы — задвоить доход или потерять его. По той же причине один прогон за раз,
|
||||
на advisory-локе Postgres.
|
||||
- **Сбои классифицируются, а не просто логируются.** 401 обновляем молча; 429 — отступаем; 5xx и
|
||||
таймаут — повторим позже, письмо только через сутки; любой другой 4xx повтором не лечится, и три
|
||||
подряд снимают рельс с эксплуатации — смена формата не должна превращаться в тысячи запросов за
|
||||
ночь. Превышение годового лимита НПД выделено отдельно: это уже не техническая ошибка, а потеря
|
||||
режима.
|
||||
|
||||
**Время.** Каждый момент отправляется в часовом поясе налогоплательщика (`BACKEND_MYNALOG_TZ`, по
|
||||
умолчанию `Europe/Moscow`): смещение решает, в какой налоговый месяц попадёт околополуночный платёж.
|
||||
В чеке стоит дата поступления денег, а не дата выгрузки. Автоматический режим просыпается раз в 15
|
||||
минут и при пустой очереди не делает ничего — в том числе не авторизуется. Отдельный суточный
|
||||
надзиратель работает **независимо** от автоматического режима и эскалирует незакрытый прошлый месяц
|
||||
1-го, 5-го и 9-го числа. (Ст. 14 ч. 3 ФЗ-422 даёт отсрочку до 9-го только для расчётов, **не**
|
||||
связанных с электронными средствами платежа; оплата картой — ЭСП, поэтому чек строго в момент
|
||||
расчёта. Автоматический режим удовлетворяет обеим трактовкам.)
|
||||
|
||||
**Ручной фолбэк.** `/_gm/mynalog.csv` отдаёт ровно то, что было бы отправлено — время, название
|
||||
услуги, сумму — плюс идентификатор строки журнала, чтобы номер выданного вручную чека можно было
|
||||
внести обратно на странице. Именно этот номер оставляет ручной приход аннулируемым при возврате,
|
||||
поэтому спрашивается он, а не галочка «сделано».
|
||||
|
||||
**Возвраты.** У налоговой нет понятия возврата: аннулирование чека **и есть** возврат. Возврат,
|
||||
пришедший раньше, чем приход был выгружен, закрывается локально как `not_required` — ничего не
|
||||
регистрируем, ничего не аннулируем, наружу не ходим. Частичный возврат вне охвата с обеих сторон
|
||||
(см. раздел про возвраты).
|
||||
|
||||
**Покупатель узнаёт трижды**, по-русски, из долговечных очередей, а не из платёжного пути:
|
||||
подтверждение покупки (в котором прямо сказано, что это **не** фискальный чек и что чек придёт
|
||||
отдельно), сам чек со ссылкой на печатную форму и — после возврата — уведомление об аннулировании.
|
||||
Первое едет по существующему outbox'у `payments.payment_events` на собственном курсоре `mailed_at`,
|
||||
два других — по колонкам строки выгрузки. Адрес — тот самый подтверждённый email-якорь D36, который
|
||||
прямая покупка требует и так.
|
||||
|
||||
**Учётные данные.** Логин и пароль от кабинета вводятся в консоли, используются один раз и нигде не
|
||||
хранятся: на диск кладётся только полученный refresh-токен, запечатанный AES-GCM на
|
||||
`BACKEND_MYNALOG_KEY` (наши собственные случайные 32 байта, а не секрет налоговой) — чтобы он не
|
||||
уезжал в дампе базы. Потеря ключа — неудобство, а не потеря: оператор входит заново.
|
||||
|
||||
## 13. Дистрибуция (native Android)
|
||||
|
||||
- **RuStore** — внешний платёжный гейт разрешён (0%); native = чистый контекст `direct`.
|
||||
|
||||
Reference in New Issue
Block a user