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