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>
76 lines
2.4 KiB
Python
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
|