feat(backend): sync endpoints + all models + seed + order workflow

- All business models: Vehicle, Order, OrderLine, Invoice, Appointment,
  CatalogMarca/Model/Ansamblu/Norma/Pret/TipDeviz/TipMotor, Mecanic
- Sync endpoints: GET /sync/full, GET /sync/changes?since=, POST /sync/push
  with tenant isolation and last-write-wins conflict resolution
- Order CRUD with state machine: DRAFT -> VALIDAT -> FACTURAT
  Auto-recalculates totals (manopera + materiale)
- Vehicle CRUD: list, create, get, update
- Seed data: 24 marci, 11 ansamble, 6 tipuri deviz, 5 tipuri motoare, 3 preturi
- Alembic migration for all business models
- 13 passing tests (auth + sync + orders)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 17:31:02 +02:00
parent ad41956ea1
commit 3a922a50e6
25 changed files with 1410 additions and 1 deletions

View File

@@ -1,4 +1,35 @@
from app.db.models.tenant import Tenant
from app.db.models.user import User
from app.db.models.vehicle import Vehicle
from app.db.models.order import Order
from app.db.models.order_line import OrderLine
from app.db.models.catalog import (
CatalogMarca,
CatalogModel,
CatalogAnsamblu,
CatalogNorma,
CatalogPret,
CatalogTipDeviz,
CatalogTipMotor,
)
from app.db.models.invoice import Invoice
from app.db.models.appointment import Appointment
from app.db.models.mecanic import Mecanic
__all__ = ["Tenant", "User"]
__all__ = [
"Tenant",
"User",
"Vehicle",
"Order",
"OrderLine",
"CatalogMarca",
"CatalogModel",
"CatalogAnsamblu",
"CatalogNorma",
"CatalogPret",
"CatalogTipDeviz",
"CatalogTipMotor",
"Invoice",
"Appointment",
"Mecanic",
]

View File

@@ -0,0 +1,11 @@
from sqlalchemy import String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
class Appointment(Base, UUIDMixin, TenantMixin, TimestampMixin):
__tablename__ = "appointments"
vehicle_id: Mapped[str] = mapped_column(String(36))
data: Mapped[str] = mapped_column(Text)
descriere: Mapped[str | None] = mapped_column(Text)

View File

@@ -0,0 +1,44 @@
from sqlalchemy import Float, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
class CatalogMarca(Base, UUIDMixin, TenantMixin, TimestampMixin):
__tablename__ = "catalog_marci"
nume: Mapped[str] = mapped_column(String(100))
class CatalogModel(Base, UUIDMixin, TimestampMixin):
__tablename__ = "catalog_modele"
marca_id: Mapped[str] = mapped_column(String(36), index=True)
nume: Mapped[str] = mapped_column(String(100))
class CatalogAnsamblu(Base, UUIDMixin, TenantMixin, TimestampMixin):
__tablename__ = "catalog_ansamble"
nume: Mapped[str] = mapped_column(String(100))
class CatalogNorma(Base, UUIDMixin, TenantMixin, TimestampMixin):
__tablename__ = "catalog_norme"
ansamblu_id: Mapped[str] = mapped_column(String(36), index=True)
descriere: Mapped[str] = mapped_column(Text)
ore: Mapped[float] = mapped_column(Float, default=0)
class CatalogPret(Base, UUIDMixin, TenantMixin, TimestampMixin):
__tablename__ = "catalog_preturi"
denumire: Mapped[str] = mapped_column(String(200))
pret: Mapped[float] = mapped_column(Float, default=0)
um: Mapped[str] = mapped_column(String(10))
class CatalogTipDeviz(Base, UUIDMixin, TenantMixin, TimestampMixin):
__tablename__ = "catalog_tipuri_deviz"
nume: Mapped[str] = mapped_column(String(100))
class CatalogTipMotor(Base, UUIDMixin, TenantMixin, TimestampMixin):
__tablename__ = "catalog_tipuri_motoare"
nume: Mapped[str] = mapped_column(String(50))

View File

