Files
scrabble-game/backend/internal/adminconsole/templates/pages/mynalog.gohtml
T
Ilia Denisov e3c2e80a0a
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
feat(payments): report income to «Мой налог»
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.
2026-07-28 15:40:36 +02:00

116 lines
5.7 KiB
Go Template

{{define "content" -}}
<h1>My Nalog</h1>
{{with .Data}}
<p class="note">Rouble income taken through the direct rail has to be registered with the
professional-income tax service by hand — the payment provider does not do it. Times are shown and
sent in <strong>{{.TimeZone}}</strong>, because the offset decides which tax month an income falls
into. The tax API is unofficial: if it stops working, use “Export CSV” and file by hand, then enter
each receipt number back here so a refund can still annul it.</p>
{{if .Backlog.Count}}
<section class="panel">
<h2>{{if .Backlog.Overdue}}Overdue{{else}}Closed month still unfiled{{end}}</h2>
<p><strong>{{.Backlog.Count}}</strong> income(s) totalling <strong>{{.Backlog.Amount}}</strong> from a
month that has already ended are still not registered. Oldest: {{.Backlog.Oldest}}.
{{if .Backlog.Overdue}}The 9th of the month has passed.{{end}}</p>
</section>
{{end}}
<section class="panel">
<h2>Tax cabinet</h2>
{{if .SignedIn}}
<p>Signed in as <strong>{{.INN}}</strong>.
{{if .LastOK}}Last successful exchange: {{.LastOK}}.{{else}}No successful exchange yet.{{end}}</p>
{{if .Paused}}
<p class="note">The export has taken itself out of service: {{.PauseReason}}</p>
<form class="form" method="post" action="/_gm/mynalog/resume"><button type="submit">Resume</button></form>
{{end}}
<div class="actions">
<form class="form" method="post" action="/_gm/mynalog/run"><button type="submit">Run export now</button></form>
<form class="form" method="post" action="/_gm/mynalog/auto">
<input type="hidden" name="enabled" value="{{if .AutoEnabled}}0{{else}}1{{end}}">
<button type="submit">{{if .AutoEnabled}}Turn the automatic export off{{else}}Turn the automatic export on{{end}}</button>
</form>
<form class="form" method="post" action="/_gm/mynalog/logout" onsubmit="return confirm('Sign out of the tax cabinet? The automatic export switches off with it and you will need the password to sign back in.')"><button type="submit" class="danger">Sign out</button></form>
<a class="export" href="/_gm/mynalog.csv">Export CSV ↓</a>
</div>
<p class="note">Automatic export is <strong>{{if .AutoEnabled}}on{{else}}off{{end}}</strong>. It only
ever runs when there is something to file, so an idle installation sends the tax service nothing.</p>
{{else}}
<p class="note">Not signed in. The password is used once to obtain a refresh token and is not stored.</p>
<form class="form" method="post" action="/_gm/mynalog/login">
<label>Login (INN) <input name="login" autocomplete="off" size="16"></label>
<label>Password <input type="password" name="password" autocomplete="off" size="24"></label>
<button type="submit">Sign in</button>
</form>
{{end}}
</section>
{{if .Attention}}
<h2>Needs attention</h2>
<p class="note">The outcome of these could not be established — the receipt may or may not exist.
New income is not filed while any remain, because filing an income twice is as wrong as not filing
it at all. Re-check asks the tax service again; “Back to queue” means you are sure nothing was
created; entering a receipt number means you filed it by hand.</p>
<table class="list">
<thead><tr><th>Time</th><th>Amount</th><th>Service name</th><th>Tries</th><th>Last error</th><th></th></tr></thead>
<tbody>
{{range .Attention}}
<tr>
<td>{{.At}}</td>
<td class="num">{{.Amount}}</td>
<td><code>{{.Name}}</code></td>
<td class="num">{{.Attempts}}</td>
<td><span class="note">{{.LastError}}</span></td>
<td>
<form class="form" method="post" action="/_gm/mynalog/recheck"><input type="hidden" name="ledger_id" value="{{.LedgerID}}"><button type="submit">Re-check</button></form>
<form class="form" method="post" action="/_gm/mynalog/manual"><input type="hidden" name="ledger_id" value="{{.LedgerID}}"><input name="receipt_uuid" placeholder="receipt number" size="14"><button type="submit">Filed by hand</button></form>
<form class="form" method="post" action="/_gm/mynalog/requeue" onsubmit="return confirm('Only do this if the tax cabinet really has no receipt for this income — otherwise it will be filed twice.')"><input type="hidden" name="ledger_id" value="{{.LedgerID}}"><button type="submit">Back to queue</button></form>
</td>
</tr>
{{end}}
</tbody>
</table>
{{end}}
<h2>To annul</h2>
<table class="list">
<thead><tr><th>Account</th><th>Receipt</th><th>State</th></tr></thead>
<tbody>
{{range .Cancels}}
<tr>
<td><a href="/_gm/users/{{.AccountID}}">card</a></td>
<td><code>{{.ReceiptUUID}}</code></td>
<td>{{if .Failed}}<span class="note">a previous attempt was refused</span>{{else}}awaiting{{end}}</td>
</tr>
{{else}}
<tr><td colspan="3"><span class="note">nothing to annul</span></td></tr>
{{end}}
</tbody>
</table>
<h2>To file</h2>
<table class="list">
<thead><tr><th>Time</th><th>Account</th><th>Amount</th><th>Service name</th><th>Tries</th><th>Last error</th><th></th></tr></thead>
<tbody>
{{range .Owed}}
<tr>
<td>{{.At}}</td>
<td><a href="/_gm/users/{{.AccountID}}">card</a></td>
<td class="num">{{.Amount}}</td>
<td><code>{{.Name}}</code></td>
<td class="num">{{.Attempts}}</td>
<td><span class="note">{{.LastError}}</span></td>
<td>
<form class="form" method="post" action="/_gm/mynalog/manual"><input type="hidden" name="ledger_id" value="{{.LedgerID}}"><input name="receipt_uuid" placeholder="receipt number" size="14"><button type="submit">Filed by hand</button></form>
<form class="form" method="post" action="/_gm/mynalog/skip" onsubmit="return confirm('Rule this income out of the export? It will never be filed.')"><input type="hidden" name="ledger_id" value="{{.LedgerID}}"><input name="reason" placeholder="reason" size="16"><button type="submit" class="danger">Rule out</button></form>
</td>
</tr>
{{else}}
<tr><td colspan="7"><span class="note">nothing to file</span></td></tr>
{{end}}
</tbody>
</table>
{{end}}
{{- end}}