pounce/backend/alembic/versions/003_add_portfolio_tables.py
yves.gugger 6e84103a5b feat: Complete business model expansion
MAJOR FEATURES:
- New pricing tiers: Scout (Free), Trader (€19/mo), Tycoon (€49/mo)
- Portfolio management: Track owned domains with purchase price, value, ROI
- Domain valuation engine: Algorithmic estimates based on length, TLD, keywords, brandability
- Dashboard tabs: Watchlist + Portfolio views
- Valuation modal: Score breakdown with confidence level

BACKEND:
- New models: PortfolioDomain, DomainValuation
- New API routes: /portfolio/* with full CRUD
- Valuation service with multi-factor algorithm
- Database migration for portfolio tables

FRONTEND:
- Updated pricing page with comparison table and billing toggle
- Dashboard with Watchlist/Portfolio tabs
- Portfolio summary stats: Total value, invested, unrealized P/L, ROI
- Add portfolio domain modal with all fields
- Domain valuation modal with score visualization
- Updated landing page with new tier pricing
- Hero section with large puma logo

DESIGN:
- Consistent minimalist dark theme
- Responsive on all devices
- Professional animations and transitions
2025-12-08 11:08:18 +01:00

77 lines
3.5 KiB
Python

"""Add portfolio and valuation tables
Revision ID: 003
Revises: 002
Create Date: 2025-01-08
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic
revision = '003'
down_revision = '002'
branch_labels = None
depends_on = None
def upgrade() -> None:
# Create portfolio_domains table
op.create_table(
'portfolio_domains',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('domain', sa.String(255), nullable=False),
sa.Column('purchase_date', sa.DateTime(), nullable=True),
sa.Column('purchase_price', sa.Float(), nullable=True),
sa.Column('purchase_registrar', sa.String(100), nullable=True),
sa.Column('registrar', sa.String(100), nullable=True),
sa.Column('renewal_date', sa.DateTime(), nullable=True),
sa.Column('renewal_cost', sa.Float(), nullable=True),
sa.Column('auto_renew', sa.Boolean(), default=True),
sa.Column('estimated_value', sa.Float(), nullable=True),
sa.Column('value_updated_at', sa.DateTime(), nullable=True),
sa.Column('is_sold', sa.Boolean(), default=False),
sa.Column('sale_date', sa.DateTime(), nullable=True),
sa.Column('sale_price', sa.Float(), nullable=True),
sa.Column('status', sa.String(50), default='active'),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('tags', sa.String(500), 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')),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_portfolio_domains_id'), 'portfolio_domains', ['id'], unique=False)
op.create_index(op.f('ix_portfolio_domains_user_id'), 'portfolio_domains', ['user_id'], unique=False)
# Create domain_valuations table
op.create_table(
'domain_valuations',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('domain', sa.String(255), nullable=False),
sa.Column('estimated_value', sa.Float(), nullable=False),
sa.Column('length_score', sa.Integer(), nullable=True),
sa.Column('tld_score', sa.Integer(), nullable=True),
sa.Column('keyword_score', sa.Integer(), nullable=True),
sa.Column('brandability_score', sa.Integer(), nullable=True),
sa.Column('moz_da', sa.Integer(), nullable=True),
sa.Column('moz_pa', sa.Integer(), nullable=True),
sa.Column('backlinks', sa.Integer(), nullable=True),
sa.Column('source', sa.String(50), default='internal'),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP')),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_domain_valuations_id'), 'domain_valuations', ['id'], unique=False)
op.create_index(op.f('ix_domain_valuations_domain'), 'domain_valuations', ['domain'], unique=False)
def downgrade() -> None:
op.drop_index(op.f('ix_domain_valuations_domain'), table_name='domain_valuations')
op.drop_index(op.f('ix_domain_valuations_id'), table_name='domain_valuations')
op.drop_table('domain_valuations')
op.drop_index(op.f('ix_portfolio_domains_user_id'), table_name='portfolio_domains')
op.drop_index(op.f('ix_portfolio_domains_id'), table_name='portfolio_domains')
op.drop_table('portfolio_domains')