import re from datetime import date, datetime from typing import List, NoReturn, Optional import oracledb from fastapi import HTTPException from shared.database.oracle_pool import oracle_pool from ..schemas.comanda import ( ComandaListItem, ComandaListResponse, ComandaRequest, ComandaResponse, ) from .. import logger _SCHEMA = "MARIUSM_AUTO" _MAX_PER_PAGE = 100 def _handle_oracle_error(e: Exception) -> NoReturn: """ Map Oracle error codes to FastAPI HTTPExceptions. Always raises. Code ranges: 20000-20999 → 422 Unprocessable (business rule errors from RAISE_APPLICATION_ERROR) 12541/12170/12154/12560 → 503 Service Unavailable (Oracle unreachable / network) 1017 → 500 + CRITICAL log (bad credentials — config error) 942 → 500 + CRITICAL log (missing object/grant — deployment error) * → 500 + ERROR log (unexpected) """ err = e.args[0] code = getattr(err, "code", 0) raw_message = getattr(err, "message", str(e)) if 20000 <= code <= 20999: # Strip "ORA-2xxxx: " prefix injected by Oracle; expose the business message only clean = re.sub(r"^ORA-\d+:\s*", "", raw_message).strip() raise HTTPException(status_code=422, detail=clean) if code == 1438: # ORA-01438: value larger than specified precision — bad input ID (e.g. id_masiniclient out of range) raise HTTPException(status_code=422, detail="Valoare invalidă pentru câmp (ID prea mare)") if code in (12541, 12170, 12154, 12560): raise HTTPException( status_code=503, detail="Serviciul bazei de date e temporar indisponibil", ) if code == 1017: logger.critical("Oracle credentials rejected (ORA-01017)", exc_info=True) raise HTTPException( status_code=500, detail="Eroare de configurare. Contactați administratorul.", ) if code == 942: logger.critical("Oracle object not found or grant missing (ORA-00942)", exc_info=True) raise HTTPException( status_code=500, detail="Eroare internă. Contactați administratorul.", ) logger.error("Unexpected Oracle error ORA-%05d", code, exc_info=True) raise HTTPException(status_code=500, detail="Eroare internă neașteptată") class ComandaService: @staticmethod async def creeaza_comanda( data: ComandaRequest, username: str, user_id: Optional[int] = None, ) -> ComandaResponse: now = datetime.now() # pcNr serves as NOM_LUCRARI.NRORD — must be unique; timestamp gives collision-safe value. pc_nr = f"W{now.strftime('%Y%m%d%H%M%S')}{now.microsecond // 1000:03d}" logger.info( "service_auto.create_comanda START", extra={ "user": username, "tip": data.tip_id, "client_id": data.id_masiniclient, "id_firma": data.id_firma, "pc_nr": pc_nr, "km": data.km_int, "ore": data.ore_functionare, }, ) async with oracle_pool.get_connection("mariusm_test") as connection: try: with connection.cursor() as cursor: # pnIdOrdl is IN OUT — setvalue(0, 0) sets the IN side to 0; # Oracle overwrites it with the new DEV_ORDL.ID_ORDL. out_id_ordl = cursor.var(oracledb.NUMBER) out_id_ordl.setvalue(0, 0) cursor.callproc( f"{_SCHEMA}.PACK_AUTO.dev_adauga_lucrare", [ _SCHEMA, # v_gcs IN VARCHAR2 now.year, # tnan IN NUMBER now.month, # tnluna IN NUMBER user_id or 0, # tnIdUtil IN NUMBER (Oracle ID_UTIL) pc_nr, # pcNr IN VARCHAR2 (NOM_LUCRARI.NRORD) None, # pnIdInsp IN NUMBER None, # pnIdAsig IN NUMBER data.nr_dosar or "", # pcNrDosar IN VARCHAR2 data.id_masiniclient, # pnIdMC IN NUMBER data.km_int, # pnKmInt IN NUMBER data.ore_functionare, # pnOreFct IN NUMBER (≥0; NOT NULL in DEV_MASINICLIENTI) data.termen, # pdTermen IN DATE data.tip_id, # pnTipCom IN NUMBER None, # pcSirIdOperatii IN VARCHAR2 — MUST be None, NOT '' data.observatii or None, # pcObservatii IN VARCHAR2 DEFAULT NULL data.defectiuni or None, # pcDefectiuni IN VARCHAR2 DEFAULT NULL 0, # pnIdPartRef IN NUMBER (decode(0) → NULL inside SP) out_id_ordl, # pnIdOrdl IN OUT NUMBER ], ) connection.commit() id_ordl = int(out_id_ordl.getvalue()) except oracledb.DatabaseError as e: try: connection.rollback() except Exception: pass # connection may be dead on network errors; ignore _handle_oracle_error(e) logger.info( "service_auto.create_comanda OK", extra={"user": username, "id_ordl": id_ordl, "nrord": pc_nr}, ) return ComandaResponse( id_ordl=id_ordl, nrord=pc_nr, mesaj=f"Comanda {pc_nr} creata cu succes.", ) @staticmethod async def get_comenzi( page: int, per_page: int, validat: Optional[int], data_de_la: Optional[date], data_pana_la: Optional[date], ) -> ComandaListResponse: per_page = min(per_page, _MAX_PER_PAGE) offset = (page - 1) * per_page where_parts = ["d.sters = 0"] filter_params: dict = {} if validat is not None: where_parts.append("d.validat = :validat") filter_params["validat"] = validat if data_de_la is not None: where_parts.append("d.datai >= :data_de_la") filter_params["data_de_la"] = data_de_la if data_pana_la is not None: # +1 day range avoids TRUNC (keeps index use on datai) where_parts.append("d.datai < :data_pana_la + 1") filter_params["data_pana_la"] = data_pana_la where_clause = " AND ".join(where_parts) base_from = f""" FROM MARIUSM_AUTO.DEV_ORDL d LEFT JOIN MARIUSM_AUTO.NOM_LUCRARI l ON d.id_lucrare = l.id_lucrare LEFT JOIN MARIUSM_AUTO.AUTO_VMASINICLIENTI mc ON d.id_masiniclient = mc.id_masiniclient LEFT JOIN MARIUSM_AUTO.DEV_TIP_DEVIZ t ON d.id_tip = t.id_tip WHERE {where_clause} """ count_query = f"SELECT COUNT(*) {base_from}" data_query = f""" SELECT d.id_ordl, l.nrord, d.datai, d.validat, d.inchis_fortat, d.id_tip, t.denumire, d.id_masiniclient, mc.nrinmat, mc.marca, mc.masina, mc.anfabricatie, mc.partener {base_from} ORDER BY d.datai DESC, d.id_ordl DESC OFFSET :offset ROWS FETCH NEXT :per_page ROWS ONLY """ try: async with oracle_pool.get_connection("mariusm_test") as conn: with conn.cursor() as cur: cur.execute(count_query, filter_params) total = cur.fetchone()[0] cur.execute( data_query, {**filter_params, "offset": offset, "per_page": per_page}, ) rows = cur.fetchall() except oracledb.DatabaseError as e: _handle_oracle_error(e) comenzi: List[ComandaListItem] = [] for r in rows: (id_ordl, nrord, datai, validat_val, inchis_fortat, id_tip, tip_denumire, id_mc, nrinmat, marca, masina, an, partener) = r if id_mc: parts = [] if marca: parts.append(marca) if masina: parts.append(masina) vehicul_str = " ".join(parts) if parts else "?" an_str = f" ({int(an)})" if an else "" vehicul = f"{partener or '?'} — {vehicul_str}, {nrinmat or '?'}{an_str}" else: vehicul = "" comenzi.append(ComandaListItem( id_ordl=int(id_ordl), nrord=nrord or "", datai=datai.strftime("%Y-%m-%d") if datai else None, validat=int(validat_val), inchis_fortat=int(inchis_fortat or 0), id_tip=int(id_tip), tip_denumire=tip_denumire or "", vehicul=vehicul, id_masiniclient=int(id_mc) if id_mc else None, )) logger.debug( "service_auto.get_comenzi page=%d per_page=%d total=%d", page, per_page, total, ) return ComandaListResponse( comenzi=comenzi, total=total, page=page, per_page=per_page, )