Files
claudetools/migrations/versions/20260309_074038_msp_quote_wizard_tables.py
azcomputerguru a1a19f8c00 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>
2026-03-09 08:14:13 -07:00

157 lines
7.6 KiB
Python

"""MSP Quote Wizard Tables
Revision ID: 20260309_074038
Revises: a0dfb0b4373c
Create Date: 2026-03-09 07:40:38
Creates the MSP Quote Wizard tables:
- quotes: Main quote records with contact info, pricing, and tracking
- quote_items: Line items for each quote (services, products, addons)
- quote_activity: Activity log for quote interactions
- quote_notifications: Email/webhook notification queue
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '20260309_074038'
down_revision: Union[str, None] = 'a0dfb0b4373c'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Create MSP Quote Wizard tables."""
# 1. Create quotes table - main quote records
op.create_table(
'quotes',
sa.Column('id', sa.CHAR(36), primary_key=True),
sa.Column('company_name', sa.String(255), nullable=True),
sa.Column('contact_name', sa.String(255), nullable=False),
sa.Column('contact_email', sa.String(255), nullable=False),
sa.Column('contact_phone', sa.String(50), nullable=True),
sa.Column('employee_count', sa.Integer(), nullable=True),
sa.Column('industry', sa.String(100), nullable=True),
sa.Column('current_it_situation', sa.Text(), nullable=True),
sa.Column('status', sa.Enum('draft', 'submitted', 'viewed', 'followed_up', 'converted', 'expired', name='quote_status'), server_default='draft'),
sa.Column('access_token', sa.String(64), unique=True, nullable=False),
sa.Column('monthly_total', sa.DECIMAL(10, 2), server_default='0'),
sa.Column('setup_total', sa.DECIMAL(10, 2), server_default='0'),
sa.Column('syncro_lead_id', sa.String(100), nullable=True),
sa.Column('syncro_synced_at', sa.DateTime(), nullable=True),
sa.Column('is_existing_customer', sa.Boolean(), server_default='0'),
sa.Column('source', sa.String(50), server_default='website'),
sa.Column('utm_source', sa.String(100), nullable=True),
sa.Column('utm_medium', sa.String(100), nullable=True),
sa.Column('utm_campaign', sa.String(100), nullable=True),
sa.Column('ip_address', sa.String(45), nullable=True),
sa.Column('user_agent', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP')),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')),
sa.Column('submitted_at', sa.DateTime(), nullable=True),
sa.Column('expires_at', sa.DateTime(), nullable=True),
)
# Indexes for quotes table
op.create_index('idx_quotes_status', 'quotes', ['status'])
op.create_index('idx_quotes_email', 'quotes', ['contact_email'])
op.create_index('idx_quotes_created', 'quotes', ['created_at'])
op.create_index('idx_quotes_token', 'quotes', ['access_token'])
# 2. Create quote_items table - line items for each quote
op.create_table(
'quote_items',
sa.Column('id', sa.CHAR(36), primary_key=True),
sa.Column('quote_id', sa.CHAR(36), nullable=False),
sa.Column('category', sa.Enum('gps_monitoring', 'support_plan', 'voip', 'web_hosting', 'email', 'hardware', 'addon', name='quote_item_category'), nullable=False),
sa.Column('product_code', sa.String(50), nullable=False),
sa.Column('product_name', sa.String(255), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('quantity', sa.Integer(), server_default='1'),
sa.Column('unit_price', sa.DECIMAL(10, 2), nullable=False),
sa.Column('setup_price', sa.DECIMAL(10, 2), server_default='0'),
sa.Column('billing_frequency', sa.Enum('monthly', 'yearly', 'one_time', name='billing_frequency'), server_default='monthly'),
sa.Column('tier', sa.String(50), nullable=True),
sa.Column('is_recommended', sa.Boolean(), server_default='0'),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP')),
sa.ForeignKeyConstraint(['quote_id'], ['quotes.id'], ondelete='CASCADE'),
)
# Indexes for quote_items table
op.create_index('idx_quote_items_quote', 'quote_items', ['quote_id'])
op.create_index('idx_quote_items_category', 'quote_items', ['category'])
# 3. Create quote_activity table - activity log for quotes
op.create_table(
'quote_activity',
sa.Column('id', sa.CHAR(36), primary_key=True),
sa.Column('quote_id', sa.CHAR(36), nullable=False),
sa.Column('action', sa.String(50), nullable=False),
sa.Column('step_name', sa.String(50), nullable=True),
sa.Column('details', sa.JSON(), nullable=True),
sa.Column('ip_address', sa.String(45), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP')),
sa.ForeignKeyConstraint(['quote_id'], ['quotes.id'], ondelete='CASCADE'),
)
# Index for quote_activity table
op.create_index('idx_quote_activity_quote', 'quote_activity', ['quote_id'])
# 4. Create quote_notifications table - notification queue
op.create_table(
'quote_notifications',
sa.Column('id', sa.CHAR(36), primary_key=True),
sa.Column('quote_id', sa.CHAR(36), nullable=False),
sa.Column('notification_type', sa.Enum('email', 'webhook', name='notification_type'), nullable=False),
sa.Column('recipient', sa.String(255), nullable=False),
sa.Column('subject', sa.String(255), nullable=True),
sa.Column('body', sa.Text(), nullable=True),
sa.Column('status', sa.Enum('pending', 'sent', 'failed', name='notification_status'), server_default='pending'),
sa.Column('attempts', sa.Integer(), server_default='0'),
sa.Column('last_attempt_at', sa.DateTime(), nullable=True),
sa.Column('sent_at', sa.DateTime(), nullable=True),
sa.Column('error_message', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP')),
sa.ForeignKeyConstraint(['quote_id'], ['quotes.id'], ondelete='CASCADE'),
)
# Indexes for quote_notifications table
op.create_index('idx_notifications_status', 'quote_notifications', ['status'])
op.create_index('idx_notifications_quote', 'quote_notifications', ['quote_id'])
def downgrade() -> None:
"""Drop MSP Quote Wizard tables in reverse order."""
# Drop quote_notifications and its indexes
op.drop_index('idx_notifications_quote', table_name='quote_notifications')
op.drop_index('idx_notifications_status', table_name='quote_notifications')
op.drop_table('quote_notifications')
# Drop quote_activity and its index
op.drop_index('idx_quote_activity_quote', table_name='quote_activity')
op.drop_table('quote_activity')
# Drop quote_items and its indexes
op.drop_index('idx_quote_items_category', table_name='quote_items')
op.drop_index('idx_quote_items_quote', table_name='quote_items')
op.drop_table('quote_items')
# Drop quotes and its indexes
op.drop_index('idx_quotes_token', table_name='quotes')
op.drop_index('idx_quotes_created', table_name='quotes')
op.drop_index('idx_quotes_email', table_name='quotes')
op.drop_index('idx_quotes_status', table_name='quotes')
op.drop_table('quotes')
# Drop the enum types
op.execute("DROP TYPE IF EXISTS notification_status")
op.execute("DROP TYPE IF EXISTS notification_type")
op.execute("DROP TYPE IF EXISTS billing_frequency")
op.execute("DROP TYPE IF EXISTS quote_item_category")
op.execute("DROP TYPE IF EXISTS quote_status")