@@ -0,0 +1,13 @@
from sqlalchemy import Float, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
class Invoice(Base, UUIDMixin, TenantMixin, TimestampMixin):
__tablename__ = "invoices"
order_id: Mapped[str] = mapped_column(String(36), index=True)
nr_factura: Mapped[str] = mapped_column(String(50))
data_factura: Mapped[str | None] = mapped_column(Text)
total: Mapped[float] = mapped_column(Float, default=0)
status: Mapped[str] = mapped_column(String(20), default="EMISA")

View File

@@ -0,0 +1,10 @@
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
class Mecanic(Base, UUIDMixin, TenantMixin, TimestampMixin):
__tablename__ = "mecanici"
nume: Mapped[str] = mapped_column(String(200))
telefon: Mapped[str | None] = mapped_column(String(20))

View File

@@ -0,0 +1,19 @@
from sqlalchemy import Float, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
class Order(Base, UUIDMixin, TenantMixin, TimestampMixin):
__tablename__ = "orders"
vehicle_id: Mapped[str] = mapped_column(String(36))
tip_deviz_id: Mapped[str | None] = mapped_column(String(36))
status: Mapped[str] = mapped_column(String(20), default="DRAFT")
data_comanda: Mapped[str | None] = mapped_column(Text)
km_intrare: Mapped[int | None] = mapped_column(Integer)
observatii: Mapped[str | None] = mapped_column(Text)
mecanic_id: Mapped[str | None] = mapped_column(String(36))
total_manopera: Mapped[float] = mapped_column(Float, default=0)
total_materiale: Mapped[float] = mapped_column(Float, default=0)
total_general: Mapped[float] = mapped_column(Float, default=0)
token_client: Mapped[str | None] = mapped_column(String(36))

View File

@@ -0,0 +1,17 @@
from sqlalchemy import Float, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
class OrderLine(Base, UUIDMixin, TenantMixin, TimestampMixin):
__tablename__ = "order_lines"
order_id: Mapped[str] = mapped_column(String(36), index=True)
tip: Mapped[str] = mapped_column(String(20)) # manopera | material
descriere: Mapped[str] = mapped_column(Text)
ore: Mapped[float] = mapped_column(Float, default=0)
pret_ora: Mapped[float] = mapped_column(Float, default=0)
cantitate: Mapped[float] = mapped_column(Float, default=0)
pret_unitar: Mapped[float] = mapped_column(Float, default=0)
um: Mapped[str | None] = mapped_column(String(10))
total: Mapped[float] = mapped_column(Float, default=0)

View File

@@ -0,0 +1,21 @@
from sqlalchemy import Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
class Vehicle(Base, UUIDMixin, TenantMixin, TimestampMixin):
__tablename__ = "vehicles"
nr_inmatriculare: Mapped[str] = mapped_column(String(20))
vin: Mapped[str | None] = mapped_column(String(17))
marca_id: Mapped[str | None] = mapped_column(String(36))
model_id: Mapped[str | None] = mapped_column(String(36))
an_fabricatie: Mapped[int | None] = mapped_column(Integer)
tip_motor_id: Mapped[str | None] = mapped_column(String(36))
capacitate_motor: Mapped[str | None] = mapped_column(String(20))
putere_kw: Mapped[str | None] = mapped_column(String(20))
client_nume: Mapped[str | None] = mapped_column(String(200))
client_telefon: Mapped[str | None] = mapped_column(String(20))
client_email: Mapped[str | None] = mapped_column(String(200))
client_cui: Mapped[str | None] = mapped_column(String(20))
client_adresa: Mapped[str | None] = mapped_column(Text)

119
backend/app/db/seed.py Normal file
View File

