ligbox-ops-platform/projects/ops-desk/legacy-app/carbonio_release_store.py
Ligbox Spec Hub 821675ab4a Reorganize monorepo into projects/wizard, ops-desk, finance
Specs stay at repo root (cross-VM). Move deploy and code into logical
projects with README per domain, updated manifest.yaml, and symlinks at
legacy paths for VM122 backward compatibility.
2026-06-19 18:55:03 +00:00

239 lines
7 KiB
Python

"""Bloqueios ACCOUNT_EXISTS — libertar e-mail Carbonio (Spec 022)."""
from __future__ import annotations
import json
import re
from datetime import datetime, timezone
from typing import Any
EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def init_schema(conn) -> None:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS carbonio_account_blocks (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL,
domain TEXT NOT NULL,
session_id TEXT,
ticket_id INTEGER,
webhook_event_id INTEGER,
error_message TEXT,
status TEXT NOT NULL DEFAULT 'pending',
resolved_by TEXT,
resolved_at TEXT,
resolution_note TEXT,
created_at TEXT NOT NULL
)
"""
)
conn.execute(
"""
CREATE INDEX IF NOT EXISTS idx_carbonio_blocks_status
ON carbonio_account_blocks(status, created_at DESC)
"""
)
conn.execute(
"""
CREATE INDEX IF NOT EXISTS idx_carbonio_blocks_session
ON carbonio_account_blocks(session_id)
"""
)
def is_account_exists_failure(event: str, data: dict | None) -> bool:
if event != "onboarding.failed":
return False
if not isinstance(data, dict):
return False
err = str(data.get("error") or data.get("message") or data.get("reason") or "")
low = err.lower()
return (
"account_exists" in low
or "account.exists" in low
or "já existe" in low
or "already exists" in low
)
def _extract_email(domain: str | None, data: dict | None) -> str | None:
if isinstance(data, dict):
for key in ("email", "account", "mailbox"):
val = (data.get(key) or "").strip().lower()
if EMAIL_RE.match(val):
return val
err = str(data.get("error") or "")
m = re.search(r"[\w.+-]+@[\w.-]+\.\w+", err)
if m:
return m.group(0).lower()
if domain and isinstance(data, dict):
local = (data.get("local_part") or "").strip().lower()
if local:
return f"{local}@{domain.lower().strip()}"
return None
def upsert_from_webhook(
conn,
*,
event: str,
domain: str | None,
session_id: str | None,
data: dict | None,
webhook_event_id: int | None,
ticket_id: int | None,
) -> dict[str, Any] | None:
if not is_account_exists_failure(event, data):
return None
email = _extract_email(domain, data)
if not email:
return None
dom = (domain or email.split("@", 1)[-1]).lower().strip()
sid = (session_id or "").strip() or None
err_msg = str((data or {}).get("error") or "")[:2000]
now = _now()
if sid:
row = conn.execute(
"""
SELECT id, status FROM carbonio_account_blocks
WHERE email = ? AND session_id = ? AND status = 'pending'
ORDER BY id DESC LIMIT 1
""",
(email, sid),
).fetchone()
if row:
conn.execute(
"""
UPDATE carbonio_account_blocks
SET error_message = ?, webhook_event_id = COALESCE(?, webhook_event_id),
ticket_id = COALESCE(?, ticket_id)
WHERE id = ?
""",
(err_msg, webhook_event_id, ticket_id, row["id"]),
)
conn.commit()
return get_block(conn, int(row["id"]))
cur = conn.execute(
"""
INSERT INTO carbonio_account_blocks
(email, domain, session_id, ticket_id, webhook_event_id, error_message, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?)
""",
(email, dom, sid, ticket_id, webhook_event_id, err_msg, now),
)
conn.commit()
return get_block(conn, int(cur.lastrowid))
def get_block(conn, block_id: int) -> dict[str, Any] | None:
row = conn.execute(
"SELECT * FROM carbonio_account_blocks WHERE id = ?",
(block_id,),
).fetchone()
return _row_to_dict(row) if row else None
def _row_to_dict(row) -> dict[str, Any]:
return {
"id": row["id"],
"email": row["email"],
"domain": row["domain"],
"session_id": row["session_id"],
"ticket_id": row["ticket_id"],
"webhook_event_id": row["webhook_event_id"],
"error_message": row["error_message"],
"status": row["status"],
"resolved_by": row["resolved_by"],
"resolved_at": row["resolved_at"],
"resolution_note": row["resolution_note"],
"created_at": row["created_at"],
}
def list_blocks(
conn,
*,
status: str | None = "pending",
session_id: str | None = None,
ticket_id: int | None = None,
limit: int = 100,
) -> dict[str, Any]:
limit = max(1, min(limit, 500))
clauses = []
params: list[Any] = []
if status and status != "all":
clauses.append("status = ?")
params.append(status)
if session_id:
clauses.append("session_id = ?")
params.append(session_id.strip())
if ticket_id:
clauses.append("ticket_id = ?")
params.append(ticket_id)
where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
rows = conn.execute(
f"""
SELECT * FROM carbonio_account_blocks {where}
ORDER BY id DESC LIMIT ?
""",
(*params, limit),
).fetchall()
total = conn.execute(
f"SELECT COUNT(*) FROM carbonio_account_blocks {where}",
tuple(params),
).fetchone()[0]
return {
"blocks": [_row_to_dict(r) for r in rows],
"total": total,
}
def resolve_block(
conn,
block_id: int,
*,
resolved_by: str,
note: str = "",
) -> dict[str, Any]:
now = _now()
cur = conn.execute(
"""
UPDATE carbonio_account_blocks
SET status = 'resolved', resolved_by = ?, resolved_at = ?, resolution_note = ?
WHERE id = ? AND status = 'pending'
""",
(resolved_by, now, note[:500], block_id),
)
if cur.rowcount == 0:
existing = get_block(conn, block_id)
if existing and existing["status"] == "resolved":
raise ValueError("already_resolved")
raise ValueError("not_found_or_locked")
conn.commit()
return get_block(conn, block_id) or {}
def append_ticket_resolution_note(conn, ticket_id: int, *, email: str, by: str) -> None:
row = conn.execute("SELECT payload FROM tickets WHERE id = ?", (ticket_id,)).fetchone()
if not row:
return
try:
payload = json.loads(row["payload"] or "{}")
except json.JSONDecodeError:
payload = {}
notes = payload.get("carbonio_release_notes") or []
notes.append({"at": _now(), "email": email, "by": by, "action": "account_deleted"})
payload["carbonio_release_notes"] = notes[-20:]
conn.execute(
"UPDATE tickets SET payload = ? WHERE id = ?",
(json.dumps(payload, ensure_ascii=False), ticket_id),
)
conn.commit()