feat: T03+T04 backend - FastAPI, DB models, Rentman integration, admin auth, APScheduler

This commit is contained in:
Implementation Engineer
2026-07-09 01:36:46 +02:00
parent 3bfa54b4b3
commit 88cff68f73
78 changed files with 2174 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
"""Equipment sync service: import from Rentman, upsert into DB, invalidate cache."""
import logging
from datetime import datetime, timezone
from typing import Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.equipment import EquipmentCache
from app.models.sync_log import SyncLog
from app.services.rentman_service import RentmanService
from app.cache import cache
logger = logging.getLogger(__name__)
class SyncService:
"""Orchestrates equipment import from Rentman into the local database."""
def __init__(self, db: AsyncSession, rentman: RentmanService | None = None) -> None:
self.db = db
self.rentman = rentman or RentmanService()
async def run_sync(self) -> dict[str, Any]:
"""Execute a full equipment sync.
1. Create sync_log entry (status=running)
2. Paginate GET /equipment from Rentman
3. Upsert each item into equipment_cache
4. Invalidate Redis cache (equipment:*)
5. Update sync_log (status=completed or failed)
Returns dict with sync_id, items_processed, status.
"""
log_entry = SyncLog(
sync_type="equipment",
status="running",
started_at=datetime.now(timezone.utc),
)
self.db.add(log_entry)
await self.db.commit()
await self.db.refresh(log_entry)
sync_id = log_entry.id
items_processed = 0
items_failed = 0
error_message: str | None = None
try:
all_equipment = await self.rentman.get_all_equipment(limit=100)
for raw_item in all_equipment:
try:
transformed = RentmanService.transform_equipment(raw_item)
await self._upsert_equipment(transformed)
items_processed += 1
except Exception as exc:
logger.warning("Failed to upsert equipment item: %s", exc)
items_failed += 1
await self.db.commit()
# Invalidate Redis cache
await cache.delete_pattern("equipment:*")
status_val = "completed"
except Exception as exc:
logger.error("Equipment sync failed: %s", exc)
error_message = str(exc)
status_val = "failed"
# Update log entry
log_entry.status = status_val
log_entry.items_processed = items_processed
log_entry.items_failed = items_failed
log_entry.error_message = error_message
log_entry.completed_at = datetime.now(timezone.utc)
await self.db.commit()
return {
"sync_id": sync_id,
"items_processed": items_processed,
"items_failed": items_failed,
"status": status_val,
}
async def _upsert_equipment(self, data: dict[str, Any]) -> None:
"""Insert or update a single equipment row by rentman_id."""
result = await self.db.execute(
select(EquipmentCache).where(EquipmentCache.rentman_id == data["rentman_id"])
)
existing = result.scalar_one_or_none()
if existing:
existing.name = data["name"]
existing.number = data.get("number", "")
existing.category = data.get("category", "")
existing.subcategory = data.get("subcategory", "")
existing.description = data.get("description", "")
existing.specifications = data.get("specifications")
existing.images = data.get("images")
existing.rental_price = data.get("rental_price")
existing.brand = data.get("brand", "")
existing.available = data.get("available", True)
else:
new_item = EquipmentCache(
rentman_id=data["rentman_id"],
name=data["name"],
number=data.get("number", ""),
category=data.get("category", ""),
subcategory=data.get("subcategory", ""),
description=data.get("description", ""),
specifications=data.get("specifications"),
images=data.get("images"),
rental_price=data.get("rental_price"),
brand=data.get("brand", ""),
available=data.get("available", True),
)
self.db.add(new_item)
async def get_last_sync(self) -> dict[str, Any]:
"""Return the most recent sync_log entry summary."""
result = await self.db.execute(
select(SyncLog).order_by(SyncLog.started_at.desc()).limit(1)
)
log = result.scalar_one_or_none()
if not log:
return {"last_sync": None, "items_processed": 0, "status": "never"}
return {
"last_sync": log.started_at,
"items_processed": log.items_processed,
"status": log.status,
}
async def get_sync_log_paginated(self, page: int = 1, page_size: int = 20) -> dict[str, Any]:
"""Return paginated sync log entries."""
offset = (page - 1) * page_size
result = await self.db.execute(
select(SyncLog)
.order_by(SyncLog.started_at.desc())
.offset(offset)
.limit(page_size)
)
logs = result.scalars().all()
count_result = await self.db.execute(select(SyncLog))
total = len(count_result.scalars().all())
return {
"items": logs,
"total": total,
"page": page,
"page_size": page_size,
"total_pages": (total + page_size - 1) // page_size if page_size > 0 else 0,
}