"""Mock scheduling provider — in-memory, for tests and demos (§16 M1). Availability: generated from tenant working hours with configurable busy blocks (class-level, settable by tests/demo seeder). Delivery: owner email, same as google_calendar. """ from __future__ import annotations import uuid from datetime import date, datetime from zoneinfo import ZoneInfo from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from gogo.domain import BookingRequestData, DeliveryResult, ServiceInfo, Slot from gogo.hours import DEFAULT_WORKING_HOURS from gogo.models import BookingRequest, Service, Tenant, utcnow from gogo.scheduling.base import register_provider from gogo.scheduling.slots import compute_slots @register_provider("mock") class MockProvider: # tenant_id(str) -> list[(start, end)] busy blocks; tests populate this busy_blocks: dict[str, list[tuple[datetime, datetime]]] = {} # test hook: freeze "now" now_override: datetime | None = None def __init__(self, session: AsyncSession, tenant: Tenant, config: dict): self.session = session self.tenant = tenant self.config = config async def get_services(self) -> list[ServiceInfo] | None: return None # no external catalog async def get_availability( self, service_id: str, date_from: date, date_to: date, home_visit: bool = False ) -> list[Slot]: service = ( await self.session.execute( select(Service).where(Service.id == uuid.UUID(service_id)) ) ).scalar_one_or_none() duration = service.duration_min if service else 30 buffer_min = service.buffer_min if service else 0 tz = ZoneInfo(self.tenant.timezone) return compute_slots( working_hours=self.tenant.working_hours or DEFAULT_WORKING_HOURS, busy=self.busy_blocks.get(str(self.tenant.id), []), duration_min=duration, buffer_min=buffer_min, date_from=date_from, date_to=date_to, tz=tz, now=self.now_override or utcnow(), min_notice_hours=self.tenant.min_notice_hours, max_days_ahead=self.tenant.max_days_ahead, ) async def deliver_request(self, booking_request: BookingRequestData) -> DeliveryResult: if booking_request.dry_run: return DeliveryResult(ok=True, detail="dry-run ok (mock)") from gogo.proposals.email import send_proposal_email req = ( await self.session.execute( select(BookingRequest).where( BookingRequest.id == uuid.UUID(booking_request.gogo_request_id) ) ) ).scalar_one() service = None if req.service_id: service = ( await self.session.execute(select(Service).where(Service.id == req.service_id)) ).scalar_one_or_none() await send_proposal_email(self.session, self.tenant, req, service) return DeliveryResult(ok=True, detail="email sent (mock)")