init commit
This commit is contained in:
67
migrations/env.py
Normal file
67
migrations/env.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""Alembic environment configuration"""
|
||||
|
||||
from logging.config import fileConfig
|
||||
import os
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
from alembic import context
|
||||
from app.core.config import get_settings
|
||||
from app.db.database import Base
|
||||
|
||||
# Get settings
|
||||
settings = get_settings()
|
||||
|
||||
# this is the Alembic Config object
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# Set sqlalchemy.url from environment
|
||||
config.set_main_option("sqlalchemy.url", settings.database_url)
|
||||
|
||||
# Add models
|
||||
target_metadata = Base.metadata
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode."""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode."""
|
||||
configuration = config.get_section(config.config_ini_section)
|
||||
configuration["sqlalchemy.url"] = settings.database_url
|
||||
connectable = engine_from_config(
|
||||
configuration,
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
compare_type=True,
|
||||
compare_server_default=True,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
24
migrations/script.py.mako
Normal file
24
migrations/script.py.mako
Normal file
@@ -0,0 +1,24 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
||||
247
migrations/versions/001_initial.py
Normal file
247
migrations/versions/001_initial.py
Normal file
@@ -0,0 +1,247 @@
|
||||
"""Initial schema migration
|
||||
|
||||
Revision ID: 001_initial
|
||||
Revises:
|
||||
Create Date: 2025-12-10
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy import text
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '001_initial'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create enum types with proper PostgreSQL syntax
|
||||
# Check if type exists before creating
|
||||
conn = op.get_bind()
|
||||
|
||||
enum_types = [
|
||||
('family_role', ['owner', 'member', 'restricted']),
|
||||
('account_type', ['card', 'cash', 'deposit', 'goal', 'other']),
|
||||
('category_type', ['expense', 'income']),
|
||||
('transaction_type', ['expense', 'income', 'transfer']),
|
||||
('budget_period', ['daily', 'weekly', 'monthly', 'yearly']),
|
||||
]
|
||||
|
||||
# Create enums with safe approach
|
||||
for enum_name, enum_values in enum_types:
|
||||
# Check if type exists
|
||||
result = conn.execute(
|
||||
text(f"SELECT EXISTS(SELECT 1 FROM pg_type WHERE typname = '{enum_name}')")
|
||||
)
|
||||
if not result.scalar():
|
||||
values_str = ', '.join(f"'{v}'" for v in enum_values)
|
||||
conn.execute(text(f"CREATE TYPE {enum_name} AS ENUM ({values_str})"))
|
||||
|
||||
# Create users table
|
||||
op.create_table(
|
||||
'users',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('telegram_id', sa.Integer(), nullable=False),
|
||||
sa.Column('username', sa.String(length=255), nullable=True),
|
||||
sa.Column('first_name', sa.String(length=255), nullable=True),
|
||||
sa.Column('last_name', sa.String(length=255), nullable=True),
|
||||
sa.Column('phone', sa.String(length=20), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('last_activity', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('telegram_id')
|
||||
)
|
||||
op.create_index(op.f('ix_users_telegram_id'), 'users', ['telegram_id'], unique=True)
|
||||
|
||||
# Create families table
|
||||
op.create_table(
|
||||
'families',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('owner_id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=255), nullable=False),
|
||||
sa.Column('description', sa.String(length=500), nullable=True),
|
||||
sa.Column('currency', sa.String(length=3), nullable=False, server_default='RUB'),
|
||||
sa.Column('invite_code', sa.String(length=20), nullable=False),
|
||||
sa.Column('notification_level', sa.String(length=50), nullable=False, server_default='all'),
|
||||
sa.Column('accounting_period', sa.String(length=20), nullable=False, server_default='month'),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('invite_code')
|
||||
)
|
||||
op.create_index(op.f('ix_families_invite_code'), 'families', ['invite_code'], unique=True)
|
||||
|
||||
# Create family_members table
|
||||
op.create_table(
|
||||
'family_members',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('family_id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('role', postgresql.ENUM('owner', 'member', 'restricted', name='family_role', create_type=False), nullable=False, server_default='member'),
|
||||
sa.Column('can_edit_budget', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('can_manage_members', sa.Boolean(), nullable=False, server_default='false'),
|
||||
sa.Column('joined_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['family_id'], ['families.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_family_members_family_id'), 'family_members', ['family_id'], unique=False)
|
||||
op.create_index(op.f('ix_family_members_user_id'), 'family_members', ['user_id'], unique=False)
|
||||
|
||||
# Create family_invites table
|
||||
op.create_table(
|
||||
'family_invites',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('family_id', sa.Integer(), nullable=False),
|
||||
sa.Column('invite_code', sa.String(length=20), nullable=False),
|
||||
sa.Column('created_by_id', sa.Integer(), nullable=False),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('expires_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['family_id'], ['families.id'], ),
|
||||
sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('invite_code')
|
||||
)
|
||||
op.create_index(op.f('ix_family_invites_family_id'), 'family_invites', ['family_id'], unique=False)
|
||||
op.create_index(op.f('ix_family_invites_invite_code'), 'family_invites', ['invite_code'], unique=True)
|
||||
|
||||
# Create accounts table
|
||||
op.create_table(
|
||||
'accounts',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('family_id', sa.Integer(), nullable=False),
|
||||
sa.Column('owner_id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=255), nullable=False),
|
||||
sa.Column('account_type', postgresql.ENUM('card', 'cash', 'deposit', 'goal', 'other', name='account_type', create_type=False), nullable=False, server_default='card'),
|
||||
sa.Column('description', sa.String(length=500), nullable=True),
|
||||
sa.Column('balance', sa.Float(), nullable=False, server_default='0'),
|
||||
sa.Column('initial_balance', sa.Float(), nullable=False, server_default='0'),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('is_archived', sa.Boolean(), nullable=False, server_default='false'),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['family_id'], ['families.id'], ),
|
||||
sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_accounts_family_id'), 'accounts', ['family_id'], unique=False)
|
||||
|
||||
# Create categories table
|
||||
op.create_table(
|
||||
'categories',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('family_id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=255), nullable=False),
|
||||
sa.Column('category_type', postgresql.ENUM('expense', 'income', name='category_type', create_type=False), nullable=False),
|
||||
sa.Column('emoji', sa.String(length=10), nullable=True),
|
||||
sa.Column('color', sa.String(length=7), nullable=True),
|
||||
sa.Column('description', sa.String(length=500), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('is_default', sa.Boolean(), nullable=False, server_default='false'),
|
||||
sa.Column('order', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['family_id'], ['families.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_categories_family_id'), 'categories', ['family_id'], unique=False)
|
||||
|
||||
# Create transactions table
|
||||
op.create_table(
|
||||
'transactions',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('family_id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('account_id', sa.Integer(), nullable=False),
|
||||
sa.Column('category_id', sa.Integer(), nullable=True),
|
||||
sa.Column('amount', sa.Float(), nullable=False),
|
||||
sa.Column('transaction_type', postgresql.ENUM('expense', 'income', 'transfer', name='transaction_type', create_type=False), nullable=False),
|
||||
sa.Column('description', sa.String(length=500), nullable=True),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.Column('tags', sa.String(length=500), nullable=True),
|
||||
sa.Column('receipt_photo_url', sa.String(length=500), nullable=True),
|
||||
sa.Column('is_recurring', sa.Boolean(), nullable=False, server_default='false'),
|
||||
sa.Column('recurrence_pattern', sa.String(length=50), nullable=True),
|
||||
sa.Column('is_confirmed', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('transaction_date', sa.DateTime(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ),
|
||||
sa.ForeignKeyConstraint(['category_id'], ['categories.id'], ),
|
||||
sa.ForeignKeyConstraint(['family_id'], ['families.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_transactions_family_id'), 'transactions', ['family_id'], unique=False)
|
||||
op.create_index(op.f('ix_transactions_transaction_date'), 'transactions', ['transaction_date'], unique=False)
|
||||
|
||||
# Create budgets table
|
||||
op.create_table(
|
||||
'budgets',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('family_id', sa.Integer(), nullable=False),
|
||||
sa.Column('category_id', sa.Integer(), nullable=True),
|
||||
sa.Column('name', sa.String(length=255), nullable=False),
|
||||
sa.Column('limit_amount', sa.Float(), nullable=False),
|
||||
sa.Column('spent_amount', sa.Float(), nullable=False, server_default='0'),
|
||||
sa.Column('period', postgresql.ENUM('daily', 'weekly', 'monthly', 'yearly', name='budget_period', create_type=False), nullable=False, server_default='monthly'),
|
||||
sa.Column('alert_threshold', sa.Float(), nullable=False, server_default='80'),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('start_date', sa.DateTime(), nullable=False),
|
||||
sa.Column('end_date', sa.DateTime(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['category_id'], ['categories.id'], ),
|
||||
sa.ForeignKeyConstraint(['family_id'], ['families.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_budgets_family_id'), 'budgets', ['family_id'], unique=False)
|
||||
|
||||
# Create goals table
|
||||
op.create_table(
|
||||
'goals',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('family_id', sa.Integer(), nullable=False),
|
||||
sa.Column('account_id', sa.Integer(), nullable=True),
|
||||
sa.Column('name', sa.String(length=255), nullable=False),
|
||||
sa.Column('description', sa.String(length=500), nullable=True),
|
||||
sa.Column('target_amount', sa.Float(), nullable=False),
|
||||
sa.Column('current_amount', sa.Float(), nullable=False, server_default='0'),
|
||||
sa.Column('priority', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('is_completed', sa.Boolean(), nullable=False, server_default='false'),
|
||||
sa.Column('target_date', sa.DateTime(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('completed_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ),
|
||||
sa.ForeignKeyConstraint(['family_id'], ['families.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_goals_family_id'), 'goals', ['family_id'], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table('goals')
|
||||
op.drop_table('budgets')
|
||||
op.drop_table('transactions')
|
||||
op.drop_table('categories')
|
||||
op.drop_table('accounts')
|
||||
op.drop_table('family_invites')
|
||||
op.drop_table('family_members')
|
||||
op.drop_table('families')
|
||||
op.drop_table('users')
|
||||
|
||||
op.execute('DROP TYPE budget_period')
|
||||
op.execute('DROP TYPE transaction_type')
|
||||
op.execute('DROP TYPE category_type')
|
||||
op.execute('DROP TYPE account_type')
|
||||
op.execute('DROP TYPE family_role')
|
||||
196
migrations/versions/002_auth_and_audit.py
Normal file
196
migrations/versions/002_auth_and_audit.py
Normal file
@@ -0,0 +1,196 @@
|
||||
"""Auth entities, audit logging, and enhanced schema
|
||||
|
||||
Revision ID: 002_auth_and_audit
|
||||
Revises: 001_initial
|
||||
Create Date: 2025-12-10
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy import text
|
||||
|
||||
revision = '002_auth_and_audit'
|
||||
down_revision = '001_initial'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create enum for transaction status
|
||||
conn = op.get_bind()
|
||||
|
||||
enum_types = [
|
||||
('transaction_status', ['draft', 'pending_approval', 'executed', 'reversed']),
|
||||
('member_role', ['owner', 'adult', 'member', 'child', 'read_only']),
|
||||
('event_action', ['create', 'update', 'delete', 'confirm', 'execute', 'reverse']),
|
||||
]
|
||||
|
||||
for enum_name, enum_values in enum_types:
|
||||
result = conn.execute(
|
||||
text(f"SELECT EXISTS(SELECT 1 FROM pg_type WHERE typname = '{enum_name}')")
|
||||
)
|
||||
if not result.scalar():
|
||||
values_str = ', '.join(f"'{v}'" for v in enum_values)
|
||||
conn.execute(text(f"CREATE TYPE {enum_name} AS ENUM ({values_str})"))
|
||||
|
||||
# 1. Add session tracking to users (for JWT blacklisting)
|
||||
op.add_column('users', sa.Column('last_login_at', sa.DateTime(), nullable=True))
|
||||
op.add_column('users', sa.Column('password_hash', sa.String(length=255), nullable=True))
|
||||
|
||||
# 2. Create sessions table (for refresh tokens)
|
||||
op.create_table(
|
||||
'sessions',
|
||||
sa.Column('id', sa.String(length=36), nullable=False, primary_key=True),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('refresh_token_hash', sa.String(length=255), nullable=False),
|
||||
sa.Column('device_id', sa.String(length=255), nullable=True),
|
||||
sa.Column('ip_address', sa.String(length=45), nullable=True),
|
||||
sa.Column('user_agent', sa.String(length=500), nullable=True),
|
||||
sa.Column('expires_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('revoked_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id']),
|
||||
)
|
||||
op.create_index('ix_sessions_user_id', 'sessions', ['user_id'])
|
||||
op.create_index('ix_sessions_expires_at', 'sessions', ['expires_at'])
|
||||
|
||||
# 3. Create telegram_identities table
|
||||
op.create_table(
|
||||
'telegram_identities',
|
||||
sa.Column('id', sa.Integer(), nullable=False, primary_key=True),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('chat_id', sa.BigInteger(), nullable=False, unique=True),
|
||||
sa.Column('username', sa.String(length=255), nullable=True),
|
||||
sa.Column('first_name', sa.String(length=255), nullable=True),
|
||||
sa.Column('last_name', sa.String(length=255), nullable=True),
|
||||
sa.Column('is_bot', sa.Boolean(), nullable=False, server_default='false'),
|
||||
sa.Column('verified_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id']),
|
||||
)
|
||||
op.create_index('ix_telegram_identities_chat_id', 'telegram_identities', ['chat_id'], unique=True)
|
||||
op.create_index('ix_telegram_identities_user_id', 'telegram_identities', ['user_id'])
|
||||
|
||||
# 4. Enhance family_members with RBAC
|
||||
op.add_column('family_members', sa.Column('role',
|
||||
postgresql.ENUM('owner', 'adult', 'member', 'child', 'read_only', name='member_role', create_type=False),
|
||||
nullable=False, server_default='member'))
|
||||
op.add_column('family_members', sa.Column('permissions',
|
||||
postgresql.JSON(), nullable=False, server_default='{}'))
|
||||
op.add_column('family_members', sa.Column('status',
|
||||
sa.String(length=50), nullable=False, server_default='active'))
|
||||
|
||||
# 5. Enhance transactions with status & approval workflow
|
||||
op.add_column('transactions', sa.Column('status',
|
||||
postgresql.ENUM('draft', 'pending_approval', 'executed', 'reversed',
|
||||
name='transaction_status', create_type=False),
|
||||
nullable=False, server_default='executed'))
|
||||
op.add_column('transactions', sa.Column('confirmation_required',
|
||||
sa.Boolean(), nullable=False, server_default='false'))
|
||||
op.add_column('transactions', sa.Column('confirmation_token',
|
||||
sa.String(length=255), nullable=True))
|
||||
op.add_column('transactions', sa.Column('approved_by_id',
|
||||
sa.Integer(), nullable=True))
|
||||
op.add_column('transactions', sa.Column('approved_at',
|
||||
sa.DateTime(), nullable=True))
|
||||
op.add_column('transactions', sa.Column('reversed_at',
|
||||
sa.DateTime(), nullable=True))
|
||||
op.add_column('transactions', sa.Column('reversal_reason',
|
||||
sa.String(length=500), nullable=True))
|
||||
op.add_column('transactions', sa.Column('original_transaction_id',
|
||||
sa.Integer(), nullable=True))
|
||||
op.add_column('transactions', sa.Column('executed_at',
|
||||
sa.DateTime(), nullable=True))
|
||||
|
||||
op.create_foreign_key(
|
||||
'fk_transactions_approved_by',
|
||||
'transactions', 'users',
|
||||
['approved_by_id'], ['id']
|
||||
)
|
||||
op.create_foreign_key(
|
||||
'fk_transactions_original',
|
||||
'transactions', 'transactions',
|
||||
['original_transaction_id'], ['id']
|
||||
)
|
||||
|
||||
# 6. Create event_log table
|
||||
op.create_table(
|
||||
'event_log',
|
||||
sa.Column('id', sa.BigInteger(), nullable=False, primary_key=True),
|
||||
sa.Column('family_id', sa.Integer(), nullable=False),
|
||||
sa.Column('entity_type', sa.String(length=50), nullable=False),
|
||||
sa.Column('entity_id', sa.Integer(), nullable=True),
|
||||
sa.Column('action', postgresql.ENUM(*['create', 'update', 'delete', 'confirm', 'execute', 'reverse'],
|
||||
name='event_action', create_type=False),
|
||||
nullable=False),
|
||||
sa.Column('actor_id', sa.Integer(), nullable=True),
|
||||
sa.Column('old_values', postgresql.JSON(), nullable=True),
|
||||
sa.Column('new_values', postgresql.JSON(), nullable=True),
|
||||
sa.Column('ip_address', sa.String(length=45), nullable=True),
|
||||
sa.Column('user_agent', sa.String(length=500), nullable=True),
|
||||
sa.Column('reason', sa.String(length=500), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['family_id'], ['families.id']),
|
||||
sa.ForeignKeyConstraint(['actor_id'], ['users.id']),
|
||||
)
|
||||
op.create_index('ix_event_log_family_id', 'event_log', ['family_id'])
|
||||
op.create_index('ix_event_log_entity', 'event_log', ['entity_type', 'entity_id'])
|
||||
op.create_index('ix_event_log_created_at', 'event_log', ['created_at'])
|
||||
op.create_index('ix_event_log_action', 'event_log', ['action'])
|
||||
|
||||
# 7. Create access_log table
|
||||
op.create_table(
|
||||
'access_log',
|
||||
sa.Column('id', sa.BigInteger(), nullable=False, primary_key=True),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('endpoint', sa.String(length=255), nullable=False),
|
||||
sa.Column('method', sa.String(length=10), nullable=False),
|
||||
sa.Column('status_code', sa.Integer(), nullable=False),
|
||||
sa.Column('response_time_ms', sa.Integer(), nullable=True),
|
||||
sa.Column('ip_address', sa.String(length=45), nullable=False),
|
||||
sa.Column('user_agent', sa.String(length=500), nullable=True),
|
||||
sa.Column('error_message', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id']),
|
||||
)
|
||||
op.create_index('ix_access_log_user_id', 'access_log', ['user_id'])
|
||||
op.create_index('ix_access_log_endpoint', 'access_log', ['endpoint'])
|
||||
op.create_index('ix_access_log_created_at', 'access_log', ['created_at'])
|
||||
|
||||
# 8. Enhance wallets with balance history
|
||||
op.add_column('accounts', sa.Column('balance_snapshot',
|
||||
sa.Numeric(precision=19, scale=2), nullable=False, server_default='0'))
|
||||
op.add_column('accounts', sa.Column('snapshot_at',
|
||||
sa.DateTime(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table('access_log')
|
||||
op.drop_table('event_log')
|
||||
op.drop_table('telegram_identities')
|
||||
op.drop_table('sessions')
|
||||
|
||||
op.drop_constraint('fk_transactions_original', 'transactions', type_='foreignkey')
|
||||
op.drop_constraint('fk_transactions_approved_by', 'transactions', type_='foreignkey')
|
||||
|
||||
op.drop_column('transactions', 'executed_at')
|
||||
op.drop_column('transactions', 'original_transaction_id')
|
||||
op.drop_column('transactions', 'reversal_reason')
|
||||
op.drop_column('transactions', 'reversed_at')
|
||||
op.drop_column('transactions', 'approved_at')
|
||||
op.drop_column('transactions', 'approved_by_id')
|
||||
op.drop_column('transactions', 'confirmation_token')
|
||||
op.drop_column('transactions', 'confirmation_required')
|
||||
op.drop_column('transactions', 'status')
|
||||
|
||||
op.drop_column('family_members', 'status')
|
||||
op.drop_column('family_members', 'permissions')
|
||||
op.drop_column('family_members', 'role')
|
||||
|
||||
op.drop_column('accounts', 'snapshot_at')
|
||||
op.drop_column('accounts', 'balance_snapshot')
|
||||
|
||||
op.drop_column('users', 'password_hash')
|
||||
op.drop_column('users', 'last_login_at')
|
||||
Reference in New Issue
Block a user