Dockerized web app (FastAPI + React + PostgreSQL) for scoping client ratecards against the GMAL master asset database. Features: - GMAL data ingestion from Excel (390 assets, 120 roles, 5 model types) - AI-powered document parsing and asset extraction (Claude Opus 4.6) - AI matching engine with parallel batching, confidence scoring, caveats - Ratecard builder with hours x volume calculation - Excel and PDF export - GMAL browser and inline editor - AI cost tracking per project (persisted to DB) - Debug panel for AI call inspection - Dark theme UI with gold (#FFC407) accent Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
181 lines
7.8 KiB
Python
181 lines
7.8 KiB
Python
"""Initial schema
|
|
|
|
Revision ID: 001
|
|
Revises:
|
|
Create Date: 2026-03-27
|
|
"""
|
|
from typing import Sequence, Union
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
revision: str = '001'
|
|
down_revision: Union[str, None] = None
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
model_type_enum = sa.Enum(
|
|
'current_oplus', 'ai_oplus', 'current_local', 'ai_local', 'asset_factory',
|
|
name='modeltype', create_type=False,
|
|
)
|
|
project_status_enum = sa.Enum(
|
|
'draft', 'parsing', 'matching', 'review', 'building', 'finalized',
|
|
name='projectstatus', create_type=False,
|
|
)
|
|
match_confidence_enum = sa.Enum(
|
|
'exact', 'close', 'multiple', 'none',
|
|
name='matchconfidence', create_type=False,
|
|
)
|
|
|
|
# Create enum types explicitly
|
|
op.execute("DO $$ BEGIN CREATE TYPE modeltype AS ENUM ('current_oplus', 'ai_oplus', 'current_local', 'ai_local', 'asset_factory'); EXCEPTION WHEN duplicate_object THEN NULL; END $$")
|
|
op.execute("DO $$ BEGIN CREATE TYPE projectstatus AS ENUM ('draft', 'parsing', 'matching', 'review', 'building', 'finalized'); EXCEPTION WHEN duplicate_object THEN NULL; END $$")
|
|
op.execute("DO $$ BEGIN CREATE TYPE matchconfidence AS ENUM ('exact', 'close', 'multiple', 'none'); EXCEPTION WHEN duplicate_object THEN NULL; END $$")
|
|
|
|
# gmal_assets
|
|
op.create_table(
|
|
'gmal_assets',
|
|
sa.Column('id', sa.Integer(), primary_key=True),
|
|
sa.Column('gmal_id', sa.String(20), nullable=False, unique=True),
|
|
sa.Column('swop_asset_id', sa.String(50)),
|
|
sa.Column('region', sa.String(20)),
|
|
sa.Column('category', sa.String(100)),
|
|
sa.Column('sub_category', sa.String(100), index=True),
|
|
sa.Column('sub_category_description', sa.Text()),
|
|
sa.Column('asset_name', sa.String(255)),
|
|
sa.Column('complexity_level', sa.Integer()),
|
|
sa.Column('complexity_name', sa.String(20)),
|
|
sa.Column('unique_name', sa.String(255)),
|
|
sa.Column('asset_description', sa.Text()),
|
|
sa.Column('complexity_description', sa.Text()),
|
|
sa.Column('caveats', sa.Text()),
|
|
sa.Column('ai_enhanced_description', sa.Text()),
|
|
sa.Column('master_adapt', sa.String(20)),
|
|
sa.Column('ai_efficiency_pct', sa.Numeric(5, 2)),
|
|
sa.Column('has_hour_routes', sa.Boolean(), default=False),
|
|
sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()),
|
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.func.now()),
|
|
)
|
|
op.create_index('ix_gmal_assets_gmal_id', 'gmal_assets', ['gmal_id'])
|
|
|
|
# roles
|
|
op.create_table(
|
|
'roles',
|
|
sa.Column('id', sa.Integer(), primary_key=True),
|
|
sa.Column('discipline', sa.String(100), nullable=False, index=True),
|
|
sa.Column('role_title', sa.String(200), nullable=False),
|
|
sa.Column('entity', sa.String(100)),
|
|
sa.Column('resource_location', sa.String(50)),
|
|
sa.Column('unique_name', sa.String(255)),
|
|
sa.Column('sort_order', sa.Integer()),
|
|
sa.Column('is_programme_role', sa.Boolean(), default=False),
|
|
sa.UniqueConstraint('role_title', 'entity', name='uq_role_title_entity'),
|
|
)
|
|
|
|
# gmal_hours
|
|
op.create_table(
|
|
'gmal_hours',
|
|
sa.Column('id', sa.Integer(), primary_key=True),
|
|
sa.Column('gmal_asset_id', sa.Integer(), sa.ForeignKey('gmal_assets.id'), nullable=False),
|
|
sa.Column('role_id', sa.Integer(), sa.ForeignKey('roles.id'), nullable=False),
|
|
sa.Column('model_type', model_type_enum, nullable=False),
|
|
sa.Column('hours', sa.Numeric(10, 2), nullable=False),
|
|
sa.UniqueConstraint('gmal_asset_id', 'role_id', 'model_type', name='uq_gmal_role_model'),
|
|
)
|
|
op.create_index('idx_gmal_hours_asset', 'gmal_hours', ['gmal_asset_id'])
|
|
op.create_index('idx_gmal_hours_model', 'gmal_hours', ['model_type'])
|
|
|
|
# gmal_service_lines
|
|
op.create_table(
|
|
'gmal_service_lines',
|
|
sa.Column('id', sa.Integer(), primary_key=True),
|
|
sa.Column('number', sa.String(20)),
|
|
sa.Column('name', sa.String(255)),
|
|
sa.Column('type', sa.String(50)),
|
|
sa.Column('gmal_id', sa.String(20), index=True),
|
|
)
|
|
|
|
# role_level_mappings
|
|
op.create_table(
|
|
'role_level_mappings',
|
|
sa.Column('id', sa.Integer(), primary_key=True),
|
|
sa.Column('role_name', sa.String(200)),
|
|
sa.Column('number', sa.String(20)),
|
|
sa.Column('level_name', sa.String(200)),
|
|
sa.Column('type', sa.String(50)),
|
|
)
|
|
|
|
# projects
|
|
op.create_table(
|
|
'projects',
|
|
sa.Column('id', sa.Integer(), primary_key=True),
|
|
sa.Column('name', sa.String(255), nullable=False),
|
|
sa.Column('client_name', sa.String(255)),
|
|
sa.Column('description', sa.Text()),
|
|
sa.Column('model_type', model_type_enum, default='current_oplus'),
|
|
sa.Column('status', project_status_enum, default='draft'),
|
|
sa.Column('source_filename', sa.String(255)),
|
|
sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()),
|
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.func.now()),
|
|
)
|
|
|
|
# client_assets
|
|
op.create_table(
|
|
'client_assets',
|
|
sa.Column('id', sa.Integer(), primary_key=True),
|
|
sa.Column('project_id', sa.Integer(), sa.ForeignKey('projects.id', ondelete='CASCADE'), nullable=False),
|
|
sa.Column('raw_name', sa.String(500)),
|
|
sa.Column('raw_description', sa.Text()),
|
|
sa.Column('volume', sa.Integer(), default=1),
|
|
sa.Column('sort_order', sa.Integer()),
|
|
sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()),
|
|
)
|
|
|
|
# matches
|
|
op.create_table(
|
|
'matches',
|
|
sa.Column('id', sa.Integer(), primary_key=True),
|
|
sa.Column('client_asset_id', sa.Integer(), sa.ForeignKey('client_assets.id', ondelete='CASCADE'), nullable=False),
|
|
sa.Column('gmal_asset_id', sa.Integer(), sa.ForeignKey('gmal_assets.id'), nullable=False),
|
|
sa.Column('confidence', match_confidence_enum, nullable=False),
|
|
sa.Column('confidence_score', sa.Numeric(3, 2)),
|
|
sa.Column('ai_reasoning', sa.Text()),
|
|
sa.Column('caveat_text', sa.Text()),
|
|
sa.Column('is_selected', sa.Boolean(), default=False),
|
|
sa.Column('rank', sa.Integer(), default=1),
|
|
sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()),
|
|
)
|
|
op.create_index('idx_matches_client_asset', 'matches', ['client_asset_id'])
|
|
|
|
# ratecard_lines
|
|
op.create_table(
|
|
'ratecard_lines',
|
|
sa.Column('id', sa.Integer(), primary_key=True),
|
|
sa.Column('project_id', sa.Integer(), sa.ForeignKey('projects.id', ondelete='CASCADE'), nullable=False),
|
|
sa.Column('client_asset_id', sa.Integer(), sa.ForeignKey('client_assets.id'), nullable=False),
|
|
sa.Column('gmal_asset_id', sa.Integer(), sa.ForeignKey('gmal_assets.id'), nullable=False),
|
|
sa.Column('role_id', sa.Integer(), sa.ForeignKey('roles.id'), nullable=False),
|
|
sa.Column('base_hours', sa.Numeric(10, 2)),
|
|
sa.Column('volume', sa.Integer(), default=1),
|
|
sa.Column('total_hours', sa.Numeric(10, 2)),
|
|
sa.Column('manual_override', sa.Numeric(10, 2)),
|
|
sa.Column('notes', sa.Text()),
|
|
)
|
|
op.create_index('idx_ratecard_project', 'ratecard_lines', ['project_id'])
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_table('ratecard_lines')
|
|
op.drop_table('matches')
|
|
op.drop_table('client_assets')
|
|
op.drop_table('projects')
|
|
op.drop_table('role_level_mappings')
|
|
op.drop_table('gmal_service_lines')
|
|
op.drop_table('gmal_hours')
|
|
op.drop_table('roles')
|
|
op.drop_table('gmal_assets')
|
|
|
|
sa.Enum(name='matchconfidence').drop(op.get_bind(), checkfirst=True)
|
|
sa.Enum(name='projectstatus').drop(op.get_bind(), checkfirst=True)
|
|
sa.Enum(name='modeltype').drop(op.get_bind(), checkfirst=True)
|