"""Equipment sync service: import from Rentman, upsert into DB, invalidate cache.""" import logging from datetime import datetime 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.utcnow(), ) 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.utcnow() 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, }