73 lines
2.3 KiB
Python
73 lines
2.3 KiB
Python
|
|
"""SchedulingProvider interface (§8) and per-tenant provider resolution.
|
||
|
|
|
||
|
|
Adding a third provider = one new module registering itself here. No changes
|
||
|
|
to the agent, tools, or proposal engine are needed.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from datetime import date
|
||
|
|
from typing import Protocol, runtime_checkable
|
||
|
|
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
|
||
|
|
from gogo.domain import BookingRequestData, DeliveryResult, ServiceInfo, Slot
|
||
|
|
from gogo.models import ProviderConfig, Tenant
|
||
|
|
|
||
|
|
|
||
|
|
@runtime_checkable
|
||
|
|
class SchedulingProvider(Protocol):
|
||
|
|
"""Availability lookup + proposal delivery for one tenant."""
|
||
|
|
|
||
|
|
async def get_services(self) -> list[ServiceInfo] | None:
|
||
|
|
"""Optional catalog sync; None = provider has no catalog."""
|
||
|
|
...
|
||
|
|
|
||
|
|
async def get_availability(
|
||
|
|
self, service_id: str, date_from: date, date_to: date, home_visit: bool = False
|
||
|
|
) -> list[Slot]:
|
||
|
|
"""Ready-to-offer free slots. The agent picks up to 3."""
|
||
|
|
...
|
||
|
|
|
||
|
|
async def deliver_request(self, booking_request: BookingRequestData) -> DeliveryResult:
|
||
|
|
"""Deliver the proposal to the owner (email / partner push).
|
||
|
|
|
||
|
|
Outcome is signaled back via webhook/polling (partner) or the owner's
|
||
|
|
email/dashboard actions (google_calendar, mock).
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
|
||
|
|
|
||
|
|
_REGISTRY: dict[str, type] = {}
|
||
|
|
|
||
|
|
|
||
|
|
def register_provider(name: str):
|
||
|
|
def deco(cls):
|
||
|
|
_REGISTRY[name] = cls
|
||
|
|
return cls
|
||
|
|
|
||
|
|
return deco
|
||
|
|
|
||
|
|
|
||
|
|
async def get_provider(session: AsyncSession, tenant: Tenant) -> SchedulingProvider:
|
||
|
|
"""Instantiate the tenant's configured provider."""
|
||
|
|
# Imports here to avoid circulars; modules self-register on import.
|
||
|
|
from gogo.scheduling import google_calendar, mock, partner_api # noqa: F401
|
||
|
|
|
||
|
|
cls = _REGISTRY.get(tenant.scheduling_provider)
|
||
|
|
if cls is None:
|
||
|
|
raise ValueError(f"Unknown scheduling provider: {tenant.scheduling_provider}")
|
||
|
|
config = await _load_config(session, tenant)
|
||
|
|
return cls(session=session, tenant=tenant, config=config)
|
||
|
|
|
||
|
|
|
||
|
|
async def _load_config(session: AsyncSession, tenant: Tenant) -> dict:
|
||
|
|
from sqlalchemy import select
|
||
|
|
|
||
|
|
row = (
|
||
|
|
await session.execute(
|
||
|
|
select(ProviderConfig).where(ProviderConfig.tenant_id == tenant.id)
|
||
|
|
)
|
||
|
|
).scalar_one_or_none()
|
||
|
|
return row.config if row else {}
|