""" Pydantic schemas for Quote models. Request and response schemas for the MSP Quote Wizard including public and admin-facing operations. """ from datetime import datetime from decimal import Decimal from enum import Enum from typing import Optional from uuid import UUID from pydantic import BaseModel, Field, EmailStr, field_validator class QuoteStatus(str, Enum): """Status options for quotes.""" DRAFT = "draft" SUBMITTED = "submitted" REVIEWING = "reviewing" APPROVED = "approved" REJECTED = "rejected" EXPIRED = "expired" class ServiceCategory(str, Enum): """Service category options for quote items.""" MANAGED_SERVICES = "managed_services" SECURITY = "security" BACKUP = "backup" CLOUD = "cloud" HARDWARE = "hardware" SOFTWARE = "software" CONSULTING = "consulting" SUPPORT = "support" class BillingFrequency(str, Enum): """Billing frequency options for quote items.""" MONTHLY = "monthly" QUARTERLY = "quarterly" ANNUAL = "annual" ONE_TIME = "one_time" class NotificationType(str, Enum): """Notification types for quote events.""" EMAIL_SENT = "email_sent" SMS_SENT = "sms_sent" ADMIN_ALERT = "admin_alert" REMINDER_SENT = "reminder_sent" # ============================================================================ # Quote Item Schemas # ============================================================================ class QuoteItemBase(BaseModel): """Base schema with shared QuoteItem fields.""" service_name: str = Field(..., description="Name of the service", min_length=1, max_length=255) service_description: Optional[str] = Field(None, description="Detailed description of the service") category: ServiceCategory = Field( ServiceCategory.MANAGED_SERVICES, description="Service category" ) billing_frequency: BillingFrequency = Field( BillingFrequency.MONTHLY, description="Billing frequency" ) unit_price: Decimal = Field(..., description="Price per unit", ge=0) quantity: int = Field(1, description="Number of units", ge=1) setup_fee: Decimal = Field(Decimal("0.00"), description="One-time setup fee", ge=0) is_required: bool = Field(False, description="Whether this item is required") sort_order: int = Field(0, description="Display order within the quote") class QuoteItemCreate(QuoteItemBase): """Schema for creating a new QuoteItem.""" pass class QuoteItemUpdate(BaseModel): """Schema for updating an existing QuoteItem. All fields optional.""" service_name: Optional[str] = Field(None, min_length=1, max_length=255) service_description: Optional[str] = None category: Optional[ServiceCategory] = None billing_frequency: Optional[BillingFrequency] = None unit_price: Optional[Decimal] = Field(None, ge=0) quantity: Optional[int] = Field(None, ge=1) setup_fee: Optional[Decimal] = Field(None, ge=0) is_required: Optional[bool] = None sort_order: Optional[int] = None class QuoteItemResponse(QuoteItemBase): """Schema for QuoteItem responses with ID and computed fields.""" id: UUID = Field(..., description="Unique identifier for the quote item") quote_id: UUID = Field(..., description="Reference to the parent quote") line_total: Decimal = Field(..., description="Calculated line total (unit_price * quantity)") monthly_amount: Decimal = Field(..., description="Calculated monthly amount") created_at: datetime = Field(..., description="Timestamp when item was created") updated_at: datetime = Field(..., description="Timestamp when item was last updated") model_config = {"from_attributes": True} # ============================================================================ # Quote Schemas # ============================================================================ class QuoteBase(BaseModel): """Base schema with shared Quote fields.""" company_name: Optional[str] = Field(None, description="Prospect company name", max_length=255) contact_name: Optional[str] = Field(None, description="Primary contact name", max_length=255) contact_email: Optional[EmailStr] = Field(None, description="Contact email address") contact_phone: Optional[str] = Field(None, description="Contact phone number", max_length=50) employee_count: Optional[int] = Field(None, description="Number of employees/users", ge=1) notes: Optional[str] = Field(None, description="Customer notes or special requirements") class QuoteCreate(BaseModel): """Schema for creating a new Quote draft.""" employee_count: Optional[int] = Field(None, description="Number of employees/users", ge=1) notes: Optional[str] = Field(None, description="Initial notes") # Items can optionally be provided at creation items: Optional[list[QuoteItemCreate]] = Field(None, description="Initial quote items") class QuoteUpdate(BaseModel): """Schema for updating a Quote during wizard flow.""" company_name: Optional[str] = Field(None, max_length=255) contact_name: Optional[str] = Field(None, max_length=255) contact_email: Optional[EmailStr] = None contact_phone: Optional[str] = Field(None, max_length=50) employee_count: Optional[int] = Field(None, ge=1) notes: Optional[str] = None # Items to add/update items: Optional[list[QuoteItemCreate]] = Field(None, description="Items to set (replaces existing)") class QuoteSubmit(BaseModel): """Schema for final quote submission with required contact info.""" company_name: str = Field(..., description="Company name (required for submission)", min_length=1, max_length=255) contact_name: str = Field(..., description="Contact name (required for submission)", min_length=1, max_length=255) contact_email: EmailStr = Field(..., description="Email address (required for submission)") contact_phone: Optional[str] = Field(None, description="Phone number", max_length=50) notes: Optional[str] = Field(None, description="Additional notes") @field_validator("company_name", "contact_name") @classmethod def strip_whitespace(cls, v: str) -> str: """Strip whitespace from string fields.""" return v.strip() if v else v class QuoteResponse(QuoteBase): """Schema for public Quote responses with items.""" id: UUID = Field(..., description="Unique identifier for the quote") access_token: str = Field(..., description="Access token for public URL") status: QuoteStatus = Field(..., description="Current quote status") monthly_total: Decimal = Field(..., description="Calculated monthly recurring total") setup_total: Decimal = Field(..., description="Calculated one-time setup total") annual_total: Decimal = Field(..., description="Calculated annual total") expires_at: Optional[datetime] = Field(None, description="Quote expiration date") submitted_at: Optional[datetime] = Field(None, description="When quote was submitted") created_at: datetime = Field(..., description="Timestamp when quote was created") updated_at: datetime = Field(..., description="Timestamp when quote was last updated") items: list[QuoteItemResponse] = Field(default_factory=list, description="Quote line items") model_config = {"from_attributes": True} class QuoteCreatedResponse(BaseModel): """Schema for quote creation response with access URL info.""" id: UUID = Field(..., description="Unique identifier for the quote") access_token: str = Field(..., description="Access token for public URL") status: QuoteStatus = Field(..., description="Current quote status") message: str = Field(..., description="Success message") model_config = {"from_attributes": True} # ============================================================================ # Quote Activity Schemas # ============================================================================ class QuoteActivityResponse(BaseModel): """Schema for QuoteActivity responses.""" id: UUID = Field(..., description="Unique identifier for the activity") quote_id: UUID = Field(..., description="Reference to the parent quote") action: str = Field(..., description="Action performed") description: Optional[str] = Field(None, description="Detailed description") actor: Optional[str] = Field(None, description="Who performed the action") ip_address: Optional[str] = Field(None, description="IP address of the actor") created_at: datetime = Field(..., description="Timestamp of the action") model_config = {"from_attributes": True} # ============================================================================ # Quote Notification Schemas # ============================================================================ class QuoteNotificationResponse(BaseModel): """Schema for QuoteNotification responses.""" id: UUID = Field(..., description="Unique identifier for the notification") quote_id: UUID = Field(..., description="Reference to the parent quote") notification_type: NotificationType = Field(..., description="Type of notification") recipient: str = Field(..., description="Notification recipient") subject: Optional[str] = Field(None, description="Notification subject") status: str = Field(..., description="Delivery status") sent_at: Optional[datetime] = Field(None, description="When notification was sent") error_message: Optional[str] = Field(None, description="Error message if failed") created_at: datetime = Field(..., description="Timestamp when created") model_config = {"from_attributes": True} # ============================================================================ # Admin Schemas # ============================================================================ class QuoteAdminUpdate(BaseModel): """Schema for admin updates to a quote.""" status: Optional[QuoteStatus] = Field(None, description="New status") admin_notes: Optional[str] = Field(None, description="Internal admin notes") expires_at: Optional[datetime] = Field(None, description="Quote expiration date") class QuoteAdminResponse(QuoteResponse): """Schema for admin Quote responses with additional fields.""" admin_notes: Optional[str] = Field(None, description="Internal admin notes") ip_address: Optional[str] = Field(None, description="IP address of the requester") user_agent: Optional[str] = Field(None, description="Browser user agent") activities: list[QuoteActivityResponse] = Field( default_factory=list, description="Activity log for this quote" ) notifications: list[QuoteNotificationResponse] = Field( default_factory=list, description="Notifications sent for this quote" ) model_config = {"from_attributes": True} # ============================================================================ # List and Stats Schemas # ============================================================================ class QuoteListItem(BaseModel): """Schema for quote list items (summary view).""" id: UUID = Field(..., description="Unique identifier") access_token: str = Field(..., description="Access token") status: QuoteStatus = Field(..., description="Current status") company_name: Optional[str] = Field(None, description="Company name") contact_name: Optional[str] = Field(None, description="Contact name") contact_email: Optional[str] = Field(None, description="Contact email") employee_count: Optional[int] = Field(None, description="Employee count") monthly_total: Decimal = Field(..., description="Monthly total") setup_total: Decimal = Field(..., description="Setup total") item_count: int = Field(..., description="Number of line items") submitted_at: Optional[datetime] = Field(None, description="Submission timestamp") created_at: datetime = Field(..., description="Creation timestamp") model_config = {"from_attributes": True} class QuoteListResponse(BaseModel): """Schema for paginated quote list responses.""" total: int = Field(..., description="Total number of quotes matching filters") skip: int = Field(..., description="Number of records skipped") limit: int = Field(..., description="Maximum number of records returned") quotes: list[QuoteListItem] = Field(..., description="List of quotes") class QuoteStatsResponse(BaseModel): """Schema for admin dashboard statistics.""" total_quotes: int = Field(..., description="Total number of quotes") quotes_by_status: dict[str, int] = Field(..., description="Quote count by status") total_monthly_value: Decimal = Field(..., description="Total monthly value of all submitted quotes") total_setup_value: Decimal = Field(..., description="Total setup value of all submitted quotes") quotes_this_month: int = Field(..., description="Quotes created this month") quotes_submitted_this_month: int = Field(..., description="Quotes submitted this month") average_monthly_value: Decimal = Field(..., description="Average monthly value per submitted quote") conversion_rate: Decimal = Field(..., description="Percentage of drafts that get submitted")