import re from datetime import date 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 _MAX_PER_PAGE = 100 def _handle_oracle_error(e: Exception) -> NoReturn: """ Map Oracle error codes to FastAPI HTTPExceptions. Always raises. Code ranges: 20001-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 20001 <= 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 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, ) -> ComandaResponse: logger.info( "service_auto.create_comanda START", extra={ "user": username, "tip": data.tip_id, "client_id": data.id_masiniclient, "id_firma": data.id_firma, }, ) async with oracle_pool.get_connection("mariusm_test") as connection: try: with connection.cursor() as cursor: out_id_ordl = cursor.var(oracledb.NUMBER) out_nrord = cursor.var(oracledb.STRING) cursor.callproc( "MARIUSM_AUTO.SP_CREEAZA_COMANDA_PROTOTIP", [ data.tip_id, # p_tip IN NUMBER data.id_masiniclient, # p_id_masiniclient IN NUMBER data.solicitari, # p_solicitari IN VARCHAR2 data.id_firma, # p_id_firma IN NUMBER data.id_sucursala, # p_id_sucursala IN NUMBER (None for parent firm) out_id_ordl, # p_id_ordl OUT NUMBER out_nrord, # p_nrord OUT VARCHAR2 ], ) connection.commit() id_ordl = int(out_id_ordl.getvalue()) nrord = out_nrord.getvalue() or "" 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": nrord}, ) return ComandaResponse( id_ordl=id_ordl, nrord=nrord, mesaj=f"Comanda {nrord} 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, )