@@ -0,0 +1,119 @@
from datetime import UTC, datetime
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.base import uuid7
from app.db.models.catalog import (
CatalogAnsamblu,
CatalogMarca,
CatalogPret,
CatalogTipDeviz,
CatalogTipMotor,
)
MARCI = [
"Audi",
"BMW",
"Citroen",
"Dacia",
"Fiat",
"Ford",
"Honda",
"Hyundai",
"Kia",
"Mazda",
"Mercedes-Benz",
"Mitsubishi",
"Nissan",
"Opel",
"Peugeot",
"Renault",
"Seat",
"Skoda",
"Suzuki",
"Toyota",
"Volkswagen",
"Volvo",
"Alfa Romeo",
"Jeep",
]
ANSAMBLE = [
"Motor",
"Cutie de viteze",
"Frane",
"Directie",
"Suspensie",
"Climatizare",
"Electrica",
"Caroserie",
"Esapament",
"Transmisie",
"Revizie",
]
TIPURI_DEVIZ = [
"Deviz reparatie",
"Deviz revizie",
"Deviz diagnosticare",
"Deviz estimativ",
"Deviz vulcanizare",
"Deviz ITP",
]
TIPURI_MOTOARE = ["Benzina", "Diesel", "Hibrid", "Electric", "GPL"]
PRETURI = [
{"denumire": "Manopera standard", "pret": 150.0, "um": "ora"},
{"denumire": "Revizie ulei + filtru", "pret": 250.0, "um": "buc"},
{"denumire": "Diagnosticare", "pret": 100.0, "um": "buc"},
]
async def seed_catalog(db: AsyncSession, tenant_id: str) -> dict:
now = datetime.now(UTC).isoformat()
counts = {}
# Marci
for name in MARCI:
db.add(
CatalogMarca(id=uuid7(), tenant_id=tenant_id, nume=name)
)
counts["marci"] = len(MARCI)
# Ansamble
for name in ANSAMBLE:
db.add(
CatalogAnsamblu(id=uuid7(), tenant_id=tenant_id, nume=name)
)
counts["ansamble"] = len(ANSAMBLE)
# Tipuri deviz
for name in TIPURI_DEVIZ:
db.add(
CatalogTipDeviz(id=uuid7(), tenant_id=tenant_id, nume=name)
)
counts["tipuri_deviz"] = len(TIPURI_DEVIZ)
# Tipuri motoare
for name in TIPURI_MOTOARE:
db.add(
CatalogTipMotor(id=uuid7(), tenant_id=tenant_id, nume=name)
)
counts["tipuri_motoare"] = len(TIPURI_MOTOARE)
# Preturi
for p in PRETURI:
db.add(
CatalogPret(
id=uuid7(),
tenant_id=tenant_id,
denumire=p["denumire"],
pret=p["pret"],
um=p["um"],
)
)
counts["preturi"] = len(PRETURI)
await db.commit()
return counts

View File

@@ -7,6 +7,9 @@ from app.auth.router import router as auth_router
from app.config import settings
from app.db.base import Base
from app.db.session import engine
from app.orders.router import router as orders_router
from app.sync.router import router as sync_router
from app.vehicles.router import router as vehicles_router
# Import models so Base.metadata knows about them
import app.db.models # noqa: F401
@@ -28,6 +31,9 @@ app.add_middleware(
allow_credentials=True,
)
app.include_router(auth_router, prefix="/api/auth")
app.include_router(sync_router, prefix="/api/sync")
app.include_router(orders_router, prefix="/api/orders")
app.include_router(vehicles_router, prefix="/api/vehicles")
@app.get("/api/health")

View File

View File

