"""initial schema Revision ID: 2bc4ab55dbe9 Revises: Create Date: 2026-09-09 13:07:31.239925+02:00 """ from collections.abc import Sequence import sqlalchemy as sa from alembic import op from sqlalchemy.dialects import postgresql revision: str = '2bc4ab55dbe9' down_revision: str | None = None branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None # Alle nativen PostgreSQL-Enum-Typen dieses Schemas. ENUM_TYPES = ( "account_type", "business_day_shift", "entry_kind", "logo_source", "logo_status", "notification_channel", "notification_status", "notification_type", "occurrence_status", ) def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table('account', sa.Column('id', sa.Integer(), nullable=False), sa.Column('name', sa.String(length=120), nullable=False), sa.Column('type', sa.Enum('checking', 'credit_card', 'savings', 'cash', name='account_type'), nullable=False), sa.Column('iban_last4', sa.String(length=4), nullable=True), sa.Column('opening_balance', sa.Numeric(precision=12, scale=2), nullable=False), sa.Column('opening_balance_date', sa.Date(), nullable=False), sa.Column('color', sa.String(length=9), nullable=False), sa.Column('icon', sa.String(length=64), nullable=False), sa.Column('is_active', sa.Boolean(), nullable=False), sa.Column('sort_order', sa.Integer(), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), sa.PrimaryKeyConstraint('id', name=op.f('pk_account')), sa.UniqueConstraint('name', name='uq_account_name') ) op.create_index('ix_account_is_active', 'account', ['is_active'], unique=False) op.create_table('app_user', sa.Column('id', sa.Integer(), nullable=False), sa.Column('username', sa.String(length=120), nullable=False), sa.Column('email', sa.String(length=255), nullable=True), sa.Column('password_hash', sa.Text(), nullable=True), sa.Column('must_change_password', sa.Boolean(), nullable=False), sa.Column('external_subject', sa.String(length=255), nullable=True), sa.Column('last_login_at', sa.DateTime(timezone=True), nullable=True), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), sa.CheckConstraint('password_hash IS NOT NULL OR external_subject IS NOT NULL', name=op.f('ck_app_user_local_or_external_login')), sa.PrimaryKeyConstraint('id', name=op.f('pk_app_user')), sa.UniqueConstraint('external_subject', name=op.f('uq_app_user_external_subject')), sa.UniqueConstraint('username', name=op.f('uq_app_user_username')) ) op.create_table('category', sa.Column('id', sa.Integer(), nullable=False), sa.Column('parent_id', sa.Integer(), nullable=True), sa.Column('name', sa.String(length=120), nullable=False), sa.Column('kind', sa.Enum('expense', 'income', name='entry_kind'), nullable=False), sa.Column('color', sa.String(length=9), nullable=False), sa.Column('icon', sa.String(length=64), nullable=False), sa.Column('is_fixed_cost', sa.Boolean(), nullable=False), sa.Column('sort_order', sa.Integer(), nullable=False), sa.Column('is_archived', sa.Boolean(), nullable=False), sa.ForeignKeyConstraint(['parent_id'], ['category.id'], name=op.f('fk_category_parent_id_category'), ondelete='RESTRICT'), sa.PrimaryKeyConstraint('id', name=op.f('pk_category')), sa.UniqueConstraint('parent_id', 'name', name='uq_category_parent_id') ) op.create_index('ix_category_kind', 'category', ['kind'], unique=False) op.create_table('logo_asset', sa.Column('id', sa.Integer(), nullable=False), sa.Column('sha256', sa.String(length=64), nullable=False), sa.Column('mime', sa.String(length=64), nullable=False), sa.Column('width', sa.Integer(), nullable=True), sa.Column('height', sa.Integer(), nullable=True), sa.Column('file_path', sa.String(length=255), nullable=False), sa.Column('source_url', sa.Text(), nullable=True), sa.Column('fetched_at', sa.DateTime(timezone=True), nullable=False), sa.PrimaryKeyConstraint('id', name=op.f('pk_logo_asset')), sa.UniqueConstraint('sha256', name=op.f('uq_logo_asset_sha256')) ) op.create_table('notification_rule', sa.Column('id', sa.Integer(), nullable=False), sa.Column('type', sa.Enum('due_soon', 'notice_deadline', 'budget_exceeded', 'contract_renewal', name='notification_type'), nullable=False), sa.Column('lead_days', sa.Integer(), nullable=False), sa.Column('channel', sa.Enum('smtp', 'apprise', name='notification_channel'), nullable=False), sa.Column('target', sa.Text(), nullable=True), sa.Column('is_active', sa.Boolean(), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), sa.PrimaryKeyConstraint('id', name=op.f('pk_notification_rule')) ) op.create_table('budget', sa.Column('id', sa.Integer(), nullable=False), sa.Column('category_id', sa.Integer(), nullable=False), sa.Column('period_month', sa.Date(), nullable=False), sa.Column('limit_amount', sa.Numeric(precision=12, scale=2), nullable=False), sa.Column('rollover', sa.Boolean(), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), sa.CheckConstraint("date_trunc('month', period_month) = period_month", name=op.f('ck_budget_month_start')), sa.ForeignKeyConstraint(['category_id'], ['category.id'], name=op.f('fk_budget_category_id_category'), ondelete='CASCADE'), sa.PrimaryKeyConstraint('id', name=op.f('pk_budget')), sa.UniqueConstraint('category_id', 'period_month', name='uq_budget_category_id') ) op.create_table('budget_template', sa.Column('id', sa.Integer(), nullable=False), sa.Column('category_id', sa.Integer(), nullable=False), sa.Column('valid_from', sa.Date(), nullable=False), sa.Column('valid_until', sa.Date(), nullable=True), sa.Column('limit_amount', sa.Numeric(precision=12, scale=2), nullable=False), sa.Column('rollover', sa.Boolean(), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), sa.CheckConstraint("date_trunc('month', valid_from) = valid_from", name=op.f('ck_budget_template_month_start')), sa.ForeignKeyConstraint(['category_id'], ['category.id'], name=op.f('fk_budget_template_category_id_category'), ondelete='CASCADE'), sa.PrimaryKeyConstraint('id', name=op.f('pk_budget_template')), sa.UniqueConstraint('category_id', 'valid_from', name='uq_budget_template_category_id') ) op.create_table('merchant', sa.Column('id', sa.Integer(), nullable=False), sa.Column('name', sa.String(length=160), nullable=False), sa.Column('normalized_name', sa.String(length=160), nullable=False), sa.Column('domain', sa.String(length=255), nullable=True), sa.Column('aliases', postgresql.ARRAY(sa.Text()), server_default='{}', nullable=False), sa.Column('logo_asset_id', sa.Integer(), nullable=True), sa.Column('brand_color', sa.String(length=9), nullable=True), sa.Column('brand_color_dark', sa.String(length=9), nullable=True), sa.Column('logo_source', sa.Enum('simple-icons', 'logodev', 'brandfetch', 'favicon', 'upload', 'generated', name='logo_source'), nullable=True), sa.Column('logo_status', sa.Enum('pending', 'resolved', 'failed', 'manual', name='logo_status'), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), sa.ForeignKeyConstraint(['logo_asset_id'], ['logo_asset.id'], name=op.f('fk_merchant_logo_asset_id_logo_asset'), ondelete='SET NULL'), sa.PrimaryKeyConstraint('id', name=op.f('pk_merchant')), sa.UniqueConstraint('normalized_name', name=op.f('uq_merchant_normalized_name')) ) op.create_index('ix_merchant_normalized_name', 'merchant', ['normalized_name'], unique=False) op.create_table('notification_log', sa.Column('id', sa.Integer(), nullable=False), sa.Column('rule_id', sa.Integer(), nullable=False), sa.Column('ref_type', sa.String(length=40), nullable=False), sa.Column('ref_id', sa.String(length=80), nullable=False), sa.Column('dedupe_day', sa.Date(), nullable=False), sa.Column('sent_at', sa.DateTime(timezone=True), nullable=False), sa.Column('status', sa.Enum('sent', 'failed', name='notification_status'), nullable=False), sa.Column('error', sa.Text(), nullable=True), sa.ForeignKeyConstraint(['rule_id'], ['notification_rule.id'], name=op.f('fk_notification_log_rule_id_notification_rule'), ondelete='CASCADE'), sa.PrimaryKeyConstraint('id', name=op.f('pk_notification_log')), sa.UniqueConstraint('rule_id', 'ref_type', 'ref_id', 'dedupe_day', name='uq_notification_log_rule_id') ) op.create_index('ix_notification_log_sent_at', 'notification_log', ['sent_at'], unique=False) op.create_table('savings_goal', sa.Column('id', sa.Integer(), nullable=False), sa.Column('name', sa.String(length=160), nullable=False), sa.Column('target_amount', sa.Numeric(precision=12, scale=2), nullable=False), sa.Column('target_date', sa.Date(), nullable=True), sa.Column('current_amount', sa.Numeric(precision=12, scale=2), nullable=False), sa.Column('account_id', sa.Integer(), nullable=True), sa.Column('monthly_contribution', sa.Numeric(precision=12, scale=2), nullable=True), sa.Column('color', sa.String(length=9), nullable=False), sa.Column('icon', sa.String(length=64), nullable=False), sa.Column('is_archived', sa.Boolean(), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), sa.ForeignKeyConstraint(['account_id'], ['account.id'], name=op.f('fk_savings_goal_account_id_account'), ondelete='SET NULL'), sa.PrimaryKeyConstraint('id', name=op.f('pk_savings_goal')), sa.UniqueConstraint('name', name='uq_savings_goal_name') ) op.create_table('recurrence', sa.Column('id', sa.Integer(), nullable=False), sa.Column('kind', sa.Enum('expense', 'income', name='entry_kind'), nullable=False), sa.Column('title', sa.String(length=160), nullable=False), sa.Column('merchant_id', sa.Integer(), nullable=True), sa.Column('category_id', sa.Integer(), nullable=False), sa.Column('account_id', sa.Integer(), nullable=False), sa.Column('amount', sa.Numeric(precision=12, scale=2), nullable=False), sa.Column('is_variable', sa.Boolean(), nullable=False), sa.Column('currency', sa.String(length=3), nullable=False), sa.Column('rrule', sa.Text(), nullable=False), sa.Column('dtstart', sa.Date(), nullable=False), sa.Column('until', sa.Date(), nullable=True), sa.Column('business_day_shift', sa.Enum('none', 'next', 'previous', name='business_day_shift'), server_default='next', nullable=False), sa.Column('holiday_region', sa.String(length=8), server_default='DE-NW', nullable=False), sa.Column('installments_total', sa.Integer(), nullable=True), sa.Column('principal_amount', sa.Numeric(precision=12, scale=2), nullable=True), sa.Column('contract_start', sa.Date(), nullable=True), sa.Column('contract_min_term_months', sa.Integer(), nullable=True), sa.Column('contract_notice_period_days', sa.Integer(), nullable=True), sa.Column('contract_auto_renew_months', sa.Integer(), nullable=True), sa.Column('contract_cancelled_at', sa.Date(), nullable=True), sa.Column('reserve_enabled', sa.Boolean(), nullable=False), sa.Column('notes', sa.Text(), nullable=True), sa.Column('tags', postgresql.ARRAY(sa.Text()), server_default='{}', nullable=False), sa.Column('is_active', sa.Boolean(), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), sa.CheckConstraint('installments_total IS NULL OR installments_total > 0', name=op.f('ck_recurrence_installments')), sa.ForeignKeyConstraint(['account_id'], ['account.id'], name=op.f('fk_recurrence_account_id_account'), ondelete='RESTRICT'), sa.ForeignKeyConstraint(['category_id'], ['category.id'], name=op.f('fk_recurrence_category_id_category'), ondelete='RESTRICT'), sa.ForeignKeyConstraint(['merchant_id'], ['merchant.id'], name=op.f('fk_recurrence_merchant_id_merchant'), ondelete='SET NULL'), sa.PrimaryKeyConstraint('id', name=op.f('pk_recurrence')) ) op.create_index('ix_recurrence_category_id', 'recurrence', ['category_id'], unique=False) op.create_index('ix_recurrence_kind_active', 'recurrence', ['kind', 'is_active'], unique=False) op.create_index('ix_recurrence_merchant_id', 'recurrence', ['merchant_id'], unique=False) op.create_table('transaction', sa.Column('id', sa.Integer(), nullable=False), sa.Column('kind', sa.Enum('expense', 'income', name='entry_kind'), nullable=False), sa.Column('title', sa.String(length=160), nullable=False), sa.Column('merchant_id', sa.Integer(), nullable=True), sa.Column('category_id', sa.Integer(), nullable=False), sa.Column('account_id', sa.Integer(), nullable=False), sa.Column('amount', sa.Numeric(precision=12, scale=2), nullable=False), sa.Column('booking_date', sa.Date(), nullable=False), sa.Column('note', sa.Text(), nullable=True), sa.Column('tags', postgresql.ARRAY(sa.Text()), server_default='{}', nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), sa.ForeignKeyConstraint(['account_id'], ['account.id'], name=op.f('fk_transaction_account_id_account'), ondelete='RESTRICT'), sa.ForeignKeyConstraint(['category_id'], ['category.id'], name=op.f('fk_transaction_category_id_category'), ondelete='RESTRICT'), sa.ForeignKeyConstraint(['merchant_id'], ['merchant.id'], name=op.f('fk_transaction_merchant_id_merchant'), ondelete='SET NULL'), sa.PrimaryKeyConstraint('id', name=op.f('pk_transaction')) ) op.create_index('ix_transaction_booking_date', 'transaction', ['booking_date'], unique=False) op.create_index('ix_transaction_category_id', 'transaction', ['category_id'], unique=False) op.create_table('amount_version', sa.Column('id', sa.Integer(), nullable=False), sa.Column('recurrence_id', sa.Integer(), nullable=False), sa.Column('amount', sa.Numeric(precision=12, scale=2), nullable=False), sa.Column('valid_from', sa.Date(), nullable=False), sa.Column('note', sa.Text(), nullable=True), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), sa.ForeignKeyConstraint(['recurrence_id'], ['recurrence.id'], name=op.f('fk_amount_version_recurrence_id_recurrence'), ondelete='CASCADE'), sa.PrimaryKeyConstraint('id', name=op.f('pk_amount_version')), sa.UniqueConstraint('recurrence_id', 'valid_from', name='uq_amount_version_recurrence_id') ) op.create_table('occurrence', sa.Column('id', sa.Integer(), nullable=False), sa.Column('recurrence_id', sa.Integer(), nullable=False), sa.Column('occurrence_date', sa.Date(), nullable=False), sa.Column('status', sa.Enum('planned', 'confirmed', 'skipped', name='occurrence_status'), nullable=False), sa.Column('planned_amount', sa.Numeric(precision=12, scale=2), nullable=False), sa.Column('actual_amount', sa.Numeric(precision=12, scale=2), nullable=True), sa.Column('actual_date', sa.Date(), nullable=True), sa.Column('account_id', sa.Integer(), nullable=True), sa.Column('note', sa.Text(), nullable=True), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), sa.ForeignKeyConstraint(['account_id'], ['account.id'], name=op.f('fk_occurrence_account_id_account'), ondelete='RESTRICT'), sa.ForeignKeyConstraint(['recurrence_id'], ['recurrence.id'], name=op.f('fk_occurrence_recurrence_id_recurrence'), ondelete='CASCADE'), sa.PrimaryKeyConstraint('id', name=op.f('pk_occurrence')), sa.UniqueConstraint('recurrence_id', 'occurrence_date', name='uq_occurrence_recurrence_id') ) op.create_index('ix_occurrence_date', 'occurrence', ['occurrence_date'], unique=False) op.create_table('reserve_ledger', sa.Column('id', sa.Integer(), nullable=False), sa.Column('recurrence_id', sa.Integer(), nullable=False), sa.Column('period_month', sa.Date(), nullable=False), sa.Column('amount', sa.Numeric(precision=12, scale=2), nullable=False), sa.Column('is_released', sa.Boolean(), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), sa.CheckConstraint("date_trunc('month', period_month) = period_month", name=op.f('ck_reserve_ledger_month_start')), sa.ForeignKeyConstraint(['recurrence_id'], ['recurrence.id'], name=op.f('fk_reserve_ledger_recurrence_id_recurrence'), ondelete='CASCADE'), sa.PrimaryKeyConstraint('id', name=op.f('pk_reserve_ledger')), sa.UniqueConstraint('recurrence_id', 'period_month', name='uq_reserve_ledger_recurrence_id') ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.drop_table('reserve_ledger') op.drop_index('ix_occurrence_date', table_name='occurrence') op.drop_table('occurrence') op.drop_table('amount_version') op.drop_index('ix_transaction_category_id', table_name='transaction') op.drop_index('ix_transaction_booking_date', table_name='transaction') op.drop_table('transaction') op.drop_index('ix_recurrence_merchant_id', table_name='recurrence') op.drop_index('ix_recurrence_kind_active', table_name='recurrence') op.drop_index('ix_recurrence_category_id', table_name='recurrence') op.drop_table('recurrence') op.drop_table('savings_goal') op.drop_index('ix_notification_log_sent_at', table_name='notification_log') op.drop_table('notification_log') op.drop_index('ix_merchant_normalized_name', table_name='merchant') op.drop_table('merchant') op.drop_table('budget_template') op.drop_table('budget') op.drop_table('notification_rule') op.drop_table('logo_asset') op.drop_index('ix_category_kind', table_name='category') op.drop_table('category') op.drop_table('app_user') op.drop_index('ix_account_is_active', table_name='account') op.drop_table('account') # ### end Alembic commands ### # Alembic entfernt die nativen Enum-Typen nicht automatisch – ohne diesen Schritt # scheitert ein erneutes `upgrade head` an bereits vorhandenen Typen. for enum_name in ENUM_TYPES: sa.Enum(name=enum_name).drop(op.get_bind(), checkfirst=True)