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>
This commit is contained in:
87
gogo/domain.py
Normal file
87
gogo/domain.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""Shared domain value objects used across providers, agent tools and the proposal engine.
|
||||
|
||||
These are deliberately plain (pydantic) models decoupled from the ORM so that
|
||||
SchedulingProvider implementations and the agent can be tested without a database.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class BookingStatus(enum.StrEnum):
|
||||
pending = "pending"
|
||||
resolved_by_owner = "resolved_by_owner" # owner contacted client directly (primary flow)
|
||||
confirmed = "confirmed" # owner confirmed a slot → Gogo sends confirmation SMS
|
||||
rejected = "rejected"
|
||||
expired = "expired"
|
||||
|
||||
|
||||
class CallOutcome(enum.StrEnum):
|
||||
human_answered = "human_answered"
|
||||
request_created = "request_created"
|
||||
info_only = "info_only"
|
||||
message_taken = "message_taken"
|
||||
abandoned = "abandoned"
|
||||
|
||||
|
||||
class ProviderType(enum.StrEnum):
|
||||
google_calendar = "google_calendar"
|
||||
partner_api = "partner_api"
|
||||
mock = "mock" # in-memory, for tests and demos
|
||||
|
||||
|
||||
class Slot(BaseModel):
|
||||
"""One concrete offerable time slot (timezone-aware datetimes)."""
|
||||
|
||||
start: datetime
|
||||
end: datetime
|
||||
staff_id: str | None = None
|
||||
staff_name: str | None = None
|
||||
|
||||
def model_post_init(self, __context) -> None:
|
||||
if self.start.tzinfo is None or self.end.tzinfo is None:
|
||||
raise ValueError("Slot datetimes must be timezone-aware")
|
||||
|
||||
|
||||
class ServiceInfo(BaseModel):
|
||||
"""Service as seen by providers / the agent (decoupled from ORM row)."""
|
||||
|
||||
id: str # our service UUID as string, or partner service id for catalog sync
|
||||
name: str
|
||||
duration_min: int
|
||||
price_min: float | None = None
|
||||
price_max: float | None = None
|
||||
currency: str = "BAM"
|
||||
home_visit: bool = False
|
||||
agent_note: str | None = None
|
||||
active: bool = True
|
||||
|
||||
|
||||
class BookingRequestData(BaseModel):
|
||||
"""Payload the proposal engine hands to a provider's deliver_request()."""
|
||||
|
||||
gogo_request_id: str
|
||||
tenant_id: str
|
||||
created_at: datetime
|
||||
source: str # "voice" | "chat"
|
||||
client_name: str
|
||||
client_phone: str
|
||||
service_id: str | None = None
|
||||
service_name_raw: str = ""
|
||||
requested_slots: list[Slot] = Field(default_factory=list) # 0-3, ordered by preference
|
||||
time_preference_text: str = ""
|
||||
home_visit: bool = False
|
||||
address: str | None = None
|
||||
summary: str = ""
|
||||
transcript_url: str = ""
|
||||
dry_run: bool = False
|
||||
|
||||
|
||||
class DeliveryResult(BaseModel):
|
||||
ok: bool
|
||||
partner_request_id: str | None = None
|
||||
detail: str = ""
|
||||
Reference in New Issue
Block a user