"""Forms, surveys & appointment page shells — Phase 11.6. ExperienceForm / FormSubmission = form definitions + submission shells (CRM/Communication refs only). ExperienceSurvey / SurveyResponse = survey definitions + response shells. ExperienceAppointmentPage / AppointmentBookingRef = appointment page shells + external booking refs. No deep CRM/scheduling ownership; no SEO/PWA in this phase. """ from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import DateTime, Index, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.types import JSON from app.core.database import Base from app.models.base import ( ActorAuditMixin, OptimisticLockMixin, SoftDeleteMixin, TenantMixin, TimestampMixin, UUIDPrimaryKeyMixin, ) from app.models.types import ( GUID, AppointmentBookingStatus, AppointmentPageStatus, FormStatus, FormSubmissionStatus, SurveyResponseStatus, SurveyStatus, ) class ExperienceForm( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Form definition shell bound to a workspace (optionally a site/page).""" __tablename__ = "experience_forms" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) site_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) page_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) code: Mapped[str] = mapped_column(String(50), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) status: Mapped[FormStatus] = mapped_column(default=FormStatus.DRAFT, nullable=False) form_schema_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) submit_action_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "workspace_id", "code", name="uq_experience_forms_tenant_code", ), Index("ix_experience_forms_workspace", "tenant_id", "workspace_id"), Index("ix_experience_forms_site", "tenant_id", "site_id"), Index("ix_experience_forms_page", "tenant_id", "page_id"), Index("ix_experience_forms_status", "tenant_id", "status"), Index("ix_experience_forms_tenant_deleted", "tenant_id", "is_deleted"), ) class FormSubmission( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Form submission shell — payload + external refs only (no CRM ownership).""" __tablename__ = "experience_form_submissions" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) form_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) status: Mapped[FormSubmissionStatus] = mapped_column( default=FormSubmissionStatus.RECEIVED, nullable=False ) payload_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) crm_contact_ref: Mapped[str | None] = mapped_column(String(255), nullable=True) communication_ref: Mapped[str | None] = mapped_column(String(255), nullable=True) external_ref: Mapped[str | None] = mapped_column(String(255), nullable=True) submitted_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( Index("ix_experience_form_submissions_form", "tenant_id", "form_id"), Index("ix_experience_form_submissions_status", "tenant_id", "status"), Index( "ix_experience_form_submissions_tenant_deleted", "tenant_id", "is_deleted" ), ) class ExperienceSurvey( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Survey definition shell.""" __tablename__ = "experience_surveys" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) site_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) page_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) code: Mapped[str] = mapped_column(String(50), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) status: Mapped[SurveyStatus] = mapped_column( default=SurveyStatus.DRAFT, nullable=False ) questions_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "workspace_id", "code", name="uq_experience_surveys_tenant_code", ), Index("ix_experience_surveys_workspace", "tenant_id", "workspace_id"), Index("ix_experience_surveys_status", "tenant_id", "status"), Index("ix_experience_surveys_tenant_deleted", "tenant_id", "is_deleted"), ) class SurveyResponse( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Survey response shell — answers + external refs only.""" __tablename__ = "experience_survey_responses" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) survey_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) status: Mapped[SurveyResponseStatus] = mapped_column( default=SurveyResponseStatus.RECEIVED, nullable=False ) answers_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) crm_contact_ref: Mapped[str | None] = mapped_column(String(255), nullable=True) external_ref: Mapped[str | None] = mapped_column(String(255), nullable=True) submitted_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( Index("ix_experience_survey_responses_survey", "tenant_id", "survey_id"), Index("ix_experience_survey_responses_status", "tenant_id", "status"), Index( "ix_experience_survey_responses_tenant_deleted", "tenant_id", "is_deleted" ), ) class ExperienceAppointmentPage( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Appointment page shell — booking provider/service refs only (no scheduler).""" __tablename__ = "experience_appointment_pages" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) site_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) page_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) code: Mapped[str] = mapped_column(String(50), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) status: Mapped[AppointmentPageStatus] = mapped_column( default=AppointmentPageStatus.DRAFT, nullable=False ) booking_provider_ref: Mapped[str | None] = mapped_column(String(255), nullable=True) service_ref: Mapped[str | None] = mapped_column(String(255), nullable=True) timezone: Mapped[str | None] = mapped_column(String(64), nullable=True) availability_shell_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "workspace_id", "code", name="uq_experience_appointment_pages_tenant_code", ), Index("ix_experience_appointment_pages_site", "tenant_id", "site_id"), Index("ix_experience_appointment_pages_status", "tenant_id", "status"), Index( "ix_experience_appointment_pages_tenant_deleted", "tenant_id", "is_deleted" ), ) class AppointmentBookingRef( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """External booking reference shell — no scheduling engine ownership.""" __tablename__ = "experience_appointment_booking_refs" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) appointment_page_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) external_booking_ref: Mapped[str] = mapped_column(String(255), nullable=False) status: Mapped[AppointmentBookingStatus] = mapped_column( default=AppointmentBookingStatus.PENDING, nullable=False ) crm_contact_ref: Mapped[str | None] = mapped_column(String(255), nullable=True) slot_start: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) slot_end: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "appointment_page_id", "external_booking_ref", name="uq_experience_appointment_booking_refs_ext", ), Index( "ix_experience_appointment_booking_refs_page", "tenant_id", "appointment_page_id", ), Index( "ix_experience_appointment_booking_refs_status", "tenant_id", "status" ), Index( "ix_experience_appointment_booking_refs_tenant_deleted", "tenant_id", "is_deleted", ), )