Fix require_cap TypeError, rewrite /analysis/comparison, add room category sync

- require_cap was a Depends-factory but every call site uses it inline;
  make it an inline checker (fixes 500 on /analysis/hotels, /direct/*)
- /analysis/comparison returned a per-date matrix the frontend never read;
  return per-hotel aggregates (our/their avg, price index) and default to
  all active competitors so the Market Comparison table works without params
- Room categories were never populated (lost in port): add sites_list fetch
  to the Newbook client, categories list/sync/toggle endpoints, and a
  Settings card — without included categories every rates sync exits early

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-05 13:47:10 +00:00
parent c9f2dc804c
commit 69c9b16a2b
6 changed files with 5787 additions and 52 deletions

View file

@ -62,6 +62,95 @@ class RateMatrixResponse(BaseModel):
date_last_updated: Dict[str, Optional[str]] = {}
# ============================================
# ROOM CATEGORIES
# ============================================
class RoomCategory(BaseModel):
id: int
site_id: str
site_name: str
room_count: int
is_included: bool
display_order: int
class CategoryUpdate(BaseModel):
is_included: bool
@router.get("/categories", response_model=List[RoomCategory])
async def list_categories(
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""List Newbook room categories and their include-in-sync status."""
result = await db.execute(
text("""
SELECT id, site_id, site_name, room_count, is_included, display_order
FROM newbook_room_categories
ORDER BY display_order, site_name
""")
)
return [dict(r) for r in result.mappings().all()]
@router.post("/categories/sync")
async def sync_categories(
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""
Fetch room categories from the Newbook API (sites_list) and upsert them.
New categories default to included; existing include flags are preserved.
"""
from services.newbook_rates_client import NewbookRatesClient
client = await NewbookRatesClient.from_db(db)
try:
async with client:
categories = await client.get_room_categories()
except Exception as e:
logger.error(f"Category sync failed: {e}")
raise HTTPException(status_code=502, detail=f"Newbook category fetch failed: {str(e)}")
if not categories:
raise HTTPException(status_code=502, detail="Newbook returned no room categories — check credentials in Settings")
for cat in categories:
await db.execute(
text("""
INSERT INTO newbook_room_categories (site_id, site_name, room_count, fetched_at)
VALUES (:site_id, :site_name, :room_count, NOW())
ON CONFLICT (site_id) DO UPDATE SET
site_name = EXCLUDED.site_name,
room_count = EXCLUDED.room_count,
fetched_at = NOW()
"""),
{"site_id": cat["category_id"], "site_name": cat["category_name"], "room_count": cat["room_count"]}
)
await db.commit()
return {"status": "success", "count": len(categories)}
@router.patch("/categories/{category_id}")
async def update_category(
category_id: int,
body: CategoryUpdate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""Toggle whether a category is included in rate syncs."""
result = await db.execute(
text("UPDATE newbook_room_categories SET is_included = :inc WHERE id = :id RETURNING id"),
{"inc": body.is_included, "id": category_id}
)
if not result.fetchone():
raise HTTPException(status_code=404, detail="Category not found")
await db.commit()
return {"status": "success"}
# ============================================
# RATE MATRIX ENDPOINT
# ============================================