feat(service-auto): săpt 3-phase2 — toate ipotezele confirmate + modul funcțional
Backend: - service_auto module complet: router, service, schemas, 5 teste suites (22/22 passed) - 5 endpoints: GET /ping, /firme, /tip-deviz, /masini, POST /comenzi - SP_CREEAZA_COMANDA_PROTOTIP creat în MARIUSM_AUTO (VALID, 5.9ms) - oracle_pool.py: session_callback backward-compat patch - ROA_WEB user: grants SP-only confirmate (H3), mariusm_test pool switchat - pyproject.toml: integration pytest marker înregistrat Frontend: - ComandaNoua.vue: date reale din Oracle (firme/tip-deviz/masini), nu hardcodate - src/modules/service-auto/services/api.js: axios service cu Bearer token - src/router/index.js: rută /service-auto/comanda-noua Docs: - decision-log.md: verdict MERGE, toate 6 ipoteze CONFIRMED - learnings.md: 7 patterns reutilizabile - grants-audit.md: arhitectura multi-tenant + proxy auth analysis + V_NOM_FIRME loop - template-modul-oracle.md: rețetă completă pentru module Oracle noi - TODO-phase2.md: 7 items concrete Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
109
backend/modules/service_auto/services/comanda_service.py
Normal file
109
backend/modules/service_auto/services/comanda_service.py
Normal file
@@ -0,0 +1,109 @@
|
||||
import re
|
||||
from typing import NoReturn
|
||||
|
||||
import oracledb
|
||||
from fastapi import HTTPException
|
||||
from shared.database.oracle_pool import oracle_pool
|
||||
from ..schemas.comanda import ComandaRequest, ComandaResponse
|
||||
from .. import logger
|
||||
|
||||
|
||||
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
|
||||
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.",
|
||||
)
|
||||
110
backend/modules/service_auto/services/lookup_service.py
Normal file
110
backend/modules/service_auto/services/lookup_service.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
Lookup data for service_auto forms — tip deviz, masini, firme.
|
||||
|
||||
All three endpoints are read-only and infrequently changing.
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
import oracledb
|
||||
from fastapi import HTTPException
|
||||
from shared.database.oracle_pool import oracle_pool
|
||||
|
||||
from ..schemas.comanda import FirmaItem, MasinaClientItem, TipDevizItem
|
||||
from .. import logger
|
||||
|
||||
|
||||
class LookupService:
|
||||
|
||||
@staticmethod
|
||||
async def get_firme(company_ids: List[str]) -> List[FirmaItem]:
|
||||
"""
|
||||
Returns firma names for the company IDs in the user's JWT.
|
||||
Uses 'central' pool (CONTAFIN_ORACLE) to query V_NOM_FIRME.
|
||||
"""
|
||||
if not company_ids:
|
||||
return []
|
||||
|
||||
placeholders = ", ".join(f":id{i}" for i in range(len(company_ids)))
|
||||
query = f"""
|
||||
SELECT id_firma, firma, schema
|
||||
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("central") 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 "")
|
||||
for r in rows
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
async def get_tip_deviz() -> List[TipDevizItem]:
|
||||
"""
|
||||
Returns all active tip deviz from MARIUSM_AUTO.DEV_TIP_DEVIZ.
|
||||
ROA_WEB has SELECT grant on this view.
|
||||
"""
|
||||
query = """
|
||||
SELECT id_tip, denumire, inch_validare
|
||||
FROM MARIUSM_AUTO.DEV_TIP_DEVIZ
|
||||
ORDER BY id_tip
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection("mariusm_test") 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")
|
||||
|
||||
return [
|
||||
TipDevizItem(id_tip=r[0], denumire=r[1], inch_validare=r[2] or 0)
|
||||
for r in rows
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
async def get_masini() -> List[MasinaClientItem]:
|
||||
"""
|
||||
Returns active masini from MARIUSM_AUTO.AUTO_VMASINICLIENTI.
|
||||
ROA_WEB has SELECT grant on this view.
|
||||
Label format: "PARTENER — MARCA MASINA, NRINMAT (ANFABRICATIE)"
|
||||
"""
|
||||
query = """
|
||||
SELECT id_masiniclient, nrinmat, marca, masina, anfabricatie, partener
|
||||
FROM MARIUSM_AUTO.AUTO_VMASINICLIENTI
|
||||
WHERE inactiv = 0
|
||||
ORDER BY partener, nrinmat
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection("mariusm_test") 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))
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user