Add the experience service with sites through analytics/AI hooks, migrations through 0011, and phase docs/manifests marking the track complete. Co-authored-by: Cursor <cursoragent@cursor.com>
313 lines
12 KiB
Python
313 lines
12 KiB
Python
"""Forms / surveys / appointments list specifications — Phase 11.6."""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy import Select, asc, desc, or_
|
|
from sqlalchemy.sql import ColumnElement
|
|
|
|
from app.models.forms import (
|
|
AppointmentBookingRef,
|
|
ExperienceAppointmentPage,
|
|
ExperienceForm,
|
|
ExperienceSurvey,
|
|
FormSubmission,
|
|
SurveyResponse,
|
|
)
|
|
from app.models.types import (
|
|
AppointmentBookingStatus,
|
|
AppointmentPageStatus,
|
|
FormStatus,
|
|
FormSubmissionStatus,
|
|
SurveyResponseStatus,
|
|
SurveyStatus,
|
|
)
|
|
|
|
FORM_SORT_FIELDS = frozenset({"created_at", "updated_at", "code", "name", "status"})
|
|
SUBMISSION_SORT_FIELDS = frozenset({"created_at", "updated_at", "submitted_at", "status"})
|
|
SURVEY_SORT_FIELDS = frozenset({"created_at", "updated_at", "code", "name", "status"})
|
|
RESPONSE_SORT_FIELDS = frozenset({"created_at", "updated_at", "submitted_at", "status"})
|
|
APPOINTMENT_PAGE_SORT_FIELDS = frozenset(
|
|
{"created_at", "updated_at", "code", "name", "status"}
|
|
)
|
|
BOOKING_SORT_FIELDS = frozenset(
|
|
{"created_at", "updated_at", "slot_start", "status", "external_booking_ref"}
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class FormListSpec:
|
|
workspace_id: UUID | None = None
|
|
site_id: UUID | None = None
|
|
page_id: UUID | None = None
|
|
status: FormStatus | str | None = None
|
|
q: str | None = None
|
|
sort_by: str = "created_at"
|
|
sort_dir: str = "desc"
|
|
|
|
def filter_clauses(self) -> list[ColumnElement]:
|
|
clauses: list[ColumnElement] = []
|
|
if self.workspace_id is not None:
|
|
clauses.append(ExperienceForm.workspace_id == self.workspace_id)
|
|
if self.site_id is not None:
|
|
clauses.append(ExperienceForm.site_id == self.site_id)
|
|
if self.page_id is not None:
|
|
clauses.append(ExperienceForm.page_id == self.page_id)
|
|
if self.status is not None:
|
|
status = (
|
|
self.status
|
|
if isinstance(self.status, FormStatus)
|
|
else FormStatus(str(self.status))
|
|
)
|
|
clauses.append(ExperienceForm.status == status)
|
|
if self.q:
|
|
term = f"%{self.q.strip()}%"
|
|
clauses.append(
|
|
or_(
|
|
ExperienceForm.code.ilike(term),
|
|
ExperienceForm.name.ilike(term),
|
|
ExperienceForm.description.ilike(term),
|
|
)
|
|
)
|
|
return clauses
|
|
|
|
def apply(self, stmt: Select) -> Select:
|
|
clauses = self.filter_clauses()
|
|
if clauses:
|
|
stmt = stmt.where(*clauses)
|
|
sort_key = self.sort_by if self.sort_by in FORM_SORT_FIELDS else "created_at"
|
|
column = getattr(ExperienceForm, sort_key)
|
|
order = desc(column) if self.sort_dir.lower() != "asc" else asc(column)
|
|
return stmt.order_by(order)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class FormSubmissionListSpec:
|
|
workspace_id: UUID | None = None
|
|
form_id: UUID | None = None
|
|
status: FormSubmissionStatus | str | None = None
|
|
q: str | None = None
|
|
sort_by: str = "created_at"
|
|
sort_dir: str = "desc"
|
|
|
|
def filter_clauses(self) -> list[ColumnElement]:
|
|
clauses: list[ColumnElement] = []
|
|
if self.workspace_id is not None:
|
|
clauses.append(FormSubmission.workspace_id == self.workspace_id)
|
|
if self.form_id is not None:
|
|
clauses.append(FormSubmission.form_id == self.form_id)
|
|
if self.status is not None:
|
|
status = (
|
|
self.status
|
|
if isinstance(self.status, FormSubmissionStatus)
|
|
else FormSubmissionStatus(str(self.status))
|
|
)
|
|
clauses.append(FormSubmission.status == status)
|
|
if self.q:
|
|
term = f"%{self.q.strip()}%"
|
|
clauses.append(
|
|
or_(
|
|
FormSubmission.crm_contact_ref.ilike(term),
|
|
FormSubmission.external_ref.ilike(term),
|
|
FormSubmission.communication_ref.ilike(term),
|
|
)
|
|
)
|
|
return clauses
|
|
|
|
def apply(self, stmt: Select) -> Select:
|
|
clauses = self.filter_clauses()
|
|
if clauses:
|
|
stmt = stmt.where(*clauses)
|
|
sort_key = (
|
|
self.sort_by if self.sort_by in SUBMISSION_SORT_FIELDS else "created_at"
|
|
)
|
|
column = getattr(FormSubmission, sort_key)
|
|
order = desc(column) if self.sort_dir.lower() != "asc" else asc(column)
|
|
return stmt.order_by(order)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SurveyListSpec:
|
|
workspace_id: UUID | None = None
|
|
site_id: UUID | None = None
|
|
page_id: UUID | None = None
|
|
status: SurveyStatus | str | None = None
|
|
q: str | None = None
|
|
sort_by: str = "created_at"
|
|
sort_dir: str = "desc"
|
|
|
|
def filter_clauses(self) -> list[ColumnElement]:
|
|
clauses: list[ColumnElement] = []
|
|
if self.workspace_id is not None:
|
|
clauses.append(ExperienceSurvey.workspace_id == self.workspace_id)
|
|
if self.site_id is not None:
|
|
clauses.append(ExperienceSurvey.site_id == self.site_id)
|
|
if self.page_id is not None:
|
|
clauses.append(ExperienceSurvey.page_id == self.page_id)
|
|
if self.status is not None:
|
|
status = (
|
|
self.status
|
|
if isinstance(self.status, SurveyStatus)
|
|
else SurveyStatus(str(self.status))
|
|
)
|
|
clauses.append(ExperienceSurvey.status == status)
|
|
if self.q:
|
|
term = f"%{self.q.strip()}%"
|
|
clauses.append(
|
|
or_(
|
|
ExperienceSurvey.code.ilike(term),
|
|
ExperienceSurvey.name.ilike(term),
|
|
ExperienceSurvey.description.ilike(term),
|
|
)
|
|
)
|
|
return clauses
|
|
|
|
def apply(self, stmt: Select) -> Select:
|
|
clauses = self.filter_clauses()
|
|
if clauses:
|
|
stmt = stmt.where(*clauses)
|
|
sort_key = self.sort_by if self.sort_by in SURVEY_SORT_FIELDS else "created_at"
|
|
column = getattr(ExperienceSurvey, sort_key)
|
|
order = desc(column) if self.sort_dir.lower() != "asc" else asc(column)
|
|
return stmt.order_by(order)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SurveyResponseListSpec:
|
|
workspace_id: UUID | None = None
|
|
survey_id: UUID | None = None
|
|
status: SurveyResponseStatus | str | None = None
|
|
q: str | None = None
|
|
sort_by: str = "created_at"
|
|
sort_dir: str = "desc"
|
|
|
|
def filter_clauses(self) -> list[ColumnElement]:
|
|
clauses: list[ColumnElement] = []
|
|
if self.workspace_id is not None:
|
|
clauses.append(SurveyResponse.workspace_id == self.workspace_id)
|
|
if self.survey_id is not None:
|
|
clauses.append(SurveyResponse.survey_id == self.survey_id)
|
|
if self.status is not None:
|
|
status = (
|
|
self.status
|
|
if isinstance(self.status, SurveyResponseStatus)
|
|
else SurveyResponseStatus(str(self.status))
|
|
)
|
|
clauses.append(SurveyResponse.status == status)
|
|
if self.q:
|
|
term = f"%{self.q.strip()}%"
|
|
clauses.append(
|
|
or_(
|
|
SurveyResponse.crm_contact_ref.ilike(term),
|
|
SurveyResponse.external_ref.ilike(term),
|
|
)
|
|
)
|
|
return clauses
|
|
|
|
def apply(self, stmt: Select) -> Select:
|
|
clauses = self.filter_clauses()
|
|
if clauses:
|
|
stmt = stmt.where(*clauses)
|
|
sort_key = (
|
|
self.sort_by if self.sort_by in RESPONSE_SORT_FIELDS else "created_at"
|
|
)
|
|
column = getattr(SurveyResponse, sort_key)
|
|
order = desc(column) if self.sort_dir.lower() != "asc" else asc(column)
|
|
return stmt.order_by(order)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class AppointmentPageListSpec:
|
|
workspace_id: UUID | None = None
|
|
site_id: UUID | None = None
|
|
page_id: UUID | None = None
|
|
status: AppointmentPageStatus | str | None = None
|
|
q: str | None = None
|
|
sort_by: str = "created_at"
|
|
sort_dir: str = "desc"
|
|
|
|
def filter_clauses(self) -> list[ColumnElement]:
|
|
clauses: list[ColumnElement] = []
|
|
if self.workspace_id is not None:
|
|
clauses.append(ExperienceAppointmentPage.workspace_id == self.workspace_id)
|
|
if self.site_id is not None:
|
|
clauses.append(ExperienceAppointmentPage.site_id == self.site_id)
|
|
if self.page_id is not None:
|
|
clauses.append(ExperienceAppointmentPage.page_id == self.page_id)
|
|
if self.status is not None:
|
|
status = (
|
|
self.status
|
|
if isinstance(self.status, AppointmentPageStatus)
|
|
else AppointmentPageStatus(str(self.status))
|
|
)
|
|
clauses.append(ExperienceAppointmentPage.status == status)
|
|
if self.q:
|
|
term = f"%{self.q.strip()}%"
|
|
clauses.append(
|
|
or_(
|
|
ExperienceAppointmentPage.code.ilike(term),
|
|
ExperienceAppointmentPage.name.ilike(term),
|
|
ExperienceAppointmentPage.booking_provider_ref.ilike(term),
|
|
ExperienceAppointmentPage.service_ref.ilike(term),
|
|
)
|
|
)
|
|
return clauses
|
|
|
|
def apply(self, stmt: Select) -> Select:
|
|
clauses = self.filter_clauses()
|
|
if clauses:
|
|
stmt = stmt.where(*clauses)
|
|
sort_key = (
|
|
self.sort_by
|
|
if self.sort_by in APPOINTMENT_PAGE_SORT_FIELDS
|
|
else "created_at"
|
|
)
|
|
column = getattr(ExperienceAppointmentPage, sort_key)
|
|
order = desc(column) if self.sort_dir.lower() != "asc" else asc(column)
|
|
return stmt.order_by(order)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class AppointmentBookingListSpec:
|
|
workspace_id: UUID | None = None
|
|
appointment_page_id: UUID | None = None
|
|
status: AppointmentBookingStatus | str | None = None
|
|
q: str | None = None
|
|
sort_by: str = "created_at"
|
|
sort_dir: str = "desc"
|
|
|
|
def filter_clauses(self) -> list[ColumnElement]:
|
|
clauses: list[ColumnElement] = []
|
|
if self.workspace_id is not None:
|
|
clauses.append(AppointmentBookingRef.workspace_id == self.workspace_id)
|
|
if self.appointment_page_id is not None:
|
|
clauses.append(
|
|
AppointmentBookingRef.appointment_page_id == self.appointment_page_id
|
|
)
|
|
if self.status is not None:
|
|
status = (
|
|
self.status
|
|
if isinstance(self.status, AppointmentBookingStatus)
|
|
else AppointmentBookingStatus(str(self.status))
|
|
)
|
|
clauses.append(AppointmentBookingRef.status == status)
|
|
if self.q:
|
|
term = f"%{self.q.strip()}%"
|
|
clauses.append(
|
|
or_(
|
|
AppointmentBookingRef.external_booking_ref.ilike(term),
|
|
AppointmentBookingRef.crm_contact_ref.ilike(term),
|
|
)
|
|
)
|
|
return clauses
|
|
|
|
def apply(self, stmt: Select) -> Select:
|
|
clauses = self.filter_clauses()
|
|
if clauses:
|
|
stmt = stmt.where(*clauses)
|
|
sort_key = self.sort_by if self.sort_by in BOOKING_SORT_FIELDS else "created_at"
|
|
column = getattr(AppointmentBookingRef, sort_key)
|
|
order = desc(column) if self.sort_dir.lower() != "asc" else asc(column)
|
|
return stmt.order_by(order)
|