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:
@@ -27,6 +27,27 @@ async def _clear_default(session: SessionDep, keep_id: uuid.UUID) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def _ensure_np_key_unique(
|
||||
session: SessionDep, api_key: str, exclude_id: uuid.UUID | None = None
|
||||
) -> None:
|
||||
"""Один кабинет НП — одна касса, иначе неясно, от какой кассы пробивать его посылки."""
|
||||
registers = await session.scalars(
|
||||
select(CashRegister).where(CashRegister.np_api_key_enc.is_not(None))
|
||||
)
|
||||
for register in registers:
|
||||
if register.id == exclude_id:
|
||||
continue
|
||||
try:
|
||||
same = crypto.decrypt(register.np_api_key_enc) == api_key
|
||||
except crypto.DecryptionError:
|
||||
continue
|
||||
if same:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Этот ключ Новой Почты уже привязан к кассе «{register.name}»",
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=list[CashRegisterOut])
|
||||
async def list_cash_registers(session: SessionDep) -> list[CashRegisterOut]:
|
||||
registers = await session.scalars(select(CashRegister).order_by(CashRegister.created_at))
|
||||
@@ -37,12 +58,15 @@ async def list_cash_registers(session: SessionDep) -> list[CashRegisterOut]:
|
||||
async def create_cash_register(
|
||||
payload: CashRegisterCreate, request: Request, session: SessionDep, actor: AdminUser
|
||||
) -> CashRegisterOut:
|
||||
if payload.np_api_key:
|
||||
await _ensure_np_key_unique(session, payload.np_api_key)
|
||||
register = CashRegister(
|
||||
id=uuid.uuid4(),
|
||||
name=payload.name,
|
||||
fiscal_number=payload.fiscal_number,
|
||||
license_key_enc=crypto.encrypt(payload.license_key),
|
||||
cashier_pin_enc=crypto.encrypt(payload.pin_code),
|
||||
np_api_key_enc=crypto.encrypt(payload.np_api_key) if payload.np_api_key else None,
|
||||
tax_codes=payload.tax_codes,
|
||||
is_active=True,
|
||||
is_default=payload.is_default,
|
||||
@@ -56,7 +80,11 @@ async def create_cash_register(
|
||||
user=actor,
|
||||
entity_type="cash_register",
|
||||
entity_id=register.id,
|
||||
payload={"name": register.name, "fiscal_number": register.fiscal_number},
|
||||
payload={
|
||||
"name": register.name,
|
||||
"fiscal_number": register.fiscal_number,
|
||||
"np_api_key_set": register.np_api_key_enc is not None,
|
||||
},
|
||||
request=request,
|
||||
)
|
||||
await session.commit()
|
||||
@@ -80,6 +108,9 @@ async def update_cash_register(
|
||||
register.license_key_enc = crypto.encrypt(changes["license_key"])
|
||||
if "pin_code" in changes:
|
||||
register.cashier_pin_enc = crypto.encrypt(changes["pin_code"])
|
||||
if changes.get("np_api_key"):
|
||||
await _ensure_np_key_unique(session, changes["np_api_key"], exclude_id=register.id)
|
||||
register.np_api_key_enc = crypto.encrypt(changes["np_api_key"])
|
||||
if "fiscal_number" in changes:
|
||||
register.fiscal_number = changes["fiscal_number"]
|
||||
for field in ("name", "tax_codes", "is_active", "is_default"):
|
||||
@@ -94,7 +125,7 @@ async def update_cash_register(
|
||||
user=actor,
|
||||
entity_type="cash_register",
|
||||
entity_id=register.id,
|
||||
payload=changes, # license_key / pin_code редактируются в audit._sanitize
|
||||
payload=changes, # license_key / pin_code / np_api_key редактируются в audit._sanitize
|
||||
request=request,
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
Reference in New Issue
Block a user