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
77 lines
3.5 KiB
Python
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')
|
|
|