82 lines
3.0 KiB
Python
82 lines
3.0 KiB
Python
|
|
"""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)")
|