""" Lookup data for service_auto forms — tip deviz, masini, firme, asiguratori, inspectori, operatii, parteneri. Multi-tenant safe: `schema` e rezolvat din `id_firma` via `_context.get_schema()`; nu există schemă hardcodată. `server_id` propagat din JWT (`request.state.server_id`). """ import time from typing import List, Optional, Tuple import oracledb from fastapi import HTTPException from shared.database.oracle_pool import oracle_pool from ..schemas.comanda import ( AsiguratorItem, FirmaItem, InspectorItem, MasinaClientItem, MasinaDetails, OperatieItem, PartenerItem, PartnerCreateRequest, TipDevizItem, ) from .. import logger from ._context import get_schema # In-memory TTL cache: key → (monotonic_timestamp, value) _cache: dict = {} _TTL_TIP_DEVIZ = 86400 # 24 h — tip deviz changes only via DB migration _TTL_MASINI = 300 # 5 min — vehicle inventory changes regularly _TTL_ASIGURATORI = 86400 # 24 h _TTL_INSPECTORI = 86400 # 24 h per asigurator _TTL_OPERATII = 86400 # 24 h — DEV_NOM_NORME changes only via DB def _cache_get(key: str, ttl: float): entry: Optional[Tuple] = _cache.get(key) if entry and (time.monotonic() - entry[0]) < ttl: return entry[1] return None def _cache_set(key: str, value) -> None: _cache[key] = (time.monotonic(), value) def reset_cache() -> None: """Test helper.""" _cache.clear() class LookupService: @staticmethod async def get_firme( company_ids: List[str], server_id: Optional[str] = None, ) -> List[FirmaItem]: """ Returns firma names for the company IDs in the user's JWT. Query pe `CONTAFIN_ORACLE.V_NOM_FIRME` pe serverul utilizatorului. """ if not company_ids: return [] placeholders = ", ".join(f":id{i}" for i in range(len(company_ids))) query = f""" SELECT id_firma, firma, schema, id_mama FROM CONTAFIN_ORACLE.V_NOM_FIRME WHERE id_firma IN ({placeholders}) ORDER BY id_firma """ params = {f"id{i}": int(cid) for i, cid in enumerate(company_ids)} try: async with oracle_pool.get_connection(server_id) as conn: with conn.cursor() as cur: cur.execute(query, params) rows = cur.fetchall() except oracledb.DatabaseError: logger.error("get_firme Oracle error", exc_info=True) raise HTTPException(status_code=503, detail="Eroare la încărcarea firmelor") return [ FirmaItem(id_firma=r[0], firma=r[1], schema_name=r[2] or "", id_mama=r[3]) for r in rows ] @staticmethod async def get_tip_deviz( company_id: int, server_id: Optional[str] = None, ) -> List[TipDevizItem]: """ Tip deviz din `{schema}.DEV_TIP_DEVIZ`. Cached 24 h per schema. """ schema = await get_schema(company_id, server_id) cache_key = f"tip_deviz:{schema}" cached = _cache_get(cache_key, _TTL_TIP_DEVIZ) if cached is not None: return cached query = f""" SELECT id_tip, denumire, inch_validare FROM {schema}.DEV_TIP_DEVIZ ORDER BY id_tip """ try: async with oracle_pool.get_connection(server_id) as conn: with conn.cursor() as cur: cur.execute(query) rows = cur.fetchall() except oracledb.DatabaseError: logger.error("get_tip_deviz Oracle error", exc_info=True) raise HTTPException(status_code=503, detail="Eroare la încărcarea tipurilor de deviz") result = [ TipDevizItem(id_tip=r[0], denumire=r[1], inch_validare=r[2] or 0) for r in rows ] _cache_set(cache_key, result) return result @staticmethod async def get_masini( company_id: int, server_id: Optional[str] = None, ) -> List[MasinaClientItem]: """ Mașini active din `{schema}.AUTO_VMASINICLIENTI`. Cached 5 min per schema. """ schema = await get_schema(company_id, server_id) cache_key = f"masini:{schema}" cached = _cache_get(cache_key, _TTL_MASINI) if cached is not None: return cached query = f""" SELECT id_masiniclient, nrinmat, marca, masina, anfabricatie, partener FROM {schema}.AUTO_VMASINICLIENTI WHERE inactiv = 0 ORDER BY partener, nrinmat """ try: async with oracle_pool.get_connection(server_id) as conn: with conn.cursor() as cur: cur.execute(query) rows = cur.fetchall() except oracledb.DatabaseError: logger.error("get_masini Oracle error", exc_info=True) raise HTTPException(status_code=503, detail="Eroare la încărcarea mașinilor") result = [] for r in rows: id_mc, nrinmat, marca, masina, an, partener = r parts = [] if marca: parts.append(marca) if masina: parts.append(masina) vehicul = " ".join(parts) if parts else "?" an_str = f" ({int(an)})" if an else "" label = f"{partener or '?'} — {vehicul}, {nrinmat or '?'}{an_str}" result.append(MasinaClientItem(id_masiniclient=int(id_mc), label=label)) _cache_set(cache_key, result) return result @staticmethod async def get_masina_details( id_masiniclient: int, company_id: int, server_id: Optional[str] = None, ) -> Optional[MasinaDetails]: """ Detalii complete vehicul din `{schema}.AUTO_VMASINICLIENTI`. Fără cache (per-record). """ schema = await get_schema(company_id, server_id) query = f""" SELECT id_masiniclient, nrinmat, marca, masina, anfabricatie, partener, series, cilindree, puterecp, puterekw FROM {schema}.AUTO_VMASINICLIENTI WHERE id_masiniclient = :id_mc """ try: async with oracle_pool.get_connection(server_id) as conn: with conn.cursor() as cur: cur.execute(query, {"id_mc": id_masiniclient}) row = cur.fetchone() except oracledb.DatabaseError: logger.error("get_masina_details Oracle error", exc_info=True) raise HTTPException(status_code=503, detail="Eroare la încărcarea detaliilor mașinii") if not row: return None id_mc, nrinmat, marca, masina, an, partener, serie_sasiu, cilindree, putere_cp, putere_kw = row parts = [p for p in [marca, masina] if p] vehicul = " ".join(parts) if parts else "?" an_str = f" ({int(an)})" if an else "" label = f"{partener or '?'} — {vehicul}, {nrinmat or '?'}{an_str}" return MasinaDetails( id_masiniclient=int(id_mc), label=label, nr_inmatriculare=nrinmat, marca=marca, model=masina, serie_sasiu=serie_sasiu, cilindree=int(cilindree) if cilindree else None, putere_cp=int(putere_cp) if putere_cp else None, putere_kw=int(putere_kw) if putere_kw else None, client_nume=partener, ) @staticmethod async def get_asiguratori( company_id: int, server_id: Optional[str] = None, ) -> List[AsiguratorItem]: """ Asigurători activi din `{schema}.DEV_NOM_ASIGURATORI`. Cached 24h per schema. Numele din NOM_PARTENERI via FK ID_PART (DEV_NOM_ASIGURATORI nu are coloană denumire). """ schema = await get_schema(company_id, server_id) cache_key = f"asiguratori:{schema}" cached = _cache_get(cache_key, _TTL_ASIGURATORI) if cached is not None: return cached query = f""" SELECT a.id_asigurator, NVL(p.denumire, a.asigurator_vechi) AS denumire FROM {schema}.DEV_NOM_ASIGURATORI a LEFT JOIN {schema}.NOM_PARTENERI p ON a.id_part = p.id_part WHERE NVL(a.sters, 0) = 0 ORDER BY NVL(p.denumire, a.asigurator_vechi) """ try: async with oracle_pool.get_connection(server_id) as conn: with conn.cursor() as cur: cur.execute(query) rows = cur.fetchall() except oracledb.DatabaseError: logger.error("get_asiguratori Oracle error", exc_info=True) raise HTTPException(status_code=503, detail="Eroare la încărcarea asigurătorilor") result = [AsiguratorItem(id_asigurator=int(r[0]), denumire=r[1] or "") for r in rows] _cache_set(cache_key, result) return result @staticmethod async def get_inspectori( id_asigurator: int, company_id: int, server_id: Optional[str] = None, ) -> List[InspectorItem]: """ Inspectori filtrați per asigurator din `{schema}.DEV_NOM_INSPECTORI`. Cached 24h per (schema, id_asigurator). """ schema = await get_schema(company_id, server_id) cache_key = f"inspectori:{schema}:{id_asigurator}" cached = _cache_get(cache_key, _TTL_INSPECTORI) if cached is not None: return cached query = f""" SELECT id_inspector, inspector AS denumire, id_asigurator FROM {schema}.DEV_NOM_INSPECTORI WHERE id_asigurator = :id_asig AND NVL(sters, 0) = 0 ORDER BY inspector """ try: async with oracle_pool.get_connection(server_id) as conn: with conn.cursor() as cur: cur.execute(query, {"id_asig": id_asigurator}) rows = cur.fetchall() except oracledb.DatabaseError: logger.error("get_inspectori Oracle error", exc_info=True) raise HTTPException(status_code=503, detail="Eroare la încărcarea inspectorilor") result = [ InspectorItem(id_inspector=int(r[0]), denumire=r[1] or "", id_asigurator=int(r[2])) for r in rows ] _cache_set(cache_key, result) return result @staticmethod async def get_operatii( company_id: int, server_id: Optional[str] = None, ) -> List[OperatieItem]: """ Operații din `{schema}.DEV_NOM_NORME`. Cached 24h per schema. Full list; filter client-side. """ schema = await get_schema(company_id, server_id) cache_key = f"operatii:{schema}" cached = _cache_get(cache_key, _TTL_OPERATII) if cached is not None: return cached query = f""" SELECT id_norme, codop, denop, timpn FROM {schema}.DEV_NOM_NORME WHERE NVL(sters, 0) = 0 AND NVL(inactiv, 0) = 0 ORDER BY denop """ try: async with oracle_pool.get_connection(server_id) as conn: with conn.cursor() as cur: cur.execute(query) rows = cur.fetchall() except oracledb.DatabaseError: logger.error("get_operatii Oracle error", exc_info=True) raise HTTPException(status_code=503, detail="Eroare la încărcarea operațiilor") # Oracle treats '' as NULL, so NVL(col,'') can still yield None in Python. result = [ OperatieItem( id_norme=int(r[0]), codop=r[1] or "", denop=r[2] or "", timpn=float(r[3]) if r[3] is not None else None, ) for r in rows ] _cache_set(cache_key, result) return result @staticmethod async def search_parteneri( q: str, company_id: int, server_id: Optional[str] = None, ) -> List[PartenerItem]: """ Typeahead pe `{schema}.NOM_PARTENERI`. Min 2 chars, limit 50. No cache. Folosește IX_NOM_PARTENERI_DEN_UPPER; LIKE escape pentru %, _, \\. """ if len(q) < 2: return [] def _escape_like(s: str) -> str: return s.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") schema = await get_schema(company_id, server_id) query = f""" SELECT id_part, denumire FROM {schema}.NOM_PARTENERI WHERE UPPER(denumire) LIKE UPPER(:q) ESCAPE '\\' AND NVL(sters, 0) = 0 ORDER BY denumire FETCH FIRST 50 ROWS ONLY """ try: async with oracle_pool.get_connection(server_id) as conn: with conn.cursor() as cur: cur.execute(query, {"q": _escape_like(q) + "%"}) rows = cur.fetchall() except oracledb.DatabaseError: logger.error("search_parteneri Oracle error", exc_info=True) raise HTTPException(status_code=503, detail="Eroare la căutarea partenerilor") return [PartenerItem(id_part=int(r[0]), denumire=r[1] or "") for r in rows] @staticmethod async def create_partener( data: PartnerCreateRequest, server_id: Optional[str] = None, ) -> PartenerItem: """ Creează partener nou în `{schema}.NOM_PARTENERI`. - id_part alocat manual cu `NVL(MAX(id_part),0)+1` (nu există secvență/identity). - Pre-check unicitate CUI (NU există unique constraint pe COD_FISCAL) → 409. - PK collision (race) → ORA-00001 → retry o singură dată cu MAX+1 reactualizat. - ORA-00942/01031 (table missing / no privileges) → log.critical + 500 (lipsă GRANT). """ denumire = data.denumire.strip() cui = (data.cui or "").strip() or None adresa = (data.adresa or "").strip() or None schema = await get_schema(data.id_firma, server_id) try: async with oracle_pool.get_connection(server_id) as conn: with conn.cursor() as cur: # Pre-check duplicat CUI (doar dacă CUI a fost furnizat). if cui: cur.execute( f""" SELECT 1 FROM {schema}.NOM_PARTENERI WHERE cod_fiscal = :cui AND NVL(sters, 0) = 0 AND ROWNUM = 1 """, {"cui": cui}, ) if cur.fetchone(): raise HTTPException(status_code=409, detail="CUI duplicat") insert_sql = f""" INSERT INTO {schema}.NOM_PARTENERI (id_part, denumire, cod_fiscal, adresa, sters, inactiv, id_mod, tip_persoana) VALUES (:id_part, :denumire, :cui, :adresa, 0, 0, 0, 1) """ params_base = { "denumire": denumire, "cui": cui, "adresa": adresa, } new_id: Optional[int] = None last_err: Optional[oracledb.DatabaseError] = None for _attempt in range(2): cur.execute( f"SELECT NVL(MAX(id_part), 0) + 1 FROM {schema}.NOM_PARTENERI" ) candidate = int(cur.fetchone()[0]) try: cur.execute(insert_sql, {"id_part": candidate, **params_base}) new_id = candidate break except oracledb.DatabaseError as e: err_code = e.args[0].code if e.args else None if err_code == 1: # ORA-00001 PK race last_err = e continue raise if new_id is None: # Două încercări consecutive cu PK collision — escaladăm. if last_err is not None: raise last_err raise HTTPException( status_code=500, detail="Nu s-a putut aloca id_part după 2 încercări", ) conn.commit() except HTTPException: raise except oracledb.DatabaseError as e: err_code = e.args[0].code if e.args else None if err_code in (942, 1031): logger.critical( "create_partener: lipsă GRANT INSERT pe NOM_PARTENERI (schema=%s, ORA-%05d)", schema, err_code, exc_info=True, ) raise HTTPException( status_code=500, detail="Lipsă privilegii pe tabela NOM_PARTENERI; contactați administratorul.", ) logger.error("create_partener Oracle error", exc_info=True) raise HTTPException(status_code=503, detail="Eroare la crearea partenerului") return PartenerItem(id_part=new_id, denumire=denumire)