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:
@@ -1376,6 +1376,7 @@ link — misses the event; while an add-email confirmation is pending the client
|
||||
| Manual account block (suspension) | backend: a per-request gate refuses a blocked account on every `/api/v1/user/*` route except the block-status probe with **403 `account_blocked`**; the operator blocks/unblocks from the admin console (§11) |
|
||||
| User feedback gate | backend rejects a guest or a `feedback_banned` account from submitting; the **gateway** also rejects a guest's `feedback.submit` (the `Op.NonGuest` flag + `is_guest` from session resolve) with **`guest_forbidden`** before any backend call; attachments are served `nosniff` with a download disposition for non-images (§15) |
|
||||
| Admin authentication | a single Basic-Auth gate on `/_gm/*`, forwarded **verbatim** to the backend's server-rendered admin console (and, in the deployed contour, routing `/_gm/grafana/*` to Grafana). In the deploy the **caddy** owns this gate (§13); a local non-caddy run uses the gateway's own `GATEWAY_ADMIN_*` proxy, which the per-IP admin limiter class guards ahead of its Basic-Auth — the caddy-fronted path has no limiter (stock caddy), an accepted gap. The backend trusts the proxy (no admin principal) and guards its state-changing POSTs with a **same-origin** check — the console's CSRF defence. No operator identity is tracked |
|
||||
| Tax-cabinet credential at rest | the backend seals the «Мой налог» **refresh token** with AES-GCM under `BACKEND_MYNALOG_KEY` before storing it (`payments.mynalog_session`), so a database dump or a backup carries no usable credential. The cabinet login and password are typed into the admin console, used once and **never persisted**; an unset key leaves the whole export rail unregistered. The console's error page prints provider errors verbatim, so the client never puts credentials in an error string |
|
||||
| backend ↔ gateway ↔ validator trust | the network (only gateway may reach backend; the validator and the gateway's admin bot-link relay serve unauthenticated gRPC on the trusted internal segment) |
|
||||
| remote bot ↔ gateway (bot-link) | **mutual TLS**: a private CA signs the gateway server cert and the bot client cert, and each verifies the other. The bot dials out (no inbound port, no static IP), so the channel is guarded solely by mTLS — the bot client key is as sensitive as the token (§13) |
|
||||
|
||||
@@ -1543,6 +1544,13 @@ The gateway exposes the bot-link on a dedicated mTLS gRPC listener
|
||||
(`GATEWAY_BOTLINK_ADDR`, internal-only in the test contour, published in prod) plus a
|
||||
plaintext relay (`GATEWAY_BOTLINK_RELAY_ADDR`) the backend admin console calls.
|
||||
|
||||
The **backend** egresses directly (no sidecar) to the payment provider and, for the
|
||||
professional-income tax export (`docs/PAYMENTS.md` §12), to `lknpd.nalog.ru` — a Russian
|
||||
government host, so the prod main host's own Russian egress is what that path depends on. The
|
||||
export is dormant unless `BACKEND_MYNALOG_KEY` is set, and even when armed it makes no outbound
|
||||
call at all while its queue is empty: the queue is a local query, and nothing authenticates until
|
||||
there is something to file.
|
||||
|
||||
The full contour (`deploy/docker-compose.yml`) runs one `gateway`, one `backend`,
|
||||
one Postgres, the static `landing`, the Telegram `validator` and `bot` (+ the bot's VPN
|
||||
sidecar — the `bot`+`vpn` pair is gated to a `telegram-local` compose profile so the prod
|
||||
|
||||
Reference in New Issue
Block a user