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:
@@ -0,0 +1,115 @@
|
||||
{{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}}
|
||||
Reference in New Issue
Block a user