fix: align retention cleanup across notifications logs and share telemetry
This commit is contained in:
@@ -125,6 +125,11 @@ class OrderDeletionService:
|
||||
"UPDATE order_intakes SET payload_json='{}', updated_at=? WHERE order_id=?",
|
||||
(now, order_id),
|
||||
)
|
||||
if self._table_exists("delivery_notifications"):
|
||||
self._conn.execute(
|
||||
"UPDATE delivery_notifications SET payload_json='{}' WHERE order_id=?",
|
||||
(order_id,),
|
||||
)
|
||||
self._insert_audit(
|
||||
order_id=order_id,
|
||||
action="anonymize",
|
||||
|
||||
@@ -4,6 +4,7 @@ import argparse
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from data.orders.dao import OrdersDAO
|
||||
from data.orders.deletion_service import OrderDeletionService
|
||||
@@ -16,6 +17,8 @@ class RetentionCleanupResult:
|
||||
scanned: int = 0
|
||||
candidates: int = 0
|
||||
anonymized: int = 0
|
||||
deletion_logs_pruned: int = 0
|
||||
share_events_pruned: int = 0
|
||||
|
||||
|
||||
_TERMINAL_STATUSES = {"completed", "refunded"}
|
||||
@@ -55,6 +58,54 @@ def _iter_candidates(db_path: str, cutoff_iso: str) -> tuple[int, list[str]]:
|
||||
return scanned, candidates
|
||||
|
||||
|
||||
def _prune_deletion_request_log(log_path: str, candidate_ids: list[str]) -> int:
|
||||
path = Path(log_path)
|
||||
if not path.exists() or not candidate_ids:
|
||||
return 0
|
||||
keep: list[str] = []
|
||||
pruned = 0
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
item = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
keep.append(line)
|
||||
continue
|
||||
if str(item.get("order_id") or "") in candidate_ids:
|
||||
pruned += 1
|
||||
continue
|
||||
keep.append(json.dumps(item, ensure_ascii=False))
|
||||
path.write_text("\n".join(keep) + ("\n" if keep else ""), encoding="utf-8")
|
||||
return pruned
|
||||
|
||||
|
||||
def _prune_share_access_events(share_db_path: str, candidate_ids: list[str]) -> int:
|
||||
if not candidate_ids:
|
||||
return 0
|
||||
from data.share.short_link import ShortLinkService
|
||||
|
||||
service = ShortLinkService(db_path=share_db_path)
|
||||
with service._connect() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT code FROM share_links WHERE report_id IN ({})".format(
|
||||
",".join("?" for _ in candidate_ids)
|
||||
),
|
||||
tuple(candidate_ids),
|
||||
).fetchall()
|
||||
codes = [str(row[0]) for row in rows]
|
||||
if not codes:
|
||||
return 0
|
||||
conn.execute(
|
||||
"DELETE FROM share_link_access_events WHERE code IN ({})".format(
|
||||
",".join("?" for _ in codes)
|
||||
),
|
||||
tuple(codes),
|
||||
)
|
||||
conn.commit()
|
||||
return int(conn.total_changes)
|
||||
|
||||
|
||||
def resolve_cutoff_iso(
|
||||
*, cutoff_iso: str | None = None, retention_days: int | None = None
|
||||
) -> str:
|
||||
@@ -68,8 +119,24 @@ def resolve_cutoff_iso(
|
||||
|
||||
|
||||
def run_cleanup(
|
||||
db_path: str, *, cutoff_iso: str, apply: bool
|
||||
db_path: str,
|
||||
*,
|
||||
cutoff_iso: str,
|
||||
apply: bool,
|
||||
deletion_request_log_path: str | None = None,
|
||||
share_db_path: str | None = None,
|
||||
) -> RetentionCleanupResult:
|
||||
if deletion_request_log_path is None or share_db_path is None:
|
||||
try:
|
||||
from admin.config import load_settings
|
||||
|
||||
settings = load_settings()
|
||||
except Exception:
|
||||
settings = None
|
||||
if deletion_request_log_path is None and settings is not None:
|
||||
deletion_request_log_path = settings.deletion_request_log_path
|
||||
if share_db_path is None and settings is not None:
|
||||
share_db_path = settings.share_db_path
|
||||
scanned, candidate_ids = _iter_candidates(db_path, cutoff_iso)
|
||||
result = RetentionCleanupResult(
|
||||
cutoff_iso=cutoff_iso,
|
||||
@@ -88,6 +155,14 @@ def run_cleanup(
|
||||
result.anonymized += 1
|
||||
finally:
|
||||
service.close()
|
||||
if deletion_request_log_path:
|
||||
result.deletion_logs_pruned = _prune_deletion_request_log(
|
||||
deletion_request_log_path, candidate_ids
|
||||
)
|
||||
if share_db_path:
|
||||
result.share_events_pruned = _prune_share_access_events(
|
||||
share_db_path, candidate_ids
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -110,10 +185,11 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None, *, db_path: str | None = None) -> int:
|
||||
if db_path is None:
|
||||
from admin.config import load_settings
|
||||
from admin.config import load_settings
|
||||
|
||||
db_path = load_settings().orders_db_path
|
||||
settings = load_settings()
|
||||
if db_path is None:
|
||||
db_path = settings.orders_db_path
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
@@ -123,7 +199,13 @@ def main(argv: list[str] | None = None, *, db_path: str | None = None) -> int:
|
||||
)
|
||||
except ValueError as exc:
|
||||
parser.error(str(exc))
|
||||
result = run_cleanup(db_path, cutoff_iso=cutoff_iso, apply=not args.dry_run)
|
||||
result = run_cleanup(
|
||||
db_path,
|
||||
cutoff_iso=cutoff_iso,
|
||||
apply=not args.dry_run,
|
||||
deletion_request_log_path=getattr(settings, "deletion_request_log_path", None),
|
||||
share_db_path=getattr(settings, "share_db_path", None),
|
||||
)
|
||||
print(json.dumps(result.__dict__, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user