Files
lux_fiscal/backend/app/services/audit.py
lauadminandClaude Opus 5.5 518a99197f Add Checkbox ETTN receipts for Nova Poshta COD waybills
Cashier creates an ETTN receipt in Checkbox bound to the TTN with payment
control; Checkbox fiscalizes it itself when the parcel is paid for.

- cash_registers (Fernet-encrypted license key / PIN) and receipts tables
- Checkbox HTTP client + stub (ETTN does not work on test registers)
- two-phase create via ARQ job, timeout reconciliation, cron status polling
- /receipts and /cash-registers API, audit records
- dashboard: per-order and bulk create, prepayment, cancel; cash registers page

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 23:39:00 +03:00

76 lines
2.4 KiB
Python

"""Запись событий в журнал аудита."""
from __future__ import annotations
import uuid
from typing import Any
from fastapi import Request
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.models.audit import AuditLog
from app.db.models.user import User
# Ключи, значения которых не должны попасть в журнал даже случайно.
_REDACTED_KEYS = {
"password",
"new_password",
"current_password",
"token",
"access_token",
"refresh_token",
"secret",
"license_key",
"api_key",
"pin",
"pin_code",
"cashier_pin",
}
def _sanitize(payload: dict[str, Any]) -> dict[str, Any]:
"""Журнал аудита читают люди и он живёт годами — секретам там не место."""
return {
key: ("***" if key.lower() in _REDACTED_KEYS else value) for key, value in payload.items()
}
def client_ip(request: Request | None) -> str | None:
if request is None:
return None
# За nginx реальный адрес приходит в X-Forwarded-For; первый элемент — клиент.
forwarded = request.headers.get("x-forwarded-for")
if forwarded:
return forwarded.split(",")[0].strip()
return request.client.host if request.client else None
async def record(
session: AsyncSession,
*,
action: str,
user: User | None = None,
actor_label: str | None = None,
entity_type: str | None = None,
entity_id: str | uuid.UUID | None = None,
payload: dict[str, Any] | None = None,
request: Request | None = None,
) -> AuditLog:
"""Добавляет запись в журнал.
Без commit: вызывающий код сам решает границы транзакции, чтобы событие
и изменение состояния фиксировались вместе либо не фиксировались вовсе.
"""
entry = AuditLog(
user_id=user.id if user else None,
actor_label=actor_label or (user.email if user else None),
action=action,
entity_type=entity_type,
entity_id=str(entity_id) if entity_id is not None else None,
payload=_sanitize(payload or {}),
ip=client_ip(request),
user_agent=(request.headers.get("user-agent") if request else None),
)
session.add(entry)
return entry