Files
gogo-telefon/gogo/scheduling/mock.py
Senad Uka e855650f09 M1: backend core + proposal engine
- FastAPI skeleton, SQLAlchemy models (§13), Alembic initial migration
- SchedulingProvider interface with google_calendar (free/busy read-only),
  partner_api (Appendix B client) and mock implementations
- Proposal engine: create → provider-routed delivery → owner actions
  (resolve/confirm+SMS/reject) → expiry + reminders (§9)
- Signed single-use action links, .ics METHOD:REQUEST attachment
- Partner outcome webhook with HMAC verification + polling fallback
- SmsProvider (console) with Bosnian templates (§5.5), EmailProvider (console/SMTP)
- Fake partner API server in tests/ — Appendix B reference implementation
- 43 tests: slot math, proposal lifecycle, action links, partner contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 09:45:06 +02:00

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)")