Bind Nova Poshta API key to cash register (#3)

Each cash register stores its own encrypted NP API key. Status polling uses
register keys and binds an order to the register whose key sees the TTN as
its own (PhoneSender present); ETTN receipts are created from that register.
Migration 0008 moves the old NOVA_POSHTA_API_KEY into the default register.

Closes #3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 13:02:40 +03:00
co-authored by Claude Opus 5.5
parent 2b92d82693
commit 8b3c2b6d63
22 changed files with 531 additions and 73 deletions
+22 -5
View File
@@ -19,6 +19,7 @@ interface FormState {
fiscal_number: string
license_key: string
pin_code: string
np_api_key: string
tax_codes: string
is_default: boolean
}
@@ -28,6 +29,7 @@ const EMPTY_FORM: FormState = {
fiscal_number: '',
license_key: '',
pin_code: '',
np_api_key: '',
tax_codes: '',
is_default: true,
}
@@ -75,6 +77,7 @@ export function CashRegistersPage() {
fiscal_number: register.fiscal_number ?? '',
license_key: '',
pin_code: '',
np_api_key: '',
tax_codes: register.tax_codes.join(', '),
is_default: register.is_default,
})
@@ -88,7 +91,7 @@ export function CashRegistersPage() {
async function handleSubmit(event: FormEvent) {
event.preventDefault()
if (editingId) {
// Пустые ключ/PIN при редактировании — «не менять».
// Пустые ключи/PIN при редактировании — «не менять».
const payload: CashRegisterUpdate = {
name: form.name,
fiscal_number: form.fiscal_number || null,
@@ -97,6 +100,7 @@ export function CashRegistersPage() {
}
if (form.license_key) payload.license_key = form.license_key
if (form.pin_code) payload.pin_code = form.pin_code
if (form.np_api_key) payload.np_api_key = form.np_api_key
await run(() => updateCashRegister(editingId, payload), 'Касса обновлена')
} else {
await run(
@@ -106,6 +110,7 @@ export function CashRegistersPage() {
fiscal_number: form.fiscal_number || null,
license_key: form.license_key,
pin_code: form.pin_code,
np_api_key: form.np_api_key,
tax_codes: parseTaxCodes(form.tax_codes),
is_default: form.is_default,
}),
@@ -145,6 +150,7 @@ export function CashRegistersPage() {
<th>Название</th>
<th>Фиск. номер</th>
<th>Ключ лицензии</th>
<th>Ключ Новой Почты</th>
<th>Налоги</th>
<th>Статус</th>
<th>Действия</th>
@@ -153,21 +159,21 @@ export function CashRegistersPage() {
<tbody>
{isLoading && (
<tr>
<td colSpan={6} className="orders-empty">
<td colSpan={7} className="orders-empty">
Загрузка…
</td>
</tr>
)}
{isError && (
<tr>
<td colSpan={6} className="orders-empty">
<td colSpan={7} className="orders-empty">
Не удалось загрузить кассы
</td>
</tr>
)}
{registers?.length === 0 && (
<tr>
<td colSpan={6} className="orders-empty">
<td colSpan={7} className="orders-empty">
Кассы не настроены — чеки создавать нельзя
</td>
</tr>
@@ -180,6 +186,7 @@ export function CashRegistersPage() {
</td>
<td>{register.fiscal_number || '—'}</td>
<td>{register.license_key_masked}</td>
<td>{register.np_api_key_masked ?? <span className="cr-missing">не задан</span>}</td>
<td>{register.tax_codes.length ? register.tax_codes.join(', ') : 'без налога'}</td>
<td>
<span className={`orders-status orders-status--${register.is_active ? 'delivered' : 'new'}`}>
@@ -249,6 +256,16 @@ export function CashRegistersPage() {
onChange={(e) => setForm({ ...form, pin_code: e.target.value })}
/>
</label>
<label>
Ключ API Новой Почты (посылки этого кабинета пробиваются через эту кассу)
<input
required={!editingId}
autoComplete="off"
placeholder={editingId ? 'не менять' : ''}
value={form.np_api_key}
onChange={(e) => setForm({ ...form, np_api_key: e.target.value })}
/>
</label>
<label>
Коды налогов (через запятую, пусто — без налога)
<input value={form.tax_codes} onChange={(e) => setForm({ ...form, tax_codes: e.target.value })} />
@@ -259,7 +276,7 @@ export function CashRegistersPage() {
checked={form.is_default}
onChange={(e) => setForm({ ...form, is_default: e.target.checked })}
/>
Основная касса (по ней создаются чеки)
Основная касса (первой проверяет новые посылки)
</label>
<div className="cr-form-actions">
<button type="submit" className="orders-create-btn" disabled={busy}>