Phase 4 of Migration

This commit is contained in:
2026-04-17 15:44:17 +03:00
parent 83361fad77
commit da4608c937
8 changed files with 257 additions and 7 deletions

83
backend/shared/audit.py Normal file
View File

@@ -0,0 +1,83 @@
"""
Audit log utility — all services call log_action() to record staff events.
Usage:
from shared.audit import log_action
await log_action(
db, actor_id, actor_name,
action="CREATE", entity_type="customer", entity_id=cust_id,
entity_label="Church of St. George",
)
await log_action(
db, actor_id, actor_name,
action="UPDATE", entity_type="order", entity_id=order_id,
entity_label="ORD-0042",
changes={"status": {"old": "negotiating", "new": "confirmed"}},
)
Never raises — a logging failure must never break the primary operation.
The call is fire-and-forget safe: wrap in try/except internally.
"""
from datetime import datetime, timezone
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from shared.orm import AuditLog
async def log_action(
db: AsyncSession,
actor_id: str,
actor_name: str,
action: str,
entity_type: str,
entity_id: str,
entity_label: str | None = None,
changes: dict[str, Any] | None = None,
meta: dict[str, Any] | None = None,
) -> None:
"""
Insert one row into audit_log. Never raises — failures are silently swallowed
so a logging error never disrupts the primary request.
"""
try:
entry = AuditLog(
occurred_at=datetime.now(timezone.utc),
actor_id=actor_id,
actor_name=actor_name,
action=action,
entity_type=entity_type,
entity_id=entity_id,
entity_label=entity_label,
changes=changes,
meta=meta,
)
db.add(entry)
# Flush without committing — caller's transaction commits it atomically.
# If the caller hasn't started a transaction, flush still works; the
# session will auto-commit on the next explicit commit call.
await db.flush()
except Exception:
pass
def diff(old: dict, new: dict) -> dict[str, dict]:
"""
Build a changes dict from two flat dicts.
Only includes keys whose values actually changed.
Skip internal/unloggable keys (hashed_password, updated_at).
Usage:
changes = diff(old_record, new_record)
await log_action(..., changes=changes or None)
"""
_SKIP = {"hashed_password", "updated_at", "firestore_id"}
return {
k: {"old": old.get(k), "new": new.get(k)}
for k in new
if k not in _SKIP and old.get(k) != new.get(k)
}