@@ -0,0 +1,83 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.deps import get_tenant_id
from app.orders import schemas, service
router = APIRouter()
@router.get("")
async def list_orders(
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
return await service.list_orders(db, tenant_id)
@router.post("")
async def create_order(
data: schemas.CreateOrderRequest,
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
order = await service.create_order(
db,
tenant_id,
data.vehicle_id,
data.tip_deviz_id,
data.km_intrare,
data.observatii,
)
return {"id": order.id}
@router.get("/{order_id}")
async def get_order(
order_id: str,
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
result = await service.get_order(db, tenant_id, order_id)
if not result:
raise HTTPException(status_code=404, detail="Order not found")
return result
@router.post("/{order_id}/lines")
async def add_line(
order_id: str,
data: schemas.AddLineRequest,
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
try:
line = await service.add_line(
db,
tenant_id,
order_id,
data.tip,
data.descriere,
data.ore,
data.pret_ora,
data.cantitate,
data.pret_unitar,
data.um,
)
return {"id": line.id}
except ValueError as e:
raise HTTPException(status_code=422, detail=str(e))
@router.post("/{order_id}/validate")
async def validate_order(
order_id: str,
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
try:
order = await service.validate_order(db, tenant_id, order_id)
return {"status": order.status}
except ValueError as e:
raise HTTPException(status_code=422, detail=str(e))

View File

@@ -0,0 +1,18 @@
from pydantic import BaseModel
class CreateOrderRequest(BaseModel):
vehicle_id: str
tip_deviz_id: str | None = None
km_intrare: int | None = None
observatii: str | None = None
class AddLineRequest(BaseModel):
tip: str # manopera | material
descriere: str
ore: float = 0
pret_ora: float = 0
cantitate: float = 0
pret_unitar: float = 0
um: str | None = None

View File

@@ -0,0 +1,188 @@
from datetime import UTC, datetime
from sqlalchemy import select, text
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.base import uuid7
from app.db.models.order import Order
from app.db.models.order_line import OrderLine
TRANSITIONS = {"DRAFT": ["VALIDAT"], "VALIDAT": ["FACTURAT"]}
async def create_order(
db: AsyncSession,
tenant_id: str,
vehicle_id: str,
tip_deviz_id: str | None = None,
km_intrare: int | None = None,
observatii: str | None = None,
) -> Order:
now = datetime.now(UTC).isoformat()
order = Order(
id=uuid7(),
tenant_id=tenant_id,
vehicle_id=vehicle_id,
tip_deviz_id=tip_deviz_id,
status="DRAFT",
data_comanda=now.split("T")[0],
km_intrare=km_intrare,
observatii=observatii,
total_manopera=0,
total_materiale=0,
total_general=0,
token_client=uuid7(),
)
db.add(order)
await db.commit()
await db.refresh(order)
return order
async def add_line(
db: AsyncSession,
tenant_id: str,
order_id: str,
tip: str,
descriere: str,
ore: float = 0,
pret_ora: float = 0,
cantitate: float = 0,
pret_unitar: float = 0,
um: str | None = None,
) -> OrderLine:
# Check order exists and belongs to tenant
r = await db.execute(
select(Order).where(Order.id == order_id, Order.tenant_id == tenant_id)
)
order = r.scalar_one_or_none()
if not order:
raise ValueError("Order not found")
if order.status != "DRAFT":
raise ValueError("Cannot add lines to non-DRAFT order")
if tip == "manopera":
total = ore * pret_ora
else:
total = cantitate * pret_unitar
line = OrderLine(
id=uuid7(),
tenant_id=tenant_id,
order_id=order_id,
tip=tip,
descriere=descriere,
ore=ore,
pret_ora=pret_ora,
cantitate=cantitate,
pret_unitar=pret_unitar,
um=um,
total=total,
)
db.add(line)
await db.commit()
await recalc_totals(db, order_id)
return line
async def recalc_totals(db: AsyncSession, order_id: str):
lines = await db.execute(
text(
"SELECT tip, COALESCE(SUM(total), 0) as sub FROM order_lines "
"WHERE order_id = :oid GROUP BY tip"
),
{"oid": order_id},
)
totals = {r.tip: r.sub for r in lines}
manopera = totals.get("manopera", 0)
materiale = totals.get("material", 0)
await db.execute(
text(
"UPDATE orders SET total_manopera=:m, total_materiale=:mat, "
"total_general=:g, updated_at=:u WHERE id=:id"
),
{
"m": manopera,
"mat": materiale,
"g": manopera + materiale,
"u": datetime.now(UTC).isoformat(),
"id": order_id,
},
)
await db.commit()
async def validate_order(
db: AsyncSession, tenant_id: str, order_id: str
) -> Order:
r = await db.execute(
select(Order).where(Order.id == order_id, Order.tenant_id == tenant_id)
)
order = r.scalar_one_or_none()
if not order:
raise ValueError("Order not found")
if "VALIDAT" not in TRANSITIONS.get(order.status, []):
raise ValueError(f"Cannot transition from {order.status} to VALIDAT")
order.status = "VALIDAT"
order.updated_at = datetime.now(UTC).isoformat()
await db.commit()
await db.refresh(order)
return order
async def get_order(
db: AsyncSession, tenant_id: str, order_id: str
) -> dict | None:
r = await db.execute(
select(Order).where(Order.id == order_id, Order.tenant_id == tenant_id)
)
order = r.scalar_one_or_none()
if not order:
return None
r = await db.execute(
select(OrderLine).where(OrderLine.order_id == order_id)
)
lines = r.scalars().all()
return {
"id": order.id,
"vehicle_id": order.vehicle_id,
"status": order.status,
"data_comanda": order.data_comanda,
"km_intrare": order.km_intrare,
"observatii": order.observatii,
"total_manopera": order.total_manopera,
"total_materiale": order.total_materiale,
"total_general": order.total_general,
"token_client": order.token_client,
"lines": [
{
"id": l.id,
"tip": l.tip,
"descriere": l.descriere,
"ore": l.ore,
"pret_ora": l.pret_ora,
"cantitate": l.cantitate,
"pret_unitar": l.pret_unitar,
"um": l.um,
"total": l.total,
}
for l in lines
],
}
async def list_orders(db: AsyncSession, tenant_id: str) -> list:
r = await db.execute(
select(Order).where(Order.tenant_id == tenant_id)
)
orders = r.scalars().all()
return [
{
"id": o.id,
"status": o.status,
"vehicle_id": o.vehicle_id,
"total_general": o.total_general,
}
for o in orders
]

View File

View File

@@ -0,0 +1,41 @@
from datetime import UTC, datetime
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.deps import get_tenant_id
from app.sync import schemas, service
router = APIRouter()
@router.get("/full")
async def sync_full(
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
tables = await service.get_full(db, tenant_id)
return {"tables": tables, "synced_at": datetime.now(UTC).isoformat()}
@router.get("/changes")
async def sync_changes(
since: str = Query(...),
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
tables = await service.get_changes(db, tenant_id, since)
return {"tables": tables, "synced_at": datetime.now(UTC).isoformat()}
@router.post("/push", response_model=schemas.SyncPushResponse)
async def sync_push(
data: schemas.SyncPushRequest,
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
result = await service.apply_push(
db, tenant_id, [op.model_dump() for op in data.operations]
)
return result

View File

@@ -0,0 +1,18 @@
from pydantic import BaseModel
class SyncOperation(BaseModel):
table: str
id: str
operation: str # INSERT | UPDATE | DELETE
data: dict = {}
timestamp: str
class SyncPushRequest(BaseModel):
operations: list[SyncOperation]
class SyncPushResponse(BaseModel):
applied: int
conflicts: list = []

110
backend/app/sync/service.py Normal file
View File

@@ -0,0 +1,110 @@
from datetime import UTC, datetime
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
SYNCABLE_TABLES = [
"vehicles",
"orders",
"order_lines",
"invoices",
"appointments",
"catalog_marci",
"catalog_modele",
"catalog_ansamble",
"catalog_norme",
"catalog_preturi",
"catalog_tipuri_deviz",
"catalog_tipuri_motoare",
"mecanici",
]
# Tables that don't have tenant_id directly
NO_TENANT_TABLES = {"catalog_modele"}
async def get_full(db: AsyncSession, tenant_id: str) -> dict:
result = {}
for table in SYNCABLE_TABLES:
if table == "catalog_modele":
rows = await db.execute(
text(
"SELECT cm.* FROM catalog_modele cm "
"JOIN catalog_marci marc ON cm.marca_id = marc.id "
"WHERE marc.tenant_id = :tid"
),
{"tid": tenant_id},
)
else:
rows = await db.execute(
text(f"SELECT * FROM {table} WHERE tenant_id = :tid"),
{"tid": tenant_id},
)
result[table] = [dict(r._mapping) for r in rows]
return result
async def get_changes(db: AsyncSession, tenant_id: str, since: str) -> dict:
result = {}
for table in SYNCABLE_TABLES:
if table == "catalog_modele":
rows = await db.execute(
text(
"SELECT cm.* FROM catalog_modele cm "
"JOIN catalog_marci marc ON cm.marca_id = marc.id "
"WHERE marc.tenant_id = :tid AND cm.updated_at > :since"
),
{"tid": tenant_id, "since": since},
)
else:
rows = await db.execute(
text(
f"SELECT * FROM {table} WHERE tenant_id = :tid AND updated_at > :since"
),
{"tid": tenant_id, "since": since},
)
rows_list = [dict(r._mapping) for r in rows]
if rows_list:
result[table] = rows_list
return result
async def apply_push(
db: AsyncSession, tenant_id: str, operations: list
) -> dict:
applied = 0
for op in operations:
table = op["table"]
if table not in SYNCABLE_TABLES:
continue
data = op.get("data", {})
# Enforce tenant isolation (except for no-tenant tables)
if table not in NO_TENANT_TABLES:
if data.get("tenant_id") and data["tenant_id"] != tenant_id:
continue
data["tenant_id"] = tenant_id
if op["operation"] in ("INSERT", "UPDATE"):
cols = ", ".join(data.keys())
ph = ", ".join(f":{k}" for k in data.keys())
await db.execute(
text(f"INSERT OR REPLACE INTO {table} ({cols}) VALUES ({ph})"),
data,
)
applied += 1
elif op["operation"] == "DELETE":
if table in NO_TENANT_TABLES:
await db.execute(
text(f"DELETE FROM {table} WHERE id = :id"),
{"id": op["id"]},
)
else:
await db.execute(
text(
f"DELETE FROM {table} WHERE id = :id AND tenant_id = :tid"
),
{"id": op["id"], "tid": tenant_id},
)
applied += 1
await db.commit()
return {"applied": applied, "conflicts": []}

View File

View File

@@ -0,0 +1,110 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.base import uuid7
from app.db.models.vehicle import Vehicle
from app.db.session import get_db
from app.deps import get_tenant_id
from app.vehicles import schemas
router = APIRouter()
@router.get("")
async def list_vehicles(
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
r = await db.execute(
select(Vehicle).where(Vehicle.tenant_id == tenant_id)
)
vehicles = r.scalars().all()
return [
{
"id": v.id,
"nr_auto": v.nr_inmatriculare,
"marca_id": v.marca_id,
"model_id": v.model_id,
"an": v.an_fabricatie,
"client_nume": v.client_nume,
}
for v in vehicles
]
@router.post("")
async def create_vehicle(
data: schemas.CreateVehicleRequest,
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
vehicle = Vehicle(
id=uuid7(),
tenant_id=tenant_id,
nr_inmatriculare=data.nr_auto,
marca_id=data.marca_id,
model_id=data.model_id,
an_fabricatie=data.an_fabricatie,
vin=data.vin,
client_nume=data.proprietar_nume,
client_telefon=data.proprietar_telefon,
)
db.add(vehicle)
await db.commit()
return {"id": vehicle.id}
@router.get("/{vehicle_id}")
async def get_vehicle(
vehicle_id: str,
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
r = await db.execute(
select(Vehicle).where(
Vehicle.id == vehicle_id, Vehicle.tenant_id == tenant_id
)
)
v = r.scalar_one_or_none()
if not v:
raise HTTPException(status_code=404, detail="Vehicle not found")
return {
"id": v.id,
"nr_auto": v.nr_inmatriculare,
"marca_id": v.marca_id,
"model_id": v.model_id,
"an": v.an_fabricatie,
"vin": v.vin,
"client_nume": v.client_nume,
"client_telefon": v.client_telefon,
"client_email": v.client_email,
}
@router.put("/{vehicle_id}")
async def update_vehicle(
vehicle_id: str,
data: schemas.UpdateVehicleRequest,
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
r = await db.execute(
select(Vehicle).where(
Vehicle.id == vehicle_id, Vehicle.tenant_id == tenant_id
)
)
v = r.scalar_one_or_none()
if not v:
raise HTTPException(status_code=404, detail="Vehicle not found")
update_data = data.model_dump(exclude_unset=True)
if "nr_auto" in update_data:
update_data["nr_inmatriculare"] = update_data.pop("nr_auto")
if "proprietar_nume" in update_data:
update_data["client_nume"] = update_data.pop("proprietar_nume")
if "proprietar_telefon" in update_data:
update_data["client_telefon"] = update_data.pop("proprietar_telefon")
for key, value in update_data.items():
setattr(v, key, value)
await db.commit()
return {"ok": True}

View File

@@ -0,0 +1,21 @@
from pydantic import BaseModel
class CreateVehicleRequest(BaseModel):
nr_auto: str
marca_id: str | None = None
model_id: str | None = None
an_fabricatie: int | None = None
vin: str | None = None
proprietar_nume: str | None = None
proprietar_telefon: str | None = None
class UpdateVehicleRequest(BaseModel):
nr_auto: str | None = None
marca_id: str | None = None
model_id: str | None = None
an_fabricatie: int | None = None
vin: str | None = None
proprietar_nume: str | None = None
proprietar_telefon: str | None = None