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:
@@ -22,6 +22,7 @@ from sqlalchemy import (
|
||||
Numeric,
|
||||
String,
|
||||
Text,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
@@ -32,38 +33,34 @@ class QuoteStatus(str, PyEnum):
|
||||
"""Status options for quotes."""
|
||||
DRAFT = "draft"
|
||||
SUBMITTED = "submitted"
|
||||
REVIEWING = "reviewing"
|
||||
APPROVED = "approved"
|
||||
REJECTED = "rejected"
|
||||
VIEWED = "viewed"
|
||||
FOLLOWED_UP = "followed_up"
|
||||
CONVERTED = "converted"
|
||||
EXPIRED = "expired"
|
||||
|
||||
|
||||
class ServiceCategory(str, PyEnum):
|
||||
"""Service category options for quote items."""
|
||||
MANAGED_SERVICES = "managed_services"
|
||||
SECURITY = "security"
|
||||
BACKUP = "backup"
|
||||
CLOUD = "cloud"
|
||||
GPS_MONITORING = "gps_monitoring"
|
||||
SUPPORT_PLAN = "support_plan"
|
||||
VOIP = "voip"
|
||||
WEB_HOSTING = "web_hosting"
|
||||
EMAIL = "email"
|
||||
HARDWARE = "hardware"
|
||||
SOFTWARE = "software"
|
||||
CONSULTING = "consulting"
|
||||
SUPPORT = "support"
|
||||
ADDON = "addon"
|
||||
|
||||
|
||||
class BillingFrequency(str, PyEnum):
|
||||
"""Billing frequency options for quote items."""
|
||||
MONTHLY = "monthly"
|
||||
QUARTERLY = "quarterly"
|
||||
ANNUAL = "annual"
|
||||
YEARLY = "yearly"
|
||||
ONE_TIME = "one_time"
|
||||
|
||||
|
||||
class NotificationType(str, PyEnum):
|
||||
"""Notification types for quote events."""
|
||||
EMAIL_SENT = "email_sent"
|
||||
SMS_SENT = "sms_sent"
|
||||
ADMIN_ALERT = "admin_alert"
|
||||
REMINDER_SENT = "reminder_sent"
|
||||
EMAIL = "email"
|
||||
WEBHOOK = "webhook"
|
||||
|
||||
|
||||
class Quote(Base, UUIDMixin, TimestampMixin):
|
||||
@@ -75,17 +72,14 @@ class Quote(Base, UUIDMixin, TimestampMixin):
|
||||
|
||||
Attributes:
|
||||
access_token: Unique token for public access (URL-safe, 43 chars)
|
||||
status: Current quote status (draft, submitted, reviewing, etc.)
|
||||
status: Current quote status (draft, submitted, viewed, etc.)
|
||||
company_name: Prospect company name
|
||||
contact_name: Primary contact name
|
||||
contact_email: Contact email address
|
||||
contact_phone: Contact phone number
|
||||
employee_count: Number of employees/users
|
||||
notes: Customer notes or special requirements
|
||||
admin_notes: Internal admin notes (not visible to customer)
|
||||
monthly_total: Calculated monthly recurring total
|
||||
setup_total: Calculated one-time setup total
|
||||
annual_total: Calculated annual total
|
||||
expires_at: Quote expiration date
|
||||
submitted_at: Timestamp when quote was submitted
|
||||
ip_address: IP address of the requester
|
||||
@@ -109,10 +103,10 @@ class Quote(Base, UUIDMixin, TimestampMixin):
|
||||
nullable=False,
|
||||
default=QuoteStatus.DRAFT.value,
|
||||
server_default=QuoteStatus.DRAFT.value,
|
||||
doc="Quote status: draft, submitted, reviewing, approved, rejected, expired"
|
||||
doc="Quote status: draft, submitted, viewed, followed_up, converted, expired"
|
||||
)
|
||||
|
||||
# Contact information (optional until submission)
|
||||
# Contact information
|
||||
company_name: Mapped[Optional[str]] = mapped_column(
|
||||
String(255),
|
||||
doc="Prospect company name"
|
||||
@@ -139,15 +133,14 @@ class Quote(Base, UUIDMixin, TimestampMixin):
|
||||
doc="Number of employees/users"
|
||||
)
|
||||
|
||||
# Notes
|
||||
notes: Mapped[Optional[str]] = mapped_column(
|
||||
Text,
|
||||
doc="Customer notes or special requirements"
|
||||
industry: Mapped[Optional[str]] = mapped_column(
|
||||
String(100),
|
||||
doc="Industry/vertical of the prospect"
|
||||
)
|
||||
|
||||
admin_notes: Mapped[Optional[str]] = mapped_column(
|
||||
current_it_situation: Mapped[Optional[str]] = mapped_column(
|
||||
Text,
|
||||
doc="Internal admin notes (not visible to customer)"
|
||||
doc="Description of the prospect's current IT setup"
|
||||
)
|
||||
|
||||
# Calculated totals
|
||||
@@ -167,14 +160,6 @@ class Quote(Base, UUIDMixin, TimestampMixin):
|
||||
doc="Calculated one-time setup total"
|
||||
)
|
||||
|
||||
annual_total: Mapped[Decimal] = mapped_column(
|
||||
Numeric(10, 2),
|
||||
nullable=False,
|
||||
default=Decimal("0.00"),
|
||||
server_default="0.00",
|
||||
doc="Calculated annual total"
|
||||
)
|
||||
|
||||
# Timestamps
|
||||
expires_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime,
|
||||
@@ -193,10 +178,32 @@ class Quote(Base, UUIDMixin, TimestampMixin):
|
||||
)
|
||||
|
||||
user_agent: Mapped[Optional[str]] = mapped_column(
|
||||
String(500),
|
||||
Text,
|
||||
doc="Browser user agent string"
|
||||
)
|
||||
|
||||
# Marketing attribution
|
||||
source: Mapped[Optional[str]] = mapped_column(
|
||||
String(50),
|
||||
server_default="website",
|
||||
doc="Lead source (e.g., website, referral)"
|
||||
)
|
||||
|
||||
utm_source: Mapped[Optional[str]] = mapped_column(
|
||||
String(100),
|
||||
doc="UTM source parameter"
|
||||
)
|
||||
|
||||
utm_medium: Mapped[Optional[str]] = mapped_column(
|
||||
String(100),
|
||||
doc="UTM medium parameter"
|
||||
)
|
||||
|
||||
utm_campaign: Mapped[Optional[str]] = mapped_column(
|
||||
String(100),
|
||||
doc="UTM campaign parameter"
|
||||
)
|
||||
|
||||
# Syncro RMM Integration
|
||||
syncro_lead_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(100),
|
||||
@@ -242,7 +249,7 @@ class Quote(Base, UUIDMixin, TimestampMixin):
|
||||
# Constraints and indexes
|
||||
__table_args__ = (
|
||||
CheckConstraint(
|
||||
"status IN ('draft', 'submitted', 'reviewing', 'approved', 'rejected', 'expired')",
|
||||
"status IN ('draft', 'submitted', 'viewed', 'followed_up', 'converted', 'expired')",
|
||||
name="ck_quotes_status"
|
||||
),
|
||||
Index("idx_quotes_access_token", "access_token"),
|
||||
@@ -256,23 +263,25 @@ class Quote(Base, UUIDMixin, TimestampMixin):
|
||||
return f"<Quote(id='{self.id}', status='{self.status}', company='{self.company_name}')>"
|
||||
|
||||
|
||||
class QuoteItem(Base, UUIDMixin, TimestampMixin):
|
||||
class QuoteItem(Base, UUIDMixin):
|
||||
"""
|
||||
Quote item model representing a single line item in a quote.
|
||||
|
||||
Stores service details, pricing, and quantity information.
|
||||
Stores product details, pricing, and quantity information.
|
||||
|
||||
Attributes:
|
||||
quote_id: Reference to the parent quote
|
||||
service_name: Name of the service
|
||||
service_description: Detailed description of the service
|
||||
category: Service category (managed_services, security, etc.)
|
||||
billing_frequency: Billing frequency (monthly, annual, one_time)
|
||||
unit_price: Price per unit
|
||||
category: Service category (gps_monitoring, support_plan, etc.)
|
||||
product_code: Product code identifier
|
||||
product_name: Name of the product/service
|
||||
description: Detailed description of the product/service
|
||||
quantity: Number of units
|
||||
setup_fee: One-time setup fee
|
||||
is_required: Whether this item is required (cannot be removed)
|
||||
sort_order: Display order within the quote
|
||||
unit_price: Price per unit
|
||||
setup_price: One-time setup price
|
||||
billing_frequency: Billing frequency (monthly, yearly, one_time)
|
||||
tier: Pricing tier
|
||||
is_recommended: Whether this item is recommended
|
||||
created_at: Timestamp when item was created
|
||||
"""
|
||||
|
||||
__tablename__ = "quote_items"
|
||||
@@ -285,42 +294,32 @@ class QuoteItem(Base, UUIDMixin, TimestampMixin):
|
||||
doc="Reference to the parent quote"
|
||||
)
|
||||
|
||||
# Service identification
|
||||
service_name: Mapped[str] = mapped_column(
|
||||
String(255),
|
||||
nullable=False,
|
||||
doc="Name of the service"
|
||||
)
|
||||
|
||||
service_description: Mapped[Optional[str]] = mapped_column(
|
||||
Text,
|
||||
doc="Detailed description of the service"
|
||||
)
|
||||
|
||||
# Category
|
||||
category: Mapped[str] = mapped_column(
|
||||
String(50),
|
||||
nullable=False,
|
||||
default=ServiceCategory.MANAGED_SERVICES.value,
|
||||
doc="Service category: managed_services, security, backup, cloud, etc."
|
||||
doc="Service category: gps_monitoring, support_plan, voip, web_hosting, email, hardware, addon"
|
||||
)
|
||||
|
||||
# Billing
|
||||
billing_frequency: Mapped[str] = mapped_column(
|
||||
String(20),
|
||||
# Product identification
|
||||
product_code: Mapped[str] = mapped_column(
|
||||
String(50),
|
||||
nullable=False,
|
||||
default=BillingFrequency.MONTHLY.value,
|
||||
doc="Billing frequency: monthly, quarterly, annual, one_time"
|
||||
doc="Product code identifier"
|
||||
)
|
||||
|
||||
# Pricing
|
||||
unit_price: Mapped[Decimal] = mapped_column(
|
||||
Numeric(10, 2),
|
||||
product_name: Mapped[str] = mapped_column(
|
||||
String(255),
|
||||
nullable=False,
|
||||
default=Decimal("0.00"),
|
||||
doc="Price per unit"
|
||||
doc="Name of the product/service"
|
||||
)
|
||||
|
||||
description: Mapped[Optional[str]] = mapped_column(
|
||||
Text,
|
||||
doc="Detailed description of the product/service"
|
||||
)
|
||||
|
||||
# Quantity and pricing
|
||||
quantity: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
nullable=False,
|
||||
@@ -328,29 +327,49 @@ class QuoteItem(Base, UUIDMixin, TimestampMixin):
|
||||
doc="Number of units"
|
||||
)
|
||||
|
||||
setup_fee: Mapped[Decimal] = mapped_column(
|
||||
unit_price: Mapped[Decimal] = mapped_column(
|
||||
Numeric(10, 2),
|
||||
nullable=False,
|
||||
doc="Price per unit"
|
||||
)
|
||||
|
||||
setup_price: Mapped[Decimal] = mapped_column(
|
||||
Numeric(10, 2),
|
||||
nullable=False,
|
||||
default=Decimal("0.00"),
|
||||
server_default="0.00",
|
||||
doc="One-time setup fee"
|
||||
doc="One-time setup price"
|
||||
)
|
||||
|
||||
# Billing
|
||||
billing_frequency: Mapped[str] = mapped_column(
|
||||
String(20),
|
||||
nullable=False,
|
||||
default=BillingFrequency.MONTHLY.value,
|
||||
server_default="monthly",
|
||||
doc="Billing frequency: monthly, yearly, one_time"
|
||||
)
|
||||
|
||||
# Configuration
|
||||
is_required: Mapped[bool] = mapped_column(
|
||||
tier: Mapped[Optional[str]] = mapped_column(
|
||||
String(50),
|
||||
doc="Pricing tier"
|
||||
)
|
||||
|
||||
is_recommended: Mapped[bool] = mapped_column(
|
||||
Boolean,
|
||||
nullable=False,
|
||||
default=False,
|
||||
server_default="0",
|
||||
doc="Whether this item is required (cannot be removed)"
|
||||
doc="Whether this item is recommended"
|
||||
)
|
||||
|
||||
sort_order: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
# Timestamp (no updated_at in DB)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
nullable=False,
|
||||
default=0,
|
||||
server_default="0",
|
||||
doc="Display order within the quote"
|
||||
server_default=func.now(),
|
||||
doc="Timestamp when the item was created"
|
||||
)
|
||||
|
||||
# Relationships
|
||||
@@ -362,24 +381,20 @@ class QuoteItem(Base, UUIDMixin, TimestampMixin):
|
||||
# Constraints and indexes
|
||||
__table_args__ = (
|
||||
CheckConstraint(
|
||||
"category IN ('managed_services', 'security', 'backup', 'cloud', 'hardware', 'software', 'consulting', 'support')",
|
||||
"category IN ('gps_monitoring', 'support_plan', 'voip', 'web_hosting', 'email', 'hardware', 'addon')",
|
||||
name="ck_quote_items_category"
|
||||
),
|
||||
CheckConstraint(
|
||||
"billing_frequency IN ('monthly', 'quarterly', 'annual', 'one_time')",
|
||||
"billing_frequency IN ('monthly', 'yearly', 'one_time')",
|
||||
name="ck_quote_items_billing_frequency"
|
||||
),
|
||||
CheckConstraint(
|
||||
"quantity >= 1",
|
||||
name="ck_quote_items_quantity_positive"
|
||||
),
|
||||
Index("idx_quote_items_quote_id", "quote_id"),
|
||||
Index("idx_quote_items_category", "category"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of the quote item."""
|
||||
return f"<QuoteItem(service='{self.service_name}', qty={self.quantity}, price={self.unit_price})>"
|
||||
return f"<QuoteItem(product='{self.product_name}', qty={self.quantity}, price={self.unit_price})>"
|
||||
|
||||
@property
|
||||
def line_total(self) -> Decimal:
|
||||
@@ -391,15 +406,13 @@ class QuoteItem(Base, UUIDMixin, TimestampMixin):
|
||||
"""Calculate the monthly amount based on billing frequency."""
|
||||
if self.billing_frequency == BillingFrequency.MONTHLY.value:
|
||||
return self.line_total
|
||||
elif self.billing_frequency == BillingFrequency.QUARTERLY.value:
|
||||
return self.line_total / Decimal("3")
|
||||
elif self.billing_frequency == BillingFrequency.ANNUAL.value:
|
||||
elif self.billing_frequency == BillingFrequency.YEARLY.value:
|
||||
return self.line_total / Decimal("12")
|
||||
else: # one_time
|
||||
return Decimal("0.00")
|
||||
|
||||
|
||||
class QuoteActivity(Base, UUIDMixin, TimestampMixin):
|
||||
class QuoteActivity(Base, UUIDMixin):
|
||||
"""
|
||||
Quote activity model for tracking quote history and changes.
|
||||
|
||||
@@ -408,13 +421,13 @@ class QuoteActivity(Base, UUIDMixin, TimestampMixin):
|
||||
Attributes:
|
||||
quote_id: Reference to the parent quote
|
||||
action: Action performed (created, updated, submitted, etc.)
|
||||
description: Detailed description of the action
|
||||
actor: Who performed the action (email, 'system', 'admin')
|
||||
step_name: Name of the wizard step associated with the action
|
||||
details: Additional details about the action (longtext)
|
||||
ip_address: IP address of the actor
|
||||
metadata: JSON metadata about the action
|
||||
created_at: Timestamp when the activity was recorded
|
||||
"""
|
||||
|
||||
__tablename__ = "quote_activities"
|
||||
__tablename__ = "quote_activity"
|
||||
|
||||
# Foreign keys
|
||||
quote_id: Mapped[str] = mapped_column(
|
||||
@@ -431,14 +444,14 @@ class QuoteActivity(Base, UUIDMixin, TimestampMixin):
|
||||
doc="Action performed: created, updated, item_added, item_removed, submitted, status_changed, etc."
|
||||
)
|
||||
|
||||
description: Mapped[Optional[str]] = mapped_column(
|
||||
Text,
|
||||
doc="Detailed description of the action"
|
||||
step_name: Mapped[Optional[str]] = mapped_column(
|
||||
String(50),
|
||||
doc="Name of the wizard step associated with the action"
|
||||
)
|
||||
|
||||
actor: Mapped[Optional[str]] = mapped_column(
|
||||
String(255),
|
||||
doc="Who performed the action (email, 'system', 'admin')"
|
||||
details: Mapped[Optional[str]] = mapped_column(
|
||||
Text,
|
||||
doc="Additional details about the action"
|
||||
)
|
||||
|
||||
ip_address: Mapped[Optional[str]] = mapped_column(
|
||||
@@ -446,9 +459,12 @@ class QuoteActivity(Base, UUIDMixin, TimestampMixin):
|
||||
doc="IP address of the actor"
|
||||
)
|
||||
|
||||
metadata: Mapped[Optional[str]] = mapped_column(
|
||||
Text,
|
||||
doc="JSON metadata about the action"
|
||||
# Timestamp (no updated_at in DB)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
nullable=False,
|
||||
server_default=func.now(),
|
||||
doc="Timestamp when the activity was recorded"
|
||||
)
|
||||
|
||||
# Relationships
|
||||
@@ -469,21 +485,24 @@ class QuoteActivity(Base, UUIDMixin, TimestampMixin):
|
||||
return f"<QuoteActivity(quote_id='{self.quote_id}', action='{self.action}')>"
|
||||
|
||||
|
||||
class QuoteNotification(Base, UUIDMixin, TimestampMixin):
|
||||
class QuoteNotification(Base, UUIDMixin):
|
||||
"""
|
||||
Quote notification model for tracking notifications sent.
|
||||
|
||||
Records all notifications (emails, SMS, alerts) sent for a quote.
|
||||
Records all notifications (emails, webhooks) sent for a quote.
|
||||
|
||||
Attributes:
|
||||
quote_id: Reference to the parent quote
|
||||
notification_type: Type of notification (email_sent, sms_sent, etc.)
|
||||
recipient: Notification recipient (email, phone, etc.)
|
||||
notification_type: Type of notification (email, webhook)
|
||||
recipient: Notification recipient (email address, webhook URL, etc.)
|
||||
subject: Notification subject
|
||||
content: Notification content/body
|
||||
status: Delivery status (pending, sent, delivered, failed)
|
||||
sent_at: Timestamp when notification was sent
|
||||
body: Notification body content
|
||||
status: Delivery status (pending, sent, failed)
|
||||
attempts: Number of delivery attempts
|
||||
last_attempt_at: Timestamp of last delivery attempt
|
||||
sent_at: Timestamp when notification was successfully sent
|
||||
error_message: Error message if delivery failed
|
||||
created_at: Timestamp when notification was created
|
||||
"""
|
||||
|
||||
__tablename__ = "quote_notifications"
|
||||
@@ -500,23 +519,23 @@ class QuoteNotification(Base, UUIDMixin, TimestampMixin):
|
||||
notification_type: Mapped[str] = mapped_column(
|
||||
String(30),
|
||||
nullable=False,
|
||||
doc="Type of notification: email_sent, sms_sent, admin_alert, reminder_sent"
|
||||
doc="Type of notification: email, webhook"
|
||||
)
|
||||
|
||||
recipient: Mapped[str] = mapped_column(
|
||||
String(255),
|
||||
nullable=False,
|
||||
doc="Notification recipient (email, phone, etc.)"
|
||||
doc="Notification recipient (email address, webhook URL, etc.)"
|
||||
)
|
||||
|
||||
subject: Mapped[Optional[str]] = mapped_column(
|
||||
String(500),
|
||||
String(255),
|
||||
doc="Notification subject"
|
||||
)
|
||||
|
||||
content: Mapped[Optional[str]] = mapped_column(
|
||||
body: Mapped[Optional[str]] = mapped_column(
|
||||
Text,
|
||||
doc="Notification content/body"
|
||||
doc="Notification body content"
|
||||
)
|
||||
|
||||
# Status tracking
|
||||
@@ -525,12 +544,25 @@ class QuoteNotification(Base, UUIDMixin, TimestampMixin):
|
||||
nullable=False,
|
||||
default="pending",
|
||||
server_default="pending",
|
||||
doc="Delivery status: pending, sent, delivered, failed"
|
||||
doc="Delivery status: pending, sent, failed"
|
||||
)
|
||||
|
||||
attempts: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
nullable=False,
|
||||
default=0,
|
||||
server_default="0",
|
||||
doc="Number of delivery attempts"
|
||||
)
|
||||
|
||||
last_attempt_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime,
|
||||
doc="Timestamp of last delivery attempt"
|
||||
)
|
||||
|
||||
sent_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime,
|
||||
doc="Timestamp when notification was sent"
|
||||
doc="Timestamp when notification was successfully sent"
|
||||
)
|
||||
|
||||
error_message: Mapped[Optional[str]] = mapped_column(
|
||||
@@ -538,6 +570,14 @@ class QuoteNotification(Base, UUIDMixin, TimestampMixin):
|
||||
doc="Error message if delivery failed"
|
||||
)
|
||||
|
||||
# Timestamp (no updated_at in DB)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
nullable=False,
|
||||
server_default=func.now(),
|
||||
doc="Timestamp when the notification was created"
|
||||
)
|
||||
|
||||
# Relationships
|
||||
quote: Mapped["Quote"] = relationship(
|
||||
"Quote",
|
||||
@@ -547,11 +587,11 @@ class QuoteNotification(Base, UUIDMixin, TimestampMixin):
|
||||
# Constraints and indexes
|
||||
__table_args__ = (
|
||||
CheckConstraint(
|
||||
"notification_type IN ('email_sent', 'sms_sent', 'admin_alert', 'reminder_sent')",
|
||||
"notification_type IN ('email', 'webhook')",
|
||||
name="ck_quote_notifications_type"
|
||||
),
|
||||
CheckConstraint(
|
||||
"status IN ('pending', 'sent', 'delivered', 'failed')",
|
||||
"status IN ('pending', 'sent', 'failed')",
|
||||
name="ck_quote_notifications_status"
|
||||
),
|
||||
Index("idx_quote_notifications_quote_id", "quote_id"),
|
||||
|
||||
Reference in New Issue
Block a user