68 lines
1.7 KiB
Python
68 lines
1.7 KiB
Python
|
|
"""Audit trail helpers. Audit rows are append-only: the application exposes
|
||
|
|
no endpoint that updates or deletes them."""
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
|
||
|
|
from app.models import AuditLog, Ncr
|
||
|
|
|
||
|
|
|
||
|
|
def _fmt(value: Any) -> str | None:
|
||
|
|
if value is None:
|
||
|
|
return None
|
||
|
|
if isinstance(value, bool):
|
||
|
|
return "true" if value else "false"
|
||
|
|
return str(value)
|
||
|
|
|
||
|
|
|
||
|
|
def audit_event(
|
||
|
|
db: AsyncSession,
|
||
|
|
*,
|
||
|
|
user_id: int,
|
||
|
|
action: str,
|
||
|
|
ncr_id: int | None = None,
|
||
|
|
field_name: str | None = None,
|
||
|
|
old_value: Any = None,
|
||
|
|
new_value: Any = None,
|
||
|
|
detail: str | None = None,
|
||
|
|
) -> None:
|
||
|
|
db.add(
|
||
|
|
AuditLog(
|
||
|
|
ncr_id=ncr_id,
|
||
|
|
user_id=user_id,
|
||
|
|
action=action,
|
||
|
|
field_name=field_name,
|
||
|
|
old_value=_fmt(old_value),
|
||
|
|
new_value=_fmt(new_value),
|
||
|
|
detail=detail,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def apply_field_updates(
|
||
|
|
db: AsyncSession,
|
||
|
|
ncr: Ncr,
|
||
|
|
user_id: int,
|
||
|
|
updates: dict[str, Any],
|
||
|
|
action: str = "update",
|
||
|
|
) -> dict[str, tuple[Any, Any]]:
|
||
|
|
"""Set attributes on the NCR, writing one audit row per actually-changed
|
||
|
|
field. Returns {field: (old, new)} for the fields that changed."""
|
||
|
|
changes: dict[str, tuple[Any, Any]] = {}
|
||
|
|
for field, new_value in updates.items():
|
||
|
|
old_value = getattr(ncr, field)
|
||
|
|
if old_value == new_value:
|
||
|
|
continue
|
||
|
|
setattr(ncr, field, new_value)
|
||
|
|
changes[field] = (old_value, new_value)
|
||
|
|
audit_event(
|
||
|
|
db,
|
||
|
|
ncr_id=ncr.id,
|
||
|
|
user_id=user_id,
|
||
|
|
action=action,
|
||
|
|
field_name=field,
|
||
|
|
old_value=old_value,
|
||
|
|
new_value=new_value,
|
||
|
|
)
|
||
|
|
return changes
|