sync: Auto-sync from ACG-M-L5090 at 2026-03-10 19:11:00

Synced files:
- Quote wizard frontend (all components, hooks, types, config)
- API updates (config, models, routers, schemas, services)
- Client work (bg-builders, gurushow)
- Scripts (BGB Lesley termination, CIPP, Datto, migration)
- Temp files (Bardach contacts, VWP investigation, misc)
- Credentials and session logs
- Email service, PHP API, session logs

Machine: ACG-M-L5090
Timestamp: 2026-03-10 19:11:00

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 19:59:08 -07:00
parent a1a19f8c00
commit fa15b03180
169 changed files with 879909 additions and 1243 deletions

View File

@@ -7,49 +7,17 @@ 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"
from api.models.quote import (
QuoteStatus,
ServiceCategory,
BillingFrequency,
NotificationType,
)
# ============================================================================
@@ -59,21 +27,19 @@ class NotificationType(str, Enum):
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"
)
category: ServiceCategory = Field(..., description="Service category")
product_code: str = Field(..., description="Product code identifier", min_length=1, max_length=50)
product_name: str = Field(..., description="Name of the product/service", min_length=1, max_length=255)
description: Optional[str] = Field(None, description="Detailed description of the product/service")
quantity: int = Field(1, description="Number of units", ge=1)
unit_price: Decimal = Field(..., description="Price per unit", ge=0)
setup_price: Decimal = Field(Decimal("0.00"), description="One-time setup price", ge=0)
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")
tier: Optional[str] = Field(None, description="Pricing tier", max_length=50)
is_recommended: bool = Field(False, description="Whether this item is recommended")
class QuoteItemCreate(QuoteItemBase):
@@ -84,15 +50,16 @@ class QuoteItemCreate(QuoteItemBase):
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)
product_code: Optional[str] = Field(None, min_length=1, max_length=50)
product_name: Optional[str] = Field(None, min_length=1, max_length=255)
description: Optional[str] = None
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
unit_price: Optional[Decimal] = Field(None, ge=0)
setup_price: Optional[Decimal] = Field(None, ge=0)
billing_frequency: Optional[BillingFrequency] = None
tier: Optional[str] = Field(None, max_length=50)
is_recommended: Optional[bool] = None
class QuoteItemResponse(QuoteItemBase):
@@ -103,7 +70,6 @@ class QuoteItemResponse(QuoteItemBase):
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}
@@ -120,14 +86,12 @@ class QuoteBase(BaseModel):
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")
@@ -140,7 +104,6 @@ class QuoteUpdate(BaseModel):
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)")
@@ -152,7 +115,7 @@ class QuoteSubmit(BaseModel):
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")
notes: Optional[str] = Field(None, description="Additional notes from the prospect", max_length=2000)
@field_validator("company_name", "contact_name")
@classmethod
@@ -169,7 +132,6 @@ class QuoteResponse(QuoteBase):
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")
@@ -200,8 +162,8 @@ class QuoteActivityResponse(BaseModel):
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")
step_name: Optional[str] = Field(None, description="Wizard step name")
details: Optional[str] = Field(None, description="Additional details")
ip_address: Optional[str] = Field(None, description="IP address of the actor")
created_at: datetime = Field(..., description="Timestamp of the action")
@@ -221,6 +183,8 @@ class QuoteNotificationResponse(BaseModel):
recipient: str = Field(..., description="Notification recipient")
subject: Optional[str] = Field(None, description="Notification subject")
status: str = Field(..., description="Delivery status")
attempts: int = Field(0, description="Number of delivery attempts")
last_attempt_at: Optional[datetime] = Field(None, description="Last delivery attempt timestamp")
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")
@@ -236,14 +200,12 @@ 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(