After a power loss + reboot, COMENZI was queryable but not yet recovered; phase 4b-3 read it as empty and sticky-marked 3794 live orders DELETED_IN_ROA (nulling id_comanda). check_orders_exist also swallowed Oracle errors and returned a partial set, which callers misread as deletions. - check_orders_exist now re-raises on Oracle error instead of returning partial - new invoice_service.deletions_or_guard() raises MassDeletionGuard when the would-delete fraction is implausibly high (>30% of >=25 imported orders) - both deletion sites (auto sync + manual refresh) skip + log on guard trip Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
113 lines
4.7 KiB
Python
113 lines
4.7 KiB
Python
import logging
|
|
from .. import database
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ── Mass-deletion safety guard ──────────────────────────────────────────────
|
|
# If ROA appears to have lost a large fraction of its orders, it is almost
|
|
# certainly a transient/recovery state (e.g. the DB just restarted after a power
|
|
# loss and COMENZI hasn't finished recovering), NOT real deletions. In that case
|
|
# we refuse to mass-mark orders as DELETED_IN_ROA — a sticky, hard-to-reverse
|
|
# operation that nulls id_comanda. See incident 2026-06-26 (3794 false deletes).
|
|
MASS_DELETION_ABORT_FRACTION = 0.30
|
|
MASS_DELETION_ABORT_MIN = 25
|
|
|
|
|
|
class MassDeletionGuard(Exception):
|
|
"""Raised when the number of orders that would be marked deleted is
|
|
suspiciously high, indicating ROA is unavailable rather than truly purged."""
|
|
|
|
|
|
def deletions_or_guard(all_imported: list, existing_ids: set) -> list:
|
|
"""Return the subset of all_imported whose id_comanda is missing from ROA,
|
|
or raise MassDeletionGuard if that subset is implausibly large.
|
|
|
|
`existing_ids` MUST come from a successful check_orders_exist call — that
|
|
function now raises on Oracle error rather than returning a partial set, so
|
|
an empty result here means ROA genuinely has none of these orders.
|
|
"""
|
|
missing = [o for o in all_imported if o["id_comanda"] not in existing_ids]
|
|
total = len(all_imported)
|
|
if total >= MASS_DELETION_ABORT_MIN and len(missing) > total * MASS_DELETION_ABORT_FRACTION:
|
|
raise MassDeletionGuard(
|
|
f"{len(missing)}/{total} comenzi par sterse din ROA "
|
|
f"(>{int(MASS_DELETION_ABORT_FRACTION * 100)}%) — posibil ROA "
|
|
f"indisponibil/in recuperare; marcarea DELETED_IN_ROA a fost ANULATA"
|
|
)
|
|
return missing
|
|
|
|
|
|
def check_invoices_for_orders(id_comanda_list: list) -> dict:
|
|
"""Check which orders have been invoiced in Oracle (vanzari table).
|
|
Returns {id_comanda: {facturat, numar_act, serie_act, total_fara_tva, total_tva, total_cu_tva}}
|
|
"""
|
|
if not id_comanda_list or database.pool is None:
|
|
return {}
|
|
|
|
result = {}
|
|
conn = database.get_oracle_connection()
|
|
try:
|
|
with conn.cursor() as cur:
|
|
for i in range(0, len(id_comanda_list), 500):
|
|
batch = id_comanda_list[i:i+500]
|
|
placeholders = ",".join([f":c{j}" for j in range(len(batch))])
|
|
params = {f"c{j}": cid for j, cid in enumerate(batch)}
|
|
|
|
cur.execute(f"""
|
|
SELECT id_comanda, numar_act, serie_act,
|
|
total_fara_tva, total_tva, total_cu_tva,
|
|
TO_CHAR(data_act, 'YYYY-MM-DD') AS data_act
|
|
FROM vanzari
|
|
WHERE id_comanda IN ({placeholders}) AND sters = 0
|
|
""", params)
|
|
for row in cur:
|
|
result[row[0]] = {
|
|
"facturat": True,
|
|
"numar_act": row[1],
|
|
"serie_act": row[2],
|
|
"total_fara_tva": float(row[3]) if row[3] else 0,
|
|
"total_tva": float(row[4]) if row[4] else 0,
|
|
"total_cu_tva": float(row[5]) if row[5] else 0,
|
|
"data_act": row[6],
|
|
}
|
|
except Exception as e:
|
|
logger.warning(f"Invoice check failed (table may not exist): {e}")
|
|
finally:
|
|
database.pool.release(conn)
|
|
|
|
return result
|
|
|
|
|
|
def check_orders_exist(id_comanda_list: list) -> set:
|
|
"""Check which id_comanda values still exist in Oracle COMENZI (sters=0).
|
|
Returns set of id_comanda that exist.
|
|
"""
|
|
if not id_comanda_list or database.pool is None:
|
|
return set()
|
|
|
|
existing = set()
|
|
conn = database.get_oracle_connection()
|
|
try:
|
|
with conn.cursor() as cur:
|
|
for i in range(0, len(id_comanda_list), 500):
|
|
batch = id_comanda_list[i:i+500]
|
|
placeholders = ",".join([f":c{j}" for j in range(len(batch))])
|
|
params = {f"c{j}": cid for j, cid in enumerate(batch)}
|
|
|
|
cur.execute(f"""
|
|
SELECT id_comanda FROM COMENZI
|
|
WHERE id_comanda IN ({placeholders}) AND sters = 0
|
|
""", params)
|
|
for row in cur:
|
|
existing.add(row[0])
|
|
except Exception as e:
|
|
# Do NOT swallow: a partial/empty result on error would be misread by
|
|
# callers as "these orders were deleted in ROA" and trigger sticky
|
|
# DELETED_IN_ROA marking. Propagate so the caller skips deletion.
|
|
logger.warning(f"Order existence check failed: {e}")
|
|
raise
|
|
finally:
|
|
database.pool.release(conn)
|
|
|
|
return existing
|