Our Story
-Wizamart was founded with a simple idea: Luxembourg businesses deserve world-class e-commerce tools that understand their unique needs. Local languages, local payment methods, local compliance - built in from the start.
-diff --git a/Makefile b/Makefile index 48c48306..94ff6f54 100644 --- a/Makefile +++ b/Makefile @@ -162,8 +162,8 @@ db-setup: migrate-up init-prod seed-demo db-reset: @echo "⚠️ WARNING: This will DELETE ALL existing data!" - @echo "Rolling back all migrations..." - $(PYTHON) -m alembic downgrade base || true + @echo "Dropping and recreating public schema..." + $(PYTHON) -c "from app.core.config import settings; from sqlalchemy import create_engine, text; e=create_engine(settings.database_url); c=e.connect(); c.execute(text('DROP SCHEMA public CASCADE')); c.execute(text('CREATE SCHEMA public')); c.commit(); c.close()" @echo "Applying all migrations..." $(PYTHON) -m alembic upgrade head @echo "Initializing production data..." diff --git a/alembic.ini b/alembic.ini index 39759137..a596a09e 100644 --- a/alembic.ini +++ b/alembic.ini @@ -3,7 +3,7 @@ script_location = alembic prepend_sys_path = . version_path_separator = space -version_locations = alembic/versions app/modules/loyalty/migrations/versions +version_locations = alembic/versions app/modules/billing/migrations/versions app/modules/cart/migrations/versions app/modules/catalog/migrations/versions app/modules/cms/migrations/versions app/modules/customers/migrations/versions app/modules/dev_tools/migrations/versions app/modules/inventory/migrations/versions app/modules/loyalty/migrations/versions app/modules/marketplace/migrations/versions app/modules/messaging/migrations/versions app/modules/orders/migrations/versions # This will be overridden by alembic\env.py using settings.database_url sqlalchemy.url = # for PROD: sqlalchemy.url = postgresql://username:password@localhost:5432/ecommerce_db diff --git a/alembic/env.py b/alembic/env.py index 94fc7adf..cc88e911 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -29,170 +29,251 @@ from models.database.base import Base # ============================================================================ # CRITICAL: Every model must be imported here so Alembic can detect tables! # If a model is not imported, Alembic will not create/update its table. -# -# Models list must match: models/database/__init__.py # ============================================================================ print("[ALEMBIC] Importing database models...") print("=" * 70) +_import_errors = [] + # ---------------------------------------------------------------------------- -# ADMIN MODELS +# CORE MODULE (1 model) # ---------------------------------------------------------------------------- try: - from app.modules.tenancy.models import ( + from app.modules.core.models import AdminMenuConfig # noqa: F401 + + print(" ✓ Core models (1)") +except ImportError as e: + _import_errors.append(f"core: {e}") + print(f" ✗ Core models failed: {e}") + +# ---------------------------------------------------------------------------- +# TENANCY MODULE (15 models) +# ---------------------------------------------------------------------------- +try: + from app.modules.tenancy.models import ( # noqa: F401 AdminAuditLog, - AdminNotification, + AdminPlatform, AdminSession, AdminSetting, + ApplicationLog, + Merchant, + Platform, PlatformAlert, + PlatformModule, + Role, + Store, + StoreDomain, + StorePlatform, + StoreUser, + User, ) - print(" ✓ Admin models imported (5 models)") - print(" - AdminAuditLog") - print(" - AdminNotification") - print(" - AdminSetting") - print(" - PlatformAlert") - print(" - AdminSession") + print(" ✓ Tenancy models (15)") except ImportError as e: - print(f" ✗ Admin models failed: {e}") + _import_errors.append(f"tenancy: {e}") + print(f" ✗ Tenancy models failed: {e}") # ---------------------------------------------------------------------------- -# USER MODEL +# BILLING MODULE (9 models) # ---------------------------------------------------------------------------- try: - from app.modules.tenancy.models import User + from app.modules.billing.models import ( # noqa: F401 + AddOnProduct, + BillingHistory, + CapacitySnapshot, + MerchantFeatureOverride, + MerchantSubscription, + StoreAddOn, + StripeWebhookEvent, + SubscriptionTier, + TierFeatureLimit, + ) - print(" ✓ User model imported") + print(" ✓ Billing models (9)") except ImportError as e: - print(f" ✗ User model failed: {e}") + _import_errors.append(f"billing: {e}") + print(f" ✗ Billing models failed: {e}") # ---------------------------------------------------------------------------- -# STORE MODELS +# CATALOG MODULE (3 models) # ---------------------------------------------------------------------------- try: - from app.modules.tenancy.models import Role, Store, StoreUser + from app.modules.catalog.models import ( # noqa: F401 + Product, + ProductMedia, + ProductTranslation, + ) - print(" ✓ Store models imported (3 models)") - print(" - Store") - print(" - StoreUser") - print(" - Role") + print(" ✓ Catalog models (3)") except ImportError as e: - print(f" ✗ Store models failed: {e}") - -try: - from app.modules.tenancy.models import StoreDomain - - print(" ✓ StoreDomain model imported") -except ImportError as e: - print(f" ✗ StoreDomain model failed: {e}") - -try: - from app.modules.cms.models import StoreTheme - - print(" ✓ StoreTheme model imported") -except ImportError as e: - print(f" ✗ StoreTheme model failed: {e}") + _import_errors.append(f"catalog: {e}") + print(f" ✗ Catalog models failed: {e}") # ---------------------------------------------------------------------------- -# CONTENT PAGE MODEL (CMS Module) +# MARKETPLACE MODULE (10 models) # ---------------------------------------------------------------------------- try: - from app.modules.cms.models import ContentPage + from app.modules.marketplace.models import ( # noqa: F401 + LetzshopFulfillmentQueue, + LetzshopHistoricalImportJob, + LetzshopStoreCache, + LetzshopSyncLog, + MarketplaceImportError, + MarketplaceImportJob, + MarketplaceProduct, + MarketplaceProductTranslation, + StoreLetzshopCredentials, + StoreOnboarding, + ) - print(" ✓ ContentPage model imported (from CMS module)") + print(" ✓ Marketplace models (10)") except ImportError as e: - print(f" ✗ ContentPage model failed: {e}") + _import_errors.append(f"marketplace: {e}") + print(f" ✗ Marketplace models failed: {e}") # ---------------------------------------------------------------------------- -# PRODUCT MODELS +# CMS MODULE (3 models) # ---------------------------------------------------------------------------- try: - from app.modules.catalog.models import Product + from app.modules.cms.models import ( # noqa: F401 + ContentPage, + MediaFile, + StoreTheme, + ) - print(" ✓ Product model imported") + print(" ✓ CMS models (3)") except ImportError as e: - print(f" ✗ Product model failed: {e}") - -try: - from app.modules.marketplace.models import MarketplaceProduct - - print(" ✓ MarketplaceProduct model imported") -except ImportError as e: - print(f" ✗ MarketplaceProduct model failed: {e}") + _import_errors.append(f"cms: {e}") + print(f" ✗ CMS models failed: {e}") # ---------------------------------------------------------------------------- -# INVENTORY MODEL +# CUSTOMERS MODULE (3 models) # ---------------------------------------------------------------------------- try: - from app.modules.inventory.models import Inventory + from app.modules.customers.models import ( # noqa: F401 + Customer, + CustomerAddress, + PasswordResetToken, + ) - print(" ✓ Inventory model imported") + print(" ✓ Customers models (3)") except ImportError as e: - print(f" ✗ Inventory model failed: {e}") + _import_errors.append(f"customers: {e}") + print(f" ✗ Customers models failed: {e}") # ---------------------------------------------------------------------------- -# MARKETPLACE IMPORT +# ORDERS MODULE (5 models) # ---------------------------------------------------------------------------- try: - from app.modules.marketplace.models import MarketplaceImportJob + from app.modules.orders.models import ( # noqa: F401 + Invoice, + Order, + OrderItem, + OrderItemException, + StoreInvoiceSettings, + ) - print(" ✓ MarketplaceImportJob model imported") + print(" ✓ Orders models (5)") except ImportError as e: - print(f" ✗ MarketplaceImportJob model failed: {e}") + _import_errors.append(f"orders: {e}") + print(f" ✗ Orders models failed: {e}") # ---------------------------------------------------------------------------- -# CUSTOMER MODELS +# INVENTORY MODULE (2 models) # ---------------------------------------------------------------------------- try: - from app.modules.customers.models.customer import Customer, CustomerAddress + from app.modules.inventory.models import ( # noqa: F401 + Inventory, + InventoryTransaction, + ) - print(" ✓ Customer models imported (2 models)") - print(" - Customer") - print(" - CustomerAddress") + print(" ✓ Inventory models (2)") except ImportError as e: - print(f" ✗ Customer models failed: {e}") + _import_errors.append(f"inventory: {e}") + print(f" ✗ Inventory models failed: {e}") # ---------------------------------------------------------------------------- -# CART MODELS +# CART MODULE (1 model) # ---------------------------------------------------------------------------- try: - from app.modules.cart.models import CartItem + from app.modules.cart.models import CartItem # noqa: F401 - print(" ✓ Cart models imported (1 model)") - print(" - CartItem") + print(" ✓ Cart models (1)") except ImportError as e: + _import_errors.append(f"cart: {e}") print(f" ✗ Cart models failed: {e}") # ---------------------------------------------------------------------------- -# ORDER MODELS +# MESSAGING MODULE (9 models) # ---------------------------------------------------------------------------- try: - from app.modules.orders.models import Order, OrderItem + from app.modules.messaging.models import ( # noqa: F401 + AdminNotification, + Conversation, + ConversationParticipant, + EmailLog, + EmailTemplate, + Message, + MessageAttachment, + StoreEmailSettings, + StoreEmailTemplate, + ) - print(" ✓ Order models imported (2 models)") - print(" - Order") - print(" - OrderItem") + print(" ✓ Messaging models (9)") except ImportError as e: - print(f" ✗ Order models failed: {e}") + _import_errors.append(f"messaging: {e}") + print(f" ✗ Messaging models failed: {e}") + +# ---------------------------------------------------------------------------- +# LOYALTY MODULE (6 models) +# ---------------------------------------------------------------------------- +try: + from app.modules.loyalty.models import ( # noqa: F401 + AppleDeviceRegistration, + LoyaltyCard, + LoyaltyProgram, + LoyaltyTransaction, + MerchantLoyaltySettings, + StaffPin, + ) + + print(" ✓ Loyalty models (6)") +except ImportError as e: + _import_errors.append(f"loyalty: {e}") + print(f" ✗ Loyalty models failed: {e}") + +# ---------------------------------------------------------------------------- +# DEV_TOOLS MODULE (8 models) +# ---------------------------------------------------------------------------- +try: + from app.modules.dev_tools.models import ( # noqa: F401 + ArchitectureRule, + ArchitectureScan, + ArchitectureViolation, + TestCollection, + TestResult, + TestRun, + ViolationAssignment, + ViolationComment, + ) + + print(" ✓ Dev Tools models (8)") +except ImportError as e: + _import_errors.append(f"dev_tools: {e}") + print(f" ✗ Dev Tools models failed: {e}") # ============================================================================ # SUMMARY # ============================================================================ print("=" * 70) -print("[ALEMBIC] Model import completed") -print("[ALEMBIC] Tables detected in metadata:") -print("=" * 70) - -if Base.metadata.tables: - for i, table_name in enumerate(sorted(Base.metadata.tables.keys()), 1): - print(f" {i:2d}. {table_name}") +if _import_errors: + print(f"[ALEMBIC] WARNING: {len(_import_errors)} import error(s):") + for err in _import_errors: + print(f" - {err}") print("=" * 70) - print(f"[ALEMBIC] Total tables: {len(Base.metadata.tables)}") -else: - print(" ⚠️ WARNING: No tables found in metadata!") - print(" This usually means models are not being imported correctly.") +print(f"[ALEMBIC] Total tables in metadata: {len(Base.metadata.tables)}") print("=" * 70) print() diff --git a/alembic/versions/09d84a46530f_add_celery_task_id_to_job_tables.py b/alembic/versions/09d84a46530f_add_celery_task_id_to_job_tables.py deleted file mode 100644 index 4e693903..00000000 --- a/alembic/versions/09d84a46530f_add_celery_task_id_to_job_tables.py +++ /dev/null @@ -1,55 +0,0 @@ -"""add celery_task_id to job tables - -Revision ID: 09d84a46530f -Revises: y3d4e5f6g7h8 -Create Date: 2026-01-11 16:44:59.070110 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - -# revision identifiers, used by Alembic. -revision: str = '09d84a46530f' -down_revision: Union[str, None] = 'y3d4e5f6g7h8' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Add celery_task_id column to job tracking tables for Celery integration.""" - # MarketplaceImportJob - op.add_column('marketplace_import_jobs', sa.Column('celery_task_id', sa.String(length=255), nullable=True)) - op.create_index(op.f('ix_marketplace_import_jobs_celery_task_id'), 'marketplace_import_jobs', ['celery_task_id'], unique=False) - - # LetzshopHistoricalImportJob - op.add_column('letzshop_historical_import_jobs', sa.Column('celery_task_id', sa.String(length=255), nullable=True)) - op.create_index(op.f('ix_letzshop_historical_import_jobs_celery_task_id'), 'letzshop_historical_import_jobs', ['celery_task_id'], unique=False) - - # ArchitectureScan - op.add_column('architecture_scans', sa.Column('celery_task_id', sa.String(length=255), nullable=True)) - op.create_index(op.f('ix_architecture_scans_celery_task_id'), 'architecture_scans', ['celery_task_id'], unique=False) - - # TestRun - op.add_column('test_runs', sa.Column('celery_task_id', sa.String(length=255), nullable=True)) - op.create_index(op.f('ix_test_runs_celery_task_id'), 'test_runs', ['celery_task_id'], unique=False) - - -def downgrade() -> None: - """Remove celery_task_id column from job tracking tables.""" - # TestRun - op.drop_index(op.f('ix_test_runs_celery_task_id'), table_name='test_runs') - op.drop_column('test_runs', 'celery_task_id') - - # ArchitectureScan - op.drop_index(op.f('ix_architecture_scans_celery_task_id'), table_name='architecture_scans') - op.drop_column('architecture_scans', 'celery_task_id') - - # LetzshopHistoricalImportJob - op.drop_index(op.f('ix_letzshop_historical_import_jobs_celery_task_id'), table_name='letzshop_historical_import_jobs') - op.drop_column('letzshop_historical_import_jobs', 'celery_task_id') - - # MarketplaceImportJob - op.drop_index(op.f('ix_marketplace_import_jobs_celery_task_id'), table_name='marketplace_import_jobs') - op.drop_column('marketplace_import_jobs', 'celery_task_id') diff --git a/alembic/versions/0bd9ffaaced1_add_application_logs_table_for_hybrid_.py b/alembic/versions/0bd9ffaaced1_add_application_logs_table_for_hybrid_.py deleted file mode 100644 index 3d091068..00000000 --- a/alembic/versions/0bd9ffaaced1_add_application_logs_table_for_hybrid_.py +++ /dev/null @@ -1,68 +0,0 @@ -"""add application_logs table for hybrid logging - -Revision ID: 0bd9ffaaced1 -Revises: 7a7ce92593d5 -Create Date: 2025-11-29 12:44:55.427245 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '0bd9ffaaced1' -down_revision: Union[str, None] = '7a7ce92593d5' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Create application_logs table - op.create_table( - 'application_logs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('timestamp', sa.DateTime(), nullable=False), - sa.Column('level', sa.String(length=20), nullable=False), - sa.Column('logger_name', sa.String(length=200), nullable=False), - sa.Column('module', sa.String(length=200), nullable=True), - sa.Column('function_name', sa.String(length=100), nullable=True), - sa.Column('line_number', sa.Integer(), nullable=True), - sa.Column('message', sa.Text(), nullable=False), - sa.Column('exception_type', sa.String(length=200), nullable=True), - sa.Column('exception_message', sa.Text(), nullable=True), - sa.Column('stack_trace', sa.Text(), nullable=True), - sa.Column('request_id', sa.String(length=100), nullable=True), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('vendor_id', sa.Integer(), nullable=True), - sa.Column('context', sa.JSON(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=True), - sa.Column('updated_at', sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), - sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ), - sa.PrimaryKeyConstraint('id') - ) - - # Create indexes for better query performance - op.create_index(op.f('ix_application_logs_id'), 'application_logs', ['id'], unique=False) - op.create_index(op.f('ix_application_logs_timestamp'), 'application_logs', ['timestamp'], unique=False) - op.create_index(op.f('ix_application_logs_level'), 'application_logs', ['level'], unique=False) - op.create_index(op.f('ix_application_logs_logger_name'), 'application_logs', ['logger_name'], unique=False) - op.create_index(op.f('ix_application_logs_request_id'), 'application_logs', ['request_id'], unique=False) - op.create_index(op.f('ix_application_logs_user_id'), 'application_logs', ['user_id'], unique=False) - op.create_index(op.f('ix_application_logs_vendor_id'), 'application_logs', ['vendor_id'], unique=False) - - -def downgrade() -> None: - # Drop indexes - op.drop_index(op.f('ix_application_logs_vendor_id'), table_name='application_logs') - op.drop_index(op.f('ix_application_logs_user_id'), table_name='application_logs') - op.drop_index(op.f('ix_application_logs_request_id'), table_name='application_logs') - op.drop_index(op.f('ix_application_logs_logger_name'), table_name='application_logs') - op.drop_index(op.f('ix_application_logs_level'), table_name='application_logs') - op.drop_index(op.f('ix_application_logs_timestamp'), table_name='application_logs') - op.drop_index(op.f('ix_application_logs_id'), table_name='application_logs') - - # Drop table - op.drop_table('application_logs') diff --git a/alembic/versions/1b398cf45e85_add_letzshop_vendor_cache_table.py b/alembic/versions/1b398cf45e85_add_letzshop_vendor_cache_table.py deleted file mode 100644 index b113d400..00000000 --- a/alembic/versions/1b398cf45e85_add_letzshop_vendor_cache_table.py +++ /dev/null @@ -1,367 +0,0 @@ -"""add letzshop_vendor_cache table - -Revision ID: 1b398cf45e85 -Revises: 09d84a46530f -Create Date: 2026-01-13 19:38:45.423378 - -""" -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 = '1b398cf45e85' -down_revision: Union[str, None] = '09d84a46530f' -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('letzshop_vendor_cache', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('letzshop_id', sa.String(length=50), nullable=False), - sa.Column('slug', sa.String(length=200), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('company_name', sa.String(length=255), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('description_en', sa.Text(), nullable=True), - sa.Column('description_fr', sa.Text(), nullable=True), - sa.Column('description_de', sa.Text(), nullable=True), - sa.Column('email', sa.String(length=255), nullable=True), - sa.Column('phone', sa.String(length=50), nullable=True), - sa.Column('fax', sa.String(length=50), nullable=True), - sa.Column('website', sa.String(length=500), nullable=True), - sa.Column('street', sa.String(length=255), nullable=True), - sa.Column('street_number', sa.String(length=50), nullable=True), - sa.Column('city', sa.String(length=100), nullable=True), - sa.Column('zipcode', sa.String(length=20), nullable=True), - sa.Column('country_iso', sa.String(length=5), nullable=True), - sa.Column('latitude', sa.String(length=20), nullable=True), - sa.Column('longitude', sa.String(length=20), nullable=True), - sa.Column('categories', sqlite.JSON(), nullable=True), - sa.Column('background_image_url', sa.String(length=500), nullable=True), - sa.Column('social_media_links', sqlite.JSON(), nullable=True), - sa.Column('opening_hours_en', sa.Text(), nullable=True), - sa.Column('opening_hours_fr', sa.Text(), nullable=True), - sa.Column('opening_hours_de', sa.Text(), nullable=True), - sa.Column('representative_name', sa.String(length=255), nullable=True), - sa.Column('representative_title', sa.String(length=100), nullable=True), - sa.Column('claimed_by_vendor_id', sa.Integer(), nullable=True), - sa.Column('claimed_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('last_synced_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('raw_data', sqlite.JSON(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['claimed_by_vendor_id'], ['vendors.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index('idx_vendor_cache_active', 'letzshop_vendor_cache', ['is_active'], unique=False) - op.create_index('idx_vendor_cache_city', 'letzshop_vendor_cache', ['city'], unique=False) - op.create_index('idx_vendor_cache_claimed', 'letzshop_vendor_cache', ['claimed_by_vendor_id'], unique=False) - op.create_index(op.f('ix_letzshop_vendor_cache_claimed_by_vendor_id'), 'letzshop_vendor_cache', ['claimed_by_vendor_id'], unique=False) - op.create_index(op.f('ix_letzshop_vendor_cache_id'), 'letzshop_vendor_cache', ['id'], unique=False) - op.create_index(op.f('ix_letzshop_vendor_cache_letzshop_id'), 'letzshop_vendor_cache', ['letzshop_id'], unique=True) - op.create_index(op.f('ix_letzshop_vendor_cache_slug'), 'letzshop_vendor_cache', ['slug'], unique=True) - op.drop_constraint('architecture_rules_rule_id_key', 'architecture_rules', type_='unique') - op.alter_column('capacity_snapshots', 'created_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('capacity_snapshots', '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_features_id'), 'features', ['id'], unique=False) - op.create_index(op.f('ix_features_minimum_tier_id'), 'features', ['minimum_tier_id'], unique=False) - op.create_index('idx_inv_tx_order', 'inventory_transactions', ['order_id'], unique=False) - op.alter_column('invoices', 'created_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('invoices', 'updated_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('letzshop_fulfillment_queue', 'created_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('letzshop_fulfillment_queue', 'updated_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('letzshop_sync_logs', 'created_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('letzshop_sync_logs', 'updated_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('media_files', 'created_at', - existing_type=postgresql.TIMESTAMP(), - nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('media_files', 'updated_at', - existing_type=postgresql.TIMESTAMP(), - nullable=False) - op.alter_column('order_item_exceptions', 'created_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('order_item_exceptions', 'updated_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('order_items', 'created_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('order_items', 'updated_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('orders', 'created_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('orders', 'updated_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.drop_index('ix_password_reset_tokens_customer_id', table_name='password_reset_tokens') - op.create_index(op.f('ix_password_reset_tokens_id'), 'password_reset_tokens', ['id'], unique=False) - op.alter_column('product_media', 'created_at', - existing_type=postgresql.TIMESTAMP(), - nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('product_media', 'updated_at', - existing_type=postgresql.TIMESTAMP(), - nullable=False) - op.alter_column('products', 'is_digital', - existing_type=sa.BOOLEAN(), - nullable=True, - existing_server_default=sa.text('false')) - op.alter_column('products', 'product_type', - existing_type=sa.VARCHAR(length=20), - nullable=True, - existing_server_default=sa.text("'physical'::character varying")) - op.drop_index('idx_product_is_digital', table_name='products') - op.create_index(op.f('ix_products_is_digital'), 'products', ['is_digital'], unique=False) - op.drop_constraint('uq_vendor_email_settings_vendor_id', 'vendor_email_settings', type_='unique') - op.drop_index('ix_vendor_email_templates_lookup', table_name='vendor_email_templates') - op.create_index(op.f('ix_vendor_email_templates_id'), 'vendor_email_templates', ['id'], unique=False) - op.alter_column('vendor_invoice_settings', 'created_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('vendor_invoice_settings', 'updated_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.drop_constraint('vendor_invoice_settings_vendor_id_key', 'vendor_invoice_settings', type_='unique') - op.alter_column('vendor_letzshop_credentials', 'created_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('vendor_letzshop_credentials', 'updated_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.drop_constraint('vendor_letzshop_credentials_vendor_id_key', 'vendor_letzshop_credentials', type_='unique') - op.alter_column('vendor_subscriptions', 'created_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('vendor_subscriptions', 'updated_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.drop_constraint('vendor_subscriptions_vendor_id_key', 'vendor_subscriptions', type_='unique') - op.drop_constraint('fk_vendor_subscriptions_tier_id', 'vendor_subscriptions', type_='foreignkey') - op.create_foreign_key(None, 'vendor_subscriptions', 'subscription_tiers', ['tier_id'], ['id']) - op.alter_column('vendors', 'storefront_locale', - existing_type=sa.VARCHAR(length=10), - comment=None, - existing_comment='Currency/number formatting locale (NULL = inherit from platform)', - existing_nullable=True) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('vendors', 'storefront_locale', - existing_type=sa.VARCHAR(length=10), - comment='Currency/number formatting locale (NULL = inherit from platform)', - existing_nullable=True) - op.drop_constraint(None, 'vendor_subscriptions', type_='foreignkey') - op.create_foreign_key('fk_vendor_subscriptions_tier_id', 'vendor_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], ondelete='SET NULL') - op.create_unique_constraint('vendor_subscriptions_vendor_id_key', 'vendor_subscriptions', ['vendor_id']) - op.alter_column('vendor_subscriptions', 'updated_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('vendor_subscriptions', 'created_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.create_unique_constraint('vendor_letzshop_credentials_vendor_id_key', 'vendor_letzshop_credentials', ['vendor_id']) - op.alter_column('vendor_letzshop_credentials', 'updated_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('vendor_letzshop_credentials', 'created_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.create_unique_constraint('vendor_invoice_settings_vendor_id_key', 'vendor_invoice_settings', ['vendor_id']) - op.alter_column('vendor_invoice_settings', 'updated_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('vendor_invoice_settings', 'created_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.drop_index(op.f('ix_vendor_email_templates_id'), table_name='vendor_email_templates') - op.create_index('ix_vendor_email_templates_lookup', 'vendor_email_templates', ['vendor_id', 'template_code', 'language'], unique=False) - op.create_unique_constraint('uq_vendor_email_settings_vendor_id', 'vendor_email_settings', ['vendor_id']) - op.drop_index(op.f('ix_products_is_digital'), table_name='products') - op.create_index('idx_product_is_digital', 'products', ['is_digital'], unique=False) - op.alter_column('products', 'product_type', - existing_type=sa.VARCHAR(length=20), - nullable=False, - existing_server_default=sa.text("'physical'::character varying")) - op.alter_column('products', 'is_digital', - existing_type=sa.BOOLEAN(), - nullable=False, - existing_server_default=sa.text('false')) - op.alter_column('product_media', 'updated_at', - existing_type=postgresql.TIMESTAMP(), - nullable=True) - op.alter_column('product_media', 'created_at', - existing_type=postgresql.TIMESTAMP(), - nullable=True, - existing_server_default=sa.text('now()')) - op.drop_index(op.f('ix_password_reset_tokens_id'), table_name='password_reset_tokens') - op.create_index('ix_password_reset_tokens_customer_id', 'password_reset_tokens', ['customer_id'], unique=False) - op.alter_column('orders', 'updated_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('orders', 'created_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('order_items', 'updated_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('order_items', 'created_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('order_item_exceptions', 'updated_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('order_item_exceptions', 'created_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('media_files', 'updated_at', - existing_type=postgresql.TIMESTAMP(), - nullable=True) - op.alter_column('media_files', 'created_at', - existing_type=postgresql.TIMESTAMP(), - nullable=True, - existing_server_default=sa.text('now()')) - op.alter_column('letzshop_sync_logs', 'updated_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('letzshop_sync_logs', 'created_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('letzshop_fulfillment_queue', 'updated_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('letzshop_fulfillment_queue', 'created_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('invoices', 'updated_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.alter_column('invoices', 'created_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('CURRENT_TIMESTAMP')) - op.drop_index('idx_inv_tx_order', table_name='inventory_transactions') - op.drop_index(op.f('ix_features_minimum_tier_id'), table_name='features') - op.drop_index(op.f('ix_features_id'), table_name='features') - op.alter_column('capacity_snapshots', 'updated_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('capacity_snapshots', 'created_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=False, - existing_server_default=sa.text('now()')) - op.create_unique_constraint('architecture_rules_rule_id_key', 'architecture_rules', ['rule_id']) - op.drop_index(op.f('ix_letzshop_vendor_cache_slug'), table_name='letzshop_vendor_cache') - op.drop_index(op.f('ix_letzshop_vendor_cache_letzshop_id'), table_name='letzshop_vendor_cache') - op.drop_index(op.f('ix_letzshop_vendor_cache_id'), table_name='letzshop_vendor_cache') - op.drop_index(op.f('ix_letzshop_vendor_cache_claimed_by_vendor_id'), table_name='letzshop_vendor_cache') - op.drop_index('idx_vendor_cache_claimed', table_name='letzshop_vendor_cache') - op.drop_index('idx_vendor_cache_city', table_name='letzshop_vendor_cache') - op.drop_index('idx_vendor_cache_active', table_name='letzshop_vendor_cache') - op.drop_table('letzshop_vendor_cache') - # ### end Alembic commands ### diff --git a/alembic/versions/204273a59d73_add_letzshop_historical_import_jobs_.py b/alembic/versions/204273a59d73_add_letzshop_historical_import_jobs_.py deleted file mode 100644 index 44e6fe65..00000000 --- a/alembic/versions/204273a59d73_add_letzshop_historical_import_jobs_.py +++ /dev/null @@ -1,57 +0,0 @@ -"""add_letzshop_historical_import_jobs_table - -Revision ID: 204273a59d73 -Revises: cb88bc9b5f86 -Create Date: 2025-12-19 05:40:53.463341 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -# Removed: from sqlalchemy.dialects import sqlite (using sa.JSON for PostgreSQL) - -# revision identifiers, used by Alembic. -revision: str = '204273a59d73' -down_revision: Union[str, None] = 'cb88bc9b5f86' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - op.create_table('letzshop_historical_import_jobs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('vendor_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('status', sa.String(length=50), nullable=False), - sa.Column('current_phase', sa.String(length=20), nullable=True), - sa.Column('current_page', sa.Integer(), nullable=True), - sa.Column('total_pages', sa.Integer(), nullable=True), - sa.Column('shipments_fetched', sa.Integer(), nullable=True), - sa.Column('orders_processed', sa.Integer(), nullable=True), - sa.Column('orders_imported', sa.Integer(), nullable=True), - sa.Column('orders_updated', sa.Integer(), nullable=True), - sa.Column('orders_skipped', sa.Integer(), nullable=True), - sa.Column('products_matched', sa.Integer(), nullable=True), - sa.Column('products_not_found', sa.Integer(), nullable=True), - sa.Column('confirmed_stats', sa.JSON(), nullable=True), - sa.Column('declined_stats', sa.JSON(), nullable=True), - sa.Column('error_message', sa.Text(), nullable=True), - sa.Column('started_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), - sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index('idx_historical_import_vendor', 'letzshop_historical_import_jobs', ['vendor_id', 'status'], unique=False) - op.create_index(op.f('ix_letzshop_historical_import_jobs_id'), 'letzshop_historical_import_jobs', ['id'], unique=False) - op.create_index(op.f('ix_letzshop_historical_import_jobs_vendor_id'), 'letzshop_historical_import_jobs', ['vendor_id'], unique=False) - - -def downgrade() -> None: - op.drop_index(op.f('ix_letzshop_historical_import_jobs_vendor_id'), table_name='letzshop_historical_import_jobs') - op.drop_index(op.f('ix_letzshop_historical_import_jobs_id'), table_name='letzshop_historical_import_jobs') - op.drop_index('idx_historical_import_vendor', table_name='letzshop_historical_import_jobs') - op.drop_table('letzshop_historical_import_jobs') diff --git a/alembic/versions/2362c2723a93_add_order_date_to_letzshop_orders.py b/alembic/versions/2362c2723a93_add_order_date_to_letzshop_orders.py deleted file mode 100644 index c96cc7e1..00000000 --- a/alembic/versions/2362c2723a93_add_order_date_to_letzshop_orders.py +++ /dev/null @@ -1,27 +0,0 @@ -"""add_order_date_to_letzshop_orders - -Revision ID: 2362c2723a93 -Revises: 204273a59d73 -Create Date: 2025-12-19 08:46:23.731912 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '2362c2723a93' -down_revision: Union[str, None] = '204273a59d73' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Add order_date column to letzshop_orders table - op.add_column('letzshop_orders', sa.Column('order_date', sa.DateTime(timezone=True), nullable=True)) - - -def downgrade() -> None: - op.drop_column('letzshop_orders', 'order_date') diff --git a/alembic/versions/28d44d503cac_add_contact_fields_to_vendor.py b/alembic/versions/28d44d503cac_add_contact_fields_to_vendor.py deleted file mode 100644 index efe0a18d..00000000 --- a/alembic/versions/28d44d503cac_add_contact_fields_to_vendor.py +++ /dev/null @@ -1,37 +0,0 @@ -"""add contact fields to vendor - -Revision ID: 28d44d503cac -Revises: 9f3a25ea4991 -Create Date: 2025-12-03 22:26:02.161087 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '28d44d503cac' -down_revision: Union[str, None] = '9f3a25ea4991' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Add nullable contact fields to vendor table - # These allow vendor-specific branding/identity, overriding company defaults - op.add_column('vendors', sa.Column('contact_email', sa.String(255), nullable=True)) - op.add_column('vendors', sa.Column('contact_phone', sa.String(50), nullable=True)) - op.add_column('vendors', sa.Column('website', sa.String(255), nullable=True)) - op.add_column('vendors', sa.Column('business_address', sa.Text(), nullable=True)) - op.add_column('vendors', sa.Column('tax_number', sa.String(100), nullable=True)) - - -def downgrade() -> None: - # Remove contact fields from vendor table - op.drop_column('vendors', 'tax_number') - op.drop_column('vendors', 'business_address') - op.drop_column('vendors', 'website') - op.drop_column('vendors', 'contact_phone') - op.drop_column('vendors', 'contact_email') diff --git a/alembic/versions/2953ed10d22c_add_subscription_billing_tables.py b/alembic/versions/2953ed10d22c_add_subscription_billing_tables.py deleted file mode 100644 index 346270fd..00000000 --- a/alembic/versions/2953ed10d22c_add_subscription_billing_tables.py +++ /dev/null @@ -1,419 +0,0 @@ -"""add_subscription_billing_tables - -Revision ID: 2953ed10d22c -Revises: e1bfb453fbe9 -Create Date: 2025-12-25 18:29:34.167773 - -""" -from datetime import datetime -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -# Removed: from sqlalchemy.dialects import sqlite (using sa.JSON for PostgreSQL) - -# revision identifiers, used by Alembic. -revision: str = '2953ed10d22c' -down_revision: Union[str, None] = 'e1bfb453fbe9' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ========================================================================= - # Create new subscription and billing tables - # ========================================================================= - - # subscription_tiers - Database-driven tier definitions - op.create_table('subscription_tiers', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('code', sa.String(length=30), nullable=False), - sa.Column('name', sa.String(length=100), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('price_monthly_cents', sa.Integer(), nullable=False), - sa.Column('price_annual_cents', sa.Integer(), nullable=True), - sa.Column('orders_per_month', sa.Integer(), nullable=True), - sa.Column('products_limit', sa.Integer(), nullable=True), - sa.Column('team_members', sa.Integer(), nullable=True), - sa.Column('order_history_months', sa.Integer(), nullable=True), - sa.Column('features', sa.JSON(), nullable=True), - sa.Column('stripe_product_id', sa.String(length=100), nullable=True), - sa.Column('stripe_price_monthly_id', sa.String(length=100), nullable=True), - sa.Column('stripe_price_annual_id', sa.String(length=100), nullable=True), - sa.Column('display_order', sa.Integer(), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.Column('is_public', sa.Boolean(), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_subscription_tiers_code'), 'subscription_tiers', ['code'], unique=True) - op.create_index(op.f('ix_subscription_tiers_id'), 'subscription_tiers', ['id'], unique=False) - - # addon_products - Purchasable add-ons (domains, SSL, email) - op.create_table('addon_products', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('code', sa.String(length=50), nullable=False), - sa.Column('name', sa.String(length=100), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('category', sa.String(length=50), nullable=False), - sa.Column('price_cents', sa.Integer(), nullable=False), - sa.Column('billing_period', sa.String(length=20), nullable=False), - sa.Column('quantity_unit', sa.String(length=50), nullable=True), - sa.Column('quantity_value', sa.Integer(), nullable=True), - sa.Column('stripe_product_id', sa.String(length=100), nullable=True), - sa.Column('stripe_price_id', sa.String(length=100), nullable=True), - sa.Column('display_order', sa.Integer(), nullable=True), - 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.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_addon_products_category'), 'addon_products', ['category'], unique=False) - op.create_index(op.f('ix_addon_products_code'), 'addon_products', ['code'], unique=True) - op.create_index(op.f('ix_addon_products_id'), 'addon_products', ['id'], unique=False) - - # billing_history - Invoice and payment history - op.create_table('billing_history', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('vendor_id', sa.Integer(), nullable=False), - sa.Column('stripe_invoice_id', sa.String(length=100), nullable=True), - sa.Column('stripe_payment_intent_id', sa.String(length=100), nullable=True), - sa.Column('invoice_number', sa.String(length=50), nullable=True), - sa.Column('invoice_date', sa.DateTime(timezone=True), nullable=False), - sa.Column('due_date', sa.DateTime(timezone=True), nullable=True), - sa.Column('subtotal_cents', sa.Integer(), nullable=False), - sa.Column('tax_cents', sa.Integer(), nullable=False), - sa.Column('total_cents', sa.Integer(), nullable=False), - sa.Column('amount_paid_cents', sa.Integer(), nullable=False), - sa.Column('currency', sa.String(length=3), nullable=False), - sa.Column('status', sa.String(length=20), nullable=False), - sa.Column('invoice_pdf_url', sa.String(length=500), nullable=True), - sa.Column('hosted_invoice_url', sa.String(length=500), nullable=True), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('line_items', sa.JSON(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index('idx_billing_status', 'billing_history', ['vendor_id', 'status'], unique=False) - op.create_index('idx_billing_vendor_date', 'billing_history', ['vendor_id', 'invoice_date'], unique=False) - op.create_index(op.f('ix_billing_history_id'), 'billing_history', ['id'], unique=False) - op.create_index(op.f('ix_billing_history_status'), 'billing_history', ['status'], unique=False) - op.create_index(op.f('ix_billing_history_stripe_invoice_id'), 'billing_history', ['stripe_invoice_id'], unique=True) - op.create_index(op.f('ix_billing_history_vendor_id'), 'billing_history', ['vendor_id'], unique=False) - - # vendor_addons - Add-ons purchased by vendor - op.create_table('vendor_addons', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('vendor_id', sa.Integer(), nullable=False), - sa.Column('addon_product_id', sa.Integer(), nullable=False), - sa.Column('status', sa.String(length=20), nullable=False), - sa.Column('domain_name', sa.String(length=255), nullable=True), - sa.Column('quantity', sa.Integer(), nullable=False), - sa.Column('stripe_subscription_item_id', sa.String(length=100), nullable=True), - sa.Column('period_start', sa.DateTime(timezone=True), nullable=True), - sa.Column('period_end', sa.DateTime(timezone=True), nullable=True), - sa.Column('cancelled_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['addon_product_id'], ['addon_products.id'], ), - sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index('idx_vendor_addon_product', 'vendor_addons', ['vendor_id', 'addon_product_id'], unique=False) - op.create_index('idx_vendor_addon_status', 'vendor_addons', ['vendor_id', 'status'], unique=False) - op.create_index(op.f('ix_vendor_addons_addon_product_id'), 'vendor_addons', ['addon_product_id'], unique=False) - op.create_index(op.f('ix_vendor_addons_domain_name'), 'vendor_addons', ['domain_name'], unique=False) - op.create_index(op.f('ix_vendor_addons_id'), 'vendor_addons', ['id'], unique=False) - op.create_index(op.f('ix_vendor_addons_status'), 'vendor_addons', ['status'], unique=False) - op.create_index(op.f('ix_vendor_addons_vendor_id'), 'vendor_addons', ['vendor_id'], unique=False) - - # stripe_webhook_events - Webhook idempotency tracking - op.create_table('stripe_webhook_events', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('event_id', sa.String(length=100), nullable=False), - sa.Column('event_type', sa.String(length=100), nullable=False), - sa.Column('status', sa.String(length=20), nullable=False), - sa.Column('processed_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('error_message', sa.Text(), nullable=True), - sa.Column('payload_encrypted', sa.Text(), nullable=True), - sa.Column('vendor_id', sa.Integer(), nullable=True), - sa.Column('subscription_id', sa.Integer(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['subscription_id'], ['vendor_subscriptions.id'], ), - sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index('idx_webhook_event_type_status', 'stripe_webhook_events', ['event_type', 'status'], unique=False) - op.create_index(op.f('ix_stripe_webhook_events_event_id'), 'stripe_webhook_events', ['event_id'], unique=True) - op.create_index(op.f('ix_stripe_webhook_events_event_type'), 'stripe_webhook_events', ['event_type'], unique=False) - op.create_index(op.f('ix_stripe_webhook_events_id'), 'stripe_webhook_events', ['id'], unique=False) - op.create_index(op.f('ix_stripe_webhook_events_status'), 'stripe_webhook_events', ['status'], unique=False) - op.create_index(op.f('ix_stripe_webhook_events_subscription_id'), 'stripe_webhook_events', ['subscription_id'], unique=False) - op.create_index(op.f('ix_stripe_webhook_events_vendor_id'), 'stripe_webhook_events', ['vendor_id'], unique=False) - - # ========================================================================= - # Add new columns to vendor_subscriptions - # ========================================================================= - op.add_column('vendor_subscriptions', sa.Column('stripe_price_id', sa.String(length=100), nullable=True)) - op.add_column('vendor_subscriptions', sa.Column('stripe_payment_method_id', sa.String(length=100), nullable=True)) - op.add_column('vendor_subscriptions', sa.Column('proration_behavior', sa.String(length=50), nullable=True)) - op.add_column('vendor_subscriptions', sa.Column('scheduled_tier_change', sa.String(length=30), nullable=True)) - op.add_column('vendor_subscriptions', sa.Column('scheduled_change_at', sa.DateTime(timezone=True), nullable=True)) - op.add_column('vendor_subscriptions', sa.Column('payment_retry_count', sa.Integer(), server_default='0', nullable=False)) - op.add_column('vendor_subscriptions', sa.Column('last_payment_error', sa.Text(), nullable=True)) - - # ========================================================================= - # Seed subscription tiers - # ========================================================================= - now = datetime.utcnow() - - subscription_tiers = sa.table( - 'subscription_tiers', - sa.column('code', sa.String), - sa.column('name', sa.String), - sa.column('description', sa.Text), - sa.column('price_monthly_cents', sa.Integer), - sa.column('price_annual_cents', sa.Integer), - sa.column('orders_per_month', sa.Integer), - sa.column('products_limit', sa.Integer), - sa.column('team_members', sa.Integer), - sa.column('order_history_months', sa.Integer), - sa.column('features', sa.JSON), - sa.column('display_order', sa.Integer), - sa.column('is_active', sa.Boolean), - sa.column('is_public', sa.Boolean), - sa.column('created_at', sa.DateTime), - sa.column('updated_at', sa.DateTime), - ) - - op.bulk_insert(subscription_tiers, [ - { - 'code': 'essential', - 'name': 'Essential', - 'description': 'Perfect for solo vendors getting started with Letzshop', - 'price_monthly_cents': 4900, - 'price_annual_cents': 49000, - 'orders_per_month': 100, - 'products_limit': 200, - 'team_members': 1, - 'order_history_months': 6, - 'features': ['letzshop_sync', 'inventory_basic', 'invoice_lu', 'customer_view'], - 'display_order': 1, - 'is_active': True, - 'is_public': True, - 'created_at': now, - 'updated_at': now, - }, - { - 'code': 'professional', - 'name': 'Professional', - 'description': 'For active multi-channel vendors shipping EU-wide', - 'price_monthly_cents': 9900, - 'price_annual_cents': 99000, - 'orders_per_month': 500, - 'products_limit': None, - 'team_members': 3, - 'order_history_months': 24, - 'features': [ - 'letzshop_sync', 'inventory_locations', 'inventory_purchase_orders', - 'invoice_lu', 'invoice_eu_vat', 'customer_view', 'customer_export' - ], - 'display_order': 2, - 'is_active': True, - 'is_public': True, - 'created_at': now, - 'updated_at': now, - }, - { - 'code': 'business', - 'name': 'Business', - 'description': 'For high-volume vendors with teams and data-driven operations', - 'price_monthly_cents': 19900, - 'price_annual_cents': 199000, - 'orders_per_month': 2000, - 'products_limit': None, - 'team_members': 10, - 'order_history_months': None, - 'features': [ - 'letzshop_sync', 'inventory_locations', 'inventory_purchase_orders', - 'invoice_lu', 'invoice_eu_vat', 'invoice_bulk', 'customer_view', - 'customer_export', 'analytics_dashboard', 'accounting_export', - 'api_access', 'automation_rules', 'team_roles' - ], - 'display_order': 3, - 'is_active': True, - 'is_public': True, - 'created_at': now, - 'updated_at': now, - }, - { - 'code': 'enterprise', - 'name': 'Enterprise', - 'description': 'Custom solutions for large operations and agencies', - 'price_monthly_cents': 39900, - 'price_annual_cents': None, - 'orders_per_month': None, - 'products_limit': None, - 'team_members': None, - 'order_history_months': None, - 'features': [ - 'letzshop_sync', 'inventory_locations', 'inventory_purchase_orders', - 'invoice_lu', 'invoice_eu_vat', 'invoice_bulk', 'customer_view', - 'customer_export', 'analytics_dashboard', 'accounting_export', - 'api_access', 'automation_rules', 'team_roles', 'white_label', - 'multi_vendor', 'custom_integrations', 'sla_guarantee', 'dedicated_support' - ], - 'display_order': 4, - 'is_active': True, - 'is_public': False, - 'created_at': now, - 'updated_at': now, - }, - ]) - - # ========================================================================= - # Seed add-on products - # ========================================================================= - addon_products = sa.table( - 'addon_products', - sa.column('code', sa.String), - sa.column('name', sa.String), - sa.column('description', sa.Text), - sa.column('category', sa.String), - sa.column('price_cents', sa.Integer), - sa.column('billing_period', sa.String), - sa.column('quantity_unit', sa.String), - sa.column('quantity_value', sa.Integer), - sa.column('display_order', sa.Integer), - sa.column('is_active', sa.Boolean), - sa.column('created_at', sa.DateTime), - sa.column('updated_at', sa.DateTime), - ) - - op.bulk_insert(addon_products, [ - { - 'code': 'domain', - 'name': 'Custom Domain', - 'description': 'Connect your own domain with SSL certificate included', - 'category': 'domain', - 'price_cents': 1500, - 'billing_period': 'annual', - 'quantity_unit': None, - 'quantity_value': None, - 'display_order': 1, - 'is_active': True, - 'created_at': now, - 'updated_at': now, - }, - { - 'code': 'email_5', - 'name': '5 Email Addresses', - 'description': 'Professional email addresses on your domain', - 'category': 'email', - 'price_cents': 500, - 'billing_period': 'monthly', - 'quantity_unit': 'emails', - 'quantity_value': 5, - 'display_order': 2, - 'is_active': True, - 'created_at': now, - 'updated_at': now, - }, - { - 'code': 'email_10', - 'name': '10 Email Addresses', - 'description': 'Professional email addresses on your domain', - 'category': 'email', - 'price_cents': 900, - 'billing_period': 'monthly', - 'quantity_unit': 'emails', - 'quantity_value': 10, - 'display_order': 3, - 'is_active': True, - 'created_at': now, - 'updated_at': now, - }, - { - 'code': 'email_25', - 'name': '25 Email Addresses', - 'description': 'Professional email addresses on your domain', - 'category': 'email', - 'price_cents': 1900, - 'billing_period': 'monthly', - 'quantity_unit': 'emails', - 'quantity_value': 25, - 'display_order': 4, - 'is_active': True, - 'created_at': now, - 'updated_at': now, - }, - { - 'code': 'storage_10gb', - 'name': 'Additional Storage (10GB)', - 'description': 'Extra storage for product images and files', - 'category': 'storage', - 'price_cents': 500, - 'billing_period': 'monthly', - 'quantity_unit': 'GB', - 'quantity_value': 10, - 'display_order': 5, - 'is_active': True, - 'created_at': now, - 'updated_at': now, - }, - ]) - - -def downgrade() -> None: - # Remove new columns from vendor_subscriptions - op.drop_column('vendor_subscriptions', 'last_payment_error') - op.drop_column('vendor_subscriptions', 'payment_retry_count') - op.drop_column('vendor_subscriptions', 'scheduled_change_at') - op.drop_column('vendor_subscriptions', 'scheduled_tier_change') - op.drop_column('vendor_subscriptions', 'proration_behavior') - op.drop_column('vendor_subscriptions', 'stripe_payment_method_id') - op.drop_column('vendor_subscriptions', 'stripe_price_id') - - # Drop stripe_webhook_events - op.drop_index(op.f('ix_stripe_webhook_events_vendor_id'), table_name='stripe_webhook_events') - op.drop_index(op.f('ix_stripe_webhook_events_subscription_id'), table_name='stripe_webhook_events') - op.drop_index(op.f('ix_stripe_webhook_events_status'), table_name='stripe_webhook_events') - op.drop_index(op.f('ix_stripe_webhook_events_id'), table_name='stripe_webhook_events') - op.drop_index(op.f('ix_stripe_webhook_events_event_type'), table_name='stripe_webhook_events') - op.drop_index(op.f('ix_stripe_webhook_events_event_id'), table_name='stripe_webhook_events') - op.drop_index('idx_webhook_event_type_status', table_name='stripe_webhook_events') - op.drop_table('stripe_webhook_events') - - # Drop vendor_addons - op.drop_index(op.f('ix_vendor_addons_vendor_id'), table_name='vendor_addons') - op.drop_index(op.f('ix_vendor_addons_status'), table_name='vendor_addons') - op.drop_index(op.f('ix_vendor_addons_id'), table_name='vendor_addons') - op.drop_index(op.f('ix_vendor_addons_domain_name'), table_name='vendor_addons') - op.drop_index(op.f('ix_vendor_addons_addon_product_id'), table_name='vendor_addons') - op.drop_index('idx_vendor_addon_status', table_name='vendor_addons') - op.drop_index('idx_vendor_addon_product', table_name='vendor_addons') - op.drop_table('vendor_addons') - - # Drop billing_history - op.drop_index(op.f('ix_billing_history_vendor_id'), table_name='billing_history') - op.drop_index(op.f('ix_billing_history_stripe_invoice_id'), table_name='billing_history') - op.drop_index(op.f('ix_billing_history_status'), table_name='billing_history') - op.drop_index(op.f('ix_billing_history_id'), table_name='billing_history') - op.drop_index('idx_billing_vendor_date', table_name='billing_history') - op.drop_index('idx_billing_status', table_name='billing_history') - op.drop_table('billing_history') - - # Drop addon_products - op.drop_index(op.f('ix_addon_products_id'), table_name='addon_products') - op.drop_index(op.f('ix_addon_products_code'), table_name='addon_products') - op.drop_index(op.f('ix_addon_products_category'), table_name='addon_products') - op.drop_table('addon_products') - - # Drop subscription_tiers - op.drop_index(op.f('ix_subscription_tiers_id'), table_name='subscription_tiers') - op.drop_index(op.f('ix_subscription_tiers_code'), table_name='subscription_tiers') - op.drop_table('subscription_tiers') diff --git a/alembic/versions/404b3e2d2865_add_letzshop_vendor_fields_and_trial_.py b/alembic/versions/404b3e2d2865_add_letzshop_vendor_fields_and_trial_.py deleted file mode 100644 index 6fa0242f..00000000 --- a/alembic/versions/404b3e2d2865_add_letzshop_vendor_fields_and_trial_.py +++ /dev/null @@ -1,44 +0,0 @@ -"""add_letzshop_vendor_fields_and_trial_tracking - -Revision ID: 404b3e2d2865 -Revises: l0a1b2c3d4e5 -Create Date: 2025-12-27 09:49:44.715243 - -Adds: -- vendors.letzshop_vendor_id - Link to Letzshop marketplace profile -- vendors.letzshop_vendor_slug - Letzshop shop URL slug -- vendor_subscriptions.card_collected_at - Track when card was collected for trial -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '404b3e2d2865' -down_revision: Union[str, None] = 'l0a1b2c3d4e5' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Add Letzshop vendor identity fields to vendors table - op.add_column('vendors', sa.Column('letzshop_vendor_id', sa.String(length=100), nullable=True)) - op.add_column('vendors', sa.Column('letzshop_vendor_slug', sa.String(length=200), nullable=True)) - op.create_index(op.f('ix_vendors_letzshop_vendor_id'), 'vendors', ['letzshop_vendor_id'], unique=True) - op.create_index(op.f('ix_vendors_letzshop_vendor_slug'), 'vendors', ['letzshop_vendor_slug'], unique=False) - - # Add card collection tracking to vendor_subscriptions - op.add_column('vendor_subscriptions', sa.Column('card_collected_at', sa.DateTime(timezone=True), nullable=True)) - - -def downgrade() -> None: - # Remove card collection tracking from vendor_subscriptions - op.drop_column('vendor_subscriptions', 'card_collected_at') - - # Remove Letzshop vendor identity fields from vendors - op.drop_index(op.f('ix_vendors_letzshop_vendor_slug'), table_name='vendors') - op.drop_index(op.f('ix_vendors_letzshop_vendor_id'), table_name='vendors') - op.drop_column('vendors', 'letzshop_vendor_slug') - op.drop_column('vendors', 'letzshop_vendor_id') diff --git a/alembic/versions/4951b2e50581_initial_migration_all_tables.py b/alembic/versions/4951b2e50581_initial_migration_all_tables.py deleted file mode 100644 index bdf730a1..00000000 --- a/alembic/versions/4951b2e50581_initial_migration_all_tables.py +++ /dev/null @@ -1,908 +0,0 @@ -"""Initial migration - all tables - -Revision ID: 4951b2e50581 -Revises: -Create Date: 2025-10-27 22:28:33.137564 - -""" - -from typing import Sequence, Union - -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "4951b2e50581" -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "marketplace_products", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("marketplace_product_id", sa.String(), nullable=False), - sa.Column("title", sa.String(), nullable=False), - sa.Column("description", sa.String(), nullable=True), - sa.Column("link", sa.String(), nullable=True), - sa.Column("image_link", sa.String(), nullable=True), - sa.Column("availability", sa.String(), nullable=True), - sa.Column("price", sa.String(), nullable=True), - sa.Column("brand", sa.String(), nullable=True), - sa.Column("gtin", sa.String(), nullable=True), - sa.Column("mpn", sa.String(), nullable=True), - sa.Column("condition", sa.String(), nullable=True), - sa.Column("adult", sa.String(), nullable=True), - sa.Column("multipack", sa.Integer(), nullable=True), - sa.Column("is_bundle", sa.String(), nullable=True), - sa.Column("age_group", sa.String(), nullable=True), - sa.Column("color", sa.String(), nullable=True), - sa.Column("gender", sa.String(), nullable=True), - sa.Column("material", sa.String(), nullable=True), - sa.Column("pattern", sa.String(), nullable=True), - sa.Column("size", sa.String(), nullable=True), - sa.Column("size_type", sa.String(), nullable=True), - sa.Column("size_system", sa.String(), nullable=True), - sa.Column("item_group_id", sa.String(), nullable=True), - sa.Column("google_product_category", sa.String(), nullable=True), - sa.Column("product_type", sa.String(), nullable=True), - sa.Column("custom_label_0", sa.String(), nullable=True), - sa.Column("custom_label_1", sa.String(), nullable=True), - sa.Column("custom_label_2", sa.String(), nullable=True), - sa.Column("custom_label_3", sa.String(), nullable=True), - sa.Column("custom_label_4", sa.String(), nullable=True), - sa.Column("additional_image_link", sa.String(), nullable=True), - sa.Column("sale_price", sa.String(), nullable=True), - sa.Column("unit_pricing_measure", sa.String(), nullable=True), - sa.Column("unit_pricing_base_measure", sa.String(), nullable=True), - sa.Column("identifier_exists", sa.String(), nullable=True), - sa.Column("shipping", sa.String(), nullable=True), - sa.Column("currency", sa.String(), nullable=True), - sa.Column("marketplace", sa.String(), nullable=True), - sa.Column("vendor_name", sa.String(), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - "idx_marketplace_brand", - "marketplace_products", - ["marketplace", "brand"], - unique=False, - ) - op.create_index( - "idx_marketplace_vendor", - "marketplace_products", - ["marketplace", "vendor_name"], - unique=False, - ) - op.create_index( - op.f("ix_marketplace_products_availability"), - "marketplace_products", - ["availability"], - unique=False, - ) - op.create_index( - op.f("ix_marketplace_products_brand"), - "marketplace_products", - ["brand"], - unique=False, - ) - op.create_index( - op.f("ix_marketplace_products_google_product_category"), - "marketplace_products", - ["google_product_category"], - unique=False, - ) - op.create_index( - op.f("ix_marketplace_products_gtin"), - "marketplace_products", - ["gtin"], - unique=False, - ) - op.create_index( - op.f("ix_marketplace_products_id"), "marketplace_products", ["id"], unique=False - ) - op.create_index( - op.f("ix_marketplace_products_marketplace"), - "marketplace_products", - ["marketplace"], - unique=False, - ) - op.create_index( - op.f("ix_marketplace_products_marketplace_product_id"), - "marketplace_products", - ["marketplace_product_id"], - unique=True, - ) - op.create_index( - op.f("ix_marketplace_products_vendor_name"), - "marketplace_products", - ["vendor_name"], - unique=False, - ) - op.create_table( - "users", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("email", sa.String(), nullable=False), - sa.Column("username", sa.String(), nullable=False), - sa.Column("first_name", sa.String(), nullable=True), - sa.Column("last_name", sa.String(), nullable=True), - sa.Column("hashed_password", sa.String(), nullable=False), - sa.Column("role", sa.String(), nullable=False), - sa.Column("is_active", sa.Boolean(), nullable=False), - sa.Column("last_login", sa.DateTime(), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True) - op.create_index(op.f("ix_users_id"), "users", ["id"], unique=False) - op.create_index(op.f("ix_users_username"), "users", ["username"], unique=True) - op.create_table( - "admin_audit_logs", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("admin_user_id", sa.Integer(), nullable=False), - sa.Column("action", sa.String(length=100), nullable=False), - sa.Column("target_type", sa.String(length=50), nullable=False), - sa.Column("target_id", sa.String(length=100), nullable=False), - sa.Column("details", sa.JSON(), nullable=True), - sa.Column("ip_address", sa.String(length=45), nullable=True), - sa.Column("user_agent", sa.Text(), nullable=True), - sa.Column("request_id", sa.String(length=100), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint( - ["admin_user_id"], - ["users.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_admin_audit_logs_action"), "admin_audit_logs", ["action"], unique=False - ) - op.create_index( - op.f("ix_admin_audit_logs_admin_user_id"), - "admin_audit_logs", - ["admin_user_id"], - unique=False, - ) - op.create_index( - op.f("ix_admin_audit_logs_id"), "admin_audit_logs", ["id"], unique=False - ) - op.create_index( - op.f("ix_admin_audit_logs_target_id"), - "admin_audit_logs", - ["target_id"], - unique=False, - ) - op.create_index( - op.f("ix_admin_audit_logs_target_type"), - "admin_audit_logs", - ["target_type"], - unique=False, - ) - op.create_table( - "admin_notifications", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("type", sa.String(length=50), nullable=False), - sa.Column("priority", sa.String(length=20), nullable=True), - sa.Column("title", sa.String(length=200), nullable=False), - sa.Column("message", sa.Text(), nullable=False), - sa.Column("is_read", sa.Boolean(), nullable=True), - sa.Column("read_at", sa.DateTime(), nullable=True), - sa.Column("read_by_user_id", sa.Integer(), nullable=True), - sa.Column("action_required", sa.Boolean(), nullable=True), - sa.Column("action_url", sa.String(length=500), nullable=True), - sa.Column("notification_metadata", sa.JSON(), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint( - ["read_by_user_id"], - ["users.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_admin_notifications_action_required"), - "admin_notifications", - ["action_required"], - unique=False, - ) - op.create_index( - op.f("ix_admin_notifications_id"), "admin_notifications", ["id"], unique=False - ) - op.create_index( - op.f("ix_admin_notifications_is_read"), - "admin_notifications", - ["is_read"], - unique=False, - ) - op.create_index( - op.f("ix_admin_notifications_priority"), - "admin_notifications", - ["priority"], - unique=False, - ) - op.create_index( - op.f("ix_admin_notifications_type"), - "admin_notifications", - ["type"], - unique=False, - ) - op.create_table( - "admin_sessions", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("admin_user_id", sa.Integer(), nullable=False), - sa.Column("session_token", sa.String(length=255), nullable=False), - sa.Column("ip_address", sa.String(length=45), nullable=False), - sa.Column("user_agent", sa.Text(), nullable=True), - sa.Column("login_at", sa.DateTime(), nullable=False), - sa.Column("last_activity_at", sa.DateTime(), nullable=False), - sa.Column("logout_at", sa.DateTime(), nullable=True), - sa.Column("is_active", sa.Boolean(), nullable=True), - sa.Column("logout_reason", sa.String(length=50), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint( - ["admin_user_id"], - ["users.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_admin_sessions_admin_user_id"), - "admin_sessions", - ["admin_user_id"], - unique=False, - ) - op.create_index( - op.f("ix_admin_sessions_id"), "admin_sessions", ["id"], unique=False - ) - op.create_index( - op.f("ix_admin_sessions_is_active"), - "admin_sessions", - ["is_active"], - unique=False, - ) - op.create_index( - op.f("ix_admin_sessions_login_at"), "admin_sessions", ["login_at"], unique=False - ) - op.create_index( - op.f("ix_admin_sessions_session_token"), - "admin_sessions", - ["session_token"], - unique=True, - ) - op.create_table( - "admin_settings", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("key", sa.String(length=100), nullable=False), - sa.Column("value", sa.Text(), nullable=False), - sa.Column("value_type", sa.String(length=20), nullable=True), - sa.Column("category", sa.String(length=50), nullable=True), - sa.Column("description", sa.Text(), nullable=True), - sa.Column("is_encrypted", sa.Boolean(), nullable=True), - sa.Column("is_public", sa.Boolean(), nullable=True), - sa.Column("last_modified_by_user_id", sa.Integer(), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint( - ["last_modified_by_user_id"], - ["users.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_admin_settings_category"), "admin_settings", ["category"], unique=False - ) - op.create_index( - op.f("ix_admin_settings_id"), "admin_settings", ["id"], unique=False - ) - op.create_index( - op.f("ix_admin_settings_key"), "admin_settings", ["key"], unique=True - ) - op.create_table( - "platform_alerts", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("alert_type", sa.String(length=50), nullable=False), - sa.Column("severity", sa.String(length=20), nullable=False), - sa.Column("title", sa.String(length=200), nullable=False), - sa.Column("description", sa.Text(), nullable=True), - sa.Column("affected_vendors", sa.JSON(), nullable=True), - sa.Column("affected_systems", sa.JSON(), nullable=True), - sa.Column("is_resolved", sa.Boolean(), nullable=True), - sa.Column("resolved_at", sa.DateTime(), nullable=True), - sa.Column("resolved_by_user_id", sa.Integer(), nullable=True), - sa.Column("resolution_notes", sa.Text(), nullable=True), - sa.Column("auto_generated", sa.Boolean(), nullable=True), - sa.Column("occurrence_count", sa.Integer(), nullable=True), - sa.Column("first_occurred_at", sa.DateTime(), nullable=False), - sa.Column("last_occurred_at", sa.DateTime(), nullable=False), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint( - ["resolved_by_user_id"], - ["users.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_platform_alerts_alert_type"), - "platform_alerts", - ["alert_type"], - unique=False, - ) - op.create_index( - op.f("ix_platform_alerts_id"), "platform_alerts", ["id"], unique=False - ) - op.create_index( - op.f("ix_platform_alerts_is_resolved"), - "platform_alerts", - ["is_resolved"], - unique=False, - ) - op.create_index( - op.f("ix_platform_alerts_severity"), - "platform_alerts", - ["severity"], - unique=False, - ) - op.create_table( - "vendors", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("vendor_code", sa.String(), nullable=False), - sa.Column("subdomain", sa.String(length=100), nullable=False), - sa.Column("name", sa.String(), nullable=False), - sa.Column("description", sa.Text(), nullable=True), - sa.Column("owner_user_id", sa.Integer(), nullable=False), - sa.Column("contact_email", sa.String(), nullable=True), - sa.Column("contact_phone", sa.String(), nullable=True), - sa.Column("website", sa.String(), nullable=True), - sa.Column("letzshop_csv_url_fr", sa.String(), nullable=True), - sa.Column("letzshop_csv_url_en", sa.String(), nullable=True), - sa.Column("letzshop_csv_url_de", sa.String(), nullable=True), - sa.Column("business_address", sa.Text(), nullable=True), - sa.Column("tax_number", sa.String(), nullable=True), - sa.Column("is_active", sa.Boolean(), nullable=True), - sa.Column("is_verified", sa.Boolean(), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint( - ["owner_user_id"], - ["users.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_vendors_id"), "vendors", ["id"], unique=False) - op.create_index(op.f("ix_vendors_subdomain"), "vendors", ["subdomain"], unique=True) - op.create_index( - op.f("ix_vendors_vendor_code"), "vendors", ["vendor_code"], unique=True - ) - op.create_table( - "customers", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=False), - sa.Column("email", sa.String(length=255), nullable=False), - sa.Column("hashed_password", sa.String(length=255), nullable=False), - sa.Column("first_name", sa.String(length=100), nullable=True), - sa.Column("last_name", sa.String(length=100), nullable=True), - sa.Column("phone", sa.String(length=50), nullable=True), - sa.Column("customer_number", sa.String(length=100), nullable=False), - sa.Column("preferences", sa.JSON(), nullable=True), - sa.Column("marketing_consent", sa.Boolean(), nullable=True), - sa.Column("last_order_date", sa.DateTime(), nullable=True), - sa.Column("total_orders", sa.Integer(), nullable=True), - sa.Column("total_spent", sa.Numeric(precision=10, scale=2), nullable=True), - 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( - ["vendor_id"], - ["vendors.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_customers_customer_number"), - "customers", - ["customer_number"], - unique=False, - ) - op.create_index(op.f("ix_customers_email"), "customers", ["email"], unique=False) - op.create_index(op.f("ix_customers_id"), "customers", ["id"], unique=False) - op.create_table( - "marketplace_import_jobs", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=False), - sa.Column("user_id", sa.Integer(), nullable=False), - sa.Column("marketplace", sa.String(), nullable=False), - sa.Column("source_url", sa.String(), nullable=False), - sa.Column("status", sa.String(), nullable=False), - sa.Column("imported_count", sa.Integer(), nullable=True), - sa.Column("updated_count", sa.Integer(), nullable=True), - sa.Column("error_count", sa.Integer(), nullable=True), - sa.Column("total_processed", sa.Integer(), nullable=True), - sa.Column("error_message", sa.Text(), nullable=True), - sa.Column("started_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint( - ["user_id"], - ["users.id"], - ), - sa.ForeignKeyConstraint( - ["vendor_id"], - ["vendors.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - "idx_import_user_marketplace", - "marketplace_import_jobs", - ["user_id", "marketplace"], - unique=False, - ) - op.create_index( - "idx_import_vendor_created", - "marketplace_import_jobs", - ["vendor_id", "created_at"], - unique=False, - ) - op.create_index( - "idx_import_vendor_status", - "marketplace_import_jobs", - ["vendor_id", "status"], - unique=False, - ) - op.create_index( - op.f("ix_marketplace_import_jobs_id"), - "marketplace_import_jobs", - ["id"], - unique=False, - ) - op.create_index( - op.f("ix_marketplace_import_jobs_marketplace"), - "marketplace_import_jobs", - ["marketplace"], - unique=False, - ) - op.create_index( - op.f("ix_marketplace_import_jobs_vendor_id"), - "marketplace_import_jobs", - ["vendor_id"], - unique=False, - ) - op.create_table( - "products", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=False), - sa.Column("marketplace_product_id", sa.Integer(), nullable=False), - sa.Column("product_id", sa.String(), nullable=True), - sa.Column("price", sa.Float(), nullable=True), - sa.Column("sale_price", sa.Float(), nullable=True), - sa.Column("currency", sa.String(), nullable=True), - sa.Column("availability", sa.String(), nullable=True), - sa.Column("condition", sa.String(), nullable=True), - sa.Column("is_featured", sa.Boolean(), nullable=True), - sa.Column("is_active", sa.Boolean(), nullable=True), - sa.Column("display_order", sa.Integer(), nullable=True), - sa.Column("min_quantity", sa.Integer(), nullable=True), - sa.Column("max_quantity", sa.Integer(), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint( - ["marketplace_product_id"], - ["marketplace_products.id"], - ), - sa.ForeignKeyConstraint( - ["vendor_id"], - ["vendors.id"], - ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("vendor_id", "marketplace_product_id", name="uq_product"), - ) - op.create_index( - "idx_product_active", "products", ["vendor_id", "is_active"], unique=False - ) - op.create_index( - "idx_product_featured", "products", ["vendor_id", "is_featured"], unique=False - ) - op.create_index(op.f("ix_products_id"), "products", ["id"], unique=False) - op.create_table( - "roles", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=False), - sa.Column("name", sa.String(length=100), nullable=False), - sa.Column("permissions", sa.JSON(), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint( - ["vendor_id"], - ["vendors.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_roles_id"), "roles", ["id"], unique=False) - op.create_table( - "vendor_domains", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=False), - sa.Column("domain", sa.String(length=255), nullable=False), - sa.Column("is_primary", sa.Boolean(), nullable=False), - sa.Column("is_active", sa.Boolean(), nullable=False), - sa.Column("ssl_status", sa.String(length=50), nullable=True), - sa.Column("ssl_verified_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("verification_token", sa.String(length=100), nullable=True), - sa.Column("is_verified", sa.Boolean(), nullable=False), - sa.Column("verified_at", sa.DateTime(timezone=True), nullable=True), - 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"), - sa.UniqueConstraint("vendor_id", "domain", name="uq_vendor_domain"), - sa.UniqueConstraint("verification_token"), - ) - op.create_index( - "idx_domain_active", "vendor_domains", ["domain", "is_active"], unique=False - ) - op.create_index( - "idx_vendor_primary", - "vendor_domains", - ["vendor_id", "is_primary"], - unique=False, - ) - op.create_index( - op.f("ix_vendor_domains_domain"), "vendor_domains", ["domain"], unique=True - ) - op.create_index( - op.f("ix_vendor_domains_id"), "vendor_domains", ["id"], unique=False - ) - op.create_table( - "vendor_themes", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=False), - sa.Column("theme_name", sa.String(length=100), nullable=True), - sa.Column("is_active", sa.Boolean(), nullable=True), - sa.Column("colors", sa.JSON(), nullable=True), - sa.Column("font_family_heading", sa.String(length=100), nullable=True), - sa.Column("font_family_body", sa.String(length=100), nullable=True), - sa.Column("logo_url", sa.String(length=500), nullable=True), - sa.Column("logo_dark_url", sa.String(length=500), nullable=True), - sa.Column("favicon_url", sa.String(length=500), nullable=True), - sa.Column("banner_url", sa.String(length=500), nullable=True), - sa.Column("layout_style", sa.String(length=50), nullable=True), - sa.Column("header_style", sa.String(length=50), nullable=True), - sa.Column("product_card_style", sa.String(length=50), nullable=True), - sa.Column("custom_css", sa.Text(), nullable=True), - sa.Column("social_links", sa.JSON(), nullable=True), - sa.Column("meta_title_template", sa.String(length=200), nullable=True), - sa.Column("meta_description", sa.Text(), nullable=True), - 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"), - sa.UniqueConstraint("vendor_id"), - ) - op.create_index(op.f("ix_vendor_themes_id"), "vendor_themes", ["id"], unique=False) - op.create_table( - "customer_addresses", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=False), - sa.Column("customer_id", sa.Integer(), nullable=False), - sa.Column("address_type", sa.String(length=50), nullable=False), - sa.Column("first_name", sa.String(length=100), nullable=False), - sa.Column("last_name", sa.String(length=100), nullable=False), - sa.Column("company", sa.String(length=200), nullable=True), - sa.Column("address_line_1", sa.String(length=255), nullable=False), - sa.Column("address_line_2", sa.String(length=255), nullable=True), - sa.Column("city", sa.String(length=100), nullable=False), - sa.Column("postal_code", sa.String(length=20), nullable=False), - sa.Column("country", sa.String(length=100), nullable=False), - sa.Column("is_default", sa.Boolean(), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint( - ["customer_id"], - ["customers.id"], - ), - sa.ForeignKeyConstraint( - ["vendor_id"], - ["vendors.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_customer_addresses_id"), "customer_addresses", ["id"], unique=False - ) - op.create_table( - "inventory", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("product_id", sa.Integer(), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=False), - sa.Column("location", sa.String(), nullable=False), - sa.Column("quantity", sa.Integer(), nullable=False), - sa.Column("reserved_quantity", sa.Integer(), nullable=True), - sa.Column("gtin", sa.String(), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint( - ["product_id"], - ["products.id"], - ), - sa.ForeignKeyConstraint( - ["vendor_id"], - ["vendors.id"], - ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint( - "product_id", "location", name="uq_inventory_product_location" - ), - ) - op.create_index( - "idx_inventory_product_location", - "inventory", - ["product_id", "location"], - unique=False, - ) - op.create_index( - "idx_inventory_vendor_product", - "inventory", - ["vendor_id", "product_id"], - unique=False, - ) - op.create_index(op.f("ix_inventory_gtin"), "inventory", ["gtin"], unique=False) - op.create_index(op.f("ix_inventory_id"), "inventory", ["id"], unique=False) - op.create_index( - op.f("ix_inventory_location"), "inventory", ["location"], unique=False - ) - op.create_index( - op.f("ix_inventory_product_id"), "inventory", ["product_id"], unique=False - ) - op.create_index( - op.f("ix_inventory_vendor_id"), "inventory", ["vendor_id"], unique=False - ) - op.create_table( - "vendor_users", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=False), - sa.Column("user_id", sa.Integer(), nullable=False), - sa.Column("role_id", sa.Integer(), nullable=False), - sa.Column("invited_by", sa.Integer(), nullable=True), - 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( - ["invited_by"], - ["users.id"], - ), - sa.ForeignKeyConstraint( - ["role_id"], - ["roles.id"], - ), - sa.ForeignKeyConstraint( - ["user_id"], - ["users.id"], - ), - sa.ForeignKeyConstraint( - ["vendor_id"], - ["vendors.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_vendor_users_id"), "vendor_users", ["id"], unique=False) - op.create_table( - "orders", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=False), - sa.Column("customer_id", sa.Integer(), nullable=False), - sa.Column("order_number", sa.String(), nullable=False), - sa.Column("status", sa.String(), nullable=False), - sa.Column("subtotal", sa.Float(), nullable=False), - sa.Column("tax_amount", sa.Float(), nullable=True), - sa.Column("shipping_amount", sa.Float(), nullable=True), - sa.Column("discount_amount", sa.Float(), nullable=True), - sa.Column("total_amount", sa.Float(), nullable=False), - sa.Column("currency", sa.String(), nullable=True), - sa.Column("shipping_address_id", sa.Integer(), nullable=False), - sa.Column("billing_address_id", sa.Integer(), nullable=False), - sa.Column("shipping_method", sa.String(), nullable=True), - sa.Column("tracking_number", sa.String(), nullable=True), - sa.Column("customer_notes", sa.Text(), nullable=True), - sa.Column("internal_notes", sa.Text(), nullable=True), - sa.Column("paid_at", sa.DateTime(), nullable=True), - sa.Column("shipped_at", sa.DateTime(), nullable=True), - sa.Column("delivered_at", sa.DateTime(), nullable=True), - sa.Column("cancelled_at", sa.DateTime(), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint( - ["billing_address_id"], - ["customer_addresses.id"], - ), - sa.ForeignKeyConstraint( - ["customer_id"], - ["customers.id"], - ), - sa.ForeignKeyConstraint( - ["shipping_address_id"], - ["customer_addresses.id"], - ), - sa.ForeignKeyConstraint( - ["vendor_id"], - ["vendors.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_orders_customer_id"), "orders", ["customer_id"], unique=False - ) - op.create_index(op.f("ix_orders_id"), "orders", ["id"], unique=False) - op.create_index( - op.f("ix_orders_order_number"), "orders", ["order_number"], unique=True - ) - op.create_index(op.f("ix_orders_status"), "orders", ["status"], unique=False) - op.create_index(op.f("ix_orders_vendor_id"), "orders", ["vendor_id"], unique=False) - op.create_table( - "order_items", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("order_id", sa.Integer(), nullable=False), - sa.Column("product_id", sa.Integer(), nullable=False), - sa.Column("product_name", sa.String(), nullable=False), - sa.Column("product_sku", sa.String(), nullable=True), - sa.Column("quantity", sa.Integer(), nullable=False), - sa.Column("unit_price", sa.Float(), nullable=False), - sa.Column("total_price", sa.Float(), nullable=False), - sa.Column("inventory_reserved", sa.Boolean(), nullable=True), - sa.Column("inventory_fulfilled", sa.Boolean(), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint( - ["order_id"], - ["orders.id"], - ), - sa.ForeignKeyConstraint( - ["product_id"], - ["products.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_order_items_id"), "order_items", ["id"], unique=False) - op.create_index( - op.f("ix_order_items_order_id"), "order_items", ["order_id"], unique=False - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f("ix_order_items_order_id"), table_name="order_items") - op.drop_index(op.f("ix_order_items_id"), table_name="order_items") - op.drop_table("order_items") - op.drop_index(op.f("ix_orders_vendor_id"), table_name="orders") - op.drop_index(op.f("ix_orders_status"), table_name="orders") - op.drop_index(op.f("ix_orders_order_number"), table_name="orders") - op.drop_index(op.f("ix_orders_id"), table_name="orders") - op.drop_index(op.f("ix_orders_customer_id"), table_name="orders") - op.drop_table("orders") - op.drop_index(op.f("ix_vendor_users_id"), table_name="vendor_users") - op.drop_table("vendor_users") - op.drop_index(op.f("ix_inventory_vendor_id"), table_name="inventory") - op.drop_index(op.f("ix_inventory_product_id"), table_name="inventory") - op.drop_index(op.f("ix_inventory_location"), table_name="inventory") - op.drop_index(op.f("ix_inventory_id"), table_name="inventory") - op.drop_index(op.f("ix_inventory_gtin"), table_name="inventory") - op.drop_index("idx_inventory_vendor_product", table_name="inventory") - op.drop_index("idx_inventory_product_location", table_name="inventory") - op.drop_table("inventory") - op.drop_index(op.f("ix_customer_addresses_id"), table_name="customer_addresses") - op.drop_table("customer_addresses") - op.drop_index(op.f("ix_vendor_themes_id"), table_name="vendor_themes") - op.drop_table("vendor_themes") - op.drop_index(op.f("ix_vendor_domains_id"), table_name="vendor_domains") - op.drop_index(op.f("ix_vendor_domains_domain"), table_name="vendor_domains") - op.drop_index("idx_vendor_primary", table_name="vendor_domains") - op.drop_index("idx_domain_active", table_name="vendor_domains") - op.drop_table("vendor_domains") - op.drop_index(op.f("ix_roles_id"), table_name="roles") - op.drop_table("roles") - op.drop_index(op.f("ix_products_id"), table_name="products") - op.drop_index("idx_product_featured", table_name="products") - op.drop_index("idx_product_active", table_name="products") - op.drop_table("products") - op.drop_index( - op.f("ix_marketplace_import_jobs_vendor_id"), - table_name="marketplace_import_jobs", - ) - op.drop_index( - op.f("ix_marketplace_import_jobs_marketplace"), - table_name="marketplace_import_jobs", - ) - op.drop_index( - op.f("ix_marketplace_import_jobs_id"), table_name="marketplace_import_jobs" - ) - op.drop_index("idx_import_vendor_status", table_name="marketplace_import_jobs") - op.drop_index("idx_import_vendor_created", table_name="marketplace_import_jobs") - op.drop_index("idx_import_user_marketplace", table_name="marketplace_import_jobs") - op.drop_table("marketplace_import_jobs") - op.drop_index(op.f("ix_customers_id"), table_name="customers") - op.drop_index(op.f("ix_customers_email"), table_name="customers") - op.drop_index(op.f("ix_customers_customer_number"), table_name="customers") - op.drop_table("customers") - op.drop_index(op.f("ix_vendors_vendor_code"), table_name="vendors") - op.drop_index(op.f("ix_vendors_subdomain"), table_name="vendors") - op.drop_index(op.f("ix_vendors_id"), table_name="vendors") - op.drop_table("vendors") - op.drop_index(op.f("ix_platform_alerts_severity"), table_name="platform_alerts") - op.drop_index(op.f("ix_platform_alerts_is_resolved"), table_name="platform_alerts") - op.drop_index(op.f("ix_platform_alerts_id"), table_name="platform_alerts") - op.drop_index(op.f("ix_platform_alerts_alert_type"), table_name="platform_alerts") - op.drop_table("platform_alerts") - op.drop_index(op.f("ix_admin_settings_key"), table_name="admin_settings") - op.drop_index(op.f("ix_admin_settings_id"), table_name="admin_settings") - op.drop_index(op.f("ix_admin_settings_category"), table_name="admin_settings") - op.drop_table("admin_settings") - op.drop_index(op.f("ix_admin_sessions_session_token"), table_name="admin_sessions") - op.drop_index(op.f("ix_admin_sessions_login_at"), table_name="admin_sessions") - op.drop_index(op.f("ix_admin_sessions_is_active"), table_name="admin_sessions") - op.drop_index(op.f("ix_admin_sessions_id"), table_name="admin_sessions") - op.drop_index(op.f("ix_admin_sessions_admin_user_id"), table_name="admin_sessions") - op.drop_table("admin_sessions") - op.drop_index(op.f("ix_admin_notifications_type"), table_name="admin_notifications") - op.drop_index( - op.f("ix_admin_notifications_priority"), table_name="admin_notifications" - ) - op.drop_index( - op.f("ix_admin_notifications_is_read"), table_name="admin_notifications" - ) - op.drop_index(op.f("ix_admin_notifications_id"), table_name="admin_notifications") - op.drop_index( - op.f("ix_admin_notifications_action_required"), table_name="admin_notifications" - ) - op.drop_table("admin_notifications") - op.drop_index( - op.f("ix_admin_audit_logs_target_type"), table_name="admin_audit_logs" - ) - op.drop_index(op.f("ix_admin_audit_logs_target_id"), table_name="admin_audit_logs") - op.drop_index(op.f("ix_admin_audit_logs_id"), table_name="admin_audit_logs") - op.drop_index( - op.f("ix_admin_audit_logs_admin_user_id"), table_name="admin_audit_logs" - ) - op.drop_index(op.f("ix_admin_audit_logs_action"), table_name="admin_audit_logs") - op.drop_table("admin_audit_logs") - op.drop_index(op.f("ix_users_username"), table_name="users") - op.drop_index(op.f("ix_users_id"), table_name="users") - op.drop_index(op.f("ix_users_email"), table_name="users") - op.drop_table("users") - op.drop_index( - op.f("ix_marketplace_products_vendor_name"), table_name="marketplace_products" - ) - op.drop_index( - op.f("ix_marketplace_products_marketplace_product_id"), - table_name="marketplace_products", - ) - op.drop_index( - op.f("ix_marketplace_products_marketplace"), table_name="marketplace_products" - ) - op.drop_index(op.f("ix_marketplace_products_id"), table_name="marketplace_products") - op.drop_index( - op.f("ix_marketplace_products_gtin"), table_name="marketplace_products" - ) - op.drop_index( - op.f("ix_marketplace_products_google_product_category"), - table_name="marketplace_products", - ) - op.drop_index( - op.f("ix_marketplace_products_brand"), table_name="marketplace_products" - ) - op.drop_index( - op.f("ix_marketplace_products_availability"), table_name="marketplace_products" - ) - op.drop_index("idx_marketplace_vendor", table_name="marketplace_products") - op.drop_index("idx_marketplace_brand", table_name="marketplace_products") - op.drop_table("marketplace_products") - # ### end Alembic commands ### diff --git a/alembic/versions/55b92e155566_add_order_tracking_fields.py b/alembic/versions/55b92e155566_add_order_tracking_fields.py deleted file mode 100644 index 8e80682d..00000000 --- a/alembic/versions/55b92e155566_add_order_tracking_fields.py +++ /dev/null @@ -1,31 +0,0 @@ -"""add_order_tracking_fields - -Revision ID: 55b92e155566 -Revises: d2e3f4a5b6c7 -Create Date: 2025-12-20 18:07:51.144136 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '55b92e155566' -down_revision: Union[str, None] = 'd2e3f4a5b6c7' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Add new tracking fields to orders table - op.add_column('orders', sa.Column('tracking_url', sa.String(length=500), nullable=True)) - op.add_column('orders', sa.Column('shipment_number', sa.String(length=100), nullable=True)) - op.add_column('orders', sa.Column('shipping_carrier', sa.String(length=50), nullable=True)) - - -def downgrade() -> None: - op.drop_column('orders', 'shipping_carrier') - op.drop_column('orders', 'shipment_number') - op.drop_column('orders', 'tracking_url') diff --git a/alembic/versions/5818330181a5_make_vendor_owner_user_id_nullable_for_.py b/alembic/versions/5818330181a5_make_vendor_owner_user_id_nullable_for_.py deleted file mode 100644 index cb3a74bc..00000000 --- a/alembic/versions/5818330181a5_make_vendor_owner_user_id_nullable_for_.py +++ /dev/null @@ -1,48 +0,0 @@ -"""make_vendor_owner_user_id_nullable_for_company_ownership - -Revision ID: 5818330181a5 -Revises: d0325d7c0f25 -Create Date: 2025-12-01 20:30:06.158027 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '5818330181a5' -down_revision: Union[str, None] = 'd0325d7c0f25' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """ - Make vendor.owner_user_id nullable to support company-level ownership. - - Architecture Change: - - OLD: Each vendor has its own owner (vendor.owner_user_id) - - NEW: Vendors belong to a company, company has one owner (company.owner_user_id) - - This allows one company owner to manage multiple vendor brands. - """ - # Use batch operations for SQLite compatibility - with op.batch_alter_table('vendors', schema=None) as batch_op: - batch_op.alter_column('owner_user_id', - existing_type=sa.INTEGER(), - nullable=True) - - -def downgrade() -> None: - """ - Revert vendor.owner_user_id to non-nullable. - - WARNING: This will fail if there are vendors without owner_user_id! - """ - # Use batch operations for SQLite compatibility - with op.batch_alter_table('vendors', schema=None) as batch_op: - batch_op.alter_column('owner_user_id', - existing_type=sa.INTEGER(), - nullable=False) diff --git a/alembic/versions/72aa309d4007_ensure_content_pages_table_with_all_.py b/alembic/versions/72aa309d4007_ensure_content_pages_table_with_all_.py deleted file mode 100644 index 24a6d43d..00000000 --- a/alembic/versions/72aa309d4007_ensure_content_pages_table_with_all_.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Ensure content_pages table with all columns - -Revision ID: 72aa309d4007 -Revises: fef1d20ce8b4 -Create Date: 2025-11-22 15:16:13.213613 - -""" - -from typing import Sequence, Union - -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "72aa309d4007" -down_revision: Union[str, None] = "fef1d20ce8b4" -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( - "content_pages", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=True), - sa.Column("slug", sa.String(length=100), nullable=False), - sa.Column("title", sa.String(length=200), nullable=False), - sa.Column("content", sa.Text(), nullable=False), - sa.Column("content_format", sa.String(length=20), nullable=True), - sa.Column("meta_description", sa.String(length=300), nullable=True), - sa.Column("meta_keywords", sa.String(length=300), nullable=True), - sa.Column("is_published", sa.Boolean(), nullable=False), - sa.Column("published_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("display_order", sa.Integer(), nullable=True), - sa.Column("show_in_footer", sa.Boolean(), nullable=True), - sa.Column("show_in_header", sa.Boolean(), nullable=True), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), - sa.Column("created_by", sa.Integer(), nullable=True), - sa.Column("updated_by", sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(["created_by"], ["users.id"], ondelete="SET NULL"), - sa.ForeignKeyConstraint(["updated_by"], ["users.id"], ondelete="SET NULL"), - sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("vendor_id", "slug", name="uq_vendor_slug"), - ) - op.create_index( - "idx_slug_published", "content_pages", ["slug", "is_published"], unique=False - ) - op.create_index( - "idx_vendor_published", - "content_pages", - ["vendor_id", "is_published"], - unique=False, - ) - op.create_index(op.f("ix_content_pages_id"), "content_pages", ["id"], unique=False) - op.create_index( - op.f("ix_content_pages_slug"), "content_pages", ["slug"], unique=False - ) - op.create_index( - op.f("ix_content_pages_vendor_id"), "content_pages", ["vendor_id"], unique=False - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f("ix_content_pages_vendor_id"), table_name="content_pages") - op.drop_index(op.f("ix_content_pages_slug"), table_name="content_pages") - op.drop_index(op.f("ix_content_pages_id"), table_name="content_pages") - op.drop_index("idx_vendor_published", table_name="content_pages") - op.drop_index("idx_slug_published", table_name="content_pages") - op.drop_table("content_pages") - # ### end Alembic commands ### diff --git a/alembic/versions/7a7ce92593d5_add_architecture_quality_tracking_tables.py b/alembic/versions/7a7ce92593d5_add_architecture_quality_tracking_tables.py deleted file mode 100644 index f5e60325..00000000 --- a/alembic/versions/7a7ce92593d5_add_architecture_quality_tracking_tables.py +++ /dev/null @@ -1,291 +0,0 @@ -"""add_architecture_quality_tracking_tables - -Revision ID: 7a7ce92593d5 -Revises: a2064e1dfcd4 -Create Date: 2025-11-28 09:21:16.545203 - -""" - -from typing import Sequence, Union - -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "7a7ce92593d5" -down_revision: Union[str, None] = "a2064e1dfcd4" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Create architecture_scans table - op.create_table( - "architecture_scans", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column( - "timestamp", - sa.DateTime(timezone=True), - server_default=sa.text("CURRENT_TIMESTAMP"), - nullable=False, - ), - sa.Column("total_files", sa.Integer(), nullable=True), - sa.Column("total_violations", sa.Integer(), nullable=True), - sa.Column("errors", sa.Integer(), nullable=True), - sa.Column("warnings", sa.Integer(), nullable=True), - sa.Column("duration_seconds", sa.Float(), nullable=True), - sa.Column("triggered_by", sa.String(length=100), nullable=True), - sa.Column("git_commit_hash", sa.String(length=40), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_architecture_scans_id"), "architecture_scans", ["id"], unique=False - ) - op.create_index( - op.f("ix_architecture_scans_timestamp"), - "architecture_scans", - ["timestamp"], - unique=False, - ) - - # Create architecture_rules table - op.create_table( - "architecture_rules", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("rule_id", sa.String(length=20), nullable=False), - sa.Column("category", sa.String(length=50), nullable=False), - sa.Column("name", sa.String(length=200), nullable=False), - sa.Column("description", sa.Text(), nullable=True), - sa.Column("severity", sa.String(length=10), nullable=False), - sa.Column("enabled", sa.Boolean(), nullable=False, server_default="1"), - sa.Column("custom_config", sa.JSON(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.text("CURRENT_TIMESTAMP"), - nullable=False, - ), - sa.Column( - "updated_at", - sa.DateTime(timezone=True), - server_default=sa.text("CURRENT_TIMESTAMP"), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("rule_id"), - ) - op.create_index( - op.f("ix_architecture_rules_id"), "architecture_rules", ["id"], unique=False - ) - op.create_index( - op.f("ix_architecture_rules_rule_id"), - "architecture_rules", - ["rule_id"], - unique=True, - ) - - # Create architecture_violations table - op.create_table( - "architecture_violations", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("scan_id", sa.Integer(), nullable=False), - sa.Column("rule_id", sa.String(length=20), nullable=False), - sa.Column("rule_name", sa.String(length=200), nullable=False), - sa.Column("severity", sa.String(length=10), nullable=False), - sa.Column("file_path", sa.String(length=500), nullable=False), - sa.Column("line_number", sa.Integer(), nullable=False), - sa.Column("message", sa.Text(), nullable=False), - sa.Column("context", sa.Text(), nullable=True), - sa.Column("suggestion", sa.Text(), nullable=True), - sa.Column("status", sa.String(length=20), server_default="open", nullable=True), - sa.Column("assigned_to", sa.Integer(), nullable=True), - sa.Column("resolved_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("resolved_by", sa.Integer(), nullable=True), - sa.Column("resolution_note", sa.Text(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.text("CURRENT_TIMESTAMP"), - nullable=False, - ), - sa.ForeignKeyConstraint( - ["assigned_to"], - ["users.id"], - ), - sa.ForeignKeyConstraint( - ["resolved_by"], - ["users.id"], - ), - sa.ForeignKeyConstraint( - ["scan_id"], - ["architecture_scans.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_architecture_violations_file_path"), - "architecture_violations", - ["file_path"], - unique=False, - ) - op.create_index( - op.f("ix_architecture_violations_id"), - "architecture_violations", - ["id"], - unique=False, - ) - op.create_index( - op.f("ix_architecture_violations_rule_id"), - "architecture_violations", - ["rule_id"], - unique=False, - ) - op.create_index( - op.f("ix_architecture_violations_scan_id"), - "architecture_violations", - ["scan_id"], - unique=False, - ) - op.create_index( - op.f("ix_architecture_violations_severity"), - "architecture_violations", - ["severity"], - unique=False, - ) - op.create_index( - op.f("ix_architecture_violations_status"), - "architecture_violations", - ["status"], - unique=False, - ) - - # Create violation_assignments table - op.create_table( - "violation_assignments", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("violation_id", sa.Integer(), nullable=False), - sa.Column("user_id", sa.Integer(), nullable=False), - sa.Column( - "assigned_at", - sa.DateTime(timezone=True), - server_default=sa.text("CURRENT_TIMESTAMP"), - nullable=False, - ), - sa.Column("assigned_by", sa.Integer(), nullable=True), - sa.Column("due_date", sa.DateTime(timezone=True), nullable=True), - sa.Column( - "priority", sa.String(length=10), server_default="medium", nullable=True - ), - sa.ForeignKeyConstraint( - ["assigned_by"], - ["users.id"], - ), - sa.ForeignKeyConstraint( - ["user_id"], - ["users.id"], - ), - sa.ForeignKeyConstraint( - ["violation_id"], - ["architecture_violations.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_violation_assignments_id"), - "violation_assignments", - ["id"], - unique=False, - ) - op.create_index( - op.f("ix_violation_assignments_violation_id"), - "violation_assignments", - ["violation_id"], - unique=False, - ) - - # Create violation_comments table - op.create_table( - "violation_comments", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("violation_id", sa.Integer(), nullable=False), - sa.Column("user_id", sa.Integer(), nullable=False), - sa.Column("comment", sa.Text(), nullable=False), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.text("CURRENT_TIMESTAMP"), - nullable=False, - ), - sa.ForeignKeyConstraint( - ["user_id"], - ["users.id"], - ), - sa.ForeignKeyConstraint( - ["violation_id"], - ["architecture_violations.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_violation_comments_id"), "violation_comments", ["id"], unique=False - ) - op.create_index( - op.f("ix_violation_comments_violation_id"), - "violation_comments", - ["violation_id"], - unique=False, - ) - - -def downgrade() -> None: - # Drop tables in reverse order (to respect foreign key constraints) - op.drop_index( - op.f("ix_violation_comments_violation_id"), table_name="violation_comments" - ) - op.drop_index(op.f("ix_violation_comments_id"), table_name="violation_comments") - op.drop_table("violation_comments") - - op.drop_index( - op.f("ix_violation_assignments_violation_id"), - table_name="violation_assignments", - ) - op.drop_index( - op.f("ix_violation_assignments_id"), table_name="violation_assignments" - ) - op.drop_table("violation_assignments") - - op.drop_index( - op.f("ix_architecture_violations_status"), table_name="architecture_violations" - ) - op.drop_index( - op.f("ix_architecture_violations_severity"), - table_name="architecture_violations", - ) - op.drop_index( - op.f("ix_architecture_violations_scan_id"), table_name="architecture_violations" - ) - op.drop_index( - op.f("ix_architecture_violations_rule_id"), table_name="architecture_violations" - ) - op.drop_index( - op.f("ix_architecture_violations_id"), table_name="architecture_violations" - ) - op.drop_index( - op.f("ix_architecture_violations_file_path"), - table_name="architecture_violations", - ) - op.drop_table("architecture_violations") - - op.drop_index( - op.f("ix_architecture_rules_rule_id"), table_name="architecture_rules" - ) - op.drop_index(op.f("ix_architecture_rules_id"), table_name="architecture_rules") - op.drop_table("architecture_rules") - - op.drop_index( - op.f("ix_architecture_scans_timestamp"), table_name="architecture_scans" - ) - op.drop_index(op.f("ix_architecture_scans_id"), table_name="architecture_scans") - op.drop_table("architecture_scans") diff --git a/alembic/versions/82ea1b4a3ccb_add_test_run_tables.py b/alembic/versions/82ea1b4a3ccb_add_test_run_tables.py deleted file mode 100644 index 2c9f6986..00000000 --- a/alembic/versions/82ea1b4a3ccb_add_test_run_tables.py +++ /dev/null @@ -1,103 +0,0 @@ -"""add_test_run_tables - -Revision ID: 82ea1b4a3ccb -Revises: b4c5d6e7f8a9 -Create Date: 2025-12-12 22:48:09.501172 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '82ea1b4a3ccb' -down_revision: Union[str, None] = 'b4c5d6e7f8a9' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Create test_collections table - op.create_table('test_collections', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('total_tests', sa.Integer(), nullable=True), - sa.Column('total_files', sa.Integer(), nullable=True), - sa.Column('total_classes', sa.Integer(), nullable=True), - sa.Column('unit_tests', sa.Integer(), nullable=True), - sa.Column('integration_tests', sa.Integer(), nullable=True), - sa.Column('performance_tests', sa.Integer(), nullable=True), - sa.Column('system_tests', sa.Integer(), nullable=True), - sa.Column('test_files', sa.JSON(), nullable=True), - sa.Column('collected_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_test_collections_id'), 'test_collections', ['id'], unique=False) - - # Create test_runs table - op.create_table('test_runs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('timestamp', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('total_tests', sa.Integer(), nullable=True), - sa.Column('passed', sa.Integer(), nullable=True), - sa.Column('failed', sa.Integer(), nullable=True), - sa.Column('errors', sa.Integer(), nullable=True), - sa.Column('skipped', sa.Integer(), nullable=True), - sa.Column('xfailed', sa.Integer(), nullable=True), - sa.Column('xpassed', sa.Integer(), nullable=True), - sa.Column('coverage_percent', sa.Float(), nullable=True), - sa.Column('duration_seconds', sa.Float(), nullable=True), - sa.Column('triggered_by', sa.String(length=100), nullable=True), - sa.Column('git_commit_hash', sa.String(length=40), nullable=True), - sa.Column('git_branch', sa.String(length=100), nullable=True), - sa.Column('test_path', sa.String(length=500), nullable=True), - sa.Column('pytest_args', sa.String(length=500), nullable=True), - sa.Column('status', sa.String(length=20), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_test_runs_id'), 'test_runs', ['id'], unique=False) - op.create_index(op.f('ix_test_runs_status'), 'test_runs', ['status'], unique=False) - op.create_index(op.f('ix_test_runs_timestamp'), 'test_runs', ['timestamp'], unique=False) - - # Create test_results table - op.create_table('test_results', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('run_id', sa.Integer(), nullable=False), - sa.Column('node_id', sa.String(length=500), nullable=False), - sa.Column('test_name', sa.String(length=200), nullable=False), - sa.Column('test_file', sa.String(length=300), nullable=False), - sa.Column('test_class', sa.String(length=200), nullable=True), - sa.Column('outcome', sa.String(length=20), nullable=False), - sa.Column('duration_seconds', sa.Float(), nullable=True), - sa.Column('error_message', sa.Text(), nullable=True), - sa.Column('traceback', sa.Text(), nullable=True), - sa.Column('markers', sa.JSON(), nullable=True), - sa.Column('parameters', sa.JSON(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.ForeignKeyConstraint(['run_id'], ['test_runs.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_test_results_id'), 'test_results', ['id'], unique=False) - op.create_index(op.f('ix_test_results_node_id'), 'test_results', ['node_id'], unique=False) - op.create_index(op.f('ix_test_results_outcome'), 'test_results', ['outcome'], unique=False) - op.create_index(op.f('ix_test_results_run_id'), 'test_results', ['run_id'], unique=False) - - -def downgrade() -> None: - # Drop test_results table first (has foreign key to test_runs) - op.drop_index(op.f('ix_test_results_run_id'), table_name='test_results') - op.drop_index(op.f('ix_test_results_outcome'), table_name='test_results') - op.drop_index(op.f('ix_test_results_node_id'), table_name='test_results') - op.drop_index(op.f('ix_test_results_id'), table_name='test_results') - op.drop_table('test_results') - - # Drop test_runs table - op.drop_index(op.f('ix_test_runs_timestamp'), table_name='test_runs') - op.drop_index(op.f('ix_test_runs_status'), table_name='test_runs') - op.drop_index(op.f('ix_test_runs_id'), table_name='test_runs') - op.drop_table('test_runs') - - # Drop test_collections table - op.drop_index(op.f('ix_test_collections_id'), table_name='test_collections') - op.drop_table('test_collections') diff --git a/alembic/versions/91d02647efae_add_marketplace_import_errors_table.py b/alembic/versions/91d02647efae_add_marketplace_import_errors_table.py deleted file mode 100644 index 29db8b03..00000000 --- a/alembic/versions/91d02647efae_add_marketplace_import_errors_table.py +++ /dev/null @@ -1,44 +0,0 @@ -"""add marketplace import errors table - -Revision ID: 91d02647efae -Revises: 987b4ecfa503 -Create Date: 2025-12-13 13:13:46.969503 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - -# revision identifiers, used by Alembic. -revision: str = '91d02647efae' -down_revision: Union[str, None] = '987b4ecfa503' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Create marketplace_import_errors table to store detailed import error information - op.create_table('marketplace_import_errors', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('import_job_id', sa.Integer(), nullable=False), - sa.Column('row_number', sa.Integer(), nullable=False), - sa.Column('identifier', sa.String(), nullable=True), - sa.Column('error_type', sa.String(length=50), nullable=False), - sa.Column('error_message', sa.Text(), nullable=False), - sa.Column('row_data', sa.JSON(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['import_job_id'], ['marketplace_import_jobs.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_index('idx_import_error_job_id', 'marketplace_import_errors', ['import_job_id'], unique=False) - op.create_index('idx_import_error_type', 'marketplace_import_errors', ['error_type'], unique=False) - op.create_index(op.f('ix_marketplace_import_errors_id'), 'marketplace_import_errors', ['id'], unique=False) - - -def downgrade() -> None: - op.drop_index(op.f('ix_marketplace_import_errors_id'), table_name='marketplace_import_errors') - op.drop_index('idx_import_error_type', table_name='marketplace_import_errors') - op.drop_index('idx_import_error_job_id', table_name='marketplace_import_errors') - op.drop_table('marketplace_import_errors') diff --git a/alembic/versions/987b4ecfa503_add_letzshop_integration_tables.py b/alembic/versions/987b4ecfa503_add_letzshop_integration_tables.py deleted file mode 100644 index 54605a38..00000000 --- a/alembic/versions/987b4ecfa503_add_letzshop_integration_tables.py +++ /dev/null @@ -1,179 +0,0 @@ -"""add_letzshop_integration_tables - -Revision ID: 987b4ecfa503 -Revises: 82ea1b4a3ccb -Create Date: 2025-12-13 - -This migration adds: -- vendor_letzshop_credentials: Per-vendor encrypted API key storage -- letzshop_orders: Track imported orders with external IDs -- letzshop_fulfillment_queue: Queue outbound operations with retry -- letzshop_sync_logs: Audit trail for sync operations -- Adds channel fields to orders table for multi-marketplace support -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '987b4ecfa503' -down_revision: Union[str, None] = '82ea1b4a3ccb' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Add channel fields to orders table - op.add_column('orders', sa.Column('channel', sa.String(length=50), nullable=True, server_default='direct')) - op.add_column('orders', sa.Column('external_order_id', sa.String(length=100), nullable=True)) - op.add_column('orders', sa.Column('external_channel_data', sa.JSON(), nullable=True)) - op.create_index(op.f('ix_orders_channel'), 'orders', ['channel'], unique=False) - op.create_index(op.f('ix_orders_external_order_id'), 'orders', ['external_order_id'], unique=False) - - # Create vendor_letzshop_credentials table - op.create_table('vendor_letzshop_credentials', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('vendor_id', sa.Integer(), nullable=False), - sa.Column('api_key_encrypted', sa.Text(), nullable=False), - sa.Column('api_endpoint', sa.String(length=255), server_default='https://letzshop.lu/graphql', nullable=True), - sa.Column('auto_sync_enabled', sa.Boolean(), server_default='0', nullable=True), - sa.Column('sync_interval_minutes', sa.Integer(), server_default='15', nullable=True), - sa.Column('last_sync_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('last_sync_status', sa.String(length=50), nullable=True), - sa.Column('last_sync_error', sa.Text(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('vendor_id') - ) - op.create_index(op.f('ix_vendor_letzshop_credentials_id'), 'vendor_letzshop_credentials', ['id'], unique=False) - op.create_index(op.f('ix_vendor_letzshop_credentials_vendor_id'), 'vendor_letzshop_credentials', ['vendor_id'], unique=True) - - # Create letzshop_orders table - op.create_table('letzshop_orders', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('vendor_id', sa.Integer(), nullable=False), - sa.Column('letzshop_order_id', sa.String(length=100), nullable=False), - sa.Column('letzshop_shipment_id', sa.String(length=100), nullable=True), - sa.Column('letzshop_order_number', sa.String(length=100), nullable=True), - sa.Column('local_order_id', sa.Integer(), nullable=True), - sa.Column('letzshop_state', sa.String(length=50), nullable=True), - sa.Column('customer_email', sa.String(length=255), nullable=True), - sa.Column('customer_name', sa.String(length=255), nullable=True), - sa.Column('total_amount', sa.String(length=50), nullable=True), - sa.Column('currency', sa.String(length=10), server_default='EUR', nullable=True), - sa.Column('raw_order_data', sa.JSON(), nullable=True), - sa.Column('inventory_units', sa.JSON(), nullable=True), - sa.Column('sync_status', sa.String(length=50), server_default='pending', nullable=True), - sa.Column('last_synced_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('sync_error', sa.Text(), nullable=True), - sa.Column('confirmed_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('rejected_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('tracking_set_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('tracking_number', sa.String(length=100), nullable=True), - sa.Column('tracking_carrier', sa.String(length=100), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.ForeignKeyConstraint(['local_order_id'], ['orders.id'], ), - sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_letzshop_orders_id'), 'letzshop_orders', ['id'], unique=False) - op.create_index(op.f('ix_letzshop_orders_letzshop_order_id'), 'letzshop_orders', ['letzshop_order_id'], unique=False) - op.create_index(op.f('ix_letzshop_orders_letzshop_shipment_id'), 'letzshop_orders', ['letzshop_shipment_id'], unique=False) - op.create_index(op.f('ix_letzshop_orders_vendor_id'), 'letzshop_orders', ['vendor_id'], unique=False) - op.create_index('idx_letzshop_order_vendor', 'letzshop_orders', ['vendor_id', 'letzshop_order_id'], unique=False) - op.create_index('idx_letzshop_order_state', 'letzshop_orders', ['vendor_id', 'letzshop_state'], unique=False) - op.create_index('idx_letzshop_order_sync', 'letzshop_orders', ['vendor_id', 'sync_status'], unique=False) - - # Create letzshop_fulfillment_queue table - op.create_table('letzshop_fulfillment_queue', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('vendor_id', sa.Integer(), nullable=False), - sa.Column('letzshop_order_id', sa.Integer(), nullable=False), - sa.Column('operation', sa.String(length=50), nullable=False), - sa.Column('payload', sa.JSON(), nullable=False), - sa.Column('status', sa.String(length=50), server_default='pending', nullable=True), - sa.Column('attempts', sa.Integer(), server_default='0', nullable=True), - sa.Column('max_attempts', sa.Integer(), server_default='3', nullable=True), - sa.Column('last_attempt_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('next_retry_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('error_message', sa.Text(), nullable=True), - sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('response_data', sa.JSON(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.ForeignKeyConstraint(['letzshop_order_id'], ['letzshop_orders.id'], ), - sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_letzshop_fulfillment_queue_id'), 'letzshop_fulfillment_queue', ['id'], unique=False) - op.create_index(op.f('ix_letzshop_fulfillment_queue_vendor_id'), 'letzshop_fulfillment_queue', ['vendor_id'], unique=False) - op.create_index('idx_fulfillment_queue_status', 'letzshop_fulfillment_queue', ['status', 'vendor_id'], unique=False) - op.create_index('idx_fulfillment_queue_retry', 'letzshop_fulfillment_queue', ['status', 'next_retry_at'], unique=False) - - # Create letzshop_sync_logs table - op.create_table('letzshop_sync_logs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('vendor_id', sa.Integer(), nullable=False), - sa.Column('operation_type', sa.String(length=50), nullable=False), - sa.Column('direction', sa.String(length=10), nullable=False), - sa.Column('status', sa.String(length=50), nullable=False), - sa.Column('records_processed', sa.Integer(), server_default='0', nullable=True), - sa.Column('records_succeeded', sa.Integer(), server_default='0', nullable=True), - sa.Column('records_failed', sa.Integer(), server_default='0', nullable=True), - sa.Column('error_details', sa.JSON(), nullable=True), - sa.Column('started_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('duration_seconds', sa.Integer(), nullable=True), - sa.Column('triggered_by', sa.String(length=100), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_letzshop_sync_logs_id'), 'letzshop_sync_logs', ['id'], unique=False) - op.create_index(op.f('ix_letzshop_sync_logs_vendor_id'), 'letzshop_sync_logs', ['vendor_id'], unique=False) - op.create_index('idx_sync_log_vendor_type', 'letzshop_sync_logs', ['vendor_id', 'operation_type'], unique=False) - op.create_index('idx_sync_log_vendor_date', 'letzshop_sync_logs', ['vendor_id', 'started_at'], unique=False) - - -def downgrade() -> None: - # Drop letzshop_sync_logs table - op.drop_index('idx_sync_log_vendor_date', table_name='letzshop_sync_logs') - op.drop_index('idx_sync_log_vendor_type', table_name='letzshop_sync_logs') - op.drop_index(op.f('ix_letzshop_sync_logs_vendor_id'), table_name='letzshop_sync_logs') - op.drop_index(op.f('ix_letzshop_sync_logs_id'), table_name='letzshop_sync_logs') - op.drop_table('letzshop_sync_logs') - - # Drop letzshop_fulfillment_queue table - op.drop_index('idx_fulfillment_queue_retry', table_name='letzshop_fulfillment_queue') - op.drop_index('idx_fulfillment_queue_status', table_name='letzshop_fulfillment_queue') - op.drop_index(op.f('ix_letzshop_fulfillment_queue_vendor_id'), table_name='letzshop_fulfillment_queue') - op.drop_index(op.f('ix_letzshop_fulfillment_queue_id'), table_name='letzshop_fulfillment_queue') - op.drop_table('letzshop_fulfillment_queue') - - # Drop letzshop_orders table - op.drop_index('idx_letzshop_order_sync', table_name='letzshop_orders') - op.drop_index('idx_letzshop_order_state', table_name='letzshop_orders') - op.drop_index('idx_letzshop_order_vendor', table_name='letzshop_orders') - op.drop_index(op.f('ix_letzshop_orders_vendor_id'), table_name='letzshop_orders') - op.drop_index(op.f('ix_letzshop_orders_letzshop_shipment_id'), table_name='letzshop_orders') - op.drop_index(op.f('ix_letzshop_orders_letzshop_order_id'), table_name='letzshop_orders') - op.drop_index(op.f('ix_letzshop_orders_id'), table_name='letzshop_orders') - op.drop_table('letzshop_orders') - - # Drop vendor_letzshop_credentials table - op.drop_index(op.f('ix_vendor_letzshop_credentials_vendor_id'), table_name='vendor_letzshop_credentials') - op.drop_index(op.f('ix_vendor_letzshop_credentials_id'), table_name='vendor_letzshop_credentials') - op.drop_table('vendor_letzshop_credentials') - - # Drop channel fields from orders table - op.drop_index(op.f('ix_orders_external_order_id'), table_name='orders') - op.drop_index(op.f('ix_orders_channel'), table_name='orders') - op.drop_column('orders', 'external_channel_data') - op.drop_column('orders', 'external_order_id') - op.drop_column('orders', 'channel') diff --git a/alembic/versions/9f3a25ea4991_remove_vendor_owner_user_id_column.py b/alembic/versions/9f3a25ea4991_remove_vendor_owner_user_id_column.py deleted file mode 100644 index 98c9ffa6..00000000 --- a/alembic/versions/9f3a25ea4991_remove_vendor_owner_user_id_column.py +++ /dev/null @@ -1,60 +0,0 @@ -"""remove_vendor_owner_user_id_column - -Revision ID: 9f3a25ea4991 -Revises: 5818330181a5 -Create Date: 2025-12-02 17:58:45.663338 - -This migration removes the owner_user_id column from the vendors table. - -Architecture Change: -- OLD: Each vendor had its own owner (vendor.owner_user_id) -- NEW: Vendors belong to a company, company has one owner (company.owner_user_id) - -The vendor ownership is now determined via the company relationship: -- vendor.company.owner_user_id contains the owner -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '9f3a25ea4991' -down_revision: Union[str, None] = '5818330181a5' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """ - Remove owner_user_id column from vendors table. - - Ownership is now determined via the company relationship. - - Note: SQLite batch mode recreates the table without the column, - so we don't need to explicitly drop constraints. - """ - with op.batch_alter_table('vendors', schema=None) as batch_op: - # Drop the column - batch mode handles constraints automatically - batch_op.drop_column('owner_user_id') - - -def downgrade() -> None: - """ - Re-add owner_user_id column to vendors table. - - WARNING: This will add the column back but NOT restore the data. - You will need to manually populate owner_user_id from company.owner_user_id - if reverting this migration. - """ - with op.batch_alter_table('vendors', schema=None) as batch_op: - batch_op.add_column( - sa.Column('owner_user_id', sa.Integer(), nullable=True) - ) - batch_op.create_foreign_key( - 'vendors_owner_user_id_fkey', - 'users', - ['owner_user_id'], - ['id'] - ) diff --git a/alembic/versions/a2064e1dfcd4_add_cart_items_table.py b/alembic/versions/a2064e1dfcd4_add_cart_items_table.py deleted file mode 100644 index 3eca3ff3..00000000 --- a/alembic/versions/a2064e1dfcd4_add_cart_items_table.py +++ /dev/null @@ -1,67 +0,0 @@ -"""add cart_items table - -Revision ID: a2064e1dfcd4 -Revises: f68d8da5315a -Create Date: 2025-11-23 19:52:40.509538 - -""" - -from typing import Sequence, Union - -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "a2064e1dfcd4" -down_revision: Union[str, None] = "f68d8da5315a" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Create cart_items table - op.create_table( - "cart_items", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=False), - sa.Column("product_id", sa.Integer(), nullable=False), - sa.Column("session_id", sa.String(length=255), nullable=False), - sa.Column("quantity", sa.Integer(), nullable=False), - sa.Column("price_at_add", sa.Float(), nullable=False), - sa.Column("created_at", sa.DateTime(), nullable=True), - sa.Column("updated_at", sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint( - ["product_id"], - ["products.id"], - ), - sa.ForeignKeyConstraint( - ["vendor_id"], - ["vendors.id"], - ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint( - "vendor_id", "session_id", "product_id", name="uq_cart_item" - ), - ) - - # Create indexes - op.create_index( - "idx_cart_session", "cart_items", ["vendor_id", "session_id"], unique=False - ) - op.create_index("idx_cart_created", "cart_items", ["created_at"], unique=False) - op.create_index(op.f("ix_cart_items_id"), "cart_items", ["id"], unique=False) - op.create_index( - op.f("ix_cart_items_session_id"), "cart_items", ["session_id"], unique=False - ) - - -def downgrade() -> None: - # Drop indexes - op.drop_index(op.f("ix_cart_items_session_id"), table_name="cart_items") - op.drop_index(op.f("ix_cart_items_id"), table_name="cart_items") - op.drop_index("idx_cart_created", table_name="cart_items") - op.drop_index("idx_cart_session", table_name="cart_items") - - # Drop table - op.drop_table("cart_items") diff --git a/alembic/versions/a3b4c5d6e7f8_add_product_override_fields.py b/alembic/versions/a3b4c5d6e7f8_add_product_override_fields.py deleted file mode 100644 index 2f402609..00000000 --- a/alembic/versions/a3b4c5d6e7f8_add_product_override_fields.py +++ /dev/null @@ -1,128 +0,0 @@ -"""Add override fields to products table - -Revision ID: a3b4c5d6e7f8 -Revises: f2b3c4d5e6f7 -Create Date: 2025-12-11 - -This migration: -- Renames 'product_id' to 'vendor_sku' for clarity -- Adds new override fields (brand, images, digital delivery) -- Adds vendor-specific digital fulfillment fields -- Changes relationship from one-to-one to one-to-many (same marketplace product - can be in multiple vendor catalogs) - -The override pattern: NULL value means "inherit from marketplace_product". -Setting a value creates a vendor-specific override. -""" - -from typing import Sequence, Union - -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "a3b4c5d6e7f8" -down_revision: Union[str, None] = "f2b3c4d5e6f7" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Use batch mode for SQLite compatibility - with op.batch_alter_table("products", schema=None) as batch_op: - # Rename product_id to vendor_sku for clarity - batch_op.alter_column( - "product_id", - new_column_name="vendor_sku", - ) - - # Add new override fields - op.add_column( - "products", - sa.Column("brand", sa.String(), nullable=True), - ) - op.add_column( - "products", - sa.Column("primary_image_url", sa.String(), nullable=True), - ) - op.add_column( - "products", - sa.Column("additional_images", sa.JSON(), nullable=True), - ) - - # Add digital product override fields - op.add_column( - "products", - sa.Column("download_url", sa.String(), nullable=True), - ) - op.add_column( - "products", - sa.Column("license_type", sa.String(50), nullable=True), - ) - - # Add vendor-specific digital fulfillment settings - op.add_column( - "products", - sa.Column("fulfillment_email_template", sa.String(), nullable=True), - ) - - # Add supplier tracking (for products sourced from CodesWholesale, etc.) - op.add_column( - "products", - sa.Column("supplier", sa.String(50), nullable=True), - ) - op.add_column( - "products", - sa.Column("supplier_product_id", sa.String(), nullable=True), - ) - op.add_column( - "products", - sa.Column("supplier_cost", sa.Float(), nullable=True), - ) - - # Add margin/markup tracking - op.add_column( - "products", - sa.Column("margin_percent", sa.Float(), nullable=True), - ) - - # Create index for vendor_sku - op.create_index( - "idx_product_vendor_sku", - "products", - ["vendor_id", "vendor_sku"], - ) - - # Create index for supplier queries - op.create_index( - "idx_product_supplier", - "products", - ["supplier", "supplier_product_id"], - ) - - -def downgrade() -> None: - # Drop indexes - op.drop_index("idx_product_supplier", table_name="products") - op.drop_index("idx_product_vendor_sku", table_name="products") - - # Drop new columns - op.drop_column("products", "margin_percent") - op.drop_column("products", "supplier_cost") - op.drop_column("products", "supplier_product_id") - op.drop_column("products", "supplier") - op.drop_column("products", "fulfillment_email_template") - op.drop_column("products", "license_type") - op.drop_column("products", "download_url") - op.drop_column("products", "additional_images") - op.drop_column("products", "primary_image_url") - op.drop_column("products", "brand") - - # Use batch mode for SQLite compatibility - with op.batch_alter_table("products", schema=None) as batch_op: - # Rename vendor_sku back to product_id - batch_op.alter_column( - "vendor_sku", - new_column_name="product_id", - ) diff --git a/alembic/versions/a9a86cef6cca_add_letzshop_order_locale_and_country_.py b/alembic/versions/a9a86cef6cca_add_letzshop_order_locale_and_country_.py deleted file mode 100644 index 731d6ae6..00000000 --- a/alembic/versions/a9a86cef6cca_add_letzshop_order_locale_and_country_.py +++ /dev/null @@ -1,31 +0,0 @@ -"""add_letzshop_order_locale_and_country_fields - -Revision ID: a9a86cef6cca -Revises: fcfdc02d5138 -Create Date: 2025-12-17 20:55:41.477848 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'a9a86cef6cca' -down_revision: Union[str, None] = 'fcfdc02d5138' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Add new columns to letzshop_orders for customer locale and country - op.add_column('letzshop_orders', sa.Column('customer_locale', sa.String(length=10), nullable=True)) - op.add_column('letzshop_orders', sa.Column('shipping_country_iso', sa.String(length=5), nullable=True)) - op.add_column('letzshop_orders', sa.Column('billing_country_iso', sa.String(length=5), nullable=True)) - - -def downgrade() -> None: - op.drop_column('letzshop_orders', 'billing_country_iso') - op.drop_column('letzshop_orders', 'shipping_country_iso') - op.drop_column('letzshop_orders', 'customer_locale') diff --git a/alembic/versions/b412e0b49c2e_add_language_column_to_marketplace_.py b/alembic/versions/b412e0b49c2e_add_language_column_to_marketplace_.py deleted file mode 100644 index 1c1ca208..00000000 --- a/alembic/versions/b412e0b49c2e_add_language_column_to_marketplace_.py +++ /dev/null @@ -1,30 +0,0 @@ -"""add language column to marketplace_import_jobs - -Revision ID: b412e0b49c2e -Revises: 91d02647efae -Create Date: 2025-12-13 13:35:46.524893 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'b412e0b49c2e' -down_revision: Union[str, None] = '91d02647efae' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Add language column with default value for existing rows - op.add_column( - 'marketplace_import_jobs', - sa.Column('language', sa.String(length=5), nullable=False, server_default='en') - ) - - -def downgrade() -> None: - op.drop_column('marketplace_import_jobs', 'language') diff --git a/alembic/versions/b4c5d6e7f8a9_migrate_product_data_to_translations.py b/alembic/versions/b4c5d6e7f8a9_migrate_product_data_to_translations.py deleted file mode 100644 index a597de9e..00000000 --- a/alembic/versions/b4c5d6e7f8a9_migrate_product_data_to_translations.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Migrate existing product data to translation tables - -Revision ID: b4c5d6e7f8a9 -Revises: a3b4c5d6e7f8 -Create Date: 2025-12-11 - -This migration: -1. Copies existing title/description from marketplace_products to - marketplace_product_translations (default language: 'en') -2. Parses existing price strings to numeric values -3. Removes the old title/description columns from marketplace_products - -Since we're not live yet, we can safely remove the old columns -after migrating the data to the new structure. -""" - -import re -from typing import Sequence, Union - -import sqlalchemy as sa -from sqlalchemy import text - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "b4c5d6e7f8a9" -down_revision: Union[str, None] = "a3b4c5d6e7f8" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def parse_price(price_str: str) -> float | None: - """Parse price string like '19.99 EUR' to float.""" - if not price_str: - return None - - # Extract numeric value - numbers = re.findall(r"[\d.,]+", str(price_str)) - if numbers: - num_str = numbers[0].replace(",", ".") - try: - return float(num_str) - except ValueError: - pass - return None - - -def upgrade() -> None: - conn = op.get_bind() - - # Step 1: Migrate existing title/description to translations table - # Default language is 'en' for existing data - conn.execute( - text(""" - INSERT INTO marketplace_product_translations - (marketplace_product_id, language, title, description, created_at, updated_at) - SELECT - id, - 'en', - title, - description, - created_at, - updated_at - FROM marketplace_products - WHERE title IS NOT NULL - """) - ) - - # Step 2: Parse prices to numeric values - # Get all marketplace products with prices - result = conn.execute( - text("SELECT id, price, sale_price FROM marketplace_products") - ) - - for row in result: - price_numeric = parse_price(row.price) if row.price else None - sale_price_numeric = parse_price(row.sale_price) if row.sale_price else None - - if price_numeric is not None or sale_price_numeric is not None: - conn.execute( - text(""" - UPDATE marketplace_products - SET price_numeric = :price_numeric, - sale_price_numeric = :sale_price_numeric - WHERE id = :id - """), - { - "id": row.id, - "price_numeric": price_numeric, - "sale_price_numeric": sale_price_numeric, - }, - ) - - # Step 3: Since we're not live, remove the old title/description columns - # from marketplace_products (data is now in translations table) - op.drop_column("marketplace_products", "title") - op.drop_column("marketplace_products", "description") - - -def downgrade() -> None: - # Re-add title and description columns - op.add_column( - "marketplace_products", - sa.Column("title", sa.String(), nullable=True), - ) - op.add_column( - "marketplace_products", - sa.Column("description", sa.String(), nullable=True), - ) - - # Copy data back from translations (only 'en' translations) - conn = op.get_bind() - conn.execute( - text(""" - UPDATE marketplace_products - SET title = ( - SELECT title FROM marketplace_product_translations - WHERE marketplace_product_translations.marketplace_product_id = marketplace_products.id - AND marketplace_product_translations.language = 'en' - ), - description = ( - SELECT description FROM marketplace_product_translations - WHERE marketplace_product_translations.marketplace_product_id = marketplace_products.id - AND marketplace_product_translations.language = 'en' - ) - """) - ) - - # Delete the migrated translations - conn.execute( - text("DELETE FROM marketplace_product_translations WHERE language = 'en'") - ) diff --git a/alembic/versions/ba2c0ce78396_add_show_in_legal_to_content_pages.py b/alembic/versions/ba2c0ce78396_add_show_in_legal_to_content_pages.py deleted file mode 100644 index bd241679..00000000 --- a/alembic/versions/ba2c0ce78396_add_show_in_legal_to_content_pages.py +++ /dev/null @@ -1,41 +0,0 @@ -"""add show_in_legal to content_pages - -Revision ID: ba2c0ce78396 -Revises: m1b2c3d4e5f6 -Create Date: 2025-12-28 20:00:24.263518 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'ba2c0ce78396' -down_revision: Union[str, None] = 'm1b2c3d4e5f6' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Add show_in_legal column to content_pages table. - - This column controls whether a page appears in the bottom bar - alongside the copyright notice (e.g., Privacy Policy, Terms of Service). - """ - op.add_column( - 'content_pages', - sa.Column('show_in_legal', sa.Boolean(), nullable=True, default=False) - ) - - # Set default value for existing rows (PostgreSQL uses true/false for boolean) - op.execute("UPDATE content_pages SET show_in_legal = false WHERE show_in_legal IS NULL") - - # Set privacy and terms pages to show in legal by default - op.execute("UPDATE content_pages SET show_in_legal = true WHERE slug IN ('privacy', 'terms')") - - -def downgrade() -> None: - """Remove show_in_legal column from content_pages table.""" - op.drop_column('content_pages', 'show_in_legal') diff --git a/alembic/versions/c00d2985701f_add_letzshop_credentials_carrier_fields.py b/alembic/versions/c00d2985701f_add_letzshop_credentials_carrier_fields.py deleted file mode 100644 index c4a9bab2..00000000 --- a/alembic/versions/c00d2985701f_add_letzshop_credentials_carrier_fields.py +++ /dev/null @@ -1,35 +0,0 @@ -"""add_letzshop_credentials_carrier_fields - -Revision ID: c00d2985701f -Revises: 55b92e155566 -Create Date: 2025-12-20 18:49:53.432904 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'c00d2985701f' -down_revision: Union[str, None] = '55b92e155566' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Add carrier settings and test mode to vendor_letzshop_credentials - op.add_column('vendor_letzshop_credentials', sa.Column('test_mode_enabled', sa.Boolean(), nullable=True, server_default='0')) - op.add_column('vendor_letzshop_credentials', sa.Column('default_carrier', sa.String(length=50), nullable=True)) - op.add_column('vendor_letzshop_credentials', sa.Column('carrier_greco_label_url', sa.String(length=500), nullable=True, server_default='https://dispatchweb.fr/Tracky/Home/')) - op.add_column('vendor_letzshop_credentials', sa.Column('carrier_colissimo_label_url', sa.String(length=500), nullable=True)) - op.add_column('vendor_letzshop_credentials', sa.Column('carrier_xpresslogistics_label_url', sa.String(length=500), nullable=True)) - - -def downgrade() -> None: - op.drop_column('vendor_letzshop_credentials', 'carrier_xpresslogistics_label_url') - op.drop_column('vendor_letzshop_credentials', 'carrier_colissimo_label_url') - op.drop_column('vendor_letzshop_credentials', 'carrier_greco_label_url') - op.drop_column('vendor_letzshop_credentials', 'default_carrier') - op.drop_column('vendor_letzshop_credentials', 'test_mode_enabled') diff --git a/alembic/versions/c1d2e3f4a5b6_unified_order_schema.py b/alembic/versions/c1d2e3f4a5b6_unified_order_schema.py deleted file mode 100644 index 0f70e8fb..00000000 --- a/alembic/versions/c1d2e3f4a5b6_unified_order_schema.py +++ /dev/null @@ -1,452 +0,0 @@ -"""unified_order_schema - -Revision ID: c1d2e3f4a5b6 -Revises: 2362c2723a93 -Create Date: 2025-12-19 - -This migration implements the unified order schema: -- Removes the separate letzshop_orders table -- Enhances the orders table with: - - Customer/address snapshots (preserved at order time) - - External marketplace references - - Tracking provider field -- Enhances order_items with: - - GTIN fields - - External item references - - Item state for marketplace confirmation flow -- Updates letzshop_fulfillment_queue to reference orders table directly - -Design principles: -- Single orders table for all channels (direct, letzshop, etc.) -- Customer/address data snapshotted at order time -- Products must exist in catalog (enforced by FK) -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy import inspect - - -# revision identifiers, used by Alembic. -revision: str = 'c1d2e3f4a5b6' -down_revision: Union[str, None] = '2362c2723a93' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def table_exists(table_name: str) -> bool: - """Check if a table exists in the database.""" - bind = op.get_bind() - inspector = inspect(bind) - return table_name in inspector.get_table_names() - - -def index_exists(index_name: str, table_name: str) -> bool: - """Check if an index exists on a table.""" - bind = op.get_bind() - inspector = inspect(bind) - try: - indexes = inspector.get_indexes(table_name) - return any(idx['name'] == index_name for idx in indexes) - except Exception: - return False - - -def safe_drop_index(index_name: str, table_name: str) -> None: - """Drop an index if it exists.""" - if index_exists(index_name, table_name): - op.drop_index(index_name, table_name=table_name) - - -def safe_drop_table(table_name: str) -> None: - """Drop a table if it exists.""" - if table_exists(table_name): - op.drop_table(table_name) - - -def upgrade() -> None: - # ========================================================================= - # Step 1: Drop old tables that will be replaced (if they exist) - # ========================================================================= - - # Drop letzshop_fulfillment_queue (references letzshop_orders) - if table_exists('letzshop_fulfillment_queue'): - safe_drop_index('idx_fulfillment_queue_retry', 'letzshop_fulfillment_queue') - safe_drop_index('idx_fulfillment_queue_status', 'letzshop_fulfillment_queue') - safe_drop_index('ix_letzshop_fulfillment_queue_vendor_id', 'letzshop_fulfillment_queue') - safe_drop_index('ix_letzshop_fulfillment_queue_id', 'letzshop_fulfillment_queue') - op.drop_table('letzshop_fulfillment_queue') - - # Drop letzshop_orders table (replaced by unified orders) - if table_exists('letzshop_orders'): - safe_drop_index('idx_letzshop_order_sync', 'letzshop_orders') - safe_drop_index('idx_letzshop_order_state', 'letzshop_orders') - safe_drop_index('idx_letzshop_order_vendor', 'letzshop_orders') - safe_drop_index('ix_letzshop_orders_vendor_id', 'letzshop_orders') - safe_drop_index('ix_letzshop_orders_letzshop_shipment_id', 'letzshop_orders') - safe_drop_index('ix_letzshop_orders_letzshop_order_id', 'letzshop_orders') - safe_drop_index('ix_letzshop_orders_id', 'letzshop_orders') - op.drop_table('letzshop_orders') - - # Drop order_items (references orders) - if table_exists('order_items'): - safe_drop_index('ix_order_items_id', 'order_items') - safe_drop_index('ix_order_items_order_id', 'order_items') - op.drop_table('order_items') - - # Drop old orders table - if table_exists('orders'): - safe_drop_index('ix_orders_external_order_id', 'orders') - safe_drop_index('ix_orders_channel', 'orders') - safe_drop_index('ix_orders_vendor_id', 'orders') - safe_drop_index('ix_orders_status', 'orders') - safe_drop_index('ix_orders_order_number', 'orders') - safe_drop_index('ix_orders_id', 'orders') - safe_drop_index('ix_orders_customer_id', 'orders') - op.drop_table('orders') - - # ========================================================================= - # Step 2: Create new unified orders table - # ========================================================================= - op.create_table('orders', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('vendor_id', sa.Integer(), nullable=False), - sa.Column('customer_id', sa.Integer(), nullable=False), - sa.Column('order_number', sa.String(length=100), nullable=False), - - # Channel/Source - sa.Column('channel', sa.String(length=50), nullable=False, server_default='direct'), - - # External references (for marketplace orders) - sa.Column('external_order_id', sa.String(length=100), nullable=True), - sa.Column('external_shipment_id', sa.String(length=100), nullable=True), - sa.Column('external_order_number', sa.String(length=100), nullable=True), - sa.Column('external_data', sa.JSON(), nullable=True), - - # Status - sa.Column('status', sa.String(length=50), nullable=False, server_default='pending'), - - # Financials - sa.Column('subtotal', sa.Float(), nullable=True), - sa.Column('tax_amount', sa.Float(), nullable=True), - sa.Column('shipping_amount', sa.Float(), nullable=True), - sa.Column('discount_amount', sa.Float(), nullable=True), - sa.Column('total_amount', sa.Float(), nullable=False), - sa.Column('currency', sa.String(length=10), server_default='EUR', nullable=True), - - # Customer snapshot - sa.Column('customer_first_name', sa.String(length=100), nullable=False), - sa.Column('customer_last_name', sa.String(length=100), nullable=False), - sa.Column('customer_email', sa.String(length=255), nullable=False), - sa.Column('customer_phone', sa.String(length=50), nullable=True), - sa.Column('customer_locale', sa.String(length=10), nullable=True), - - # Shipping address snapshot - sa.Column('ship_first_name', sa.String(length=100), nullable=False), - sa.Column('ship_last_name', sa.String(length=100), nullable=False), - sa.Column('ship_company', sa.String(length=200), nullable=True), - sa.Column('ship_address_line_1', sa.String(length=255), nullable=False), - sa.Column('ship_address_line_2', sa.String(length=255), nullable=True), - sa.Column('ship_city', sa.String(length=100), nullable=False), - sa.Column('ship_postal_code', sa.String(length=20), nullable=False), - sa.Column('ship_country_iso', sa.String(length=5), nullable=False), - - # Billing address snapshot - sa.Column('bill_first_name', sa.String(length=100), nullable=False), - sa.Column('bill_last_name', sa.String(length=100), nullable=False), - sa.Column('bill_company', sa.String(length=200), nullable=True), - sa.Column('bill_address_line_1', sa.String(length=255), nullable=False), - sa.Column('bill_address_line_2', sa.String(length=255), nullable=True), - sa.Column('bill_city', sa.String(length=100), nullable=False), - sa.Column('bill_postal_code', sa.String(length=20), nullable=False), - sa.Column('bill_country_iso', sa.String(length=5), nullable=False), - - # Tracking - sa.Column('shipping_method', sa.String(length=100), nullable=True), - sa.Column('tracking_number', sa.String(length=100), nullable=True), - sa.Column('tracking_provider', sa.String(length=100), nullable=True), - - # Notes - sa.Column('customer_notes', sa.Text(), nullable=True), - sa.Column('internal_notes', sa.Text(), nullable=True), - - # Timestamps - sa.Column('order_date', sa.DateTime(timezone=True), nullable=False), - sa.Column('confirmed_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('shipped_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('delivered_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('cancelled_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - - # Foreign keys - sa.ForeignKeyConstraint(['customer_id'], ['customers.id']), - sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id']), - sa.PrimaryKeyConstraint('id') - ) - - # Indexes for orders - op.create_index(op.f('ix_orders_id'), 'orders', ['id'], unique=False) - op.create_index(op.f('ix_orders_vendor_id'), 'orders', ['vendor_id'], unique=False) - op.create_index(op.f('ix_orders_customer_id'), 'orders', ['customer_id'], unique=False) - op.create_index(op.f('ix_orders_order_number'), 'orders', ['order_number'], unique=True) - op.create_index(op.f('ix_orders_channel'), 'orders', ['channel'], unique=False) - op.create_index(op.f('ix_orders_status'), 'orders', ['status'], unique=False) - op.create_index(op.f('ix_orders_external_order_id'), 'orders', ['external_order_id'], unique=False) - op.create_index(op.f('ix_orders_external_shipment_id'), 'orders', ['external_shipment_id'], unique=False) - op.create_index('idx_order_vendor_status', 'orders', ['vendor_id', 'status'], unique=False) - op.create_index('idx_order_vendor_channel', 'orders', ['vendor_id', 'channel'], unique=False) - op.create_index('idx_order_vendor_date', 'orders', ['vendor_id', 'order_date'], unique=False) - - # ========================================================================= - # Step 3: Create new order_items table - # ========================================================================= - op.create_table('order_items', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('order_id', sa.Integer(), nullable=False), - sa.Column('product_id', sa.Integer(), nullable=False), - - # Product snapshot - sa.Column('product_name', sa.String(length=255), nullable=False), - sa.Column('product_sku', sa.String(length=100), nullable=True), - sa.Column('gtin', sa.String(length=50), nullable=True), - sa.Column('gtin_type', sa.String(length=20), nullable=True), - - # Pricing - sa.Column('quantity', sa.Integer(), nullable=False), - sa.Column('unit_price', sa.Float(), nullable=False), - sa.Column('total_price', sa.Float(), nullable=False), - - # External references (for marketplace items) - sa.Column('external_item_id', sa.String(length=100), nullable=True), - sa.Column('external_variant_id', sa.String(length=100), nullable=True), - - # Item state (for marketplace confirmation flow) - sa.Column('item_state', sa.String(length=50), nullable=True), - - # Inventory tracking - sa.Column('inventory_reserved', sa.Boolean(), server_default='0', nullable=True), - sa.Column('inventory_fulfilled', sa.Boolean(), server_default='0', nullable=True), - - # Timestamps - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - - # Foreign keys - sa.ForeignKeyConstraint(['order_id'], ['orders.id']), - sa.ForeignKeyConstraint(['product_id'], ['products.id']), - sa.PrimaryKeyConstraint('id') - ) - - # Indexes for order_items - op.create_index(op.f('ix_order_items_id'), 'order_items', ['id'], unique=False) - op.create_index(op.f('ix_order_items_order_id'), 'order_items', ['order_id'], unique=False) - op.create_index(op.f('ix_order_items_product_id'), 'order_items', ['product_id'], unique=False) - op.create_index(op.f('ix_order_items_gtin'), 'order_items', ['gtin'], unique=False) - - # ========================================================================= - # Step 4: Create updated letzshop_fulfillment_queue (references orders) - # ========================================================================= - op.create_table('letzshop_fulfillment_queue', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('vendor_id', sa.Integer(), nullable=False), - sa.Column('order_id', sa.Integer(), nullable=False), - - # Operation type - sa.Column('operation', sa.String(length=50), nullable=False), - - # Operation payload - sa.Column('payload', sa.JSON(), nullable=False), - - # Status and retry - sa.Column('status', sa.String(length=50), server_default='pending', nullable=True), - sa.Column('attempts', sa.Integer(), server_default='0', nullable=True), - sa.Column('max_attempts', sa.Integer(), server_default='3', nullable=True), - sa.Column('last_attempt_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('next_retry_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('error_message', sa.Text(), nullable=True), - sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), - - # Response from Letzshop - sa.Column('response_data', sa.JSON(), nullable=True), - - # Timestamps - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - - # Foreign keys - sa.ForeignKeyConstraint(['order_id'], ['orders.id']), - sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id']), - sa.PrimaryKeyConstraint('id') - ) - - # Indexes for letzshop_fulfillment_queue - op.create_index(op.f('ix_letzshop_fulfillment_queue_id'), 'letzshop_fulfillment_queue', ['id'], unique=False) - op.create_index(op.f('ix_letzshop_fulfillment_queue_vendor_id'), 'letzshop_fulfillment_queue', ['vendor_id'], unique=False) - op.create_index(op.f('ix_letzshop_fulfillment_queue_order_id'), 'letzshop_fulfillment_queue', ['order_id'], unique=False) - op.create_index('idx_fulfillment_queue_status', 'letzshop_fulfillment_queue', ['status', 'vendor_id'], unique=False) - op.create_index('idx_fulfillment_queue_retry', 'letzshop_fulfillment_queue', ['status', 'next_retry_at'], unique=False) - op.create_index('idx_fulfillment_queue_order', 'letzshop_fulfillment_queue', ['order_id'], unique=False) - - -def downgrade() -> None: - # Drop new letzshop_fulfillment_queue - safe_drop_index('idx_fulfillment_queue_order', 'letzshop_fulfillment_queue') - safe_drop_index('idx_fulfillment_queue_retry', 'letzshop_fulfillment_queue') - safe_drop_index('idx_fulfillment_queue_status', 'letzshop_fulfillment_queue') - safe_drop_index('ix_letzshop_fulfillment_queue_order_id', 'letzshop_fulfillment_queue') - safe_drop_index('ix_letzshop_fulfillment_queue_vendor_id', 'letzshop_fulfillment_queue') - safe_drop_index('ix_letzshop_fulfillment_queue_id', 'letzshop_fulfillment_queue') - safe_drop_table('letzshop_fulfillment_queue') - - # Drop new order_items - safe_drop_index('ix_order_items_gtin', 'order_items') - safe_drop_index('ix_order_items_product_id', 'order_items') - safe_drop_index('ix_order_items_order_id', 'order_items') - safe_drop_index('ix_order_items_id', 'order_items') - safe_drop_table('order_items') - - # Drop new orders - safe_drop_index('idx_order_vendor_date', 'orders') - safe_drop_index('idx_order_vendor_channel', 'orders') - safe_drop_index('idx_order_vendor_status', 'orders') - safe_drop_index('ix_orders_external_shipment_id', 'orders') - safe_drop_index('ix_orders_external_order_id', 'orders') - safe_drop_index('ix_orders_status', 'orders') - safe_drop_index('ix_orders_channel', 'orders') - safe_drop_index('ix_orders_order_number', 'orders') - safe_drop_index('ix_orders_customer_id', 'orders') - safe_drop_index('ix_orders_vendor_id', 'orders') - safe_drop_index('ix_orders_id', 'orders') - safe_drop_table('orders') - - # Recreate old orders table - op.create_table('orders', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('vendor_id', sa.Integer(), nullable=False), - sa.Column('customer_id', sa.Integer(), nullable=False), - sa.Column('order_number', sa.String(), nullable=False), - sa.Column('channel', sa.String(length=50), nullable=True, server_default='direct'), - sa.Column('external_order_id', sa.String(length=100), nullable=True), - sa.Column('external_channel_data', sa.JSON(), nullable=True), - sa.Column('status', sa.String(), nullable=False), - sa.Column('subtotal', sa.Float(), nullable=False), - sa.Column('tax_amount', sa.Float(), nullable=True), - sa.Column('shipping_amount', sa.Float(), nullable=True), - sa.Column('discount_amount', sa.Float(), nullable=True), - sa.Column('total_amount', sa.Float(), nullable=False), - sa.Column('currency', sa.String(), nullable=True), - sa.Column('shipping_address_id', sa.Integer(), nullable=False), - sa.Column('billing_address_id', sa.Integer(), nullable=False), - sa.Column('shipping_method', sa.String(), nullable=True), - sa.Column('tracking_number', sa.String(), nullable=True), - sa.Column('customer_notes', sa.Text(), nullable=True), - sa.Column('internal_notes', sa.Text(), nullable=True), - sa.Column('paid_at', sa.DateTime(), nullable=True), - sa.Column('shipped_at', sa.DateTime(), nullable=True), - sa.Column('delivered_at', sa.DateTime(), nullable=True), - sa.Column('cancelled_at', sa.DateTime(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['billing_address_id'], ['customer_addresses.id']), - sa.ForeignKeyConstraint(['customer_id'], ['customers.id']), - sa.ForeignKeyConstraint(['shipping_address_id'], ['customer_addresses.id']), - sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id']), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_orders_customer_id'), 'orders', ['customer_id'], unique=False) - op.create_index(op.f('ix_orders_id'), 'orders', ['id'], unique=False) - op.create_index(op.f('ix_orders_order_number'), 'orders', ['order_number'], unique=True) - op.create_index(op.f('ix_orders_status'), 'orders', ['status'], unique=False) - op.create_index(op.f('ix_orders_vendor_id'), 'orders', ['vendor_id'], unique=False) - op.create_index(op.f('ix_orders_channel'), 'orders', ['channel'], unique=False) - op.create_index(op.f('ix_orders_external_order_id'), 'orders', ['external_order_id'], unique=False) - - # Recreate old order_items table - op.create_table('order_items', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('order_id', sa.Integer(), nullable=False), - sa.Column('product_id', sa.Integer(), nullable=False), - sa.Column('product_name', sa.String(), nullable=False), - sa.Column('product_sku', sa.String(), nullable=True), - sa.Column('quantity', sa.Integer(), nullable=False), - sa.Column('unit_price', sa.Float(), nullable=False), - sa.Column('total_price', sa.Float(), nullable=False), - sa.Column('inventory_reserved', sa.Boolean(), nullable=True), - sa.Column('inventory_fulfilled', sa.Boolean(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['order_id'], ['orders.id']), - sa.ForeignKeyConstraint(['product_id'], ['products.id']), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_order_items_id'), 'order_items', ['id'], unique=False) - op.create_index(op.f('ix_order_items_order_id'), 'order_items', ['order_id'], unique=False) - - # Recreate old letzshop_orders table - op.create_table('letzshop_orders', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('vendor_id', sa.Integer(), nullable=False), - sa.Column('letzshop_order_id', sa.String(length=100), nullable=False), - sa.Column('letzshop_shipment_id', sa.String(length=100), nullable=True), - sa.Column('letzshop_order_number', sa.String(length=100), nullable=True), - sa.Column('local_order_id', sa.Integer(), nullable=True), - sa.Column('letzshop_state', sa.String(length=50), nullable=True), - sa.Column('customer_email', sa.String(length=255), nullable=True), - sa.Column('customer_name', sa.String(length=255), nullable=True), - sa.Column('total_amount', sa.String(length=50), nullable=True), - sa.Column('currency', sa.String(length=10), server_default='EUR', nullable=True), - sa.Column('customer_locale', sa.String(length=10), nullable=True), - sa.Column('shipping_country_iso', sa.String(length=5), nullable=True), - sa.Column('billing_country_iso', sa.String(length=5), nullable=True), - sa.Column('order_date', sa.DateTime(timezone=True), nullable=True), - sa.Column('raw_order_data', sa.JSON(), nullable=True), - sa.Column('inventory_units', sa.JSON(), nullable=True), - sa.Column('sync_status', sa.String(length=50), server_default='pending', nullable=True), - sa.Column('last_synced_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('sync_error', sa.Text(), nullable=True), - sa.Column('confirmed_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('rejected_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('tracking_set_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('tracking_number', sa.String(length=100), nullable=True), - sa.Column('tracking_carrier', sa.String(length=100), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.ForeignKeyConstraint(['local_order_id'], ['orders.id']), - sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id']), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_letzshop_orders_id'), 'letzshop_orders', ['id'], unique=False) - op.create_index(op.f('ix_letzshop_orders_letzshop_order_id'), 'letzshop_orders', ['letzshop_order_id'], unique=False) - op.create_index(op.f('ix_letzshop_orders_letzshop_shipment_id'), 'letzshop_orders', ['letzshop_shipment_id'], unique=False) - op.create_index(op.f('ix_letzshop_orders_vendor_id'), 'letzshop_orders', ['vendor_id'], unique=False) - op.create_index('idx_letzshop_order_vendor', 'letzshop_orders', ['vendor_id', 'letzshop_order_id'], unique=False) - op.create_index('idx_letzshop_order_state', 'letzshop_orders', ['vendor_id', 'letzshop_state'], unique=False) - op.create_index('idx_letzshop_order_sync', 'letzshop_orders', ['vendor_id', 'sync_status'], unique=False) - - # Recreate old letzshop_fulfillment_queue table - op.create_table('letzshop_fulfillment_queue', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('vendor_id', sa.Integer(), nullable=False), - sa.Column('letzshop_order_id', sa.Integer(), nullable=False), - sa.Column('operation', sa.String(length=50), nullable=False), - sa.Column('payload', sa.JSON(), nullable=False), - sa.Column('status', sa.String(length=50), server_default='pending', nullable=True), - sa.Column('attempts', sa.Integer(), server_default='0', nullable=True), - sa.Column('max_attempts', sa.Integer(), server_default='3', nullable=True), - sa.Column('last_attempt_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('next_retry_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('error_message', sa.Text(), nullable=True), - sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('response_data', sa.JSON(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.ForeignKeyConstraint(['letzshop_order_id'], ['letzshop_orders.id']), - sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id']), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_letzshop_fulfillment_queue_id'), 'letzshop_fulfillment_queue', ['id'], unique=False) - op.create_index(op.f('ix_letzshop_fulfillment_queue_vendor_id'), 'letzshop_fulfillment_queue', ['vendor_id'], unique=False) - op.create_index('idx_fulfillment_queue_status', 'letzshop_fulfillment_queue', ['status', 'vendor_id'], unique=False) - op.create_index('idx_fulfillment_queue_retry', 'letzshop_fulfillment_queue', ['status', 'next_retry_at'], unique=False) diff --git a/alembic/versions/c9e22eadf533_add_tax_rate_cost_and_letzshop_settings.py b/alembic/versions/c9e22eadf533_add_tax_rate_cost_and_letzshop_settings.py deleted file mode 100644 index 6f1dcc02..00000000 --- a/alembic/versions/c9e22eadf533_add_tax_rate_cost_and_letzshop_settings.py +++ /dev/null @@ -1,64 +0,0 @@ -"""add_tax_rate_cost_and_letzshop_settings - -Revision ID: c9e22eadf533 -Revises: e1f2a3b4c5d6 -Create Date: 2025-12-20 21:13:30.709696 - -Adds: -- tax_rate_percent to products and marketplace_products (NOT NULL, default 17) -- cost_cents to products (for profit calculation) -- Letzshop feed settings to vendors (tax_rate, boost_sort, delivery_method, preorder_days) -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'c9e22eadf533' -down_revision: Union[str, None] = 'e1f2a3b4c5d6' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # === MARKETPLACE PRODUCTS: Add tax_rate_percent === - with op.batch_alter_table('marketplace_products', schema=None) as batch_op: - batch_op.add_column(sa.Column('tax_rate_percent', sa.Integer(), nullable=False, server_default='17')) - - # === PRODUCTS: Add tax_rate_percent and cost_cents, rename supplier_cost_cents === - with op.batch_alter_table('products', schema=None) as batch_op: - batch_op.add_column(sa.Column('tax_rate_percent', sa.Integer(), nullable=False, server_default='17')) - batch_op.add_column(sa.Column('cost_cents', sa.Integer(), nullable=True)) - # Drop old supplier_cost_cents column (data migrated to cost_cents if needed) - try: - batch_op.drop_column('supplier_cost_cents') - except Exception: - pass # Column may not exist - - # === VENDORS: Add Letzshop feed settings === - with op.batch_alter_table('vendors', schema=None) as batch_op: - batch_op.add_column(sa.Column('letzshop_default_tax_rate', sa.Integer(), nullable=False, server_default='17')) - batch_op.add_column(sa.Column('letzshop_boost_sort', sa.String(length=10), nullable=True, server_default='5.0')) - batch_op.add_column(sa.Column('letzshop_delivery_method', sa.String(length=100), nullable=True, server_default='package_delivery')) - batch_op.add_column(sa.Column('letzshop_preorder_days', sa.Integer(), nullable=True, server_default='1')) - - -def downgrade() -> None: - # === VENDORS: Remove Letzshop feed settings === - with op.batch_alter_table('vendors', schema=None) as batch_op: - batch_op.drop_column('letzshop_preorder_days') - batch_op.drop_column('letzshop_delivery_method') - batch_op.drop_column('letzshop_boost_sort') - batch_op.drop_column('letzshop_default_tax_rate') - - # === PRODUCTS: Remove tax_rate_percent and cost_cents === - with op.batch_alter_table('products', schema=None) as batch_op: - batch_op.drop_column('cost_cents') - batch_op.drop_column('tax_rate_percent') - batch_op.add_column(sa.Column('supplier_cost_cents', sa.Integer(), nullable=True)) - - # === MARKETPLACE PRODUCTS: Remove tax_rate_percent === - with op.batch_alter_table('marketplace_products', schema=None) as batch_op: - batch_op.drop_column('tax_rate_percent') diff --git a/alembic/versions/cb88bc9b5f86_add_gtin_columns_to_product_table.py b/alembic/versions/cb88bc9b5f86_add_gtin_columns_to_product_table.py deleted file mode 100644 index faa6a494..00000000 --- a/alembic/versions/cb88bc9b5f86_add_gtin_columns_to_product_table.py +++ /dev/null @@ -1,37 +0,0 @@ -"""add_gtin_columns_to_product_table - -Revision ID: cb88bc9b5f86 -Revises: a9a86cef6cca -Create Date: 2025-12-18 20:54:55.185857 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'cb88bc9b5f86' -down_revision: Union[str, None] = 'a9a86cef6cca' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Add GTIN (EAN/UPC barcode) columns to products table for order EAN matching - # gtin: The barcode number (e.g., "0889698273022") - # gtin_type: The format type from Letzshop (e.g., "gtin13", "gtin14", "isbn13") - op.add_column('products', sa.Column('gtin', sa.String(length=50), nullable=True)) - op.add_column('products', sa.Column('gtin_type', sa.String(length=20), nullable=True)) - - # Add index for EAN lookups during order matching - op.create_index('idx_product_gtin', 'products', ['gtin'], unique=False) - op.create_index('idx_product_vendor_gtin', 'products', ['vendor_id', 'gtin'], unique=False) - - -def downgrade() -> None: - op.drop_index('idx_product_vendor_gtin', table_name='products') - op.drop_index('idx_product_gtin', table_name='products') - op.drop_column('products', 'gtin_type') - op.drop_column('products', 'gtin') diff --git a/alembic/versions/core_001_initial.py b/alembic/versions/core_001_initial.py new file mode 100644 index 00000000..2ad2d5e5 --- /dev/null +++ b/alembic/versions/core_001_initial.py @@ -0,0 +1,333 @@ +"""core initial - tenancy, platform, admin tables + +Revision ID: core_001 +Revises: +Create Date: 2026-02-07 +""" +from alembic import op +import sqlalchemy as sa + +revision = "core_001" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # --- platforms --- + op.create_table( + "platforms", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("code", sa.String(50), unique=True, nullable=False, index=True, comment="Unique platform identifier (e.g., 'oms', 'loyalty', 'sites')"), + sa.Column("name", sa.String(100), nullable=False, comment="Display name (e.g., 'Wizamart OMS')"), + sa.Column("description", sa.Text(), nullable=True, comment="Platform description for admin/marketing purposes"), + sa.Column("domain", sa.String(255), unique=True, nullable=True, index=True, comment="Production domain (e.g., 'oms.lu', 'loyalty.lu')"), + sa.Column("path_prefix", sa.String(50), unique=True, nullable=True, index=True, comment="Development path prefix (e.g., 'oms' for localhost:9999/oms/*)"), + sa.Column("logo", sa.String(500), nullable=True, comment="Logo URL for light mode"), + sa.Column("logo_dark", sa.String(500), nullable=True, comment="Logo URL for dark mode"), + sa.Column("favicon", sa.String(500), nullable=True, comment="Favicon URL"), + sa.Column("theme_config", sa.JSON(), nullable=True, server_default="{}", comment="Theme configuration (colors, fonts, etc.)"), + sa.Column("default_language", sa.String(5), nullable=False, server_default="fr", comment="Default language code (e.g., 'fr', 'en', 'de')"), + sa.Column("supported_languages", sa.JSON(), nullable=False, comment="List of supported language codes"), + sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true", comment="Whether the platform is active and accessible"), + sa.Column("is_public", sa.Boolean(), nullable=False, server_default="true", comment="Whether the platform is visible in public listings"), + sa.Column("settings", sa.JSON(), nullable=True, server_default="{}", comment="Platform-specific settings and feature flags"), + sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + ) + op.create_index("idx_platform_active", "platforms", ["is_active"]) + op.create_index("idx_platform_public", "platforms", ["is_public", "is_active"]) + + # --- users --- + op.create_table( + "users", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("email", sa.String(), unique=True, nullable=False, index=True), + sa.Column("username", sa.String(), unique=True, nullable=False, index=True), + sa.Column("first_name", sa.String(), nullable=True), + sa.Column("last_name", sa.String(), nullable=True), + sa.Column("hashed_password", sa.String(), nullable=False), + sa.Column("role", sa.String(), nullable=False, server_default="store"), + sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"), + sa.Column("is_email_verified", sa.Boolean(), nullable=False, server_default="false"), + sa.Column("is_super_admin", sa.Boolean(), nullable=False, server_default="false"), + sa.Column("preferred_language", sa.String(5), nullable=True), + sa.Column("last_login", sa.DateTime(), nullable=True), + sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + ) + + # --- merchants --- + op.create_table( + "merchants", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("name", sa.String(), nullable=False, index=True), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("owner_user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=False), + sa.Column("contact_email", sa.String(), nullable=False), + sa.Column("contact_phone", sa.String(), nullable=True), + sa.Column("website", sa.String(), nullable=True), + sa.Column("business_address", sa.Text(), nullable=True), + sa.Column("tax_number", sa.String(), nullable=True), + sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"), + sa.Column("is_verified", sa.Boolean(), nullable=False, server_default="false"), + sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + ) + + # --- stores --- + op.create_table( + "stores", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("merchant_id", sa.Integer(), sa.ForeignKey("merchants.id"), nullable=False, index=True), + sa.Column("store_code", sa.String(), unique=True, nullable=False, index=True), + sa.Column("subdomain", sa.String(100), unique=True, nullable=False, index=True), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("letzshop_csv_url_fr", sa.String(), nullable=True), + sa.Column("letzshop_csv_url_en", sa.String(), nullable=True), + sa.Column("letzshop_csv_url_de", sa.String(), nullable=True), + sa.Column("letzshop_store_id", sa.String(100), unique=True, nullable=True, index=True), + sa.Column("letzshop_store_slug", sa.String(200), nullable=True, index=True), + sa.Column("letzshop_default_tax_rate", sa.Integer(), nullable=False, server_default="17"), + sa.Column("letzshop_boost_sort", sa.String(10), nullable=True, server_default="5.0"), + sa.Column("letzshop_delivery_method", sa.String(100), nullable=True, server_default="package_delivery"), + sa.Column("letzshop_preorder_days", sa.Integer(), nullable=True, server_default="1"), + sa.Column("is_active", sa.Boolean(), nullable=True, server_default="true"), + sa.Column("is_verified", sa.Boolean(), nullable=True, server_default="false"), + sa.Column("contact_email", sa.String(255), nullable=True), + sa.Column("contact_phone", sa.String(50), nullable=True), + sa.Column("website", sa.String(255), nullable=True), + sa.Column("business_address", sa.Text(), nullable=True), + sa.Column("tax_number", sa.String(100), nullable=True), + sa.Column("default_language", sa.String(5), nullable=False, server_default="fr"), + sa.Column("dashboard_language", sa.String(5), nullable=False, server_default="fr"), + sa.Column("storefront_language", sa.String(5), nullable=False, server_default="fr"), + sa.Column("storefront_languages", sa.JSON(), nullable=False), + sa.Column("storefront_locale", sa.String(10), nullable=True), + sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + ) + + # --- roles --- + op.create_table( + "roles", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=False), + sa.Column("name", sa.String(100), nullable=False), + sa.Column("permissions", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + ) + + # --- store_users --- + op.create_table( + "store_users", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=False), + sa.Column("user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=False), + sa.Column("user_type", sa.String(), nullable=False, server_default="member"), + sa.Column("role_id", sa.Integer(), sa.ForeignKey("roles.id"), nullable=True), + sa.Column("invited_by", sa.Integer(), sa.ForeignKey("users.id"), nullable=True), + sa.Column("invitation_token", sa.String(), nullable=True, index=True), + sa.Column("invitation_sent_at", sa.DateTime(), nullable=True), + sa.Column("invitation_accepted_at", sa.DateTime(), nullable=True), + sa.Column("is_active", sa.Boolean(), nullable=False, server_default="false"), + sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + ) + + # --- store_domains --- + op.create_table( + "store_domains", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id", ondelete="CASCADE"), nullable=False), + sa.Column("domain", sa.String(255), unique=True, nullable=False, index=True), + sa.Column("is_primary", sa.Boolean(), nullable=False, server_default="false"), + sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"), + sa.Column("ssl_status", sa.String(50), nullable=True, server_default="pending"), + sa.Column("ssl_verified_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("verification_token", sa.String(100), unique=True, nullable=True), + sa.Column("is_verified", sa.Boolean(), nullable=False, server_default="false"), + sa.Column("verified_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.UniqueConstraint("store_id", "domain", name="uq_store_domain"), + ) + op.create_index("idx_domain_active", "store_domains", ["domain", "is_active"]) + op.create_index("idx_store_domain_primary", "store_domains", ["store_id", "is_primary"]) + + # --- admin_platforms --- + op.create_table( + "admin_platforms", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("user_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True, comment="Reference to the admin user"), + sa.Column("platform_id", sa.Integer(), sa.ForeignKey("platforms.id", ondelete="CASCADE"), nullable=False, index=True, comment="Reference to the platform"), + sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true", comment="Whether the admin assignment is active"), + sa.Column("assigned_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False, comment="When the admin was assigned to this platform"), + sa.Column("assigned_by_user_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True, comment="Super admin who made this assignment"), + sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.UniqueConstraint("user_id", "platform_id", name="uq_admin_platform"), + ) + op.create_index("idx_admin_platform_active", "admin_platforms", ["user_id", "platform_id", "is_active"]) + op.create_index("idx_admin_platform_user_active", "admin_platforms", ["user_id", "is_active"]) + + # --- platform_modules --- + op.create_table( + "platform_modules", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("platform_id", sa.Integer(), sa.ForeignKey("platforms.id", ondelete="CASCADE"), nullable=False, comment="Platform this module configuration belongs to"), + sa.Column("module_code", sa.String(50), nullable=False, comment="Module code (e.g., 'billing', 'inventory', 'orders')"), + sa.Column("is_enabled", sa.Boolean(), nullable=False, server_default="true", comment="Whether this module is currently enabled for the platform"), + sa.Column("enabled_at", sa.DateTime(timezone=True), nullable=True, comment="When the module was last enabled"), + sa.Column("enabled_by_user_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True, comment="User who enabled the module"), + sa.Column("disabled_at", sa.DateTime(timezone=True), nullable=True, comment="When the module was last disabled"), + sa.Column("disabled_by_user_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True, comment="User who disabled the module"), + sa.Column("config", sa.JSON(), nullable=False, server_default="{}", comment="Module-specific configuration for this platform"), + sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.UniqueConstraint("platform_id", "module_code", name="uq_platform_module"), + ) + op.create_index("idx_platform_module_platform_id", "platform_modules", ["platform_id"]) + op.create_index("idx_platform_module_code", "platform_modules", ["module_code"]) + op.create_index("idx_platform_module_enabled", "platform_modules", ["platform_id", "is_enabled"]) + + # --- admin_audit_logs --- + op.create_table( + "admin_audit_logs", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("admin_user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=False, index=True), + sa.Column("action", sa.String(100), nullable=False, index=True), + sa.Column("target_type", sa.String(50), nullable=False, index=True), + sa.Column("target_id", sa.String(100), nullable=False, index=True), + sa.Column("details", sa.JSON(), nullable=True), + sa.Column("ip_address", sa.String(45), nullable=True), + sa.Column("user_agent", sa.Text(), nullable=True), + sa.Column("request_id", sa.String(100), nullable=True), + sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + ) + + # --- admin_settings --- + op.create_table( + "admin_settings", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("key", sa.String(100), unique=True, nullable=False, index=True), + sa.Column("value", sa.Text(), nullable=False), + sa.Column("value_type", sa.String(20), nullable=True, server_default="string"), + sa.Column("category", sa.String(50), nullable=True, index=True), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("is_encrypted", sa.Boolean(), nullable=True, server_default="false"), + sa.Column("is_public", sa.Boolean(), nullable=True, server_default="false"), + sa.Column("last_modified_by_user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=True), + sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + ) + + # --- platform_alerts --- + op.create_table( + "platform_alerts", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("alert_type", sa.String(50), nullable=False, index=True), + sa.Column("severity", sa.String(20), nullable=False, index=True), + sa.Column("title", sa.String(200), nullable=False), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("affected_stores", sa.JSON(), nullable=True), + sa.Column("affected_systems", sa.JSON(), nullable=True), + sa.Column("is_resolved", sa.Boolean(), nullable=True, server_default="false", index=True), + sa.Column("resolved_at", sa.DateTime(), nullable=True), + sa.Column("resolved_by_user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=True), + sa.Column("resolution_notes", sa.Text(), nullable=True), + sa.Column("auto_generated", sa.Boolean(), nullable=True, server_default="true"), + sa.Column("occurrence_count", sa.Integer(), nullable=True, server_default="1"), + sa.Column("first_occurred_at", sa.DateTime(), nullable=False), + sa.Column("last_occurred_at", sa.DateTime(), nullable=False), + sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + ) + + # --- admin_sessions --- + op.create_table( + "admin_sessions", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("admin_user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=False, index=True), + sa.Column("session_token", sa.String(255), unique=True, nullable=False, index=True), + sa.Column("ip_address", sa.String(45), nullable=False), + sa.Column("user_agent", sa.Text(), nullable=True), + sa.Column("login_at", sa.DateTime(), nullable=False, index=True), + sa.Column("last_activity_at", sa.DateTime(), nullable=False), + sa.Column("logout_at", sa.DateTime(), nullable=True), + sa.Column("is_active", sa.Boolean(), nullable=True, server_default="true", index=True), + sa.Column("logout_reason", sa.String(50), nullable=True), + sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + ) + + # --- application_logs --- + op.create_table( + "application_logs", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("timestamp", sa.DateTime(), nullable=False, index=True), + sa.Column("level", sa.String(20), nullable=False, index=True), + sa.Column("logger_name", sa.String(200), nullable=False, index=True), + sa.Column("module", sa.String(200), nullable=True), + sa.Column("function_name", sa.String(100), nullable=True), + sa.Column("line_number", sa.Integer(), nullable=True), + sa.Column("message", sa.Text(), nullable=False), + sa.Column("exception_type", sa.String(200), nullable=True), + sa.Column("exception_message", sa.Text(), nullable=True), + sa.Column("stack_trace", sa.Text(), nullable=True), + sa.Column("request_id", sa.String(100), nullable=True, index=True), + sa.Column("user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=True, index=True), + sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=True, index=True), + sa.Column("context", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + ) + + # --- admin_menu_configs --- + op.create_table( + "admin_menu_configs", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("frontend_type", sa.Enum("platform", "admin", "store", "storefront", "merchant", name="frontendtype"), nullable=False, index=True, comment="Which frontend this config applies to (admin or store)"), + sa.Column("platform_id", sa.Integer(), sa.ForeignKey("platforms.id", ondelete="CASCADE"), nullable=True, index=True, comment="Platform scope - applies to users/stores of this platform"), + sa.Column("user_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=True, index=True, comment="User scope - applies to this specific super admin (admin frontend only)"), + sa.Column("menu_item_id", sa.String(50), nullable=False, index=True, comment="Menu item identifier from registry (e.g., 'products', 'inventory')"), + sa.Column("is_visible", sa.Boolean(), nullable=False, server_default="true", comment="Whether this menu item is visible (False = hidden)"), + sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.UniqueConstraint("frontend_type", "platform_id", "menu_item_id", name="uq_frontend_platform_menu_config"), + sa.UniqueConstraint("frontend_type", "user_id", "menu_item_id", name="uq_frontend_user_menu_config"), + sa.CheckConstraint( + "(platform_id IS NOT NULL AND user_id IS NULL) OR (platform_id IS NULL AND user_id IS NOT NULL)", + name="ck_admin_menu_config_scope", + ), + sa.CheckConstraint( + "(user_id IS NULL) OR (frontend_type = 'admin')", + name="ck_user_scope_admin_only", + ), + ) + op.create_index("idx_admin_menu_config_frontend_platform", "admin_menu_configs", ["frontend_type", "platform_id"]) + op.create_index("idx_admin_menu_config_frontend_user", "admin_menu_configs", ["frontend_type", "user_id"]) + op.create_index("idx_admin_menu_config_platform_visible", "admin_menu_configs", ["platform_id", "is_visible"]) + op.create_index("idx_admin_menu_config_user_visible", "admin_menu_configs", ["user_id", "is_visible"]) + + +def downgrade() -> None: + op.drop_table("admin_menu_configs") + op.drop_table("application_logs") + op.drop_table("admin_sessions") + op.drop_table("platform_alerts") + op.drop_table("admin_settings") + op.drop_table("admin_audit_logs") + op.drop_table("platform_modules") + op.drop_table("admin_platforms") + op.drop_table("store_domains") + op.drop_table("store_users") + op.drop_table("roles") + op.drop_table("stores") + op.drop_table("merchants") + op.drop_table("users") + op.drop_table("platforms") + sa.Enum(name="frontendtype").drop(op.get_bind(), checkfirst=True) diff --git a/alembic/versions/d0325d7c0f25_add_companies_table_and_restructure_.py b/alembic/versions/d0325d7c0f25_add_companies_table_and_restructure_.py deleted file mode 100644 index ceb0bba0..00000000 --- a/alembic/versions/d0325d7c0f25_add_companies_table_and_restructure_.py +++ /dev/null @@ -1,77 +0,0 @@ -"""add_companies_table_and_restructure_vendors - -Revision ID: d0325d7c0f25 -Revises: 0bd9ffaaced1 -Create Date: 2025-11-30 14:58:17.165142 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'd0325d7c0f25' -down_revision: Union[str, None] = '0bd9ffaaced1' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Create companies table - op.create_table( - 'companies', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('owner_user_id', sa.Integer(), nullable=False), - sa.Column('contact_email', sa.String(), nullable=False), - sa.Column('contact_phone', sa.String(), nullable=True), - sa.Column('website', sa.String(), nullable=True), - sa.Column('business_address', sa.Text(), nullable=True), - sa.Column('tax_number', sa.String(), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'), - sa.Column('is_verified', sa.Boolean(), nullable=False, server_default='false'), - sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.func.now()), - sa.Column('updated_at', sa.DateTime(), nullable=False, server_default=sa.func.now(), onupdate=sa.func.now()), - sa.ForeignKeyConstraint(['owner_user_id'], ['users.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_companies_id'), 'companies', ['id'], unique=False) - op.create_index(op.f('ix_companies_name'), 'companies', ['name'], unique=False) - - # Use batch mode for SQLite to modify vendors table - with op.batch_alter_table('vendors', schema=None) as batch_op: - # Add company_id column - batch_op.add_column(sa.Column('company_id', sa.Integer(), nullable=True)) - batch_op.create_index(batch_op.f('ix_vendors_company_id'), ['company_id'], unique=False) - batch_op.create_foreign_key('fk_vendors_company_id', 'companies', ['company_id'], ['id']) - - # Remove old contact fields - batch_op.drop_column('contact_email') - batch_op.drop_column('contact_phone') - batch_op.drop_column('website') - batch_op.drop_column('business_address') - batch_op.drop_column('tax_number') - - -def downgrade() -> None: - # Use batch mode for SQLite to modify vendors table - with op.batch_alter_table('vendors', schema=None) as batch_op: - # Re-add contact fields to vendors - batch_op.add_column(sa.Column('tax_number', sa.String(), nullable=True)) - batch_op.add_column(sa.Column('business_address', sa.Text(), nullable=True)) - batch_op.add_column(sa.Column('website', sa.String(), nullable=True)) - batch_op.add_column(sa.Column('contact_phone', sa.String(), nullable=True)) - batch_op.add_column(sa.Column('contact_email', sa.String(), nullable=True)) - - # Remove company_id from vendors - batch_op.drop_constraint('fk_vendors_company_id', type_='foreignkey') - batch_op.drop_index(batch_op.f('ix_vendors_company_id')) - batch_op.drop_column('company_id') - - # Drop companies table - op.drop_index(op.f('ix_companies_name'), table_name='companies') - op.drop_index(op.f('ix_companies_id'), table_name='companies') - op.drop_table('companies') diff --git a/alembic/versions/d2e3f4a5b6c7_add_order_item_exceptions.py b/alembic/versions/d2e3f4a5b6c7_add_order_item_exceptions.py deleted file mode 100644 index 3de56663..00000000 --- a/alembic/versions/d2e3f4a5b6c7_add_order_item_exceptions.py +++ /dev/null @@ -1,179 +0,0 @@ -"""add_order_item_exceptions - -Revision ID: d2e3f4a5b6c7 -Revises: c1d2e3f4a5b6 -Create Date: 2025-12-20 - -This migration adds the Order Item Exception system: -- Adds needs_product_match column to order_items table -- Creates order_item_exceptions table for tracking unmatched products - -The exception system allows marketplace orders to be imported even when -products are not found by GTIN. Items are linked to a placeholder product -and exceptions are tracked for QC resolution. -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy import inspect - - -# revision identifiers, used by Alembic. -revision: str = 'd2e3f4a5b6c7' -down_revision: Union[str, None] = 'c1d2e3f4a5b6' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def column_exists(table_name: str, column_name: str) -> bool: - """Check if a column exists in a table.""" - bind = op.get_bind() - inspector = inspect(bind) - columns = [col['name'] for col in inspector.get_columns(table_name)] - return column_name in columns - - -def table_exists(table_name: str) -> bool: - """Check if a table exists in the database.""" - bind = op.get_bind() - inspector = inspect(bind) - return table_name in inspector.get_table_names() - - -def index_exists(index_name: str, table_name: str) -> bool: - """Check if an index exists on a table.""" - bind = op.get_bind() - inspector = inspect(bind) - try: - indexes = inspector.get_indexes(table_name) - return any(idx['name'] == index_name for idx in indexes) - except Exception: - return False - - -def upgrade() -> None: - # ========================================================================= - # Step 1: Add needs_product_match column to order_items - # ========================================================================= - if not column_exists('order_items', 'needs_product_match'): - op.add_column( - 'order_items', - sa.Column( - 'needs_product_match', - sa.Boolean(), - server_default='0', - nullable=False - ) - ) - - if not index_exists('ix_order_items_needs_product_match', 'order_items'): - op.create_index( - 'ix_order_items_needs_product_match', - 'order_items', - ['needs_product_match'] - ) - - # ========================================================================= - # Step 2: Create order_item_exceptions table - # ========================================================================= - if not table_exists('order_item_exceptions'): - op.create_table( - 'order_item_exceptions', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('order_item_id', sa.Integer(), nullable=False), - sa.Column('vendor_id', sa.Integer(), nullable=False), - sa.Column('original_gtin', sa.String(length=50), nullable=True), - sa.Column('original_product_name', sa.String(length=500), nullable=True), - sa.Column('original_sku', sa.String(length=100), nullable=True), - sa.Column( - 'exception_type', - sa.String(length=50), - nullable=False, - server_default='product_not_found' - ), - sa.Column( - 'status', - sa.String(length=50), - nullable=False, - server_default='pending' - ), - sa.Column('resolved_product_id', sa.Integer(), nullable=True), - sa.Column('resolved_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('resolved_by', sa.Integer(), nullable=True), - sa.Column('resolution_notes', sa.Text(), nullable=True), - sa.Column( - 'created_at', - sa.DateTime(timezone=True), - server_default=sa.text('(CURRENT_TIMESTAMP)'), - nullable=False - ), - sa.Column( - 'updated_at', - sa.DateTime(timezone=True), - server_default=sa.text('(CURRENT_TIMESTAMP)'), - nullable=False - ), - sa.ForeignKeyConstraint( - ['order_item_id'], - ['order_items.id'], - ondelete='CASCADE' - ), - sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id']), - sa.ForeignKeyConstraint(['resolved_product_id'], ['products.id']), - sa.ForeignKeyConstraint(['resolved_by'], ['users.id']), - sa.PrimaryKeyConstraint('id') - ) - - # Create indexes - op.create_index( - 'ix_order_item_exceptions_id', - 'order_item_exceptions', - ['id'] - ) - op.create_index( - 'ix_order_item_exceptions_vendor_id', - 'order_item_exceptions', - ['vendor_id'] - ) - op.create_index( - 'ix_order_item_exceptions_status', - 'order_item_exceptions', - ['status'] - ) - op.create_index( - 'idx_exception_vendor_status', - 'order_item_exceptions', - ['vendor_id', 'status'] - ) - op.create_index( - 'idx_exception_gtin', - 'order_item_exceptions', - ['vendor_id', 'original_gtin'] - ) - - # Unique constraint on order_item_id (one exception per item) - op.create_index( - 'uq_order_item_exception', - 'order_item_exceptions', - ['order_item_id'], - unique=True - ) - - -def downgrade() -> None: - # Drop order_item_exceptions table - if table_exists('order_item_exceptions'): - op.drop_index('uq_order_item_exception', table_name='order_item_exceptions') - op.drop_index('idx_exception_gtin', table_name='order_item_exceptions') - op.drop_index('idx_exception_vendor_status', table_name='order_item_exceptions') - op.drop_index('ix_order_item_exceptions_status', table_name='order_item_exceptions') - op.drop_index('ix_order_item_exceptions_vendor_id', table_name='order_item_exceptions') - op.drop_index('ix_order_item_exceptions_id', table_name='order_item_exceptions') - op.drop_table('order_item_exceptions') - - # Remove needs_product_match column from order_items - if column_exists('order_items', 'needs_product_match'): - if index_exists('ix_order_items_needs_product_match', 'order_items'): - op.drop_index('ix_order_items_needs_product_match', table_name='order_items') - op.drop_column('order_items', 'needs_product_match') diff --git a/alembic/versions/d7a4a3f06394_add_email_templates_and_logs_tables.py b/alembic/versions/d7a4a3f06394_add_email_templates_and_logs_tables.py deleted file mode 100644 index e3dfe996..00000000 --- a/alembic/versions/d7a4a3f06394_add_email_templates_and_logs_tables.py +++ /dev/null @@ -1,332 +0,0 @@ -"""add email templates and logs tables - -Revision ID: d7a4a3f06394 -Revises: 404b3e2d2865 -Create Date: 2025-12-27 20:48:00.661523 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy import text - - -# revision identifiers, used by Alembic. -revision: str = 'd7a4a3f06394' -down_revision: Union[str, None] = '404b3e2d2865' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Create email_templates table - op.create_table('email_templates', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('code', sa.String(length=100), nullable=False), - sa.Column('language', sa.String(length=5), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('category', sa.String(length=50), nullable=False), - sa.Column('subject', sa.String(length=500), nullable=False), - sa.Column('body_html', sa.Text(), nullable=False), - sa.Column('body_text', sa.Text(), nullable=True), - sa.Column('variables', sa.Text(), nullable=True), - 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.PrimaryKeyConstraint('id'), - ) - op.create_index(op.f('ix_email_templates_category'), 'email_templates', ['category'], unique=False) - op.create_index(op.f('ix_email_templates_code'), 'email_templates', ['code'], unique=False) - op.create_index(op.f('ix_email_templates_id'), 'email_templates', ['id'], unique=False) - - # Create email_logs table - op.create_table('email_logs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('template_code', sa.String(length=100), nullable=True), - sa.Column('template_id', sa.Integer(), nullable=True), - sa.Column('recipient_email', sa.String(length=255), nullable=False), - sa.Column('recipient_name', sa.String(length=255), nullable=True), - sa.Column('subject', sa.String(length=500), nullable=False), - sa.Column('body_html', sa.Text(), nullable=True), - sa.Column('body_text', sa.Text(), nullable=True), - sa.Column('from_email', sa.String(length=255), nullable=False), - sa.Column('from_name', sa.String(length=255), nullable=True), - sa.Column('reply_to', sa.String(length=255), nullable=True), - sa.Column('status', sa.String(length=20), nullable=False), - sa.Column('sent_at', sa.DateTime(), nullable=True), - sa.Column('delivered_at', sa.DateTime(), nullable=True), - sa.Column('opened_at', sa.DateTime(), nullable=True), - sa.Column('clicked_at', sa.DateTime(), nullable=True), - sa.Column('error_message', sa.Text(), nullable=True), - sa.Column('retry_count', sa.Integer(), nullable=False), - sa.Column('provider', sa.String(length=50), nullable=True), - sa.Column('provider_message_id', sa.String(length=255), nullable=True), - sa.Column('vendor_id', sa.Integer(), nullable=True), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('related_type', sa.String(length=50), nullable=True), - sa.Column('related_id', sa.Integer(), nullable=True), - sa.Column('extra_data', sa.Text(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['template_id'], ['email_templates.id']), - sa.ForeignKeyConstraint(['user_id'], ['users.id']), - sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id']), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_email_logs_id'), 'email_logs', ['id'], unique=False) - op.create_index(op.f('ix_email_logs_provider_message_id'), 'email_logs', ['provider_message_id'], unique=False) - op.create_index(op.f('ix_email_logs_recipient_email'), 'email_logs', ['recipient_email'], unique=False) - op.create_index(op.f('ix_email_logs_status'), 'email_logs', ['status'], unique=False) - op.create_index(op.f('ix_email_logs_template_code'), 'email_logs', ['template_code'], unique=False) - op.create_index(op.f('ix_email_logs_user_id'), 'email_logs', ['user_id'], unique=False) - op.create_index(op.f('ix_email_logs_vendor_id'), 'email_logs', ['vendor_id'], unique=False) - - # application_logs - alter columns - op.alter_column('application_logs', 'created_at', existing_type=sa.DATETIME(), nullable=False) - op.alter_column('application_logs', 'updated_at', existing_type=sa.DATETIME(), nullable=False) - - # capacity_snapshots indexes (PostgreSQL IF EXISTS/IF NOT EXISTS) - op.execute(text("DROP INDEX IF EXISTS ix_capacity_snapshots_date")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_capacity_snapshots_date ON capacity_snapshots (snapshot_date)")) - op.execute(text("CREATE UNIQUE INDEX IF NOT EXISTS ix_capacity_snapshots_snapshot_date ON capacity_snapshots (snapshot_date)")) - - # cart_items - alter columns - op.alter_column('cart_items', 'created_at', existing_type=sa.DATETIME(), nullable=False) - op.alter_column('cart_items', 'updated_at', existing_type=sa.DATETIME(), nullable=False) - - # customer_addresses index rename - op.execute(text("DROP INDEX IF EXISTS ix_customers_addresses_id")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_customer_addresses_id ON customer_addresses (id)")) - - # inventory - alter columns and constraints - op.alter_column('inventory', 'warehouse', existing_type=sa.VARCHAR(), nullable=False) - op.alter_column('inventory', 'bin_location', existing_type=sa.VARCHAR(), nullable=False) - op.alter_column('inventory', 'location', existing_type=sa.VARCHAR(), nullable=True) - op.execute(text("DROP INDEX IF EXISTS idx_inventory_product_location")) - op.execute(text("ALTER TABLE inventory DROP CONSTRAINT IF EXISTS uq_inventory_product_location")) - op.execute(text(""" - DO $$ - BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'uq_inventory_product_warehouse_bin') THEN - ALTER TABLE inventory ADD CONSTRAINT uq_inventory_product_warehouse_bin UNIQUE (product_id, warehouse, bin_location); - END IF; - END $$; - """)) - - # marketplace_import_errors and translations indexes - op.execute(text("CREATE INDEX IF NOT EXISTS ix_marketplace_import_errors_import_job_id ON marketplace_import_errors (import_job_id)")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_marketplace_product_translations_id ON marketplace_product_translations (id)")) - - # marketplace_products - alter columns - op.alter_column('marketplace_products', 'is_digital', existing_type=sa.BOOLEAN(), nullable=True) - op.alter_column('marketplace_products', 'is_active', existing_type=sa.BOOLEAN(), nullable=True) - - # marketplace_products indexes - op.execute(text("DROP INDEX IF EXISTS idx_mp_is_active")) - op.execute(text("DROP INDEX IF EXISTS idx_mp_platform")) - op.execute(text("DROP INDEX IF EXISTS idx_mp_sku")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_marketplace_products_is_active ON marketplace_products (is_active)")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_marketplace_products_is_digital ON marketplace_products (is_digital)")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_marketplace_products_mpn ON marketplace_products (mpn)")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_marketplace_products_platform ON marketplace_products (platform)")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_marketplace_products_sku ON marketplace_products (sku)")) - - # order_item_exceptions - constraints and indexes - op.execute(text("DROP INDEX IF EXISTS uq_order_item_exception")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_order_item_exceptions_original_gtin ON order_item_exceptions (original_gtin)")) - op.execute(text(""" - DO $$ - BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'uq_order_item_exceptions_order_item_id') THEN - ALTER TABLE order_item_exceptions ADD CONSTRAINT uq_order_item_exceptions_order_item_id UNIQUE (order_item_id); - END IF; - END $$; - """)) - - # order_items - alter column - op.alter_column('order_items', 'needs_product_match', existing_type=sa.BOOLEAN(), nullable=True) - - # order_items indexes - op.execute(text("DROP INDEX IF EXISTS ix_order_items_gtin")) - op.execute(text("DROP INDEX IF EXISTS ix_order_items_product_id")) - - # product_translations index - op.execute(text("CREATE INDEX IF NOT EXISTS ix_product_translations_id ON product_translations (id)")) - - # products indexes - op.execute(text("DROP INDEX IF EXISTS idx_product_active")) - op.execute(text("DROP INDEX IF EXISTS idx_product_featured")) - op.execute(text("DROP INDEX IF EXISTS idx_product_gtin")) - op.execute(text("DROP INDEX IF EXISTS idx_product_vendor_gtin")) - - # products constraint - op.execute(text("ALTER TABLE products DROP CONSTRAINT IF EXISTS uq_product")) - op.execute(text(""" - DO $$ - BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'uq_vendor_marketplace_product') THEN - ALTER TABLE products ADD CONSTRAINT uq_vendor_marketplace_product UNIQUE (vendor_id, marketplace_product_id); - END IF; - END $$; - """)) - - # products new indexes - op.execute(text("CREATE INDEX IF NOT EXISTS idx_product_vendor_active ON products (vendor_id, is_active)")) - op.execute(text("CREATE INDEX IF NOT EXISTS idx_product_vendor_featured ON products (vendor_id, is_featured)")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_products_gtin ON products (gtin)")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_products_vendor_sku ON products (vendor_sku)")) - - # vendor_domains indexes - op.execute(text("DROP INDEX IF EXISTS ix_vendors_domains_domain")) - op.execute(text("DROP INDEX IF EXISTS ix_vendors_domains_id")) - op.execute(text("CREATE UNIQUE INDEX IF NOT EXISTS ix_vendor_domains_domain ON vendor_domains (domain)")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_vendor_domains_id ON vendor_domains (id)")) - - # vendor_subscriptions - alter column and FK - op.alter_column('vendor_subscriptions', 'payment_retry_count', existing_type=sa.INTEGER(), nullable=False) - op.execute(text(""" - DO $$ - BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_vendor_subscriptions_tier_id') THEN - ALTER TABLE vendor_subscriptions ADD CONSTRAINT fk_vendor_subscriptions_tier_id - FOREIGN KEY (tier_id) REFERENCES subscription_tiers(id); - END IF; - END $$; - """)) - - # vendor_themes indexes - op.execute(text("DROP INDEX IF EXISTS ix_vendors_themes_id")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_vendor_themes_id ON vendor_themes (id)")) - - # vendor_users indexes - op.execute(text("DROP INDEX IF EXISTS ix_vendors_users_id")) - op.execute(text("DROP INDEX IF EXISTS ix_vendors_users_invitation_token")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_vendor_users_id ON vendor_users (id)")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_vendor_users_invitation_token ON vendor_users (invitation_token)")) - - # vendors - alter column - op.alter_column('vendors', 'company_id', existing_type=sa.INTEGER(), nullable=False) - - -def downgrade() -> None: - # vendors - op.alter_column('vendors', 'company_id', existing_type=sa.INTEGER(), nullable=True) - - # vendor_users indexes - op.execute(text("DROP INDEX IF EXISTS ix_vendor_users_invitation_token")) - op.execute(text("DROP INDEX IF EXISTS ix_vendor_users_id")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_vendors_users_invitation_token ON vendor_users (invitation_token)")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_vendors_users_id ON vendor_users (id)")) - - # vendor_themes indexes - op.execute(text("DROP INDEX IF EXISTS ix_vendor_themes_id")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_vendors_themes_id ON vendor_themes (id)")) - - # vendor_subscriptions - op.execute(text("ALTER TABLE vendor_subscriptions DROP CONSTRAINT IF EXISTS fk_vendor_subscriptions_tier_id")) - op.alter_column('vendor_subscriptions', 'payment_retry_count', existing_type=sa.INTEGER(), nullable=True) - - # vendor_domains indexes - op.execute(text("DROP INDEX IF EXISTS ix_vendor_domains_id")) - op.execute(text("DROP INDEX IF EXISTS ix_vendor_domains_domain")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_vendors_domains_id ON vendor_domains (id)")) - op.execute(text("CREATE UNIQUE INDEX IF NOT EXISTS ix_vendors_domains_domain ON vendor_domains (domain)")) - - # products constraint and indexes - op.execute(text("ALTER TABLE products DROP CONSTRAINT IF EXISTS uq_vendor_marketplace_product")) - op.execute(text(""" - DO $$ - BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'uq_product') THEN - ALTER TABLE products ADD CONSTRAINT uq_product UNIQUE (vendor_id, marketplace_product_id); - END IF; - END $$; - """)) - - op.execute(text("DROP INDEX IF EXISTS ix_products_vendor_sku")) - op.execute(text("DROP INDEX IF EXISTS ix_products_gtin")) - op.execute(text("DROP INDEX IF EXISTS idx_product_vendor_featured")) - op.execute(text("DROP INDEX IF EXISTS idx_product_vendor_active")) - op.execute(text("CREATE INDEX IF NOT EXISTS idx_product_vendor_gtin ON products (vendor_id, gtin)")) - op.execute(text("CREATE INDEX IF NOT EXISTS idx_product_gtin ON products (gtin)")) - op.execute(text("CREATE INDEX IF NOT EXISTS idx_product_featured ON products (vendor_id, is_featured)")) - op.execute(text("CREATE INDEX IF NOT EXISTS idx_product_active ON products (vendor_id, is_active)")) - - # product_translations - op.execute(text("DROP INDEX IF EXISTS ix_product_translations_id")) - - # order_items - op.execute(text("CREATE INDEX IF NOT EXISTS ix_order_items_product_id ON order_items (product_id)")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_order_items_gtin ON order_items (gtin)")) - op.alter_column('order_items', 'needs_product_match', existing_type=sa.BOOLEAN(), nullable=False) - - # order_item_exceptions - op.execute(text("ALTER TABLE order_item_exceptions DROP CONSTRAINT IF EXISTS uq_order_item_exceptions_order_item_id")) - op.execute(text("DROP INDEX IF EXISTS ix_order_item_exceptions_original_gtin")) - op.execute(text("CREATE UNIQUE INDEX IF NOT EXISTS uq_order_item_exception ON order_item_exceptions (order_item_id)")) - - # marketplace_products indexes - op.execute(text("DROP INDEX IF EXISTS ix_marketplace_products_sku")) - op.execute(text("DROP INDEX IF EXISTS ix_marketplace_products_platform")) - op.execute(text("DROP INDEX IF EXISTS ix_marketplace_products_mpn")) - op.execute(text("DROP INDEX IF EXISTS ix_marketplace_products_is_digital")) - op.execute(text("DROP INDEX IF EXISTS ix_marketplace_products_is_active")) - op.execute(text("CREATE INDEX IF NOT EXISTS idx_mp_sku ON marketplace_products (sku)")) - op.execute(text("CREATE INDEX IF NOT EXISTS idx_mp_platform ON marketplace_products (platform)")) - op.execute(text("CREATE INDEX IF NOT EXISTS idx_mp_is_active ON marketplace_products (is_active)")) - - # marketplace_products columns - op.alter_column('marketplace_products', 'is_active', existing_type=sa.BOOLEAN(), nullable=False) - op.alter_column('marketplace_products', 'is_digital', existing_type=sa.BOOLEAN(), nullable=False) - - # marketplace imports - op.execute(text("DROP INDEX IF EXISTS ix_marketplace_product_translations_id")) - op.execute(text("DROP INDEX IF EXISTS ix_marketplace_import_errors_import_job_id")) - - # inventory - op.execute(text("ALTER TABLE inventory DROP CONSTRAINT IF EXISTS uq_inventory_product_warehouse_bin")) - op.execute(text(""" - DO $$ - BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'uq_inventory_product_location') THEN - ALTER TABLE inventory ADD CONSTRAINT uq_inventory_product_location UNIQUE (product_id, location); - END IF; - END $$; - """)) - op.execute(text("CREATE INDEX IF NOT EXISTS idx_inventory_product_location ON inventory (product_id, location)")) - op.alter_column('inventory', 'location', existing_type=sa.VARCHAR(), nullable=False) - op.alter_column('inventory', 'bin_location', existing_type=sa.VARCHAR(), nullable=True) - op.alter_column('inventory', 'warehouse', existing_type=sa.VARCHAR(), nullable=True) - - # customer_addresses - op.execute(text("DROP INDEX IF EXISTS ix_customer_addresses_id")) - op.execute(text("CREATE INDEX IF NOT EXISTS ix_customers_addresses_id ON customer_addresses (id)")) - - # cart_items - op.alter_column('cart_items', 'updated_at', existing_type=sa.DATETIME(), nullable=True) - op.alter_column('cart_items', 'created_at', existing_type=sa.DATETIME(), nullable=True) - - # capacity_snapshots - op.execute(text("DROP INDEX IF EXISTS ix_capacity_snapshots_snapshot_date")) - op.execute(text("DROP INDEX IF EXISTS ix_capacity_snapshots_date")) - op.execute(text("CREATE UNIQUE INDEX IF NOT EXISTS ix_capacity_snapshots_date ON capacity_snapshots (snapshot_date)")) - - # application_logs - op.alter_column('application_logs', 'updated_at', existing_type=sa.DATETIME(), nullable=True) - op.alter_column('application_logs', 'created_at', existing_type=sa.DATETIME(), nullable=True) - - # Drop email tables - op.drop_index(op.f('ix_email_logs_vendor_id'), table_name='email_logs') - op.drop_index(op.f('ix_email_logs_user_id'), table_name='email_logs') - op.drop_index(op.f('ix_email_logs_template_code'), table_name='email_logs') - op.drop_index(op.f('ix_email_logs_status'), table_name='email_logs') - op.drop_index(op.f('ix_email_logs_recipient_email'), table_name='email_logs') - op.drop_index(op.f('ix_email_logs_provider_message_id'), table_name='email_logs') - op.drop_index(op.f('ix_email_logs_id'), table_name='email_logs') - op.drop_table('email_logs') - op.drop_index(op.f('ix_email_templates_id'), table_name='email_templates') - op.drop_index(op.f('ix_email_templates_code'), table_name='email_templates') - op.drop_index(op.f('ix_email_templates_category'), table_name='email_templates') - op.drop_table('email_templates') diff --git a/alembic/versions/e1a2b3c4d5e6_add_product_type_and_digital_fields.py b/alembic/versions/e1a2b3c4d5e6_add_product_type_and_digital_fields.py deleted file mode 100644 index 6b728a8a..00000000 --- a/alembic/versions/e1a2b3c4d5e6_add_product_type_and_digital_fields.py +++ /dev/null @@ -1,204 +0,0 @@ -"""Add product type and digital fields to marketplace_products - -Revision ID: e1a2b3c4d5e6 -Revises: 28d44d503cac -Create Date: 2025-12-11 - -This migration adds support for: -- Product type classification (physical, digital, service, subscription) -- Digital product fields (delivery method, platform, region restrictions) -- Numeric price fields for filtering/sorting -- Additional images as JSON array -- Source URL tracking -- Flexible attributes JSON column -- Active status flag - -It also renames 'product_type' to 'product_type_raw' to preserve the original -Google Shopping feed value while using 'product_type' for the new enum. -""" - -from typing import Sequence, Union - -import sqlalchemy as sa -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "e1a2b3c4d5e6" -down_revision: Union[str, None] = "28d44d503cac" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Rename existing product_type column to product_type_raw - # to preserve the original Google Shopping feed value - op.alter_column( - "marketplace_products", - "product_type", - new_column_name="product_type_raw", - ) - - # Add new product classification columns - op.add_column( - "marketplace_products", - sa.Column( - "product_type_enum", - sa.String(20), - nullable=False, - server_default="physical", - ), - ) - op.add_column( - "marketplace_products", - sa.Column( - "is_digital", - sa.Boolean(), - nullable=False, - server_default=sa.text("false"), - ), - ) - - # Add digital product specific fields - op.add_column( - "marketplace_products", - sa.Column("digital_delivery_method", sa.String(20), nullable=True), - ) - op.add_column( - "marketplace_products", - sa.Column("platform", sa.String(50), nullable=True), - ) - op.add_column( - "marketplace_products", - sa.Column("region_restrictions", sa.JSON(), nullable=True), - ) - op.add_column( - "marketplace_products", - sa.Column("license_type", sa.String(50), nullable=True), - ) - - # Add source tracking - op.add_column( - "marketplace_products", - sa.Column("source_url", sa.String(), nullable=True), - ) - - # Add numeric price fields for filtering/sorting - op.add_column( - "marketplace_products", - sa.Column("price_numeric", sa.Float(), nullable=True), - ) - op.add_column( - "marketplace_products", - sa.Column("sale_price_numeric", sa.Float(), nullable=True), - ) - - # Add flexible attributes JSON column - op.add_column( - "marketplace_products", - sa.Column("attributes", sa.JSON(), nullable=True), - ) - - # Add additional images as JSON array (complements existing additional_image_link) - op.add_column( - "marketplace_products", - sa.Column("additional_images", sa.JSON(), nullable=True), - ) - - # Add active status flag - op.add_column( - "marketplace_products", - sa.Column( - "is_active", - sa.Boolean(), - nullable=False, - server_default=sa.text("true"), - ), - ) - - # Add SKU field for internal reference - op.add_column( - "marketplace_products", - sa.Column("sku", sa.String(), nullable=True), - ) - - # Add weight fields for physical products - op.add_column( - "marketplace_products", - sa.Column("weight", sa.Float(), nullable=True), - ) - op.add_column( - "marketplace_products", - sa.Column("weight_unit", sa.String(10), nullable=True, server_default="kg"), - ) - op.add_column( - "marketplace_products", - sa.Column("dimensions", sa.JSON(), nullable=True), - ) - - # Add category_path for normalized hierarchy - op.add_column( - "marketplace_products", - sa.Column("category_path", sa.String(), nullable=True), - ) - - # Create indexes for new columns - op.create_index( - "idx_mp_product_type", - "marketplace_products", - ["product_type_enum", "is_digital"], - ) - op.create_index( - "idx_mp_is_active", - "marketplace_products", - ["is_active"], - ) - op.create_index( - "idx_mp_platform", - "marketplace_products", - ["platform"], - ) - op.create_index( - "idx_mp_sku", - "marketplace_products", - ["sku"], - ) - op.create_index( - "idx_mp_gtin_marketplace", - "marketplace_products", - ["gtin", "marketplace"], - ) - - -def downgrade() -> None: - # Drop indexes - op.drop_index("idx_mp_gtin_marketplace", table_name="marketplace_products") - op.drop_index("idx_mp_sku", table_name="marketplace_products") - op.drop_index("idx_mp_platform", table_name="marketplace_products") - op.drop_index("idx_mp_is_active", table_name="marketplace_products") - op.drop_index("idx_mp_product_type", table_name="marketplace_products") - - # Drop new columns - op.drop_column("marketplace_products", "category_path") - op.drop_column("marketplace_products", "dimensions") - op.drop_column("marketplace_products", "weight_unit") - op.drop_column("marketplace_products", "weight") - op.drop_column("marketplace_products", "sku") - op.drop_column("marketplace_products", "is_active") - op.drop_column("marketplace_products", "additional_images") - op.drop_column("marketplace_products", "attributes") - op.drop_column("marketplace_products", "sale_price_numeric") - op.drop_column("marketplace_products", "price_numeric") - op.drop_column("marketplace_products", "source_url") - op.drop_column("marketplace_products", "license_type") - op.drop_column("marketplace_products", "region_restrictions") - op.drop_column("marketplace_products", "platform") - op.drop_column("marketplace_products", "digital_delivery_method") - op.drop_column("marketplace_products", "is_digital") - op.drop_column("marketplace_products", "product_type_enum") - - # Rename product_type_raw back to product_type - op.alter_column( - "marketplace_products", - "product_type_raw", - new_column_name="product_type", - ) diff --git a/alembic/versions/e1bfb453fbe9_add_warehouse_and_bin_location_to_.py b/alembic/versions/e1bfb453fbe9_add_warehouse_and_bin_location_to_.py deleted file mode 100644 index 80d263a7..00000000 --- a/alembic/versions/e1bfb453fbe9_add_warehouse_and_bin_location_to_.py +++ /dev/null @@ -1,90 +0,0 @@ -"""add_warehouse_and_bin_location_to_inventory - -Revision ID: e1bfb453fbe9 -Revises: j8e9f0a1b2c3 -Create Date: 2025-12-25 12:21:24.006548 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy import text - - -# revision identifiers, used by Alembic. -revision: str = 'e1bfb453fbe9' -down_revision: Union[str, None] = 'j8e9f0a1b2c3' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def get_column_names(conn, table_name: str) -> set: - """Get column names for a table (PostgreSQL).""" - result = conn.execute(text( - "SELECT column_name FROM information_schema.columns " - "WHERE table_name = :table AND table_schema = 'public'" - ), {"table": table_name}) - return {row[0] for row in result.fetchall()} - - -def get_index_names(conn, table_name: str) -> set: - """Get index names for a table (PostgreSQL).""" - result = conn.execute(text( - "SELECT indexname FROM pg_indexes " - "WHERE tablename = :table AND schemaname = 'public'" - ), {"table": table_name}) - return {row[0] for row in result.fetchall()} - - -def upgrade() -> None: - conn = op.get_bind() - - # Check if columns already exist (idempotent) - columns = get_column_names(conn, "inventory") - - if 'warehouse' not in columns: - op.add_column('inventory', sa.Column('warehouse', sa.String(), nullable=False, server_default='strassen')) - - if 'bin_location' not in columns: - op.add_column('inventory', sa.Column('bin_location', sa.String(), nullable=False, server_default='')) - - # Migrate existing data: copy location to bin_location, set default warehouse - conn.execute(text(""" - UPDATE inventory - SET bin_location = COALESCE(location, 'UNKNOWN'), - warehouse = 'strassen' - WHERE bin_location IS NULL OR bin_location = '' - """)) - - # Create indexes if they don't exist - existing_indexes = get_index_names(conn, "inventory") - - if 'idx_inventory_warehouse_bin' not in existing_indexes: - op.create_index('idx_inventory_warehouse_bin', 'inventory', ['warehouse', 'bin_location'], unique=False) - if 'ix_inventory_bin_location' not in existing_indexes: - op.create_index(op.f('ix_inventory_bin_location'), 'inventory', ['bin_location'], unique=False) - if 'ix_inventory_warehouse' not in existing_indexes: - op.create_index(op.f('ix_inventory_warehouse'), 'inventory', ['warehouse'], unique=False) - - -def downgrade() -> None: - conn = op.get_bind() - - # Check which indexes exist before dropping - existing_indexes = get_index_names(conn, "inventory") - - if 'ix_inventory_warehouse' in existing_indexes: - op.drop_index(op.f('ix_inventory_warehouse'), table_name='inventory') - if 'ix_inventory_bin_location' in existing_indexes: - op.drop_index(op.f('ix_inventory_bin_location'), table_name='inventory') - if 'idx_inventory_warehouse_bin' in existing_indexes: - op.drop_index('idx_inventory_warehouse_bin', table_name='inventory') - - # Check if columns exist before dropping - columns = get_column_names(conn, "inventory") - - if 'bin_location' in columns: - op.drop_column('inventory', 'bin_location') - if 'warehouse' in columns: - op.drop_column('inventory', 'warehouse') diff --git a/alembic/versions/e1f2a3b4c5d6_convert_prices_to_integer_cents.py b/alembic/versions/e1f2a3b4c5d6_convert_prices_to_integer_cents.py deleted file mode 100644 index e35312a9..00000000 --- a/alembic/versions/e1f2a3b4c5d6_convert_prices_to_integer_cents.py +++ /dev/null @@ -1,223 +0,0 @@ -"""convert_prices_to_integer_cents - -Revision ID: e1f2a3b4c5d6 -Revises: c00d2985701f -Create Date: 2025-12-20 21:30:00.000000 - -Converts all price/amount columns from Float to Integer cents. -This follows e-commerce best practices (Stripe, PayPal, Shopify) for -precise monetary calculations. - -Example: €105.91 is stored as 10591 (integer cents) - -Affected tables: -- products: price, sale_price, supplier_cost, margin_percent -- orders: subtotal, tax_amount, shipping_amount, discount_amount, total_amount -- order_items: unit_price, total_price -- cart_items: price_at_add -- marketplace_products: price_numeric, sale_price_numeric - -See docs/architecture/money-handling.md for full documentation. -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'e1f2a3b4c5d6' -down_revision: Union[str, None] = 'c00d2985701f' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # SQLite requires batch mode for column alterations - # Strategy: Add new _cents columns, migrate data, drop old columns - - # === PRODUCTS TABLE === - with op.batch_alter_table('products', schema=None) as batch_op: - # Add new cents columns - batch_op.add_column(sa.Column('price_cents', sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column('sale_price_cents', sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column('supplier_cost_cents', sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column('margin_percent_x100', sa.Integer(), nullable=True)) - - # Migrate data for products - op.execute('UPDATE products SET price_cents = ROUND(COALESCE(price, 0) * 100)') - op.execute('UPDATE products SET sale_price_cents = ROUND(sale_price * 100) WHERE sale_price IS NOT NULL') - op.execute('UPDATE products SET supplier_cost_cents = ROUND(supplier_cost * 100) WHERE supplier_cost IS NOT NULL') - op.execute('UPDATE products SET margin_percent_x100 = ROUND(margin_percent * 100) WHERE margin_percent IS NOT NULL') - - # Drop old columns - with op.batch_alter_table('products', schema=None) as batch_op: - batch_op.drop_column('price') - batch_op.drop_column('sale_price') - batch_op.drop_column('supplier_cost') - batch_op.drop_column('margin_percent') - - # === ORDERS TABLE === - with op.batch_alter_table('orders', schema=None) as batch_op: - batch_op.add_column(sa.Column('subtotal_cents', sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column('tax_amount_cents', sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column('shipping_amount_cents', sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column('discount_amount_cents', sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column('total_amount_cents', sa.Integer(), nullable=True)) - - # Migrate data for orders - op.execute('UPDATE orders SET subtotal_cents = ROUND(COALESCE(subtotal, 0) * 100)') - op.execute('UPDATE orders SET tax_amount_cents = ROUND(COALESCE(tax_amount, 0) * 100)') - op.execute('UPDATE orders SET shipping_amount_cents = ROUND(COALESCE(shipping_amount, 0) * 100)') - op.execute('UPDATE orders SET discount_amount_cents = ROUND(COALESCE(discount_amount, 0) * 100)') - op.execute('UPDATE orders SET total_amount_cents = ROUND(COALESCE(total_amount, 0) * 100)') - - # Make total_amount_cents NOT NULL after migration - with op.batch_alter_table('orders', schema=None) as batch_op: - batch_op.drop_column('subtotal') - batch_op.drop_column('tax_amount') - batch_op.drop_column('shipping_amount') - batch_op.drop_column('discount_amount') - batch_op.drop_column('total_amount') - # Alter total_amount_cents to be NOT NULL - batch_op.alter_column('total_amount_cents', - existing_type=sa.Integer(), - nullable=False) - - # === ORDER_ITEMS TABLE === - with op.batch_alter_table('order_items', schema=None) as batch_op: - batch_op.add_column(sa.Column('unit_price_cents', sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column('total_price_cents', sa.Integer(), nullable=True)) - - # Migrate data for order_items - op.execute('UPDATE order_items SET unit_price_cents = ROUND(COALESCE(unit_price, 0) * 100)') - op.execute('UPDATE order_items SET total_price_cents = ROUND(COALESCE(total_price, 0) * 100)') - - with op.batch_alter_table('order_items', schema=None) as batch_op: - batch_op.drop_column('unit_price') - batch_op.drop_column('total_price') - batch_op.alter_column('unit_price_cents', - existing_type=sa.Integer(), - nullable=False) - batch_op.alter_column('total_price_cents', - existing_type=sa.Integer(), - nullable=False) - - # === CART_ITEMS TABLE === - with op.batch_alter_table('cart_items', schema=None) as batch_op: - batch_op.add_column(sa.Column('price_at_add_cents', sa.Integer(), nullable=True)) - - # Migrate data for cart_items - op.execute('UPDATE cart_items SET price_at_add_cents = ROUND(COALESCE(price_at_add, 0) * 100)') - - with op.batch_alter_table('cart_items', schema=None) as batch_op: - batch_op.drop_column('price_at_add') - batch_op.alter_column('price_at_add_cents', - existing_type=sa.Integer(), - nullable=False) - - # === MARKETPLACE_PRODUCTS TABLE === - with op.batch_alter_table('marketplace_products', schema=None) as batch_op: - batch_op.add_column(sa.Column('price_cents', sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column('sale_price_cents', sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column('weight_grams', sa.Integer(), nullable=True)) - - # Migrate data for marketplace_products - op.execute('UPDATE marketplace_products SET price_cents = ROUND(price_numeric * 100) WHERE price_numeric IS NOT NULL') - op.execute('UPDATE marketplace_products SET sale_price_cents = ROUND(sale_price_numeric * 100) WHERE sale_price_numeric IS NOT NULL') - op.execute('UPDATE marketplace_products SET weight_grams = ROUND(weight * 1000) WHERE weight IS NOT NULL') - - with op.batch_alter_table('marketplace_products', schema=None) as batch_op: - batch_op.drop_column('price_numeric') - batch_op.drop_column('sale_price_numeric') - batch_op.drop_column('weight') - - -def downgrade() -> None: - # === MARKETPLACE_PRODUCTS TABLE === - with op.batch_alter_table('marketplace_products', schema=None) as batch_op: - batch_op.add_column(sa.Column('price_numeric', sa.Float(), nullable=True)) - batch_op.add_column(sa.Column('sale_price_numeric', sa.Float(), nullable=True)) - batch_op.add_column(sa.Column('weight', sa.Float(), nullable=True)) - - op.execute('UPDATE marketplace_products SET price_numeric = price_cents / 100.0 WHERE price_cents IS NOT NULL') - op.execute('UPDATE marketplace_products SET sale_price_numeric = sale_price_cents / 100.0 WHERE sale_price_cents IS NOT NULL') - op.execute('UPDATE marketplace_products SET weight = weight_grams / 1000.0 WHERE weight_grams IS NOT NULL') - - with op.batch_alter_table('marketplace_products', schema=None) as batch_op: - batch_op.drop_column('price_cents') - batch_op.drop_column('sale_price_cents') - batch_op.drop_column('weight_grams') - - # === CART_ITEMS TABLE === - with op.batch_alter_table('cart_items', schema=None) as batch_op: - batch_op.add_column(sa.Column('price_at_add', sa.Float(), nullable=True)) - - op.execute('UPDATE cart_items SET price_at_add = price_at_add_cents / 100.0') - - with op.batch_alter_table('cart_items', schema=None) as batch_op: - batch_op.drop_column('price_at_add_cents') - batch_op.alter_column('price_at_add', - existing_type=sa.Float(), - nullable=False) - - # === ORDER_ITEMS TABLE === - with op.batch_alter_table('order_items', schema=None) as batch_op: - batch_op.add_column(sa.Column('unit_price', sa.Float(), nullable=True)) - batch_op.add_column(sa.Column('total_price', sa.Float(), nullable=True)) - - op.execute('UPDATE order_items SET unit_price = unit_price_cents / 100.0') - op.execute('UPDATE order_items SET total_price = total_price_cents / 100.0') - - with op.batch_alter_table('order_items', schema=None) as batch_op: - batch_op.drop_column('unit_price_cents') - batch_op.drop_column('total_price_cents') - batch_op.alter_column('unit_price', - existing_type=sa.Float(), - nullable=False) - batch_op.alter_column('total_price', - existing_type=sa.Float(), - nullable=False) - - # === ORDERS TABLE === - with op.batch_alter_table('orders', schema=None) as batch_op: - batch_op.add_column(sa.Column('subtotal', sa.Float(), nullable=True)) - batch_op.add_column(sa.Column('tax_amount', sa.Float(), nullable=True)) - batch_op.add_column(sa.Column('shipping_amount', sa.Float(), nullable=True)) - batch_op.add_column(sa.Column('discount_amount', sa.Float(), nullable=True)) - batch_op.add_column(sa.Column('total_amount', sa.Float(), nullable=True)) - - op.execute('UPDATE orders SET subtotal = subtotal_cents / 100.0') - op.execute('UPDATE orders SET tax_amount = tax_amount_cents / 100.0') - op.execute('UPDATE orders SET shipping_amount = shipping_amount_cents / 100.0') - op.execute('UPDATE orders SET discount_amount = discount_amount_cents / 100.0') - op.execute('UPDATE orders SET total_amount = total_amount_cents / 100.0') - - with op.batch_alter_table('orders', schema=None) as batch_op: - batch_op.drop_column('subtotal_cents') - batch_op.drop_column('tax_amount_cents') - batch_op.drop_column('shipping_amount_cents') - batch_op.drop_column('discount_amount_cents') - batch_op.drop_column('total_amount_cents') - batch_op.alter_column('total_amount', - existing_type=sa.Float(), - nullable=False) - - # === PRODUCTS TABLE === - with op.batch_alter_table('products', schema=None) as batch_op: - batch_op.add_column(sa.Column('price', sa.Float(), nullable=True)) - batch_op.add_column(sa.Column('sale_price', sa.Float(), nullable=True)) - batch_op.add_column(sa.Column('supplier_cost', sa.Float(), nullable=True)) - batch_op.add_column(sa.Column('margin_percent', sa.Float(), nullable=True)) - - op.execute('UPDATE products SET price = price_cents / 100.0 WHERE price_cents IS NOT NULL') - op.execute('UPDATE products SET sale_price = sale_price_cents / 100.0 WHERE sale_price_cents IS NOT NULL') - op.execute('UPDATE products SET supplier_cost = supplier_cost_cents / 100.0 WHERE supplier_cost_cents IS NOT NULL') - op.execute('UPDATE products SET margin_percent = margin_percent_x100 / 100.0 WHERE margin_percent_x100 IS NOT NULL') - - with op.batch_alter_table('products', schema=None) as batch_op: - batch_op.drop_column('price_cents') - batch_op.drop_column('sale_price_cents') - batch_op.drop_column('supplier_cost_cents') - batch_op.drop_column('margin_percent_x100') diff --git a/alembic/versions/e3f4a5b6c7d8_add_messaging_tables.py b/alembic/versions/e3f4a5b6c7d8_add_messaging_tables.py deleted file mode 100644 index 62b9937d..00000000 --- a/alembic/versions/e3f4a5b6c7d8_add_messaging_tables.py +++ /dev/null @@ -1,339 +0,0 @@ -"""add_messaging_tables - -Revision ID: e3f4a5b6c7d8 -Revises: c9e22eadf533 -Create Date: 2025-12-21 - -This migration adds the messaging system tables: -- conversations: Threaded conversation threads -- conversation_participants: Links users/customers to conversations -- messages: Individual messages within conversations -- message_attachments: File attachments for messages - -Supports three communication channels: -- Admin <-> Vendor -- Vendor <-> Customer -- Admin <-> Customer -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy import inspect - - -# revision identifiers, used by Alembic. -revision: str = "e3f4a5b6c7d8" -down_revision: Union[str, None] = "c9e22eadf533" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def table_exists(table_name: str) -> bool: - """Check if a table exists in the database.""" - bind = op.get_bind() - inspector = inspect(bind) - return table_name in inspector.get_table_names() - - -def index_exists(index_name: str, table_name: str) -> bool: - """Check if an index exists on a table.""" - bind = op.get_bind() - inspector = inspect(bind) - try: - indexes = inspector.get_indexes(table_name) - return any(idx["name"] == index_name for idx in indexes) - except Exception: - return False - - -def upgrade() -> None: - # ========================================================================= - # Step 1: Create conversations table - # ========================================================================= - if not table_exists("conversations"): - op.create_table( - "conversations", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column( - "conversation_type", - sa.Enum( - "admin_vendor", - "vendor_customer", - "admin_customer", - name="conversationtype", - ), - nullable=False, - ), - sa.Column("subject", sa.String(length=500), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=True), - sa.Column("is_closed", sa.Boolean(), nullable=False, server_default="0"), - sa.Column("closed_at", sa.DateTime(), nullable=True), - sa.Column( - "closed_by_type", - sa.Enum("admin", "vendor", "customer", name="participanttype"), - nullable=True, - ), - sa.Column("closed_by_id", sa.Integer(), nullable=True), - sa.Column("last_message_at", sa.DateTime(), nullable=True), - sa.Column("message_count", sa.Integer(), nullable=False, server_default="0"), - sa.Column( - "created_at", - sa.DateTime(), - nullable=False, - server_default=sa.text("CURRENT_TIMESTAMP"), - ), - sa.Column( - "updated_at", - sa.DateTime(), - nullable=False, - server_default=sa.text("CURRENT_TIMESTAMP"), - ), - sa.ForeignKeyConstraint( - ["vendor_id"], - ["vendors.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_conversations_id"), "conversations", ["id"], unique=False - ) - op.create_index( - op.f("ix_conversations_conversation_type"), - "conversations", - ["conversation_type"], - unique=False, - ) - op.create_index( - op.f("ix_conversations_vendor_id"), - "conversations", - ["vendor_id"], - unique=False, - ) - op.create_index( - op.f("ix_conversations_last_message_at"), - "conversations", - ["last_message_at"], - unique=False, - ) - op.create_index( - "ix_conversations_type_vendor", - "conversations", - ["conversation_type", "vendor_id"], - unique=False, - ) - - # ========================================================================= - # Step 2: Create conversation_participants table - # ========================================================================= - if not table_exists("conversation_participants"): - op.create_table( - "conversation_participants", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("conversation_id", sa.Integer(), nullable=False), - sa.Column( - "participant_type", - sa.Enum("admin", "vendor", "customer", name="participanttype"), - nullable=False, - ), - sa.Column("participant_id", sa.Integer(), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=True), - sa.Column("unread_count", sa.Integer(), nullable=False, server_default="0"), - sa.Column("last_read_at", sa.DateTime(), nullable=True), - sa.Column( - "email_notifications", sa.Boolean(), nullable=False, server_default="1" - ), - sa.Column("muted", sa.Boolean(), nullable=False, server_default="0"), - sa.Column( - "created_at", - sa.DateTime(), - nullable=False, - server_default=sa.text("CURRENT_TIMESTAMP"), - ), - sa.Column( - "updated_at", - sa.DateTime(), - nullable=False, - server_default=sa.text("CURRENT_TIMESTAMP"), - ), - sa.ForeignKeyConstraint( - ["conversation_id"], - ["conversations.id"], - ondelete="CASCADE", - ), - sa.ForeignKeyConstraint( - ["vendor_id"], - ["vendors.id"], - ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint( - "conversation_id", - "participant_type", - "participant_id", - name="uq_conversation_participant", - ), - ) - op.create_index( - op.f("ix_conversation_participants_id"), - "conversation_participants", - ["id"], - unique=False, - ) - op.create_index( - op.f("ix_conversation_participants_conversation_id"), - "conversation_participants", - ["conversation_id"], - unique=False, - ) - op.create_index( - op.f("ix_conversation_participants_participant_id"), - "conversation_participants", - ["participant_id"], - unique=False, - ) - op.create_index( - "ix_participant_lookup", - "conversation_participants", - ["participant_type", "participant_id"], - unique=False, - ) - - # ========================================================================= - # Step 3: Create messages table - # ========================================================================= - if not table_exists("messages"): - op.create_table( - "messages", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("conversation_id", sa.Integer(), nullable=False), - sa.Column( - "sender_type", - sa.Enum("admin", "vendor", "customer", name="participanttype"), - nullable=False, - ), - sa.Column("sender_id", sa.Integer(), nullable=False), - sa.Column("content", sa.Text(), nullable=False), - sa.Column( - "is_system_message", sa.Boolean(), nullable=False, server_default="0" - ), - sa.Column("is_deleted", sa.Boolean(), nullable=False, server_default="0"), - sa.Column("deleted_at", sa.DateTime(), nullable=True), - sa.Column( - "deleted_by_type", - sa.Enum("admin", "vendor", "customer", name="participanttype"), - nullable=True, - ), - sa.Column("deleted_by_id", sa.Integer(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(), - nullable=False, - server_default=sa.text("CURRENT_TIMESTAMP"), - ), - sa.Column( - "updated_at", - sa.DateTime(), - nullable=False, - server_default=sa.text("CURRENT_TIMESTAMP"), - ), - sa.ForeignKeyConstraint( - ["conversation_id"], - ["conversations.id"], - ondelete="CASCADE", - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_messages_id"), "messages", ["id"], unique=False) - op.create_index( - op.f("ix_messages_conversation_id"), - "messages", - ["conversation_id"], - unique=False, - ) - op.create_index( - op.f("ix_messages_sender_id"), "messages", ["sender_id"], unique=False - ) - op.create_index( - "ix_messages_conversation_created", - "messages", - ["conversation_id", "created_at"], - unique=False, - ) - - # ========================================================================= - # Step 4: Create message_attachments table - # ========================================================================= - if not table_exists("message_attachments"): - op.create_table( - "message_attachments", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("message_id", sa.Integer(), nullable=False), - sa.Column("filename", sa.String(length=255), nullable=False), - sa.Column("original_filename", sa.String(length=255), nullable=False), - sa.Column("file_path", sa.String(length=1000), nullable=False), - sa.Column("file_size", sa.Integer(), nullable=False), - sa.Column("mime_type", sa.String(length=100), nullable=False), - sa.Column("is_image", sa.Boolean(), nullable=False, server_default="0"), - sa.Column("image_width", sa.Integer(), nullable=True), - sa.Column("image_height", sa.Integer(), nullable=True), - sa.Column("thumbnail_path", sa.String(length=1000), nullable=True), - sa.Column( - "created_at", - sa.DateTime(), - nullable=False, - server_default=sa.text("CURRENT_TIMESTAMP"), - ), - sa.Column( - "updated_at", - sa.DateTime(), - nullable=False, - server_default=sa.text("CURRENT_TIMESTAMP"), - ), - sa.ForeignKeyConstraint( - ["message_id"], - ["messages.id"], - ondelete="CASCADE", - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_message_attachments_id"), - "message_attachments", - ["id"], - unique=False, - ) - op.create_index( - op.f("ix_message_attachments_message_id"), - "message_attachments", - ["message_id"], - unique=False, - ) - - # ========================================================================= - # Step 5: Add platform setting for attachment size limit - # ========================================================================= - # Note: This will be added via seed script or manually - # Key: message_attachment_max_size_mb - # Value: 10 - # Category: messaging - - -def downgrade() -> None: - # Drop tables in reverse order (respecting foreign keys) - if table_exists("message_attachments"): - op.drop_table("message_attachments") - - if table_exists("messages"): - op.drop_table("messages") - - if table_exists("conversation_participants"): - op.drop_table("conversation_participants") - - if table_exists("conversations"): - op.drop_table("conversations") - - # Note: Enum types are not dropped automatically - # They can be manually dropped with: - # op.execute("DROP TYPE IF EXISTS conversationtype") - # op.execute("DROP TYPE IF EXISTS participanttype") diff --git a/alembic/versions/f2b3c4d5e6f7_create_translation_tables.py b/alembic/versions/f2b3c4d5e6f7_create_translation_tables.py deleted file mode 100644 index 9db90556..00000000 --- a/alembic/versions/f2b3c4d5e6f7_create_translation_tables.py +++ /dev/null @@ -1,147 +0,0 @@ -"""Create translation tables for multi-language support - -Revision ID: f2b3c4d5e6f7 -Revises: e1a2b3c4d5e6 -Create Date: 2025-12-11 - -This migration creates: -- marketplace_product_translations: Localized content from marketplace sources -- product_translations: Vendor-specific localized overrides - -The translation tables support multi-language product information with -language fallback capabilities. Fields in product_translations can be -NULL to inherit from marketplace_product_translations. -""" - -from typing import Sequence, Union - -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "f2b3c4d5e6f7" -down_revision: Union[str, None] = "e1a2b3c4d5e6" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Create marketplace_product_translations table - # Note: Unique constraint is included in create_table for SQLite compatibility - op.create_table( - "marketplace_product_translations", - sa.Column("id", sa.Integer(), primary_key=True), - sa.Column( - "marketplace_product_id", - sa.Integer(), - sa.ForeignKey("marketplace_products.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column("language", sa.String(5), nullable=False), - # Localized content - sa.Column("title", sa.String(), nullable=False), - sa.Column("description", sa.Text(), nullable=True), - sa.Column("short_description", sa.String(500), nullable=True), - # SEO fields - sa.Column("meta_title", sa.String(70), nullable=True), - sa.Column("meta_description", sa.String(160), nullable=True), - sa.Column("url_slug", sa.String(255), nullable=True), - # Source tracking - sa.Column("source_import_id", sa.Integer(), nullable=True), - sa.Column("source_file", sa.String(), nullable=True), - # Timestamps - sa.Column( - "created_at", - sa.DateTime(), - nullable=False, - server_default=sa.text("CURRENT_TIMESTAMP"), - ), - sa.Column( - "updated_at", - sa.DateTime(), - nullable=False, - server_default=sa.text("CURRENT_TIMESTAMP"), - ), - # Unique constraint included in table creation for SQLite - sa.UniqueConstraint( - "marketplace_product_id", - "language", - name="uq_marketplace_product_translation", - ), - ) - - # Create indexes for marketplace_product_translations - op.create_index( - "idx_mpt_marketplace_product_id", - "marketplace_product_translations", - ["marketplace_product_id"], - ) - op.create_index( - "idx_mpt_language", - "marketplace_product_translations", - ["language"], - ) - - # Create product_translations table - # Note: Unique constraint is included in create_table for SQLite compatibility - op.create_table( - "product_translations", - sa.Column("id", sa.Integer(), primary_key=True), - sa.Column( - "product_id", - sa.Integer(), - sa.ForeignKey("products.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column("language", sa.String(5), nullable=False), - # Overridable localized content (NULL = inherit from marketplace) - sa.Column("title", sa.String(), nullable=True), - sa.Column("description", sa.Text(), nullable=True), - sa.Column("short_description", sa.String(500), nullable=True), - # SEO overrides - sa.Column("meta_title", sa.String(70), nullable=True), - sa.Column("meta_description", sa.String(160), nullable=True), - sa.Column("url_slug", sa.String(255), nullable=True), - # Timestamps - sa.Column( - "created_at", - sa.DateTime(), - nullable=False, - server_default=sa.text("CURRENT_TIMESTAMP"), - ), - sa.Column( - "updated_at", - sa.DateTime(), - nullable=False, - server_default=sa.text("CURRENT_TIMESTAMP"), - ), - # Unique constraint included in table creation for SQLite - sa.UniqueConstraint("product_id", "language", name="uq_product_translation"), - ) - - # Create indexes for product_translations - op.create_index( - "idx_pt_product_id", - "product_translations", - ["product_id"], - ) - op.create_index( - "idx_pt_product_language", - "product_translations", - ["product_id", "language"], - ) - - -def downgrade() -> None: - # Drop product_translations table and its indexes - op.drop_index("idx_pt_product_language", table_name="product_translations") - op.drop_index("idx_pt_product_id", table_name="product_translations") - op.drop_table("product_translations") - - # Drop marketplace_product_translations table and its indexes - op.drop_index("idx_mpt_language", table_name="marketplace_product_translations") - op.drop_index( - "idx_mpt_marketplace_product_id", table_name="marketplace_product_translations" - ) - op.drop_table("marketplace_product_translations") diff --git a/alembic/versions/f4a5b6c7d8e9_add_validator_type_to_code_quality.py b/alembic/versions/f4a5b6c7d8e9_add_validator_type_to_code_quality.py deleted file mode 100644 index 35e48b91..00000000 --- a/alembic/versions/f4a5b6c7d8e9_add_validator_type_to_code_quality.py +++ /dev/null @@ -1,95 +0,0 @@ -"""add_validator_type_to_code_quality - -Revision ID: f4a5b6c7d8e9 -Revises: e3f4a5b6c7d8 -Create Date: 2025-12-21 - -This migration adds validator_type column to architecture scans and violations -to support multiple validator types (architecture, security, performance). -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = "f4a5b6c7d8e9" -down_revision: Union[str, None] = "e3f4a5b6c7d8" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Add validator_type to architecture_scans table - op.add_column( - "architecture_scans", - sa.Column( - "validator_type", - sa.String(length=20), - nullable=False, - server_default="architecture", - ), - ) - op.create_index( - op.f("ix_architecture_scans_validator_type"), - "architecture_scans", - ["validator_type"], - unique=False, - ) - - # Add validator_type to architecture_violations table - op.add_column( - "architecture_violations", - sa.Column( - "validator_type", - sa.String(length=20), - nullable=False, - server_default="architecture", - ), - ) - op.create_index( - op.f("ix_architecture_violations_validator_type"), - "architecture_violations", - ["validator_type"], - unique=False, - ) - - # Add validator_type to architecture_rules table - op.add_column( - "architecture_rules", - sa.Column( - "validator_type", - sa.String(length=20), - nullable=False, - server_default="architecture", - ), - ) - op.create_index( - op.f("ix_architecture_rules_validator_type"), - "architecture_rules", - ["validator_type"], - unique=False, - ) - - -def downgrade() -> None: - # Drop indexes first - op.drop_index( - op.f("ix_architecture_rules_validator_type"), - table_name="architecture_rules", - ) - op.drop_index( - op.f("ix_architecture_violations_validator_type"), - table_name="architecture_violations", - ) - op.drop_index( - op.f("ix_architecture_scans_validator_type"), - table_name="architecture_scans", - ) - - # Drop columns - op.drop_column("architecture_rules", "validator_type") - op.drop_column("architecture_violations", "validator_type") - op.drop_column("architecture_scans", "validator_type") diff --git a/alembic/versions/f68d8da5315a_add_template_field_to_content_pages_for_.py b/alembic/versions/f68d8da5315a_add_template_field_to_content_pages_for_.py deleted file mode 100644 index b095835d..00000000 --- a/alembic/versions/f68d8da5315a_add_template_field_to_content_pages_for_.py +++ /dev/null @@ -1,34 +0,0 @@ -"""add template field to content pages for landing page designs - -Revision ID: f68d8da5315a -Revises: 72aa309d4007 -Create Date: 2025-11-22 23:51:40.694983 - -""" - -from typing import Sequence, Union - -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "f68d8da5315a" -down_revision: Union[str, None] = "72aa309d4007" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Add template column to content_pages table - op.add_column( - "content_pages", - sa.Column( - "template", sa.String(length=50), nullable=False, server_default="default" - ), - ) - - -def downgrade() -> None: - # Remove template column from content_pages table - op.drop_column("content_pages", "template") diff --git a/alembic/versions/fa7d4d10e358_add_rbac_enhancements.py b/alembic/versions/fa7d4d10e358_add_rbac_enhancements.py deleted file mode 100644 index 6c0decce..00000000 --- a/alembic/versions/fa7d4d10e358_add_rbac_enhancements.py +++ /dev/null @@ -1,148 +0,0 @@ -"""add_rbac_enhancements - -Revision ID: fa7d4d10e358 -Revises: 4951b2e50581 -Create Date: 2025-11-13 16:51:25.010057 - -SQLite-compatible version -""" - -from typing import Sequence, Union - -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "fa7d4d10e358" -down_revision: Union[str, None] = "4951b2e50581" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade(): - """Upgrade database schema for RBAC enhancements. - - SQLite-compatible version using batch operations for table modifications. - """ - - # ======================================================================== - # User table changes - # ======================================================================== - with op.batch_alter_table("users", schema=None) as batch_op: - batch_op.add_column( - sa.Column( - "is_email_verified", - sa.Boolean(), - nullable=False, - server_default="false", - ) - ) - - # Set existing active users as verified - op.execute("UPDATE users SET is_email_verified = TRUE WHERE is_active = TRUE") - - # ======================================================================== - # VendorUser table changes (requires table recreation for SQLite) - # ======================================================================== - with op.batch_alter_table("vendor_users", schema=None) as batch_op: - # Add new columns - batch_op.add_column( - sa.Column( - "user_type", - sa.String(length=20), - nullable=False, - server_default="member", - ) - ) - batch_op.add_column( - sa.Column("invitation_token", sa.String(length=100), nullable=True) - ) - batch_op.add_column( - sa.Column("invitation_sent_at", sa.DateTime(), nullable=True) - ) - batch_op.add_column( - sa.Column("invitation_accepted_at", sa.DateTime(), nullable=True) - ) - - # Create index on invitation_token - batch_op.create_index("idx_vendor_users_invitation_token", ["invitation_token"]) - - # Modify role_id to be nullable (this recreates the table in SQLite) - batch_op.alter_column("role_id", existing_type=sa.Integer(), nullable=True) - - # Change is_active default (this recreates the table in SQLite) - batch_op.alter_column( - "is_active", existing_type=sa.Boolean(), server_default="false" - ) - - # Set owners correctly (after table modifications) - # SQLite-compatible UPDATE with subquery - op.execute( - """ - UPDATE vendor_users - SET user_type = 'owner' - WHERE (vendor_id, user_id) IN ( - SELECT id, owner_user_id - FROM vendors - ) - """ - ) - - # Set existing owners as active - op.execute( - """ - UPDATE vendor_users - SET is_active = TRUE - WHERE user_type = 'owner' - """ - ) - - # ======================================================================== - # Role table changes - # ======================================================================== - with op.batch_alter_table("roles", schema=None) as batch_op: - # Create index on vendor_id and name - batch_op.create_index("idx_roles_vendor_name", ["vendor_id", "name"]) - - # Note: JSONB conversion only for PostgreSQL - # SQLite stores JSON as TEXT by default, no conversion needed - - -def downgrade(): - """Downgrade database schema. - - SQLite-compatible version using batch operations. - """ - - # ======================================================================== - # Role table changes - # ======================================================================== - with op.batch_alter_table("roles", schema=None) as batch_op: - batch_op.drop_index("idx_roles_vendor_name") - - # ======================================================================== - # VendorUser table changes - # ======================================================================== - with op.batch_alter_table("vendor_users", schema=None) as batch_op: - # Revert is_active default - batch_op.alter_column( - "is_active", existing_type=sa.Boolean(), server_default="true" - ) - - # Revert role_id to NOT NULL - # Note: This might fail if there are NULL values - batch_op.alter_column("role_id", existing_type=sa.Integer(), nullable=False) - - # Drop indexes and columns - batch_op.drop_index("idx_vendor_users_invitation_token") - batch_op.drop_column("invitation_accepted_at") - batch_op.drop_column("invitation_sent_at") - batch_op.drop_column("invitation_token") - batch_op.drop_column("user_type") - - # ======================================================================== - # User table changes - # ======================================================================== - with op.batch_alter_table("users", schema=None) as batch_op: - batch_op.drop_column("is_email_verified") diff --git a/alembic/versions/fcfdc02d5138_add_language_settings_to_vendor_user_.py b/alembic/versions/fcfdc02d5138_add_language_settings_to_vendor_user_.py deleted file mode 100644 index 34d7fcfb..00000000 --- a/alembic/versions/fcfdc02d5138_add_language_settings_to_vendor_user_.py +++ /dev/null @@ -1,84 +0,0 @@ -"""add_language_settings_to_vendor_user_customer - -Revision ID: fcfdc02d5138 -Revises: b412e0b49c2e -Create Date: 2025-12-13 20:08:27.120863 - -This migration adds language preference fields to support multi-language UI: -- Vendor: default_language, dashboard_language, storefront_language -- User: preferred_language -- Customer: preferred_language - -Supported languages: en (English), fr (French), de (German), lb (Luxembourgish) -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'fcfdc02d5138' -down_revision: Union[str, None] = 'b412e0b49c2e' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ======================================================================== - # Vendor language settings - # ======================================================================== - # default_language: Default language for vendor content (products, etc.) - op.add_column( - 'vendors', - sa.Column('default_language', sa.String(5), nullable=False, server_default='fr') - ) - # dashboard_language: Language for vendor team dashboard UI - op.add_column( - 'vendors', - sa.Column('dashboard_language', sa.String(5), nullable=False, server_default='fr') - ) - # storefront_language: Default language for customer-facing shop - op.add_column( - 'vendors', - sa.Column('storefront_language', sa.String(5), nullable=False, server_default='fr') - ) - # storefront_languages: JSON array of enabled languages for storefront - # Allows vendors to enable/disable specific languages - op.add_column( - 'vendors', - sa.Column( - 'storefront_languages', - sa.JSON, - nullable=False, - server_default='["fr", "de", "en"]' - ) - ) - - # ======================================================================== - # User language preference - # ======================================================================== - # preferred_language: User's preferred UI language (NULL = use context default) - op.add_column( - 'users', - sa.Column('preferred_language', sa.String(5), nullable=True) - ) - - # ======================================================================== - # Customer language preference - # ======================================================================== - # preferred_language: Customer's preferred language (NULL = use storefront default) - op.add_column( - 'customers', - sa.Column('preferred_language', sa.String(5), nullable=True) - ) - - -def downgrade() -> None: - # Remove columns in reverse order - op.drop_column('customers', 'preferred_language') - op.drop_column('users', 'preferred_language') - op.drop_column('vendors', 'storefront_languages') - op.drop_column('vendors', 'storefront_language') - op.drop_column('vendors', 'dashboard_language') - op.drop_column('vendors', 'default_language') diff --git a/alembic/versions/fef1d20ce8b4_add_content_pages_table_for_cms.py b/alembic/versions/fef1d20ce8b4_add_content_pages_table_for_cms.py deleted file mode 100644 index cc24f81a..00000000 --- a/alembic/versions/fef1d20ce8b4_add_content_pages_table_for_cms.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Add content_pages table for CMS - -Revision ID: fef1d20ce8b4 -Revises: fa7d4d10e358 -Create Date: 2025-11-22 13:41:18.069674 - -""" - -from typing import Sequence, Union - -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "fef1d20ce8b4" -down_revision: Union[str, None] = "fa7d4d10e358" -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.drop_index("idx_roles_vendor_name", table_name="roles") - op.drop_index("idx_vendor_users_invitation_token", table_name="vendor_users") - op.create_index( - op.f("ix_vendor_users_invitation_token"), - "vendor_users", - ["invitation_token"], - unique=False, - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f("ix_vendor_users_invitation_token"), table_name="vendor_users") - op.create_index( - "idx_vendor_users_invitation_token", - "vendor_users", - ["invitation_token"], - unique=False, - ) - op.create_index( - "idx_roles_vendor_name", "roles", ["vendor_id", "name"], unique=False - ) - # ### end Alembic commands ### diff --git a/alembic/versions/g5b6c7d8e9f0_add_scan_status_fields.py b/alembic/versions/g5b6c7d8e9f0_add_scan_status_fields.py deleted file mode 100644 index 1558f49c..00000000 --- a/alembic/versions/g5b6c7d8e9f0_add_scan_status_fields.py +++ /dev/null @@ -1,82 +0,0 @@ -"""add_scan_status_fields - -Add background task status fields to architecture_scans table -for harmonized background task architecture. - -Revision ID: g5b6c7d8e9f0 -Revises: f4a5b6c7d8e9 -Create Date: 2024-12-21 - -""" - -from collections.abc import Sequence - -import sqlalchemy as sa -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "g5b6c7d8e9f0" -down_revision: str | None = "f4a5b6c7d8e9" -branch_labels: str | Sequence[str] | None = None -depends_on: str | Sequence[str] | None = None - - -def upgrade() -> None: - # Add status field with default 'completed' for existing records - # New records will use 'pending' as default - op.add_column( - "architecture_scans", - sa.Column( - "status", - sa.String(length=30), - nullable=False, - server_default="completed", # Existing scans are already completed - ), - ) - op.create_index( - op.f("ix_architecture_scans_status"), "architecture_scans", ["status"] - ) - - # Add started_at - for existing records, use timestamp as started_at - op.add_column( - "architecture_scans", - sa.Column("started_at", sa.DateTime(timezone=True), nullable=True), - ) - - # Add completed_at - for existing records, use timestamp + duration as completed_at - op.add_column( - "architecture_scans", - sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), - ) - - # Add error_message for failed scans - op.add_column( - "architecture_scans", - sa.Column("error_message", sa.Text(), nullable=True), - ) - - # Add progress_message for showing current step - op.add_column( - "architecture_scans", - sa.Column("progress_message", sa.String(length=255), nullable=True), - ) - - # Update existing records to have proper started_at and completed_at - # This is done via raw SQL for efficiency (PostgreSQL syntax) - op.execute( - """ - UPDATE architecture_scans - SET started_at = timestamp, - completed_at = timestamp + (COALESCE(duration_seconds, 0) || ' seconds')::interval - WHERE started_at IS NULL - """ - ) - - -def downgrade() -> None: - op.drop_index(op.f("ix_architecture_scans_status"), table_name="architecture_scans") - op.drop_column("architecture_scans", "progress_message") - op.drop_column("architecture_scans", "error_message") - op.drop_column("architecture_scans", "completed_at") - op.drop_column("architecture_scans", "started_at") - op.drop_column("architecture_scans", "status") diff --git a/alembic/versions/h6c7d8e9f0a1_add_invoice_tables.py b/alembic/versions/h6c7d8e9f0a1_add_invoice_tables.py deleted file mode 100644 index b88cabc2..00000000 --- a/alembic/versions/h6c7d8e9f0a1_add_invoice_tables.py +++ /dev/null @@ -1,198 +0,0 @@ -"""Add invoice tables - -Revision ID: h6c7d8e9f0a1 -Revises: g5b6c7d8e9f0 -Create Date: 2025-12-24 - -This migration adds: -- vendor_invoice_settings: Per-vendor invoice configuration -- invoices: Invoice records with seller/buyer snapshots -""" - -from typing import Sequence, Union - -import sqlalchemy as sa -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "h6c7d8e9f0a1" -down_revision: Union[str, None] = "g5b6c7d8e9f0" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Create vendor_invoice_settings table - op.create_table( - "vendor_invoice_settings", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=False), - # Company details - sa.Column("company_name", sa.String(length=255), nullable=False), - sa.Column("company_address", sa.String(length=255), nullable=True), - sa.Column("company_city", sa.String(length=100), nullable=True), - sa.Column("company_postal_code", sa.String(length=20), nullable=True), - sa.Column( - "company_country", sa.String(length=2), server_default="LU", nullable=False - ), - # VAT information - sa.Column("vat_number", sa.String(length=50), nullable=True), - sa.Column("is_vat_registered", sa.Boolean(), server_default="1", nullable=False), - # OSS - sa.Column("is_oss_registered", sa.Boolean(), server_default="0", nullable=False), - sa.Column("oss_registration_country", sa.String(length=2), nullable=True), - # Invoice numbering - sa.Column( - "invoice_prefix", sa.String(length=20), server_default="INV", nullable=False - ), - sa.Column("invoice_next_number", sa.Integer(), server_default="1", nullable=False), - sa.Column( - "invoice_number_padding", sa.Integer(), server_default="5", nullable=False - ), - # Payment information - sa.Column("payment_terms", sa.Text(), nullable=True), - sa.Column("bank_name", sa.String(length=255), nullable=True), - sa.Column("bank_iban", sa.String(length=50), nullable=True), - sa.Column("bank_bic", sa.String(length=20), nullable=True), - # Footer - sa.Column("footer_text", sa.Text(), nullable=True), - # Default VAT rate - sa.Column( - "default_vat_rate", sa.Numeric(precision=5, scale=2), server_default="17.00", nullable=False - ), - # Timestamps - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=False, - ), - sa.Column( - "updated_at", - sa.DateTime(timezone=True), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=False, - ), - sa.ForeignKeyConstraint( - ["vendor_id"], - ["vendors.id"], - ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("vendor_id"), - ) - op.create_index( - op.f("ix_vendor_invoice_settings_id"), - "vendor_invoice_settings", - ["id"], - unique=False, - ) - op.create_index( - op.f("ix_vendor_invoice_settings_vendor_id"), - "vendor_invoice_settings", - ["vendor_id"], - unique=True, - ) - - # Create invoices table - op.create_table( - "invoices", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=False), - sa.Column("order_id", sa.Integer(), nullable=True), - # Invoice identification - sa.Column("invoice_number", sa.String(length=50), nullable=False), - sa.Column("invoice_date", sa.DateTime(timezone=True), nullable=False), - # Status - sa.Column( - "status", sa.String(length=20), server_default="draft", nullable=False - ), - # Snapshots (JSON) - sa.Column("seller_details", sa.JSON(), nullable=False), - sa.Column("buyer_details", sa.JSON(), nullable=False), - sa.Column("line_items", sa.JSON(), nullable=False), - # VAT information - sa.Column( - "vat_regime", sa.String(length=20), server_default="domestic", nullable=False - ), - sa.Column("destination_country", sa.String(length=2), nullable=True), - sa.Column("vat_rate", sa.Numeric(precision=5, scale=2), nullable=False), - sa.Column("vat_rate_label", sa.String(length=50), nullable=True), - # Amounts (in cents) - sa.Column("currency", sa.String(length=3), server_default="EUR", nullable=False), - sa.Column("subtotal_cents", sa.Integer(), nullable=False), - sa.Column("vat_amount_cents", sa.Integer(), nullable=False), - sa.Column("total_cents", sa.Integer(), nullable=False), - # Payment info - sa.Column("payment_terms", sa.Text(), nullable=True), - sa.Column("bank_details", sa.JSON(), nullable=True), - sa.Column("footer_text", sa.Text(), nullable=True), - # PDF - sa.Column("pdf_generated_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("pdf_path", sa.String(length=500), nullable=True), - # Notes - sa.Column("notes", sa.Text(), nullable=True), - # Timestamps - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=False, - ), - sa.Column( - "updated_at", - sa.DateTime(timezone=True), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=False, - ), - sa.ForeignKeyConstraint( - ["vendor_id"], - ["vendors.id"], - ), - sa.ForeignKeyConstraint( - ["order_id"], - ["orders.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_invoices_id"), "invoices", ["id"], unique=False) - op.create_index(op.f("ix_invoices_vendor_id"), "invoices", ["vendor_id"], unique=False) - op.create_index(op.f("ix_invoices_order_id"), "invoices", ["order_id"], unique=False) - op.create_index( - "idx_invoice_vendor_number", - "invoices", - ["vendor_id", "invoice_number"], - unique=True, - ) - op.create_index( - "idx_invoice_vendor_date", - "invoices", - ["vendor_id", "invoice_date"], - unique=False, - ) - op.create_index( - "idx_invoice_status", - "invoices", - ["vendor_id", "status"], - unique=False, - ) - - -def downgrade() -> None: - # Drop invoices table - op.drop_index("idx_invoice_status", table_name="invoices") - op.drop_index("idx_invoice_vendor_date", table_name="invoices") - op.drop_index("idx_invoice_vendor_number", table_name="invoices") - op.drop_index(op.f("ix_invoices_order_id"), table_name="invoices") - op.drop_index(op.f("ix_invoices_vendor_id"), table_name="invoices") - op.drop_index(op.f("ix_invoices_id"), table_name="invoices") - op.drop_table("invoices") - - # Drop vendor_invoice_settings table - op.drop_index( - op.f("ix_vendor_invoice_settings_vendor_id"), - table_name="vendor_invoice_settings", - ) - op.drop_index( - op.f("ix_vendor_invoice_settings_id"), table_name="vendor_invoice_settings" - ) - op.drop_table("vendor_invoice_settings") diff --git a/alembic/versions/i7d8e9f0a1b2_add_vendor_subscriptions.py b/alembic/versions/i7d8e9f0a1b2_add_vendor_subscriptions.py deleted file mode 100644 index cff6e188..00000000 --- a/alembic/versions/i7d8e9f0a1b2_add_vendor_subscriptions.py +++ /dev/null @@ -1,148 +0,0 @@ -"""Add vendor subscriptions table - -Revision ID: i7d8e9f0a1b2 -Revises: h6c7d8e9f0a1 -Create Date: 2025-12-24 - -This migration adds: -- vendor_subscriptions: Per-vendor subscription tracking with tier limits -""" - -from typing import Sequence, Union - -import sqlalchemy as sa -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "i7d8e9f0a1b2" -down_revision: Union[str, None] = "h6c7d8e9f0a1" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Create vendor_subscriptions table - op.create_table( - "vendor_subscriptions", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=False), - # Tier and status - sa.Column( - "tier", sa.String(length=20), server_default="essential", nullable=False - ), - sa.Column( - "status", sa.String(length=20), server_default="trial", nullable=False - ), - # Billing period - sa.Column("period_start", sa.DateTime(timezone=True), nullable=False), - sa.Column("period_end", sa.DateTime(timezone=True), nullable=False), - sa.Column("is_annual", sa.Boolean(), server_default="0", nullable=False), - # Trial - sa.Column("trial_ends_at", sa.DateTime(timezone=True), nullable=True), - # Usage counters - sa.Column("orders_this_period", sa.Integer(), server_default="0", nullable=False), - sa.Column("orders_limit_reached_at", sa.DateTime(timezone=True), nullable=True), - # Custom overrides - sa.Column("custom_orders_limit", sa.Integer(), nullable=True), - sa.Column("custom_products_limit", sa.Integer(), nullable=True), - sa.Column("custom_team_limit", sa.Integer(), nullable=True), - # Payment (future Stripe integration) - sa.Column("stripe_customer_id", sa.String(length=100), nullable=True), - sa.Column("stripe_subscription_id", sa.String(length=100), nullable=True), - # Cancellation - sa.Column("cancelled_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("cancellation_reason", sa.Text(), nullable=True), - # Timestamps - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=False, - ), - sa.Column( - "updated_at", - sa.DateTime(timezone=True), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=False, - ), - sa.ForeignKeyConstraint( - ["vendor_id"], - ["vendors.id"], - ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("vendor_id"), - ) - op.create_index( - op.f("ix_vendor_subscriptions_id"), - "vendor_subscriptions", - ["id"], - unique=False, - ) - op.create_index( - op.f("ix_vendor_subscriptions_vendor_id"), - "vendor_subscriptions", - ["vendor_id"], - unique=True, - ) - op.create_index( - op.f("ix_vendor_subscriptions_tier"), - "vendor_subscriptions", - ["tier"], - unique=False, - ) - op.create_index( - op.f("ix_vendor_subscriptions_status"), - "vendor_subscriptions", - ["status"], - unique=False, - ) - op.create_index( - op.f("ix_vendor_subscriptions_stripe_customer_id"), - "vendor_subscriptions", - ["stripe_customer_id"], - unique=False, - ) - op.create_index( - op.f("ix_vendor_subscriptions_stripe_subscription_id"), - "vendor_subscriptions", - ["stripe_subscription_id"], - unique=False, - ) - op.create_index( - "idx_subscription_vendor_status", - "vendor_subscriptions", - ["vendor_id", "status"], - unique=False, - ) - op.create_index( - "idx_subscription_period", - "vendor_subscriptions", - ["period_start", "period_end"], - unique=False, - ) - - -def downgrade() -> None: - op.drop_index("idx_subscription_period", table_name="vendor_subscriptions") - op.drop_index("idx_subscription_vendor_status", table_name="vendor_subscriptions") - op.drop_index( - op.f("ix_vendor_subscriptions_stripe_subscription_id"), - table_name="vendor_subscriptions", - ) - op.drop_index( - op.f("ix_vendor_subscriptions_stripe_customer_id"), - table_name="vendor_subscriptions", - ) - op.drop_index( - op.f("ix_vendor_subscriptions_status"), table_name="vendor_subscriptions" - ) - op.drop_index( - op.f("ix_vendor_subscriptions_tier"), table_name="vendor_subscriptions" - ) - op.drop_index( - op.f("ix_vendor_subscriptions_vendor_id"), table_name="vendor_subscriptions" - ) - op.drop_index( - op.f("ix_vendor_subscriptions_id"), table_name="vendor_subscriptions" - ) - op.drop_table("vendor_subscriptions") diff --git a/alembic/versions/j8e9f0a1b2c3_product_independence_populate_fields.py b/alembic/versions/j8e9f0a1b2c3_product_independence_populate_fields.py deleted file mode 100644 index e03e6244..00000000 --- a/alembic/versions/j8e9f0a1b2c3_product_independence_populate_fields.py +++ /dev/null @@ -1,262 +0,0 @@ -"""Populate product fields from marketplace for independence refactor - -Revision ID: j8e9f0a1b2c3 -Revises: i7d8e9f0a1b2 -Create Date: 2025-12-24 - -This migration populates NULL fields on products and product_translations -with values from their linked marketplace products. This is part of the -"product independence" refactor where products become standalone entities -instead of inheriting from marketplace products via NULL fallback. - -After this migration: -- All Product fields will have actual values (no NULL inheritance) -- All ProductTranslation records will exist with actual values -- The marketplace_product_id FK is kept for "view original source" feature -""" - -from typing import Sequence, Union - -from alembic import op -from sqlalchemy import text - -# revision identifiers, used by Alembic. -revision: str = "j8e9f0a1b2c3" -down_revision: Union[str, None] = "i7d8e9f0a1b2" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Populate NULL product fields with marketplace product values.""" - - # Get database connection for raw SQL - connection = op.get_bind() - - # ========================================================================= - # STEP 1: Populate Product fields from MarketplaceProduct - # ========================================================================= - - # Price cents - connection.execute(text(""" - UPDATE products - SET price_cents = ( - SELECT mp.price_cents - FROM marketplace_products mp - WHERE mp.id = products.marketplace_product_id - ) - WHERE price_cents IS NULL - AND marketplace_product_id IS NOT NULL - """)) - - # Sale price cents - connection.execute(text(""" - UPDATE products - SET sale_price_cents = ( - SELECT mp.sale_price_cents - FROM marketplace_products mp - WHERE mp.id = products.marketplace_product_id - ) - WHERE sale_price_cents IS NULL - AND marketplace_product_id IS NOT NULL - """)) - - # Currency (default to EUR if marketplace has NULL) - connection.execute(text(""" - UPDATE products - SET currency = COALESCE( - (SELECT mp.currency FROM marketplace_products mp WHERE mp.id = products.marketplace_product_id), - 'EUR' - ) - WHERE currency IS NULL - AND marketplace_product_id IS NOT NULL - """)) - - # Brand - connection.execute(text(""" - UPDATE products - SET brand = ( - SELECT mp.brand - FROM marketplace_products mp - WHERE mp.id = products.marketplace_product_id - ) - WHERE brand IS NULL - AND marketplace_product_id IS NOT NULL - """)) - - # Condition - connection.execute(text(""" - UPDATE products - SET condition = ( - SELECT mp.condition - FROM marketplace_products mp - WHERE mp.id = products.marketplace_product_id - ) - WHERE condition IS NULL - AND marketplace_product_id IS NOT NULL - """)) - - # Availability - connection.execute(text(""" - UPDATE products - SET availability = ( - SELECT mp.availability - FROM marketplace_products mp - WHERE mp.id = products.marketplace_product_id - ) - WHERE availability IS NULL - AND marketplace_product_id IS NOT NULL - """)) - - # Primary image URL (marketplace uses 'image_link') - connection.execute(text(""" - UPDATE products - SET primary_image_url = ( - SELECT mp.image_link - FROM marketplace_products mp - WHERE mp.id = products.marketplace_product_id - ) - WHERE primary_image_url IS NULL - AND marketplace_product_id IS NOT NULL - """)) - - # Additional images - connection.execute(text(""" - UPDATE products - SET additional_images = ( - SELECT mp.additional_images - FROM marketplace_products mp - WHERE mp.id = products.marketplace_product_id - ) - WHERE additional_images IS NULL - AND marketplace_product_id IS NOT NULL - """)) - - # ========================================================================= - # STEP 2: Create missing ProductTranslation records from MarketplaceProductTranslation - # ========================================================================= - - # Insert missing translations (where product doesn't have translation for a language - # that the marketplace product has) - connection.execute(text(""" - INSERT INTO product_translations (product_id, language, title, description, short_description, - meta_title, meta_description, url_slug, created_at, updated_at) - SELECT - p.id, - mpt.language, - mpt.title, - mpt.description, - mpt.short_description, - mpt.meta_title, - mpt.meta_description, - mpt.url_slug, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP - FROM products p - JOIN marketplace_products mp ON mp.id = p.marketplace_product_id - JOIN marketplace_product_translations mpt ON mpt.marketplace_product_id = mp.id - WHERE NOT EXISTS ( - SELECT 1 FROM product_translations pt - WHERE pt.product_id = p.id AND pt.language = mpt.language - ) - """)) - - # ========================================================================= - # STEP 3: Update existing ProductTranslation NULL fields with marketplace values - # ========================================================================= - - # Update title where NULL - connection.execute(text(""" - UPDATE product_translations - SET title = ( - SELECT mpt.title - FROM products p - JOIN marketplace_products mp ON mp.id = p.marketplace_product_id - JOIN marketplace_product_translations mpt ON mpt.marketplace_product_id = mp.id - AND mpt.language = product_translations.language - WHERE p.id = product_translations.product_id - ) - WHERE title IS NULL - """)) - - # Update description where NULL - connection.execute(text(""" - UPDATE product_translations - SET description = ( - SELECT mpt.description - FROM products p - JOIN marketplace_products mp ON mp.id = p.marketplace_product_id - JOIN marketplace_product_translations mpt ON mpt.marketplace_product_id = mp.id - AND mpt.language = product_translations.language - WHERE p.id = product_translations.product_id - ) - WHERE description IS NULL - """)) - - # Update short_description where NULL - connection.execute(text(""" - UPDATE product_translations - SET short_description = ( - SELECT mpt.short_description - FROM products p - JOIN marketplace_products mp ON mp.id = p.marketplace_product_id - JOIN marketplace_product_translations mpt ON mpt.marketplace_product_id = mp.id - AND mpt.language = product_translations.language - WHERE p.id = product_translations.product_id - ) - WHERE short_description IS NULL - """)) - - # Update meta_title where NULL - connection.execute(text(""" - UPDATE product_translations - SET meta_title = ( - SELECT mpt.meta_title - FROM products p - JOIN marketplace_products mp ON mp.id = p.marketplace_product_id - JOIN marketplace_product_translations mpt ON mpt.marketplace_product_id = mp.id - AND mpt.language = product_translations.language - WHERE p.id = product_translations.product_id - ) - WHERE meta_title IS NULL - """)) - - # Update meta_description where NULL - connection.execute(text(""" - UPDATE product_translations - SET meta_description = ( - SELECT mpt.meta_description - FROM products p - JOIN marketplace_products mp ON mp.id = p.marketplace_product_id - JOIN marketplace_product_translations mpt ON mpt.marketplace_product_id = mp.id - AND mpt.language = product_translations.language - WHERE p.id = product_translations.product_id - ) - WHERE meta_description IS NULL - """)) - - # Update url_slug where NULL - connection.execute(text(""" - UPDATE product_translations - SET url_slug = ( - SELECT mpt.url_slug - FROM products p - JOIN marketplace_products mp ON mp.id = p.marketplace_product_id - JOIN marketplace_product_translations mpt ON mpt.marketplace_product_id = mp.id - AND mpt.language = product_translations.language - WHERE p.id = product_translations.product_id - ) - WHERE url_slug IS NULL - """)) - - -def downgrade() -> None: - """ - Downgrade is a no-op for data population. - - The data was copied, not moved. The original marketplace product data - is still intact. We don't reset fields to NULL because: - 1. It would lose any vendor customizations made after migration - 2. The model code may still work with populated fields - """ - pass diff --git a/alembic/versions/k9f0a1b2c3d4_add_tier_id_fk_to_subscriptions.py b/alembic/versions/k9f0a1b2c3d4_add_tier_id_fk_to_subscriptions.py deleted file mode 100644 index b7d74ac5..00000000 --- a/alembic/versions/k9f0a1b2c3d4_add_tier_id_fk_to_subscriptions.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Add tier_id FK to vendor_subscriptions - -Revision ID: k9f0a1b2c3d4 -Revises: 2953ed10d22c -Create Date: 2025-12-26 - -Adds tier_id column to vendor_subscriptions table with FK to subscription_tiers. -Backfills tier_id based on existing tier (code) values. -""" - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = "k9f0a1b2c3d4" -down_revision = "2953ed10d22c" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # Use batch mode for SQLite compatibility - with op.batch_alter_table("vendor_subscriptions", schema=None) as batch_op: - # Add tier_id column (nullable for backfill) - batch_op.add_column( - sa.Column("tier_id", sa.Integer(), nullable=True) - ) - # Create index for tier_id - batch_op.create_index( - "ix_vendor_subscriptions_tier_id", - ["tier_id"], - unique=False, - ) - # Add FK constraint - batch_op.create_foreign_key( - "fk_vendor_subscriptions_tier_id", - "subscription_tiers", - ["tier_id"], - ["id"], - ondelete="SET NULL", - ) - - # Backfill tier_id from tier code - # This updates existing subscriptions to link to their tier - op.execute( - """ - UPDATE vendor_subscriptions - SET tier_id = ( - SELECT id FROM subscription_tiers - WHERE subscription_tiers.code = vendor_subscriptions.tier - ) - WHERE EXISTS ( - SELECT 1 FROM subscription_tiers - WHERE subscription_tiers.code = vendor_subscriptions.tier - ) - """ - ) - - -def downgrade() -> None: - # In SQLite batch mode, we must explicitly drop the index before dropping - # the column, otherwise batch mode will try to recreate the index on the - # new table (which won't have the column). - with op.batch_alter_table("vendor_subscriptions", schema=None) as batch_op: - # First drop the index on tier_id - batch_op.drop_index("ix_vendor_subscriptions_tier_id") - # Then drop the column (FK is automatically removed with the column) - batch_op.drop_column("tier_id") diff --git a/alembic/versions/l0a1b2c3d4e5_add_capacity_snapshots_table.py b/alembic/versions/l0a1b2c3d4e5_add_capacity_snapshots_table.py deleted file mode 100644 index a87beb08..00000000 --- a/alembic/versions/l0a1b2c3d4e5_add_capacity_snapshots_table.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Add capacity_snapshots table - -Revision ID: l0a1b2c3d4e5 -Revises: k9f0a1b2c3d4 -Create Date: 2025-12-26 - -Adds table for tracking daily platform capacity metrics for growth forecasting. -""" - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = "l0a1b2c3d4e5" -down_revision = "k9f0a1b2c3d4" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - op.create_table( - "capacity_snapshots", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("snapshot_date", sa.DateTime(timezone=True), nullable=False), - # Vendor metrics - sa.Column("total_vendors", sa.Integer(), nullable=False, server_default="0"), - sa.Column("active_vendors", sa.Integer(), nullable=False, server_default="0"), - sa.Column("trial_vendors", sa.Integer(), nullable=False, server_default="0"), - # Subscription metrics - sa.Column("total_subscriptions", sa.Integer(), nullable=False, server_default="0"), - sa.Column("active_subscriptions", sa.Integer(), nullable=False, server_default="0"), - # Resource metrics - sa.Column("total_products", sa.Integer(), nullable=False, server_default="0"), - sa.Column("total_orders_month", sa.Integer(), nullable=False, server_default="0"), - sa.Column("total_team_members", sa.Integer(), nullable=False, server_default="0"), - # Storage metrics - sa.Column("storage_used_gb", sa.Numeric(10, 2), nullable=False, server_default="0"), - sa.Column("db_size_mb", sa.Numeric(10, 2), nullable=False, server_default="0"), - # Capacity metrics - sa.Column("theoretical_products_limit", sa.Integer(), nullable=True), - sa.Column("theoretical_orders_limit", sa.Integer(), nullable=True), - sa.Column("theoretical_team_limit", sa.Integer(), nullable=True), - # Tier distribution - sa.Column("tier_distribution", sa.JSON(), nullable=True), - # Performance metrics - sa.Column("avg_response_ms", sa.Integer(), nullable=True), - sa.Column("peak_cpu_percent", sa.Numeric(5, 2), nullable=True), - sa.Column("peak_memory_percent", sa.Numeric(5, 2), nullable=True), - # Timestamps - sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), - # Primary key - sa.PrimaryKeyConstraint("id"), - ) - - # Create indexes - op.create_index("ix_capacity_snapshots_id", "capacity_snapshots", ["id"], unique=False) - op.create_index("ix_capacity_snapshots_date", "capacity_snapshots", ["snapshot_date"], unique=True) - - -def downgrade() -> None: - op.drop_index("ix_capacity_snapshots_date", table_name="capacity_snapshots") - op.drop_index("ix_capacity_snapshots_id", table_name="capacity_snapshots") - op.drop_table("capacity_snapshots") diff --git a/alembic/versions/m1b2c3d4e5f6_add_vendor_onboarding_table.py b/alembic/versions/m1b2c3d4e5f6_add_vendor_onboarding_table.py deleted file mode 100644 index 849da006..00000000 --- a/alembic/versions/m1b2c3d4e5f6_add_vendor_onboarding_table.py +++ /dev/null @@ -1,71 +0,0 @@ -"""add vendor onboarding table - -Revision ID: m1b2c3d4e5f6 -Revises: d7a4a3f06394 -Create Date: 2025-12-27 22:00:00.000000 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'm1b2c3d4e5f6' -down_revision: Union[str, None] = 'd7a4a3f06394' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - op.create_table('vendor_onboarding', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('vendor_id', sa.Integer(), nullable=False), - # Overall status - sa.Column('status', sa.String(length=20), nullable=False, server_default='not_started'), - sa.Column('current_step', sa.String(length=30), nullable=False, server_default='company_profile'), - # Step 1: Company Profile - sa.Column('step_company_profile_completed', sa.Boolean(), nullable=False, server_default=sa.text('false')), - sa.Column('step_company_profile_completed_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('step_company_profile_data', sa.JSON(), nullable=True), - # Step 2: Letzshop API Configuration - sa.Column('step_letzshop_api_completed', sa.Boolean(), nullable=False, server_default=sa.text('false')), - sa.Column('step_letzshop_api_completed_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('step_letzshop_api_connection_verified', sa.Boolean(), nullable=False, server_default=sa.text('false')), - # Step 3: Product Import - sa.Column('step_product_import_completed', sa.Boolean(), nullable=False, server_default=sa.text('false')), - sa.Column('step_product_import_completed_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('step_product_import_csv_url_set', sa.Boolean(), nullable=False, server_default=sa.text('false')), - # Step 4: Order Sync - sa.Column('step_order_sync_completed', sa.Boolean(), nullable=False, server_default=sa.text('false')), - sa.Column('step_order_sync_completed_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('step_order_sync_job_id', sa.Integer(), nullable=True), - # Completion tracking - sa.Column('started_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), - # Admin override - sa.Column('skipped_by_admin', sa.Boolean(), nullable=False, server_default=sa.text('false')), - sa.Column('skipped_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('skipped_reason', sa.Text(), nullable=True), - sa.Column('skipped_by_user_id', sa.Integer(), nullable=True), - # Timestamps - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - # Constraints - sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['skipped_by_user_id'], ['users.id']), - sa.PrimaryKeyConstraint('id'), - ) - op.create_index(op.f('ix_vendor_onboarding_id'), 'vendor_onboarding', ['id'], unique=False) - op.create_index(op.f('ix_vendor_onboarding_vendor_id'), 'vendor_onboarding', ['vendor_id'], unique=True) - op.create_index(op.f('ix_vendor_onboarding_status'), 'vendor_onboarding', ['status'], unique=False) - op.create_index('idx_onboarding_vendor_status', 'vendor_onboarding', ['vendor_id', 'status'], unique=False) - - -def downgrade() -> None: - op.drop_index('idx_onboarding_vendor_status', table_name='vendor_onboarding') - op.drop_index(op.f('ix_vendor_onboarding_status'), table_name='vendor_onboarding') - op.drop_index(op.f('ix_vendor_onboarding_vendor_id'), table_name='vendor_onboarding') - op.drop_index(op.f('ix_vendor_onboarding_id'), table_name='vendor_onboarding') - op.drop_table('vendor_onboarding') diff --git a/alembic/versions/n2c3d4e5f6a7_add_features_table.py b/alembic/versions/n2c3d4e5f6a7_add_features_table.py deleted file mode 100644 index 27b36e85..00000000 --- a/alembic/versions/n2c3d4e5f6a7_add_features_table.py +++ /dev/null @@ -1,292 +0,0 @@ -"""add features table and seed data - -Revision ID: n2c3d4e5f6a7 -Revises: ba2c0ce78396 -Create Date: 2025-12-31 10:00:00.000000 - -""" - -import json -from typing import Sequence, Union - -import sqlalchemy as sa -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "n2c3d4e5f6a7" -down_revision: Union[str, None] = "ba2c0ce78396" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -# ============================================================================ -# Feature Definitions -# ============================================================================ -# category, code, name, description, ui_location, ui_icon, ui_route, display_order -FEATURES = [ - # Orders (category: orders) - ("orders", "order_management", "Order Management", "View and manage orders", "sidebar", "clipboard-list", "/vendor/{code}/orders", 1), - ("orders", "order_bulk_actions", "Bulk Order Actions", "Process multiple orders at once", "inline", None, None, 2), - ("orders", "order_export", "Order Export", "Export orders to CSV/Excel", "inline", "download", None, 3), - ("orders", "automation_rules", "Automation Rules", "Automatic order processing rules", "sidebar", "cog", "/vendor/{code}/automation", 4), - - # Inventory (category: inventory) - ("inventory", "inventory_basic", "Basic Inventory", "Track product stock levels", "sidebar", "cube", "/vendor/{code}/inventory", 1), - ("inventory", "inventory_locations", "Warehouse Locations", "Manage multiple warehouse locations", "inline", "map-pin", None, 2), - ("inventory", "inventory_purchase_orders", "Purchase Orders", "Create and manage purchase orders", "sidebar", "shopping-cart", "/vendor/{code}/purchase-orders", 3), - ("inventory", "low_stock_alerts", "Low Stock Alerts", "Get notified when stock is low", "inline", "bell", None, 4), - - # Analytics (category: analytics) - ("analytics", "basic_reports", "Basic Reports", "Essential sales and order reports", "sidebar", "chart-pie", "/vendor/{code}/reports", 1), - ("analytics", "analytics_dashboard", "Analytics Dashboard", "Advanced analytics with charts and trends", "sidebar", "chart-bar", "/vendor/{code}/analytics", 2), - ("analytics", "custom_reports", "Custom Reports", "Build custom report configurations", "inline", "document-report", None, 3), - ("analytics", "export_reports", "Export Reports", "Export reports to various formats", "inline", "download", None, 4), - - # Invoicing (category: invoicing) - ("invoicing", "invoice_lu", "Luxembourg Invoicing", "Generate compliant Luxembourg invoices", "sidebar", "document-text", "/vendor/{code}/invoices", 1), - ("invoicing", "invoice_eu_vat", "EU VAT Support", "Handle EU VAT for cross-border sales", "inline", "globe", None, 2), - ("invoicing", "invoice_bulk", "Bulk Invoicing", "Generate invoices in bulk", "inline", "document-duplicate", None, 3), - ("invoicing", "accounting_export", "Accounting Export", "Export to accounting software formats", "inline", "calculator", None, 4), - - # Integrations (category: integrations) - ("integrations", "letzshop_sync", "Letzshop Sync", "Sync orders and products with Letzshop", "settings", "refresh", None, 1), - ("integrations", "api_access", "API Access", "REST API access for custom integrations", "settings", "code", "/vendor/{code}/settings/api", 2), - ("integrations", "webhooks", "Webhooks", "Receive real-time event notifications", "settings", "lightning-bolt", "/vendor/{code}/settings/webhooks", 3), - ("integrations", "custom_integrations", "Custom Integrations", "Connect with any third-party service", "settings", "puzzle", None, 4), - - # Team (category: team) - ("team", "single_user", "Single User", "One user account", "api", None, None, 1), - ("team", "team_basic", "Team Access", "Invite team members", "sidebar", "users", "/vendor/{code}/team", 2), - ("team", "team_roles", "Team Roles", "Role-based permissions for team members", "inline", "shield-check", None, 3), - ("team", "audit_log", "Audit Log", "Track all user actions", "sidebar", "clipboard-check", "/vendor/{code}/audit-log", 4), - - # Branding (category: branding) - ("branding", "basic_shop", "Basic Shop", "Your shop on the platform", "api", None, None, 1), - ("branding", "custom_domain", "Custom Domain", "Use your own domain name", "settings", "globe-alt", None, 2), - ("branding", "white_label", "White Label", "Remove platform branding entirely", "settings", "color-swatch", None, 3), - - # Customers (category: customers) - ("customers", "customer_view", "Customer View", "View customer information", "sidebar", "user-group", "/vendor/{code}/customers", 1), - ("customers", "customer_export", "Customer Export", "Export customer data", "inline", "download", None, 2), - ("customers", "customer_messaging", "Customer Messaging", "Send messages to customers", "inline", "chat", None, 3), -] - -# ============================================================================ -# Tier Feature Assignments -# ============================================================================ -# tier_code -> list of feature codes -TIER_FEATURES = { - "essential": [ - "order_management", - "inventory_basic", - "basic_reports", - "invoice_lu", - "letzshop_sync", - "single_user", - "basic_shop", - "customer_view", - ], - "professional": [ - # All Essential features - "order_management", - "order_bulk_actions", - "order_export", - "inventory_basic", - "inventory_locations", - "inventory_purchase_orders", - "low_stock_alerts", - "basic_reports", - "invoice_lu", - "invoice_eu_vat", - "letzshop_sync", - "team_basic", - "basic_shop", - "customer_view", - "customer_export", - ], - "business": [ - # All Professional features - "order_management", - "order_bulk_actions", - "order_export", - "automation_rules", - "inventory_basic", - "inventory_locations", - "inventory_purchase_orders", - "low_stock_alerts", - "basic_reports", - "analytics_dashboard", - "custom_reports", - "export_reports", - "invoice_lu", - "invoice_eu_vat", - "invoice_bulk", - "accounting_export", - "letzshop_sync", - "api_access", - "webhooks", - "team_basic", - "team_roles", - "audit_log", - "basic_shop", - "custom_domain", - "customer_view", - "customer_export", - "customer_messaging", - ], - "enterprise": [ - # All features - "order_management", - "order_bulk_actions", - "order_export", - "automation_rules", - "inventory_basic", - "inventory_locations", - "inventory_purchase_orders", - "low_stock_alerts", - "basic_reports", - "analytics_dashboard", - "custom_reports", - "export_reports", - "invoice_lu", - "invoice_eu_vat", - "invoice_bulk", - "accounting_export", - "letzshop_sync", - "api_access", - "webhooks", - "custom_integrations", - "team_basic", - "team_roles", - "audit_log", - "basic_shop", - "custom_domain", - "white_label", - "customer_view", - "customer_export", - "customer_messaging", - ], -} - -# Minimum tier for each feature (for upgrade prompts) -# Maps feature_code -> tier_code -MINIMUM_TIER = { - # Essential - "order_management": "essential", - "inventory_basic": "essential", - "basic_reports": "essential", - "invoice_lu": "essential", - "letzshop_sync": "essential", - "single_user": "essential", - "basic_shop": "essential", - "customer_view": "essential", - # Professional - "order_bulk_actions": "professional", - "order_export": "professional", - "inventory_locations": "professional", - "inventory_purchase_orders": "professional", - "low_stock_alerts": "professional", - "invoice_eu_vat": "professional", - "team_basic": "professional", - "customer_export": "professional", - # Business - "automation_rules": "business", - "analytics_dashboard": "business", - "custom_reports": "business", - "export_reports": "business", - "invoice_bulk": "business", - "accounting_export": "business", - "api_access": "business", - "webhooks": "business", - "team_roles": "business", - "audit_log": "business", - "custom_domain": "business", - "customer_messaging": "business", - # Enterprise - "custom_integrations": "enterprise", - "white_label": "enterprise", -} - - -def upgrade() -> None: - # Create features table - op.create_table( - "features", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("code", sa.String(50), nullable=False), - sa.Column("name", sa.String(100), nullable=False), - sa.Column("description", sa.Text(), nullable=True), - sa.Column("category", sa.String(50), nullable=False), - sa.Column("ui_location", sa.String(50), nullable=True), - sa.Column("ui_icon", sa.String(50), nullable=True), - sa.Column("ui_route", sa.String(100), nullable=True), - sa.Column("ui_badge_text", sa.String(20), nullable=True), - sa.Column("minimum_tier_id", sa.Integer(), nullable=True), - sa.Column("is_active", sa.Boolean(), nullable=False, default=True), - sa.Column("is_visible", sa.Boolean(), nullable=False, default=True), - sa.Column("display_order", sa.Integer(), nullable=False, default=0), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(["minimum_tier_id"], ["subscription_tiers.id"]), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index("ix_features_code", "features", ["code"], unique=True) - op.create_index("ix_features_category", "features", ["category"], unique=False) - op.create_index("idx_feature_category_order", "features", ["category", "display_order"]) - op.create_index("idx_feature_active_visible", "features", ["is_active", "is_visible"]) - - # Get connection for data operations - conn = op.get_bind() - - # Get tier IDs - tier_ids = {} - result = conn.execute(sa.text("SELECT id, code FROM subscription_tiers")) - for row in result: - tier_ids[row[1]] = row[0] - - # Insert features - now = sa.func.now() - for category, code, name, description, ui_location, ui_icon, ui_route, display_order in FEATURES: - minimum_tier_code = MINIMUM_TIER.get(code) - minimum_tier_id = tier_ids.get(minimum_tier_code) if minimum_tier_code else None - - conn.execute( - sa.text(""" - INSERT INTO features (code, name, description, category, ui_location, ui_icon, ui_route, - minimum_tier_id, is_active, is_visible, display_order, created_at, updated_at) - VALUES (:code, :name, :description, :category, :ui_location, :ui_icon, :ui_route, - :minimum_tier_id, true, true, :display_order, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - """), - { - "code": code, - "name": name, - "description": description, - "category": category, - "ui_location": ui_location, - "ui_icon": ui_icon, - "ui_route": ui_route, - "minimum_tier_id": minimum_tier_id, - "display_order": display_order, - }, - ) - - # Update subscription_tiers with feature arrays - for tier_code, features in TIER_FEATURES.items(): - features_json = json.dumps(features) - conn.execute( - sa.text("UPDATE subscription_tiers SET features = :features WHERE code = :code"), - {"features": features_json, "code": tier_code}, - ) - - -def downgrade() -> None: - # Clear features from subscription_tiers - conn = op.get_bind() - conn.execute(sa.text("UPDATE subscription_tiers SET features = '[]'")) - - # Drop features table - op.drop_index("idx_feature_active_visible", table_name="features") - op.drop_index("idx_feature_category_order", table_name="features") - op.drop_index("ix_features_category", table_name="features") - op.drop_index("ix_features_code", table_name="features") - op.drop_table("features") diff --git a/alembic/versions/o3c4d5e6f7a8_add_inventory_transactions_table.py b/alembic/versions/o3c4d5e6f7a8_add_inventory_transactions_table.py deleted file mode 100644 index fef620d5..00000000 --- a/alembic/versions/o3c4d5e6f7a8_add_inventory_transactions_table.py +++ /dev/null @@ -1,144 +0,0 @@ -"""Add inventory_transactions table - -Revision ID: o3c4d5e6f7a8 -Revises: n2c3d4e5f6a7 -Create Date: 2026-01-01 - -Adds an audit trail for inventory movements: -- Track all stock changes (reserve, fulfill, release, adjust, set) -- Link transactions to orders for traceability -- Store quantity snapshots for historical analysis -""" - -from alembic import op -import sqlalchemy as sa - -# revision identifiers, used by Alembic. -revision = "o3c4d5e6f7a8" -down_revision = "n2c3d4e5f6a7" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # Create transaction type enum - transaction_type_enum = sa.Enum( - "reserve", - "fulfill", - "release", - "adjust", - "set", - "import", - "return", - name="transactiontype", - ) - - op.create_table( - "inventory_transactions", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=False), - sa.Column("product_id", sa.Integer(), nullable=False), - sa.Column("inventory_id", sa.Integer(), nullable=True), - sa.Column("transaction_type", transaction_type_enum, nullable=False), - sa.Column("quantity_change", sa.Integer(), nullable=False), - sa.Column("quantity_after", sa.Integer(), nullable=False), - sa.Column("reserved_after", sa.Integer(), nullable=False, server_default="0"), - sa.Column("location", sa.String(), nullable=True), - sa.Column("warehouse", sa.String(), nullable=True), - sa.Column("order_id", sa.Integer(), nullable=True), - sa.Column("order_number", sa.String(), nullable=True), - sa.Column("reason", sa.Text(), nullable=True), - sa.Column("created_by", sa.String(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - nullable=False, - server_default=sa.func.now(), - ), - sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"]), - sa.ForeignKeyConstraint(["product_id"], ["products.id"]), - sa.ForeignKeyConstraint(["inventory_id"], ["inventory.id"]), - sa.ForeignKeyConstraint(["order_id"], ["orders.id"]), - sa.PrimaryKeyConstraint("id"), - ) - - # Create indexes - op.create_index( - "ix_inventory_transactions_id", - "inventory_transactions", - ["id"], - ) - op.create_index( - "ix_inventory_transactions_vendor_id", - "inventory_transactions", - ["vendor_id"], - ) - op.create_index( - "ix_inventory_transactions_product_id", - "inventory_transactions", - ["product_id"], - ) - op.create_index( - "ix_inventory_transactions_inventory_id", - "inventory_transactions", - ["inventory_id"], - ) - op.create_index( - "ix_inventory_transactions_transaction_type", - "inventory_transactions", - ["transaction_type"], - ) - op.create_index( - "ix_inventory_transactions_order_id", - "inventory_transactions", - ["order_id"], - ) - op.create_index( - "ix_inventory_transactions_created_at", - "inventory_transactions", - ["created_at"], - ) - op.create_index( - "idx_inv_tx_vendor_product", - "inventory_transactions", - ["vendor_id", "product_id"], - ) - op.create_index( - "idx_inv_tx_vendor_created", - "inventory_transactions", - ["vendor_id", "created_at"], - ) - op.create_index( - "idx_inv_tx_type_created", - "inventory_transactions", - ["transaction_type", "created_at"], - ) - - -def downgrade() -> None: - op.drop_index("idx_inv_tx_type_created", table_name="inventory_transactions") - op.drop_index("idx_inv_tx_vendor_created", table_name="inventory_transactions") - op.drop_index("idx_inv_tx_vendor_product", table_name="inventory_transactions") - op.drop_index( - "ix_inventory_transactions_created_at", table_name="inventory_transactions" - ) - op.drop_index( - "ix_inventory_transactions_order_id", table_name="inventory_transactions" - ) - op.drop_index( - "ix_inventory_transactions_transaction_type", table_name="inventory_transactions" - ) - op.drop_index( - "ix_inventory_transactions_inventory_id", table_name="inventory_transactions" - ) - op.drop_index( - "ix_inventory_transactions_product_id", table_name="inventory_transactions" - ) - op.drop_index( - "ix_inventory_transactions_vendor_id", table_name="inventory_transactions" - ) - op.drop_index("ix_inventory_transactions_id", table_name="inventory_transactions") - op.drop_table("inventory_transactions") - - # Drop enum - sa.Enum(name="transactiontype").drop(op.get_bind(), checkfirst=True) diff --git a/alembic/versions/p4d5e6f7a8b9_add_shipped_quantity_to_order_items.py b/alembic/versions/p4d5e6f7a8b9_add_shipped_quantity_to_order_items.py deleted file mode 100644 index cbd2069b..00000000 --- a/alembic/versions/p4d5e6f7a8b9_add_shipped_quantity_to_order_items.py +++ /dev/null @@ -1,39 +0,0 @@ -# alembic/versions/p4d5e6f7a8b9_add_shipped_quantity_to_order_items.py -"""Add shipped_quantity to order_items for partial shipments. - -Revision ID: p4d5e6f7a8b9 -Revises: o3c4d5e6f7a8 -Create Date: 2026-01-01 12:00:00.000000 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'p4d5e6f7a8b9' -down_revision: Union[str, None] = 'o3c4d5e6f7a8' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Add shipped_quantity column to order_items - op.add_column( - 'order_items', - sa.Column('shipped_quantity', sa.Integer(), nullable=False, server_default='0') - ) - - # Set shipped_quantity = quantity for already fulfilled items - # This handles existing data where inventory_fulfilled is True - op.execute(""" - UPDATE order_items - SET shipped_quantity = quantity - WHERE inventory_fulfilled = true - """) - - -def downgrade() -> None: - op.drop_column('order_items', 'shipped_quantity') diff --git a/alembic/versions/q5e6f7a8b9c0_add_vat_fields_to_orders.py b/alembic/versions/q5e6f7a8b9c0_add_vat_fields_to_orders.py deleted file mode 100644 index b7926c76..00000000 --- a/alembic/versions/q5e6f7a8b9c0_add_vat_fields_to_orders.py +++ /dev/null @@ -1,72 +0,0 @@ -# alembic/versions/q5e6f7a8b9c0_add_vat_fields_to_orders.py -"""Add VAT fields to orders table. - -Adds vat_regime, vat_rate, vat_rate_label, and vat_destination_country -to enable proper VAT tracking at order creation time, aligned with -invoice VAT logic. - -Revision ID: q5e6f7a8b9c0 -Revises: p4d5e6f7a8b9 -Create Date: 2026-01-02 10:00:00.000000 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'q5e6f7a8b9c0' -down_revision: Union[str, None] = 'p4d5e6f7a8b9' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Add VAT regime (domestic, oss, reverse_charge, origin, exempt) - op.add_column( - 'orders', - sa.Column('vat_regime', sa.String(20), nullable=True) - ) - - # Add VAT rate as percentage (e.g., 17.00 for 17%) - op.add_column( - 'orders', - sa.Column('vat_rate', sa.Numeric(5, 2), nullable=True) - ) - - # Add human-readable VAT label (e.g., "Luxembourg VAT 17%") - op.add_column( - 'orders', - sa.Column('vat_rate_label', sa.String(100), nullable=True) - ) - - # Add destination country for cross-border sales (ISO code) - op.add_column( - 'orders', - sa.Column('vat_destination_country', sa.String(2), nullable=True) - ) - - # Populate VAT fields for existing orders based on shipping country - # Default to 'domestic' for LU orders and 'origin' for other EU orders - op.execute(""" - UPDATE orders - SET vat_regime = CASE - WHEN ship_country_iso = 'LU' THEN 'domestic' - WHEN ship_country_iso IN ('AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE') THEN 'origin' - ELSE 'exempt' - END, - vat_destination_country = CASE - WHEN ship_country_iso != 'LU' AND ship_country_iso IN ('AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE') THEN ship_country_iso - ELSE NULL - END - WHERE vat_regime IS NULL - """) - - -def downgrade() -> None: - op.drop_column('orders', 'vat_destination_country') - op.drop_column('orders', 'vat_rate_label') - op.drop_column('orders', 'vat_rate') - op.drop_column('orders', 'vat_regime') diff --git a/alembic/versions/r6f7a8b9c0d1_add_country_iso_to_addresses.py b/alembic/versions/r6f7a8b9c0d1_add_country_iso_to_addresses.py deleted file mode 100644 index e7af3c97..00000000 --- a/alembic/versions/r6f7a8b9c0d1_add_country_iso_to_addresses.py +++ /dev/null @@ -1,144 +0,0 @@ -"""Add country_iso to customer_addresses - -Revision ID: r6f7a8b9c0d1 -Revises: q5e6f7a8b9c0 -Create Date: 2026-01-02 - -Adds country_iso field to customer_addresses table and renames -country to country_name for clarity. - -This migration is idempotent - it checks for existing columns before -making changes. -""" - -from alembic import op -import sqlalchemy as sa -from sqlalchemy import text - - -# revision identifiers, used by Alembic. -revision = "r6f7a8b9c0d1" -down_revision = "q5e6f7a8b9c0" -branch_labels = None -depends_on = None - - -# Country name to ISO code mapping for backfill -COUNTRY_ISO_MAP = { - "Luxembourg": "LU", - "Germany": "DE", - "France": "FR", - "Belgium": "BE", - "Netherlands": "NL", - "Austria": "AT", - "Italy": "IT", - "Spain": "ES", - "Portugal": "PT", - "Poland": "PL", - "Czech Republic": "CZ", - "Czechia": "CZ", - "Slovakia": "SK", - "Hungary": "HU", - "Romania": "RO", - "Bulgaria": "BG", - "Greece": "GR", - "Croatia": "HR", - "Slovenia": "SI", - "Estonia": "EE", - "Latvia": "LV", - "Lithuania": "LT", - "Finland": "FI", - "Sweden": "SE", - "Denmark": "DK", - "Ireland": "IE", - "Cyprus": "CY", - "Malta": "MT", - "United Kingdom": "GB", - "Switzerland": "CH", - "United States": "US", -} - - -def get_column_names(connection, table_name): - """Get list of column names for a table (PostgreSQL).""" - result = connection.execute(text( - "SELECT column_name FROM information_schema.columns " - "WHERE table_name = :table AND table_schema = 'public'" - ), {"table": table_name}) - return [row[0] for row in result] - - -def upgrade() -> None: - connection = op.get_bind() - columns = get_column_names(connection, "customer_addresses") - - # Check if we need to do anything (idempotent check) - has_country = "country" in columns - has_country_name = "country_name" in columns - has_country_iso = "country_iso" in columns - - # If already has new columns, nothing to do - if has_country_name and has_country_iso: - print(" Columns country_name and country_iso already exist, skipping") - return - - # If has old 'country' column, rename it (PostgreSQL supports direct rename) - if has_country and not has_country_name: - op.alter_column( - "customer_addresses", - "country", - new_column_name="country_name", - ) - - # Add country_iso if it doesn't exist - if not has_country_iso: - op.add_column( - "customer_addresses", - sa.Column("country_iso", sa.String(5), nullable=True) - ) - - # Backfill country_iso from country_name - for country_name, iso_code in COUNTRY_ISO_MAP.items(): - connection.execute( - text( - "UPDATE customer_addresses SET country_iso = :iso " - "WHERE country_name = :name" - ), - {"iso": iso_code, "name": country_name}, - ) - - # Set default for any remaining NULL values - connection.execute( - text( - "UPDATE customer_addresses SET country_iso = 'LU' " - "WHERE country_iso IS NULL" - ) - ) - - # Make country_iso NOT NULL (PostgreSQL supports direct alter) - op.alter_column( - "customer_addresses", - "country_iso", - existing_type=sa.String(5), - nullable=False, - ) - - -def downgrade() -> None: - connection = op.get_bind() - columns = get_column_names(connection, "customer_addresses") - - has_country_name = "country_name" in columns - has_country_iso = "country_iso" in columns - has_country = "country" in columns - - # Only downgrade if in the new state - if has_country_name and not has_country: - op.alter_column( - "customer_addresses", - "country_name", - new_column_name="country", - ) - - if has_country_iso: - op.drop_column("customer_addresses", "country_iso") diff --git a/alembic/versions/s7a8b9c0d1e2_add_storefront_locale_to_vendors.py b/alembic/versions/s7a8b9c0d1e2_add_storefront_locale_to_vendors.py deleted file mode 100644 index b11cb9a0..00000000 --- a/alembic/versions/s7a8b9c0d1e2_add_storefront_locale_to_vendors.py +++ /dev/null @@ -1,40 +0,0 @@ -# alembic/versions/s7a8b9c0d1e2_add_storefront_locale_to_vendors.py -"""Add storefront_locale to vendors for currency formatting. - -Revision ID: s7a8b9c0d1e2 -Revises: r6f7a8b9c0d1 -Create Date: 2026-01-02 20:00:00.000000 - -This migration adds a nullable storefront_locale field to vendors. -NULL means the vendor inherits from platform defaults. -Examples: 'fr-LU', 'de-DE', 'en-GB' -""" - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = "s7a8b9c0d1e2" -down_revision = "r6f7a8b9c0d1" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - """Add storefront_locale column to vendors table.""" - # Nullable - NULL means "inherit from platform default" - op.add_column( - "vendors", - sa.Column( - "storefront_locale", - sa.String(10), - nullable=True, - comment="Currency/number formatting locale (NULL = inherit from platform)", - ), - ) - - -def downgrade() -> None: - """Remove storefront_locale column from vendors table.""" - op.drop_column("vendors", "storefront_locale") diff --git a/alembic/versions/t001_rename_company_vendor_to_merchant_store.py b/alembic/versions/t001_rename_company_vendor_to_merchant_store.py deleted file mode 100644 index c9bc03bc..00000000 --- a/alembic/versions/t001_rename_company_vendor_to_merchant_store.py +++ /dev/null @@ -1,220 +0,0 @@ -"""Rename Company/Vendor to Merchant/Store terminology. - -Revision ID: t001_terminology -Revises: loyalty_003_phase2 -Create Date: 2026-02-06 22:00:00.000000 - -Major terminology migration: -- companies -> merchants -- vendors -> stores -- company_id -> merchant_id (in all child tables) -- vendor_id -> store_id (in all child tables) -- vendor_code -> store_code -- letzshop_vendor_id -> letzshop_store_id -- letzshop_vendor_slug -> letzshop_store_slug -- vendor_name -> store_name (in marketplace_products) -- All vendor-prefixed tables renamed to store-prefixed -- company_loyalty_settings -> merchant_loyalty_settings -- letzshop_vendor_cache -> letzshop_store_cache -""" - -from typing import Sequence, Union - -from alembic import op -from sqlalchemy import text - -# revision identifiers, used by Alembic. -revision: str = "t001_terminology" -down_revision: Union[str, None] = "loyalty_003_phase2" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def _col_exists(table: str, col: str) -> bool: - """Check if column exists using raw SQL.""" - conn = op.get_bind() - result = conn.execute(text( - "SELECT 1 FROM information_schema.columns " - "WHERE table_schema='public' AND table_name=:t AND column_name=:c" - ), {"t": table, "c": col}) - return result.fetchone() is not None - - -def _table_exists(table: str) -> bool: - """Check if table exists using raw SQL.""" - conn = op.get_bind() - result = conn.execute(text( - "SELECT 1 FROM information_schema.tables " - "WHERE table_schema='public' AND table_name=:t" - ), {"t": table}) - return result.fetchone() is not None - - -def upgrade() -> None: - """Rename all Company/Vendor references to Merchant/Store.""" - - # ====================================================================== - # STEP 1: Rename columns in child tables FIRST (before renaming parent tables) - # ====================================================================== - - # --- company_id -> merchant_id --- - op.alter_column("vendors", "company_id", new_column_name="merchant_id") - op.alter_column("loyalty_programs", "company_id", new_column_name="merchant_id") - op.alter_column("loyalty_cards", "company_id", new_column_name="merchant_id") - op.alter_column("loyalty_transactions", "company_id", new_column_name="merchant_id") - op.alter_column("company_loyalty_settings", "company_id", new_column_name="merchant_id") - op.alter_column("staff_pins", "company_id", new_column_name="merchant_id") - - # --- vendor_id -> store_id (in all child tables) --- - op.alter_column("products", "vendor_id", new_column_name="store_id") - op.alter_column("customers", "vendor_id", new_column_name="store_id") - op.alter_column("customer_addresses", "vendor_id", new_column_name="store_id") - op.alter_column("orders", "vendor_id", new_column_name="store_id") - op.alter_column("order_item_exceptions", "vendor_id", new_column_name="store_id") - op.alter_column("invoices", "vendor_id", new_column_name="store_id") - op.alter_column("inventory", "vendor_id", new_column_name="store_id") - op.alter_column("inventory_transactions", "vendor_id", new_column_name="store_id") - op.alter_column("marketplace_import_jobs", "vendor_id", new_column_name="store_id") - op.alter_column("letzshop_fulfillment_queue", "vendor_id", new_column_name="store_id") - op.alter_column("letzshop_sync_logs", "vendor_id", new_column_name="store_id") - op.alter_column("letzshop_historical_import_jobs", "vendor_id", new_column_name="store_id") - op.alter_column("vendor_users", "vendor_id", new_column_name="store_id") - op.alter_column("roles", "vendor_id", new_column_name="store_id") - op.alter_column("vendor_domains", "vendor_id", new_column_name="store_id") - op.alter_column("vendor_platforms", "vendor_id", new_column_name="store_id") - op.alter_column("vendor_addons", "vendor_id", new_column_name="store_id") - op.alter_column("vendor_subscriptions", "vendor_id", new_column_name="store_id") - op.alter_column("billing_history", "vendor_id", new_column_name="store_id") - op.alter_column("content_pages", "vendor_id", new_column_name="store_id") - op.alter_column("vendor_themes", "vendor_id", new_column_name="store_id") - op.alter_column("media_files", "vendor_id", new_column_name="store_id") - op.alter_column("vendor_email_templates", "vendor_id", new_column_name="store_id") - op.alter_column("vendor_email_settings", "vendor_id", new_column_name="store_id") - op.alter_column("vendor_letzshop_credentials", "vendor_id", new_column_name="store_id") - op.alter_column("vendor_onboarding", "vendor_id", new_column_name="store_id") - op.alter_column("vendor_invoice_settings", "vendor_id", new_column_name="store_id") - op.alter_column("staff_pins", "vendor_id", new_column_name="store_id") - op.alter_column("loyalty_cards", "enrolled_at_vendor_id", new_column_name="enrolled_at_store_id") - op.alter_column("loyalty_transactions", "vendor_id", new_column_name="store_id") - - # Columns that may not exist yet (defined in models but not yet migrated) - if _col_exists("letzshop_fulfillment_queue", "claimed_by_vendor_id"): - op.alter_column("letzshop_fulfillment_queue", "claimed_by_vendor_id", new_column_name="claimed_by_store_id") - if _col_exists("admin_audit_logs", "vendor_id"): - op.alter_column("admin_audit_logs", "vendor_id", new_column_name="store_id") - if _col_exists("messages", "vendor_id"): - op.alter_column("messages", "vendor_id", new_column_name="store_id") - if _col_exists("conversations", "vendor_id"): - op.alter_column("conversations", "vendor_id", new_column_name="store_id") - if _table_exists("emails") and _col_exists("emails", "vendor_id"): - op.alter_column("emails", "vendor_id", new_column_name="store_id") - if _table_exists("carts") and _col_exists("carts", "vendor_id"): - op.alter_column("carts", "vendor_id", new_column_name="store_id") - - # --- Other vendor-prefixed columns --- - op.alter_column("vendors", "vendor_code", new_column_name="store_code") - op.alter_column("vendors", "letzshop_vendor_id", new_column_name="letzshop_store_id") - op.alter_column("vendors", "letzshop_vendor_slug", new_column_name="letzshop_store_slug") - op.alter_column("marketplace_products", "vendor_name", new_column_name="store_name") - - # ====================================================================== - # STEP 2: Rename parent tables - # ====================================================================== - op.rename_table("companies", "merchants") - op.rename_table("vendors", "stores") - - # ====================================================================== - # STEP 3: Rename vendor-prefixed child tables - # ====================================================================== - op.rename_table("vendor_users", "store_users") - op.rename_table("vendor_domains", "store_domains") - op.rename_table("vendor_platforms", "store_platforms") - op.rename_table("vendor_themes", "store_themes") - op.rename_table("vendor_email_templates", "store_email_templates") - op.rename_table("vendor_email_settings", "store_email_settings") - op.rename_table("vendor_addons", "store_addons") - op.rename_table("vendor_subscriptions", "store_subscriptions") - op.rename_table("vendor_letzshop_credentials", "store_letzshop_credentials") - op.rename_table("vendor_onboarding", "store_onboarding") - op.rename_table("vendor_invoice_settings", "store_invoice_settings") - op.rename_table("company_loyalty_settings", "merchant_loyalty_settings") - op.rename_table("letzshop_vendor_cache", "letzshop_store_cache") - - -def downgrade() -> None: - """Revert all Merchant/Store references back to Company/Vendor.""" - - # STEP 1: Revert table renames - op.rename_table("letzshop_store_cache", "letzshop_vendor_cache") - op.rename_table("merchant_loyalty_settings", "company_loyalty_settings") - op.rename_table("store_invoice_settings", "vendor_invoice_settings") - op.rename_table("store_onboarding", "vendor_onboarding") - op.rename_table("store_letzshop_credentials", "vendor_letzshop_credentials") - op.rename_table("store_subscriptions", "vendor_subscriptions") - op.rename_table("store_addons", "vendor_addons") - op.rename_table("store_email_settings", "vendor_email_settings") - op.rename_table("store_email_templates", "vendor_email_templates") - op.rename_table("store_themes", "vendor_themes") - op.rename_table("store_platforms", "vendor_platforms") - op.rename_table("store_domains", "vendor_domains") - op.rename_table("store_users", "vendor_users") - op.rename_table("stores", "vendors") - op.rename_table("merchants", "companies") - - # STEP 2: Revert column renames - op.alter_column("vendors", "store_code", new_column_name="vendor_code") - op.alter_column("vendors", "letzshop_store_id", new_column_name="letzshop_vendor_id") - op.alter_column("vendors", "letzshop_store_slug", new_column_name="letzshop_vendor_slug") - op.alter_column("marketplace_products", "store_name", new_column_name="vendor_name") - - op.alter_column("vendors", "merchant_id", new_column_name="company_id") - op.alter_column("loyalty_programs", "merchant_id", new_column_name="company_id") - op.alter_column("loyalty_cards", "merchant_id", new_column_name="company_id") - op.alter_column("loyalty_transactions", "merchant_id", new_column_name="company_id") - op.alter_column("company_loyalty_settings", "merchant_id", new_column_name="company_id") - op.alter_column("staff_pins", "merchant_id", new_column_name="company_id") - - op.alter_column("products", "store_id", new_column_name="vendor_id") - op.alter_column("customers", "store_id", new_column_name="vendor_id") - op.alter_column("customer_addresses", "store_id", new_column_name="vendor_id") - op.alter_column("orders", "store_id", new_column_name="vendor_id") - op.alter_column("order_item_exceptions", "store_id", new_column_name="vendor_id") - op.alter_column("invoices", "store_id", new_column_name="vendor_id") - op.alter_column("inventory", "store_id", new_column_name="vendor_id") - op.alter_column("inventory_transactions", "store_id", new_column_name="vendor_id") - op.alter_column("marketplace_import_jobs", "store_id", new_column_name="vendor_id") - op.alter_column("letzshop_fulfillment_queue", "store_id", new_column_name="vendor_id") - op.alter_column("letzshop_sync_logs", "store_id", new_column_name="vendor_id") - op.alter_column("letzshop_historical_import_jobs", "store_id", new_column_name="vendor_id") - op.alter_column("vendor_users", "store_id", new_column_name="vendor_id") - op.alter_column("roles", "store_id", new_column_name="vendor_id") - op.alter_column("vendor_domains", "store_id", new_column_name="vendor_id") - op.alter_column("vendor_platforms", "store_id", new_column_name="vendor_id") - op.alter_column("vendor_addons", "store_id", new_column_name="vendor_id") - op.alter_column("vendor_subscriptions", "store_id", new_column_name="vendor_id") - op.alter_column("billing_history", "store_id", new_column_name="vendor_id") - op.alter_column("content_pages", "store_id", new_column_name="vendor_id") - op.alter_column("vendor_themes", "store_id", new_column_name="vendor_id") - op.alter_column("media_files", "store_id", new_column_name="vendor_id") - op.alter_column("vendor_email_templates", "store_id", new_column_name="vendor_id") - op.alter_column("vendor_email_settings", "store_id", new_column_name="vendor_id") - op.alter_column("vendor_letzshop_credentials", "store_id", new_column_name="vendor_id") - op.alter_column("vendor_onboarding", "store_id", new_column_name="vendor_id") - op.alter_column("vendor_invoice_settings", "store_id", new_column_name="vendor_id") - op.alter_column("staff_pins", "store_id", new_column_name="vendor_id") - op.alter_column("loyalty_cards", "enrolled_at_store_id", new_column_name="enrolled_at_vendor_id") - op.alter_column("loyalty_transactions", "store_id", new_column_name="vendor_id") - - # Conditional columns - if _col_exists("letzshop_fulfillment_queue", "claimed_by_store_id"): - op.alter_column("letzshop_fulfillment_queue", "claimed_by_store_id", new_column_name="claimed_by_vendor_id") - if _col_exists("admin_audit_logs", "store_id"): - op.alter_column("admin_audit_logs", "store_id", new_column_name="vendor_id") - if _col_exists("messages", "store_id"): - op.alter_column("messages", "store_id", new_column_name="vendor_id") - if _col_exists("conversations", "store_id"): - op.alter_column("conversations", "store_id", new_column_name="vendor_id") - if _table_exists("emails") and _col_exists("emails", "store_id"): - op.alter_column("emails", "store_id", new_column_name="vendor_id") - if _table_exists("carts") and _col_exists("carts", "store_id"): - op.alter_column("carts", "store_id", new_column_name="vendor_id") diff --git a/alembic/versions/t8b9c0d1e2f3_add_password_reset_tokens.py b/alembic/versions/t8b9c0d1e2f3_add_password_reset_tokens.py deleted file mode 100644 index 329a3189..00000000 --- a/alembic/versions/t8b9c0d1e2f3_add_password_reset_tokens.py +++ /dev/null @@ -1,53 +0,0 @@ -"""add password_reset_tokens table - -Revision ID: t8b9c0d1e2f3 -Revises: s7a8b9c0d1e2 -Create Date: 2026-01-03 - -""" - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = "t8b9c0d1e2f3" -down_revision = "s7a8b9c0d1e2" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - op.create_table( - "password_reset_tokens", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("customer_id", sa.Integer(), nullable=False), - sa.Column("token_hash", sa.String(64), nullable=False), - sa.Column("expires_at", sa.DateTime(), nullable=False), - sa.Column("used_at", sa.DateTime(), nullable=True), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False - ), - sa.ForeignKeyConstraint( - ["customer_id"], - ["customers.id"], - ondelete="CASCADE", - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - "ix_password_reset_tokens_customer_id", - "password_reset_tokens", - ["customer_id"], - ) - op.create_index( - "ix_password_reset_tokens_token_hash", - "password_reset_tokens", - ["token_hash"], - ) - - -def downgrade() -> None: - op.drop_index("ix_password_reset_tokens_token_hash", table_name="password_reset_tokens") - op.drop_index("ix_password_reset_tokens_customer_id", table_name="password_reset_tokens") - op.drop_table("password_reset_tokens") diff --git a/alembic/versions/u9c0d1e2f3g4_add_vendor_email_templates.py b/alembic/versions/u9c0d1e2f3g4_add_vendor_email_templates.py deleted file mode 100644 index 9cf16718..00000000 --- a/alembic/versions/u9c0d1e2f3g4_add_vendor_email_templates.py +++ /dev/null @@ -1,114 +0,0 @@ -# alembic/versions/u9c0d1e2f3g4_add_vendor_email_templates.py -"""Add vendor email templates and enhance email_templates table. - -Revision ID: u9c0d1e2f3g4 -Revises: t8b9c0d1e2f3 -Create Date: 2026-01-03 - -Changes: -- Add is_platform_only column to email_templates (templates that vendors cannot override) -- Add required_variables column to email_templates (JSON list of required variables) -- Create vendor_email_templates table for vendor-specific template overrides -""" - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = "u9c0d1e2f3g4" -down_revision = "t8b9c0d1e2f3" -branch_labels = None -depends_on = None - - -def upgrade(): - # Add new columns to email_templates - op.add_column( - "email_templates", - sa.Column("is_platform_only", sa.Boolean(), nullable=False, server_default="0"), - ) - op.add_column( - "email_templates", - sa.Column("required_variables", sa.Text(), nullable=True), - ) - - # Create vendor_email_templates table - op.create_table( - "vendor_email_templates", - sa.Column("id", sa.Integer(), nullable=False, autoincrement=True), - sa.Column("vendor_id", sa.Integer(), nullable=False), - sa.Column("template_code", sa.String(100), nullable=False), - sa.Column("language", sa.String(5), nullable=False, server_default="en"), - sa.Column("name", sa.String(255), nullable=True), - sa.Column("subject", sa.String(500), nullable=False), - sa.Column("body_html", sa.Text(), nullable=False), - sa.Column("body_text", sa.Text(), nullable=True), - sa.Column("is_active", sa.Boolean(), nullable=False, server_default="1"), - sa.Column( - "created_at", - sa.DateTime(), - nullable=False, - server_default=sa.text("CURRENT_TIMESTAMP"), - ), - sa.Column( - "updated_at", - sa.DateTime(), - nullable=False, - server_default=sa.text("CURRENT_TIMESTAMP"), - ), - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint( - ["vendor_id"], - ["vendors.id"], - name="fk_vendor_email_templates_vendor_id", - ondelete="CASCADE", - ), - sa.UniqueConstraint( - "vendor_id", - "template_code", - "language", - name="uq_vendor_email_template_code_language", - ), - ) - - # Create indexes for performance - op.create_index( - "ix_vendor_email_templates_vendor_id", - "vendor_email_templates", - ["vendor_id"], - ) - op.create_index( - "ix_vendor_email_templates_template_code", - "vendor_email_templates", - ["template_code"], - ) - op.create_index( - "ix_vendor_email_templates_lookup", - "vendor_email_templates", - ["vendor_id", "template_code", "language"], - ) - - # Add unique constraint to email_templates for code+language - # This ensures we can reliably look up platform templates - op.create_index( - "ix_email_templates_code_language", - "email_templates", - ["code", "language"], - unique=True, - ) - - -def downgrade(): - # Drop indexes - op.drop_index("ix_email_templates_code_language", table_name="email_templates") - op.drop_index("ix_vendor_email_templates_lookup", table_name="vendor_email_templates") - op.drop_index("ix_vendor_email_templates_template_code", table_name="vendor_email_templates") - op.drop_index("ix_vendor_email_templates_vendor_id", table_name="vendor_email_templates") - - # Drop vendor_email_templates table - op.drop_table("vendor_email_templates") - - # Remove new columns from email_templates - op.drop_column("email_templates", "required_variables") - op.drop_column("email_templates", "is_platform_only") diff --git a/alembic/versions/v0a1b2c3d4e5_add_vendor_email_settings.py b/alembic/versions/v0a1b2c3d4e5_add_vendor_email_settings.py deleted file mode 100644 index ac5307a1..00000000 --- a/alembic/versions/v0a1b2c3d4e5_add_vendor_email_settings.py +++ /dev/null @@ -1,102 +0,0 @@ -# alembic/versions/v0a1b2c3d4e5_add_vendor_email_settings.py -"""Add vendor email settings table. - -Revision ID: v0a1b2c3d4e5 -Revises: u9c0d1e2f3g4 -Create Date: 2026-01-05 - -Changes: -- Create vendor_email_settings table for vendor SMTP/email provider configuration -- Vendors must configure this to send transactional emails -- Premium providers (SendGrid, Mailgun, SES) are tier-gated (Business+) -""" - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = "v0a1b2c3d4e5" -down_revision = "u9c0d1e2f3g4" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # Create vendor_email_settings table - op.create_table( - "vendor_email_settings", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=False), - # Sender Identity - sa.Column("from_email", sa.String(255), nullable=False), - sa.Column("from_name", sa.String(100), nullable=False), - sa.Column("reply_to_email", sa.String(255), nullable=True), - # Signature/Footer - sa.Column("signature_text", sa.Text(), nullable=True), - sa.Column("signature_html", sa.Text(), nullable=True), - # Provider Configuration - sa.Column("provider", sa.String(20), nullable=False, default="smtp"), - # SMTP Settings - sa.Column("smtp_host", sa.String(255), nullable=True), - sa.Column("smtp_port", sa.Integer(), nullable=True, default=587), - sa.Column("smtp_username", sa.String(255), nullable=True), - sa.Column("smtp_password", sa.String(500), nullable=True), - sa.Column("smtp_use_tls", sa.Boolean(), nullable=False, default=True), - sa.Column("smtp_use_ssl", sa.Boolean(), nullable=False, default=False), - # SendGrid Settings - sa.Column("sendgrid_api_key", sa.String(500), nullable=True), - # Mailgun Settings - sa.Column("mailgun_api_key", sa.String(500), nullable=True), - sa.Column("mailgun_domain", sa.String(255), nullable=True), - # Amazon SES Settings - sa.Column("ses_access_key_id", sa.String(100), nullable=True), - sa.Column("ses_secret_access_key", sa.String(500), nullable=True), - sa.Column("ses_region", sa.String(50), nullable=True, default="eu-west-1"), - # Status & Verification - sa.Column("is_configured", sa.Boolean(), nullable=False, default=False), - sa.Column("is_verified", sa.Boolean(), nullable=False, default=False), - sa.Column("last_verified_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("verification_error", sa.Text(), nullable=True), - # Timestamps - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - # Constraints - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint( - ["vendor_id"], - ["vendors.id"], - name="fk_vendor_email_settings_vendor_id", - ondelete="CASCADE", - ), - sa.UniqueConstraint("vendor_id", name="uq_vendor_email_settings_vendor_id"), - ) - - # Create indexes - op.create_index( - "ix_vendor_email_settings_id", - "vendor_email_settings", - ["id"], - unique=False, - ) - op.create_index( - "ix_vendor_email_settings_vendor_id", - "vendor_email_settings", - ["vendor_id"], - unique=True, - ) - op.create_index( - "idx_vendor_email_settings_configured", - "vendor_email_settings", - ["vendor_id", "is_configured"], - ) - - -def downgrade() -> None: - # Drop indexes - op.drop_index("idx_vendor_email_settings_configured", table_name="vendor_email_settings") - op.drop_index("ix_vendor_email_settings_vendor_id", table_name="vendor_email_settings") - op.drop_index("ix_vendor_email_settings_id", table_name="vendor_email_settings") - - # Drop table - op.drop_table("vendor_email_settings") diff --git a/alembic/versions/w1b2c3d4e5f6_add_media_library_tables.py b/alembic/versions/w1b2c3d4e5f6_add_media_library_tables.py deleted file mode 100644 index 795b0827..00000000 --- a/alembic/versions/w1b2c3d4e5f6_add_media_library_tables.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Add media library tables - -Revision ID: w1b2c3d4e5f6 -Revises: v0a1b2c3d4e5 -Create Date: 2026-01-06 10:00:00.000000 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - -# revision identifiers, used by Alembic. -revision: str = "w1b2c3d4e5f6" -down_revision: Union[str, None] = "v0a1b2c3d4e5" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Create media_files table - op.create_table( - "media_files", - sa.Column("id", sa.Integer(), primary_key=True, index=True), - sa.Column("vendor_id", sa.Integer(), sa.ForeignKey("vendors.id"), nullable=False), - # File identification - sa.Column("filename", sa.String(255), nullable=False), - sa.Column("original_filename", sa.String(255)), - sa.Column("file_path", sa.String(500), nullable=False), - # File properties - sa.Column("media_type", sa.String(20), nullable=False), # image, video, document - sa.Column("mime_type", sa.String(100)), - sa.Column("file_size", sa.Integer()), - # Image/video dimensions - sa.Column("width", sa.Integer()), - sa.Column("height", sa.Integer()), - # Thumbnail - sa.Column("thumbnail_path", sa.String(500)), - # Metadata - sa.Column("alt_text", sa.String(500)), - sa.Column("description", sa.Text()), - sa.Column("folder", sa.String(100), default="general"), - sa.Column("tags", sa.JSON()), - sa.Column("extra_metadata", sa.JSON()), - # Status - sa.Column("is_optimized", sa.Boolean(), default=False), - sa.Column("optimized_size", sa.Integer()), - # Usage tracking - sa.Column("usage_count", sa.Integer(), default=0), - # Timestamps - sa.Column("created_at", sa.DateTime(), server_default=sa.func.now()), - sa.Column("updated_at", sa.DateTime(), onupdate=sa.func.now()), - ) - - # Create indexes for media_files - op.create_index("idx_media_vendor_id", "media_files", ["vendor_id"]) - op.create_index("idx_media_vendor_folder", "media_files", ["vendor_id", "folder"]) - op.create_index("idx_media_vendor_type", "media_files", ["vendor_id", "media_type"]) - op.create_index("idx_media_filename", "media_files", ["filename"]) - - # Create product_media table (many-to-many relationship) - op.create_table( - "product_media", - sa.Column("id", sa.Integer(), primary_key=True, index=True), - sa.Column( - "product_id", - sa.Integer(), - sa.ForeignKey("products.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column( - "media_id", - sa.Integer(), - sa.ForeignKey("media_files.id", ondelete="CASCADE"), - nullable=False, - ), - # Usage type - sa.Column("usage_type", sa.String(50), nullable=False, default="gallery"), - # Display order for galleries - sa.Column("display_order", sa.Integer(), default=0), - # Variant-specific - sa.Column("variant_id", sa.Integer()), - # Timestamps - sa.Column("created_at", sa.DateTime(), server_default=sa.func.now()), - sa.Column("updated_at", sa.DateTime(), onupdate=sa.func.now()), - # Unique constraint - sa.UniqueConstraint("product_id", "media_id", "usage_type", name="uq_product_media_usage"), - ) - - # Create indexes for product_media - op.create_index("idx_product_media_product", "product_media", ["product_id"]) - op.create_index("idx_product_media_media", "product_media", ["media_id"]) - # Note: Unique constraint is defined in the table creation above via SQLAlchemy model - # SQLite doesn't support adding constraints after table creation - - -def downgrade() -> None: - # Drop product_media table - op.drop_index("idx_product_media_media", table_name="product_media") - op.drop_index("idx_product_media_product", table_name="product_media") - op.drop_table("product_media") - - # Drop media_files table - op.drop_index("idx_media_filename", table_name="media_files") - op.drop_index("idx_media_vendor_type", table_name="media_files") - op.drop_index("idx_media_vendor_folder", table_name="media_files") - op.drop_index("idx_media_vendor_id", table_name="media_files") - op.drop_table("media_files") diff --git a/alembic/versions/x2c3d4e5f6g7_make_marketplace_product_id_nullable.py b/alembic/versions/x2c3d4e5f6g7_make_marketplace_product_id_nullable.py deleted file mode 100644 index b8e334bb..00000000 --- a/alembic/versions/x2c3d4e5f6g7_make_marketplace_product_id_nullable.py +++ /dev/null @@ -1,43 +0,0 @@ -# alembic/versions/x2c3d4e5f6g7_make_marketplace_product_id_nullable.py -"""Make marketplace_product_id nullable for direct product creation. - -Revision ID: x2c3d4e5f6g7 -Revises: w1b2c3d4e5f6 -Create Date: 2026-01-06 23:15:00.000000 - -""" - -from collections.abc import Sequence - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = "x2c3d4e5f6g7" -down_revision: str = "w1b2c3d4e5f6" -branch_labels: str | Sequence[str] | None = None -depends_on: str | Sequence[str] | None = None - - -def upgrade() -> None: - """Make marketplace_product_id nullable to allow direct product creation.""" - # SQLite doesn't support ALTER COLUMN, so we need to recreate the table - # For SQLite, we use batch mode which handles this automatically - with op.batch_alter_table("products") as batch_op: - batch_op.alter_column( - "marketplace_product_id", - existing_type=sa.Integer(), - nullable=True, - ) - - -def downgrade() -> None: - """Revert marketplace_product_id to NOT NULL.""" - # Note: This will fail if there are any NULL values in the column - with op.batch_alter_table("products") as batch_op: - batch_op.alter_column( - "marketplace_product_id", - existing_type=sa.Integer(), - nullable=False, - ) diff --git a/alembic/versions/y3d4e5f6g7h8_add_product_type_columns.py b/alembic/versions/y3d4e5f6g7h8_add_product_type_columns.py deleted file mode 100644 index a7db7e52..00000000 --- a/alembic/versions/y3d4e5f6g7h8_add_product_type_columns.py +++ /dev/null @@ -1,47 +0,0 @@ -# alembic/versions/y3d4e5f6g7h8_add_product_type_columns.py -"""Add is_digital and product_type columns to products table. - -Makes Product fully independent from MarketplaceProduct for product type info. - -Revision ID: y3d4e5f6g7h8 -Revises: x2c3d4e5f6g7 -Create Date: 2026-01-07 10:00:00.000000 - -""" - -from collections.abc import Sequence - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = "y3d4e5f6g7h8" -down_revision: str = "x2c3d4e5f6g7" -branch_labels: str | Sequence[str] | None = None -depends_on: str | Sequence[str] | None = None - - -def upgrade() -> None: - """Add is_digital and product_type columns to products table.""" - with op.batch_alter_table("products") as batch_op: - batch_op.add_column( - sa.Column("is_digital", sa.Boolean(), nullable=False, server_default="0") - ) - batch_op.add_column( - sa.Column( - "product_type", - sa.String(20), - nullable=False, - server_default="physical", - ) - ) - batch_op.create_index("idx_product_is_digital", ["is_digital"]) - - -def downgrade() -> None: - """Remove is_digital and product_type columns.""" - with op.batch_alter_table("products") as batch_op: - batch_op.drop_index("idx_product_is_digital") - batch_op.drop_column("product_type") - batch_op.drop_column("is_digital") diff --git a/alembic/versions/z4e5f6a7b8c9_add_multi_platform_support.py b/alembic/versions/z4e5f6a7b8c9_add_multi_platform_support.py deleted file mode 100644 index ea8b53b4..00000000 --- a/alembic/versions/z4e5f6a7b8c9_add_multi_platform_support.py +++ /dev/null @@ -1,374 +0,0 @@ -"""add multi-platform support - -Revision ID: z4e5f6a7b8c9 -Revises: 1b398cf45e85 -Create Date: 2026-01-18 12:00:00.000000 - -This migration adds multi-platform support: -1. Creates platforms table for business offerings (OMS, Loyalty, etc.) -2. Creates vendor_platforms junction table for many-to-many relationship -3. Adds platform_id and CMS limits to subscription_tiers -4. Adds platform_id and is_platform_page to content_pages -5. Inserts default "oms" platform -6. Backfills existing data to OMS platform -7. Creates vendor_platforms entries for existing vendors -""" - -import json -from typing import Sequence, Union - -import sqlalchemy as sa -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "z4e5f6a7b8c9" -down_revision: Union[str, None] = "1b398cf45e85" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - -# Platform marketing page slugs (is_platform_page=True) -PLATFORM_PAGE_SLUGS = [ - "platform_homepage", - "home", - "pricing", - "about", - "contact", - "faq", - "terms", - "privacy", - "features", - "integrations", -] - -# CMS limits per tier -CMS_TIER_LIMITS = { - "essential": {"cms_pages_limit": 3, "cms_custom_pages_limit": 0}, - "professional": {"cms_pages_limit": 10, "cms_custom_pages_limit": 5}, - "business": {"cms_pages_limit": 30, "cms_custom_pages_limit": 20}, - "enterprise": {"cms_pages_limit": None, "cms_custom_pages_limit": None}, # Unlimited -} - -# CMS features per tier -CMS_TIER_FEATURES = { - "essential": ["cms_basic"], - "professional": ["cms_basic", "cms_custom_pages", "cms_seo"], - "business": ["cms_basic", "cms_custom_pages", "cms_seo", "cms_templates"], - "enterprise": ["cms_basic", "cms_custom_pages", "cms_unlimited_pages", "cms_templates", "cms_seo", "cms_scheduling"], -} - - -def upgrade() -> None: - conn = op.get_bind() - - # ========================================================================= - # 1. Create platforms table - # ========================================================================= - op.create_table( - "platforms", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("code", sa.String(50), nullable=False), - sa.Column("name", sa.String(100), nullable=False), - sa.Column("description", sa.Text(), nullable=True), - sa.Column("domain", sa.String(255), nullable=True), - sa.Column("path_prefix", sa.String(50), nullable=True), - sa.Column("logo", sa.String(500), nullable=True), - sa.Column("logo_dark", sa.String(500), nullable=True), - sa.Column("favicon", sa.String(500), nullable=True), - sa.Column("theme_config", sa.JSON(), nullable=True), - sa.Column("default_language", sa.String(5), nullable=False, server_default="fr"), - sa.Column("supported_languages", sa.JSON(), nullable=False), - sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"), - sa.Column("is_public", sa.Boolean(), nullable=False, server_default="true"), - sa.Column("settings", sa.JSON(), nullable=True), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index("ix_platforms_code", "platforms", ["code"], unique=True) - op.create_index("ix_platforms_domain", "platforms", ["domain"], unique=True) - op.create_index("ix_platforms_path_prefix", "platforms", ["path_prefix"], unique=True) - op.create_index("idx_platform_active", "platforms", ["is_active"]) - op.create_index("idx_platform_public", "platforms", ["is_public", "is_active"]) - - # ========================================================================= - # 2. Create vendor_platforms junction table - # ========================================================================= - op.create_table( - "vendor_platforms", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("vendor_id", sa.Integer(), nullable=False), - sa.Column("platform_id", sa.Integer(), nullable=False), - sa.Column("tier_id", sa.Integer(), nullable=True), - sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"), - sa.Column("is_primary", sa.Boolean(), nullable=False, server_default="false"), - sa.Column("custom_subdomain", sa.String(100), nullable=True), - sa.Column("settings", sa.JSON(), nullable=True), - sa.Column("joined_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), - sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), - sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), - sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["platform_id"], ["platforms.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["tier_id"], ["subscription_tiers.id"], ondelete="SET NULL"), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index("ix_vendor_platforms_vendor_id", "vendor_platforms", ["vendor_id"]) - op.create_index("ix_vendor_platforms_platform_id", "vendor_platforms", ["platform_id"]) - op.create_index("ix_vendor_platforms_tier_id", "vendor_platforms", ["tier_id"]) - op.create_index("idx_vendor_platform_active", "vendor_platforms", ["vendor_id", "platform_id", "is_active"]) - op.create_index("idx_vendor_platform_primary", "vendor_platforms", ["vendor_id", "is_primary"]) - op.create_unique_constraint("uq_vendor_platform", "vendor_platforms", ["vendor_id", "platform_id"]) - - # ========================================================================= - # 3. Add platform_id and CMS columns to subscription_tiers - # ========================================================================= - # Add platform_id column (nullable for global tiers) - op.add_column( - "subscription_tiers", - sa.Column("platform_id", sa.Integer(), nullable=True), - ) - op.create_index("ix_subscription_tiers_platform_id", "subscription_tiers", ["platform_id"]) - op.create_foreign_key( - "fk_subscription_tiers_platform_id", - "subscription_tiers", - "platforms", - ["platform_id"], - ["id"], - ondelete="CASCADE", - ) - - # Add CMS limit columns - op.add_column( - "subscription_tiers", - sa.Column("cms_pages_limit", sa.Integer(), nullable=True), - ) - op.add_column( - "subscription_tiers", - sa.Column("cms_custom_pages_limit", sa.Integer(), nullable=True), - ) - op.create_index("idx_tier_platform_active", "subscription_tiers", ["platform_id", "is_active"]) - - # ========================================================================= - # 4. Add platform_id and is_platform_page to content_pages - # ========================================================================= - # Add platform_id column (will be set to NOT NULL after backfill) - op.add_column( - "content_pages", - sa.Column("platform_id", sa.Integer(), nullable=True), - ) - op.create_index("ix_content_pages_platform_id", "content_pages", ["platform_id"]) - - # Add is_platform_page column - op.add_column( - "content_pages", - sa.Column("is_platform_page", sa.Boolean(), nullable=False, server_default="false"), - ) - op.create_index("idx_platform_page_type", "content_pages", ["platform_id", "is_platform_page"]) - - # ========================================================================= - # 5. Insert default OMS platform - # ========================================================================= - conn.execute( - sa.text(""" - INSERT INTO platforms (code, name, description, domain, path_prefix, default_language, - supported_languages, is_active, is_public, theme_config, settings, - created_at, updated_at) - VALUES ('oms', 'Wizamart OMS', 'Order Management System for Luxembourg merchants', - 'oms.lu', 'oms', 'fr', '["fr", "de", "en"]', true, true, '{}', '{}', - CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - """) - ) - - # Get OMS platform ID for backfilling - result = conn.execute(sa.text("SELECT id FROM platforms WHERE code = 'oms'")) - oms_platform_id = result.fetchone()[0] - - # ========================================================================= - # 6. Backfill content_pages with platform_id - # ========================================================================= - conn.execute( - sa.text(f"UPDATE content_pages SET platform_id = {oms_platform_id} WHERE platform_id IS NULL") - ) - - # Set is_platform_page=True for platform marketing page slugs - # Only for pages that have vendor_id=NULL (platform-level pages) - slugs_list = ", ".join([f"'{slug}'" for slug in PLATFORM_PAGE_SLUGS]) - conn.execute( - sa.text(f""" - UPDATE content_pages - SET is_platform_page = true - WHERE vendor_id IS NULL AND slug IN ({slugs_list}) - """) - ) - - # Make platform_id NOT NULL after backfill - op.alter_column("content_pages", "platform_id", nullable=False) - - # Add foreign key constraint - op.create_foreign_key( - "fk_content_pages_platform_id", - "content_pages", - "platforms", - ["platform_id"], - ["id"], - ondelete="CASCADE", - ) - - # ========================================================================= - # 7. Update content_pages constraints - # ========================================================================= - # Drop old unique constraint - op.drop_constraint("uq_vendor_slug", "content_pages", type_="unique") - - # Create new unique constraint including platform_id - op.create_unique_constraint( - "uq_platform_vendor_slug", - "content_pages", - ["platform_id", "vendor_id", "slug"], - ) - - # Update indexes - op.drop_index("idx_vendor_published", table_name="content_pages") - op.drop_index("idx_slug_published", table_name="content_pages") - op.create_index("idx_platform_vendor_published", "content_pages", ["platform_id", "vendor_id", "is_published"]) - op.create_index("idx_platform_slug_published", "content_pages", ["platform_id", "slug", "is_published"]) - - # ========================================================================= - # 8. Update subscription_tiers with CMS limits - # ========================================================================= - for tier_code, limits in CMS_TIER_LIMITS.items(): - cms_pages = limits["cms_pages_limit"] if limits["cms_pages_limit"] is not None else "NULL" - cms_custom = limits["cms_custom_pages_limit"] if limits["cms_custom_pages_limit"] is not None else "NULL" - conn.execute( - sa.text(f""" - UPDATE subscription_tiers - SET cms_pages_limit = {cms_pages}, - cms_custom_pages_limit = {cms_custom} - WHERE code = '{tier_code}' - """) - ) - - # Add CMS features to each tier - for tier_code, cms_features in CMS_TIER_FEATURES.items(): - # Get current features - result = conn.execute( - sa.text(f"SELECT features FROM subscription_tiers WHERE code = '{tier_code}'") - ) - row = result.fetchone() - if row and row[0]: - current_features = json.loads(row[0]) if isinstance(row[0], str) else row[0] - else: - current_features = [] - - # Add CMS features that aren't already present - for feature in cms_features: - if feature not in current_features: - current_features.append(feature) - - # Update features - features_json = json.dumps(current_features) - conn.execute( - sa.text(f"UPDATE subscription_tiers SET features = '{features_json}' WHERE code = '{tier_code}'") - ) - - # ========================================================================= - # 9. Create vendor_platforms entries for existing vendors - # ========================================================================= - # Get all vendors with their subscription tier_id - conn.execute( - sa.text(f""" - INSERT INTO vendor_platforms (vendor_id, platform_id, tier_id, is_active, is_primary, - joined_at, created_at, updated_at) - SELECT v.id, {oms_platform_id}, vs.tier_id, v.is_active, true, - v.created_at, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP - FROM vendors v - LEFT JOIN vendor_subscriptions vs ON vs.vendor_id = v.id - """) - ) - - # ========================================================================= - # 10. Add CMS feature records to features table - # ========================================================================= - # Get minimum tier IDs for CMS features - tier_ids = {} - result = conn.execute(sa.text("SELECT id, code FROM subscription_tiers")) - for row in result: - tier_ids[row[1]] = row[0] - - cms_features = [ - ("cms", "cms_basic", "Basic CMS", "Override default pages with custom content", "settings", "document-text", None, tier_ids.get("essential"), 1), - ("cms", "cms_custom_pages", "Custom Pages", "Create custom pages beyond defaults", "settings", "document-add", None, tier_ids.get("professional"), 2), - ("cms", "cms_unlimited_pages", "Unlimited Pages", "No page limit", "settings", "documents", None, tier_ids.get("enterprise"), 3), - ("cms", "cms_templates", "Page Templates", "Access to page templates", "settings", "template", None, tier_ids.get("business"), 4), - ("cms", "cms_seo", "Advanced SEO", "SEO metadata and optimization", "settings", "search", None, tier_ids.get("professional"), 5), - ("cms", "cms_scheduling", "Page Scheduling", "Schedule page publish/unpublish", "settings", "clock", None, tier_ids.get("enterprise"), 6), - ] - - for category, code, name, description, ui_location, ui_icon, ui_route, minimum_tier_id, display_order in cms_features: - min_tier_val = minimum_tier_id if minimum_tier_id else "NULL" - conn.execute( - sa.text(f""" - INSERT INTO features (code, name, description, category, ui_location, ui_icon, ui_route, - minimum_tier_id, is_active, is_visible, display_order, created_at, updated_at) - VALUES ('{code}', '{name}', '{description}', '{category}', '{ui_location}', '{ui_icon}', NULL, - {min_tier_val}, true, true, {display_order}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - ON CONFLICT (code) DO NOTHING - """) - ) - - -def downgrade() -> None: - conn = op.get_bind() - - # Remove CMS features from features table - conn.execute( - sa.text(""" - DELETE FROM features - WHERE code IN ('cms_basic', 'cms_custom_pages', 'cms_unlimited_pages', - 'cms_templates', 'cms_seo', 'cms_scheduling') - """) - ) - - # Remove CMS features from subscription_tiers - result = conn.execute(sa.text("SELECT id, code, features FROM subscription_tiers")) - for row in result: - tier_id, tier_code, features_data = row - if features_data: - current_features = json.loads(features_data) if isinstance(features_data, str) else features_data - # Remove CMS features - updated_features = [f for f in current_features if not f.startswith("cms_")] - features_json = json.dumps(updated_features) - conn.execute( - sa.text(f"UPDATE subscription_tiers SET features = '{features_json}' WHERE id = {tier_id}") - ) - - # Drop vendor_platforms table - op.drop_table("vendor_platforms") - - # Restore old content_pages constraints - op.drop_constraint("uq_platform_vendor_slug", "content_pages", type_="unique") - op.drop_index("idx_platform_vendor_published", table_name="content_pages") - op.drop_index("idx_platform_slug_published", table_name="content_pages") - op.drop_index("idx_platform_page_type", table_name="content_pages") - op.drop_constraint("fk_content_pages_platform_id", "content_pages", type_="foreignkey") - op.drop_index("ix_content_pages_platform_id", table_name="content_pages") - op.drop_column("content_pages", "is_platform_page") - op.drop_column("content_pages", "platform_id") - op.create_unique_constraint("uq_vendor_slug", "content_pages", ["vendor_id", "slug"]) - op.create_index("idx_vendor_published", "content_pages", ["vendor_id", "is_published"]) - op.create_index("idx_slug_published", "content_pages", ["slug", "is_published"]) - - # Remove subscription_tiers platform columns - op.drop_index("idx_tier_platform_active", table_name="subscription_tiers") - op.drop_constraint("fk_subscription_tiers_platform_id", "subscription_tiers", type_="foreignkey") - op.drop_index("ix_subscription_tiers_platform_id", table_name="subscription_tiers") - op.drop_column("subscription_tiers", "cms_custom_pages_limit") - op.drop_column("subscription_tiers", "cms_pages_limit") - op.drop_column("subscription_tiers", "platform_id") - - # Drop platforms table - op.drop_index("idx_platform_public", table_name="platforms") - op.drop_index("idx_platform_active", table_name="platforms") - op.drop_index("ix_platforms_path_prefix", table_name="platforms") - op.drop_index("ix_platforms_domain", table_name="platforms") - op.drop_index("ix_platforms_code", table_name="platforms") - op.drop_table("platforms") diff --git a/alembic/versions/z5f6g7h8i9j0_add_loyalty_platform.py b/alembic/versions/z5f6g7h8i9j0_add_loyalty_platform.py deleted file mode 100644 index 12e09ac9..00000000 --- a/alembic/versions/z5f6g7h8i9j0_add_loyalty_platform.py +++ /dev/null @@ -1,424 +0,0 @@ -"""add loyalty platform - -Revision ID: z5f6g7h8i9j0 -Revises: z4e5f6a7b8c9 -Create Date: 2026-01-19 12:00:00.000000 - -This migration adds the Loyalty+ platform: -1. Inserts loyalty platform record -2. Creates platform marketing pages (home, pricing, features, how-it-works) -3. Creates vendor default pages (about, rewards-catalog, terms, privacy) -""" - -from typing import Sequence, Union - -import sqlalchemy as sa -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "z5f6g7h8i9j0" -down_revision: Union[str, None] = "z4e5f6a7b8c9" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - conn = op.get_bind() - - # ========================================================================= - # 1. Insert Loyalty platform - # ========================================================================= - conn.execute( - sa.text(""" - INSERT INTO platforms (code, name, description, domain, path_prefix, default_language, - supported_languages, is_active, is_public, theme_config, settings, - created_at, updated_at) - VALUES ('loyalty', 'Loyalty+', 'Customer loyalty program platform for Luxembourg businesses', - 'loyalty.lu', 'loyalty', 'fr', '["fr", "de", "en"]', true, true, - '{"primary_color": "#8B5CF6", "secondary_color": "#A78BFA"}', - '{"features": ["points", "rewards", "tiers", "analytics"]}', - CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - """) - ) - - # Get the Loyalty platform ID - result = conn.execute(sa.text("SELECT id FROM platforms WHERE code = 'loyalty'")) - loyalty_platform_id = result.fetchone()[0] - - # ========================================================================= - # 2. Create platform marketing pages (is_platform_page=True) - # ========================================================================= - platform_pages = [ - { - "slug": "home", - "title": "Loyalty+ - Customer Loyalty Platform", - "content": """
Reward your customers, increase retention, and grow your business with Loyalty+
-Create custom point systems that incentivize repeat purchases and customer engagement.
-Reward your best customers with exclusive benefits and VIP treatment.
-Track program performance and customer behavior with detailed insights.
-Choose the plan that fits your business
-Everything you need to build and manage a successful loyalty program
-Create flexible point systems with custom earning rules based on purchases, actions, or special events.
-Offer enticing rewards that keep customers coming back.
-Recognize and reward your most loyal customers with tiered benefits.
-Make data-driven decisions with comprehensive analytics.
-Launch your loyalty program in just a few steps
-Create your account and choose your plan. No credit card required for the free trial.
-Set up your point rules, rewards, and member tiers using our intuitive dashboard.
-Connect Loyalty+ to your POS, e-commerce, or app using our APIs and plugins.
-Invite your customers and watch your loyalty program drive results.
-Welcome to our customer loyalty program! We value your continued support and want to reward you for being part of our community.
- -Simply sign up, start earning points with every purchase, and redeem them for rewards you'll love.
-Browse our selection of rewards and redeem your hard-earned points!
- -Your rewards catalog will appear here once configured.
-Last updated: January 2026
- -Membership in our loyalty program is free and open to all customers who meet the eligibility requirements.
- -Points are earned on qualifying purchases. The earning rate and qualifying purchases are determined by the program operator and may change with notice.
- -Points can be redeemed for rewards as shown in the rewards catalog. Minimum point thresholds may apply.
- -Points may expire after a period of account inactivity. Members will be notified before points expire.
- -We reserve the right to modify, suspend, or terminate the program with reasonable notice to members.
- -Your personal information is handled in accordance with our Privacy Policy.
-Last updated: January 2026
- -We collect information you provide when joining our loyalty program, including:
-Your information helps us:
-We implement appropriate security measures to protect your personal information in accordance with GDPR and Luxembourg data protection laws.
- -You have the right to access, correct, or delete your personal data. Contact us to exercise these rights.
- -For privacy inquiries, please contact our data protection officer.
-All-in-one e-commerce, loyalty, and business solutions for Luxembourg merchants
-Order Management System for multi-channel selling. Manage orders, inventory, and fulfillment from one dashboard.
- Learn More -Customer loyalty platform to reward your customers and increase retention. Points, rewards, and member tiers.
- Learn More -Create beautiful websites for your local business. No coding required.
- Coming Soon -We're building the tools Luxembourg businesses need to thrive in the digital economy. From order management to customer loyalty, we provide the infrastructure that powers local commerce.
-Wizamart was founded with a simple idea: Luxembourg businesses deserve world-class e-commerce tools that understand their unique needs. Local languages, local payment methods, local compliance - built in from the start.
-We're a team of developers, designers, and business experts based in Luxembourg. We understand the local market because we're part of it.
-Each platform has its own pricing. Choose the tools your business needs.
- -Order Management System
-Customer Loyalty Platform
-Use multiple platforms together
-Wizamart is a suite of business tools for Luxembourg merchants, including order management (OMS), customer loyalty (Loyalty+), and website building (Site Builder).
-No! Each platform works independently. Use one, two, or all three - whatever fits your business needs.
-All platforms support French, German, and English - the three main languages of Luxembourg.
-Yes! All platforms offer a 14-day free trial with no credit card required.
-We accept credit cards, SEPA direct debit, and bank transfers.
-Yes, you can cancel your subscription at any time. No long-term contracts required.
-All plans include email support. Professional and Business plans include priority support with faster response times.
-Yes! We offer guided onboarding for all new customers to help you get started quickly.
-We'd love to hear from you. Get in touch with our team.
- - - -Wizamart S.à r.l.
- Luxembourg City
- Luxembourg
Last updated: January 2026
- -These Terms of Service govern your use of Wizamart platforms and services.
- -By accessing or using our services, you agree to be bound by these Terms.
- -Wizamart provides e-commerce and business management tools including order management, loyalty programs, and website building services.
- -You must provide accurate information when creating an account and keep your login credentials secure.
- -Subscription fees are billed in advance on a monthly or annual basis. Prices are listed in EUR and include applicable VAT for Luxembourg customers.
- -We process personal data in accordance with our Privacy Policy and applicable data protection laws including GDPR.
- -Our liability is limited to the amount paid for services in the 12 months preceding any claim.
- -These Terms are governed by Luxembourg law. Disputes shall be resolved in Luxembourg courts.
- -For questions about these Terms, contact us at legal@wizamart.lu
-Last updated: January 2026
- -Wizamart S.à r.l. ("we", "us") is committed to protecting your privacy. This policy explains how we collect, use, and protect your personal data.
- -Wizamart S.à r.l.
Luxembourg City, Luxembourg
Email: privacy@wizamart.lu
Under GDPR, you have the right to:
-To exercise your rights or ask questions, contact our Data Protection Officer at privacy@wizamart.lu
-