sync: Auto-sync from Mikes-MacBook-Air.local at 2026-03-09 08:14:13
Synced files: - Session logs updated - Latest context and credentials - Command/directive updates Machine: Mikes-MacBook-Air.local Timestamp: 2026-03-09 08:14:13 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,20 @@ from .m365_tenant import M365TenantBase, M365TenantCreate, M365TenantResponse, M
|
||||
from .machine import MachineBase, MachineCreate, MachineResponse, MachineUpdate
|
||||
from .network import NetworkBase, NetworkCreate, NetworkResponse, NetworkUpdate
|
||||
from .project import ProjectBase, ProjectCreate, ProjectResponse, ProjectUpdate
|
||||
from .quote import (
|
||||
QuoteCreate,
|
||||
QuoteCreatedResponse,
|
||||
QuoteItemCreate,
|
||||
QuoteItemResponse,
|
||||
QuoteItemUpdate,
|
||||
QuoteListResponse,
|
||||
QuoteResponse,
|
||||
QuoteAdminResponse,
|
||||
QuoteAdminUpdate,
|
||||
QuoteStatsResponse,
|
||||
QuoteSubmit,
|
||||
QuoteUpdate,
|
||||
)
|
||||
from .security_incident import SecurityIncidentBase, SecurityIncidentCreate, SecurityIncidentResponse, SecurityIncidentUpdate
|
||||
from .service import ServiceBase, ServiceCreate, ServiceResponse, ServiceUpdate
|
||||
from .session import SessionBase, SessionCreate, SessionResponse, SessionUpdate
|
||||
@@ -109,4 +123,17 @@ __all__ = [
|
||||
"SecurityIncidentCreate",
|
||||
"SecurityIncidentUpdate",
|
||||
"SecurityIncidentResponse",
|
||||
# Quote schemas
|
||||
"QuoteCreate",
|
||||
"QuoteCreatedResponse",
|
||||
"QuoteItemCreate",
|
||||
"QuoteItemResponse",
|
||||
"QuoteItemUpdate",
|
||||
"QuoteListResponse",
|
||||
"QuoteResponse",
|
||||
"QuoteAdminResponse",
|
||||
"QuoteAdminUpdate",
|
||||
"QuoteStatsResponse",
|
||||
"QuoteSubmit",
|
||||
"QuoteUpdate",
|
||||
]
|
||||
|
||||
303
api/schemas/quote.py
Normal file
303
api/schemas/quote.py
Normal file
@@ -0,0 +1,303 @@
|
||||
"""
|
||||
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")
|
||||
Reference in New Issue
Block a user