from datetime import datetime from typing import Any from uuid import UUID from fastapi import APIRouter, Depends, Query from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession from src.api.v1.dependencies.auth import get_current_user, require_role from src.core.exceptions import NotFoundException from src.domain.entities import SyncJobStatus, SyncStrategy, UserRole from src.infrastructure.database.repositories import SyncJobRepository, SyncLogRepository from src.infrastructure.database.session import get_db_session from src.services.auth.security import TokenData from src.services.synchronization.sync_service import SyncService router = APIRouter(prefix="/pharmacies/{pharmacy_id}/sync-jobs", tags=["Sync Jobs"]) class SyncJobCreateRequest(BaseModel): connector_id: UUID strategy: SyncStrategy = SyncStrategy.INCREMENTAL class SyncJobResponse(BaseModel): id: UUID pharmacy_id: UUID connector_id: UUID strategy: str status: str started_at: datetime | None completed_at: datetime | None items_total: int | None items_processed: int | None items_failed: int | None error_message: str | None metadata: dict[str, Any] | None created_at: datetime | None updated_at: datetime | None class SyncJobListResponse(BaseModel): items: list[SyncJobResponse] total: int offset: int limit: int class SyncLogResponse(BaseModel): id: UUID sync_job_id: UUID level: str message: str details: dict[str, Any] | None created_at: datetime | None def _entity_to_response(e: Any) -> SyncJobResponse: return SyncJobResponse( id=e.id, pharmacy_id=e.pharmacy_id, connector_id=e.connector_id, strategy=e.strategy, status=e.status, started_at=e.started_at, completed_at=e.completed_at, items_total=e.items_total, items_processed=e.items_processed, items_failed=e.items_failed, error_message=e.error_message, metadata=e.metadata_, created_at=e.created_at, updated_at=e.updated_at, ) def _get_service(session: AsyncSession) -> SyncService: return SyncService( sync_job_repo=SyncJobRepository(session), sync_log_repo=SyncLogRepository(session), ) @router.get("", response_model=SyncJobListResponse) async def list_sync_jobs( pharmacy_id: UUID, status: str | None = None, offset: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=200), session: AsyncSession = Depends(get_db_session), current_user: TokenData = Depends(get_current_user), ): service = _get_service(session) entities, total = await service.list_sync_jobs(pharmacy_id, status=status, offset=offset, limit=limit) return SyncJobListResponse(items=[_entity_to_response(e) for e in entities], total=total, offset=offset, limit=limit) @router.get("/{job_id}", response_model=SyncJobResponse) async def get_sync_job( pharmacy_id: UUID, job_id: UUID, session: AsyncSession = Depends(get_db_session), current_user: TokenData = Depends(get_current_user), ): service = _get_service(session) entity = await service.get_sync_job(job_id) if not entity or entity.pharmacy_id != pharmacy_id: raise NotFoundException("SyncJob", str(job_id)) return _entity_to_response(entity) @router.post("", response_model=SyncJobResponse, status_code=201) async def create_sync_job( pharmacy_id: UUID, body: SyncJobCreateRequest, session: AsyncSession = Depends(get_db_session), current_user: TokenData = Depends(require_role(UserRole.OPERATOR)), ): service = _get_service(session) created = await service.create_sync_job( pharmacy_id=pharmacy_id, connector_id=body.connector_id, strategy=body.strategy, ) return _entity_to_response(created) @router.post("/{job_id}/trigger", response_model=SyncJobResponse) async def trigger_sync( pharmacy_id: UUID, job_id: UUID, session: AsyncSession = Depends(get_db_session), current_user: TokenData = Depends(require_role(UserRole.OPERATOR)), ): service = _get_service(session) updated = await service.trigger_sync(job_id) return _entity_to_response(updated) @router.post("/{job_id}/cancel", response_model=SyncJobResponse) async def cancel_sync( pharmacy_id: UUID, job_id: UUID, session: AsyncSession = Depends(get_db_session), current_user: TokenData = Depends(require_role(UserRole.OPERATOR)), ): service = _get_service(session) updated = await service.cancel_sync(job_id) return _entity_to_response(updated) class SyncCompleteRequest(BaseModel): items_total: int | None = None items_processed: int | None = None items_failed: int | None = None duration_ms: int = 0 class SyncFailRequest(BaseModel): error_message: str @router.post("/{job_id}/complete", response_model=SyncJobResponse) async def complete_sync( pharmacy_id: UUID, job_id: UUID, body: SyncCompleteRequest, session: AsyncSession = Depends(get_db_session), current_user: TokenData = Depends(require_role(UserRole.OPERATOR)), ): service = _get_service(session) updated = await service.complete_sync(job_id, items_total=body.items_total, items_processed=body.items_processed, items_failed=body.items_failed) return _entity_to_response(updated) @router.post("/{job_id}/fail", response_model=SyncJobResponse) async def fail_sync( pharmacy_id: UUID, job_id: UUID, body: SyncFailRequest, session: AsyncSession = Depends(get_db_session), current_user: TokenData = Depends(require_role(UserRole.OPERATOR)), ): service = _get_service(session) updated = await service.fail_sync(job_id, error_message=body.error_message) return _entity_to_response(updated) @router.get("/{job_id}/logs", response_model=list[SyncLogResponse]) async def get_sync_logs( pharmacy_id: UUID, job_id: UUID, level: str | None = None, offset: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=500), session: AsyncSession = Depends(get_db_session), current_user: TokenData = Depends(get_current_user), ): service = _get_service(session) logs = await service.get_sync_logs(job_id, level=level, offset=offset, limit=limit) return [ SyncLogResponse( id=l.id, sync_job_id=l.sync_job_id, level=l.level, message=l.message, details=l.details, created_at=l.created_at, ) for l in logs ]