New enterprise feature
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
"""Comprehensive initial schema with all tenant service tables and columns, including coupon tenant_id nullable change
|
||||
"""Comprehensive unified initial schema with all tenant service tables and columns
|
||||
|
||||
Revision ID: 001_unified_initial_schema
|
||||
Revises:
|
||||
Create Date: 2025-11-06 14:00:00.000000+00:00
|
||||
Revises:
|
||||
Create Date: 2025-11-27 12:00:00.000000+00:00
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
@@ -56,7 +56,7 @@ def upgrade() -> None:
|
||||
op.create_index(op.f('ix_audit_logs_tenant_id'), 'audit_logs', ['tenant_id'], unique=False)
|
||||
op.create_index(op.f('ix_audit_logs_user_id'), 'audit_logs', ['user_id'], unique=False)
|
||||
|
||||
# Create tenants table
|
||||
# Create tenants table with all columns including hierarchy fields
|
||||
op.create_table('tenants',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('name', sa.String(length=200), nullable=False),
|
||||
@@ -82,7 +82,11 @@ def upgrade() -> None:
|
||||
sa.Column('metadata_', sa.JSON(), nullable=True),
|
||||
sa.Column('owner_id', sa.UUID(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, default=sa.text('CURRENT_TIMESTAMP'), onupdate=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP'), onupdate=sa.text('CURRENT_TIMESTAMP')),
|
||||
# Enterprise tier hierarchy fields
|
||||
sa.Column('parent_tenant_id', sa.UUID(), nullable=True),
|
||||
sa.Column('tenant_type', sa.String(length=50), nullable=False, server_default='standalone'),
|
||||
sa.Column('hierarchy_path', sa.String(length=500), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('subdomain')
|
||||
)
|
||||
@@ -91,6 +95,25 @@ def upgrade() -> None:
|
||||
op.create_index(op.f('ix_tenants_is_demo'), 'tenants', ['is_demo'], unique=False)
|
||||
op.create_index(op.f('ix_tenants_is_demo_template'), 'tenants', ['is_demo_template'], unique=False)
|
||||
op.create_index(op.f('ix_tenants_owner_id'), 'tenants', ['owner_id'], unique=False)
|
||||
# Hierarchy indexes
|
||||
op.create_index('ix_tenants_parent_tenant_id', 'tenants', ['parent_tenant_id'])
|
||||
op.create_index('ix_tenants_tenant_type', 'tenants', ['tenant_type'])
|
||||
op.create_index('ix_tenants_hierarchy_path', 'tenants', ['hierarchy_path'])
|
||||
# Add foreign key constraint for hierarchy
|
||||
op.create_foreign_key(
|
||||
'fk_tenants_parent_tenant',
|
||||
'tenants',
|
||||
'tenants',
|
||||
['parent_tenant_id'],
|
||||
['id'],
|
||||
ondelete='RESTRICT'
|
||||
)
|
||||
# Add check constraint to prevent circular hierarchy
|
||||
op.create_check_constraint(
|
||||
'check_parent_not_self',
|
||||
'tenants',
|
||||
'id != parent_tenant_id'
|
||||
)
|
||||
|
||||
# Create tenant_members table
|
||||
op.create_table('tenant_members',
|
||||
@@ -103,13 +126,13 @@ def upgrade() -> None:
|
||||
sa.Column('invited_by', sa.UUID(), nullable=True),
|
||||
sa.Column('invited_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('joined_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.ForeignKeyConstraint(['tenant_id'], ['tenants.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_tenant_members_user_id'), 'tenant_members', ['user_id'], unique=False)
|
||||
|
||||
# Create tenant_settings table with current model structure
|
||||
# Create tenant_settings table with all settings including notification settings
|
||||
op.create_table('tenant_settings',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('tenant_id', sa.UUID(), nullable=False),
|
||||
@@ -124,8 +147,29 @@ def upgrade() -> None:
|
||||
sa.Column('moq_settings', postgresql.JSON(astext_type=sa.Text()), nullable=False),
|
||||
sa.Column('supplier_selection_settings', postgresql.JSON(astext_type=sa.Text()), nullable=False),
|
||||
sa.Column('ml_insights_settings', postgresql.JSON(astext_type=sa.Text()), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, default=sa.text('CURRENT_TIMESTAMP'), onupdate=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('notification_settings', postgresql.JSON(astext_type=sa.Text()), nullable=False,
|
||||
server_default=sa.text("""'{
|
||||
"whatsapp_enabled": false,
|
||||
"whatsapp_phone_number_id": "",
|
||||
"whatsapp_access_token": "",
|
||||
"whatsapp_business_account_id": "",
|
||||
"whatsapp_api_version": "v18.0",
|
||||
"whatsapp_default_language": "es",
|
||||
"email_enabled": true,
|
||||
"email_from_address": "",
|
||||
"email_from_name": "",
|
||||
"email_reply_to": "",
|
||||
"enable_po_notifications": true,
|
||||
"enable_inventory_alerts": true,
|
||||
"enable_production_alerts": true,
|
||||
"enable_forecast_alerts": true,
|
||||
"po_notification_channels": ["email"],
|
||||
"inventory_alert_channels": ["email"],
|
||||
"production_alert_channels": ["email"],
|
||||
"forecast_alert_channels": ["email"]
|
||||
}'::jsonb""")),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP'), onupdate=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.ForeignKeyConstraint(['tenant_id'], ['tenants.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('tenant_id')
|
||||
@@ -149,8 +193,8 @@ def upgrade() -> None:
|
||||
sa.Column('max_locations', sa.Integer(), nullable=True),
|
||||
sa.Column('max_products', sa.Integer(), nullable=True),
|
||||
sa.Column('features', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, default=sa.text('CURRENT_TIMESTAMP'), onupdate=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP'), onupdate=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.ForeignKeyConstraint(['tenant_id'], ['tenants.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
@@ -167,7 +211,7 @@ def upgrade() -> None:
|
||||
sa.Column('valid_from', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('valid_until', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('active', sa.Boolean(), nullable=False, default=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('extra_data', sa.JSON(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['tenant_id'], ['tenants.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
@@ -183,7 +227,7 @@ def upgrade() -> None:
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('tenant_id', sa.String(length=255), nullable=False),
|
||||
sa.Column('coupon_code', sa.String(length=50), nullable=False),
|
||||
sa.Column('redeemed_at', sa.DateTime(timezone=True), nullable=False, default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('redeemed_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('discount_applied', sa.JSON(), nullable=False),
|
||||
sa.Column('extra_data', sa.JSON(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['coupon_code'], ['coupons.code'], ),
|
||||
@@ -215,8 +259,8 @@ def upgrade() -> None:
|
||||
sa.Column('recurrence_pattern', sa.String(200), nullable=True),
|
||||
sa.Column('actual_impact_multiplier', sa.Float, nullable=True),
|
||||
sa.Column('actual_sales_increase_percent', sa.Float, nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, default=sa.text('CURRENT_TIMESTAMP'), onupdate=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP'), onupdate=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('created_by', sa.String(255), nullable=True),
|
||||
sa.Column('notes', sa.Text, nullable=True),
|
||||
)
|
||||
@@ -234,8 +278,8 @@ def upgrade() -> None:
|
||||
sa.Column('default_affected_categories', sa.String(500), nullable=True),
|
||||
sa.Column('recurrence_pattern', sa.String(200), nullable=False),
|
||||
sa.Column('is_active', sa.Boolean, default=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, default=sa.text('CURRENT_TIMESTAMP'), onupdate=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP'), onupdate=sa.text('CURRENT_TIMESTAMP')),
|
||||
)
|
||||
|
||||
# Create indexes for better query performance on events
|
||||
@@ -243,8 +287,44 @@ def upgrade() -> None:
|
||||
op.create_index('ix_events_type_date', 'events', ['event_type', 'event_date'])
|
||||
op.create_index('ix_event_templates_tenant_active', 'event_templates', ['tenant_id', 'is_active'])
|
||||
|
||||
# Create tenant_locations table (from 004 migration)
|
||||
op.create_table('tenant_locations',
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column('tenant_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column('name', sa.String(length=200), nullable=False),
|
||||
sa.Column('location_type', sa.String(length=50), nullable=False), # central_production, retail_outlet
|
||||
sa.Column('address', sa.Text(), nullable=False),
|
||||
sa.Column('city', sa.String(length=100), nullable=False, server_default='Madrid'),
|
||||
sa.Column('postal_code', sa.String(length=10), nullable=False),
|
||||
sa.Column('latitude', sa.Float(), nullable=True),
|
||||
sa.Column('longitude', sa.Float(), nullable=True),
|
||||
sa.Column('delivery_windows', postgresql.JSON(astext_type=sa.Text()), nullable=True),
|
||||
sa.Column('capacity', sa.Integer(), nullable=True),
|
||||
sa.Column('max_delivery_radius_km', sa.Float(), nullable=True, default=50.0),
|
||||
sa.Column('operational_hours', postgresql.JSON(astext_type=sa.Text()), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, default=True),
|
||||
sa.Column('contact_person', sa.String(length=200), nullable=True),
|
||||
sa.Column('contact_phone', sa.String(length=20), nullable=True),
|
||||
sa.Column('contact_email', sa.String(length=255), nullable=True),
|
||||
sa.Column('delivery_schedule_config', postgresql.JSON(astext_type=sa.Text()), nullable=True),
|
||||
sa.Column('metadata_', postgresql.JSON(astext_type=sa.Text()), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP'), onupdate=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.ForeignKeyConstraint(['tenant_id'], ['tenants.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('ix_tenant_locations_tenant_id', 'tenant_locations', ['tenant_id'])
|
||||
op.create_index('ix_tenant_locations_location_type', 'tenant_locations', ['location_type'])
|
||||
op.create_index('ix_tenant_locations_coordinates', 'tenant_locations', ['latitude', 'longitude'])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Drop tenant_locations table
|
||||
op.drop_index('ix_tenant_locations_coordinates')
|
||||
op.drop_index('ix_tenant_locations_location_type')
|
||||
op.drop_index('ix_tenant_locations_tenant_id')
|
||||
op.drop_table('tenant_locations')
|
||||
|
||||
# Drop indexes for events
|
||||
op.drop_index('ix_event_templates_tenant_active', table_name='event_templates')
|
||||
op.drop_index('ix_events_type_date', table_name='events')
|
||||
@@ -275,6 +355,12 @@ def downgrade() -> None:
|
||||
op.drop_index(op.f('ix_tenant_members_user_id'), table_name='tenant_members')
|
||||
op.drop_table('tenant_members')
|
||||
|
||||
# Drop tenant hierarchy constraints and indexes
|
||||
op.drop_constraint('check_parent_not_self', 'tenants', type_='check')
|
||||
op.drop_constraint('fk_tenants_parent_tenant', 'tenants', type_='foreignkey')
|
||||
op.drop_index('ix_tenants_hierarchy_path', table_name='tenants')
|
||||
op.drop_index('ix_tenants_tenant_type', table_name='tenants')
|
||||
op.drop_index('ix_tenants_parent_tenant_id', table_name='tenants')
|
||||
op.drop_index(op.f('ix_tenants_owner_id'), table_name='tenants')
|
||||
op.drop_index(op.f('ix_tenants_is_demo_template'), table_name='tenants')
|
||||
op.drop_index(op.f('ix_tenants_is_demo'), table_name='tenants')
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
"""Add notification_settings column to tenant_settings table
|
||||
|
||||
Revision ID: 002_add_notification_settings
|
||||
Revises: 001_unified_initial_schema
|
||||
Create Date: 2025-11-13 15:00:00.000000+00:00
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '002_add_notification_settings'
|
||||
down_revision: Union[str, None] = '001_unified_initial_schema'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Add notification_settings column with default values"""
|
||||
|
||||
# Add column with default value as JSONB
|
||||
op.add_column(
|
||||
'tenant_settings',
|
||||
sa.Column(
|
||||
'notification_settings',
|
||||
postgresql.JSON(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default=sa.text("""'{
|
||||
"whatsapp_enabled": false,
|
||||
"whatsapp_phone_number_id": "",
|
||||
"whatsapp_access_token": "",
|
||||
"whatsapp_business_account_id": "",
|
||||
"whatsapp_api_version": "v18.0",
|
||||
"whatsapp_default_language": "es",
|
||||
"email_enabled": true,
|
||||
"email_from_address": "",
|
||||
"email_from_name": "",
|
||||
"email_reply_to": "",
|
||||
"enable_po_notifications": true,
|
||||
"enable_inventory_alerts": true,
|
||||
"enable_production_alerts": true,
|
||||
"enable_forecast_alerts": true,
|
||||
"po_notification_channels": ["email"],
|
||||
"inventory_alert_channels": ["email"],
|
||||
"production_alert_channels": ["email"],
|
||||
"forecast_alert_channels": ["email"]
|
||||
}'::jsonb""")
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Remove notification_settings column"""
|
||||
op.drop_column('tenant_settings', 'notification_settings')
|
||||
Reference in New Issue
Block a user