Files
hms-licht-ton/backend/app/routers/equipment.py
T
Agent Zero df50ea8b4b fix: repair Rentman equipment sync with real API field mapping
- Map real Rentman fields: price -> rental_price, folder -> category/subcategory
  (resolved via folder hierarchy to top-level categories), code -> number,
  shop_description_long (fallback short/external_remark) -> description (HTML stripped)
- Sync only in_shop items; exclude archived/temporary from public catalog
- Extract known brands from equipment names (d&b, Shure, Pioneer, ...)
- Build specifications from physical fields (power, weight, dimensions)
- Persist equipment images in docker volume (survive redeploys)
- Router: filter available items, add price_asc/price_desc sorting
- Frontend: show prices in catalog card, detail page and cart with
  per-day subtotal; price sorting options
- Tests: use real Rentman field names in fixtures, mock folder_map
2026-09-24 21:26:18 +02:00

110 lines
4.4 KiB
Python

"""Equipment API router: list, detail, categories, image."""
import os
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi.responses import FileResponse
from sqlalchemy import select, func, or_
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Any
from app.database import get_db
from app.models.equipment import EquipmentCache
from app.schemas.equipment import EquipmentItem, EquipmentDetail, PaginatedResponse
from app.cache import cache
router = APIRouter(prefix="/api/equipment", tags=["equipment"])
IMAGES_DIR = "/data/images/equipment"
@router.get("", response_model=PaginatedResponse)
async def list_equipment(
search: str | None = Query(None),
category: str | None = Query(None),
sort: str | None = Query("name_asc"),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
) -> Any:
"""Return paginated equipment list with optional search/filter/sort."""
cache_key = f"equipment:list:{search}:{category}:{sort}:{page}:{page_size}"
cached = await cache.get(cache_key)
if cached:
return cached
query = select(EquipmentCache).where(EquipmentCache.available == True) # noqa: E712
count_query = select(func.count(EquipmentCache.id)).where(EquipmentCache.available == True) # noqa: E712
if search:
query = query.where(EquipmentCache.name.ilike(f"%{search}%"))
count_query = count_query.where(EquipmentCache.name.ilike(f"%{search}%"))
if category:
query = query.where(EquipmentCache.category == category)
count_query = count_query.where(EquipmentCache.category == category)
if sort == "name_desc":
query = query.order_by(EquipmentCache.name.desc())
elif sort == "price_asc":
query = query.order_by(EquipmentCache.rental_price.asc().nulls_last(), EquipmentCache.name.asc())
elif sort == "price_desc":
query = query.order_by(EquipmentCache.rental_price.desc().nulls_last(), EquipmentCache.name.asc())
else:
query = query.order_by(EquipmentCache.name.asc())
total_result = await db.execute(count_query)
total = total_result.scalar() or 0
offset = (page - 1) * page_size
result = await db.execute(query.offset(offset).limit(page_size))
items = result.scalars().all()
response = {
"items": [EquipmentItem.model_validate(item).model_dump() for item in items],
"total": total,
"page": page,
"page_size": page_size,
"total_pages": (total + page_size - 1) // page_size if page_size > 0 else 0,
}
await cache.set(cache_key, response, ttl=3600)
return response
@router.get("/categories", response_model=list[str])
async def list_categories(db: AsyncSession = Depends(get_db)) -> Any:
"""Return all distinct equipment categories."""
cached = await cache.get("equipment:categories")
if cached:
return cached
result = await db.execute(
select(EquipmentCache.category).distinct().where(
EquipmentCache.category.isnot(None), EquipmentCache.available == True # noqa: E712
)
)
categories = [row[0] for row in result.fetchall() if row[0]]
await cache.set("equipment:categories", categories, ttl=3600)
return categories
@router.get("/{equipment_id}/image")
async def get_equipment_image(equipment_id: int, db: AsyncSession = Depends(get_db)) -> Any:
"""Serve the locally stored image for an equipment item."""
image_path = os.path.join(IMAGES_DIR, f"{equipment_id}.jpg")
if os.path.exists(image_path):
return FileResponse(image_path, media_type="image/jpeg")
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Image not found")
@router.get("/{equipment_id}", response_model=EquipmentDetail)
async def get_equipment(equipment_id: int, db: AsyncSession = Depends(get_db)) -> Any:
"""Return a single equipment detail by ID."""
cache_key = f"equipment:detail:{equipment_id}"
cached = await cache.get(cache_key)
if cached:
return cached
result = await db.execute(select(EquipmentCache).where(EquipmentCache.id == equipment_id))
item = result.scalar_one_or_none()
if not item:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found")
response = EquipmentDetail.model_validate(item).model_dump()
await cache.set(cache_key, response, ttl=3600)
return response