"""add loyalty module tables Revision ID: 0fb5d6d6ff97 Revises: zd3n4o5p6q7r8 Create Date: 2026-01-28 22:55:34.074321 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import postgresql from sqlalchemy.dialects import sqlite # revision identifiers, used by Alembic. revision: str = '0fb5d6d6ff97' down_revision: Union[str, None] = 'zd3n4o5p6q7r8' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table('loyalty_programs', sa.Column('id', sa.Integer(), nullable=False), sa.Column('vendor_id', sa.Integer(), nullable=False), sa.Column('loyalty_type', sa.String(length=20), nullable=False), sa.Column('stamps_target', sa.Integer(), nullable=False, comment='Number of stamps needed for reward'), sa.Column('stamps_reward_description', sa.String(length=255), nullable=False, comment='Description of stamp reward'), sa.Column('stamps_reward_value_cents', sa.Integer(), nullable=True, comment='Value of stamp reward in cents (for analytics)'), sa.Column('points_per_euro', sa.Integer(), nullable=False, comment='Points earned per euro spent'), sa.Column('points_rewards', sqlite.JSON(), nullable=False, comment='List of point rewards: [{id, name, points_required, description}]'), sa.Column('cooldown_minutes', sa.Integer(), nullable=False, comment='Minutes between stamps for same card'), sa.Column('max_daily_stamps', sa.Integer(), nullable=False, comment='Maximum stamps per card per day'), sa.Column('require_staff_pin', sa.Boolean(), nullable=False, comment='Require staff PIN for stamp/points operations'), sa.Column('card_name', sa.String(length=100), nullable=True, comment='Display name for loyalty card'), sa.Column('card_color', sa.String(length=7), nullable=False, comment='Primary color for card (hex)'), sa.Column('card_secondary_color', sa.String(length=7), nullable=True, comment='Secondary color for card (hex)'), sa.Column('logo_url', sa.String(length=500), nullable=True, comment='URL to vendor logo for card'), sa.Column('hero_image_url', sa.String(length=500), nullable=True, comment='URL to hero image for card'), sa.Column('google_issuer_id', sa.String(length=100), nullable=True, comment='Google Wallet Issuer ID'), sa.Column('google_class_id', sa.String(length=200), nullable=True, comment='Google Wallet Loyalty Class ID'), sa.Column('apple_pass_type_id', sa.String(length=100), nullable=True, comment='Apple Wallet Pass Type ID'), sa.Column('terms_text', sa.Text(), nullable=True, comment='Loyalty program terms and conditions'), sa.Column('privacy_url', sa.String(length=500), nullable=True, comment='URL to privacy policy'), sa.Column('is_active', sa.Boolean(), nullable=False), sa.Column('activated_at', sa.DateTime(timezone=True), nullable=True, comment='When program was first activated'), sa.Column('created_at', sa.DateTime(), nullable=False), sa.Column('updated_at', sa.DateTime(), nullable=False), sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_index('idx_loyalty_program_vendor_active', 'loyalty_programs', ['vendor_id', 'is_active'], unique=False) op.create_index(op.f('ix_loyalty_programs_id'), 'loyalty_programs', ['id'], unique=False) op.create_index(op.f('ix_loyalty_programs_is_active'), 'loyalty_programs', ['is_active'], unique=False) op.create_index(op.f('ix_loyalty_programs_vendor_id'), 'loyalty_programs', ['vendor_id'], unique=True) op.create_table('loyalty_cards', sa.Column('id', sa.Integer(), nullable=False), sa.Column('customer_id', sa.Integer(), nullable=False), sa.Column('program_id', sa.Integer(), nullable=False), sa.Column('vendor_id', sa.Integer(), nullable=False, comment='Denormalized for query performance'), sa.Column('card_number', sa.String(length=20), nullable=False, comment='Human-readable card number'), sa.Column('qr_code_data', sa.String(length=50), nullable=False, comment='Data encoded in QR code for scanning'), sa.Column('stamp_count', sa.Integer(), nullable=False, comment='Current stamps toward next reward'), sa.Column('total_stamps_earned', sa.Integer(), nullable=False, comment='Lifetime stamps earned'), sa.Column('stamps_redeemed', sa.Integer(), nullable=False, comment='Total rewards redeemed (stamps reset on redemption)'), sa.Column('points_balance', sa.Integer(), nullable=False, comment='Current available points'), sa.Column('total_points_earned', sa.Integer(), nullable=False, comment='Lifetime points earned'), sa.Column('points_redeemed', sa.Integer(), nullable=False, comment='Lifetime points redeemed'), sa.Column('google_object_id', sa.String(length=200), nullable=True, comment='Google Wallet Loyalty Object ID'), sa.Column('google_object_jwt', sa.String(length=2000), nullable=True, comment="JWT for Google Wallet 'Add to Wallet' button"), sa.Column('apple_serial_number', sa.String(length=100), nullable=True, comment='Apple Wallet pass serial number'), sa.Column('apple_auth_token', sa.String(length=100), nullable=True, comment='Apple Wallet authentication token for updates'), sa.Column('last_stamp_at', sa.DateTime(timezone=True), nullable=True, comment='Last stamp added (for cooldown)'), sa.Column('last_points_at', sa.DateTime(timezone=True), nullable=True, comment='Last points earned'), sa.Column('last_redemption_at', sa.DateTime(timezone=True), nullable=True, comment='Last reward redemption'), sa.Column('is_active', sa.Boolean(), nullable=False), sa.Column('created_at', sa.DateTime(), nullable=False), sa.Column('updated_at', sa.DateTime(), nullable=False), sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['program_id'], ['loyalty_programs.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_index('idx_loyalty_card_customer_program', 'loyalty_cards', ['customer_id', 'program_id'], unique=True) op.create_index('idx_loyalty_card_vendor_active', 'loyalty_cards', ['vendor_id', 'is_active'], unique=False) op.create_index(op.f('ix_loyalty_cards_apple_serial_number'), 'loyalty_cards', ['apple_serial_number'], unique=True) op.create_index(op.f('ix_loyalty_cards_card_number'), 'loyalty_cards', ['card_number'], unique=True) op.create_index(op.f('ix_loyalty_cards_customer_id'), 'loyalty_cards', ['customer_id'], unique=False) op.create_index(op.f('ix_loyalty_cards_google_object_id'), 'loyalty_cards', ['google_object_id'], unique=False) op.create_index(op.f('ix_loyalty_cards_id'), 'loyalty_cards', ['id'], unique=False) op.create_index(op.f('ix_loyalty_cards_is_active'), 'loyalty_cards', ['is_active'], unique=False) op.create_index(op.f('ix_loyalty_cards_program_id'), 'loyalty_cards', ['program_id'], unique=False) op.create_index(op.f('ix_loyalty_cards_qr_code_data'), 'loyalty_cards', ['qr_code_data'], unique=True) op.create_index(op.f('ix_loyalty_cards_vendor_id'), 'loyalty_cards', ['vendor_id'], unique=False) op.create_table('staff_pins', sa.Column('id', sa.Integer(), nullable=False), sa.Column('program_id', sa.Integer(), nullable=False), sa.Column('vendor_id', sa.Integer(), nullable=False, comment='Denormalized for query performance'), sa.Column('name', sa.String(length=100), nullable=False, comment='Staff member name'), sa.Column('staff_id', sa.String(length=50), nullable=True, comment='Optional staff ID/employee number'), sa.Column('pin_hash', sa.String(length=255), nullable=False, comment='bcrypt hash of PIN'), sa.Column('failed_attempts', sa.Integer(), nullable=False, comment='Consecutive failed PIN attempts'), sa.Column('locked_until', sa.DateTime(timezone=True), nullable=True, comment='Lockout expires at this time'), sa.Column('last_used_at', sa.DateTime(timezone=True), nullable=True, comment='Last successful use of PIN'), sa.Column('is_active', sa.Boolean(), nullable=False), sa.Column('created_at', sa.DateTime(), nullable=False), sa.Column('updated_at', sa.DateTime(), nullable=False), sa.ForeignKeyConstraint(['program_id'], ['loyalty_programs.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_index('idx_staff_pin_program_active', 'staff_pins', ['program_id', 'is_active'], unique=False) op.create_index('idx_staff_pin_vendor_active', 'staff_pins', ['vendor_id', 'is_active'], unique=False) op.create_index(op.f('ix_staff_pins_id'), 'staff_pins', ['id'], unique=False) op.create_index(op.f('ix_staff_pins_is_active'), 'staff_pins', ['is_active'], unique=False) op.create_index(op.f('ix_staff_pins_program_id'), 'staff_pins', ['program_id'], unique=False) op.create_index(op.f('ix_staff_pins_staff_id'), 'staff_pins', ['staff_id'], unique=False) op.create_index(op.f('ix_staff_pins_vendor_id'), 'staff_pins', ['vendor_id'], unique=False) op.create_table('apple_device_registrations', sa.Column('id', sa.Integer(), nullable=False), sa.Column('card_id', sa.Integer(), nullable=False), sa.Column('device_library_identifier', sa.String(length=100), nullable=False, comment='Unique identifier for the device/library'), sa.Column('push_token', sa.String(length=100), nullable=False, comment='APNs push token for this device'), sa.Column('created_at', sa.DateTime(), nullable=False), sa.Column('updated_at', sa.DateTime(), nullable=False), sa.ForeignKeyConstraint(['card_id'], ['loyalty_cards.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_index('idx_apple_device_card', 'apple_device_registrations', ['device_library_identifier', 'card_id'], unique=True) op.create_index(op.f('ix_apple_device_registrations_card_id'), 'apple_device_registrations', ['card_id'], unique=False) op.create_index(op.f('ix_apple_device_registrations_device_library_identifier'), 'apple_device_registrations', ['device_library_identifier'], unique=False) op.create_index(op.f('ix_apple_device_registrations_id'), 'apple_device_registrations', ['id'], unique=False) op.create_table('loyalty_transactions', sa.Column('id', sa.Integer(), nullable=False), sa.Column('card_id', sa.Integer(), nullable=False), sa.Column('vendor_id', sa.Integer(), nullable=False, comment='Denormalized for query performance'), sa.Column('staff_pin_id', sa.Integer(), nullable=True, comment='Staff PIN used for this operation'), sa.Column('transaction_type', sa.String(length=30), nullable=False), sa.Column('stamps_delta', sa.Integer(), nullable=False, comment='Change in stamps (+1 for earn, -N for redeem)'), sa.Column('points_delta', sa.Integer(), nullable=False, comment='Change in points (+N for earn, -N for redeem)'), sa.Column('stamps_balance_after', sa.Integer(), nullable=True, comment='Stamp count after this transaction'), sa.Column('points_balance_after', sa.Integer(), nullable=True, comment='Points balance after this transaction'), sa.Column('purchase_amount_cents', sa.Integer(), nullable=True, comment='Purchase amount in cents (for points calculation)'), sa.Column('order_reference', sa.String(length=100), nullable=True, comment='Reference to order that triggered points'), sa.Column('reward_id', sa.String(length=50), nullable=True, comment='ID of redeemed reward (from program.points_rewards)'), sa.Column('reward_description', sa.String(length=255), nullable=True, comment='Description of redeemed reward'), sa.Column('ip_address', sa.String(length=45), nullable=True, comment='IP address of requester (IPv4 or IPv6)'), sa.Column('user_agent', sa.String(length=500), nullable=True, comment='User agent string'), sa.Column('notes', sa.Text(), nullable=True, comment='Additional notes (e.g., reason for adjustment)'), sa.Column('transaction_at', sa.DateTime(timezone=True), nullable=False, comment='When the transaction occurred (may differ from created_at)'), sa.Column('created_at', sa.DateTime(), nullable=False), sa.Column('updated_at', sa.DateTime(), nullable=False), sa.ForeignKeyConstraint(['card_id'], ['loyalty_cards.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['staff_pin_id'], ['staff_pins.id'], ondelete='SET NULL'), sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_index('idx_loyalty_tx_card_type', 'loyalty_transactions', ['card_id', 'transaction_type'], unique=False) op.create_index('idx_loyalty_tx_type_date', 'loyalty_transactions', ['transaction_type', 'transaction_at'], unique=False) op.create_index('idx_loyalty_tx_vendor_date', 'loyalty_transactions', ['vendor_id', 'transaction_at'], unique=False) op.create_index(op.f('ix_loyalty_transactions_card_id'), 'loyalty_transactions', ['card_id'], unique=False) op.create_index(op.f('ix_loyalty_transactions_id'), 'loyalty_transactions', ['id'], unique=False) op.create_index(op.f('ix_loyalty_transactions_order_reference'), 'loyalty_transactions', ['order_reference'], unique=False) op.create_index(op.f('ix_loyalty_transactions_staff_pin_id'), 'loyalty_transactions', ['staff_pin_id'], unique=False) op.create_index(op.f('ix_loyalty_transactions_transaction_at'), 'loyalty_transactions', ['transaction_at'], unique=False) op.create_index(op.f('ix_loyalty_transactions_transaction_type'), 'loyalty_transactions', ['transaction_type'], unique=False) op.create_index(op.f('ix_loyalty_transactions_vendor_id'), 'loyalty_transactions', ['vendor_id'], unique=False) op.alter_column('admin_menu_configs', 'platform_id', existing_type=sa.INTEGER(), comment='Platform scope - applies to users/vendors of this platform', existing_comment='Platform scope - applies to all platform admins of this platform', existing_nullable=True) op.alter_column('admin_menu_configs', 'user_id', existing_type=sa.INTEGER(), comment='User scope - applies to this specific super admin (admin frontend only)', existing_comment='User scope - applies to this specific super admin', existing_nullable=True) op.alter_column('admin_menu_configs', 'created_at', existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, existing_server_default=sa.text('now()')) op.alter_column('admin_menu_configs', 'updated_at', existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, existing_server_default=sa.text('now()')) op.drop_index('idx_admin_menu_configs_frontend_type', table_name='admin_menu_configs') op.drop_index('idx_admin_menu_configs_menu_item_id', table_name='admin_menu_configs') op.drop_index('idx_admin_menu_configs_platform_id', table_name='admin_menu_configs') op.drop_index('idx_admin_menu_configs_user_id', table_name='admin_menu_configs') op.create_index(op.f('ix_admin_menu_configs_frontend_type'), 'admin_menu_configs', ['frontend_type'], unique=False) op.create_index(op.f('ix_admin_menu_configs_id'), 'admin_menu_configs', ['id'], unique=False) op.create_index(op.f('ix_admin_menu_configs_menu_item_id'), 'admin_menu_configs', ['menu_item_id'], unique=False) op.create_index(op.f('ix_admin_menu_configs_platform_id'), 'admin_menu_configs', ['platform_id'], unique=False) op.create_index(op.f('ix_admin_menu_configs_user_id'), 'admin_menu_configs', ['user_id'], unique=False) op.alter_column('admin_platforms', 'created_at', existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, existing_server_default=sa.text('now()')) op.alter_column('admin_platforms', 'updated_at', existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, existing_server_default=sa.text('now()')) op.drop_index('idx_admin_platforms_platform_id', table_name='admin_platforms') op.drop_index('idx_admin_platforms_user_id', table_name='admin_platforms') op.create_index(op.f('ix_admin_platforms_id'), 'admin_platforms', ['id'], unique=False) op.create_index(op.f('ix_admin_platforms_platform_id'), 'admin_platforms', ['platform_id'], unique=False) op.create_index(op.f('ix_admin_platforms_user_id'), 'admin_platforms', ['user_id'], unique=False) op.alter_column('content_pages', 'platform_id', existing_type=sa.INTEGER(), comment='Platform this page belongs to', existing_nullable=False) op.alter_column('content_pages', 'vendor_id', existing_type=sa.INTEGER(), comment='Vendor this page belongs to (NULL for platform/default pages)', existing_nullable=True) op.alter_column('content_pages', 'is_platform_page', existing_type=sa.BOOLEAN(), comment='True = platform marketing page (homepage, pricing); False = vendor default or override', existing_nullable=False, existing_server_default=sa.text('false')) op.alter_column('platform_modules', 'created_at', existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, existing_server_default=sa.text('now()')) op.alter_column('platform_modules', 'updated_at', existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, existing_server_default=sa.text('now()')) op.create_index(op.f('ix_platform_modules_id'), 'platform_modules', ['id'], unique=False) op.alter_column('platforms', 'code', existing_type=sa.VARCHAR(length=50), comment="Unique platform identifier (e.g., 'oms', 'loyalty', 'sites')", existing_nullable=False) op.alter_column('platforms', 'name', existing_type=sa.VARCHAR(length=100), comment="Display name (e.g., 'Wizamart OMS')", existing_nullable=False) op.alter_column('platforms', 'description', existing_type=sa.TEXT(), comment='Platform description for admin/marketing purposes', existing_nullable=True) op.alter_column('platforms', 'domain', existing_type=sa.VARCHAR(length=255), comment="Production domain (e.g., 'oms.lu', 'loyalty.lu')", existing_nullable=True) op.alter_column('platforms', 'path_prefix', existing_type=sa.VARCHAR(length=50), comment="Development path prefix (e.g., 'oms' for localhost:9999/oms/*)", existing_nullable=True) op.alter_column('platforms', 'logo', existing_type=sa.VARCHAR(length=500), comment='Logo URL for light mode', existing_nullable=True) op.alter_column('platforms', 'logo_dark', existing_type=sa.VARCHAR(length=500), comment='Logo URL for dark mode', existing_nullable=True) op.alter_column('platforms', 'favicon', existing_type=sa.VARCHAR(length=500), comment='Favicon URL', existing_nullable=True) op.alter_column('platforms', 'theme_config', existing_type=postgresql.JSON(astext_type=sa.Text()), comment='Theme configuration (colors, fonts, etc.)', existing_nullable=True) op.alter_column('platforms', 'default_language', existing_type=sa.VARCHAR(length=5), comment="Default language code (e.g., 'fr', 'en', 'de')", existing_nullable=False, existing_server_default=sa.text("'fr'::character varying")) op.alter_column('platforms', 'supported_languages', existing_type=postgresql.JSON(astext_type=sa.Text()), comment='List of supported language codes', existing_nullable=False) op.alter_column('platforms', 'is_active', existing_type=sa.BOOLEAN(), comment='Whether the platform is active and accessible', existing_nullable=False, existing_server_default=sa.text('true')) op.alter_column('platforms', 'is_public', existing_type=sa.BOOLEAN(), comment='Whether the platform is visible in public listings', existing_nullable=False, existing_server_default=sa.text('true')) op.alter_column('platforms', 'settings', existing_type=postgresql.JSON(astext_type=sa.Text()), comment='Platform-specific settings and feature flags', existing_nullable=True) op.alter_column('platforms', 'created_at', existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, existing_server_default=sa.text('now()')) op.alter_column('platforms', 'updated_at', existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, existing_server_default=sa.text('now()')) op.create_index(op.f('ix_platforms_id'), 'platforms', ['id'], unique=False) op.alter_column('subscription_tiers', 'platform_id', existing_type=sa.INTEGER(), comment='Platform this tier belongs to (NULL = global tier)', existing_nullable=True) op.alter_column('subscription_tiers', 'cms_pages_limit', existing_type=sa.INTEGER(), comment='Total CMS pages limit (NULL = unlimited)', existing_nullable=True) op.alter_column('subscription_tiers', 'cms_custom_pages_limit', existing_type=sa.INTEGER(), comment='Custom pages limit, excluding overrides (NULL = unlimited)', existing_nullable=True) op.drop_index('ix_subscription_tiers_code', table_name='subscription_tiers') op.create_index(op.f('ix_subscription_tiers_code'), 'subscription_tiers', ['code'], unique=False) op.alter_column('users', 'is_super_admin', existing_type=sa.BOOLEAN(), comment=None, existing_comment='Whether this admin has access to all platforms (super admin)', existing_nullable=False, existing_server_default=sa.text('false')) op.alter_column('vendor_platforms', 'vendor_id', existing_type=sa.INTEGER(), comment='Reference to the vendor', existing_nullable=False) op.alter_column('vendor_platforms', 'platform_id', existing_type=sa.INTEGER(), comment='Reference to the platform', existing_nullable=False) op.alter_column('vendor_platforms', 'tier_id', existing_type=sa.INTEGER(), comment='Platform-specific subscription tier', existing_nullable=True) op.alter_column('vendor_platforms', 'is_active', existing_type=sa.BOOLEAN(), comment='Whether the vendor is active on this platform', existing_nullable=False, existing_server_default=sa.text('true')) op.alter_column('vendor_platforms', 'is_primary', existing_type=sa.BOOLEAN(), comment="Whether this is the vendor's primary platform", existing_nullable=False, existing_server_default=sa.text('false')) op.alter_column('vendor_platforms', 'custom_subdomain', existing_type=sa.VARCHAR(length=100), comment='Platform-specific subdomain (if different from main subdomain)', existing_nullable=True) op.alter_column('vendor_platforms', 'settings', existing_type=postgresql.JSON(astext_type=sa.Text()), comment='Platform-specific vendor settings', existing_nullable=True) op.alter_column('vendor_platforms', 'joined_at', existing_type=postgresql.TIMESTAMP(timezone=True), comment='When the vendor joined this platform', existing_nullable=False, existing_server_default=sa.text('now()')) op.alter_column('vendor_platforms', 'created_at', existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, existing_server_default=sa.text('now()')) op.alter_column('vendor_platforms', 'updated_at', existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, existing_server_default=sa.text('now()')) op.create_index(op.f('ix_vendor_platforms_id'), 'vendor_platforms', ['id'], unique=False) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.drop_index(op.f('ix_vendor_platforms_id'), table_name='vendor_platforms') op.alter_column('vendor_platforms', 'updated_at', existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, existing_server_default=sa.text('now()')) op.alter_column('vendor_platforms', 'created_at', existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, existing_server_default=sa.text('now()')) op.alter_column('vendor_platforms', 'joined_at', existing_type=postgresql.TIMESTAMP(timezone=True), comment=None, existing_comment='When the vendor joined this platform', existing_nullable=False, existing_server_default=sa.text('now()')) op.alter_column('vendor_platforms', 'settings', existing_type=postgresql.JSON(astext_type=sa.Text()), comment=None, existing_comment='Platform-specific vendor settings', existing_nullable=True) op.alter_column('vendor_platforms', 'custom_subdomain', existing_type=sa.VARCHAR(length=100), comment=None, existing_comment='Platform-specific subdomain (if different from main subdomain)', existing_nullable=True) op.alter_column('vendor_platforms', 'is_primary', existing_type=sa.BOOLEAN(), comment=None, existing_comment="Whether this is the vendor's primary platform", existing_nullable=False, existing_server_default=sa.text('false')) op.alter_column('vendor_platforms', 'is_active', existing_type=sa.BOOLEAN(), comment=None, existing_comment='Whether the vendor is active on this platform', existing_nullable=False, existing_server_default=sa.text('true')) op.alter_column('vendor_platforms', 'tier_id', existing_type=sa.INTEGER(), comment=None, existing_comment='Platform-specific subscription tier', existing_nullable=True) op.alter_column('vendor_platforms', 'platform_id', existing_type=sa.INTEGER(), comment=None, existing_comment='Reference to the platform', existing_nullable=False) op.alter_column('vendor_platforms', 'vendor_id', existing_type=sa.INTEGER(), comment=None, existing_comment='Reference to the vendor', existing_nullable=False) op.alter_column('users', 'is_super_admin', existing_type=sa.BOOLEAN(), comment='Whether this admin has access to all platforms (super admin)', existing_nullable=False, existing_server_default=sa.text('false')) op.drop_index(op.f('ix_subscription_tiers_code'), table_name='subscription_tiers') op.create_index('ix_subscription_tiers_code', 'subscription_tiers', ['code'], unique=True) op.alter_column('subscription_tiers', 'cms_custom_pages_limit', existing_type=sa.INTEGER(), comment=None, existing_comment='Custom pages limit, excluding overrides (NULL = unlimited)', existing_nullable=True) op.alter_column('subscription_tiers', 'cms_pages_limit', existing_type=sa.INTEGER(), comment=None, existing_comment='Total CMS pages limit (NULL = unlimited)', existing_nullable=True) op.alter_column('subscription_tiers', 'platform_id', existing_type=sa.INTEGER(), comment=None, existing_comment='Platform this tier belongs to (NULL = global tier)', existing_nullable=True) op.drop_index(op.f('ix_platforms_id'), table_name='platforms') op.alter_column('platforms', 'updated_at', existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, existing_server_default=sa.text('now()')) op.alter_column('platforms', 'created_at', existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, existing_server_default=sa.text('now()')) op.alter_column('platforms', 'settings', existing_type=postgresql.JSON(astext_type=sa.Text()), comment=None, existing_comment='Platform-specific settings and feature flags', existing_nullable=True) op.alter_column('platforms', 'is_public', existing_type=sa.BOOLEAN(), comment=None, existing_comment='Whether the platform is visible in public listings', existing_nullable=False, existing_server_default=sa.text('true')) op.alter_column('platforms', 'is_active', existing_type=sa.BOOLEAN(), comment=None, existing_comment='Whether the platform is active and accessible', existing_nullable=False, existing_server_default=sa.text('true')) op.alter_column('platforms', 'supported_languages', existing_type=postgresql.JSON(astext_type=sa.Text()), comment=None, existing_comment='List of supported language codes', existing_nullable=False) op.alter_column('platforms', 'default_language', existing_type=sa.VARCHAR(length=5), comment=None, existing_comment="Default language code (e.g., 'fr', 'en', 'de')", existing_nullable=False, existing_server_default=sa.text("'fr'::character varying")) op.alter_column('platforms', 'theme_config', existing_type=postgresql.JSON(astext_type=sa.Text()), comment=None, existing_comment='Theme configuration (colors, fonts, etc.)', existing_nullable=True) op.alter_column('platforms', 'favicon', existing_type=sa.VARCHAR(length=500), comment=None, existing_comment='Favicon URL', existing_nullable=True) op.alter_column('platforms', 'logo_dark', existing_type=sa.VARCHAR(length=500), comment=None, existing_comment='Logo URL for dark mode', existing_nullable=True) op.alter_column('platforms', 'logo', existing_type=sa.VARCHAR(length=500), comment=None, existing_comment='Logo URL for light mode', existing_nullable=True) op.alter_column('platforms', 'path_prefix', existing_type=sa.VARCHAR(length=50), comment=None, existing_comment="Development path prefix (e.g., 'oms' for localhost:9999/oms/*)", existing_nullable=True) op.alter_column('platforms', 'domain', existing_type=sa.VARCHAR(length=255), comment=None, existing_comment="Production domain (e.g., 'oms.lu', 'loyalty.lu')", existing_nullable=True) op.alter_column('platforms', 'description', existing_type=sa.TEXT(), comment=None, existing_comment='Platform description for admin/marketing purposes', existing_nullable=True) op.alter_column('platforms', 'name', existing_type=sa.VARCHAR(length=100), comment=None, existing_comment="Display name (e.g., 'Wizamart OMS')", existing_nullable=False) op.alter_column('platforms', 'code', existing_type=sa.VARCHAR(length=50), comment=None, existing_comment="Unique platform identifier (e.g., 'oms', 'loyalty', 'sites')", existing_nullable=False) op.drop_index(op.f('ix_platform_modules_id'), table_name='platform_modules') op.alter_column('platform_modules', 'updated_at', existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, existing_server_default=sa.text('now()')) op.alter_column('platform_modules', 'created_at', existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, existing_server_default=sa.text('now()')) op.alter_column('content_pages', 'is_platform_page', existing_type=sa.BOOLEAN(), comment=None, existing_comment='True = platform marketing page (homepage, pricing); False = vendor default or override', existing_nullable=False, existing_server_default=sa.text('false')) op.alter_column('content_pages', 'vendor_id', existing_type=sa.INTEGER(), comment=None, existing_comment='Vendor this page belongs to (NULL for platform/default pages)', existing_nullable=True) op.alter_column('content_pages', 'platform_id', existing_type=sa.INTEGER(), comment=None, existing_comment='Platform this page belongs to', existing_nullable=False) op.drop_index(op.f('ix_admin_platforms_user_id'), table_name='admin_platforms') op.drop_index(op.f('ix_admin_platforms_platform_id'), table_name='admin_platforms') op.drop_index(op.f('ix_admin_platforms_id'), table_name='admin_platforms') op.create_index('idx_admin_platforms_user_id', 'admin_platforms', ['user_id'], unique=False) op.create_index('idx_admin_platforms_platform_id', 'admin_platforms', ['platform_id'], unique=False) op.alter_column('admin_platforms', 'updated_at', existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, existing_server_default=sa.text('now()')) op.alter_column('admin_platforms', 'created_at', existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, existing_server_default=sa.text('now()')) op.drop_index(op.f('ix_admin_menu_configs_user_id'), table_name='admin_menu_configs') op.drop_index(op.f('ix_admin_menu_configs_platform_id'), table_name='admin_menu_configs') op.drop_index(op.f('ix_admin_menu_configs_menu_item_id'), table_name='admin_menu_configs') op.drop_index(op.f('ix_admin_menu_configs_id'), table_name='admin_menu_configs') op.drop_index(op.f('ix_admin_menu_configs_frontend_type'), table_name='admin_menu_configs') op.create_index('idx_admin_menu_configs_user_id', 'admin_menu_configs', ['user_id'], unique=False) op.create_index('idx_admin_menu_configs_platform_id', 'admin_menu_configs', ['platform_id'], unique=False) op.create_index('idx_admin_menu_configs_menu_item_id', 'admin_menu_configs', ['menu_item_id'], unique=False) op.create_index('idx_admin_menu_configs_frontend_type', 'admin_menu_configs', ['frontend_type'], unique=False) op.alter_column('admin_menu_configs', 'updated_at', existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, existing_server_default=sa.text('now()')) op.alter_column('admin_menu_configs', 'created_at', existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, existing_server_default=sa.text('now()')) op.alter_column('admin_menu_configs', 'user_id', existing_type=sa.INTEGER(), comment='User scope - applies to this specific super admin', existing_comment='User scope - applies to this specific super admin (admin frontend only)', existing_nullable=True) op.alter_column('admin_menu_configs', 'platform_id', existing_type=sa.INTEGER(), comment='Platform scope - applies to all platform admins of this platform', existing_comment='Platform scope - applies to users/vendors of this platform', existing_nullable=True) op.drop_index(op.f('ix_loyalty_transactions_vendor_id'), table_name='loyalty_transactions') op.drop_index(op.f('ix_loyalty_transactions_transaction_type'), table_name='loyalty_transactions') op.drop_index(op.f('ix_loyalty_transactions_transaction_at'), table_name='loyalty_transactions') op.drop_index(op.f('ix_loyalty_transactions_staff_pin_id'), table_name='loyalty_transactions') op.drop_index(op.f('ix_loyalty_transactions_order_reference'), table_name='loyalty_transactions') op.drop_index(op.f('ix_loyalty_transactions_id'), table_name='loyalty_transactions') op.drop_index(op.f('ix_loyalty_transactions_card_id'), table_name='loyalty_transactions') op.drop_index('idx_loyalty_tx_vendor_date', table_name='loyalty_transactions') op.drop_index('idx_loyalty_tx_type_date', table_name='loyalty_transactions') op.drop_index('idx_loyalty_tx_card_type', table_name='loyalty_transactions') op.drop_table('loyalty_transactions') op.drop_index(op.f('ix_apple_device_registrations_id'), table_name='apple_device_registrations') op.drop_index(op.f('ix_apple_device_registrations_device_library_identifier'), table_name='apple_device_registrations') op.drop_index(op.f('ix_apple_device_registrations_card_id'), table_name='apple_device_registrations') op.drop_index('idx_apple_device_card', table_name='apple_device_registrations') op.drop_table('apple_device_registrations') op.drop_index(op.f('ix_staff_pins_vendor_id'), table_name='staff_pins') op.drop_index(op.f('ix_staff_pins_staff_id'), table_name='staff_pins') op.drop_index(op.f('ix_staff_pins_program_id'), table_name='staff_pins') op.drop_index(op.f('ix_staff_pins_is_active'), table_name='staff_pins') op.drop_index(op.f('ix_staff_pins_id'), table_name='staff_pins') op.drop_index('idx_staff_pin_vendor_active', table_name='staff_pins') op.drop_index('idx_staff_pin_program_active', table_name='staff_pins') op.drop_table('staff_pins') op.drop_index(op.f('ix_loyalty_cards_vendor_id'), table_name='loyalty_cards') op.drop_index(op.f('ix_loyalty_cards_qr_code_data'), table_name='loyalty_cards') op.drop_index(op.f('ix_loyalty_cards_program_id'), table_name='loyalty_cards') op.drop_index(op.f('ix_loyalty_cards_is_active'), table_name='loyalty_cards') op.drop_index(op.f('ix_loyalty_cards_id'), table_name='loyalty_cards') op.drop_index(op.f('ix_loyalty_cards_google_object_id'), table_name='loyalty_cards') op.drop_index(op.f('ix_loyalty_cards_customer_id'), table_name='loyalty_cards') op.drop_index(op.f('ix_loyalty_cards_card_number'), table_name='loyalty_cards') op.drop_index(op.f('ix_loyalty_cards_apple_serial_number'), table_name='loyalty_cards') op.drop_index('idx_loyalty_card_vendor_active', table_name='loyalty_cards') op.drop_index('idx_loyalty_card_customer_program', table_name='loyalty_cards') op.drop_table('loyalty_cards') op.drop_index(op.f('ix_loyalty_programs_vendor_id'), table_name='loyalty_programs') op.drop_index(op.f('ix_loyalty_programs_is_active'), table_name='loyalty_programs') op.drop_index(op.f('ix_loyalty_programs_id'), table_name='loyalty_programs') op.drop_index('idx_loyalty_program_vendor_active', table_name='loyalty_programs') op.drop_table('loyalty_programs') # ### end Alembic commands ###