# services/external/app/repositories/calendar_repository.py """ Calendar Repository - Manages school calendars and tenant location contexts """ from typing import List, Dict, Any, Optional from datetime import datetime from sqlalchemy import select, and_, or_ from sqlalchemy.ext.asyncio import AsyncSession import structlog import uuid from app.models.calendar import SchoolCalendar, TenantLocationContext logger = structlog.get_logger() class CalendarRepository: """Repository for school calendar and tenant location data""" def __init__(self, session: AsyncSession): self.session = session # ===== School Calendar Operations ===== async def create_school_calendar( self, city_id: str, calendar_name: str, school_type: str, academic_year: str, holiday_periods: List[Dict[str, Any]], school_hours: Dict[str, Any], source: Optional[str] = None, enabled: bool = True ) -> SchoolCalendar: """Create a new school calendar""" try: calendar = SchoolCalendar( id=uuid.uuid4(), city_id=city_id, calendar_name=calendar_name, school_type=school_type, academic_year=academic_year, holiday_periods=holiday_periods, school_hours=school_hours, source=source, enabled=enabled ) self.session.add(calendar) await self.session.commit() await self.session.refresh(calendar) logger.info( "School calendar created", calendar_id=str(calendar.id), city_id=city_id, school_type=school_type ) return calendar except Exception as e: await self.session.rollback() logger.error( "Error creating school calendar", city_id=city_id, error=str(e) ) raise async def get_calendar_by_id( self, calendar_id: uuid.UUID ) -> Optional[SchoolCalendar]: """Get school calendar by ID""" stmt = select(SchoolCalendar).where(SchoolCalendar.id == calendar_id) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def get_calendars_by_city( self, city_id: str, enabled_only: bool = True ) -> List[SchoolCalendar]: """Get all school calendars for a city""" stmt = select(SchoolCalendar).where(SchoolCalendar.city_id == city_id) if enabled_only: stmt = stmt.where(SchoolCalendar.enabled == True) stmt = stmt.order_by(SchoolCalendar.academic_year.desc(), SchoolCalendar.school_type) result = await self.session.execute(stmt) return list(result.scalars().all()) async def get_calendar_by_city_type_year( self, city_id: str, school_type: str, academic_year: str ) -> Optional[SchoolCalendar]: """Get specific calendar by city, type, and year""" stmt = select(SchoolCalendar).where( and_( SchoolCalendar.city_id == city_id, SchoolCalendar.school_type == school_type, SchoolCalendar.academic_year == academic_year, SchoolCalendar.enabled == True ) ) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def update_calendar( self, calendar_id: uuid.UUID, **kwargs ) -> Optional[SchoolCalendar]: """Update school calendar""" try: calendar = await self.get_calendar_by_id(calendar_id) if not calendar: return None for key, value in kwargs.items(): if hasattr(calendar, key): setattr(calendar, key, value) calendar.updated_at = datetime.utcnow() await self.session.commit() await self.session.refresh(calendar) logger.info( "School calendar updated", calendar_id=str(calendar_id), fields=list(kwargs.keys()) ) return calendar except Exception as e: await self.session.rollback() logger.error( "Error updating school calendar", calendar_id=str(calendar_id), error=str(e) ) raise async def delete_calendar(self, calendar_id: uuid.UUID) -> bool: """Delete school calendar""" try: calendar = await self.get_calendar_by_id(calendar_id) if not calendar: return False await self.session.delete(calendar) await self.session.commit() logger.info("School calendar deleted", calendar_id=str(calendar_id)) return True except Exception as e: await self.session.rollback() logger.error( "Error deleting school calendar", calendar_id=str(calendar_id), error=str(e) ) raise # ===== Tenant Location Context Operations ===== async def create_or_update_tenant_location_context( self, tenant_id: uuid.UUID, city_id: str, school_calendar_id: Optional[uuid.UUID] = None, neighborhood: Optional[str] = None, local_events: Optional[List[Dict[str, Any]]] = None, notes: Optional[str] = None ) -> TenantLocationContext: """Create or update tenant location context""" try: # Check if context exists existing = await self.get_tenant_location_context(tenant_id) if existing: # Update existing existing.city_id = city_id if school_calendar_id is not None: existing.school_calendar_id = school_calendar_id if neighborhood is not None: existing.neighborhood = neighborhood if local_events is not None: existing.local_events = local_events if notes is not None: existing.notes = notes existing.updated_at = datetime.utcnow() await self.session.commit() await self.session.refresh(existing) logger.info( "Tenant location context updated", tenant_id=str(tenant_id) ) return existing else: # Create new context = TenantLocationContext( tenant_id=tenant_id, city_id=city_id, school_calendar_id=school_calendar_id, neighborhood=neighborhood, local_events=local_events or [], notes=notes ) self.session.add(context) await self.session.commit() await self.session.refresh(context) logger.info( "Tenant location context created", tenant_id=str(tenant_id), city_id=city_id ) return context except Exception as e: await self.session.rollback() logger.error( "Error creating/updating tenant location context", tenant_id=str(tenant_id), error=str(e) ) raise async def get_tenant_location_context( self, tenant_id: uuid.UUID ) -> Optional[TenantLocationContext]: """Get tenant location context""" stmt = select(TenantLocationContext).where( TenantLocationContext.tenant_id == tenant_id ) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def get_tenant_with_calendar( self, tenant_id: uuid.UUID ) -> Optional[Dict[str, Any]]: """Get tenant location context with full calendar details""" context = await self.get_tenant_location_context(tenant_id) if not context: return None result = { "tenant_id": str(context.tenant_id), "city_id": context.city_id, "neighborhood": context.neighborhood, "local_events": context.local_events, "notes": context.notes, "calendar": None } if context.school_calendar_id: calendar = await self.get_calendar_by_id(context.school_calendar_id) if calendar: result["calendar"] = { "calendar_id": str(calendar.id), "calendar_name": calendar.calendar_name, "school_type": calendar.school_type, "academic_year": calendar.academic_year, "holiday_periods": calendar.holiday_periods, "school_hours": calendar.school_hours, "source": calendar.source } return result async def delete_tenant_location_context( self, tenant_id: uuid.UUID ) -> bool: """Delete tenant location context""" try: context = await self.get_tenant_location_context(tenant_id) if not context: return False await self.session.delete(context) await self.session.commit() logger.info( "Tenant location context deleted", tenant_id=str(tenant_id) ) return True except Exception as e: await self.session.rollback() logger.error( "Error deleting tenant location context", tenant_id=str(tenant_id), error=str(e) ) raise # ===== Helper Methods ===== async def get_all_tenants_for_calendar( self, calendar_id: uuid.UUID ) -> List[TenantLocationContext]: """Get all tenants using a specific calendar""" stmt = select(TenantLocationContext).where( TenantLocationContext.school_calendar_id == calendar_id ) result = await self.session.execute(stmt) return list(result.scalars().all())