"""FetchDataService โ€” stable contract for parental-leave data acquisition. All features (analytics, tips, planning) depend on this interface, never on FK's DOM or a specific scraper. Two concrete implementations: - ManualFetchService โ€” always ships, always works (manual user entry) - FKScraperService โ€” behind settings.scraper_enabled guard (pending legal review) See ARCHITECTURE.md ยง2.3 for the legal/ToS gate before shipping scraping. """ from __future__ import annotations from abc import ABC, abstractmethod from datetime import datetime from uuid import UUID, uuid4 from app.entities.parental_leave import ParentalLeaveFetch, TierData from app.repositories.parental_leave_repository import ParentalLeaveRepository from app.utils.errors import ValidationError class FetchDataService(ABC): @abstractmethod async def get_latest( self, personal_data_uuid: UUID, child_id: UUID ) -> ParentalLeaveFetch | None: """Return the most recent snapshot for this parent+child, or None.""" @abstractmethod async def list_history( self, personal_data_uuid: UUID, child_id: UUID ) -> list[ParentalLeaveFetch]: """Return all historical snapshots, newest first.""" class ManualFetchService(FetchDataService): """Persist user-entered FK data as a snapshot with source='manual'.""" def __init__(self, repo: ParentalLeaveRepository) -> None: self._repo = repo async def save_manual_entry( self, personal_data_uuid: UUID, workspace_id: UUID, child_id: UUID, total_days: int, days_used: int, reserved_days_total: int, reserved_days_left: int, transferable_days_left: int, dubbeldagar_left: int, sjukpenning_total: int, sjukpenning_used: int, lagsta_niva_total: int, lagsta_niva_used: int, ) -> ParentalLeaveFetch: """Validate and persist a manual data-entry snapshot.""" days_left = total_days - days_used if days_left < 0: raise ValidationError("days_used cannot exceed total_days") if reserved_days_left > reserved_days_total: raise ValidationError("reserved_days_left cannot exceed reserved_days_total") tier_data = TierData( sjukpenning_total=sjukpenning_total, sjukpenning_used=sjukpenning_used, sjukpenning_left=sjukpenning_total - sjukpenning_used, lagsta_niva_total=lagsta_niva_total, lagsta_niva_used=lagsta_niva_used, lagsta_niva_left=lagsta_niva_total - lagsta_niva_used, ) now = datetime.now() fetch = ParentalLeaveFetch( id=uuid4(), personal_data_uuid=personal_data_uuid, workspace_id=workspace_id, child_id=child_id, source="manual", fetched_at=now, total_days=total_days, days_used=days_used, days_left=days_left, reserved_days_total=reserved_days_total, reserved_days_left=reserved_days_left, transferable_days_left=transferable_days_left, dubbeldagar_left=dubbeldagar_left, tier_data=tier_data, created_at=now, updated_at=now, ) return await self._repo.create(fetch) async def get_latest( self, personal_data_uuid: UUID, child_id: UUID ) -> ParentalLeaveFetch | None: return await self._repo.get_latest_by_parent(personal_data_uuid, child_id) async def list_history( self, personal_data_uuid: UUID, child_id: UUID ) -> list[ParentalLeaveFetch]: return await self._repo.list_history(personal_data_uuid, child_id)