diff --git a/.architecture-rules/api.yaml b/.architecture-rules/api.yaml index d9e9364a..d992bc91 100644 --- a/.architecture-rules/api.yaml +++ b/.architecture-rules/api.yaml @@ -24,7 +24,9 @@ api_endpoint_rules: SCHEMA LOCATION: All response schemas must be defined in models/schema/*.py, never inline in endpoint files. This ensures schemas are reusable and discoverable. pattern: - file_pattern: "app/api/v1/**/*.py" + file_pattern: + - "app/api/v1/**/*.py" + - "app/modules/*/routes/api/**/*.py" anti_patterns: - "return dict" - "-> dict" @@ -82,7 +84,9 @@ api_endpoint_rules: # In app/api/v1/admin/my_feature.py from models.schema.my_feature import MyRequest pattern: - file_pattern: "app/api/v1/**/*.py" + file_pattern: + - "app/api/v1/**/*.py" + - "app/modules/*/routes/api/**/*.py" anti_patterns: - "from pydantic import" - "from pydantic.main import" @@ -118,7 +122,9 @@ api_endpoint_rules: - db.query() - complex queries are business logic - db.delete() - deleting entities is business logic pattern: - file_pattern: "app/api/v1/**/*.py" + file_pattern: + - "app/api/v1/**/*.py" + - "app/modules/*/routes/api/**/*.py" anti_patterns: - "db.add(" - "db.delete(" @@ -155,7 +161,9 @@ api_endpoint_rules: # Dependency guarantees token_vendor_id is present return order_service.get_orders(db, current_user.token_vendor_id) pattern: - file_pattern: "app/api/v1/**/*.py" + file_pattern: + - "app/api/v1/**/*.py" + - "app/modules/*/routes/api/**/*.py" anti_patterns: - "raise HTTPException" - "raise InvalidTokenException" @@ -248,7 +256,9 @@ api_endpoint_rules: - from models.database.* - from app.modules.*.models.* pattern: - file_pattern: "app/api/**/*.py" + file_pattern: + - "app/api/**/*.py" + - "app/modules/*/routes/api/**/*.py" anti_patterns: - "from models\\.database\\." - "from app\\.modules\\.[a-z_]+\\.models\\." diff --git a/alembic/versions_backup/09d84a46530f_add_celery_task_id_to_job_tables.py b/alembic/versions_backup/09d84a46530f_add_celery_task_id_to_job_tables.py index 4e693903..b5732770 100644 --- a/alembic/versions_backup/09d84a46530f_add_celery_task_id_to_job_tables.py +++ b/alembic/versions_backup/09d84a46530f_add_celery_task_id_to_job_tables.py @@ -5,51 +5,52 @@ Revises: y3d4e5f6g7h8 Create Date: 2026-01-11 16:44:59.070110 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op + # 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 +revision: str = "09d84a46530f" +down_revision: str | None = "y3d4e5f6g7h8" +branch_labels: str | Sequence[str] | None = None +depends_on: 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) + 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) + 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) + 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) + 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') + 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') + 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') + 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') + 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_backup/0bd9ffaaced1_add_application_logs_table_for_hybrid_.py b/alembic/versions_backup/0bd9ffaaced1_add_application_logs_table_for_hybrid_.py index 3d091068..695ec102 100644 --- a/alembic/versions_backup/0bd9ffaaced1_add_application_logs_table_for_hybrid_.py +++ b/alembic/versions_backup/0bd9ffaaced1_add_application_logs_table_for_hybrid_.py @@ -5,64 +5,64 @@ Revises: 7a7ce92593d5 Create Date: 2025-11-29 12:44:55.427245 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "0bd9ffaaced1" +down_revision: str | None = "7a7ce92593d5" +branch_labels: str | Sequence[str] | None = None +depends_on: 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') + "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) + 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') + 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') + op.drop_table("application_logs") diff --git a/alembic/versions_backup/1b398cf45e85_add_letzshop_vendor_cache_table.py b/alembic/versions_backup/1b398cf45e85_add_letzshop_vendor_cache_table.py index b113d400..b67a91d4 100644 --- a/alembic/versions_backup/1b398cf45e85_add_letzshop_vendor_cache_table.py +++ b/alembic/versions_backup/1b398cf45e85_add_letzshop_vendor_cache_table.py @@ -5,363 +5,363 @@ Revises: 09d84a46530f Create Date: 2026-01-13 19:38:45.423378 """ -from typing import Sequence, Union +from collections.abc import Sequence + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql, sqlite 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 +revision: str = "1b398cf45e85" +down_revision: str | None = "09d84a46530f" +branch_labels: str | Sequence[str] | None = None +depends_on: 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_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', + 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_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_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_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_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_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_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_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_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_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', + 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_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_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_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_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_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_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_server_default=sa.text("now()")) + op.alter_column("product_media", "updated_at", existing_type=postgresql.TIMESTAMP(), nullable=False) - op.alter_column('products', 'is_digital', + 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_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', + 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_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_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_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_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_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_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_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', + op.alter_column("vendors", "storefront_locale", existing_type=sa.VARCHAR(length=10), - comment='Currency/number formatting locale (NULL = inherit from platform)', + 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', + 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_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_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_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_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_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_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', + 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_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', + 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_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_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_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_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_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_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_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', + 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_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_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_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_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_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_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_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_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') + 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_backup/204273a59d73_add_letzshop_historical_import_jobs_.py b/alembic/versions_backup/204273a59d73_add_letzshop_historical_import_jobs_.py index 44e6fe65..c326b952 100644 --- a/alembic/versions_backup/204273a59d73_add_letzshop_historical_import_jobs_.py +++ b/alembic/versions_backup/204273a59d73_add_letzshop_historical_import_jobs_.py @@ -5,53 +5,55 @@ Revises: cb88bc9b5f86 Create Date: 2025-12-19 05:40:53.463341 """ -from typing import Sequence, Union +from collections.abc import Sequence + +import sqlalchemy as sa 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 +revision: str = "204273a59d73" +down_revision: str | None = "cb88bc9b5f86" +branch_labels: str | Sequence[str] | None = None +depends_on: 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_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) + 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') + 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_backup/2362c2723a93_add_order_date_to_letzshop_orders.py b/alembic/versions_backup/2362c2723a93_add_order_date_to_letzshop_orders.py index c96cc7e1..52125995 100644 --- a/alembic/versions_backup/2362c2723a93_add_order_date_to_letzshop_orders.py +++ b/alembic/versions_backup/2362c2723a93_add_order_date_to_letzshop_orders.py @@ -5,23 +5,23 @@ Revises: 204273a59d73 Create Date: 2025-12-19 08:46:23.731912 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "2362c2723a93" +down_revision: str | None = "204273a59d73" +branch_labels: str | Sequence[str] | None = None +depends_on: 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)) + 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') + op.drop_column("letzshop_orders", "order_date") diff --git a/alembic/versions_backup/28d44d503cac_add_contact_fields_to_vendor.py b/alembic/versions_backup/28d44d503cac_add_contact_fields_to_vendor.py index efe0a18d..15238d4f 100644 --- a/alembic/versions_backup/28d44d503cac_add_contact_fields_to_vendor.py +++ b/alembic/versions_backup/28d44d503cac_add_contact_fields_to_vendor.py @@ -5,33 +5,33 @@ Revises: 9f3a25ea4991 Create Date: 2025-12-03 22:26:02.161087 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "28d44d503cac" +down_revision: str | None = "9f3a25ea4991" +branch_labels: str | Sequence[str] | None = None +depends_on: 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)) + 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') + 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_backup/2953ed10d22c_add_subscription_billing_tables.py b/alembic/versions_backup/2953ed10d22c_add_subscription_billing_tables.py index 346270fd..82c5f4f5 100644 --- a/alembic/versions_backup/2953ed10d22c_add_subscription_billing_tables.py +++ b/alembic/versions_backup/2953ed10d22c_add_subscription_billing_tables.py @@ -5,18 +5,20 @@ Revises: e1bfb453fbe9 Create Date: 2025-12-25 18:29:34.167773 """ +from collections.abc import Sequence from datetime import datetime -from typing import Sequence, Union + +import sqlalchemy as sa 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 +revision: str = "2953ed10d22c" +down_revision: str | None = "e1bfb453fbe9" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: @@ -25,146 +27,146 @@ def upgrade() -> None: # ========================================================================= # 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_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) + 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_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) + 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_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) + 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_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) + 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_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) + 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)) + 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 @@ -172,106 +174,106 @@ def upgrade() -> None: 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), + "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": "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' + "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, + "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' + "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, + "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' + "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, + "display_order": 4, + "is_active": True, + "is_public": False, + "created_at": now, + "updated_at": now, }, ]) @@ -279,141 +281,141 @@ def upgrade() -> None: # 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), + "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": "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_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_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": "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, + "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') + 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') + 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') + 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') + 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') + 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') + 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_backup/404b3e2d2865_add_letzshop_vendor_fields_and_trial_.py b/alembic/versions_backup/404b3e2d2865_add_letzshop_vendor_fields_and_trial_.py index 6fa0242f..e57ecdb0 100644 --- a/alembic/versions_backup/404b3e2d2865_add_letzshop_vendor_fields_and_trial_.py +++ b/alembic/versions_backup/404b3e2d2865_add_letzshop_vendor_fields_and_trial_.py @@ -9,36 +9,36 @@ Adds: - 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 collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "404b3e2d2865" +down_revision: str | None = "l0a1b2c3d4e5" +branch_labels: str | Sequence[str] | None = None +depends_on: 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) + 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)) + 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') + 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') + 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_backup/4951b2e50581_initial_migration_all_tables.py b/alembic/versions_backup/4951b2e50581_initial_migration_all_tables.py index bdf730a1..2d74e741 100644 --- a/alembic/versions_backup/4951b2e50581_initial_migration_all_tables.py +++ b/alembic/versions_backup/4951b2e50581_initial_migration_all_tables.py @@ -6,7 +6,7 @@ Create Date: 2025-10-27 22:28:33.137564 """ -from typing import Sequence, Union +from collections.abc import Sequence import sqlalchemy as sa @@ -14,9 +14,9 @@ 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 +down_revision: str | None = None +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/alembic/versions_backup/55b92e155566_add_order_tracking_fields.py b/alembic/versions_backup/55b92e155566_add_order_tracking_fields.py index 8e80682d..031afa87 100644 --- a/alembic/versions_backup/55b92e155566_add_order_tracking_fields.py +++ b/alembic/versions_backup/55b92e155566_add_order_tracking_fields.py @@ -5,27 +5,27 @@ Revises: d2e3f4a5b6c7 Create Date: 2025-12-20 18:07:51.144136 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "55b92e155566" +down_revision: str | None = "d2e3f4a5b6c7" +branch_labels: str | Sequence[str] | None = None +depends_on: 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)) + 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') + op.drop_column("orders", "shipping_carrier") + op.drop_column("orders", "shipment_number") + op.drop_column("orders", "tracking_url") diff --git a/alembic/versions_backup/5818330181a5_make_vendor_owner_user_id_nullable_for_.py b/alembic/versions_backup/5818330181a5_make_vendor_owner_user_id_nullable_for_.py index cb3a74bc..fb360e18 100644 --- a/alembic/versions_backup/5818330181a5_make_vendor_owner_user_id_nullable_for_.py +++ b/alembic/versions_backup/5818330181a5_make_vendor_owner_user_id_nullable_for_.py @@ -5,17 +5,17 @@ Revises: d0325d7c0f25 Create Date: 2025-12-01 20:30:06.158027 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "5818330181a5" +down_revision: str | None = "d0325d7c0f25" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: @@ -29,8 +29,8 @@ def upgrade() -> None: 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', + with op.batch_alter_table("vendors", schema=None) as batch_op: + batch_op.alter_column("owner_user_id", existing_type=sa.INTEGER(), nullable=True) @@ -42,7 +42,7 @@ def downgrade() -> None: 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', + 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_backup/72aa309d4007_ensure_content_pages_table_with_all_.py b/alembic/versions_backup/72aa309d4007_ensure_content_pages_table_with_all_.py index 24a6d43d..9cb8ff16 100644 --- a/alembic/versions_backup/72aa309d4007_ensure_content_pages_table_with_all_.py +++ b/alembic/versions_backup/72aa309d4007_ensure_content_pages_table_with_all_.py @@ -6,7 +6,7 @@ Create Date: 2025-11-22 15:16:13.213613 """ -from typing import Sequence, Union +from collections.abc import Sequence import sqlalchemy as sa @@ -14,9 +14,9 @@ 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 +down_revision: str | None = "fef1d20ce8b4" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/alembic/versions_backup/7a7ce92593d5_add_architecture_quality_tracking_tables.py b/alembic/versions_backup/7a7ce92593d5_add_architecture_quality_tracking_tables.py index f5e60325..46bf1b64 100644 --- a/alembic/versions_backup/7a7ce92593d5_add_architecture_quality_tracking_tables.py +++ b/alembic/versions_backup/7a7ce92593d5_add_architecture_quality_tracking_tables.py @@ -6,18 +6,17 @@ Create Date: 2025-11-28 09:21:16.545203 """ -from typing import Sequence, Union +from collections.abc import Sequence 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 +down_revision: str | None = "a2064e1dfcd4" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/alembic/versions_backup/82ea1b4a3ccb_add_test_run_tables.py b/alembic/versions_backup/82ea1b4a3ccb_add_test_run_tables.py index 2c9f6986..871e4e8f 100644 --- a/alembic/versions_backup/82ea1b4a3ccb_add_test_run_tables.py +++ b/alembic/versions_backup/82ea1b4a3ccb_add_test_run_tables.py @@ -5,99 +5,99 @@ Revises: b4c5d6e7f8a9 Create Date: 2025-12-12 22:48:09.501172 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "82ea1b4a3ccb" +down_revision: str | None = "b4c5d6e7f8a9" +branch_labels: str | Sequence[str] | None = None +depends_on: 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_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) + 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_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) + 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_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) + 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') + 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') + 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') + op.drop_index(op.f("ix_test_collections_id"), table_name="test_collections") + op.drop_table("test_collections") diff --git a/alembic/versions_backup/91d02647efae_add_marketplace_import_errors_table.py b/alembic/versions_backup/91d02647efae_add_marketplace_import_errors_table.py index 29db8b03..4fb78b25 100644 --- a/alembic/versions_backup/91d02647efae_add_marketplace_import_errors_table.py +++ b/alembic/versions_backup/91d02647efae_add_marketplace_import_errors_table.py @@ -5,40 +5,41 @@ Revises: 987b4ecfa503 Create Date: 2025-12-13 13:13:46.969503 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op + # 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 +revision: str = "91d02647efae" +down_revision: str | None = "987b4ecfa503" +branch_labels: str | Sequence[str] | None = None +depends_on: 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_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) + 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') + 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_backup/987b4ecfa503_add_letzshop_integration_tables.py b/alembic/versions_backup/987b4ecfa503_add_letzshop_integration_tables.py index 54605a38..d4388b59 100644 --- a/alembic/versions_backup/987b4ecfa503_add_letzshop_integration_tables.py +++ b/alembic/versions_backup/987b4ecfa503_add_letzshop_integration_tables.py @@ -11,169 +11,169 @@ This migration adds: - letzshop_sync_logs: Audit trail for sync operations - Adds channel fields to orders table for multi-marketplace support """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "987b4ecfa503" +down_revision: str | None = "82ea1b4a3ccb" +branch_labels: str | Sequence[str] | None = None +depends_on: 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) + 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_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) + 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_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) + 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_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) + 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_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) + 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') + 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') + 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') + 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') + 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') + 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_backup/9f3a25ea4991_remove_vendor_owner_user_id_column.py b/alembic/versions_backup/9f3a25ea4991_remove_vendor_owner_user_id_column.py index 98c9ffa6..f631f8df 100644 --- a/alembic/versions_backup/9f3a25ea4991_remove_vendor_owner_user_id_column.py +++ b/alembic/versions_backup/9f3a25ea4991_remove_vendor_owner_user_id_column.py @@ -13,17 +13,17 @@ Architecture Change: The vendor ownership is now determined via the company relationship: - vendor.company.owner_user_id contains the owner """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "9f3a25ea4991" +down_revision: str | None = "5818330181a5" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: @@ -35,9 +35,9 @@ def upgrade() -> None: 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: + 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') + batch_op.drop_column("owner_user_id") def downgrade() -> None: @@ -48,13 +48,13 @@ def downgrade() -> None: 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: + with op.batch_alter_table("vendors", schema=None) as batch_op: batch_op.add_column( - sa.Column('owner_user_id', sa.Integer(), nullable=True) + sa.Column("owner_user_id", sa.Integer(), nullable=True) ) batch_op.create_foreign_key( - 'vendors_owner_user_id_fkey', - 'users', - ['owner_user_id'], - ['id'] + "vendors_owner_user_id_fkey", + "users", + ["owner_user_id"], + ["id"] ) diff --git a/alembic/versions_backup/a2064e1dfcd4_add_cart_items_table.py b/alembic/versions_backup/a2064e1dfcd4_add_cart_items_table.py index 3eca3ff3..c3200a1e 100644 --- a/alembic/versions_backup/a2064e1dfcd4_add_cart_items_table.py +++ b/alembic/versions_backup/a2064e1dfcd4_add_cart_items_table.py @@ -6,7 +6,7 @@ Create Date: 2025-11-23 19:52:40.509538 """ -from typing import Sequence, Union +from collections.abc import Sequence import sqlalchemy as sa @@ -14,9 +14,9 @@ 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 +down_revision: str | None = "f68d8da5315a" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/alembic/versions_backup/a3b4c5d6e7f8_add_product_override_fields.py b/alembic/versions_backup/a3b4c5d6e7f8_add_product_override_fields.py index 2f402609..e8674cc3 100644 --- a/alembic/versions_backup/a3b4c5d6e7f8_add_product_override_fields.py +++ b/alembic/versions_backup/a3b4c5d6e7f8_add_product_override_fields.py @@ -15,7 +15,7 @@ The override pattern: NULL value means "inherit from marketplace_product". Setting a value creates a vendor-specific override. """ -from typing import Sequence, Union +from collections.abc import Sequence import sqlalchemy as sa @@ -23,9 +23,9 @@ 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 +down_revision: str | None = "f2b3c4d5e6f7" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/alembic/versions_backup/a9a86cef6cca_add_letzshop_order_locale_and_country_.py b/alembic/versions_backup/a9a86cef6cca_add_letzshop_order_locale_and_country_.py index 731d6ae6..9e4a0854 100644 --- a/alembic/versions_backup/a9a86cef6cca_add_letzshop_order_locale_and_country_.py +++ b/alembic/versions_backup/a9a86cef6cca_add_letzshop_order_locale_and_country_.py @@ -5,27 +5,27 @@ Revises: fcfdc02d5138 Create Date: 2025-12-17 20:55:41.477848 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "a9a86cef6cca" +down_revision: str | None = "fcfdc02d5138" +branch_labels: str | Sequence[str] | None = None +depends_on: 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)) + 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') + 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_backup/b412e0b49c2e_add_language_column_to_marketplace_.py b/alembic/versions_backup/b412e0b49c2e_add_language_column_to_marketplace_.py index 1c1ca208..7637af95 100644 --- a/alembic/versions_backup/b412e0b49c2e_add_language_column_to_marketplace_.py +++ b/alembic/versions_backup/b412e0b49c2e_add_language_column_to_marketplace_.py @@ -5,26 +5,26 @@ Revises: 91d02647efae Create Date: 2025-12-13 13:35:46.524893 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "b412e0b49c2e" +down_revision: str | None = "91d02647efae" +branch_labels: str | Sequence[str] | None = None +depends_on: 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') + "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') + op.drop_column("marketplace_import_jobs", "language") diff --git a/alembic/versions_backup/b4c5d6e7f8a9_migrate_product_data_to_translations.py b/alembic/versions_backup/b4c5d6e7f8a9_migrate_product_data_to_translations.py index a597de9e..f8c54bdb 100644 --- a/alembic/versions_backup/b4c5d6e7f8a9_migrate_product_data_to_translations.py +++ b/alembic/versions_backup/b4c5d6e7f8a9_migrate_product_data_to_translations.py @@ -15,7 +15,7 @@ after migrating the data to the new structure. """ import re -from typing import Sequence, Union +from collections.abc import Sequence import sqlalchemy as sa from sqlalchemy import text @@ -24,9 +24,9 @@ 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 +down_revision: str | None = "a3b4c5d6e7f8" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def parse_price(price_str: str) -> float | None: diff --git a/alembic/versions_backup/ba2c0ce78396_add_show_in_legal_to_content_pages.py b/alembic/versions_backup/ba2c0ce78396_add_show_in_legal_to_content_pages.py index bd241679..1f78a8d8 100644 --- a/alembic/versions_backup/ba2c0ce78396_add_show_in_legal_to_content_pages.py +++ b/alembic/versions_backup/ba2c0ce78396_add_show_in_legal_to_content_pages.py @@ -5,17 +5,17 @@ Revises: m1b2c3d4e5f6 Create Date: 2025-12-28 20:00:24.263518 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "ba2c0ce78396" +down_revision: str | None = "m1b2c3d4e5f6" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: @@ -25,8 +25,8 @@ def upgrade() -> None: 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) + "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) @@ -38,4 +38,4 @@ def upgrade() -> None: def downgrade() -> None: """Remove show_in_legal column from content_pages table.""" - op.drop_column('content_pages', 'show_in_legal') + op.drop_column("content_pages", "show_in_legal") diff --git a/alembic/versions_backup/c00d2985701f_add_letzshop_credentials_carrier_fields.py b/alembic/versions_backup/c00d2985701f_add_letzshop_credentials_carrier_fields.py index c4a9bab2..d9d47aba 100644 --- a/alembic/versions_backup/c00d2985701f_add_letzshop_credentials_carrier_fields.py +++ b/alembic/versions_backup/c00d2985701f_add_letzshop_credentials_carrier_fields.py @@ -5,31 +5,31 @@ Revises: 55b92e155566 Create Date: 2025-12-20 18:49:53.432904 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "c00d2985701f" +down_revision: str | None = "55b92e155566" +branch_labels: str | Sequence[str] | None = None +depends_on: 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)) + 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') + 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_backup/c1d2e3f4a5b6_unified_order_schema.py b/alembic/versions_backup/c1d2e3f4a5b6_unified_order_schema.py index 0f70e8fb..ff44d79b 100644 --- a/alembic/versions_backup/c1d2e3f4a5b6_unified_order_schema.py +++ b/alembic/versions_backup/c1d2e3f4a5b6_unified_order_schema.py @@ -21,18 +21,18 @@ Design principles: - Customer/address data snapshotted at order time - Products must exist in catalog (enforced by FK) """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa from sqlalchemy import inspect +from alembic import op # 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 +revision: str = "c1d2e3f4a5b6" +down_revision: str | None = "2362c2723a93" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def table_exists(table_name: str) -> bool: @@ -48,7 +48,7 @@ def index_exists(index_name: str, table_name: str) -> bool: inspector = inspect(bind) try: indexes = inspector.get_indexes(table_name) - return any(idx['name'] == index_name for idx in indexes) + return any(idx["name"] == index_name for idx in indexes) except Exception: return False @@ -71,382 +71,382 @@ def upgrade() -> None: # ========================================================================= # 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') + 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') + 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') + 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') + 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), + 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'), + 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), + 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'), + 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), + 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), + 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), + 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), + 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), + 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), + 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), + 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') + 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) + 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), + 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), + 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), + 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), + 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), + 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), + 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), + 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') + 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) + 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), + 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), + sa.Column("operation", sa.String(length=50), nullable=False), # Operation payload - sa.Column('payload', sa.JSON(), nullable=False), + 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), + 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), + 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), + 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') + 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) + 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') + 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') + 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') + 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_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) + 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_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) + 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_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) + 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_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) + 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_backup/c9e22eadf533_add_tax_rate_cost_and_letzshop_settings.py b/alembic/versions_backup/c9e22eadf533_add_tax_rate_cost_and_letzshop_settings.py index 6f1dcc02..e2a068ca 100644 --- a/alembic/versions_backup/c9e22eadf533_add_tax_rate_cost_and_letzshop_settings.py +++ b/alembic/versions_backup/c9e22eadf533_add_tax_rate_cost_and_letzshop_settings.py @@ -9,56 +9,56 @@ Adds: - 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 collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "c9e22eadf533" +down_revision: str | None = "e1f2a3b4c5d6" +branch_labels: str | Sequence[str] | None = None +depends_on: 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')) + 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)) + 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') + 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')) + 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') + 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)) + 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') + with op.batch_alter_table("marketplace_products", schema=None) as batch_op: + batch_op.drop_column("tax_rate_percent") diff --git a/alembic/versions_backup/cb88bc9b5f86_add_gtin_columns_to_product_table.py b/alembic/versions_backup/cb88bc9b5f86_add_gtin_columns_to_product_table.py index faa6a494..376e88fe 100644 --- a/alembic/versions_backup/cb88bc9b5f86_add_gtin_columns_to_product_table.py +++ b/alembic/versions_backup/cb88bc9b5f86_add_gtin_columns_to_product_table.py @@ -5,33 +5,33 @@ Revises: a9a86cef6cca Create Date: 2025-12-18 20:54:55.185857 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "cb88bc9b5f86" +down_revision: str | None = "a9a86cef6cca" +branch_labels: str | Sequence[str] | None = None +depends_on: 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)) + 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) + 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') + 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_backup/d0325d7c0f25_add_companies_table_and_restructure_.py b/alembic/versions_backup/d0325d7c0f25_add_companies_table_and_restructure_.py index ceb0bba0..c4379821 100644 --- a/alembic/versions_backup/d0325d7c0f25_add_companies_table_and_restructure_.py +++ b/alembic/versions_backup/d0325d7c0f25_add_companies_table_and_restructure_.py @@ -5,73 +5,73 @@ Revises: 0bd9ffaaced1 Create Date: 2025-11-30 14:58:17.165142 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "d0325d7c0f25" +down_revision: str | None = "0bd9ffaaced1" +branch_labels: str | Sequence[str] | None = None +depends_on: 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') + "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) + 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: + 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']) + 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') + 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: + 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)) + 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') + 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') + 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_backup/d2e3f4a5b6c7_add_order_item_exceptions.py b/alembic/versions_backup/d2e3f4a5b6c7_add_order_item_exceptions.py index 3de56663..d29ac1a5 100644 --- a/alembic/versions_backup/d2e3f4a5b6c7_add_order_item_exceptions.py +++ b/alembic/versions_backup/d2e3f4a5b6c7_add_order_item_exceptions.py @@ -12,25 +12,25 @@ 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 collections.abc import Sequence -from alembic import op import sqlalchemy as sa from sqlalchemy import inspect +from alembic import op # 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 +revision: str = "d2e3f4a5b6c7" +down_revision: str | None = "c1d2e3f4a5b6" +branch_labels: str | Sequence[str] | None = None +depends_on: 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)] + columns = [col["name"] for col in inspector.get_columns(table_name)] return column_name in columns @@ -47,7 +47,7 @@ def index_exists(index_name: str, table_name: str) -> bool: inspector = inspect(bind) try: indexes = inspector.get_indexes(table_name) - return any(idx['name'] == index_name for idx in indexes) + return any(idx["name"] == index_name for idx in indexes) except Exception: return False @@ -56,124 +56,124 @@ def upgrade() -> None: # ========================================================================= # Step 1: Add needs_product_match column to order_items # ========================================================================= - if not column_exists('order_items', 'needs_product_match'): + if not column_exists("order_items", "needs_product_match"): op.add_column( - 'order_items', + "order_items", sa.Column( - 'needs_product_match', + "needs_product_match", sa.Boolean(), - server_default='0', + server_default="0", nullable=False ) ) - if not index_exists('ix_order_items_needs_product_match', 'order_items'): + 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'] + "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'): + 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), + "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', + "exception_type", sa.String(length=50), nullable=False, - server_default='product_not_found' + server_default="product_not_found" ), sa.Column( - 'status', + "status", sa.String(length=50), nullable=False, - server_default='pending' + 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("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', + "created_at", sa.DateTime(timezone=True), - server_default=sa.text('(CURRENT_TIMESTAMP)'), + server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False ), sa.Column( - 'updated_at', + "updated_at", sa.DateTime(timezone=True), - server_default=sa.text('(CURRENT_TIMESTAMP)'), + server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False ), sa.ForeignKeyConstraint( - ['order_item_id'], - ['order_items.id'], - ondelete='CASCADE' + ["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') + 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'] + "ix_order_item_exceptions_id", + "order_item_exceptions", + ["id"] ) op.create_index( - 'ix_order_item_exceptions_vendor_id', - 'order_item_exceptions', - ['vendor_id'] + "ix_order_item_exceptions_vendor_id", + "order_item_exceptions", + ["vendor_id"] ) op.create_index( - 'ix_order_item_exceptions_status', - 'order_item_exceptions', - ['status'] + "ix_order_item_exceptions_status", + "order_item_exceptions", + ["status"] ) op.create_index( - 'idx_exception_vendor_status', - 'order_item_exceptions', - ['vendor_id', 'status'] + "idx_exception_vendor_status", + "order_item_exceptions", + ["vendor_id", "status"] ) op.create_index( - 'idx_exception_gtin', - 'order_item_exceptions', - ['vendor_id', 'original_gtin'] + "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'], + "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') + 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') + 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_backup/d7a4a3f06394_add_email_templates_and_logs_tables.py b/alembic/versions_backup/d7a4a3f06394_add_email_templates_and_logs_tables.py index e3dfe996..a7facd44 100644 --- a/alembic/versions_backup/d7a4a3f06394_add_email_templates_and_logs_tables.py +++ b/alembic/versions_backup/d7a4a3f06394_add_email_templates_and_logs_tables.py @@ -5,87 +5,87 @@ Revises: 404b3e2d2865 Create Date: 2025-12-27 20:48:00.661523 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa from sqlalchemy import text +from alembic import op # 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 +revision: str = "d7a4a3f06394" +down_revision: str | None = "404b3e2d2865" +branch_labels: str | Sequence[str] | None = None +depends_on: 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_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) + 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_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) + 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) + 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")) @@ -93,17 +93,17 @@ def upgrade() -> None: 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) + 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.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(""" @@ -120,8 +120,8 @@ def upgrade() -> None: 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) + 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")) @@ -146,7 +146,7 @@ def upgrade() -> None: """)) # order_items - alter column - op.alter_column('order_items', 'needs_product_match', existing_type=sa.BOOLEAN(), nullable=True) + 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")) @@ -185,7 +185,7 @@ def upgrade() -> None: 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.alter_column("vendor_subscriptions", "payment_retry_count", existing_type=sa.INTEGER(), nullable=False) op.execute(text(""" DO $$ BEGIN @@ -207,12 +207,12 @@ def upgrade() -> None: 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) + 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) + 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")) @@ -226,7 +226,7 @@ def downgrade() -> None: # 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) + 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")) @@ -260,7 +260,7 @@ def downgrade() -> None: # 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) + 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")) @@ -278,8 +278,8 @@ def downgrade() -> None: 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) + 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")) @@ -296,17 +296,17 @@ def downgrade() -> None: 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) + 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) + 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")) @@ -314,19 +314,19 @@ def downgrade() -> None: 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) + 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') + 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_backup/e1a2b3c4d5e6_add_product_type_and_digital_fields.py b/alembic/versions_backup/e1a2b3c4d5e6_add_product_type_and_digital_fields.py index 6b728a8a..d3059094 100644 --- a/alembic/versions_backup/e1a2b3c4d5e6_add_product_type_and_digital_fields.py +++ b/alembic/versions_backup/e1a2b3c4d5e6_add_product_type_and_digital_fields.py @@ -17,16 +17,17 @@ 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 +from collections.abc import Sequence 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 +down_revision: str | None = "28d44d503cac" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/alembic/versions_backup/e1bfb453fbe9_add_warehouse_and_bin_location_to_.py b/alembic/versions_backup/e1bfb453fbe9_add_warehouse_and_bin_location_to_.py index 80d263a7..831fe07a 100644 --- a/alembic/versions_backup/e1bfb453fbe9_add_warehouse_and_bin_location_to_.py +++ b/alembic/versions_backup/e1bfb453fbe9_add_warehouse_and_bin_location_to_.py @@ -5,18 +5,18 @@ Revises: j8e9f0a1b2c3 Create Date: 2025-12-25 12:21:24.006548 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa from sqlalchemy import text +from alembic import op # 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 +revision: str = "e1bfb453fbe9" +down_revision: str | None = "j8e9f0a1b2c3" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def get_column_names(conn, table_name: str) -> set: @@ -43,11 +43,11 @@ def upgrade() -> None: # 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 "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='')) + 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(""" @@ -60,12 +60,12 @@ def upgrade() -> None: # 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) + 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: @@ -74,17 +74,17 @@ def downgrade() -> None: # 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') + 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') + 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_backup/e1f2a3b4c5d6_convert_prices_to_integer_cents.py b/alembic/versions_backup/e1f2a3b4c5d6_convert_prices_to_integer_cents.py index e35312a9..bb5b2a07 100644 --- a/alembic/versions_backup/e1f2a3b4c5d6_convert_prices_to_integer_cents.py +++ b/alembic/versions_backup/e1f2a3b4c5d6_convert_prices_to_integer_cents.py @@ -20,17 +20,17 @@ Affected tables: See docs/architecture/money-handling.md for full documentation. """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "e1f2a3b4c5d6" +down_revision: str | None = "c00d2985701f" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: @@ -38,186 +38,186 @@ def upgrade() -> None: # Strategy: Add new _cents columns, migrate data, drop old columns # === PRODUCTS TABLE === - with op.batch_alter_table('products', schema=None) as batch_op: + 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)) + 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') + 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') + 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)) + 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)') + 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') + 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', + 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)) + 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)') + 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', + 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', + 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)) + 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)') + 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', + 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)) + 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') + 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') + 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)) + 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') + 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') + 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)) + 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') + 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', + 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)) + 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') + 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', + 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', + 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)) + 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') + 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', + 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)) + 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') + 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') + 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_backup/e3f4a5b6c7d8_add_messaging_tables.py b/alembic/versions_backup/e3f4a5b6c7d8_add_messaging_tables.py index 62b9937d..2f84bb9d 100644 --- a/alembic/versions_backup/e3f4a5b6c7d8_add_messaging_tables.py +++ b/alembic/versions_backup/e3f4a5b6c7d8_add_messaging_tables.py @@ -16,18 +16,18 @@ Supports three communication channels: - Admin <-> Customer """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa from sqlalchemy import inspect +from alembic import op # 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 +down_revision: str | None = "c9e22eadf533" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def table_exists(table_name: str) -> bool: diff --git a/alembic/versions_backup/f2b3c4d5e6f7_create_translation_tables.py b/alembic/versions_backup/f2b3c4d5e6f7_create_translation_tables.py index 9db90556..f42573e4 100644 --- a/alembic/versions_backup/f2b3c4d5e6f7_create_translation_tables.py +++ b/alembic/versions_backup/f2b3c4d5e6f7_create_translation_tables.py @@ -13,7 +13,7 @@ language fallback capabilities. Fields in product_translations can be NULL to inherit from marketplace_product_translations. """ -from typing import Sequence, Union +from collections.abc import Sequence import sqlalchemy as sa @@ -21,9 +21,9 @@ 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 +down_revision: str | None = "e1a2b3c4d5e6" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/alembic/versions_backup/f4a5b6c7d8e9_add_validator_type_to_code_quality.py b/alembic/versions_backup/f4a5b6c7d8e9_add_validator_type_to_code_quality.py index 35e48b91..cac6fdc2 100644 --- a/alembic/versions_backup/f4a5b6c7d8e9_add_validator_type_to_code_quality.py +++ b/alembic/versions_backup/f4a5b6c7d8e9_add_validator_type_to_code_quality.py @@ -8,17 +8,17 @@ 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 collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +down_revision: str | None = "e3f4a5b6c7d8" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/alembic/versions_backup/f68d8da5315a_add_template_field_to_content_pages_for_.py b/alembic/versions_backup/f68d8da5315a_add_template_field_to_content_pages_for_.py index b095835d..55df5940 100644 --- a/alembic/versions_backup/f68d8da5315a_add_template_field_to_content_pages_for_.py +++ b/alembic/versions_backup/f68d8da5315a_add_template_field_to_content_pages_for_.py @@ -6,7 +6,7 @@ Create Date: 2025-11-22 23:51:40.694983 """ -from typing import Sequence, Union +from collections.abc import Sequence import sqlalchemy as sa @@ -14,9 +14,9 @@ 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 +down_revision: str | None = "72aa309d4007" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/alembic/versions_backup/fa7d4d10e358_add_rbac_enhancements.py b/alembic/versions_backup/fa7d4d10e358_add_rbac_enhancements.py index 6c0decce..4f61eaf5 100644 --- a/alembic/versions_backup/fa7d4d10e358_add_rbac_enhancements.py +++ b/alembic/versions_backup/fa7d4d10e358_add_rbac_enhancements.py @@ -7,7 +7,7 @@ Create Date: 2025-11-13 16:51:25.010057 SQLite-compatible version """ -from typing import Sequence, Union +from collections.abc import Sequence import sqlalchemy as sa @@ -15,9 +15,9 @@ 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 +down_revision: str | None = "4951b2e50581" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade(): @@ -80,10 +80,10 @@ def upgrade(): # SQLite-compatible UPDATE with subquery op.execute( """ - UPDATE vendor_users - SET user_type = 'owner' + UPDATE vendor_users + SET user_type = 'owner' WHERE (vendor_id, user_id) IN ( - SELECT id, owner_user_id + SELECT id, owner_user_id FROM vendors ) """ @@ -92,8 +92,8 @@ def upgrade(): # Set existing owners as active op.execute( """ - UPDATE vendor_users - SET is_active = TRUE + UPDATE vendor_users + SET is_active = TRUE WHERE user_type = 'owner' """ ) diff --git a/alembic/versions_backup/fcfdc02d5138_add_language_settings_to_vendor_user_.py b/alembic/versions_backup/fcfdc02d5138_add_language_settings_to_vendor_user_.py index 34d7fcfb..0db4e9b0 100644 --- a/alembic/versions_backup/fcfdc02d5138_add_language_settings_to_vendor_user_.py +++ b/alembic/versions_backup/fcfdc02d5138_add_language_settings_to_vendor_user_.py @@ -11,17 +11,17 @@ This migration adds language preference fields to support multi-language UI: Supported languages: en (English), fr (French), de (German), lb (Luxembourgish) """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "fcfdc02d5138" +down_revision: str | None = "b412e0b49c2e" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: @@ -30,25 +30,25 @@ def upgrade() -> None: # ======================================================================== # 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') + "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') + "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') + "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', + "vendors", sa.Column( - 'storefront_languages', + "storefront_languages", sa.JSON, nullable=False, server_default='["fr", "de", "en"]' @@ -60,8 +60,8 @@ def upgrade() -> None: # ======================================================================== # preferred_language: User's preferred UI language (NULL = use context default) op.add_column( - 'users', - sa.Column('preferred_language', sa.String(5), nullable=True) + "users", + sa.Column("preferred_language", sa.String(5), nullable=True) ) # ======================================================================== @@ -69,16 +69,16 @@ def upgrade() -> None: # ======================================================================== # preferred_language: Customer's preferred language (NULL = use storefront default) op.add_column( - 'customers', - sa.Column('preferred_language', sa.String(5), nullable=True) + "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') + 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_backup/fef1d20ce8b4_add_content_pages_table_for_cms.py b/alembic/versions_backup/fef1d20ce8b4_add_content_pages_table_for_cms.py index cc24f81a..16ba9f6f 100644 --- a/alembic/versions_backup/fef1d20ce8b4_add_content_pages_table_for_cms.py +++ b/alembic/versions_backup/fef1d20ce8b4_add_content_pages_table_for_cms.py @@ -6,17 +6,15 @@ Create Date: 2025-11-22 13:41:18.069674 """ -from typing import Sequence, Union - -import sqlalchemy as sa +from collections.abc import Sequence 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 +down_revision: str | None = "fa7d4d10e358" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/alembic/versions_backup/g5b6c7d8e9f0_add_scan_status_fields.py b/alembic/versions_backup/g5b6c7d8e9f0_add_scan_status_fields.py index 1558f49c..128bf12b 100644 --- a/alembic/versions_backup/g5b6c7d8e9f0_add_scan_status_fields.py +++ b/alembic/versions_backup/g5b6c7d8e9f0_add_scan_status_fields.py @@ -12,6 +12,7 @@ Create Date: 2024-12-21 from collections.abc import Sequence import sqlalchemy as sa + from alembic import op # revision identifiers, used by Alembic. diff --git a/alembic/versions_backup/h6c7d8e9f0a1_add_invoice_tables.py b/alembic/versions_backup/h6c7d8e9f0a1_add_invoice_tables.py index b88cabc2..14842707 100644 --- a/alembic/versions_backup/h6c7d8e9f0a1_add_invoice_tables.py +++ b/alembic/versions_backup/h6c7d8e9f0a1_add_invoice_tables.py @@ -9,16 +9,17 @@ This migration adds: - invoices: Invoice records with seller/buyer snapshots """ -from typing import Sequence, Union +from collections.abc import Sequence 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 +down_revision: str | None = "g5b6c7d8e9f0" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/alembic/versions_backup/i7d8e9f0a1b2_add_vendor_subscriptions.py b/alembic/versions_backup/i7d8e9f0a1b2_add_vendor_subscriptions.py index cff6e188..c936bc2e 100644 --- a/alembic/versions_backup/i7d8e9f0a1b2_add_vendor_subscriptions.py +++ b/alembic/versions_backup/i7d8e9f0a1b2_add_vendor_subscriptions.py @@ -8,16 +8,17 @@ This migration adds: - vendor_subscriptions: Per-vendor subscription tracking with tier limits """ -from typing import Sequence, Union +from collections.abc import Sequence 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 +down_revision: str | None = "h6c7d8e9f0a1" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/alembic/versions_backup/j8e9f0a1b2c3_product_independence_populate_fields.py b/alembic/versions_backup/j8e9f0a1b2c3_product_independence_populate_fields.py index e03e6244..70ccf112 100644 --- a/alembic/versions_backup/j8e9f0a1b2c3_product_independence_populate_fields.py +++ b/alembic/versions_backup/j8e9f0a1b2c3_product_independence_populate_fields.py @@ -15,16 +15,17 @@ After this migration: - The marketplace_product_id FK is kept for "view original source" feature """ -from typing import Sequence, Union +from collections.abc import Sequence + +from sqlalchemy import text 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 +down_revision: str | None = "i7d8e9f0a1b2" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: @@ -259,4 +260,3 @@ def downgrade() -> None: 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_backup/k9f0a1b2c3d4_add_tier_id_fk_to_subscriptions.py b/alembic/versions_backup/k9f0a1b2c3d4_add_tier_id_fk_to_subscriptions.py index b7d74ac5..a185eba2 100644 --- a/alembic/versions_backup/k9f0a1b2c3d4_add_tier_id_fk_to_subscriptions.py +++ b/alembic/versions_backup/k9f0a1b2c3d4_add_tier_id_fk_to_subscriptions.py @@ -8,9 +8,9 @@ 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 +from alembic import op # revision identifiers, used by Alembic. revision = "k9f0a1b2c3d4" diff --git a/alembic/versions_backup/l0a1b2c3d4e5_add_capacity_snapshots_table.py b/alembic/versions_backup/l0a1b2c3d4e5_add_capacity_snapshots_table.py index a87beb08..4a2e6f5b 100644 --- a/alembic/versions_backup/l0a1b2c3d4e5_add_capacity_snapshots_table.py +++ b/alembic/versions_backup/l0a1b2c3d4e5_add_capacity_snapshots_table.py @@ -7,9 +7,9 @@ Create Date: 2025-12-26 Adds table for tracking daily platform capacity metrics for growth forecasting. """ -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision = "l0a1b2c3d4e5" diff --git a/alembic/versions_backup/m1b2c3d4e5f6_add_vendor_onboarding_table.py b/alembic/versions_backup/m1b2c3d4e5f6_add_vendor_onboarding_table.py index 849da006..2f9b5f31 100644 --- a/alembic/versions_backup/m1b2c3d4e5f6_add_vendor_onboarding_table.py +++ b/alembic/versions_backup/m1b2c3d4e5f6_add_vendor_onboarding_table.py @@ -5,67 +5,67 @@ Revises: d7a4a3f06394 Create Date: 2025-12-27 22:00:00.000000 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "m1b2c3d4e5f6" +down_revision: str | None = "d7a4a3f06394" +branch_labels: str | Sequence[str] | None = None +depends_on: 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), + 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'), + 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), + 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')), + 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')), + 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), + 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), + 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), + 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), + 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'), + 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) + 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') + 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_backup/module_billing/billing_001_merchant_subscriptions_and_feature_limits.py b/alembic/versions_backup/module_billing/billing_001_merchant_subscriptions_and_feature_limits.py index 2f7612ee..533d07d5 100644 --- a/alembic/versions_backup/module_billing/billing_001_merchant_subscriptions_and_feature_limits.py +++ b/alembic/versions_backup/module_billing/billing_001_merchant_subscriptions_and_feature_limits.py @@ -17,9 +17,9 @@ Alters: Revision ID: billing_001 """ -from alembic import op import sqlalchemy as sa +from alembic import op # Revision identifiers revision = "billing_001" diff --git a/alembic/versions_backup/module_loyalty/loyalty_001_add_loyalty_module_tables.py b/alembic/versions_backup/module_loyalty/loyalty_001_add_loyalty_module_tables.py index 3b3a54f5..b66f8b8b 100644 --- a/alembic/versions_backup/module_loyalty/loyalty_001_add_loyalty_module_tables.py +++ b/alembic/versions_backup/module_loyalty/loyalty_001_add_loyalty_module_tables.py @@ -5,646 +5,646 @@ Revises: zd3n4o5p6q7r8 Create Date: 2026-01-28 22:55:34.074321 """ -from typing import Sequence, Union +from collections.abc import Sequence + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql, sqlite from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql -from sqlalchemy.dialects import sqlite # revision identifiers, used by Alembic. -revision: str = '0fb5d6d6ff97' -down_revision: Union[str, None] = 'zd3n4o5p6q7r8' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +revision: str = "0fb5d6d6ff97" +down_revision: str | None = "zd3n4o5p6q7r8" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('loyalty_programs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('store_id', sa.Integer(), nullable=False), - sa.Column('loyalty_type', sa.String(length=20), nullable=False), - sa.Column('stamps_target', sa.Integer(), nullable=False, comment='Number of stamps needed for reward'), - sa.Column('stamps_reward_description', sa.String(length=255), nullable=False, comment='Description of stamp reward'), - sa.Column('stamps_reward_value_cents', sa.Integer(), nullable=True, comment='Value of stamp reward in cents (for analytics)'), - sa.Column('points_per_euro', sa.Integer(), nullable=False, comment='Points earned per euro spent'), - sa.Column('points_rewards', sqlite.JSON(), nullable=False, comment='List of point rewards: [{id, name, points_required, description}]'), - sa.Column('cooldown_minutes', sa.Integer(), nullable=False, comment='Minutes between stamps for same card'), - sa.Column('max_daily_stamps', sa.Integer(), nullable=False, comment='Maximum stamps per card per day'), - sa.Column('require_staff_pin', sa.Boolean(), nullable=False, comment='Require staff PIN for stamp/points operations'), - sa.Column('card_name', sa.String(length=100), nullable=True, comment='Display name for loyalty card'), - sa.Column('card_color', sa.String(length=7), nullable=False, comment='Primary color for card (hex)'), - sa.Column('card_secondary_color', sa.String(length=7), nullable=True, comment='Secondary color for card (hex)'), - sa.Column('logo_url', sa.String(length=500), nullable=True, comment='URL to store logo for card'), - sa.Column('hero_image_url', sa.String(length=500), nullable=True, comment='URL to hero image for card'), - sa.Column('google_issuer_id', sa.String(length=100), nullable=True, comment='Google Wallet Issuer ID'), - sa.Column('google_class_id', sa.String(length=200), nullable=True, comment='Google Wallet Loyalty Class ID'), - sa.Column('apple_pass_type_id', sa.String(length=100), nullable=True, comment='Apple Wallet Pass Type ID'), - sa.Column('terms_text', sa.Text(), nullable=True, comment='Loyalty program terms and conditions'), - sa.Column('privacy_url', sa.String(length=500), nullable=True, comment='URL to privacy policy'), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.Column('activated_at', sa.DateTime(timezone=True), nullable=True, comment='When program was first activated'), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_table("loyalty_programs", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("store_id", sa.Integer(), nullable=False), + sa.Column("loyalty_type", sa.String(length=20), nullable=False), + sa.Column("stamps_target", sa.Integer(), nullable=False, comment="Number of stamps needed for reward"), + sa.Column("stamps_reward_description", sa.String(length=255), nullable=False, comment="Description of stamp reward"), + sa.Column("stamps_reward_value_cents", sa.Integer(), nullable=True, comment="Value of stamp reward in cents (for analytics)"), + sa.Column("points_per_euro", sa.Integer(), nullable=False, comment="Points earned per euro spent"), + sa.Column("points_rewards", sqlite.JSON(), nullable=False, comment="List of point rewards: [{id, name, points_required, description}]"), + sa.Column("cooldown_minutes", sa.Integer(), nullable=False, comment="Minutes between stamps for same card"), + sa.Column("max_daily_stamps", sa.Integer(), nullable=False, comment="Maximum stamps per card per day"), + sa.Column("require_staff_pin", sa.Boolean(), nullable=False, comment="Require staff PIN for stamp/points operations"), + sa.Column("card_name", sa.String(length=100), nullable=True, comment="Display name for loyalty card"), + sa.Column("card_color", sa.String(length=7), nullable=False, comment="Primary color for card (hex)"), + sa.Column("card_secondary_color", sa.String(length=7), nullable=True, comment="Secondary color for card (hex)"), + sa.Column("logo_url", sa.String(length=500), nullable=True, comment="URL to store logo for card"), + sa.Column("hero_image_url", sa.String(length=500), nullable=True, comment="URL to hero image for card"), + sa.Column("google_issuer_id", sa.String(length=100), nullable=True, comment="Google Wallet Issuer ID"), + sa.Column("google_class_id", sa.String(length=200), nullable=True, comment="Google Wallet Loyalty Class ID"), + sa.Column("apple_pass_type_id", sa.String(length=100), nullable=True, comment="Apple Wallet Pass Type ID"), + sa.Column("terms_text", sa.Text(), nullable=True, comment="Loyalty program terms and conditions"), + sa.Column("privacy_url", sa.String(length=500), nullable=True, comment="URL to privacy policy"), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.Column("activated_at", sa.DateTime(timezone=True), nullable=True, comment="When program was first activated"), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(["store_id"], ["stores.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id") ) - op.create_index('idx_loyalty_program_store_active', 'loyalty_programs', ['store_id', 'is_active'], unique=False) - op.create_index(op.f('ix_loyalty_programs_id'), 'loyalty_programs', ['id'], unique=False) - op.create_index(op.f('ix_loyalty_programs_is_active'), 'loyalty_programs', ['is_active'], unique=False) - op.create_index(op.f('ix_loyalty_programs_store_id'), 'loyalty_programs', ['store_id'], unique=True) - op.create_table('loyalty_cards', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('customer_id', sa.Integer(), nullable=False), - sa.Column('program_id', sa.Integer(), nullable=False), - sa.Column('store_id', sa.Integer(), nullable=False, comment='Denormalized for query performance'), - sa.Column('card_number', sa.String(length=20), nullable=False, comment='Human-readable card number'), - sa.Column('qr_code_data', sa.String(length=50), nullable=False, comment='Data encoded in QR code for scanning'), - sa.Column('stamp_count', sa.Integer(), nullable=False, comment='Current stamps toward next reward'), - sa.Column('total_stamps_earned', sa.Integer(), nullable=False, comment='Lifetime stamps earned'), - sa.Column('stamps_redeemed', sa.Integer(), nullable=False, comment='Total rewards redeemed (stamps reset on redemption)'), - sa.Column('points_balance', sa.Integer(), nullable=False, comment='Current available points'), - sa.Column('total_points_earned', sa.Integer(), nullable=False, comment='Lifetime points earned'), - sa.Column('points_redeemed', sa.Integer(), nullable=False, comment='Lifetime points redeemed'), - sa.Column('google_object_id', sa.String(length=200), nullable=True, comment='Google Wallet Loyalty Object ID'), - sa.Column('google_object_jwt', sa.String(length=2000), nullable=True, comment="JWT for Google Wallet 'Add to Wallet' button"), - sa.Column('apple_serial_number', sa.String(length=100), nullable=True, comment='Apple Wallet pass serial number'), - sa.Column('apple_auth_token', sa.String(length=100), nullable=True, comment='Apple Wallet authentication token for updates'), - sa.Column('last_stamp_at', sa.DateTime(timezone=True), nullable=True, comment='Last stamp added (for cooldown)'), - sa.Column('last_points_at', sa.DateTime(timezone=True), nullable=True, comment='Last points earned'), - sa.Column('last_redemption_at', sa.DateTime(timezone=True), nullable=True, comment='Last reward redemption'), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['program_id'], ['loyalty_programs.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_index("idx_loyalty_program_store_active", "loyalty_programs", ["store_id", "is_active"], unique=False) + op.create_index(op.f("ix_loyalty_programs_id"), "loyalty_programs", ["id"], unique=False) + op.create_index(op.f("ix_loyalty_programs_is_active"), "loyalty_programs", ["is_active"], unique=False) + op.create_index(op.f("ix_loyalty_programs_store_id"), "loyalty_programs", ["store_id"], unique=True) + op.create_table("loyalty_cards", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("customer_id", sa.Integer(), nullable=False), + sa.Column("program_id", sa.Integer(), nullable=False), + sa.Column("store_id", sa.Integer(), nullable=False, comment="Denormalized for query performance"), + sa.Column("card_number", sa.String(length=20), nullable=False, comment="Human-readable card number"), + sa.Column("qr_code_data", sa.String(length=50), nullable=False, comment="Data encoded in QR code for scanning"), + sa.Column("stamp_count", sa.Integer(), nullable=False, comment="Current stamps toward next reward"), + sa.Column("total_stamps_earned", sa.Integer(), nullable=False, comment="Lifetime stamps earned"), + sa.Column("stamps_redeemed", sa.Integer(), nullable=False, comment="Total rewards redeemed (stamps reset on redemption)"), + sa.Column("points_balance", sa.Integer(), nullable=False, comment="Current available points"), + sa.Column("total_points_earned", sa.Integer(), nullable=False, comment="Lifetime points earned"), + sa.Column("points_redeemed", sa.Integer(), nullable=False, comment="Lifetime points redeemed"), + sa.Column("google_object_id", sa.String(length=200), nullable=True, comment="Google Wallet Loyalty Object ID"), + sa.Column("google_object_jwt", sa.String(length=2000), nullable=True, comment="JWT for Google Wallet 'Add to Wallet' button"), + sa.Column("apple_serial_number", sa.String(length=100), nullable=True, comment="Apple Wallet pass serial number"), + sa.Column("apple_auth_token", sa.String(length=100), nullable=True, comment="Apple Wallet authentication token for updates"), + sa.Column("last_stamp_at", sa.DateTime(timezone=True), nullable=True, comment="Last stamp added (for cooldown)"), + sa.Column("last_points_at", sa.DateTime(timezone=True), nullable=True, comment="Last points earned"), + sa.Column("last_redemption_at", sa.DateTime(timezone=True), nullable=True, comment="Last reward redemption"), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(["customer_id"], ["customers.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["program_id"], ["loyalty_programs.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["store_id"], ["stores.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id") ) - op.create_index('idx_loyalty_card_customer_program', 'loyalty_cards', ['customer_id', 'program_id'], unique=True) - op.create_index('idx_loyalty_card_store_active', 'loyalty_cards', ['store_id', 'is_active'], unique=False) - op.create_index(op.f('ix_loyalty_cards_apple_serial_number'), 'loyalty_cards', ['apple_serial_number'], unique=True) - op.create_index(op.f('ix_loyalty_cards_card_number'), 'loyalty_cards', ['card_number'], unique=True) - op.create_index(op.f('ix_loyalty_cards_customer_id'), 'loyalty_cards', ['customer_id'], unique=False) - op.create_index(op.f('ix_loyalty_cards_google_object_id'), 'loyalty_cards', ['google_object_id'], unique=False) - op.create_index(op.f('ix_loyalty_cards_id'), 'loyalty_cards', ['id'], unique=False) - op.create_index(op.f('ix_loyalty_cards_is_active'), 'loyalty_cards', ['is_active'], unique=False) - op.create_index(op.f('ix_loyalty_cards_program_id'), 'loyalty_cards', ['program_id'], unique=False) - op.create_index(op.f('ix_loyalty_cards_qr_code_data'), 'loyalty_cards', ['qr_code_data'], unique=True) - op.create_index(op.f('ix_loyalty_cards_store_id'), 'loyalty_cards', ['store_id'], unique=False) - op.create_table('staff_pins', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('program_id', sa.Integer(), nullable=False), - sa.Column('store_id', sa.Integer(), nullable=False, comment='Denormalized for query performance'), - sa.Column('name', sa.String(length=100), nullable=False, comment='Staff member name'), - sa.Column('staff_id', sa.String(length=50), nullable=True, comment='Optional staff ID/employee number'), - sa.Column('pin_hash', sa.String(length=255), nullable=False, comment='bcrypt hash of PIN'), - sa.Column('failed_attempts', sa.Integer(), nullable=False, comment='Consecutive failed PIN attempts'), - sa.Column('locked_until', sa.DateTime(timezone=True), nullable=True, comment='Lockout expires at this time'), - sa.Column('last_used_at', sa.DateTime(timezone=True), nullable=True, comment='Last successful use of PIN'), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['program_id'], ['loyalty_programs.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_index("idx_loyalty_card_customer_program", "loyalty_cards", ["customer_id", "program_id"], unique=True) + op.create_index("idx_loyalty_card_store_active", "loyalty_cards", ["store_id", "is_active"], unique=False) + op.create_index(op.f("ix_loyalty_cards_apple_serial_number"), "loyalty_cards", ["apple_serial_number"], unique=True) + op.create_index(op.f("ix_loyalty_cards_card_number"), "loyalty_cards", ["card_number"], unique=True) + op.create_index(op.f("ix_loyalty_cards_customer_id"), "loyalty_cards", ["customer_id"], unique=False) + op.create_index(op.f("ix_loyalty_cards_google_object_id"), "loyalty_cards", ["google_object_id"], unique=False) + op.create_index(op.f("ix_loyalty_cards_id"), "loyalty_cards", ["id"], unique=False) + op.create_index(op.f("ix_loyalty_cards_is_active"), "loyalty_cards", ["is_active"], unique=False) + op.create_index(op.f("ix_loyalty_cards_program_id"), "loyalty_cards", ["program_id"], unique=False) + op.create_index(op.f("ix_loyalty_cards_qr_code_data"), "loyalty_cards", ["qr_code_data"], unique=True) + op.create_index(op.f("ix_loyalty_cards_store_id"), "loyalty_cards", ["store_id"], unique=False) + op.create_table("staff_pins", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("program_id", sa.Integer(), nullable=False), + sa.Column("store_id", sa.Integer(), nullable=False, comment="Denormalized for query performance"), + sa.Column("name", sa.String(length=100), nullable=False, comment="Staff member name"), + sa.Column("staff_id", sa.String(length=50), nullable=True, comment="Optional staff ID/employee number"), + sa.Column("pin_hash", sa.String(length=255), nullable=False, comment="bcrypt hash of PIN"), + sa.Column("failed_attempts", sa.Integer(), nullable=False, comment="Consecutive failed PIN attempts"), + sa.Column("locked_until", sa.DateTime(timezone=True), nullable=True, comment="Lockout expires at this time"), + sa.Column("last_used_at", sa.DateTime(timezone=True), nullable=True, comment="Last successful use of PIN"), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(["program_id"], ["loyalty_programs.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["store_id"], ["stores.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id") ) - op.create_index('idx_staff_pin_program_active', 'staff_pins', ['program_id', 'is_active'], unique=False) - op.create_index('idx_staff_pin_store_active', 'staff_pins', ['store_id', 'is_active'], unique=False) - op.create_index(op.f('ix_staff_pins_id'), 'staff_pins', ['id'], unique=False) - op.create_index(op.f('ix_staff_pins_is_active'), 'staff_pins', ['is_active'], unique=False) - op.create_index(op.f('ix_staff_pins_program_id'), 'staff_pins', ['program_id'], unique=False) - op.create_index(op.f('ix_staff_pins_staff_id'), 'staff_pins', ['staff_id'], unique=False) - op.create_index(op.f('ix_staff_pins_store_id'), 'staff_pins', ['store_id'], unique=False) - op.create_table('apple_device_registrations', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('card_id', sa.Integer(), nullable=False), - sa.Column('device_library_identifier', sa.String(length=100), nullable=False, comment='Unique identifier for the device/library'), - sa.Column('push_token', sa.String(length=100), nullable=False, comment='APNs push token for this device'), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['card_id'], ['loyalty_cards.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_index("idx_staff_pin_program_active", "staff_pins", ["program_id", "is_active"], unique=False) + op.create_index("idx_staff_pin_store_active", "staff_pins", ["store_id", "is_active"], unique=False) + op.create_index(op.f("ix_staff_pins_id"), "staff_pins", ["id"], unique=False) + op.create_index(op.f("ix_staff_pins_is_active"), "staff_pins", ["is_active"], unique=False) + op.create_index(op.f("ix_staff_pins_program_id"), "staff_pins", ["program_id"], unique=False) + op.create_index(op.f("ix_staff_pins_staff_id"), "staff_pins", ["staff_id"], unique=False) + op.create_index(op.f("ix_staff_pins_store_id"), "staff_pins", ["store_id"], unique=False) + op.create_table("apple_device_registrations", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("card_id", sa.Integer(), nullable=False), + sa.Column("device_library_identifier", sa.String(length=100), nullable=False, comment="Unique identifier for the device/library"), + sa.Column("push_token", sa.String(length=100), nullable=False, comment="APNs push token for this device"), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(["card_id"], ["loyalty_cards.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id") ) - op.create_index('idx_apple_device_card', 'apple_device_registrations', ['device_library_identifier', 'card_id'], unique=True) - op.create_index(op.f('ix_apple_device_registrations_card_id'), 'apple_device_registrations', ['card_id'], unique=False) - op.create_index(op.f('ix_apple_device_registrations_device_library_identifier'), 'apple_device_registrations', ['device_library_identifier'], unique=False) - op.create_index(op.f('ix_apple_device_registrations_id'), 'apple_device_registrations', ['id'], unique=False) - op.create_table('loyalty_transactions', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('card_id', sa.Integer(), nullable=False), - sa.Column('store_id', sa.Integer(), nullable=False, comment='Denormalized for query performance'), - sa.Column('staff_pin_id', sa.Integer(), nullable=True, comment='Staff PIN used for this operation'), - sa.Column('transaction_type', sa.String(length=30), nullable=False), - sa.Column('stamps_delta', sa.Integer(), nullable=False, comment='Change in stamps (+1 for earn, -N for redeem)'), - sa.Column('points_delta', sa.Integer(), nullable=False, comment='Change in points (+N for earn, -N for redeem)'), - sa.Column('stamps_balance_after', sa.Integer(), nullable=True, comment='Stamp count after this transaction'), - sa.Column('points_balance_after', sa.Integer(), nullable=True, comment='Points balance after this transaction'), - sa.Column('purchase_amount_cents', sa.Integer(), nullable=True, comment='Purchase amount in cents (for points calculation)'), - sa.Column('order_reference', sa.String(length=100), nullable=True, comment='Reference to order that triggered points'), - sa.Column('reward_id', sa.String(length=50), nullable=True, comment='ID of redeemed reward (from program.points_rewards)'), - sa.Column('reward_description', sa.String(length=255), nullable=True, comment='Description of redeemed reward'), - sa.Column('ip_address', sa.String(length=45), nullable=True, comment='IP address of requester (IPv4 or IPv6)'), - sa.Column('user_agent', sa.String(length=500), nullable=True, comment='User agent string'), - sa.Column('notes', sa.Text(), nullable=True, comment='Additional notes (e.g., reason for adjustment)'), - sa.Column('transaction_at', sa.DateTime(timezone=True), nullable=False, comment='When the transaction occurred (may differ from created_at)'), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['card_id'], ['loyalty_cards.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['staff_pin_id'], ['staff_pins.id'], ondelete='SET NULL'), - sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_index("idx_apple_device_card", "apple_device_registrations", ["device_library_identifier", "card_id"], unique=True) + op.create_index(op.f("ix_apple_device_registrations_card_id"), "apple_device_registrations", ["card_id"], unique=False) + op.create_index(op.f("ix_apple_device_registrations_device_library_identifier"), "apple_device_registrations", ["device_library_identifier"], unique=False) + op.create_index(op.f("ix_apple_device_registrations_id"), "apple_device_registrations", ["id"], unique=False) + op.create_table("loyalty_transactions", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("card_id", sa.Integer(), nullable=False), + sa.Column("store_id", sa.Integer(), nullable=False, comment="Denormalized for query performance"), + sa.Column("staff_pin_id", sa.Integer(), nullable=True, comment="Staff PIN used for this operation"), + sa.Column("transaction_type", sa.String(length=30), nullable=False), + sa.Column("stamps_delta", sa.Integer(), nullable=False, comment="Change in stamps (+1 for earn, -N for redeem)"), + sa.Column("points_delta", sa.Integer(), nullable=False, comment="Change in points (+N for earn, -N for redeem)"), + sa.Column("stamps_balance_after", sa.Integer(), nullable=True, comment="Stamp count after this transaction"), + sa.Column("points_balance_after", sa.Integer(), nullable=True, comment="Points balance after this transaction"), + sa.Column("purchase_amount_cents", sa.Integer(), nullable=True, comment="Purchase amount in cents (for points calculation)"), + sa.Column("order_reference", sa.String(length=100), nullable=True, comment="Reference to order that triggered points"), + sa.Column("reward_id", sa.String(length=50), nullable=True, comment="ID of redeemed reward (from program.points_rewards)"), + sa.Column("reward_description", sa.String(length=255), nullable=True, comment="Description of redeemed reward"), + sa.Column("ip_address", sa.String(length=45), nullable=True, comment="IP address of requester (IPv4 or IPv6)"), + sa.Column("user_agent", sa.String(length=500), nullable=True, comment="User agent string"), + sa.Column("notes", sa.Text(), nullable=True, comment="Additional notes (e.g., reason for adjustment)"), + sa.Column("transaction_at", sa.DateTime(timezone=True), nullable=False, comment="When the transaction occurred (may differ from created_at)"), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(["card_id"], ["loyalty_cards.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["staff_pin_id"], ["staff_pins.id"], ondelete="SET NULL"), + sa.ForeignKeyConstraint(["store_id"], ["stores.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id") ) - op.create_index('idx_loyalty_tx_card_type', 'loyalty_transactions', ['card_id', 'transaction_type'], unique=False) - op.create_index('idx_loyalty_tx_type_date', 'loyalty_transactions', ['transaction_type', 'transaction_at'], unique=False) - op.create_index('idx_loyalty_tx_store_date', 'loyalty_transactions', ['store_id', 'transaction_at'], unique=False) - op.create_index(op.f('ix_loyalty_transactions_card_id'), 'loyalty_transactions', ['card_id'], unique=False) - op.create_index(op.f('ix_loyalty_transactions_id'), 'loyalty_transactions', ['id'], unique=False) - op.create_index(op.f('ix_loyalty_transactions_order_reference'), 'loyalty_transactions', ['order_reference'], unique=False) - op.create_index(op.f('ix_loyalty_transactions_staff_pin_id'), 'loyalty_transactions', ['staff_pin_id'], unique=False) - op.create_index(op.f('ix_loyalty_transactions_transaction_at'), 'loyalty_transactions', ['transaction_at'], unique=False) - op.create_index(op.f('ix_loyalty_transactions_transaction_type'), 'loyalty_transactions', ['transaction_type'], unique=False) - op.create_index(op.f('ix_loyalty_transactions_store_id'), 'loyalty_transactions', ['store_id'], unique=False) - op.alter_column('admin_menu_configs', 'platform_id', + op.create_index("idx_loyalty_tx_card_type", "loyalty_transactions", ["card_id", "transaction_type"], unique=False) + op.create_index("idx_loyalty_tx_type_date", "loyalty_transactions", ["transaction_type", "transaction_at"], unique=False) + op.create_index("idx_loyalty_tx_store_date", "loyalty_transactions", ["store_id", "transaction_at"], unique=False) + op.create_index(op.f("ix_loyalty_transactions_card_id"), "loyalty_transactions", ["card_id"], unique=False) + op.create_index(op.f("ix_loyalty_transactions_id"), "loyalty_transactions", ["id"], unique=False) + op.create_index(op.f("ix_loyalty_transactions_order_reference"), "loyalty_transactions", ["order_reference"], unique=False) + op.create_index(op.f("ix_loyalty_transactions_staff_pin_id"), "loyalty_transactions", ["staff_pin_id"], unique=False) + op.create_index(op.f("ix_loyalty_transactions_transaction_at"), "loyalty_transactions", ["transaction_at"], unique=False) + op.create_index(op.f("ix_loyalty_transactions_transaction_type"), "loyalty_transactions", ["transaction_type"], unique=False) + op.create_index(op.f("ix_loyalty_transactions_store_id"), "loyalty_transactions", ["store_id"], unique=False) + op.alter_column("admin_menu_configs", "platform_id", existing_type=sa.INTEGER(), - comment='Platform scope - applies to users/stores of this platform', - existing_comment='Platform scope - applies to all platform admins of this platform', + comment="Platform scope - applies to users/stores of this platform", + existing_comment="Platform scope - applies to all platform admins of this platform", existing_nullable=True) - op.alter_column('admin_menu_configs', 'user_id', + op.alter_column("admin_menu_configs", "user_id", existing_type=sa.INTEGER(), - comment='User scope - applies to this specific super admin (admin frontend only)', - existing_comment='User scope - applies to this specific super admin', + comment="User scope - applies to this specific super admin (admin frontend only)", + existing_comment="User scope - applies to this specific super admin", existing_nullable=True) - op.alter_column('admin_menu_configs', 'created_at', + op.alter_column("admin_menu_configs", "created_at", existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('admin_menu_configs', 'updated_at', + existing_server_default=sa.text("now()")) + op.alter_column("admin_menu_configs", "updated_at", existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.drop_index('idx_admin_menu_configs_frontend_type', table_name='admin_menu_configs') - op.drop_index('idx_admin_menu_configs_menu_item_id', table_name='admin_menu_configs') - op.drop_index('idx_admin_menu_configs_platform_id', table_name='admin_menu_configs') - op.drop_index('idx_admin_menu_configs_user_id', table_name='admin_menu_configs') - op.create_index(op.f('ix_admin_menu_configs_frontend_type'), 'admin_menu_configs', ['frontend_type'], unique=False) - op.create_index(op.f('ix_admin_menu_configs_id'), 'admin_menu_configs', ['id'], unique=False) - op.create_index(op.f('ix_admin_menu_configs_menu_item_id'), 'admin_menu_configs', ['menu_item_id'], unique=False) - op.create_index(op.f('ix_admin_menu_configs_platform_id'), 'admin_menu_configs', ['platform_id'], unique=False) - op.create_index(op.f('ix_admin_menu_configs_user_id'), 'admin_menu_configs', ['user_id'], unique=False) - op.alter_column('admin_platforms', 'created_at', + existing_server_default=sa.text("now()")) + op.drop_index("idx_admin_menu_configs_frontend_type", table_name="admin_menu_configs") + op.drop_index("idx_admin_menu_configs_menu_item_id", table_name="admin_menu_configs") + op.drop_index("idx_admin_menu_configs_platform_id", table_name="admin_menu_configs") + op.drop_index("idx_admin_menu_configs_user_id", table_name="admin_menu_configs") + op.create_index(op.f("ix_admin_menu_configs_frontend_type"), "admin_menu_configs", ["frontend_type"], unique=False) + op.create_index(op.f("ix_admin_menu_configs_id"), "admin_menu_configs", ["id"], unique=False) + op.create_index(op.f("ix_admin_menu_configs_menu_item_id"), "admin_menu_configs", ["menu_item_id"], unique=False) + op.create_index(op.f("ix_admin_menu_configs_platform_id"), "admin_menu_configs", ["platform_id"], unique=False) + op.create_index(op.f("ix_admin_menu_configs_user_id"), "admin_menu_configs", ["user_id"], unique=False) + op.alter_column("admin_platforms", "created_at", existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('admin_platforms', 'updated_at', + existing_server_default=sa.text("now()")) + op.alter_column("admin_platforms", "updated_at", existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.drop_index('idx_admin_platforms_platform_id', table_name='admin_platforms') - op.drop_index('idx_admin_platforms_user_id', table_name='admin_platforms') - op.create_index(op.f('ix_admin_platforms_id'), 'admin_platforms', ['id'], unique=False) - op.create_index(op.f('ix_admin_platforms_platform_id'), 'admin_platforms', ['platform_id'], unique=False) - op.create_index(op.f('ix_admin_platforms_user_id'), 'admin_platforms', ['user_id'], unique=False) - op.alter_column('content_pages', 'platform_id', + existing_server_default=sa.text("now()")) + op.drop_index("idx_admin_platforms_platform_id", table_name="admin_platforms") + op.drop_index("idx_admin_platforms_user_id", table_name="admin_platforms") + op.create_index(op.f("ix_admin_platforms_id"), "admin_platforms", ["id"], unique=False) + op.create_index(op.f("ix_admin_platforms_platform_id"), "admin_platforms", ["platform_id"], unique=False) + op.create_index(op.f("ix_admin_platforms_user_id"), "admin_platforms", ["user_id"], unique=False) + op.alter_column("content_pages", "platform_id", existing_type=sa.INTEGER(), - comment='Platform this page belongs to', + comment="Platform this page belongs to", existing_nullable=False) - op.alter_column('content_pages', 'store_id', + op.alter_column("content_pages", "store_id", existing_type=sa.INTEGER(), - comment='Store this page belongs to (NULL for platform/default pages)', + comment="Store this page belongs to (NULL for platform/default pages)", existing_nullable=True) - op.alter_column('content_pages', 'is_platform_page', + op.alter_column("content_pages", "is_platform_page", existing_type=sa.BOOLEAN(), - comment='True = platform marketing page (homepage, pricing); False = store default or override', + comment="True = platform marketing page (homepage, pricing); False = store default or override", existing_nullable=False, - existing_server_default=sa.text('false')) - op.alter_column('platform_modules', 'created_at', + existing_server_default=sa.text("false")) + op.alter_column("platform_modules", "created_at", existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('platform_modules', 'updated_at', + existing_server_default=sa.text("now()")) + op.alter_column("platform_modules", "updated_at", existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.create_index(op.f('ix_platform_modules_id'), 'platform_modules', ['id'], unique=False) - op.alter_column('platforms', 'code', + existing_server_default=sa.text("now()")) + op.create_index(op.f("ix_platform_modules_id"), "platform_modules", ["id"], unique=False) + op.alter_column("platforms", "code", existing_type=sa.VARCHAR(length=50), comment="Unique platform identifier (e.g., 'oms', 'loyalty', 'sites')", existing_nullable=False) - op.alter_column('platforms', 'name', + op.alter_column("platforms", "name", existing_type=sa.VARCHAR(length=100), comment="Display name (e.g., 'Wizamart OMS')", existing_nullable=False) - op.alter_column('platforms', 'description', + op.alter_column("platforms", "description", existing_type=sa.TEXT(), - comment='Platform description for admin/marketing purposes', + comment="Platform description for admin/marketing purposes", existing_nullable=True) - op.alter_column('platforms', 'domain', + op.alter_column("platforms", "domain", existing_type=sa.VARCHAR(length=255), comment="Production domain (e.g., 'oms.lu', 'loyalty.lu')", existing_nullable=True) - op.alter_column('platforms', 'path_prefix', + op.alter_column("platforms", "path_prefix", existing_type=sa.VARCHAR(length=50), comment="Development path prefix (e.g., 'oms' for localhost:9999/oms/*)", existing_nullable=True) - op.alter_column('platforms', 'logo', + op.alter_column("platforms", "logo", existing_type=sa.VARCHAR(length=500), - comment='Logo URL for light mode', + comment="Logo URL for light mode", existing_nullable=True) - op.alter_column('platforms', 'logo_dark', + op.alter_column("platforms", "logo_dark", existing_type=sa.VARCHAR(length=500), - comment='Logo URL for dark mode', + comment="Logo URL for dark mode", existing_nullable=True) - op.alter_column('platforms', 'favicon', + op.alter_column("platforms", "favicon", existing_type=sa.VARCHAR(length=500), - comment='Favicon URL', + comment="Favicon URL", existing_nullable=True) - op.alter_column('platforms', 'theme_config', + op.alter_column("platforms", "theme_config", existing_type=postgresql.JSON(astext_type=sa.Text()), - comment='Theme configuration (colors, fonts, etc.)', + comment="Theme configuration (colors, fonts, etc.)", existing_nullable=True) - op.alter_column('platforms', 'default_language', + op.alter_column("platforms", "default_language", existing_type=sa.VARCHAR(length=5), comment="Default language code (e.g., 'fr', 'en', 'de')", existing_nullable=False, existing_server_default=sa.text("'fr'::character varying")) - op.alter_column('platforms', 'supported_languages', + op.alter_column("platforms", "supported_languages", existing_type=postgresql.JSON(astext_type=sa.Text()), - comment='List of supported language codes', + comment="List of supported language codes", existing_nullable=False) - op.alter_column('platforms', 'is_active', + op.alter_column("platforms", "is_active", existing_type=sa.BOOLEAN(), - comment='Whether the platform is active and accessible', + comment="Whether the platform is active and accessible", existing_nullable=False, - existing_server_default=sa.text('true')) - op.alter_column('platforms', 'is_public', + existing_server_default=sa.text("true")) + op.alter_column("platforms", "is_public", existing_type=sa.BOOLEAN(), - comment='Whether the platform is visible in public listings', + comment="Whether the platform is visible in public listings", existing_nullable=False, - existing_server_default=sa.text('true')) - op.alter_column('platforms', 'settings', + existing_server_default=sa.text("true")) + op.alter_column("platforms", "settings", existing_type=postgresql.JSON(astext_type=sa.Text()), - comment='Platform-specific settings and feature flags', + comment="Platform-specific settings and feature flags", existing_nullable=True) - op.alter_column('platforms', 'created_at', + op.alter_column("platforms", "created_at", existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('platforms', 'updated_at', + existing_server_default=sa.text("now()")) + op.alter_column("platforms", "updated_at", existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.create_index(op.f('ix_platforms_id'), 'platforms', ['id'], unique=False) - op.alter_column('subscription_tiers', 'platform_id', + existing_server_default=sa.text("now()")) + op.create_index(op.f("ix_platforms_id"), "platforms", ["id"], unique=False) + op.alter_column("subscription_tiers", "platform_id", existing_type=sa.INTEGER(), - comment='Platform this tier belongs to (NULL = global tier)', + comment="Platform this tier belongs to (NULL = global tier)", existing_nullable=True) - op.alter_column('subscription_tiers', 'cms_pages_limit', + op.alter_column("subscription_tiers", "cms_pages_limit", existing_type=sa.INTEGER(), - comment='Total CMS pages limit (NULL = unlimited)', + comment="Total CMS pages limit (NULL = unlimited)", existing_nullable=True) - op.alter_column('subscription_tiers', 'cms_custom_pages_limit', + op.alter_column("subscription_tiers", "cms_custom_pages_limit", existing_type=sa.INTEGER(), - comment='Custom pages limit, excluding overrides (NULL = unlimited)', + comment="Custom pages limit, excluding overrides (NULL = unlimited)", existing_nullable=True) - op.drop_index('ix_subscription_tiers_code', table_name='subscription_tiers') - op.create_index(op.f('ix_subscription_tiers_code'), 'subscription_tiers', ['code'], unique=False) - op.alter_column('users', 'is_super_admin', + op.drop_index("ix_subscription_tiers_code", table_name="subscription_tiers") + op.create_index(op.f("ix_subscription_tiers_code"), "subscription_tiers", ["code"], unique=False) + op.alter_column("users", "is_super_admin", existing_type=sa.BOOLEAN(), comment=None, - existing_comment='Whether this admin has access to all platforms (super admin)', + existing_comment="Whether this admin has access to all platforms (super admin)", existing_nullable=False, - existing_server_default=sa.text('false')) - op.alter_column('store_platforms', 'store_id', + existing_server_default=sa.text("false")) + op.alter_column("store_platforms", "store_id", existing_type=sa.INTEGER(), - comment='Reference to the store', + comment="Reference to the store", existing_nullable=False) - op.alter_column('store_platforms', 'platform_id', + op.alter_column("store_platforms", "platform_id", existing_type=sa.INTEGER(), - comment='Reference to the platform', + comment="Reference to the platform", existing_nullable=False) - op.alter_column('store_platforms', 'tier_id', + op.alter_column("store_platforms", "tier_id", existing_type=sa.INTEGER(), - comment='Platform-specific subscription tier', + comment="Platform-specific subscription tier", existing_nullable=True) - op.alter_column('store_platforms', 'is_active', + op.alter_column("store_platforms", "is_active", existing_type=sa.BOOLEAN(), - comment='Whether the store is active on this platform', + comment="Whether the store is active on this platform", existing_nullable=False, - existing_server_default=sa.text('true')) - op.alter_column('store_platforms', 'is_primary', + existing_server_default=sa.text("true")) + op.alter_column("store_platforms", "is_primary", existing_type=sa.BOOLEAN(), comment="Whether this is the store's primary platform", existing_nullable=False, - existing_server_default=sa.text('false')) - op.alter_column('store_platforms', 'custom_subdomain', + existing_server_default=sa.text("false")) + op.alter_column("store_platforms", "custom_subdomain", existing_type=sa.VARCHAR(length=100), - comment='Platform-specific subdomain (if different from main subdomain)', + comment="Platform-specific subdomain (if different from main subdomain)", existing_nullable=True) - op.alter_column('store_platforms', 'settings', + op.alter_column("store_platforms", "settings", existing_type=postgresql.JSON(astext_type=sa.Text()), - comment='Platform-specific store settings', + comment="Platform-specific store settings", existing_nullable=True) - op.alter_column('store_platforms', 'joined_at', + op.alter_column("store_platforms", "joined_at", existing_type=postgresql.TIMESTAMP(timezone=True), - comment='When the store joined this platform', + comment="When the store joined this platform", existing_nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('store_platforms', 'created_at', + existing_server_default=sa.text("now()")) + op.alter_column("store_platforms", "created_at", existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('store_platforms', 'updated_at', + existing_server_default=sa.text("now()")) + op.alter_column("store_platforms", "updated_at", existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.create_index(op.f('ix_store_platforms_id'), 'store_platforms', ['id'], unique=False) + existing_server_default=sa.text("now()")) + op.create_index(op.f("ix_store_platforms_id"), "store_platforms", ["id"], unique=False) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_store_platforms_id'), table_name='store_platforms') - op.alter_column('store_platforms', 'updated_at', + op.drop_index(op.f("ix_store_platforms_id"), table_name="store_platforms") + op.alter_column("store_platforms", "updated_at", existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('store_platforms', 'created_at', + existing_server_default=sa.text("now()")) + op.alter_column("store_platforms", "created_at", existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('store_platforms', 'joined_at', + existing_server_default=sa.text("now()")) + op.alter_column("store_platforms", "joined_at", existing_type=postgresql.TIMESTAMP(timezone=True), comment=None, - existing_comment='When the store joined this platform', + existing_comment="When the store joined this platform", existing_nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('store_platforms', 'settings', + existing_server_default=sa.text("now()")) + op.alter_column("store_platforms", "settings", existing_type=postgresql.JSON(astext_type=sa.Text()), comment=None, - existing_comment='Platform-specific store settings', + existing_comment="Platform-specific store settings", existing_nullable=True) - op.alter_column('store_platforms', 'custom_subdomain', + op.alter_column("store_platforms", "custom_subdomain", existing_type=sa.VARCHAR(length=100), comment=None, - existing_comment='Platform-specific subdomain (if different from main subdomain)', + existing_comment="Platform-specific subdomain (if different from main subdomain)", existing_nullable=True) - op.alter_column('store_platforms', 'is_primary', + op.alter_column("store_platforms", "is_primary", existing_type=sa.BOOLEAN(), comment=None, existing_comment="Whether this is the store's primary platform", existing_nullable=False, - existing_server_default=sa.text('false')) - op.alter_column('store_platforms', 'is_active', + existing_server_default=sa.text("false")) + op.alter_column("store_platforms", "is_active", existing_type=sa.BOOLEAN(), comment=None, - existing_comment='Whether the store is active on this platform', + existing_comment="Whether the store is active on this platform", existing_nullable=False, - existing_server_default=sa.text('true')) - op.alter_column('store_platforms', 'tier_id', + existing_server_default=sa.text("true")) + op.alter_column("store_platforms", "tier_id", existing_type=sa.INTEGER(), comment=None, - existing_comment='Platform-specific subscription tier', + existing_comment="Platform-specific subscription tier", existing_nullable=True) - op.alter_column('store_platforms', 'platform_id', + op.alter_column("store_platforms", "platform_id", existing_type=sa.INTEGER(), comment=None, - existing_comment='Reference to the platform', + existing_comment="Reference to the platform", existing_nullable=False) - op.alter_column('store_platforms', 'store_id', + op.alter_column("store_platforms", "store_id", existing_type=sa.INTEGER(), comment=None, - existing_comment='Reference to the store', + existing_comment="Reference to the store", existing_nullable=False) - op.alter_column('users', 'is_super_admin', + op.alter_column("users", "is_super_admin", existing_type=sa.BOOLEAN(), - comment='Whether this admin has access to all platforms (super admin)', + comment="Whether this admin has access to all platforms (super admin)", existing_nullable=False, - existing_server_default=sa.text('false')) - op.drop_index(op.f('ix_subscription_tiers_code'), table_name='subscription_tiers') - op.create_index('ix_subscription_tiers_code', 'subscription_tiers', ['code'], unique=True) - op.alter_column('subscription_tiers', 'cms_custom_pages_limit', + existing_server_default=sa.text("false")) + op.drop_index(op.f("ix_subscription_tiers_code"), table_name="subscription_tiers") + op.create_index("ix_subscription_tiers_code", "subscription_tiers", ["code"], unique=True) + op.alter_column("subscription_tiers", "cms_custom_pages_limit", existing_type=sa.INTEGER(), comment=None, - existing_comment='Custom pages limit, excluding overrides (NULL = unlimited)', + existing_comment="Custom pages limit, excluding overrides (NULL = unlimited)", existing_nullable=True) - op.alter_column('subscription_tiers', 'cms_pages_limit', + op.alter_column("subscription_tiers", "cms_pages_limit", existing_type=sa.INTEGER(), comment=None, - existing_comment='Total CMS pages limit (NULL = unlimited)', + existing_comment="Total CMS pages limit (NULL = unlimited)", existing_nullable=True) - op.alter_column('subscription_tiers', 'platform_id', + op.alter_column("subscription_tiers", "platform_id", existing_type=sa.INTEGER(), comment=None, - existing_comment='Platform this tier belongs to (NULL = global tier)', + existing_comment="Platform this tier belongs to (NULL = global tier)", existing_nullable=True) - op.drop_index(op.f('ix_platforms_id'), table_name='platforms') - op.alter_column('platforms', 'updated_at', + op.drop_index(op.f("ix_platforms_id"), table_name="platforms") + op.alter_column("platforms", "updated_at", existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('platforms', 'created_at', + existing_server_default=sa.text("now()")) + op.alter_column("platforms", "created_at", existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('platforms', 'settings', + existing_server_default=sa.text("now()")) + op.alter_column("platforms", "settings", existing_type=postgresql.JSON(astext_type=sa.Text()), comment=None, - existing_comment='Platform-specific settings and feature flags', + existing_comment="Platform-specific settings and feature flags", existing_nullable=True) - op.alter_column('platforms', 'is_public', + op.alter_column("platforms", "is_public", existing_type=sa.BOOLEAN(), comment=None, - existing_comment='Whether the platform is visible in public listings', + existing_comment="Whether the platform is visible in public listings", existing_nullable=False, - existing_server_default=sa.text('true')) - op.alter_column('platforms', 'is_active', + existing_server_default=sa.text("true")) + op.alter_column("platforms", "is_active", existing_type=sa.BOOLEAN(), comment=None, - existing_comment='Whether the platform is active and accessible', + existing_comment="Whether the platform is active and accessible", existing_nullable=False, - existing_server_default=sa.text('true')) - op.alter_column('platforms', 'supported_languages', + existing_server_default=sa.text("true")) + op.alter_column("platforms", "supported_languages", existing_type=postgresql.JSON(astext_type=sa.Text()), comment=None, - existing_comment='List of supported language codes', + existing_comment="List of supported language codes", existing_nullable=False) - op.alter_column('platforms', 'default_language', + op.alter_column("platforms", "default_language", existing_type=sa.VARCHAR(length=5), comment=None, existing_comment="Default language code (e.g., 'fr', 'en', 'de')", existing_nullable=False, existing_server_default=sa.text("'fr'::character varying")) - op.alter_column('platforms', 'theme_config', + op.alter_column("platforms", "theme_config", existing_type=postgresql.JSON(astext_type=sa.Text()), comment=None, - existing_comment='Theme configuration (colors, fonts, etc.)', + existing_comment="Theme configuration (colors, fonts, etc.)", existing_nullable=True) - op.alter_column('platforms', 'favicon', + op.alter_column("platforms", "favicon", existing_type=sa.VARCHAR(length=500), comment=None, - existing_comment='Favicon URL', + existing_comment="Favicon URL", existing_nullable=True) - op.alter_column('platforms', 'logo_dark', + op.alter_column("platforms", "logo_dark", existing_type=sa.VARCHAR(length=500), comment=None, - existing_comment='Logo URL for dark mode', + existing_comment="Logo URL for dark mode", existing_nullable=True) - op.alter_column('platforms', 'logo', + op.alter_column("platforms", "logo", existing_type=sa.VARCHAR(length=500), comment=None, - existing_comment='Logo URL for light mode', + existing_comment="Logo URL for light mode", existing_nullable=True) - op.alter_column('platforms', 'path_prefix', + op.alter_column("platforms", "path_prefix", existing_type=sa.VARCHAR(length=50), comment=None, existing_comment="Development path prefix (e.g., 'oms' for localhost:9999/oms/*)", existing_nullable=True) - op.alter_column('platforms', 'domain', + op.alter_column("platforms", "domain", existing_type=sa.VARCHAR(length=255), comment=None, existing_comment="Production domain (e.g., 'oms.lu', 'loyalty.lu')", existing_nullable=True) - op.alter_column('platforms', 'description', + op.alter_column("platforms", "description", existing_type=sa.TEXT(), comment=None, - existing_comment='Platform description for admin/marketing purposes', + existing_comment="Platform description for admin/marketing purposes", existing_nullable=True) - op.alter_column('platforms', 'name', + op.alter_column("platforms", "name", existing_type=sa.VARCHAR(length=100), comment=None, existing_comment="Display name (e.g., 'Wizamart OMS')", existing_nullable=False) - op.alter_column('platforms', 'code', + op.alter_column("platforms", "code", existing_type=sa.VARCHAR(length=50), comment=None, existing_comment="Unique platform identifier (e.g., 'oms', 'loyalty', 'sites')", existing_nullable=False) - op.drop_index(op.f('ix_platform_modules_id'), table_name='platform_modules') - op.alter_column('platform_modules', 'updated_at', + op.drop_index(op.f("ix_platform_modules_id"), table_name="platform_modules") + op.alter_column("platform_modules", "updated_at", existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('platform_modules', 'created_at', + existing_server_default=sa.text("now()")) + op.alter_column("platform_modules", "created_at", existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('content_pages', 'is_platform_page', + existing_server_default=sa.text("now()")) + op.alter_column("content_pages", "is_platform_page", existing_type=sa.BOOLEAN(), comment=None, - existing_comment='True = platform marketing page (homepage, pricing); False = store default or override', + existing_comment="True = platform marketing page (homepage, pricing); False = store default or override", existing_nullable=False, - existing_server_default=sa.text('false')) - op.alter_column('content_pages', 'store_id', + existing_server_default=sa.text("false")) + op.alter_column("content_pages", "store_id", existing_type=sa.INTEGER(), comment=None, - existing_comment='Store this page belongs to (NULL for platform/default pages)', + existing_comment="Store this page belongs to (NULL for platform/default pages)", existing_nullable=True) - op.alter_column('content_pages', 'platform_id', + op.alter_column("content_pages", "platform_id", existing_type=sa.INTEGER(), comment=None, - existing_comment='Platform this page belongs to', + existing_comment="Platform this page belongs to", existing_nullable=False) - op.drop_index(op.f('ix_admin_platforms_user_id'), table_name='admin_platforms') - op.drop_index(op.f('ix_admin_platforms_platform_id'), table_name='admin_platforms') - op.drop_index(op.f('ix_admin_platforms_id'), table_name='admin_platforms') - op.create_index('idx_admin_platforms_user_id', 'admin_platforms', ['user_id'], unique=False) - op.create_index('idx_admin_platforms_platform_id', 'admin_platforms', ['platform_id'], unique=False) - op.alter_column('admin_platforms', 'updated_at', + op.drop_index(op.f("ix_admin_platforms_user_id"), table_name="admin_platforms") + op.drop_index(op.f("ix_admin_platforms_platform_id"), table_name="admin_platforms") + op.drop_index(op.f("ix_admin_platforms_id"), table_name="admin_platforms") + op.create_index("idx_admin_platforms_user_id", "admin_platforms", ["user_id"], unique=False) + op.create_index("idx_admin_platforms_platform_id", "admin_platforms", ["platform_id"], unique=False) + op.alter_column("admin_platforms", "updated_at", existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('admin_platforms', 'created_at', + existing_server_default=sa.text("now()")) + op.alter_column("admin_platforms", "created_at", existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.drop_index(op.f('ix_admin_menu_configs_user_id'), table_name='admin_menu_configs') - op.drop_index(op.f('ix_admin_menu_configs_platform_id'), table_name='admin_menu_configs') - op.drop_index(op.f('ix_admin_menu_configs_menu_item_id'), table_name='admin_menu_configs') - op.drop_index(op.f('ix_admin_menu_configs_id'), table_name='admin_menu_configs') - op.drop_index(op.f('ix_admin_menu_configs_frontend_type'), table_name='admin_menu_configs') - op.create_index('idx_admin_menu_configs_user_id', 'admin_menu_configs', ['user_id'], unique=False) - op.create_index('idx_admin_menu_configs_platform_id', 'admin_menu_configs', ['platform_id'], unique=False) - op.create_index('idx_admin_menu_configs_menu_item_id', 'admin_menu_configs', ['menu_item_id'], unique=False) - op.create_index('idx_admin_menu_configs_frontend_type', 'admin_menu_configs', ['frontend_type'], unique=False) - op.alter_column('admin_menu_configs', 'updated_at', + existing_server_default=sa.text("now()")) + op.drop_index(op.f("ix_admin_menu_configs_user_id"), table_name="admin_menu_configs") + op.drop_index(op.f("ix_admin_menu_configs_platform_id"), table_name="admin_menu_configs") + op.drop_index(op.f("ix_admin_menu_configs_menu_item_id"), table_name="admin_menu_configs") + op.drop_index(op.f("ix_admin_menu_configs_id"), table_name="admin_menu_configs") + op.drop_index(op.f("ix_admin_menu_configs_frontend_type"), table_name="admin_menu_configs") + op.create_index("idx_admin_menu_configs_user_id", "admin_menu_configs", ["user_id"], unique=False) + op.create_index("idx_admin_menu_configs_platform_id", "admin_menu_configs", ["platform_id"], unique=False) + op.create_index("idx_admin_menu_configs_menu_item_id", "admin_menu_configs", ["menu_item_id"], unique=False) + op.create_index("idx_admin_menu_configs_frontend_type", "admin_menu_configs", ["frontend_type"], unique=False) + op.alter_column("admin_menu_configs", "updated_at", existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('admin_menu_configs', 'created_at', + existing_server_default=sa.text("now()")) + op.alter_column("admin_menu_configs", "created_at", existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=False, - existing_server_default=sa.text('now()')) - op.alter_column('admin_menu_configs', 'user_id', + existing_server_default=sa.text("now()")) + op.alter_column("admin_menu_configs", "user_id", existing_type=sa.INTEGER(), - comment='User scope - applies to this specific super admin', - existing_comment='User scope - applies to this specific super admin (admin frontend only)', + comment="User scope - applies to this specific super admin", + existing_comment="User scope - applies to this specific super admin (admin frontend only)", existing_nullable=True) - op.alter_column('admin_menu_configs', 'platform_id', + op.alter_column("admin_menu_configs", "platform_id", existing_type=sa.INTEGER(), - comment='Platform scope - applies to all platform admins of this platform', - existing_comment='Platform scope - applies to users/stores of this platform', + comment="Platform scope - applies to all platform admins of this platform", + existing_comment="Platform scope - applies to users/stores of this platform", existing_nullable=True) - op.drop_index(op.f('ix_loyalty_transactions_store_id'), table_name='loyalty_transactions') - op.drop_index(op.f('ix_loyalty_transactions_transaction_type'), table_name='loyalty_transactions') - op.drop_index(op.f('ix_loyalty_transactions_transaction_at'), table_name='loyalty_transactions') - op.drop_index(op.f('ix_loyalty_transactions_staff_pin_id'), table_name='loyalty_transactions') - op.drop_index(op.f('ix_loyalty_transactions_order_reference'), table_name='loyalty_transactions') - op.drop_index(op.f('ix_loyalty_transactions_id'), table_name='loyalty_transactions') - op.drop_index(op.f('ix_loyalty_transactions_card_id'), table_name='loyalty_transactions') - op.drop_index('idx_loyalty_tx_store_date', table_name='loyalty_transactions') - op.drop_index('idx_loyalty_tx_type_date', table_name='loyalty_transactions') - op.drop_index('idx_loyalty_tx_card_type', table_name='loyalty_transactions') - op.drop_table('loyalty_transactions') - op.drop_index(op.f('ix_apple_device_registrations_id'), table_name='apple_device_registrations') - op.drop_index(op.f('ix_apple_device_registrations_device_library_identifier'), table_name='apple_device_registrations') - op.drop_index(op.f('ix_apple_device_registrations_card_id'), table_name='apple_device_registrations') - op.drop_index('idx_apple_device_card', table_name='apple_device_registrations') - op.drop_table('apple_device_registrations') - op.drop_index(op.f('ix_staff_pins_store_id'), table_name='staff_pins') - op.drop_index(op.f('ix_staff_pins_staff_id'), table_name='staff_pins') - op.drop_index(op.f('ix_staff_pins_program_id'), table_name='staff_pins') - op.drop_index(op.f('ix_staff_pins_is_active'), table_name='staff_pins') - op.drop_index(op.f('ix_staff_pins_id'), table_name='staff_pins') - op.drop_index('idx_staff_pin_store_active', table_name='staff_pins') - op.drop_index('idx_staff_pin_program_active', table_name='staff_pins') - op.drop_table('staff_pins') - op.drop_index(op.f('ix_loyalty_cards_store_id'), table_name='loyalty_cards') - op.drop_index(op.f('ix_loyalty_cards_qr_code_data'), table_name='loyalty_cards') - op.drop_index(op.f('ix_loyalty_cards_program_id'), table_name='loyalty_cards') - op.drop_index(op.f('ix_loyalty_cards_is_active'), table_name='loyalty_cards') - op.drop_index(op.f('ix_loyalty_cards_id'), table_name='loyalty_cards') - op.drop_index(op.f('ix_loyalty_cards_google_object_id'), table_name='loyalty_cards') - op.drop_index(op.f('ix_loyalty_cards_customer_id'), table_name='loyalty_cards') - op.drop_index(op.f('ix_loyalty_cards_card_number'), table_name='loyalty_cards') - op.drop_index(op.f('ix_loyalty_cards_apple_serial_number'), table_name='loyalty_cards') - op.drop_index('idx_loyalty_card_store_active', table_name='loyalty_cards') - op.drop_index('idx_loyalty_card_customer_program', table_name='loyalty_cards') - op.drop_table('loyalty_cards') - op.drop_index(op.f('ix_loyalty_programs_store_id'), table_name='loyalty_programs') - op.drop_index(op.f('ix_loyalty_programs_is_active'), table_name='loyalty_programs') - op.drop_index(op.f('ix_loyalty_programs_id'), table_name='loyalty_programs') - op.drop_index('idx_loyalty_program_store_active', table_name='loyalty_programs') - op.drop_table('loyalty_programs') + op.drop_index(op.f("ix_loyalty_transactions_store_id"), table_name="loyalty_transactions") + op.drop_index(op.f("ix_loyalty_transactions_transaction_type"), table_name="loyalty_transactions") + op.drop_index(op.f("ix_loyalty_transactions_transaction_at"), table_name="loyalty_transactions") + op.drop_index(op.f("ix_loyalty_transactions_staff_pin_id"), table_name="loyalty_transactions") + op.drop_index(op.f("ix_loyalty_transactions_order_reference"), table_name="loyalty_transactions") + op.drop_index(op.f("ix_loyalty_transactions_id"), table_name="loyalty_transactions") + op.drop_index(op.f("ix_loyalty_transactions_card_id"), table_name="loyalty_transactions") + op.drop_index("idx_loyalty_tx_store_date", table_name="loyalty_transactions") + op.drop_index("idx_loyalty_tx_type_date", table_name="loyalty_transactions") + op.drop_index("idx_loyalty_tx_card_type", table_name="loyalty_transactions") + op.drop_table("loyalty_transactions") + op.drop_index(op.f("ix_apple_device_registrations_id"), table_name="apple_device_registrations") + op.drop_index(op.f("ix_apple_device_registrations_device_library_identifier"), table_name="apple_device_registrations") + op.drop_index(op.f("ix_apple_device_registrations_card_id"), table_name="apple_device_registrations") + op.drop_index("idx_apple_device_card", table_name="apple_device_registrations") + op.drop_table("apple_device_registrations") + op.drop_index(op.f("ix_staff_pins_store_id"), table_name="staff_pins") + op.drop_index(op.f("ix_staff_pins_staff_id"), table_name="staff_pins") + op.drop_index(op.f("ix_staff_pins_program_id"), table_name="staff_pins") + op.drop_index(op.f("ix_staff_pins_is_active"), table_name="staff_pins") + op.drop_index(op.f("ix_staff_pins_id"), table_name="staff_pins") + op.drop_index("idx_staff_pin_store_active", table_name="staff_pins") + op.drop_index("idx_staff_pin_program_active", table_name="staff_pins") + op.drop_table("staff_pins") + op.drop_index(op.f("ix_loyalty_cards_store_id"), table_name="loyalty_cards") + op.drop_index(op.f("ix_loyalty_cards_qr_code_data"), table_name="loyalty_cards") + op.drop_index(op.f("ix_loyalty_cards_program_id"), table_name="loyalty_cards") + op.drop_index(op.f("ix_loyalty_cards_is_active"), table_name="loyalty_cards") + op.drop_index(op.f("ix_loyalty_cards_id"), table_name="loyalty_cards") + op.drop_index(op.f("ix_loyalty_cards_google_object_id"), table_name="loyalty_cards") + op.drop_index(op.f("ix_loyalty_cards_customer_id"), table_name="loyalty_cards") + op.drop_index(op.f("ix_loyalty_cards_card_number"), table_name="loyalty_cards") + op.drop_index(op.f("ix_loyalty_cards_apple_serial_number"), table_name="loyalty_cards") + op.drop_index("idx_loyalty_card_store_active", table_name="loyalty_cards") + op.drop_index("idx_loyalty_card_customer_program", table_name="loyalty_cards") + op.drop_table("loyalty_cards") + op.drop_index(op.f("ix_loyalty_programs_store_id"), table_name="loyalty_programs") + op.drop_index(op.f("ix_loyalty_programs_is_active"), table_name="loyalty_programs") + op.drop_index(op.f("ix_loyalty_programs_id"), table_name="loyalty_programs") + op.drop_index("idx_loyalty_program_store_active", table_name="loyalty_programs") + op.drop_table("loyalty_programs") # ### end Alembic commands ### diff --git a/alembic/versions_backup/module_loyalty/loyalty_003_phase2_merchant_based.py b/alembic/versions_backup/module_loyalty/loyalty_003_phase2_merchant_based.py index 7040b76d..164c81ee 100644 --- a/alembic/versions_backup/module_loyalty/loyalty_003_phase2_merchant_based.py +++ b/alembic/versions_backup/module_loyalty/loyalty_003_phase2_merchant_based.py @@ -15,17 +15,18 @@ Phase 2 changes: - NEW COLUMN on loyalty_cards: last_activity_at """ -from typing import Sequence, Union +from collections.abc import Sequence import sqlalchemy as sa -from alembic import op from sqlalchemy import text +from alembic import op + # revision identifiers, used by Alembic. revision: str = "loyalty_003_phase2" -down_revision: Union[str, None] = "0fb5d6d6ff97" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "0fb5d6d6ff97" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/alembic/versions_backup/n2c3d4e5f6a7_add_features_table.py b/alembic/versions_backup/n2c3d4e5f6a7_add_features_table.py index 27b36e85..ddfd69e4 100644 --- a/alembic/versions_backup/n2c3d4e5f6a7_add_features_table.py +++ b/alembic/versions_backup/n2c3d4e5f6a7_add_features_table.py @@ -7,16 +7,17 @@ Create Date: 2025-12-31 10:00:00.000000 """ import json -from typing import Sequence, Union +from collections.abc import Sequence 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 +down_revision: str | None = "ba2c0ce78396" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None # ============================================================================ @@ -245,7 +246,7 @@ def upgrade() -> None: tier_ids[row[1]] = row[0] # Insert features - now = sa.func.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 diff --git a/alembic/versions_backup/o3c4d5e6f7a8_add_inventory_transactions_table.py b/alembic/versions_backup/o3c4d5e6f7a8_add_inventory_transactions_table.py index fef620d5..10c2279d 100644 --- a/alembic/versions_backup/o3c4d5e6f7a8_add_inventory_transactions_table.py +++ b/alembic/versions_backup/o3c4d5e6f7a8_add_inventory_transactions_table.py @@ -10,9 +10,10 @@ Adds an audit trail for inventory movements: - Store quantity snapshots for historical analysis """ -from alembic import op import sqlalchemy as sa +from alembic import op + # revision identifiers, used by Alembic. revision = "o3c4d5e6f7a8" down_revision = "n2c3d4e5f6a7" diff --git a/alembic/versions_backup/p4d5e6f7a8b9_add_shipped_quantity_to_order_items.py b/alembic/versions_backup/p4d5e6f7a8b9_add_shipped_quantity_to_order_items.py index cbd2069b..6c7161af 100644 --- a/alembic/versions_backup/p4d5e6f7a8b9_add_shipped_quantity_to_order_items.py +++ b/alembic/versions_backup/p4d5e6f7a8b9_add_shipped_quantity_to_order_items.py @@ -6,24 +6,24 @@ Revises: o3c4d5e6f7a8 Create Date: 2026-01-01 12:00:00.000000 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "p4d5e6f7a8b9" +down_revision: str | None = "o3c4d5e6f7a8" +branch_labels: str | Sequence[str] | None = None +depends_on: 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') + "order_items", + sa.Column("shipped_quantity", sa.Integer(), nullable=False, server_default="0") ) # Set shipped_quantity = quantity for already fulfilled items @@ -36,4 +36,4 @@ def upgrade() -> None: def downgrade() -> None: - op.drop_column('order_items', 'shipped_quantity') + op.drop_column("order_items", "shipped_quantity") diff --git a/alembic/versions_backup/q5e6f7a8b9c0_add_vat_fields_to_orders.py b/alembic/versions_backup/q5e6f7a8b9c0_add_vat_fields_to_orders.py index b7926c76..87bc42d8 100644 --- a/alembic/versions_backup/q5e6f7a8b9c0_add_vat_fields_to_orders.py +++ b/alembic/versions_backup/q5e6f7a8b9c0_add_vat_fields_to_orders.py @@ -10,42 +10,42 @@ Revises: p4d5e6f7a8b9 Create Date: 2026-01-02 10:00:00.000000 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # 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 +revision: str = "q5e6f7a8b9c0" +down_revision: str | None = "p4d5e6f7a8b9" +branch_labels: str | Sequence[str] | None = None +depends_on: 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) + "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) + "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) + "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) + "orders", + sa.Column("vat_destination_country", sa.String(2), nullable=True) ) # Populate VAT fields for existing orders based on shipping country @@ -66,7 +66,7 @@ def upgrade() -> None: 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') + 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_backup/r6f7a8b9c0d1_add_country_iso_to_addresses.py b/alembic/versions_backup/r6f7a8b9c0d1_add_country_iso_to_addresses.py index e7af3c97..ea2d862a 100644 --- a/alembic/versions_backup/r6f7a8b9c0d1_add_country_iso_to_addresses.py +++ b/alembic/versions_backup/r6f7a8b9c0d1_add_country_iso_to_addresses.py @@ -11,10 +11,10 @@ This migration is idempotent - it checks for existing columns before making changes. """ -from alembic import op import sqlalchemy as sa from sqlalchemy import text +from alembic import op # revision identifiers, used by Alembic. revision = "r6f7a8b9c0d1" diff --git a/alembic/versions_backup/s7a8b9c0d1e2_add_storefront_locale_to_vendors.py b/alembic/versions_backup/s7a8b9c0d1e2_add_storefront_locale_to_vendors.py index b11cb9a0..ffc01fbf 100644 --- a/alembic/versions_backup/s7a8b9c0d1e2_add_storefront_locale_to_vendors.py +++ b/alembic/versions_backup/s7a8b9c0d1e2_add_storefront_locale_to_vendors.py @@ -10,9 +10,9 @@ NULL means the vendor inherits from platform defaults. Examples: 'fr-LU', 'de-DE', 'en-GB' """ -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision = "s7a8b9c0d1e2" diff --git a/alembic/versions_backup/t001_rename_company_vendor_to_merchant_store.py b/alembic/versions_backup/t001_rename_company_vendor_to_merchant_store.py index 15644522..254fa619 100644 --- a/alembic/versions_backup/t001_rename_company_vendor_to_merchant_store.py +++ b/alembic/versions_backup/t001_rename_company_vendor_to_merchant_store.py @@ -18,16 +18,17 @@ Major terminology migration: - letzshop_vendor_cache -> letzshop_store_cache """ -from typing import Sequence, Union +from collections.abc import Sequence + +from sqlalchemy import text 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 +down_revision: str | None = "loyalty_003_phase2" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def _col_exists(table: str, col: str) -> bool: diff --git a/alembic/versions_backup/t002_rename_vendor_constraints_and_indexes.py b/alembic/versions_backup/t002_rename_vendor_constraints_and_indexes.py index 082b81f7..f884d9e0 100644 --- a/alembic/versions_backup/t002_rename_vendor_constraints_and_indexes.py +++ b/alembic/versions_backup/t002_rename_vendor_constraints_and_indexes.py @@ -8,15 +8,15 @@ Completes the Company/Vendor -> Merchant/Store terminology migration by renaming 4 constraints and 12 indexes that still used "vendor" in their names. """ -from typing import Sequence, Union +from collections.abc import Sequence from alembic import op # revision identifiers, used by Alembic. revision: str = "t002_constraints" -down_revision: Union[str, None] = "t001_terminology" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "t001_terminology" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None # (old_name, new_name, table) — table is needed for RENAME CONSTRAINT CONSTRAINTS = [ diff --git a/alembic/versions_backup/t8b9c0d1e2f3_add_password_reset_tokens.py b/alembic/versions_backup/t8b9c0d1e2f3_add_password_reset_tokens.py index 329a3189..16b7d9fc 100644 --- a/alembic/versions_backup/t8b9c0d1e2f3_add_password_reset_tokens.py +++ b/alembic/versions_backup/t8b9c0d1e2f3_add_password_reset_tokens.py @@ -6,9 +6,9 @@ Create Date: 2026-01-03 """ -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision = "t8b9c0d1e2f3" diff --git a/alembic/versions_backup/u9c0d1e2f3g4_add_vendor_email_templates.py b/alembic/versions_backup/u9c0d1e2f3g4_add_vendor_email_templates.py index 9cf16718..0c633b8e 100644 --- a/alembic/versions_backup/u9c0d1e2f3g4_add_vendor_email_templates.py +++ b/alembic/versions_backup/u9c0d1e2f3g4_add_vendor_email_templates.py @@ -11,9 +11,9 @@ Changes: - Create vendor_email_templates table for vendor-specific template overrides """ -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision = "u9c0d1e2f3g4" diff --git a/alembic/versions_backup/v0a1b2c3d4e5_add_vendor_email_settings.py b/alembic/versions_backup/v0a1b2c3d4e5_add_vendor_email_settings.py index ac5307a1..67fff26f 100644 --- a/alembic/versions_backup/v0a1b2c3d4e5_add_vendor_email_settings.py +++ b/alembic/versions_backup/v0a1b2c3d4e5_add_vendor_email_settings.py @@ -11,9 +11,9 @@ Changes: - Premium providers (SendGrid, Mailgun, SES) are tier-gated (Business+) """ -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision = "v0a1b2c3d4e5" diff --git a/alembic/versions_backup/w1b2c3d4e5f6_add_media_library_tables.py b/alembic/versions_backup/w1b2c3d4e5f6_add_media_library_tables.py index 795b0827..5725b683 100644 --- a/alembic/versions_backup/w1b2c3d4e5f6_add_media_library_tables.py +++ b/alembic/versions_backup/w1b2c3d4e5f6_add_media_library_tables.py @@ -5,16 +5,17 @@ Revises: v0a1b2c3d4e5 Create Date: 2026-01-06 10:00:00.000000 """ -from typing import Sequence, Union +from collections.abc import Sequence + +import sqlalchemy as sa 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 +down_revision: str | None = "v0a1b2c3d4e5" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/alembic/versions_backup/x2c3d4e5f6g7_make_marketplace_product_id_nullable.py b/alembic/versions_backup/x2c3d4e5f6g7_make_marketplace_product_id_nullable.py index b8e334bb..c2671077 100644 --- a/alembic/versions_backup/x2c3d4e5f6g7_make_marketplace_product_id_nullable.py +++ b/alembic/versions_backup/x2c3d4e5f6g7_make_marketplace_product_id_nullable.py @@ -9,9 +9,9 @@ Create Date: 2026-01-06 23:15:00.000000 from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision: str = "x2c3d4e5f6g7" diff --git a/alembic/versions_backup/y3d4e5f6g7h8_add_product_type_columns.py b/alembic/versions_backup/y3d4e5f6g7h8_add_product_type_columns.py index a7db7e52..37be39a2 100644 --- a/alembic/versions_backup/y3d4e5f6g7h8_add_product_type_columns.py +++ b/alembic/versions_backup/y3d4e5f6g7h8_add_product_type_columns.py @@ -11,9 +11,9 @@ Create Date: 2026-01-07 10:00:00.000000 from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision: str = "y3d4e5f6g7h8" diff --git a/alembic/versions_backup/z4e5f6a7b8c9_add_multi_platform_support.py b/alembic/versions_backup/z4e5f6a7b8c9_add_multi_platform_support.py index ea8b53b4..ff61b342 100644 --- a/alembic/versions_backup/z4e5f6a7b8c9_add_multi_platform_support.py +++ b/alembic/versions_backup/z4e5f6a7b8c9_add_multi_platform_support.py @@ -15,16 +15,17 @@ This migration adds multi-platform support: """ import json -from typing import Sequence, Union +from collections.abc import Sequence 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 +down_revision: str | None = "1b398cf45e85" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None # Platform marketing page slugs (is_platform_page=True) PLATFORM_PAGE_SLUGS = [ @@ -303,7 +304,7 @@ def upgrade() -> None: ("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: + 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""" diff --git a/alembic/versions_backup/z5f6g7h8i9j0_add_loyalty_platform.py b/alembic/versions_backup/z5f6g7h8i9j0_add_loyalty_platform.py index 12e09ac9..644b3151 100644 --- a/alembic/versions_backup/z5f6g7h8i9j0_add_loyalty_platform.py +++ b/alembic/versions_backup/z5f6g7h8i9j0_add_loyalty_platform.py @@ -10,16 +10,17 @@ This migration adds the Loyalty+ platform: 3. Creates vendor default pages (about, rewards-catalog, terms, privacy) """ -from typing import Sequence, Union +from collections.abc import Sequence 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 +down_revision: str | None = "z4e5f6a7b8c9" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/alembic/versions_backup/z6g7h8i9j0k1_add_main_platform.py b/alembic/versions_backup/z6g7h8i9j0k1_add_main_platform.py index d25de645..8bef2f61 100644 --- a/alembic/versions_backup/z6g7h8i9j0k1_add_main_platform.py +++ b/alembic/versions_backup/z6g7h8i9j0k1_add_main_platform.py @@ -17,16 +17,17 @@ All other platforms are accessed via: - Production: {code}.lu or custom domain """ -from typing import Sequence, Union +from collections.abc import Sequence import sqlalchemy as sa + from alembic import op # revision identifiers, used by Alembic. revision: str = "z6g7h8i9j0k1" -down_revision: Union[str, None] = "z5f6g7h8i9j0" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "z5f6g7h8i9j0" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/alembic/versions_backup/z7h8i9j0k1l2_fix_content_page_nullable_columns.py b/alembic/versions_backup/z7h8i9j0k1l2_fix_content_page_nullable_columns.py index 12a0fe1f..2b2bbde0 100644 --- a/alembic/versions_backup/z7h8i9j0k1l2_fix_content_page_nullable_columns.py +++ b/alembic/versions_backup/z7h8i9j0k1l2_fix_content_page_nullable_columns.py @@ -9,9 +9,9 @@ This migration: 2. Alters columns to be NOT NULL """ -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision = "z7h8i9j0k1l2" diff --git a/alembic/versions_backup/z8i9j0k1l2m3_add_sections_to_content_pages.py b/alembic/versions_backup/z8i9j0k1l2m3_add_sections_to_content_pages.py index dbf4da8d..89bc703f 100644 --- a/alembic/versions_backup/z8i9j0k1l2m3_add_sections_to_content_pages.py +++ b/alembic/versions_backup/z8i9j0k1l2m3_add_sections_to_content_pages.py @@ -9,9 +9,9 @@ The sections column stores hero, features, pricing, and cta configurations with TranslatableText pattern for i18n. """ -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision = "z8i9j0k1l2m3" diff --git a/alembic/versions_backup/z9j0k1l2m3n4_add_admin_platform_roles.py b/alembic/versions_backup/z9j0k1l2m3n4_add_admin_platform_roles.py index 0f5ca272..701ab60b 100644 --- a/alembic/versions_backup/z9j0k1l2m3n4_add_admin_platform_roles.py +++ b/alembic/versions_backup/z9j0k1l2m3n4_add_admin_platform_roles.py @@ -13,11 +13,10 @@ Platform admins are assigned to specific platforms via admin_platforms. Existing admins are migrated to super admins for backward compatibility. """ -from datetime import UTC, datetime -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision = "z9j0k1l2m3n4" diff --git a/alembic/versions_backup/za0k1l2m3n4o5_add_admin_menu_config.py b/alembic/versions_backup/za0k1l2m3n4o5_add_admin_menu_config.py index b2c18cba..419f876f 100644 --- a/alembic/versions_backup/za0k1l2m3n4o5_add_admin_menu_config.py +++ b/alembic/versions_backup/za0k1l2m3n4o5_add_admin_menu_config.py @@ -11,9 +11,9 @@ Adds configurable admin sidebar menus: - Mandatory items enforced at application level (companies, vendors, users, settings) """ -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision = "za0k1l2m3n4o5" diff --git a/alembic/versions_backup/zb1l2m3n4o5p6_add_frontend_type_to_menu_config.py b/alembic/versions_backup/zb1l2m3n4o5p6_add_frontend_type_to_menu_config.py index ded81419..7dbdb455 100644 --- a/alembic/versions_backup/zb1l2m3n4o5p6_add_frontend_type_to_menu_config.py +++ b/alembic/versions_backup/zb1l2m3n4o5p6_add_frontend_type_to_menu_config.py @@ -12,9 +12,9 @@ Also updates unique constraints to include frontend_type and adds a check constraint ensuring user_id scope is only used for admin frontend. """ -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision = "zb1l2m3n4o5p6" @@ -25,7 +25,7 @@ depends_on = None def upgrade() -> None: # 1. Create the enum type for frontend_type - frontend_type_enum = sa.Enum('admin', 'vendor', name='frontendtype') + frontend_type_enum = sa.Enum("admin", "vendor", name="frontendtype") frontend_type_enum.create(op.get_bind(), checkfirst=True) # 2. Add frontend_type column with default value @@ -33,7 +33,7 @@ def upgrade() -> None: "admin_menu_configs", sa.Column( "frontend_type", - sa.Enum('admin', 'vendor', name='frontendtype'), + sa.Enum("admin", "vendor", name="frontendtype"), nullable=False, server_default="admin", comment="Which frontend this config applies to (admin or vendor)", @@ -114,4 +114,4 @@ def downgrade() -> None: op.drop_column("admin_menu_configs", "frontend_type") # Drop the enum type - sa.Enum('admin', 'vendor', name='frontendtype').drop(op.get_bind(), checkfirst=True) + sa.Enum("admin", "vendor", name="frontendtype").drop(op.get_bind(), checkfirst=True) diff --git a/alembic/versions_backup/zc2m3n4o5p6q7_add_platform_modules_table.py b/alembic/versions_backup/zc2m3n4o5p6q7_add_platform_modules_table.py index 4f31fc16..680f78f0 100644 --- a/alembic/versions_backup/zc2m3n4o5p6q7_add_platform_modules_table.py +++ b/alembic/versions_backup/zc2m3n4o5p6q7_add_platform_modules_table.py @@ -13,9 +13,9 @@ This replaces the simpler Platform.settings["enabled_modules"] JSON approach for better auditability and query capabilities. """ -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision = "zc2m3n4o5p6q7" diff --git a/alembic/versions_backup/zd3n4o5p6q7r8_promote_cms_customers_to_core.py b/alembic/versions_backup/zd3n4o5p6q7r8_promote_cms_customers_to_core.py index 00af6d27..0aa8a840 100644 --- a/alembic/versions_backup/zd3n4o5p6q7r8_promote_cms_customers_to_core.py +++ b/alembic/versions_backup/zd3n4o5p6q7r8_promote_cms_customers_to_core.py @@ -9,10 +9,11 @@ This migration ensures that CMS and Customers modules are enabled for all platfo since they are now core modules that cannot be disabled. """ -from datetime import datetime, timezone +from datetime import UTC, datetime + +import sqlalchemy as sa from alembic import op -import sqlalchemy as sa # revision identifiers, used by Alembic. revision = "zd3n4o5p6q7r8" @@ -30,7 +31,7 @@ def upgrade() -> None: sa.text("SELECT id FROM platforms") ).fetchall() - now = datetime.now(timezone.utc) + now = datetime.now(UTC) core_modules = ["cms", "customers"] for (platform_id,) in platforms: @@ -80,4 +81,3 @@ def downgrade() -> None: break functionality. It just removes the explicit enabling done by upgrade. """ # No-op: We don't want to disable core modules - pass diff --git a/app/api/deps.py b/app/api/deps.py index ff154396..8be273e4 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -44,22 +44,22 @@ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from sqlalchemy.orm import Session from app.core.database import get_db +from app.modules.enums import FrontendType from app.modules.tenancy.exceptions import ( AdminRequiredException, InsufficientPermissionsException, InsufficientStorePermissionsException, InvalidTokenException, - UnauthorizedStoreAccessException, StoreNotFoundException, StoreOwnerOnlyException, + UnauthorizedStoreAccessException, ) +from app.modules.tenancy.models import Store +from app.modules.tenancy.models import User as UserModel from app.modules.tenancy.services.store_service import store_service from middleware.auth import AuthManager from middleware.rate_limiter import RateLimiter -from app.modules.tenancy.models import User as UserModel -from app.modules.tenancy.models import Store from models.schema.auth import UserContext -from app.modules.enums import FrontendType # Initialize dependencies security = HTTPBearer(auto_error=False) # auto_error=False prevents automatic 403 @@ -485,10 +485,9 @@ def require_module_access(module_code: str, frontend_type: FrontendType): if user_context.is_super_admin: # Super admins bypass module checks return user_context - else: - platform = getattr(request.state, "admin_platform", None) - if platform: - platform_id = platform.id + platform = getattr(request.state, "admin_platform", None) + if platform: + platform_id = platform.id except Exception: pass @@ -572,10 +571,10 @@ def require_menu_access(menu_item_id: str, frontend_type: "FrontendType"): Returns: Dependency function that validates menu access and returns User """ - from app.modules.registry import get_menu_item_module - from app.modules.service import module_service from app.modules.core.services.menu_service import menu_service from app.modules.enums import FrontendType as FT + from app.modules.registry import get_menu_item_module + from app.modules.service import module_service def _check_menu_access( request: Request, @@ -941,52 +940,82 @@ def get_current_merchant_optional( return None -def require_merchant_owner(merchant_id: int): +def get_merchant_for_current_user( + request: Request, + current_user: UserContext = Depends(get_current_merchant_api), + db: Session = Depends(get_db), +): """ - Dependency factory to require ownership of a specific merchant. + Get the active merchant owned by the current API user. - Usage: - @router.get("/merchants/{merchant_id}/subscriptions") - def list_subscriptions( - merchant_id: int, - user: UserContext = Depends(require_merchant_owner(merchant_id)) - ): - ... + Used by merchant API endpoints (header-only auth) that need the Merchant object. + Stores the merchant on request.state.merchant for endpoint use. + + Returns: + Merchant ORM object + + Raises: + MerchantNotFoundException: If user owns no active merchants """ + from app.modules.tenancy.exceptions import MerchantNotFoundException + from app.modules.tenancy.models import Merchant - def _check_merchant_ownership( - request: Request, - credentials: HTTPAuthorizationCredentials | None = Depends(security), - merchant_token: str | None = Cookie(None), - db: Session = Depends(get_db), - ) -> UserContext: - user_context = get_current_merchant_from_cookie_or_header( - request, credentials, merchant_token, db + merchant = ( + db.query(Merchant) + .filter( + Merchant.owner_user_id == current_user.id, + Merchant.is_active == True, # noqa: E712 + ) + .order_by(Merchant.id) + .first() + ) + + if not merchant: + raise MerchantNotFoundException( + str(current_user.id), identifier_type="owner_user_id" ) - # Verify user owns this specific merchant - from app.modules.tenancy.models import Merchant - merchant = ( - db.query(Merchant) - .filter( - Merchant.id == merchant_id, - Merchant.owner_user_id == user_context.id, - Merchant.is_active == True, # noqa: E712 - ) - .first() + request.state.merchant = merchant + return merchant + + +def get_merchant_for_current_user_page( + request: Request, + current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header), + db: Session = Depends(get_db), +): + """ + Get the active merchant owned by the current page user. + + Used by merchant page routes (cookie+header auth) that need the Merchant object. + Stores the merchant on request.state.merchant for endpoint use. + + Returns: + Merchant ORM object + + Raises: + MerchantNotFoundException: If user owns no active merchants + """ + from app.modules.tenancy.exceptions import MerchantNotFoundException + from app.modules.tenancy.models import Merchant + + merchant = ( + db.query(Merchant) + .filter( + Merchant.owner_user_id == current_user.id, + Merchant.is_active == True, # noqa: E712 + ) + .order_by(Merchant.id) + .first() + ) + + if not merchant: + raise MerchantNotFoundException( + str(current_user.id), identifier_type="owner_user_id" ) - if not merchant: - raise InsufficientPermissionsException( - f"You do not own merchant {merchant_id}" - ) - - # Store merchant in request state for endpoint use - request.state.merchant = merchant - - return user_context - - return _check_merchant_ownership + request.state.merchant = merchant + return merchant # ============================================================================ diff --git a/app/api/main.py b/app/api/main.py index 828992d5..58b84d96 100644 --- a/app/api/main.py +++ b/app/api/main.py @@ -10,7 +10,7 @@ This module provides: from fastapi import APIRouter -from app.api.v1 import admin, merchant, platform, storefront, store, webhooks +from app.api.v1 import admin, merchant, platform, store, storefront, webhooks api_router = APIRouter() diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py index 4432d581..20f8d69e 100644 --- a/app/api/v1/__init__.py +++ b/app/api/v1/__init__.py @@ -3,6 +3,6 @@ API Version 1 - All endpoints """ -from . import admin, merchant, storefront, store +from . import admin, merchant, store, storefront __all__ = ["admin", "merchant", "store", "storefront"] diff --git a/app/api/v1/admin/__init__.py b/app/api/v1/admin/__init__.py index 21f1e950..8be47cb5 100644 --- a/app/api/v1/admin/__init__.py +++ b/app/api/v1/admin/__init__.py @@ -25,7 +25,6 @@ IMPORTANT: from fastapi import APIRouter - # Create admin router router = APIRouter() diff --git a/app/api/v1/merchant/__init__.py b/app/api/v1/merchant/__init__.py index 0bffa2e3..e7b45c25 100644 --- a/app/api/v1/merchant/__init__.py +++ b/app/api/v1/merchant/__init__.py @@ -16,7 +16,6 @@ IMPORTANT: from fastapi import APIRouter - # Create merchant router router = APIRouter() diff --git a/app/api/v1/platform/signup.py b/app/api/v1/platform/signup.py index f0d8e657..234ec2c4 100644 --- a/app/api/v1/platform/signup.py +++ b/app/api/v1/platform/signup.py @@ -20,7 +20,9 @@ from sqlalchemy.orm import Session from app.core.database import get_db from app.core.environment import should_use_secure_cookies -from app.modules.marketplace.services.platform_signup_service import platform_signup_service +from app.modules.marketplace.services.platform_signup_service import ( + platform_signup_service, +) router = APIRouter() logger = logging.getLogger(__name__) diff --git a/app/core/lifespan.py b/app/core/lifespan.py index 0f813283..ce85512e 100644 --- a/app/core/lifespan.py +++ b/app/core/lifespan.py @@ -65,7 +65,7 @@ def get_migration_status(): try: from alembic.config import Config - alembic_cfg = Config("alembic.ini") + Config("alembic.ini") # This would need more implementation to actually check status # For now, just return a placeholder diff --git a/app/core/logging.py b/app/core/logging.py index f58b1b54..9d188927 100644 --- a/app/core/logging.py +++ b/app/core/logging.py @@ -102,7 +102,9 @@ def get_log_level_from_db(): """ try: from app.core.database import SessionLocal - from app.modules.core.services.admin_settings_service import admin_settings_service + from app.modules.core.services.admin_settings_service import ( + admin_settings_service, + ) db = SessionLocal() if not db: @@ -127,7 +129,9 @@ def get_rotation_settings_from_db(): """ try: from app.core.database import SessionLocal - from app.modules.core.services.admin_settings_service import admin_settings_service + from app.modules.core.services.admin_settings_service import ( + admin_settings_service, + ) db = SessionLocal() if not db: diff --git a/app/core/observability.py b/app/core/observability.py index f5ae0f29..defff19e 100644 --- a/app/core/observability.py +++ b/app/core/observability.py @@ -30,7 +30,7 @@ import logging import time from collections.abc import Callable from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime from enum import Enum from typing import Any @@ -61,7 +61,7 @@ class HealthCheckResult: message: str = "" latency_ms: float = 0.0 details: dict[str, Any] = field(default_factory=dict) - checked_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + checked_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @dataclass @@ -70,7 +70,7 @@ class AggregatedHealth: status: HealthStatus checks: list[HealthCheckResult] - timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + timestamp: datetime = field(default_factory=lambda: datetime.now(UTC)) def to_dict(self) -> dict[str, Any]: """Convert to dictionary for JSON response.""" diff --git a/app/handlers/stripe_webhook.py b/app/handlers/stripe_webhook.py index a93d27c8..e97ac463 100644 --- a/app/handlers/stripe_webhook.py +++ b/app/handlers/stripe_webhook.py @@ -10,7 +10,7 @@ Processes webhook events from Stripe: """ import logging -from datetime import datetime, timezone +from datetime import UTC, datetime import stripe from sqlalchemy.orm import Session @@ -19,10 +19,10 @@ from app.modules.billing.models import ( AddOnProduct, BillingHistory, MerchantSubscription, + StoreAddOn, StripeWebhookEvent, SubscriptionStatus, SubscriptionTier, - StoreAddOn, ) from app.modules.tenancy.models import Store, StorePlatform @@ -68,7 +68,7 @@ class StripeWebhookHandler: if existing.status == "processed": logger.info(f"Skipping duplicate event {event_id}") return {"status": "skipped", "reason": "duplicate"} - elif existing.status == "failed": + if existing.status == "failed": logger.info(f"Retrying previously failed event {event_id}") else: # Record the event @@ -86,14 +86,14 @@ class StripeWebhookHandler: if not handler: logger.debug(f"No handler for event type {event_type}") existing.status = "processed" - existing.processed_at = datetime.now(timezone.utc) + existing.processed_at = datetime.now(UTC) db.commit() return {"status": "ignored", "reason": f"no handler for {event_type}"} try: result = handler(db, event) existing.status = "processed" - existing.processed_at = datetime.now(timezone.utc) + existing.processed_at = datetime.now(UTC) db.commit() logger.info(f"Successfully processed event {event_id} ({event_type})") return {"status": "processed", "result": result} @@ -181,15 +181,15 @@ class StripeWebhookHandler: if session.subscription: stripe_sub = stripe.Subscription.retrieve(session.subscription) subscription.period_start = datetime.fromtimestamp( - stripe_sub.current_period_start, tz=timezone.utc + stripe_sub.current_period_start, tz=UTC ) subscription.period_end = datetime.fromtimestamp( - stripe_sub.current_period_end, tz=timezone.utc + stripe_sub.current_period_end, tz=UTC ) if stripe_sub.trial_end: subscription.trial_ends_at = datetime.fromtimestamp( - stripe_sub.trial_end, tz=timezone.utc + stripe_sub.trial_end, tz=UTC ) logger.info(f"Subscription checkout completed for merchant {merchant_id}") @@ -264,10 +264,10 @@ class StripeWebhookHandler: try: stripe_sub = stripe.Subscription.retrieve(session.subscription) period_start = datetime.fromtimestamp( - stripe_sub.current_period_start, tz=timezone.utc + stripe_sub.current_period_start, tz=UTC ) period_end = datetime.fromtimestamp( - stripe_sub.current_period_end, tz=timezone.utc + stripe_sub.current_period_end, tz=UTC ) except Exception as e: logger.warning(f"Could not retrieve subscription period: {e}") @@ -320,10 +320,10 @@ class StripeWebhookHandler: subscription.stripe_subscription_id = stripe_sub.id subscription.status = self._map_stripe_status(stripe_sub.status) subscription.period_start = datetime.fromtimestamp( - stripe_sub.current_period_start, tz=timezone.utc + stripe_sub.current_period_start, tz=UTC ) subscription.period_end = datetime.fromtimestamp( - stripe_sub.current_period_end, tz=timezone.utc + stripe_sub.current_period_end, tz=UTC ) logger.info(f"Subscription created for merchant {subscription.merchant_id}") @@ -348,15 +348,15 @@ class StripeWebhookHandler: # Update status and period subscription.status = self._map_stripe_status(stripe_sub.status) subscription.period_start = datetime.fromtimestamp( - stripe_sub.current_period_start, tz=timezone.utc + stripe_sub.current_period_start, tz=UTC ) subscription.period_end = datetime.fromtimestamp( - stripe_sub.current_period_end, tz=timezone.utc + stripe_sub.current_period_end, tz=UTC ) # Handle cancellation if stripe_sub.cancel_at_period_end: - subscription.cancelled_at = datetime.now(timezone.utc) + subscription.cancelled_at = datetime.now(UTC) subscription.cancellation_reason = stripe_sub.metadata.get( "cancellation_reason", "user_request" ) @@ -407,7 +407,7 @@ class StripeWebhookHandler: # Cancel the subscription subscription.status = SubscriptionStatus.CANCELLED.value - subscription.cancelled_at = datetime.now(timezone.utc) + subscription.cancelled_at = datetime.now(UTC) # Find all stores for this merchant, then cancel their add-ons store_ids = [ @@ -429,7 +429,7 @@ class StripeWebhookHandler: addon_count = 0 for addon in cancelled_addons: addon.status = "cancelled" - addon.cancelled_at = datetime.now(timezone.utc) + addon.cancelled_at = datetime.now(UTC) addon_count += 1 if addon_count > 0: @@ -463,7 +463,7 @@ class StripeWebhookHandler: stripe_invoice_id=invoice.id, stripe_payment_intent_id=invoice.payment_intent, invoice_number=invoice.number, - invoice_date=datetime.fromtimestamp(invoice.created, tz=timezone.utc), + invoice_date=datetime.fromtimestamp(invoice.created, tz=UTC), subtotal_cents=invoice.subtotal, tax_cents=invoice.tax or 0, total_cents=invoice.total, @@ -550,8 +550,8 @@ class StripeWebhookHandler: merchant_id=subscription.merchant_id, stripe_invoice_id=invoice.id, invoice_number=invoice.number, - invoice_date=datetime.fromtimestamp(invoice.created, tz=timezone.utc), - due_date=datetime.fromtimestamp(invoice.due_date, tz=timezone.utc) + invoice_date=datetime.fromtimestamp(invoice.created, tz=UTC), + due_date=datetime.fromtimestamp(invoice.due_date, tz=UTC) if invoice.due_date else None, subtotal_cents=invoice.subtotal, diff --git a/app/modules/__init__.py b/app/modules/__init__.py index 6023549f..49b63de7 100644 --- a/app/modules/__init__.py +++ b/app/modules/__init__.py @@ -54,31 +54,31 @@ Usage: """ from app.modules.base import ModuleDefinition, ScheduledTask -from app.modules.task_base import ModuleTask, DatabaseTask -from app.modules.tasks import ( - discover_module_tasks, - build_beat_schedule, - parse_schedule, - get_module_task_routes, +from app.modules.events import ( + ModuleEvent, + ModuleEventBus, + ModuleEventData, + module_event_bus, ) from app.modules.registry import ( - MODULES, CORE_MODULES, - OPTIONAL_MODULES, INTERNAL_MODULES, + MODULES, + OPTIONAL_MODULES, get_core_module_codes, - get_optional_module_codes, get_internal_module_codes, get_module_tier, + get_optional_module_codes, is_core_module, is_internal_module, ) from app.modules.service import ModuleService, module_service -from app.modules.events import ( - ModuleEvent, - ModuleEventData, - ModuleEventBus, - module_event_bus, +from app.modules.task_base import DatabaseTask, ModuleTask +from app.modules.tasks import ( + build_beat_schedule, + discover_module_tasks, + get_module_task_routes, + parse_schedule, ) __all__ = [ diff --git a/app/modules/analytics/__init__.py b/app/modules/analytics/__init__.py index fc20eb1d..7cf1af11 100644 --- a/app/modules/analytics/__init__.py +++ b/app/modules/analytics/__init__.py @@ -25,7 +25,7 @@ def __getattr__(name: str): from app.modules.analytics.definition import analytics_module return analytics_module - elif name == "get_analytics_module_with_routers": + if name == "get_analytics_module_with_routers": from app.modules.analytics.definition import get_analytics_module_with_routers return get_analytics_module_with_routers diff --git a/app/modules/analytics/definition.py b/app/modules/analytics/definition.py index 796a4968..4eb715f6 100644 --- a/app/modules/analytics/definition.py +++ b/app/modules/analytics/definition.py @@ -6,7 +6,12 @@ Defines the analytics module including its features, menu items, route configurations, and self-contained module settings. """ -from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition +from app.modules.base import ( + MenuItemDefinition, + MenuSectionDefinition, + ModuleDefinition, + PermissionDefinition, +) from app.modules.enums import FrontendType @@ -26,7 +31,9 @@ def _get_store_page_router(): def _get_feature_provider(): """Lazy import of feature provider to avoid circular imports.""" - from app.modules.analytics.services.analytics_features import analytics_feature_provider + from app.modules.analytics.services.analytics_features import ( + analytics_feature_provider, + ) return analytics_feature_provider diff --git a/app/modules/analytics/routes/__init__.py b/app/modules/analytics/routes/__init__.py index b08fa128..2fe42a11 100644 --- a/app/modules/analytics/routes/__init__.py +++ b/app/modules/analytics/routes/__init__.py @@ -24,7 +24,7 @@ def __getattr__(name: str): if name == "store_api_router": from app.modules.analytics.routes.api import store_router return store_router - elif name == "store_page_router": + if name == "store_page_router": from app.modules.analytics.routes.pages import store_router return store_router raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/app/modules/analytics/routes/api/store.py b/app/modules/analytics/routes/api/store.py index 2adfeaff..9796f130 100644 --- a/app/modules/analytics/routes/api/store.py +++ b/app/modules/analytics/routes/api/store.py @@ -16,14 +16,14 @@ from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session from app.api.deps import get_current_store_api, get_db, require_module_access -from app.modules.billing.dependencies.feature_gate import RequireFeature -from app.modules.analytics.services import stats_service from app.modules.analytics.schemas import ( StoreAnalyticsCatalog, StoreAnalyticsImports, StoreAnalyticsInventory, StoreAnalyticsResponse, ) +from app.modules.analytics.services import stats_service +from app.modules.billing.dependencies.feature_gate import RequireFeature from app.modules.enums import FrontendType from app.modules.tenancy.models import User diff --git a/app/modules/analytics/routes/pages/admin.py b/app/modules/analytics/routes/pages/admin.py index 30b82f9a..c1985196 100644 --- a/app/modules/analytics/routes/pages/admin.py +++ b/app/modules/analytics/routes/pages/admin.py @@ -14,9 +14,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access from app.modules.core.utils.page_context import get_admin_context -from app.templates_config import templates from app.modules.enums import FrontendType from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/analytics/routes/pages/store.py b/app/modules/analytics/routes/pages/store.py index 22e41588..81126baf 100644 --- a/app/modules/analytics/routes/pages/store.py +++ b/app/modules/analytics/routes/pages/store.py @@ -12,10 +12,11 @@ from fastapi.responses import HTMLResponse from sqlalchemy.orm import Session from app.api.deps import get_current_store_from_cookie_or_header, get_db -from app.modules.core.services.platform_settings_service import platform_settings_service # noqa: MOD-004 - shared platform service +from app.modules.core.services.platform_settings_service import ( + platform_settings_service, # noqa: MOD-004 - shared platform service +) +from app.modules.tenancy.models import Store, User from app.templates_config import templates -from app.modules.tenancy.models import User -from app.modules.tenancy.models import Store logger = logging.getLogger(__name__) diff --git a/app/modules/analytics/schemas/__init__.py b/app/modules/analytics/schemas/__init__.py index 9fd08af6..8bcefddc 100644 --- a/app/modules/analytics/schemas/__init__.py +++ b/app/modules/analytics/schemas/__init__.py @@ -6,29 +6,29 @@ This is the canonical location for analytics schemas. """ from app.modules.analytics.schemas.stats import ( - StatsResponse, - MarketplaceStatsResponse, - ImportStatsResponse, - UserStatsResponse, - StoreStatsResponse, - ProductStatsResponse, - PlatformStatsResponse, - OrderStatsBasicResponse, AdminDashboardResponse, - StoreProductStats, - StoreOrderStats, - StoreCustomerStats, - StoreRevenueStats, - StoreInfo, - StoreDashboardStatsResponse, - StoreAnalyticsImports, - StoreAnalyticsCatalog, - StoreAnalyticsInventory, - StoreAnalyticsResponse, - ValidatorStats, CodeQualityDashboardStatsResponse, CustomerStatsResponse, + ImportStatsResponse, + MarketplaceStatsResponse, + OrderStatsBasicResponse, OrderStatsResponse, + PlatformStatsResponse, + ProductStatsResponse, + StatsResponse, + StoreAnalyticsCatalog, + StoreAnalyticsImports, + StoreAnalyticsInventory, + StoreAnalyticsResponse, + StoreCustomerStats, + StoreDashboardStatsResponse, + StoreInfo, + StoreOrderStats, + StoreProductStats, + StoreRevenueStats, + StoreStatsResponse, + UserStatsResponse, + ValidatorStats, ) __all__ = [ diff --git a/app/modules/analytics/schemas/stats.py b/app/modules/analytics/schemas/stats.py index 5d63da39..a8106bbc 100644 --- a/app/modules/analytics/schemas/stats.py +++ b/app/modules/analytics/schemas/stats.py @@ -23,7 +23,6 @@ from app.modules.core.schemas.dashboard import ( PlatformStatsResponse, ProductStatsResponse, StatsResponse, - UserStatsResponse, StoreCustomerStats, StoreDashboardStatsResponse, StoreInfo, @@ -31,9 +30,9 @@ from app.modules.core.schemas.dashboard import ( StoreProductStats, StoreRevenueStats, StoreStatsResponse, + UserStatsResponse, ) - # ============================================================================ # Store Analytics (Analytics-specific, not in core) # ============================================================================ diff --git a/app/modules/analytics/services/__init__.py b/app/modules/analytics/services/__init__.py index 1832ff70..8ab28cce 100644 --- a/app/modules/analytics/services/__init__.py +++ b/app/modules/analytics/services/__init__.py @@ -6,8 +6,8 @@ This is the canonical location for analytics services. """ from app.modules.analytics.services.stats_service import ( - stats_service, StatsService, + stats_service, ) __all__ = [ diff --git a/app/modules/analytics/services/analytics_features.py b/app/modules/analytics/services/analytics_features.py index cb6f7272..1b6c24bf 100644 --- a/app/modules/analytics/services/analytics_features.py +++ b/app/modules/analytics/services/analytics_features.py @@ -12,11 +12,8 @@ from __future__ import annotations import logging from typing import TYPE_CHECKING -from sqlalchemy import func - from app.modules.contracts.features import ( FeatureDeclaration, - FeatureProviderProtocol, FeatureScope, FeatureType, FeatureUsage, diff --git a/app/modules/analytics/services/stats_service.py b/app/modules/analytics/services/stats_service.py index 935cfb21..03f49362 100644 --- a/app/modules/analytics/services/stats_service.py +++ b/app/modules/analytics/services/stats_service.py @@ -18,14 +18,16 @@ from typing import Any from sqlalchemy import func from sqlalchemy.orm import Session -from app.modules.tenancy.exceptions import AdminOperationException, StoreNotFoundException +from app.modules.catalog.models import Product from app.modules.customers.models.customer import Customer from app.modules.inventory.models import Inventory from app.modules.marketplace.models import MarketplaceImportJob, MarketplaceProduct from app.modules.orders.models import Order -from app.modules.catalog.models import Product -from app.modules.tenancy.models import User -from app.modules.tenancy.models import Store +from app.modules.tenancy.exceptions import ( + AdminOperationException, + StoreNotFoundException, +) +from app.modules.tenancy.models import Store, User logger = logging.getLogger(__name__) diff --git a/app/modules/base.py b/app/modules/base.py index d2645ed3..3d148de7 100644 --- a/app/modules/base.py +++ b/app/modules/base.py @@ -36,9 +36,10 @@ Self-Contained Module Structure: └── locales/ # Translation files """ +from collections.abc import Callable from dataclasses import dataclass, field from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from fastapi import APIRouter @@ -52,7 +53,6 @@ if TYPE_CHECKING: from app.modules.enums import FrontendType - # ============================================================================= # Menu Item Definitions # ============================================================================= @@ -805,10 +805,9 @@ class ModuleDefinition: """ if self.is_core: return "core" - elif self.is_internal: + if self.is_internal: return "internal" - else: - return "optional" + return "optional" # ========================================================================= # Context Provider Methods diff --git a/app/modules/billing/__init__.py b/app/modules/billing/__init__.py index db870f67..c66f1d48 100644 --- a/app/modules/billing/__init__.py +++ b/app/modules/billing/__init__.py @@ -39,7 +39,7 @@ def __getattr__(name: str): if name == "billing_module": from app.modules.billing.definition import billing_module return billing_module - elif name == "get_billing_module_with_routers": + if name == "get_billing_module_with_routers": from app.modules.billing.definition import get_billing_module_with_routers return get_billing_module_with_routers raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/app/modules/billing/definition.py b/app/modules/billing/definition.py index 41d1bd54..f8e665ca 100644 --- a/app/modules/billing/definition.py +++ b/app/modules/billing/definition.py @@ -9,7 +9,13 @@ route configurations, and scheduled tasks. import logging from typing import Any -from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition, ScheduledTask +from app.modules.base import ( + MenuItemDefinition, + MenuSectionDefinition, + ModuleDefinition, + PermissionDefinition, + ScheduledTask, +) from app.modules.enums import FrontendType logger = logging.getLogger(__name__) diff --git a/app/modules/billing/dependencies/__init__.py b/app/modules/billing/dependencies/__init__.py index 1d5c1f15..c4897712 100644 --- a/app/modules/billing/dependencies/__init__.py +++ b/app/modules/billing/dependencies/__init__.py @@ -2,9 +2,9 @@ """FastAPI dependencies for the billing module.""" from .feature_gate import ( - require_feature, - RequireFeature, FeatureNotAvailableError, + RequireFeature, + require_feature, ) __all__ = [ diff --git a/app/modules/billing/dependencies/feature_gate.py b/app/modules/billing/dependencies/feature_gate.py index bcd7233d..20750a62 100644 --- a/app/modules/billing/dependencies/feature_gate.py +++ b/app/modules/billing/dependencies/feature_gate.py @@ -37,7 +37,7 @@ Usage: import asyncio import functools import logging -from typing import Callable +from collections.abc import Callable from fastapi import Depends, HTTPException from sqlalchemy.orm import Session @@ -106,7 +106,7 @@ class RequireFeature: for feature_code in self.feature_codes: if feature_service.has_feature_for_store(db, store_id, feature_code): - return None + return # None of the features are available feature_code = self.feature_codes[0] @@ -204,8 +204,7 @@ def require_feature(*feature_codes: str) -> Callable: if asyncio.iscoroutinefunction(func): return async_wrapper - else: - return sync_wrapper + return sync_wrapper return decorator diff --git a/app/modules/billing/migrations/versions/billing_001_initial.py b/app/modules/billing/migrations/versions/billing_001_initial.py index eaa49168..c1ef20aa 100644 --- a/app/modules/billing/migrations/versions/billing_001_initial.py +++ b/app/modules/billing/migrations/versions/billing_001_initial.py @@ -4,9 +4,10 @@ Revision ID: billing_001 Revises: core_001 Create Date: 2026-02-07 """ -from alembic import op import sqlalchemy as sa +from alembic import op + revision = "billing_001" down_revision = "core_001" branch_labels = None diff --git a/app/modules/billing/models/subscription.py b/app/modules/billing/models/subscription.py index afe2708c..2e60a992 100644 --- a/app/modules/billing/models/subscription.py +++ b/app/modules/billing/models/subscription.py @@ -14,7 +14,6 @@ Feature limits per tier are in tier_feature_limit.py. """ import enum -from datetime import UTC, datetime from sqlalchemy import ( Boolean, diff --git a/app/modules/billing/routes/api/admin.py b/app/modules/billing/routes/api/admin.py index 203a3785..35a54431 100644 --- a/app/modules/billing/routes/api/admin.py +++ b/app/modules/billing/routes/api/admin.py @@ -17,8 +17,6 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api, require_module_access from app.core.database import get_db from app.exceptions import ResourceNotFoundException -from app.modules.billing.services import admin_subscription_service, subscription_service -from app.modules.enums import FrontendType from app.modules.billing.schemas import ( BillingHistoryListResponse, BillingHistoryWithMerchant, @@ -33,6 +31,11 @@ from app.modules.billing.schemas import ( SubscriptionTierResponse, SubscriptionTierUpdate, ) +from app.modules.billing.services import ( + admin_subscription_service, + subscription_service, +) +from app.modules.enums import FrontendType from models.schema.auth import UserContext logger = logging.getLogger(__name__) diff --git a/app/modules/billing/routes/api/admin_features.py b/app/modules/billing/routes/api/admin_features.py index 155cd975..7a7e1c06 100644 --- a/app/modules/billing/routes/api/admin_features.py +++ b/app/modules/billing/routes/api/admin_features.py @@ -17,16 +17,19 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api, require_module_access from app.core.database import get_db -from app.modules.billing.services.feature_aggregator import feature_aggregator -from app.modules.billing.models.tier_feature_limit import TierFeatureLimit, MerchantFeatureOverride from app.modules.billing.models import SubscriptionTier +from app.modules.billing.models.tier_feature_limit import ( + MerchantFeatureOverride, + TierFeatureLimit, +) from app.modules.billing.schemas import ( - FeatureDeclarationResponse, FeatureCatalogResponse, - TierFeatureLimitEntry, + FeatureDeclarationResponse, MerchantFeatureOverrideEntry, MerchantFeatureOverrideResponse, + TierFeatureLimitEntry, ) +from app.modules.billing.services.feature_aggregator import feature_aggregator from app.modules.enums import FrontendType from models.schema.auth import UserContext diff --git a/app/modules/billing/routes/api/merchant.py b/app/modules/billing/routes/api/merchant.py index 5ea5c7d0..14332319 100644 --- a/app/modules/billing/routes/api/merchant.py +++ b/app/modules/billing/routes/api/merchant.py @@ -8,9 +8,9 @@ Provides subscription management and billing operations for merchant owners: - Stripe checkout session creation - Invoice history -Authentication: merchant_token cookie or Authorization header. +Authentication: Authorization header (API-only, no cookies for CSRF safety). The user must own at least one active merchant (validated by -get_current_merchant_from_cookie_or_header). +get_merchant_for_current_user). Auto-discovered by the route system (merchant.py in routes/api/ triggers registration under /api/v1/merchants/billing/*). @@ -18,22 +18,26 @@ registration under /api/v1/merchants/billing/*). import logging -from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request -from pydantic import BaseModel +from fastapi import APIRouter, Depends, Path, Query, Request from sqlalchemy.orm import Session -from app.api.deps import get_current_merchant_from_cookie_or_header +from app.api.deps import get_merchant_for_current_user from app.core.database import get_db from app.modules.billing.schemas import ( + ChangeTierRequest, + ChangeTierResponse, CheckoutRequest, CheckoutResponse, + MerchantPortalAvailableTiersResponse, + MerchantPortalInvoiceListResponse, + MerchantPortalSubscriptionDetailResponse, + MerchantPortalSubscriptionItem, + MerchantPortalSubscriptionListResponse, MerchantSubscriptionResponse, TierInfo, ) from app.modules.billing.services.billing_service import billing_service from app.modules.billing.services.subscription_service import subscription_service -from app.modules.tenancy.models import Merchant -from models.schema.auth import UserContext logger = logging.getLogger(__name__) @@ -44,49 +48,15 @@ ROUTE_CONFIG = { router = APIRouter() -# ============================================================================ -# Helpers -# ============================================================================ - - -def _get_user_merchant(db: Session, user_context: UserContext) -> Merchant: - """ - Get the first active merchant owned by the current user. - - Args: - db: Database session - user_context: Authenticated user context - - Returns: - Merchant: The user's active merchant - - Raises: - HTTPException 404: If the user has no active merchants - """ - merchant = ( - db.query(Merchant) - .filter( - Merchant.owner_user_id == user_context.id, - Merchant.is_active == True, # noqa: E712 - ) - .first() - ) - - if not merchant: - raise HTTPException(status_code=404, detail="No active merchant found") - - return merchant - - # ============================================================================ # Subscription Endpoints # ============================================================================ -@router.get("/subscriptions") +@router.get("/subscriptions", response_model=MerchantPortalSubscriptionListResponse) def list_merchant_subscriptions( request: Request, - current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header), + merchant=Depends(get_merchant_for_current_user), db: Session = Depends(get_db), ): """ @@ -95,7 +65,6 @@ def list_merchant_subscriptions( Returns subscriptions across all platforms the merchant is subscribed to, including tier information and status. """ - merchant = _get_user_merchant(db, current_user) subscriptions = subscription_service.get_merchant_subscriptions(db, merchant.id) items = [] @@ -104,16 +73,21 @@ def list_merchant_subscriptions( data["tier"] = sub.tier.code if sub.tier else None data["tier_name"] = sub.tier.name if sub.tier else None data["platform_name"] = sub.platform.name if sub.platform else "" - items.append(data) + items.append(MerchantPortalSubscriptionItem(**data)) - return {"subscriptions": items, "total": len(items)} + return MerchantPortalSubscriptionListResponse( + subscriptions=items, total=len(items) + ) -@router.get("/subscriptions/{platform_id}") +@router.get( + "/subscriptions/{platform_id}", + response_model=MerchantPortalSubscriptionDetailResponse, +) def get_merchant_subscription( request: Request, platform_id: int = Path(..., description="Platform ID"), - current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header), + merchant=Depends(get_merchant_for_current_user), db: Session = Depends(get_db), ): """ @@ -121,21 +95,25 @@ def get_merchant_subscription( Returns the subscription with tier information for the given platform. """ - merchant = _get_user_merchant(db, current_user) subscription = subscription_service.get_merchant_subscription( db, merchant.id, platform_id ) if not subscription: - raise HTTPException( - status_code=404, - detail=f"No subscription found for platform {platform_id}", + from app.exceptions.base import ResourceNotFoundException + + raise ResourceNotFoundException( + resource_type="Subscription", + identifier=f"merchant={merchant.id}, platform={platform_id}", + error_code="SUBSCRIPTION_NOT_FOUND", ) sub_data = MerchantSubscriptionResponse.model_validate(subscription).model_dump() sub_data["tier"] = subscription.tier.code if subscription.tier else None sub_data["tier_name"] = subscription.tier.name if subscription.tier else None - sub_data["platform_name"] = subscription.platform.name if subscription.platform else "" + sub_data["platform_name"] = ( + subscription.platform.name if subscription.platform else "" + ) tier_info = None if subscription.tier: @@ -146,20 +124,25 @@ def get_merchant_subscription( description=tier.description, price_monthly_cents=tier.price_monthly_cents, price_annual_cents=tier.price_annual_cents, - feature_codes=tier.get_feature_codes() if hasattr(tier, "get_feature_codes") else [], + feature_codes=( + tier.get_feature_codes() if hasattr(tier, "get_feature_codes") else [] + ), ) - return { - "subscription": sub_data, - "tier": tier_info, - } + return MerchantPortalSubscriptionDetailResponse( + subscription=MerchantPortalSubscriptionItem(**sub_data), + tier=tier_info, + ) -@router.get("/subscriptions/{platform_id}/tiers") +@router.get( + "/subscriptions/{platform_id}/tiers", + response_model=MerchantPortalAvailableTiersResponse, +) def get_available_tiers( request: Request, platform_id: int = Path(..., description="Platform ID"), - current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header), + merchant=Depends(get_merchant_for_current_user), db: Session = Depends(get_db), ): """ @@ -168,7 +151,6 @@ def get_available_tiers( Returns all public tiers with upgrade/downgrade flags relative to the merchant's current tier. """ - merchant = _get_user_merchant(db, current_user) subscription = subscription_service.get_merchant_subscription( db, merchant.id, platform_id ) @@ -182,25 +164,21 @@ def get_available_tiers( if subscription and subscription.tier: current_tier_code = subscription.tier.code - return { - "tiers": tier_list, - "current_tier": current_tier_code, - } + return MerchantPortalAvailableTiersResponse( + tiers=tier_list, + current_tier=current_tier_code, + ) -class ChangeTierRequest(BaseModel): - """Request for changing subscription tier.""" - - tier_code: str - is_annual: bool = False - - -@router.post("/subscriptions/{platform_id}/change-tier") +@router.post( + "/subscriptions/{platform_id}/change-tier", + response_model=ChangeTierResponse, +) def change_subscription_tier( request: Request, tier_data: ChangeTierRequest, platform_id: int = Path(..., description="Platform ID"), - current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header), + merchant=Depends(get_merchant_for_current_user), db: Session = Depends(get_db), ): """ @@ -208,7 +186,6 @@ def change_subscription_tier( Handles both Stripe-connected and non-Stripe subscriptions. """ - merchant = _get_user_merchant(db, current_user) result = billing_service.change_tier( db, merchant.id, platform_id, tier_data.tier_code, tier_data.is_annual ) @@ -230,7 +207,7 @@ def create_checkout_session( request: Request, checkout_data: CheckoutRequest, platform_id: int = Path(..., description="Platform ID"), - current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header), + merchant=Depends(get_merchant_for_current_user), db: Session = Depends(get_db), ): """ @@ -239,12 +216,14 @@ def create_checkout_session( Starts a new subscription or upgrades an existing one to the requested tier. """ - merchant = _get_user_merchant(db, current_user) - # Build success/cancel URLs from request base_url = str(request.base_url).rstrip("/") - success_url = f"{base_url}/merchants/billing/subscriptions/{platform_id}?checkout=success" - cancel_url = f"{base_url}/merchants/billing/subscriptions/{platform_id}?checkout=cancelled" + success_url = ( + f"{base_url}/merchants/billing/subscriptions/{platform_id}?checkout=success" + ) + cancel_url = ( + f"{base_url}/merchants/billing/subscriptions/{platform_id}?checkout=cancelled" + ) result = billing_service.create_checkout_session( db=db, @@ -274,12 +253,12 @@ def create_checkout_session( # ============================================================================ -@router.get("/invoices") +@router.get("/invoices", response_model=MerchantPortalInvoiceListResponse) def get_invoices( request: Request, skip: int = Query(0, ge=0, description="Number of records to skip"), limit: int = Query(20, ge=1, le=100, description="Max records to return"), - current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header), + merchant=Depends(get_merchant_for_current_user), db: Session = Depends(get_db), ): """ @@ -287,14 +266,12 @@ def get_invoices( Returns paginated billing history entries ordered by date descending. """ - merchant = _get_user_merchant(db, current_user) - invoices, total = billing_service.get_invoices( db, merchant.id, skip=skip, limit=limit ) - return { - "invoices": [ + return MerchantPortalInvoiceListResponse( + invoices=[ { "id": inv.id, "invoice_number": inv.invoice_number, @@ -309,11 +286,13 @@ def get_invoices( "pdf_url": inv.invoice_pdf_url, "hosted_url": inv.hosted_invoice_url, "description": inv.description, - "created_at": inv.created_at.isoformat() if inv.created_at else None, + "created_at": ( + inv.created_at.isoformat() if inv.created_at else None + ), } for inv in invoices ], - "total": total, - "skip": skip, - "limit": limit, - } + total=total, + skip=skip, + limit=limit, + ) diff --git a/app/modules/billing/routes/api/platform.py b/app/modules/billing/routes/api/platform.py index 8ad1238b..586108d6 100644 --- a/app/modules/billing/routes/api/platform.py +++ b/app/modules/billing/routes/api/platform.py @@ -14,8 +14,10 @@ from sqlalchemy.orm import Session from app.core.database import get_db from app.exceptions import ResourceNotFoundException -from app.modules.billing.services.platform_pricing_service import platform_pricing_service -from app.modules.billing.models import TierCode, SubscriptionTier +from app.modules.billing.models import SubscriptionTier, TierCode +from app.modules.billing.services.platform_pricing_service import ( + platform_pricing_service, +) router = APIRouter(prefix="/pricing") diff --git a/app/modules/billing/routes/api/store.py b/app/modules/billing/routes/api/store.py index cb523219..ce94f2ce 100644 --- a/app/modules/billing/routes/api/store.py +++ b/app/modules/billing/routes/api/store.py @@ -14,7 +14,6 @@ from pydantic import BaseModel from sqlalchemy.orm import Session from app.api.deps import get_current_store_api, require_module_access -from app.core.config import settings from app.core.database import get_db from app.modules.billing.services import billing_service, subscription_service from app.modules.enums import FrontendType @@ -218,9 +217,9 @@ def get_invoices( # ============================================================================ # Include all billing-related store sub-routers -from app.modules.billing.routes.api.store_features import store_features_router -from app.modules.billing.routes.api.store_checkout import store_checkout_router from app.modules.billing.routes.api.store_addons import store_addons_router +from app.modules.billing.routes.api.store_checkout import store_checkout_router +from app.modules.billing.routes.api.store_features import store_features_router from app.modules.billing.routes.api.store_usage import store_usage_router store_router.include_router(store_features_router, tags=["store-features"]) diff --git a/app/modules/billing/routes/api/store_checkout.py b/app/modules/billing/routes/api/store_checkout.py index d79b2a91..616850f8 100644 --- a/app/modules/billing/routes/api/store_checkout.py +++ b/app/modules/billing/routes/api/store_checkout.py @@ -22,7 +22,7 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_api, require_module_access from app.core.config import settings from app.core.database import get_db -from app.modules.billing.services import billing_service, subscription_service +from app.modules.billing.services import billing_service from app.modules.enums import FrontendType from models.schema.auth import UserContext diff --git a/app/modules/billing/routes/pages/admin.py b/app/modules/billing/routes/pages/admin.py index f1c97553..24e94920 100644 --- a/app/modules/billing/routes/pages/admin.py +++ b/app/modules/billing/routes/pages/admin.py @@ -14,9 +14,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access from app.modules.core.utils.page_context import get_admin_context -from app.templates_config import templates from app.modules.enums import FrontendType from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/billing/routes/pages/merchant.py b/app/modules/billing/routes/pages/merchant.py index 0b9174d6..40893846 100644 --- a/app/modules/billing/routes/pages/merchant.py +++ b/app/modules/billing/routes/pages/merchant.py @@ -124,7 +124,7 @@ async def merchant_subscription_detail_page( # ============================================================================ -@router.get("/billing", response_class=HTMLResponse, include_in_schema=False) +@router.get("/invoices", response_class=HTMLResponse, include_in_schema=False) async def merchant_billing_history_page( request: Request, current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header), diff --git a/app/modules/billing/routes/pages/store.py b/app/modules/billing/routes/pages/store.py index c6e70370..b5e4a555 100644 --- a/app/modules/billing/routes/pages/store.py +++ b/app/modules/billing/routes/pages/store.py @@ -13,8 +13,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_from_cookie_or_header, get_db from app.modules.core.utils.page_context import get_store_context -from app.templates_config import templates from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/billing/schemas/__init__.py b/app/modules/billing/schemas/__init__.py index 244b6490..7a2522f8 100644 --- a/app/modules/billing/schemas/__init__.py +++ b/app/modules/billing/schemas/__init__.py @@ -12,51 +12,59 @@ Usage: ) """ +from app.modules.billing.schemas.billing import ( + BillingHistoryListResponse, + # Billing History schemas + BillingHistoryResponse, + BillingHistoryWithMerchant, + # Checkout & Portal schemas + CheckoutRequest, + CheckoutResponse, + FeatureCatalogResponse, + # Feature Catalog schemas + FeatureDeclarationResponse, + # Merchant Feature Override schemas + MerchantFeatureOverrideEntry, + MerchantFeatureOverrideResponse, + MerchantSubscriptionAdminCreate, + # Merchant Subscription Admin schemas + MerchantSubscriptionAdminResponse, + MerchantSubscriptionAdminUpdate, + MerchantSubscriptionListResponse, + MerchantSubscriptionWithMerchant, + PortalSessionResponse, + # Stats schemas + SubscriptionStatsResponse, + SubscriptionTierBase, + SubscriptionTierCreate, + SubscriptionTierListResponse, + SubscriptionTierResponse, + SubscriptionTierUpdate, + # Subscription Tier Admin schemas + TierFeatureLimitEntry, +) from app.modules.billing.schemas.subscription import ( - # Tier schemas - TierFeatureLimitResponse, - TierInfo, - # Subscription schemas - MerchantSubscriptionCreate, - MerchantSubscriptionUpdate, - MerchantSubscriptionResponse, - MerchantSubscriptionStatusResponse, + ChangeTierRequest, + ChangeTierResponse, + FeatureCheckResponse, # Feature summary schemas FeatureSummaryResponse, # Limit check schemas LimitCheckResult, - FeatureCheckResponse, -) -from app.modules.billing.schemas.billing import ( - # Subscription Tier Admin schemas - TierFeatureLimitEntry, - SubscriptionTierBase, - SubscriptionTierCreate, - SubscriptionTierUpdate, - SubscriptionTierResponse, - SubscriptionTierListResponse, - # Merchant Subscription Admin schemas - MerchantSubscriptionAdminResponse, - MerchantSubscriptionWithMerchant, - MerchantSubscriptionListResponse, - MerchantSubscriptionAdminCreate, - MerchantSubscriptionAdminUpdate, - # Merchant Feature Override schemas - MerchantFeatureOverrideEntry, - MerchantFeatureOverrideResponse, - # Billing History schemas - BillingHistoryResponse, - BillingHistoryWithMerchant, - BillingHistoryListResponse, - # Checkout & Portal schemas - CheckoutRequest, - CheckoutResponse, - PortalSessionResponse, - # Stats schemas - SubscriptionStatsResponse, - # Feature Catalog schemas - FeatureDeclarationResponse, - FeatureCatalogResponse, + MerchantPortalAvailableTiersResponse, + MerchantPortalInvoiceListResponse, + MerchantPortalSubscriptionDetailResponse, + # Merchant portal schemas + MerchantPortalSubscriptionItem, + MerchantPortalSubscriptionListResponse, + # Subscription schemas + MerchantSubscriptionCreate, + MerchantSubscriptionResponse, + MerchantSubscriptionStatusResponse, + MerchantSubscriptionUpdate, + # Tier schemas + TierFeatureLimitResponse, + TierInfo, ) __all__ = [ @@ -73,6 +81,14 @@ __all__ = [ # Limit check schemas (subscription.py) "LimitCheckResult", "FeatureCheckResponse", + # Merchant portal schemas (subscription.py) + "MerchantPortalSubscriptionItem", + "MerchantPortalSubscriptionListResponse", + "MerchantPortalSubscriptionDetailResponse", + "MerchantPortalAvailableTiersResponse", + "ChangeTierRequest", + "ChangeTierResponse", + "MerchantPortalInvoiceListResponse", # Subscription Tier Admin schemas (billing.py) "TierFeatureLimitEntry", "SubscriptionTierBase", diff --git a/app/modules/billing/schemas/billing.py b/app/modules/billing/schemas/billing.py index 580c4323..79a365c4 100644 --- a/app/modules/billing/schemas/billing.py +++ b/app/modules/billing/schemas/billing.py @@ -9,7 +9,6 @@ from datetime import datetime from pydantic import BaseModel, ConfigDict, Field, field_validator - # ============================================================================ # Subscription Tier Schemas # ============================================================================ diff --git a/app/modules/billing/schemas/subscription.py b/app/modules/billing/schemas/subscription.py index c1f3c31d..017cb716 100644 --- a/app/modules/billing/schemas/subscription.py +++ b/app/modules/billing/schemas/subscription.py @@ -9,7 +9,6 @@ from datetime import datetime from pydantic import BaseModel, ConfigDict, Field - # ============================================================================ # Tier Information Schemas # ============================================================================ @@ -141,3 +140,82 @@ class FeatureCheckResponse(BaseModel): message: str | None = None +# ============================================================================ +# Merchant Portal Schemas (for merchant-facing routes) +# ============================================================================ + + +class MerchantPortalSubscriptionItem(BaseModel): + """Subscription item with tier and platform names for merchant portal list.""" + + model_config = ConfigDict(from_attributes=True) + + # Base subscription fields (mirror MerchantSubscriptionResponse) + id: int + merchant_id: int + platform_id: int + tier_id: int | None + status: str + is_annual: bool + period_start: datetime + period_end: datetime + trial_ends_at: datetime | None + stripe_customer_id: str | None = None + cancelled_at: datetime | None = None + is_active: bool + is_trial: bool + trial_days_remaining: int | None + created_at: datetime + updated_at: datetime + + # Enrichment fields + tier: str | None = None + tier_name: str | None = None + platform_name: str = "" + + +class MerchantPortalSubscriptionListResponse(BaseModel): + """Paginated subscription list for merchant portal.""" + + subscriptions: list[MerchantPortalSubscriptionItem] + total: int + + +class MerchantPortalSubscriptionDetailResponse(BaseModel): + """Subscription detail with tier info for merchant portal.""" + + subscription: MerchantPortalSubscriptionItem + tier: TierInfo | None = None + + +class MerchantPortalAvailableTiersResponse(BaseModel): + """Available tiers for a platform.""" + + tiers: list[dict] + current_tier: str | None = None + + +class ChangeTierRequest(BaseModel): + """Request for changing subscription tier.""" + + tier_code: str + is_annual: bool = False + + +class ChangeTierResponse(BaseModel): + """Response after tier change.""" + + message: str + new_tier: str | None = None + effective_immediately: bool = False + + +class MerchantPortalInvoiceListResponse(BaseModel): + """Paginated invoice list for merchant portal.""" + + invoices: list[dict] + total: int + skip: int + limit: int + + diff --git a/app/modules/billing/services/__init__.py b/app/modules/billing/services/__init__.py index b7c7ed3f..dc19b3df 100644 --- a/app/modules/billing/services/__init__.py +++ b/app/modules/billing/services/__init__.py @@ -5,13 +5,13 @@ Billing module services. Provides subscription management, Stripe integration, and admin operations. """ -from app.modules.billing.services.subscription_service import ( - SubscriptionService, - subscription_service, -) -from app.modules.billing.services.stripe_service import ( - StripeService, - stripe_service, +from app.modules.billing.exceptions import ( + BillingServiceError, + NoActiveSubscriptionError, + PaymentSystemNotConfiguredError, + StripePriceNotConfiguredError, + SubscriptionNotCancelledError, + TierNotFoundError, ) from app.modules.billing.services.admin_subscription_service import ( AdminSubscriptionService, @@ -21,34 +21,34 @@ from app.modules.billing.services.billing_service import ( BillingService, billing_service, ) -from app.modules.billing.exceptions import ( - BillingServiceError, - PaymentSystemNotConfiguredError, - TierNotFoundError, - StripePriceNotConfiguredError, - NoActiveSubscriptionError, - SubscriptionNotCancelledError, +from app.modules.billing.services.capacity_forecast_service import ( + CapacityForecastService, + capacity_forecast_service, ) from app.modules.billing.services.feature_service import ( FeatureService, feature_service, ) -from app.modules.billing.services.capacity_forecast_service import ( - CapacityForecastService, - capacity_forecast_service, -) from app.modules.billing.services.platform_pricing_service import ( PlatformPricingService, platform_pricing_service, ) +from app.modules.billing.services.stripe_service import ( + StripeService, + stripe_service, +) +from app.modules.billing.services.subscription_service import ( + SubscriptionService, + subscription_service, +) from app.modules.billing.services.usage_service import ( - UsageService, - usage_service, - UsageData, - UsageMetricData, + LimitCheckData, TierInfoData, UpgradeTierData, - LimitCheckData, + UsageData, + UsageMetricData, + UsageService, + usage_service, ) __all__ = [ diff --git a/app/modules/billing/services/admin_subscription_service.py b/app/modules/billing/services/admin_subscription_service.py index 8357a0ad..105d1b1a 100644 --- a/app/modules/billing/services/admin_subscription_service.py +++ b/app/modules/billing/services/admin_subscription_service.py @@ -324,7 +324,7 @@ class AdminSubscriptionService: .all() ) - tier_distribution = {tier_name: count for tier_name, count in tier_counts} + tier_distribution = dict(tier_counts) # Calculate MRR (Monthly Recurring Revenue) mrr_cents = 0 diff --git a/app/modules/billing/services/billing_features.py b/app/modules/billing/services/billing_features.py index 92118eb8..6d13cff2 100644 --- a/app/modules/billing/services/billing_features.py +++ b/app/modules/billing/services/billing_features.py @@ -13,7 +13,6 @@ from typing import TYPE_CHECKING from app.modules.contracts.features import ( FeatureDeclaration, - FeatureProviderProtocol, FeatureScope, FeatureType, FeatureUsage, diff --git a/app/modules/billing/services/billing_service.py b/app/modules/billing/services/billing_service.py index b3e108d3..f7db848e 100644 --- a/app/modules/billing/services/billing_service.py +++ b/app/modules/billing/services/billing_service.py @@ -15,15 +15,6 @@ from datetime import datetime from sqlalchemy.orm import Session -from app.modules.billing.services.stripe_service import stripe_service -from app.modules.billing.services.subscription_service import subscription_service -from app.modules.billing.models import ( - AddOnProduct, - BillingHistory, - MerchantSubscription, - SubscriptionTier, - StoreAddOn, -) from app.modules.billing.exceptions import ( BillingServiceError, NoActiveSubscriptionError, @@ -32,6 +23,15 @@ from app.modules.billing.exceptions import ( SubscriptionNotCancelledError, TierNotFoundError, ) +from app.modules.billing.models import ( + AddOnProduct, + BillingHistory, + MerchantSubscription, + StoreAddOn, + SubscriptionTier, +) +from app.modules.billing.services.stripe_service import stripe_service +from app.modules.billing.services.subscription_service import subscription_service logger = logging.getLogger(__name__) diff --git a/app/modules/billing/services/capacity_forecast_service.py b/app/modules/billing/services/capacity_forecast_service.py index fa4a73bb..54d8e180 100644 --- a/app/modules/billing/services/capacity_forecast_service.py +++ b/app/modules/billing/services/capacity_forecast_service.py @@ -49,7 +49,9 @@ class CapacityForecastService: Should be called by a daily background job. """ from app.modules.cms.services.media_service import media_service - from app.modules.monitoring.services.platform_health_service import platform_health_service + from app.modules.monitoring.services.platform_health_service import ( + platform_health_service, + ) now = datetime.now(UTC) today = now.replace(hour=0, minute=0, second=0, microsecond=0) @@ -234,7 +236,9 @@ class CapacityForecastService: Returns prioritized list of recommendations. """ - from app.modules.monitoring.services.platform_health_service import platform_health_service + from app.modules.monitoring.services.platform_health_service import ( + platform_health_service, + ) recommendations = [] diff --git a/app/modules/billing/services/feature_aggregator.py b/app/modules/billing/services/feature_aggregator.py index 7192d092..d51e4da9 100644 --- a/app/modules/billing/services/feature_aggregator.py +++ b/app/modules/billing/services/feature_aggregator.py @@ -229,7 +229,7 @@ class FeatureAggregatorService: if decl.scope == FeatureScope.STORE and store_id is not None: usage = self.get_store_usage(db, store_id) return usage.get(feature_code) - elif decl.scope == FeatureScope.MERCHANT and merchant_id is not None and platform_id is not None: + if decl.scope == FeatureScope.MERCHANT and merchant_id is not None and platform_id is not None: usage = self.get_merchant_usage(db, merchant_id, platform_id) return usage.get(feature_code) diff --git a/app/modules/billing/services/feature_service.py b/app/modules/billing/services/feature_service.py index 85c30a25..02409cc6 100644 --- a/app/modules/billing/services/feature_service.py +++ b/app/modules/billing/services/feature_service.py @@ -33,9 +33,8 @@ from app.modules.billing.models import ( MerchantFeatureOverride, MerchantSubscription, SubscriptionTier, - TierFeatureLimit, ) -from app.modules.contracts.features import FeatureScope, FeatureType +from app.modules.contracts.features import FeatureType logger = logging.getLogger(__name__) @@ -397,7 +396,6 @@ class FeatureService: } # Get all usage at once - store_usage = {} merchant_usage = feature_aggregator.get_merchant_usage(db, merchant_id, platform_id) summaries = [] diff --git a/app/modules/billing/services/stripe_service.py b/app/modules/billing/services/stripe_service.py index a58d63a4..b5630a68 100644 --- a/app/modules/billing/services/stripe_service.py +++ b/app/modules/billing/services/stripe_service.py @@ -11,7 +11,6 @@ Provides: """ import logging -from datetime import datetime import stripe from sqlalchemy.orm import Session @@ -22,10 +21,7 @@ from app.modules.billing.exceptions import ( WebhookVerificationException, ) from app.modules.billing.models import ( - BillingHistory, MerchantSubscription, - SubscriptionStatus, - SubscriptionTier, ) from app.modules.tenancy.models import Store diff --git a/app/modules/billing/services/subscription_service.py b/app/modules/billing/services/subscription_service.py index a0d7fb76..7024fd26 100644 --- a/app/modules/billing/services/subscription_service.py +++ b/app/modules/billing/services/subscription_service.py @@ -29,8 +29,7 @@ from datetime import UTC, datetime, timedelta from sqlalchemy.orm import Session, joinedload from app.modules.billing.exceptions import ( - SubscriptionNotFoundException, - TierLimitExceededException, # Re-exported for backward compatibility + SubscriptionNotFoundException, # Re-exported for backward compatibility ) from app.modules.billing.models import ( MerchantSubscription, diff --git a/app/modules/billing/services/usage_service.py b/app/modules/billing/services/usage_service.py index 9724c9d2..125bd655 100644 --- a/app/modules/billing/services/usage_service.py +++ b/app/modules/billing/services/usage_service.py @@ -92,7 +92,9 @@ class UsageService: self, db: Session, store_id: int ) -> MerchantSubscription | None: """Resolve store_id to MerchantSubscription.""" - from app.modules.billing.services.subscription_service import subscription_service + from app.modules.billing.services.subscription_service import ( + subscription_service, + ) return subscription_service.get_subscription_for_store(db, store_id) def get_store_usage(self, db: Session, store_id: int) -> UsageData: diff --git a/app/modules/billing/tasks/__init__.py b/app/modules/billing/tasks/__init__.py index 4efa7ef3..20e98c1b 100644 --- a/app/modules/billing/tasks/__init__.py +++ b/app/modules/billing/tasks/__init__.py @@ -12,10 +12,10 @@ Note: capture_capacity_snapshot moved to monitoring module. """ from app.modules.billing.tasks.subscription import ( - reset_period_counters, check_trial_expirations, - sync_stripe_status, cleanup_stale_subscriptions, + reset_period_counters, + sync_stripe_status, ) __all__ = [ diff --git a/app/modules/billing/tests/integration/test_admin_routes.py b/app/modules/billing/tests/integration/test_admin_routes.py index 86023941..508f9d28 100644 --- a/app/modules/billing/tests/integration/test_admin_routes.py +++ b/app/modules/billing/tests/integration/test_admin_routes.py @@ -21,7 +21,6 @@ from app.modules.billing.models import ( ) from app.modules.tenancy.models import Merchant, Platform, User - # ============================================================================ # Fixtures # ============================================================================ diff --git a/app/modules/billing/tests/integration/test_merchant_routes.py b/app/modules/billing/tests/integration/test_merchant_routes.py index 434cf444..5aa0dc12 100644 --- a/app/modules/billing/tests/integration/test_merchant_routes.py +++ b/app/modules/billing/tests/integration/test_merchant_routes.py @@ -15,7 +15,7 @@ from unittest.mock import patch import pytest -from app.api.deps import get_current_merchant_from_cookie_or_header +from app.api.deps import get_current_merchant_api, get_merchant_for_current_user from app.modules.billing.models import ( BillingHistory, MerchantSubscription, @@ -26,7 +26,6 @@ from app.modules.tenancy.models import Merchant, Platform, User from main import app from models.schema.auth import UserContext - # ============================================================================ # Fixtures # ============================================================================ @@ -156,7 +155,7 @@ def merch_invoices(db, merch_merchant): @pytest.fixture def merch_auth_headers(merch_owner, merch_merchant): - """Override auth dependency to return a UserContext for the merchant owner.""" + """Override auth dependencies to return merchant/user for the merchant owner.""" user_context = UserContext( id=merch_owner.id, email=merch_owner.email, @@ -165,13 +164,17 @@ def merch_auth_headers(merch_owner, merch_merchant): is_active=True, ) - def _override(): + def _override_merchant(): + return merch_merchant + + def _override_user(): return user_context - app.dependency_overrides[get_current_merchant_from_cookie_or_header] = _override + app.dependency_overrides[get_merchant_for_current_user] = _override_merchant + app.dependency_overrides[get_current_merchant_api] = _override_user yield {"Authorization": "Bearer fake-token"} - if get_current_merchant_from_cookie_or_header in app.dependency_overrides: - del app.dependency_overrides[get_current_merchant_from_cookie_or_header] + app.dependency_overrides.pop(get_merchant_for_current_user, None) + app.dependency_overrides.pop(get_current_merchant_api, None) # ============================================================================ diff --git a/app/modules/billing/tests/integration/test_platform_routes.py b/app/modules/billing/tests/integration/test_platform_routes.py index 5d29af77..409acd46 100644 --- a/app/modules/billing/tests/integration/test_platform_routes.py +++ b/app/modules/billing/tests/integration/test_platform_routes.py @@ -19,7 +19,6 @@ from app.modules.billing.models import ( ) from app.modules.tenancy.models import Platform - # ============================================================================ # Fixtures # ============================================================================ diff --git a/app/modules/billing/tests/integration/test_store_routes.py b/app/modules/billing/tests/integration/test_store_routes.py index fb7479e7..4673df77 100644 --- a/app/modules/billing/tests/integration/test_store_routes.py +++ b/app/modules/billing/tests/integration/test_store_routes.py @@ -27,7 +27,6 @@ from app.modules.tenancy.models import Merchant, Platform, Store, User from app.modules.tenancy.models.store import StoreUser, StoreUserType from app.modules.tenancy.models.store_platform import StorePlatform - # ============================================================================ # Fixtures # ============================================================================ diff --git a/app/modules/billing/tests/unit/test_admin_subscription_service.py b/app/modules/billing/tests/unit/test_admin_subscription_service.py index 74411309..1c7bec11 100644 --- a/app/modules/billing/tests/unit/test_admin_subscription_service.py +++ b/app/modules/billing/tests/unit/test_admin_subscription_service.py @@ -22,7 +22,6 @@ from app.modules.billing.services.admin_subscription_service import ( ) from app.modules.tenancy.models import Merchant - # ============================================================================ # Tier Management # ============================================================================ diff --git a/app/modules/billing/tests/unit/test_billing_service.py b/app/modules/billing/tests/unit/test_billing_service.py index 579bb9c8..cd9a926a 100644 --- a/app/modules/billing/tests/unit/test_billing_service.py +++ b/app/modules/billing/tests/unit/test_billing_service.py @@ -6,6 +6,13 @@ from unittest.mock import MagicMock, patch import pytest +from app.modules.billing.models import ( + AddOnProduct, + BillingHistory, + MerchantSubscription, + SubscriptionStatus, + SubscriptionTier, +) from app.modules.billing.services.billing_service import ( BillingService, NoActiveSubscriptionError, @@ -14,15 +21,6 @@ from app.modules.billing.services.billing_service import ( SubscriptionNotCancelledError, TierNotFoundError, ) -from app.modules.billing.models import ( - AddOnProduct, - BillingHistory, - MerchantSubscription, - SubscriptionStatus, - SubscriptionTier, - StoreAddOn, -) - # ============================================================================ # Tier Lookup diff --git a/app/modules/billing/tests/unit/test_capacity_forecast_service.py b/app/modules/billing/tests/unit/test_capacity_forecast_service.py index f3b01c87..258614f4 100644 --- a/app/modules/billing/tests/unit/test_capacity_forecast_service.py +++ b/app/modules/billing/tests/unit/test_capacity_forecast_service.py @@ -11,16 +11,15 @@ Tests cover: from datetime import UTC, datetime, timedelta from decimal import Decimal -from unittest.mock import MagicMock, patch import pytest +from app.modules.billing.models import CapacitySnapshot from app.modules.billing.services.capacity_forecast_service import ( INFRASTRUCTURE_SCALING, CapacityForecastService, capacity_forecast_service, ) -from app.modules.billing.models import CapacitySnapshot @pytest.mark.unit diff --git a/app/modules/billing/tests/unit/test_stripe_webhook_handler.py b/app/modules/billing/tests/unit/test_stripe_webhook_handler.py index 9ea2a4a0..ec265995 100644 --- a/app/modules/billing/tests/unit/test_stripe_webhook_handler.py +++ b/app/modules/billing/tests/unit/test_stripe_webhook_handler.py @@ -1,14 +1,13 @@ # tests/unit/services/test_stripe_webhook_handler.py """Unit tests for StripeWebhookHandler.""" -from datetime import datetime, timezone -from unittest.mock import MagicMock, patch +from datetime import UTC, datetime +from unittest.mock import MagicMock import pytest from app.handlers.stripe_webhook import StripeWebhookHandler from app.modules.billing.models import ( - BillingHistory, MerchantSubscription, StripeWebhookEvent, SubscriptionStatus, @@ -175,8 +174,8 @@ def test_subscription(db, test_store): store_id=test_store.id, tier="essential", status=SubscriptionStatus.TRIAL, - period_start=datetime.now(timezone.utc), - period_end=datetime.now(timezone.utc), + period_start=datetime.now(UTC), + period_end=datetime.now(UTC), ) db.add(subscription) db.commit() @@ -207,8 +206,8 @@ def test_active_subscription(db, test_store): status=SubscriptionStatus.ACTIVE, stripe_customer_id="cus_test123", stripe_subscription_id="sub_test123", - period_start=datetime.now(timezone.utc), - period_end=datetime.now(timezone.utc), + period_start=datetime.now(UTC), + period_end=datetime.now(UTC), ) db.add(subscription) db.commit() @@ -248,8 +247,8 @@ def mock_subscription_updated_event(): event.data.object.id = "sub_test123" event.data.object.customer = "cus_test123" event.data.object.status = "active" - event.data.object.current_period_start = int(datetime.now(timezone.utc).timestamp()) - event.data.object.current_period_end = int(datetime.now(timezone.utc).timestamp()) + event.data.object.current_period_start = int(datetime.now(UTC).timestamp()) + event.data.object.current_period_end = int(datetime.now(UTC).timestamp()) event.data.object.cancel_at_period_end = False event.data.object.items.data = [] event.data.object.metadata = {} @@ -277,7 +276,7 @@ def mock_invoice_paid_event(): event.data.object.customer = "cus_test123" event.data.object.payment_intent = "pi_test123" event.data.object.number = "INV-001" - event.data.object.created = int(datetime.now(timezone.utc).timestamp()) + event.data.object.created = int(datetime.now(UTC).timestamp()) event.data.object.subtotal = 4900 event.data.object.tax = 0 event.data.object.total = 4900 diff --git a/app/modules/billing/tests/unit/test_subscription_service.py b/app/modules/billing/tests/unit/test_subscription_service.py index 019275e8..5139ec2b 100644 --- a/app/modules/billing/tests/unit/test_subscription_service.py +++ b/app/modules/billing/tests/unit/test_subscription_service.py @@ -10,11 +10,9 @@ from app.modules.billing.models import ( MerchantSubscription, SubscriptionStatus, SubscriptionTier, - TierCode, ) from app.modules.billing.services.subscription_service import SubscriptionService - # ============================================================================ # Tier Information # ============================================================================ diff --git a/app/modules/cart/definition.py b/app/modules/cart/definition.py index a21477de..13261e89 100644 --- a/app/modules/cart/definition.py +++ b/app/modules/cart/definition.py @@ -8,7 +8,6 @@ It is session-based and does not require customer authentication. from app.modules.base import ModuleDefinition, PermissionDefinition - # ============================================================================= # Router Lazy Imports # ============================================================================= diff --git a/app/modules/cart/migrations/versions/cart_001_initial.py b/app/modules/cart/migrations/versions/cart_001_initial.py index 4366c5b9..3d631f30 100644 --- a/app/modules/cart/migrations/versions/cart_001_initial.py +++ b/app/modules/cart/migrations/versions/cart_001_initial.py @@ -4,9 +4,10 @@ Revision ID: cart_001 Revises: inventory_001 Create Date: 2026-02-07 """ -from alembic import op import sqlalchemy as sa +from alembic import op + revision = "cart_001" down_revision = "inventory_001" branch_labels = None diff --git a/app/modules/cart/routes/api/storefront.py b/app/modules/cart/routes/api/storefront.py index 241c1d42..80b9b858 100644 --- a/app/modules/cart/routes/api/storefront.py +++ b/app/modules/cart/routes/api/storefront.py @@ -15,7 +15,6 @@ from fastapi import APIRouter, Body, Depends, Path from sqlalchemy.orm import Session from app.core.database import get_db -from app.modules.cart.services import cart_service from app.modules.cart.schemas import ( AddToCartRequest, CartOperationResponse, @@ -23,8 +22,9 @@ from app.modules.cart.schemas import ( ClearCartResponse, UpdateCartItemRequest, ) -from middleware.store_context import require_store_context +from app.modules.cart.services import cart_service from app.modules.tenancy.models import Store +from middleware.store_context import require_store_context router = APIRouter() logger = logging.getLogger(__name__) diff --git a/app/modules/cart/schemas/__init__.py b/app/modules/cart/schemas/__init__.py index 2601c1a6..b30800b6 100644 --- a/app/modules/cart/schemas/__init__.py +++ b/app/modules/cart/schemas/__init__.py @@ -3,11 +3,11 @@ from app.modules.cart.schemas.cart import ( AddToCartRequest, - UpdateCartItemRequest, CartItemResponse, - CartResponse, CartOperationResponse, + CartResponse, ClearCartResponse, + UpdateCartItemRequest, ) __all__ = [ diff --git a/app/modules/cart/services/__init__.py b/app/modules/cart/services/__init__.py index c9f3be33..c2a75b17 100644 --- a/app/modules/cart/services/__init__.py +++ b/app/modules/cart/services/__init__.py @@ -1,6 +1,6 @@ # app/modules/cart/services/__init__.py """Cart module services.""" -from app.modules.cart.services.cart_service import cart_service, CartService +from app.modules.cart.services.cart_service import CartService, cart_service __all__ = ["cart_service", "CartService"] diff --git a/app/modules/cart/services/cart_service.py b/app/modules/cart/services/cart_service.py index b40a15e2..ff8ae53a 100644 --- a/app/modules/cart/services/cart_service.py +++ b/app/modules/cart/services/cart_service.py @@ -21,10 +21,10 @@ from app.modules.cart.exceptions import ( InsufficientInventoryForCartException, InvalidCartQuantityException, ) -from app.modules.catalog.exceptions import ProductNotFoundException -from app.utils.money import cents_to_euros from app.modules.cart.models.cart import CartItem +from app.modules.catalog.exceptions import ProductNotFoundException from app.modules.catalog.models import Product +from app.utils.money import cents_to_euros logger = logging.getLogger(__name__) diff --git a/app/modules/catalog/definition.py b/app/modules/catalog/definition.py index b4717af3..cb4b1cfa 100644 --- a/app/modules/catalog/definition.py +++ b/app/modules/catalog/definition.py @@ -9,7 +9,6 @@ from app.modules.base import ( ) from app.modules.enums import FrontendType - # ============================================================================= # Router Lazy Imports # ============================================================================= diff --git a/app/modules/catalog/migrations/versions/catalog_001_initial.py b/app/modules/catalog/migrations/versions/catalog_001_initial.py index be745718..9b730dcb 100644 --- a/app/modules/catalog/migrations/versions/catalog_001_initial.py +++ b/app/modules/catalog/migrations/versions/catalog_001_initial.py @@ -4,9 +4,10 @@ Revision ID: catalog_001 Revises: cms_001 Create Date: 2026-02-07 """ -from alembic import op import sqlalchemy as sa +from alembic import op + revision = "catalog_001" down_revision = "cms_001" branch_labels = None diff --git a/app/modules/catalog/routes/api/__init__.py b/app/modules/catalog/routes/api/__init__.py index b8500a66..6c5b62a9 100644 --- a/app/modules/catalog/routes/api/__init__.py +++ b/app/modules/catalog/routes/api/__init__.py @@ -19,7 +19,7 @@ def __getattr__(name: str): if name == "admin_router": from app.modules.catalog.routes.api.admin import admin_router return admin_router - elif name == "store_router": + if name == "store_router": from app.modules.catalog.routes.api.store import store_router return store_router raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/app/modules/catalog/routes/api/admin.py b/app/modules/catalog/routes/api/admin.py index bdb1d29c..b872ae94 100644 --- a/app/modules/catalog/routes/api/admin.py +++ b/app/modules/catalog/routes/api/admin.py @@ -18,9 +18,6 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api, require_module_access from app.core.database import get_db from app.modules.billing.services.subscription_service import subscription_service -from app.modules.catalog.services.store_product_service import store_product_service -from app.modules.enums import FrontendType -from models.schema.auth import UserContext from app.modules.catalog.schemas import ( CatalogStore, CatalogStoresResponse, @@ -33,6 +30,9 @@ from app.modules.catalog.schemas import ( StoreProductStats, StoreProductUpdate, ) +from app.modules.catalog.services.store_product_service import store_product_service +from app.modules.enums import FrontendType +from models.schema.auth import UserContext admin_router = APIRouter( prefix="/store-products", diff --git a/app/modules/catalog/routes/api/store.py b/app/modules/catalog/routes/api/store.py index 1e5a2764..4b5c6398 100644 --- a/app/modules/catalog/routes/api/store.py +++ b/app/modules/catalog/routes/api/store.py @@ -15,11 +15,7 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_api, require_module_access from app.core.database import get_db -from app.modules.catalog.services.product_service import product_service from app.modules.billing.services.subscription_service import subscription_service -from app.modules.catalog.services.store_product_service import store_product_service -from app.modules.enums import FrontendType -from models.schema.auth import UserContext from app.modules.catalog.schemas import ( ProductCreate, ProductDeleteResponse, @@ -31,6 +27,10 @@ from app.modules.catalog.schemas import ( StoreDirectProductCreate, StoreProductCreateResponse, ) +from app.modules.catalog.services.product_service import product_service +from app.modules.catalog.services.store_product_service import store_product_service +from app.modules.enums import FrontendType +from models.schema.auth import UserContext store_router = APIRouter( prefix="/products", diff --git a/app/modules/catalog/routes/api/storefront.py b/app/modules/catalog/routes/api/storefront.py index 2f142b30..44fca02f 100644 --- a/app/modules/catalog/routes/api/storefront.py +++ b/app/modules/catalog/routes/api/storefront.py @@ -15,14 +15,14 @@ from fastapi import APIRouter, Depends, Path, Query, Request from sqlalchemy.orm import Session from app.core.database import get_db -from app.modules.catalog.services import catalog_service from app.modules.catalog.schemas import ( ProductDetailResponse, ProductListResponse, ProductResponse, ) -from middleware.store_context import require_store_context +from app.modules.catalog.services import catalog_service from app.modules.tenancy.models import Store +from middleware.store_context import require_store_context router = APIRouter() logger = logging.getLogger(__name__) diff --git a/app/modules/catalog/routes/pages/admin.py b/app/modules/catalog/routes/pages/admin.py index 55edbd7b..c8ddc1ca 100644 --- a/app/modules/catalog/routes/pages/admin.py +++ b/app/modules/catalog/routes/pages/admin.py @@ -14,9 +14,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access from app.modules.core.utils.page_context import get_admin_context -from app.templates_config import templates from app.modules.enums import FrontendType from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/catalog/routes/pages/store.py b/app/modules/catalog/routes/pages/store.py index c04a07f5..082c23f7 100644 --- a/app/modules/catalog/routes/pages/store.py +++ b/app/modules/catalog/routes/pages/store.py @@ -13,8 +13,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_from_cookie_or_header, get_db from app.modules.core.utils.page_context import get_store_context -from app.templates_config import templates from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/catalog/schemas/__init__.py b/app/modules/catalog/schemas/__init__.py index ab644bf5..c2506d4f 100644 --- a/app/modules/catalog/schemas/__init__.py +++ b/app/modules/catalog/schemas/__init__.py @@ -3,34 +3,38 @@ from app.modules.catalog.schemas.catalog import ( ProductDetailResponse as CatalogProductDetailResponse, +) +from app.modules.catalog.schemas.catalog import ( ProductListResponse as CatalogProductListResponse, +) +from app.modules.catalog.schemas.catalog import ( ProductResponse as CatalogProductResponse, ) from app.modules.catalog.schemas.product import ( ProductCreate, - ProductUpdate, - ProductResponse, + ProductDeleteResponse, ProductDetailResponse, ProductListResponse, - ProductDeleteResponse, + ProductResponse, ProductToggleResponse, + ProductUpdate, ) from app.modules.catalog.schemas.store_product import ( + # Catalog store schemas + CatalogStore, + CatalogStoresResponse, + RemoveProductResponse, + StoreDirectProductCreate, + StoreProductCreate, + StoreProductCreateResponse, + StoreProductDetail, # List/Detail schemas StoreProductListItem, StoreProductListResponse, StoreProductStats, - StoreProductDetail, - # Catalog store schemas - CatalogStore, - CatalogStoresResponse, + StoreProductUpdate, # CRUD schemas TranslationUpdate, - StoreProductCreate, - StoreDirectProductCreate, - StoreProductUpdate, - StoreProductCreateResponse, - RemoveProductResponse, ) __all__ = [ diff --git a/app/modules/catalog/schemas/catalog.py b/app/modules/catalog/schemas/catalog.py index 70161c04..94c2b8c5 100644 --- a/app/modules/catalog/schemas/catalog.py +++ b/app/modules/catalog/schemas/catalog.py @@ -8,7 +8,7 @@ For store product management, see the products module. from datetime import datetime -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict from app.modules.inventory.schemas import InventoryLocationResponse from app.modules.marketplace.schemas import MarketplaceProductResponse diff --git a/app/modules/catalog/services/catalog_features.py b/app/modules/catalog/services/catalog_features.py index d366f2fa..72dec672 100644 --- a/app/modules/catalog/services/catalog_features.py +++ b/app/modules/catalog/services/catalog_features.py @@ -14,7 +14,6 @@ from sqlalchemy import func from app.modules.contracts.features import ( FeatureDeclaration, - FeatureProviderProtocol, FeatureScope, FeatureType, FeatureUsage, diff --git a/app/modules/catalog/services/catalog_metrics.py b/app/modules/catalog/services/catalog_metrics.py index ab947d5b..dd23e6a4 100644 --- a/app/modules/catalog/services/catalog_metrics.py +++ b/app/modules/catalog/services/catalog_metrics.py @@ -16,9 +16,8 @@ from sqlalchemy import func from sqlalchemy.orm import Session from app.modules.contracts.metrics import ( - MetricValue, MetricsContext, - MetricsProviderProtocol, + MetricValue, ) if TYPE_CHECKING: @@ -95,7 +94,7 @@ class CatalogMetricsProvider: new_products = new_products_query.count() # Products with translations - products_with_translations = ( + ( db.query(func.count(func.distinct(Product.id))) .filter(Product.store_id == store_id) .join(Product.translations) diff --git a/app/modules/catalog/services/product_service.py b/app/modules/catalog/services/product_service.py index e3f7555d..632aadd0 100644 --- a/app/modules/catalog/services/product_service.py +++ b/app/modules/catalog/services/product_service.py @@ -18,9 +18,9 @@ from app.modules.catalog.exceptions import ( ProductAlreadyExistsException, ProductNotFoundException, ) -from app.modules.marketplace.models import MarketplaceProduct from app.modules.catalog.models import Product from app.modules.catalog.schemas import ProductCreate, ProductUpdate +from app.modules.marketplace.models import MarketplaceProduct logger = logging.getLogger(__name__) diff --git a/app/modules/checkout/definition.py b/app/modules/checkout/definition.py index c7b91c62..11c19803 100644 --- a/app/modules/checkout/definition.py +++ b/app/modules/checkout/definition.py @@ -8,7 +8,6 @@ Orchestrates payment processing and order creation. from app.modules.base import ModuleDefinition, PermissionDefinition - # ============================================================================= # Router Lazy Imports # ============================================================================= diff --git a/app/modules/checkout/routes/api/storefront.py b/app/modules/checkout/routes/api/storefront.py index 6e254ad8..8138c340 100644 --- a/app/modules/checkout/routes/api/storefront.py +++ b/app/modules/checkout/routes/api/storefront.py @@ -18,7 +18,6 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_customer_api from app.core.database import get_db -from app.modules.tenancy.exceptions import StoreNotFoundException from app.modules.cart.services import cart_service from app.modules.checkout.schemas import ( CheckoutRequest, @@ -27,11 +26,14 @@ from app.modules.checkout.schemas import ( ) from app.modules.checkout.services import checkout_service from app.modules.customers.schemas import CustomerContext -from app.modules.orders.services import order_service -from app.modules.messaging.services.email_service import EmailService # noqa: MOD-004 - Core email service -from middleware.store_context import require_store_context -from app.modules.tenancy.models import Store +from app.modules.messaging.services.email_service import ( + EmailService, # noqa: MOD-004 - Core email service +) from app.modules.orders.schemas import OrderCreate, OrderResponse +from app.modules.orders.services import order_service +from app.modules.tenancy.exceptions import StoreNotFoundException +from app.modules.tenancy.models import Store +from middleware.store_context import require_store_context router = APIRouter() logger = logging.getLogger(__name__) diff --git a/app/modules/cms/definition.py b/app/modules/cms/definition.py index 98b21176..d3abe850 100644 --- a/app/modules/cms/definition.py +++ b/app/modules/cms/definition.py @@ -15,7 +15,12 @@ This is a self-contained module with: import logging from typing import Any -from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition +from app.modules.base import ( + MenuItemDefinition, + MenuSectionDefinition, + ModuleDefinition, + PermissionDefinition, +) from app.modules.enums import FrontendType logger = logging.getLogger(__name__) diff --git a/app/modules/cms/migrations/versions/cms_001_initial.py b/app/modules/cms/migrations/versions/cms_001_initial.py index ee64db47..28c53c61 100644 --- a/app/modules/cms/migrations/versions/cms_001_initial.py +++ b/app/modules/cms/migrations/versions/cms_001_initial.py @@ -4,9 +4,10 @@ Revision ID: cms_001 Revises: marketplace_001 Create Date: 2026-02-07 """ -from alembic import op import sqlalchemy as sa +from alembic import op + revision = "cms_001" down_revision = "marketplace_001" branch_labels = None diff --git a/app/modules/cms/models/content_page.py b/app/modules/cms/models/content_page.py index 6200f188..f3e253de 100644 --- a/app/modules/cms/models/content_page.py +++ b/app/modules/cms/models/content_page.py @@ -191,10 +191,9 @@ class ContentPage(Base): """Get the tier level of this page for display purposes.""" if self.is_platform_page: return "platform" - elif self.store_id is None: + if self.store_id is None: return "store_default" - else: - return "store_override" + return "store_override" def to_dict(self): """Convert to dictionary for API responses.""" diff --git a/app/modules/cms/routes/__init__.py b/app/modules/cms/routes/__init__.py index a3b3c2f1..ed292a5a 100644 --- a/app/modules/cms/routes/__init__.py +++ b/app/modules/cms/routes/__init__.py @@ -22,10 +22,10 @@ def __getattr__(name: str): if name == "admin_router": from app.modules.cms.routes.admin import admin_router return admin_router - elif name == "store_router": + if name == "store_router": from app.modules.cms.routes.store import store_router return store_router - elif name == "store_media_router": + if name == "store_media_router": from app.modules.cms.routes.store import store_media_router return store_media_router raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/app/modules/cms/routes/api/admin_content_pages.py b/app/modules/cms/routes/api/admin_content_pages.py index e445ea0f..7c4a0c5f 100644 --- a/app/modules/cms/routes/api/admin_content_pages.py +++ b/app/modules/cms/routes/api/admin_content_pages.py @@ -17,8 +17,8 @@ from app.api.deps import get_current_admin_api, get_db from app.exceptions import ValidationException from app.modules.cms.schemas import ( ContentPageCreate, - ContentPageUpdate, ContentPageResponse, + ContentPageUpdate, HomepageSectionsResponse, SectionUpdateResponse, ) diff --git a/app/modules/cms/routes/api/admin_images.py b/app/modules/cms/routes/api/admin_images.py index 379c3b62..4478bf29 100644 --- a/app/modules/cms/routes/api/admin_images.py +++ b/app/modules/cms/routes/api/admin_images.py @@ -13,9 +13,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db +from app.modules.cms.schemas.image import ImageStorageStats from app.modules.cms.services.media_service import media_service from models.schema.auth import UserContext -from app.modules.cms.schemas.image import ImageStorageStats admin_images_router = APIRouter(prefix="/images") logger = logging.getLogger(__name__) diff --git a/app/modules/cms/routes/api/admin_media.py b/app/modules/cms/routes/api/admin_media.py index 3c7485dc..65051508 100644 --- a/app/modules/cms/routes/api/admin_media.py +++ b/app/modules/cms/routes/api/admin_media.py @@ -12,14 +12,14 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db -from app.modules.cms.services.media_service import media_service -from models.schema.auth import UserContext from app.modules.cms.schemas.media import ( MediaDetailResponse, MediaItemResponse, MediaListResponse, MediaUploadResponse, ) +from app.modules.cms.services.media_service import media_service +from models.schema.auth import UserContext admin_media_router = APIRouter(prefix="/media") logger = logging.getLogger(__name__) diff --git a/app/modules/cms/routes/api/admin_store_themes.py b/app/modules/cms/routes/api/admin_store_themes.py index 512027c5..aeb0b948 100644 --- a/app/modules/cms/routes/api/admin_store_themes.py +++ b/app/modules/cms/routes/api/admin_store_themes.py @@ -18,15 +18,15 @@ from fastapi import APIRouter, Depends, Path from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api, get_db -from app.modules.cms.services.store_theme_service import store_theme_service -from models.schema.auth import UserContext from app.modules.cms.schemas.store_theme import ( + StoreThemeResponse, + StoreThemeUpdate, ThemeDeleteResponse, ThemePresetListResponse, ThemePresetResponse, - StoreThemeResponse, - StoreThemeUpdate, ) +from app.modules.cms.services.store_theme_service import store_theme_service +from models.schema.auth import UserContext admin_store_themes_router = APIRouter(prefix="/store-themes") logger = logging.getLogger(__name__) diff --git a/app/modules/cms/routes/api/store_content_pages.py b/app/modules/cms/routes/api/store_content_pages.py index f0ec17a9..802cd8b1 100644 --- a/app/modules/cms/routes/api/store_content_pages.py +++ b/app/modules/cms/routes/api/store_content_pages.py @@ -19,14 +19,16 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_api, get_db from app.modules.cms.exceptions import ContentPageNotFoundException from app.modules.cms.schemas import ( + CMSUsageResponse, + ContentPageResponse, StoreContentPageCreate, StoreContentPageUpdate, - ContentPageResponse, - CMSUsageResponse, ) from app.modules.cms.services import content_page_service -from app.modules.tenancy.services.store_service import StoreService # noqa: MOD-004 - shared platform service from app.modules.tenancy.models import User +from app.modules.tenancy.services.store_service import ( + StoreService, # noqa: MOD-004 - shared platform service +) store_service = StoreService() diff --git a/app/modules/cms/routes/api/store_media.py b/app/modules/cms/routes/api/store_media.py index 6cecba75..71b4eaaa 100644 --- a/app/modules/cms/routes/api/store_media.py +++ b/app/modules/cms/routes/api/store_media.py @@ -14,9 +14,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_api from app.core.database import get_db from app.modules.cms.exceptions import MediaOptimizationException -from app.modules.cms.services.media_service import media_service -from models.schema.auth import UserContext from app.modules.cms.schemas.media import ( + FailedFileInfo, MediaDetailResponse, MediaItemResponse, MediaListResponse, @@ -26,8 +25,9 @@ from app.modules.cms.schemas.media import ( MultipleUploadResponse, OptimizationResultResponse, UploadedFileInfo, - FailedFileInfo, ) +from app.modules.cms.services.media_service import media_service +from models.schema.auth import UserContext store_media_router = APIRouter(prefix="/media") logger = logging.getLogger(__name__) diff --git a/app/modules/cms/routes/api/storefront.py b/app/modules/cms/routes/api/storefront.py index 26b68398..97c82a00 100644 --- a/app/modules/cms/routes/api/storefront.py +++ b/app/modules/cms/routes/api/storefront.py @@ -13,8 +13,8 @@ from sqlalchemy.orm import Session from app.core.database import get_db from app.modules.cms.schemas import ( - PublicContentPageResponse, ContentPageListItem, + PublicContentPageResponse, ) from app.modules.cms.services import content_page_service diff --git a/app/modules/cms/routes/pages/admin.py b/app/modules/cms/routes/pages/admin.py index d03afa54..37b239a2 100644 --- a/app/modules/cms/routes/pages/admin.py +++ b/app/modules/cms/routes/pages/admin.py @@ -10,9 +10,9 @@ from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access -from app.templates_config import templates from app.modules.enums import FrontendType from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/cms/routes/pages/store.py b/app/modules/cms/routes/pages/store.py index d161cbbf..d27a3b50 100644 --- a/app/modules/cms/routes/pages/store.py +++ b/app/modules/cms/routes/pages/store.py @@ -13,10 +13,11 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_from_cookie_or_header, get_db from app.modules.cms.services import content_page_service -from app.modules.core.services.platform_settings_service import platform_settings_service # noqa: MOD-004 - shared platform service +from app.modules.core.services.platform_settings_service import ( + platform_settings_service, # noqa: MOD-004 - shared platform service +) +from app.modules.tenancy.models import Store, User from app.templates_config import templates -from app.modules.tenancy.models import User -from app.modules.tenancy.models import Store logger = logging.getLogger(__name__) diff --git a/app/modules/cms/schemas/__init__.py b/app/modules/cms/schemas/__init__.py index e192c164..de04a9ec 100644 --- a/app/modules/cms/schemas/__init__.py +++ b/app/modules/cms/schemas/__init__.py @@ -4,35 +4,42 @@ CMS module Pydantic schemas for API request/response validation. """ from app.modules.cms.schemas.content_page import ( + CMSUsageResponse, # Admin schemas ContentPageCreate, - ContentPageUpdate, + ContentPageListItem, ContentPageResponse, - HomepageSectionsResponse as ContentPageHomepageSectionsResponse, + ContentPageUpdate, + # Public/Shop schemas + PublicContentPageResponse, SectionUpdateResponse, # Store schemas StoreContentPageCreate, StoreContentPageUpdate, - CMSUsageResponse, - # Public/Shop schemas - PublicContentPageResponse, - ContentPageListItem, +) +from app.modules.cms.schemas.content_page import ( + HomepageSectionsResponse as ContentPageHomepageSectionsResponse, ) from app.modules.cms.schemas.homepage_sections import ( - # Translatable text - TranslatableText, + CTASection, + FeatureCard, + FeaturesSection, # Section components HeroButton, HeroSection, - FeatureCard, - FeaturesSection, - PricingSection, - CTASection, # Main structure HomepageSections, + HomepageSectionsResponse, + PricingSection, # API schemas SectionUpdateRequest, - HomepageSectionsResponse, + # Translatable text + TranslatableText, +) + +# Image schemas +from app.modules.cms.schemas.image import ( + ImageStorageStats, ) # Media schemas @@ -51,23 +58,18 @@ from app.modules.cms.schemas.media import ( UploadedFileInfo, ) -# Image schemas -from app.modules.cms.schemas.image import ( - ImageStorageStats, -) - # Theme schemas from app.modules.cms.schemas.store_theme import ( - ThemeDeleteResponse, - ThemePresetListResponse, - ThemePresetPreview, - ThemePresetResponse, StoreThemeBranding, StoreThemeColors, StoreThemeFonts, StoreThemeLayout, StoreThemeResponse, StoreThemeUpdate, + ThemeDeleteResponse, + ThemePresetListResponse, + ThemePresetPreview, + ThemePresetResponse, ) __all__ = [ diff --git a/app/modules/cms/schemas/content_page.py b/app/modules/cms/schemas/content_page.py index ccf166c0..8fd7205b 100644 --- a/app/modules/cms/schemas/content_page.py +++ b/app/modules/cms/schemas/content_page.py @@ -10,7 +10,6 @@ Schemas are organized by context: from pydantic import BaseModel, Field - # ============================================================================ # ADMIN SCHEMAS # ============================================================================ diff --git a/app/modules/cms/schemas/homepage_sections.py b/app/modules/cms/schemas/homepage_sections.py index 283c3be7..68dde068 100644 --- a/app/modules/cms/schemas/homepage_sections.py +++ b/app/modules/cms/schemas/homepage_sections.py @@ -18,8 +18,8 @@ Example JSON structure: } """ + from pydantic import BaseModel, Field -from typing import Optional class TranslatableText(BaseModel): @@ -59,13 +59,13 @@ class HeroSection(BaseModel): """Hero section configuration.""" enabled: bool = True - badge_text: Optional[TranslatableText] = None + badge_text: TranslatableText | None = None title: TranslatableText = Field(default_factory=TranslatableText) subtitle: TranslatableText = Field(default_factory=TranslatableText) background_type: str = Field( default="gradient", description="gradient, image, solid" ) - background_image: Optional[str] = None + background_image: str | None = None buttons: list[HeroButton] = Field(default_factory=list) @@ -82,7 +82,7 @@ class FeaturesSection(BaseModel): enabled: bool = True title: TranslatableText = Field(default_factory=TranslatableText) - subtitle: Optional[TranslatableText] = None + subtitle: TranslatableText | None = None features: list[FeatureCard] = Field(default_factory=list) layout: str = Field(default="grid", description="grid, list, cards") @@ -92,7 +92,7 @@ class PricingSection(BaseModel): enabled: bool = True title: TranslatableText = Field(default_factory=TranslatableText) - subtitle: Optional[TranslatableText] = None + subtitle: TranslatableText | None = None use_subscription_tiers: bool = Field( default=True, description="Pull pricing from subscription_tiers table dynamically" ) @@ -103,7 +103,7 @@ class CTASection(BaseModel): enabled: bool = True title: TranslatableText = Field(default_factory=TranslatableText) - subtitle: Optional[TranslatableText] = None + subtitle: TranslatableText | None = None buttons: list[HeroButton] = Field(default_factory=list) background_type: str = Field( default="gradient", description="gradient, image, solid" @@ -113,10 +113,10 @@ class CTASection(BaseModel): class HomepageSections(BaseModel): """Complete homepage sections structure.""" - hero: Optional[HeroSection] = None - features: Optional[FeaturesSection] = None - pricing: Optional[PricingSection] = None - cta: Optional[CTASection] = None + hero: HeroSection | None = None + features: FeaturesSection | None = None + pricing: PricingSection | None = None + cta: CTASection | None = None @classmethod def get_empty_structure(cls, languages: list[str]) -> "HomepageSections": @@ -169,6 +169,6 @@ class SectionUpdateRequest(BaseModel): class HomepageSectionsResponse(BaseModel): """Response containing all homepage sections with platform language info.""" - sections: Optional[HomepageSections] = None + sections: HomepageSections | None = None supported_languages: list[str] = Field(default_factory=lambda: ["fr", "de", "en"]) default_language: str = "fr" diff --git a/app/modules/cms/services/__init__.py b/app/modules/cms/services/__init__.py index c809d007..8acad576 100644 --- a/app/modules/cms/services/__init__.py +++ b/app/modules/cms/services/__init__.py @@ -13,15 +13,15 @@ from app.modules.cms.services.media_service import ( MediaService, media_service, ) +from app.modules.cms.services.store_email_settings_service import ( + StoreEmailSettingsService, + get_store_email_settings_service, # Deprecated: use store_email_settings_service + store_email_settings_service, +) from app.modules.cms.services.store_theme_service import ( StoreThemeService, store_theme_service, ) -from app.modules.cms.services.store_email_settings_service import ( - StoreEmailSettingsService, - store_email_settings_service, - get_store_email_settings_service, # Deprecated: use store_email_settings_service -) __all__ = [ "ContentPageService", diff --git a/app/modules/cms/services/cms_features.py b/app/modules/cms/services/cms_features.py index d0d102a1..5cb1542f 100644 --- a/app/modules/cms/services/cms_features.py +++ b/app/modules/cms/services/cms_features.py @@ -16,7 +16,6 @@ from sqlalchemy import func from app.modules.contracts.features import ( FeatureDeclaration, - FeatureProviderProtocol, FeatureScope, FeatureType, FeatureUsage, diff --git a/app/modules/cms/services/cms_metrics.py b/app/modules/cms/services/cms_metrics.py index e751d8bc..5222c608 100644 --- a/app/modules/cms/services/cms_metrics.py +++ b/app/modules/cms/services/cms_metrics.py @@ -15,9 +15,8 @@ from sqlalchemy import func from sqlalchemy.orm import Session from app.modules.contracts.metrics import ( - MetricValue, MetricsContext, - MetricsProviderProtocol, + MetricValue, ) if TYPE_CHECKING: diff --git a/app/modules/cms/services/content_page_service.py b/app/modules/cms/services/content_page_service.py index 73553ae5..690cefe2 100644 --- a/app/modules/cms/services/content_page_service.py +++ b/app/modules/cms/services/content_page_service.py @@ -96,7 +96,7 @@ class ContentPageService: db.query(ContentPage) .filter( and_( - ContentPage.store_id == None, + ContentPage.store_id is None, ContentPage.is_platform_page == False, *base_filters, ) @@ -136,7 +136,7 @@ class ContentPageService: filters = [ ContentPage.platform_id == platform_id, ContentPage.slug == slug, - ContentPage.store_id == None, + ContentPage.store_id is None, ContentPage.is_platform_page == True, ] @@ -209,7 +209,7 @@ class ContentPageService: db.query(ContentPage) .filter( and_( - ContentPage.store_id == None, + ContentPage.store_id is None, ContentPage.is_platform_page == False, *base_filters, ) @@ -252,7 +252,7 @@ class ContentPageService: """ filters = [ ContentPage.platform_id == platform_id, - ContentPage.store_id == None, + ContentPage.store_id is None, ContentPage.is_platform_page == True, ] @@ -291,7 +291,7 @@ class ContentPageService: """ filters = [ ContentPage.platform_id == platform_id, - ContentPage.store_id == None, + ContentPage.store_id is None, ContentPage.is_platform_page == False, ] @@ -760,12 +760,12 @@ class ContentPageService: if page_tier == "platform": filters.append(ContentPage.is_platform_page == True) - filters.append(ContentPage.store_id == None) + filters.append(ContentPage.store_id is None) elif page_tier == "store_default": filters.append(ContentPage.is_platform_page == False) - filters.append(ContentPage.store_id == None) + filters.append(ContentPage.store_id is None) elif page_tier == "store_override": - filters.append(ContentPage.store_id != None) + filters.append(ContentPage.store_id is not None) return ( db.query(ContentPage) @@ -942,10 +942,10 @@ class ContentPageService: ValueError: If section name is invalid """ from app.modules.cms.schemas import ( - HeroSection, - FeaturesSection, - PricingSection, CTASection, + FeaturesSection, + HeroSection, + PricingSection, ) SECTION_SCHEMAS = { diff --git a/app/modules/cms/services/media_service.py b/app/modules/cms/services/media_service.py index d96c52f0..d8e1bbef 100644 --- a/app/modules/cms/services/media_service.py +++ b/app/modules/cms/services/media_service.py @@ -11,7 +11,6 @@ This module provides: import logging import mimetypes -import os import shutil import uuid from datetime import UTC, datetime @@ -22,11 +21,10 @@ from sqlalchemy import func, or_ from sqlalchemy.orm import Session from app.modules.cms.exceptions import ( + MediaFileTooLargeException, MediaNotFoundException, - MediaUploadException, MediaValidationException, UnsupportedMediaTypeException, - MediaFileTooLargeException, ) from app.modules.cms.models import MediaFile diff --git a/app/modules/cms/services/store_email_settings_service.py b/app/modules/cms/services/store_email_settings_service.py index 10bc1b00..e5582800 100644 --- a/app/modules/cms/services/store_email_settings_service.py +++ b/app/modules/cms/services/store_email_settings_service.py @@ -20,17 +20,16 @@ from sqlalchemy.orm import Session from app.exceptions import ( AuthorizationException, + ExternalServiceException, ResourceNotFoundException, ValidationException, - ExternalServiceException, -) -from app.modules.tenancy.models import Store -from app.modules.messaging.models import ( - StoreEmailSettings, - EmailProvider, - PREMIUM_EMAIL_PROVIDERS, ) from app.modules.billing.models import TierCode +from app.modules.messaging.models import ( + PREMIUM_EMAIL_PROVIDERS, + EmailProvider, + StoreEmailSettings, +) logger = logging.getLogger(__name__) @@ -343,7 +342,7 @@ class StoreEmailSettingsService: from_email=(settings.from_email, settings.from_name), to_emails=to_email, subject="Wizamart Email Configuration Test", - html_content=f""" + html_content="""

Email Configuration Test

@@ -376,7 +375,7 @@ class StoreEmailSettingsService: "from": f"{settings.from_name} <{settings.from_email}>", "to": to_email, "subject": "Wizamart Email Configuration Test", - "html": f""" + "html": """

Email Configuration Test

@@ -421,7 +420,7 @@ class StoreEmailSettingsService: "Subject": {"Data": "Wizamart Email Configuration Test"}, "Body": { "Html": { - "Data": f""" + "Data": """

Email Configuration Test

diff --git a/app/modules/cms/services/store_theme_service.py b/app/modules/cms/services/store_theme_service.py index 7d7e0f42..132b2bb9 100644 --- a/app/modules/cms/services/store_theme_service.py +++ b/app/modules/cms/services/store_theme_service.py @@ -11,6 +11,16 @@ import re from sqlalchemy.orm import Session +from app.modules.cms.exceptions import ( + InvalidColorFormatException, + InvalidFontFamilyException, + StoreThemeNotFoundException, + ThemeOperationException, + ThemePresetNotFoundException, + ThemeValidationException, +) +from app.modules.cms.models import StoreTheme +from app.modules.cms.schemas.store_theme import StoreThemeUpdate, ThemePresetPreview from app.modules.cms.services.theme_presets import ( THEME_PRESETS, apply_preset, @@ -18,17 +28,7 @@ from app.modules.cms.services.theme_presets import ( get_preset_preview, ) from app.modules.tenancy.exceptions import StoreNotFoundException -from app.modules.cms.exceptions import ( - InvalidColorFormatException, - InvalidFontFamilyException, - ThemeOperationException, - ThemePresetNotFoundException, - ThemeValidationException, - StoreThemeNotFoundException, -) from app.modules.tenancy.models import Store -from app.modules.cms.models import StoreTheme -from app.modules.cms.schemas.store_theme import ThemePresetPreview, StoreThemeUpdate logger = logging.getLogger(__name__) diff --git a/app/modules/config.py b/app/modules/config.py index d595c5cb..dc1893c2 100644 --- a/app/modules/config.py +++ b/app/modules/config.py @@ -89,13 +89,13 @@ def discover_module_config(module_code: str) -> "BaseSettings | None": # First, try to get an instantiated config if hasattr(config_module, "config"): - config = getattr(config_module, "config") + config = config_module.config logger.debug(f"Loaded config instance for module {module_code}") return config # Otherwise, try to instantiate from config_class if hasattr(config_module, "config_class"): - config_class = getattr(config_module, "config_class") + config_class = config_module.config_class config = config_class() logger.debug(f"Instantiated config class for module {module_code}") return config @@ -121,7 +121,7 @@ def discover_all_module_configs() -> dict[str, "BaseSettings"]: Returns: Dict mapping module code to config instance """ - configs: dict[str, "BaseSettings"] = {} + configs: dict[str, BaseSettings] = {} for module_dir in sorted(MODULES_DIR.iterdir()): if not module_dir.is_dir(): diff --git a/app/modules/contracts/__init__.py b/app/modules/contracts/__init__.py index 09c31dfe..dfc4e690 100644 --- a/app/modules/contracts/__init__.py +++ b/app/modules/contracts/__init__.py @@ -59,9 +59,9 @@ from app.modules.contracts.features import ( FeatureUsage, ) from app.modules.contracts.metrics import ( - MetricValue, MetricsContext, MetricsProviderProtocol, + MetricValue, ) from app.modules.contracts.widgets import ( BreakdownWidget, diff --git a/app/modules/contracts/audit.py b/app/modules/contracts/audit.py index 8042b4d5..59676c03 100644 --- a/app/modules/contracts/audit.py +++ b/app/modules/contracts/audit.py @@ -47,7 +47,7 @@ Usage: ) """ -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable if TYPE_CHECKING: diff --git a/app/modules/contracts/base.py b/app/modules/contracts/base.py index 17a7193c..b9e96963 100644 --- a/app/modules/contracts/base.py +++ b/app/modules/contracts/base.py @@ -27,7 +27,6 @@ class ServiceProtocol(Protocol): - Easy testing with mock sessions """ - pass @runtime_checkable diff --git a/app/modules/contracts/features.py b/app/modules/contracts/features.py index c3c0c546..ca248251 100644 --- a/app/modules/contracts/features.py +++ b/app/modules/contracts/features.py @@ -57,7 +57,7 @@ Usage: """ import enum -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import TYPE_CHECKING, Protocol, runtime_checkable if TYPE_CHECKING: diff --git a/app/modules/contracts/metrics.py b/app/modules/contracts/metrics.py index 113cd538..c4c949c1 100644 --- a/app/modules/contracts/metrics.py +++ b/app/modules/contracts/metrics.py @@ -37,7 +37,7 @@ Usage: # 3. Metrics appear automatically in dashboards when module is enabled """ -from dataclasses import dataclass, field +from dataclasses import dataclass from datetime import datetime from typing import TYPE_CHECKING, Protocol, runtime_checkable diff --git a/app/modules/core/exceptions.py b/app/modules/core/exceptions.py index effc2aff..ffd0d53c 100644 --- a/app/modules/core/exceptions.py +++ b/app/modules/core/exceptions.py @@ -13,22 +13,18 @@ from app.exceptions import WizamartException class CoreException(WizamartException): """Base exception for core module.""" - pass class MenuConfigurationError(CoreException): """Error in menu configuration.""" - pass class SettingsError(CoreException): """Error in platform settings.""" - pass class DashboardError(CoreException): """Error in dashboard operations.""" - pass diff --git a/app/modules/core/models/__init__.py b/app/modules/core/models/__init__.py index e13278c2..21ff739b 100644 --- a/app/modules/core/models/__init__.py +++ b/app/modules/core/models/__init__.py @@ -6,9 +6,9 @@ This is the canonical location for core module models. """ from app.modules.core.models.admin_menu_config import ( + MANDATORY_MENU_ITEMS, AdminMenuConfig, FrontendType, - MANDATORY_MENU_ITEMS, ) __all__ = [ diff --git a/app/modules/core/models/admin_menu_config.py b/app/modules/core/models/admin_menu_config.py index 2bd864da..0217cd81 100644 --- a/app/modules/core/models/admin_menu_config.py +++ b/app/modules/core/models/admin_menu_config.py @@ -33,10 +33,10 @@ from sqlalchemy import ( from sqlalchemy.orm import relationship from app.core.database import Base -from models.database.base import TimestampMixin # Import FrontendType and MANDATORY_MENU_ITEMS from the central location -from app.modules.enums import FrontendType, MANDATORY_MENU_ITEMS +from app.modules.enums import MANDATORY_MENU_ITEMS, FrontendType +from models.database.base import TimestampMixin class AdminMenuConfig(Base, TimestampMixin): diff --git a/app/modules/core/routes/api/admin.py b/app/modules/core/routes/api/admin.py index acecc1f3..a8ceff16 100644 --- a/app/modules/core/routes/api/admin.py +++ b/app/modules/core/routes/api/admin.py @@ -11,8 +11,8 @@ Aggregates all admin core routes: from fastapi import APIRouter from .admin_dashboard import admin_dashboard_router -from .admin_settings import admin_settings_router from .admin_menu_config import router as admin_menu_config_router +from .admin_settings import admin_settings_router admin_router = APIRouter() diff --git a/app/modules/core/routes/api/admin_dashboard.py b/app/modules/core/routes/api/admin_dashboard.py index becf7c3d..fd2dcdd8 100644 --- a/app/modules/core/routes/api/admin_dashboard.py +++ b/app/modules/core/routes/api/admin_dashboard.py @@ -29,8 +29,8 @@ from app.modules.core.schemas.dashboard import ( PlatformStatsResponse, ProductStatsResponse, StatsResponse, - UserStatsResponse, StoreStatsResponse, + UserStatsResponse, ) from app.modules.core.services.stats_aggregator import stats_aggregator from app.modules.core.services.widget_aggregator import widget_aggregator diff --git a/app/modules/core/routes/api/admin_menu_config.py b/app/modules/core/routes/api/admin_menu_config.py index ef067a7c..725e2313 100644 --- a/app/modules/core/routes/api/admin_menu_config.py +++ b/app/modules/core/routes/api/admin_menu_config.py @@ -28,9 +28,9 @@ from app.api.deps import ( get_db, ) from app.modules.core.services.menu_service import MenuItemConfig, menu_service -from app.modules.tenancy.services.platform_service import platform_service from app.modules.enums import FrontendType # noqa: API-007 - Enum for type safety -from app.utils.i18n import translate, DEFAULT_LANGUAGE +from app.modules.tenancy.services.platform_service import platform_service +from app.utils.i18n import DEFAULT_LANGUAGE, translate from models.schema.auth import UserContext logger = logging.getLogger(__name__) diff --git a/app/modules/core/routes/api/admin_settings.py b/app/modules/core/routes/api/admin_settings.py index f17698b1..ce1f5bfa 100644 --- a/app/modules/core/routes/api/admin_settings.py +++ b/app/modules/core/routes/api/admin_settings.py @@ -19,10 +19,9 @@ from app.api.deps import get_current_admin_api from app.core.config import settings as app_settings from app.core.database import get_db from app.exceptions import ResourceNotFoundException -from app.modules.tenancy.exceptions import ConfirmationRequiredException from app.modules.core.services.admin_settings_service import admin_settings_service from app.modules.core.services.audit_aggregator import audit_aggregator -from models.schema.auth import UserContext +from app.modules.tenancy.exceptions import ConfirmationRequiredException from app.modules.tenancy.schemas.admin import ( AdminSettingCreate, AdminSettingDefaultResponse, @@ -33,6 +32,7 @@ from app.modules.tenancy.schemas.admin import ( RowsPerPageResponse, RowsPerPageUpdateResponse, ) +from models.schema.auth import UserContext admin_settings_router = APIRouter(prefix="/settings") logger = logging.getLogger(__name__) @@ -664,7 +664,7 @@ def send_test_email( to_email=request.to_email, to_name=None, subject="Wizamart Platform - Test Email", - body_html=""" + body_html=f"""

Test Email from Wizamart

@@ -672,15 +672,12 @@ def send_test_email(

If you received this email, your email settings are working correctly!


- Provider: {provider}
- From: {from_email} + Provider: {app_settings.email_provider}
+ From: {app_settings.email_from_address}

- """.format( - provider=app_settings.email_provider, - from_email=app_settings.email_from_address, - ), + """, body_text=f"Test email from Wizamart platform.\n\nProvider: {app_settings.email_provider}\nFrom: {app_settings.email_from_address}", is_platform_email=True, ) @@ -702,11 +699,10 @@ def send_test_email( success=True, message=f"Test email sent to {request.to_email}", ) - else: - return TestEmailResponse( - success=False, - message=email_log.error_message or "Failed to send test email. Check server logs for details.", - ) + return TestEmailResponse( + success=False, + message=email_log.error_message or "Failed to send test email. Check server logs for details.", + ) except Exception as e: logger.error(f"Failed to send test email: {e}") diff --git a/app/modules/core/routes/api/store_settings.py b/app/modules/core/routes/api/store_settings.py index 5f1960e4..091cf624 100644 --- a/app/modules/core/routes/api/store_settings.py +++ b/app/modules/core/routes/api/store_settings.py @@ -14,7 +14,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_api from app.core.database import get_db -from app.modules.core.services.platform_settings_service import platform_settings_service +from app.modules.core.services.platform_settings_service import ( + platform_settings_service, +) from app.modules.tenancy.services.store_service import store_service from models.schema.auth import UserContext diff --git a/app/modules/core/routes/pages/admin.py b/app/modules/core/routes/pages/admin.py index 4487d2d7..c750fe81 100644 --- a/app/modules/core/routes/pages/admin.py +++ b/app/modules/core/routes/pages/admin.py @@ -15,9 +15,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_optional, get_db, require_menu_access from app.modules.core.utils.page_context import get_admin_context -from app.templates_config import templates from app.modules.enums import FrontendType from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/core/routes/pages/merchant.py b/app/modules/core/routes/pages/merchant.py index 550afbdc..930f4b85 100644 --- a/app/modules/core/routes/pages/merchant.py +++ b/app/modules/core/routes/pages/merchant.py @@ -15,7 +15,11 @@ from fastapi import APIRouter, Depends, Request from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy.orm import Session -from app.api.deps import get_current_merchant_from_cookie_or_header, get_current_merchant_optional, get_db +from app.api.deps import ( + get_current_merchant_from_cookie_or_header, + get_current_merchant_optional, + get_db, +) from app.modules.core.utils.page_context import get_context_for_frontend from app.modules.enums import FrontendType from app.templates_config import templates diff --git a/app/modules/core/routes/pages/store.py b/app/modules/core/routes/pages/store.py index 4c190346..0a5f9603 100644 --- a/app/modules/core/routes/pages/store.py +++ b/app/modules/core/routes/pages/store.py @@ -13,8 +13,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_from_cookie_or_header, get_db from app.modules.core.utils.page_context import get_store_context -from app.templates_config import templates from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/core/schemas/__init__.py b/app/modules/core/schemas/__init__.py index d493dee0..23947d28 100644 --- a/app/modules/core/schemas/__init__.py +++ b/app/modules/core/schemas/__init__.py @@ -11,7 +11,6 @@ from app.modules.core.schemas.dashboard import ( PlatformStatsResponse, ProductStatsResponse, StatsResponse, - UserStatsResponse, StoreCustomerStats, StoreDashboardStatsResponse, StoreInfo, @@ -19,6 +18,7 @@ from app.modules.core.schemas.dashboard import ( StoreProductStats, StoreRevenueStats, StoreStatsResponse, + UserStatsResponse, ) __all__ = [ diff --git a/app/modules/core/schemas/dashboard.py b/app/modules/core/schemas/dashboard.py index 9a78aaa6..be5ed065 100644 --- a/app/modules/core/schemas/dashboard.py +++ b/app/modules/core/schemas/dashboard.py @@ -14,7 +14,6 @@ from typing import Any from pydantic import BaseModel, Field - # ============================================================================ # User Statistics # ============================================================================ diff --git a/app/modules/core/services/__init__.py b/app/modules/core/services/__init__.py index 6fe9e01d..0a7e68e2 100644 --- a/app/modules/core/services/__init__.py +++ b/app/modules/core/services/__init__.py @@ -15,13 +15,17 @@ from app.modules.core.services.admin_settings_service import ( admin_settings_service, ) from app.modules.core.services.auth_service import AuthService, auth_service -from app.modules.core.services.menu_service import MenuItemConfig, MenuService, menu_service from app.modules.core.services.menu_discovery_service import ( DiscoveredMenuItem, DiscoveredMenuSection, MenuDiscoveryService, menu_discovery_service, ) +from app.modules.core.services.menu_service import ( + MenuItemConfig, + MenuService, + menu_service, +) from app.modules.core.services.platform_settings_service import ( PlatformSettingsService, platform_settings_service, diff --git a/app/modules/core/services/auth_service.py b/app/modules/core/services/auth_service.py index 1411668e..11adf758 100644 --- a/app/modules/core/services/auth_service.py +++ b/app/modules/core/services/auth_service.py @@ -17,10 +17,12 @@ from typing import Any from sqlalchemy.orm import Session -from app.modules.tenancy.exceptions import InvalidCredentialsException, UserNotActiveException +from app.modules.tenancy.exceptions import ( + InvalidCredentialsException, + UserNotActiveException, +) +from app.modules.tenancy.models import Store, StoreUser, User from middleware.auth import AuthManager -from app.modules.tenancy.models import User -from app.modules.tenancy.models import Store, StoreUser from models.schema.auth import UserLogin logger = logging.getLogger(__name__) diff --git a/app/modules/core/services/menu_discovery_service.py b/app/modules/core/services/menu_discovery_service.py index 0017f46d..15f8158c 100644 --- a/app/modules/core/services/menu_discovery_service.py +++ b/app/modules/core/services/menu_discovery_service.py @@ -34,7 +34,7 @@ from dataclasses import dataclass, field from sqlalchemy.orm import Session -from app.modules.base import MenuItemDefinition, MenuSectionDefinition +from app.modules.base import MenuSectionDefinition from app.modules.enums import FrontendType from app.modules.service import module_service @@ -112,7 +112,7 @@ class MenuDiscoveryService: ft: [] for ft in FrontendType } - for module_code, module_def in MODULES.items(): + for _module_code, module_def in MODULES.items(): for frontend_type, sections in module_def.menus.items(): all_menus[frontend_type].extend(deepcopy(sections)) diff --git a/app/modules/core/services/menu_service.py b/app/modules/core/services/menu_service.py index 9c62d22f..70909dca 100644 --- a/app/modules/core/services/menu_service.py +++ b/app/modules/core/services/menu_service.py @@ -29,15 +29,14 @@ Usage: """ import logging -from copy import deepcopy from dataclasses import dataclass from sqlalchemy.orm import Session -from app.modules.service import module_service from app.modules.core.models import AdminMenuConfig from app.modules.core.services.menu_discovery_service import menu_discovery_service from app.modules.enums import FrontendType +from app.modules.service import module_service logger = logging.getLogger(__name__) @@ -160,7 +159,7 @@ class MenuService: # Filter by module enablement if platform is specified if platform_id: - module_available_items = module_service.get_module_menu_items( + module_service.get_module_menu_items( db, platform_id, frontend_type ) # Only keep items from enabled modules (or items not associated with any module) @@ -715,8 +714,7 @@ class MenuService: ) if platform_id: return q.filter(AdminMenuConfig.platform_id == platform_id) - else: - return q.filter(AdminMenuConfig.user_id == user_id) + return q.filter(AdminMenuConfig.user_id == user_id) # Check if any visible records exist (valid opt-in config) visible_count = scope_query().filter( @@ -730,7 +728,7 @@ class MenuService: total_count = scope_query().count() if total_count > 0: # Clean up old records first - deleted = scope_query().delete(synchronize_session='fetch') + deleted = scope_query().delete(synchronize_session="fetch") db.flush() # Ensure deletes are applied before inserts logger.info(f"Cleaned up {deleted} old menu config records before initialization") diff --git a/app/modules/core/services/stats_aggregator.py b/app/modules/core/services/stats_aggregator.py index eaa26e5f..90f6aff1 100644 --- a/app/modules/core/services/stats_aggregator.py +++ b/app/modules/core/services/stats_aggregator.py @@ -32,9 +32,9 @@ from typing import TYPE_CHECKING, Any from sqlalchemy.orm import Session from app.modules.contracts.metrics import ( - MetricValue, MetricsContext, MetricsProviderProtocol, + MetricValue, ) if TYPE_CHECKING: diff --git a/app/modules/core/services/storage_service.py b/app/modules/core/services/storage_service.py index b23c9240..67aaca33 100644 --- a/app/modules/core/services/storage_service.py +++ b/app/modules/core/services/storage_service.py @@ -39,7 +39,6 @@ class StorageBackend(ABC): Returns: Public URL to access the file """ - pass @abstractmethod async def delete(self, file_path: str) -> bool: @@ -52,7 +51,6 @@ class StorageBackend(ABC): Returns: True if file was deleted, False if not found """ - pass @abstractmethod def get_url(self, file_path: str) -> str: @@ -65,7 +63,6 @@ class StorageBackend(ABC): Returns: Public URL to access the file """ - pass @abstractmethod async def exists(self, file_path: str) -> bool: @@ -78,7 +75,6 @@ class StorageBackend(ABC): Returns: True if file exists """ - pass class LocalStorageBackend(StorageBackend): @@ -227,10 +223,9 @@ class R2StorageBackend(StorageBackend): if self.public_url: # Use custom domain return f"{self.public_url.rstrip('/')}/{file_path}" - else: - # Use default R2 public URL pattern - # Note: Bucket must have public access enabled - return f"https://{self.bucket_name}.{settings.r2_account_id}.r2.dev/{file_path}" + # Use default R2 public URL pattern + # Note: Bucket must have public access enabled + return f"https://{self.bucket_name}.{settings.r2_account_id}.r2.dev/{file_path}" async def exists(self, file_path: str) -> bool: """Check if file exists in R2.""" diff --git a/app/modules/core/utils/__init__.py b/app/modules/core/utils/__init__.py index 5428b28c..04d7d393 100644 --- a/app/modules/core/utils/__init__.py +++ b/app/modules/core/utils/__init__.py @@ -2,11 +2,11 @@ """Core module utilities.""" from .page_context import ( - get_context_for_frontend, get_admin_context, + get_context_for_frontend, + get_platform_context, get_store_context, get_storefront_context, - get_platform_context, ) __all__ = [ diff --git a/app/modules/customers/__init__.py b/app/modules/customers/__init__.py index 86693dd1..12668c9d 100644 --- a/app/modules/customers/__init__.py +++ b/app/modules/customers/__init__.py @@ -25,7 +25,7 @@ def __getattr__(name: str): from app.modules.customers.definition import customers_module return customers_module - elif name == "get_customers_module_with_routers": + if name == "get_customers_module_with_routers": from app.modules.customers.definition import get_customers_module_with_routers return get_customers_module_with_routers diff --git a/app/modules/customers/definition.py b/app/modules/customers/definition.py index 53a45f8f..59b2109e 100644 --- a/app/modules/customers/definition.py +++ b/app/modules/customers/definition.py @@ -31,14 +31,18 @@ def _get_store_router(): def _get_metrics_provider(): """Lazy import of metrics provider to avoid circular imports.""" - from app.modules.customers.services.customer_metrics import customer_metrics_provider + from app.modules.customers.services.customer_metrics import ( + customer_metrics_provider, + ) return customer_metrics_provider def _get_feature_provider(): """Lazy import of feature provider to avoid circular imports.""" - from app.modules.customers.services.customer_features import customer_feature_provider + from app.modules.customers.services.customer_features import ( + customer_feature_provider, + ) return customer_feature_provider diff --git a/app/modules/customers/migrations/versions/customers_001_initial.py b/app/modules/customers/migrations/versions/customers_001_initial.py index 6793139e..3948d302 100644 --- a/app/modules/customers/migrations/versions/customers_001_initial.py +++ b/app/modules/customers/migrations/versions/customers_001_initial.py @@ -4,9 +4,10 @@ Revision ID: customers_001 Revises: catalog_001 Create Date: 2026-02-07 """ -from alembic import op import sqlalchemy as sa +from alembic import op + revision = "customers_001" down_revision = "catalog_001" branch_labels = None diff --git a/app/modules/customers/routes/__init__.py b/app/modules/customers/routes/__init__.py index e29293bf..f88c6359 100644 --- a/app/modules/customers/routes/__init__.py +++ b/app/modules/customers/routes/__init__.py @@ -22,7 +22,7 @@ def __getattr__(name: str): if name == "admin_router": from app.modules.customers.routes.admin import admin_router return admin_router - elif name == "store_router": + if name == "store_router": from app.modules.customers.routes.store import store_router return store_router raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/app/modules/customers/routes/api/__init__.py b/app/modules/customers/routes/api/__init__.py index 7f0914b4..fddfa3a8 100644 --- a/app/modules/customers/routes/api/__init__.py +++ b/app/modules/customers/routes/api/__init__.py @@ -19,7 +19,7 @@ def __getattr__(name: str): if name == "admin_router": from app.modules.customers.routes.api.admin import admin_router return admin_router - elif name == "store_router": + if name == "store_router": from app.modules.customers.routes.api.store import store_router return store_router raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/app/modules/customers/routes/api/admin.py b/app/modules/customers/routes/api/admin.py index 3cbc1010..a4919399 100644 --- a/app/modules/customers/routes/api/admin.py +++ b/app/modules/customers/routes/api/admin.py @@ -10,15 +10,15 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api, require_module_access from app.core.database import get_db -from app.modules.customers.services import admin_customer_service -from app.modules.enums import FrontendType -from models.schema.auth import UserContext from app.modules.customers.schemas import ( CustomerDetailResponse, CustomerListResponse, CustomerMessageResponse, CustomerStatisticsResponse, ) +from app.modules.customers.services import admin_customer_service +from app.modules.enums import FrontendType +from models.schema.auth import UserContext # Create module-aware router admin_router = APIRouter( diff --git a/app/modules/customers/routes/api/store.py b/app/modules/customers/routes/api/store.py index 9d754baa..95423d18 100644 --- a/app/modules/customers/routes/api/store.py +++ b/app/modules/customers/routes/api/store.py @@ -13,9 +13,6 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_api, require_module_access from app.core.database import get_db -from app.modules.customers.services import customer_service -from app.modules.enums import FrontendType -from models.schema.auth import UserContext from app.modules.customers.schemas import ( CustomerDetailResponse, CustomerMessageResponse, @@ -23,6 +20,9 @@ from app.modules.customers.schemas import ( CustomerUpdate, StoreCustomerListResponse, ) +from app.modules.customers.services import customer_service +from app.modules.enums import FrontendType +from models.schema.auth import UserContext # Create module-aware router store_router = APIRouter( diff --git a/app/modules/customers/routes/api/storefront.py b/app/modules/customers/routes/api/storefront.py index 79a6659a..de43854d 100644 --- a/app/modules/customers/routes/api/storefront.py +++ b/app/modules/customers/routes/api/storefront.py @@ -24,31 +24,35 @@ from app.api.deps import get_current_customer_api from app.core.database import get_db from app.core.environment import should_use_secure_cookies from app.exceptions import ValidationException -from app.modules.tenancy.exceptions import StoreNotFoundException -from app.modules.customers.schemas import CustomerContext -from app.modules.customers.services import ( - customer_address_service, - customer_service, +from app.modules.core.services.auth_service import ( + AuthService, # noqa: MOD-004 - Core auth service ) -from app.modules.core.services.auth_service import AuthService # noqa: MOD-004 - Core auth service -from app.modules.messaging.services.email_service import EmailService # noqa: MOD-004 - Core email service from app.modules.customers.models import PasswordResetToken -from models.schema.auth import ( - LogoutResponse, - PasswordResetRequestResponse, - PasswordResetResponse, - UserLogin, -) from app.modules.customers.schemas import ( CustomerAddressCreate, CustomerAddressListResponse, CustomerAddressResponse, CustomerAddressUpdate, + CustomerContext, CustomerPasswordChange, CustomerRegister, CustomerResponse, CustomerUpdate, ) +from app.modules.customers.services import ( + customer_address_service, + customer_service, +) +from app.modules.messaging.services.email_service import ( + EmailService, # noqa: MOD-004 - Core email service +) +from app.modules.tenancy.exceptions import StoreNotFoundException +from models.schema.auth import ( + LogoutResponse, + PasswordResetRequestResponse, + PasswordResetResponse, + UserLogin, +) router = APIRouter() logger = logging.getLogger(__name__) diff --git a/app/modules/customers/routes/pages/admin.py b/app/modules/customers/routes/pages/admin.py index f51e52bc..18fe1a6d 100644 --- a/app/modules/customers/routes/pages/admin.py +++ b/app/modules/customers/routes/pages/admin.py @@ -12,9 +12,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access from app.modules.core.utils.page_context import get_admin_context -from app.templates_config import templates from app.modules.enums import FrontendType from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/customers/routes/pages/store.py b/app/modules/customers/routes/pages/store.py index 0b1bf4a3..bd811f20 100644 --- a/app/modules/customers/routes/pages/store.py +++ b/app/modules/customers/routes/pages/store.py @@ -12,8 +12,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_from_cookie_or_header, get_db from app.modules.core.utils.page_context import get_store_context -from app.templates_config import templates from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/customers/schemas/__init__.py b/app/modules/customers/schemas/__init__.py index aef7949e..7c701f41 100644 --- a/app/modules/customers/schemas/__init__.py +++ b/app/modules/customers/schemas/__init__.py @@ -15,31 +15,31 @@ Usage: from app.modules.customers.schemas.context import CustomerContext from app.modules.customers.schemas.customer import ( - # Registration & Authentication - CustomerRegister, - CustomerUpdate, - CustomerPasswordChange, - # Customer Response - CustomerResponse, - CustomerListResponse, - # Address - CustomerAddressCreate, - CustomerAddressUpdate, - CustomerAddressResponse, - CustomerAddressListResponse, - # Preferences - CustomerPreferencesUpdate, - # Store Management - CustomerMessageResponse, - StoreCustomerListResponse, - CustomerDetailResponse, - CustomerOrderInfo, - CustomerOrdersResponse, - CustomerStatisticsResponse, + AdminCustomerDetailResponse, # Admin Management AdminCustomerItem, AdminCustomerListResponse, - AdminCustomerDetailResponse, + # Address + CustomerAddressCreate, + CustomerAddressListResponse, + CustomerAddressResponse, + CustomerAddressUpdate, + CustomerDetailResponse, + CustomerListResponse, + # Store Management + CustomerMessageResponse, + CustomerOrderInfo, + CustomerOrdersResponse, + CustomerPasswordChange, + # Preferences + CustomerPreferencesUpdate, + # Registration & Authentication + CustomerRegister, + # Customer Response + CustomerResponse, + CustomerStatisticsResponse, + CustomerUpdate, + StoreCustomerListResponse, ) __all__ = [ diff --git a/app/modules/customers/schemas/customer.py b/app/modules/customers/schemas/customer.py index 337223ac..f0c499c2 100644 --- a/app/modules/customers/schemas/customer.py +++ b/app/modules/customers/schemas/customer.py @@ -14,7 +14,6 @@ from decimal import Decimal from pydantic import BaseModel, EmailStr, Field, field_validator - # ============================================================================ # Customer Registration & Authentication # ============================================================================ @@ -339,4 +338,3 @@ class AdminCustomerListResponse(BaseModel): class AdminCustomerDetailResponse(AdminCustomerItem): """Detailed customer response for admin.""" - pass diff --git a/app/modules/customers/services/__init__.py b/app/modules/customers/services/__init__.py index 5907596a..3b176620 100644 --- a/app/modules/customers/services/__init__.py +++ b/app/modules/customers/services/__init__.py @@ -12,17 +12,17 @@ Usage: ) """ -from app.modules.customers.services.customer_service import ( - customer_service, - CustomerService, -) from app.modules.customers.services.admin_customer_service import ( - admin_customer_service, AdminCustomerService, + admin_customer_service, ) from app.modules.customers.services.customer_address_service import ( - customer_address_service, CustomerAddressService, + customer_address_service, +) +from app.modules.customers.services.customer_service import ( + CustomerService, + customer_service, ) __all__ = [ diff --git a/app/modules/customers/services/customer_features.py b/app/modules/customers/services/customer_features.py index 6bc1b92d..07bf757c 100644 --- a/app/modules/customers/services/customer_features.py +++ b/app/modules/customers/services/customer_features.py @@ -13,7 +13,6 @@ from typing import TYPE_CHECKING from app.modules.contracts.features import ( FeatureDeclaration, - FeatureProviderProtocol, FeatureScope, FeatureType, FeatureUsage, diff --git a/app/modules/customers/services/customer_metrics.py b/app/modules/customers/services/customer_metrics.py index 0df5b63b..5bbed274 100644 --- a/app/modules/customers/services/customer_metrics.py +++ b/app/modules/customers/services/customer_metrics.py @@ -16,9 +16,8 @@ from sqlalchemy import func from sqlalchemy.orm import Session from app.modules.contracts.metrics import ( - MetricValue, MetricsContext, - MetricsProviderProtocol, + MetricValue, ) if TYPE_CHECKING: diff --git a/app/modules/customers/services/customer_service.py b/app/modules/customers/services/customer_service.py index 96fd2b63..f782679c 100644 --- a/app/modules/customers/services/customer_service.py +++ b/app/modules/customers/services/customer_service.py @@ -13,6 +13,7 @@ from typing import Any from sqlalchemy import and_ from sqlalchemy.orm import Session +from app.modules.core.services.auth_service import AuthService from app.modules.customers.exceptions import ( CustomerNotActiveException, CustomerNotFoundException, @@ -22,10 +23,12 @@ from app.modules.customers.exceptions import ( InvalidPasswordResetTokenException, PasswordTooShortException, ) -from app.modules.tenancy.exceptions import StoreNotActiveException, StoreNotFoundException -from app.modules.core.services.auth_service import AuthService from app.modules.customers.models import Customer, PasswordResetToken from app.modules.customers.schemas import CustomerRegister, CustomerUpdate +from app.modules.tenancy.exceptions import ( + StoreNotActiveException, + StoreNotFoundException, +) from app.modules.tenancy.models import Store logger = logging.getLogger(__name__) diff --git a/app/modules/customers/tests/unit/test_admin_customer_service.py b/app/modules/customers/tests/unit/test_admin_customer_service.py index 0486426e..175d5ee8 100644 --- a/app/modules/customers/tests/unit/test_admin_customer_service.py +++ b/app/modules/customers/tests/unit/test_admin_customer_service.py @@ -8,8 +8,8 @@ from decimal import Decimal import pytest from app.modules.customers.exceptions import CustomerNotFoundException -from app.modules.customers.services.admin_customer_service import AdminCustomerService from app.modules.customers.models.customer import Customer +from app.modules.customers.services.admin_customer_service import AdminCustomerService @pytest.fixture diff --git a/app/modules/customers/tests/unit/test_customer_address_service.py b/app/modules/customers/tests/unit/test_customer_address_service.py index eb0793b3..99688110 100644 --- a/app/modules/customers/tests/unit/test_customer_address_service.py +++ b/app/modules/customers/tests/unit/test_customer_address_service.py @@ -5,10 +5,15 @@ Unit tests for CustomerAddressService. import pytest -from app.modules.customers.exceptions import AddressLimitExceededException, AddressNotFoundException -from app.modules.customers.services.customer_address_service import CustomerAddressService +from app.modules.customers.exceptions import ( + AddressLimitExceededException, + AddressNotFoundException, +) from app.modules.customers.models.customer import CustomerAddress from app.modules.customers.schemas import CustomerAddressCreate, CustomerAddressUpdate +from app.modules.customers.services.customer_address_service import ( + CustomerAddressService, +) @pytest.fixture diff --git a/app/modules/dev_tools/__init__.py b/app/modules/dev_tools/__init__.py index f4ea1bd0..66169e14 100644 --- a/app/modules/dev_tools/__init__.py +++ b/app/modules/dev_tools/__init__.py @@ -36,7 +36,7 @@ def __getattr__(name: str): from app.modules.dev_tools.definition import dev_tools_module return dev_tools_module - elif name == "get_dev_tools_module_with_routers": + if name == "get_dev_tools_module_with_routers": from app.modules.dev_tools.definition import get_dev_tools_module_with_routers return get_dev_tools_module_with_routers diff --git a/app/modules/dev_tools/definition.py b/app/modules/dev_tools/definition.py index dc6ff03f..2e99aa76 100644 --- a/app/modules/dev_tools/definition.py +++ b/app/modules/dev_tools/definition.py @@ -15,7 +15,6 @@ Dev-Tools is an internal module providing: from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition from app.modules.enums import FrontendType - # Dev-Tools module definition # Note: API routes (code quality, tests) have been moved to monitoring module. # This module retains models, services, and page routes only. diff --git a/app/modules/dev_tools/exceptions.py b/app/modules/dev_tools/exceptions.py index 670814e9..baa12c42 100644 --- a/app/modules/dev_tools/exceptions.py +++ b/app/modules/dev_tools/exceptions.py @@ -14,16 +14,15 @@ from app.exceptions.base import ( # Re-export code quality exceptions from their module location from app.modules.monitoring.exceptions import ( - ViolationNotFoundException, - ScanNotFoundException, - ScanExecutionException, - ScanTimeoutException, - ScanParseException, - ViolationOperationException, InvalidViolationStatusException, + ScanExecutionException, + ScanNotFoundException, + ScanParseException, + ScanTimeoutException, + ViolationNotFoundException, + ViolationOperationException, ) - # ============================================================================= # Test Runner Exceptions (defined here as they don't exist in legacy location) # ============================================================================= diff --git a/app/modules/dev_tools/migrations/versions/dev_tools_001_initial.py b/app/modules/dev_tools/migrations/versions/dev_tools_001_initial.py index 53fb0ecb..7e859497 100644 --- a/app/modules/dev_tools/migrations/versions/dev_tools_001_initial.py +++ b/app/modules/dev_tools/migrations/versions/dev_tools_001_initial.py @@ -4,9 +4,10 @@ Revision ID: dev_tools_001 Revises: loyalty_001 Create Date: 2026-02-07 """ -from alembic import op import sqlalchemy as sa +from alembic import op + revision = "dev_tools_001" down_revision = "loyalty_002" branch_labels = None diff --git a/app/modules/dev_tools/models/__init__.py b/app/modules/dev_tools/models/__init__.py index a7c12827..30bc8b3b 100644 --- a/app/modules/dev_tools/models/__init__.py +++ b/app/modules/dev_tools/models/__init__.py @@ -19,16 +19,16 @@ Usage: """ from app.modules.dev_tools.models.architecture_scan import ( + ArchitectureRule, ArchitectureScan, ArchitectureViolation, - ArchitectureRule, ViolationAssignment, ViolationComment, ) from app.modules.dev_tools.models.test_run import ( - TestRun, - TestResult, TestCollection, + TestResult, + TestRun, ) __all__ = [ diff --git a/app/modules/dev_tools/routes/pages/admin.py b/app/modules/dev_tools/routes/pages/admin.py index dc3260eb..04d4f169 100644 --- a/app/modules/dev_tools/routes/pages/admin.py +++ b/app/modules/dev_tools/routes/pages/admin.py @@ -17,9 +17,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access from app.modules.core.utils.page_context import get_admin_context -from app.templates_config import templates from app.modules.enums import FrontendType from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/dev_tools/services/__init__.py b/app/modules/dev_tools/services/__init__.py index 95ef0a4b..44a78644 100644 --- a/app/modules/dev_tools/services/__init__.py +++ b/app/modules/dev_tools/services/__init__.py @@ -10,18 +10,18 @@ Services: """ from app.modules.dev_tools.services.code_quality_service import ( - code_quality_service, - CodeQualityService, - VALIDATOR_ARCHITECTURE, - VALIDATOR_SECURITY, - VALIDATOR_PERFORMANCE, VALID_VALIDATOR_TYPES, - VALIDATOR_SCRIPTS, + VALIDATOR_ARCHITECTURE, VALIDATOR_NAMES, + VALIDATOR_PERFORMANCE, + VALIDATOR_SCRIPTS, + VALIDATOR_SECURITY, + CodeQualityService, + code_quality_service, ) from app.modules.dev_tools.services.test_runner_service import ( - test_runner_service, TestRunnerService, + test_runner_service, ) __all__ = [ diff --git a/app/modules/dev_tools/services/code_quality_service.py b/app/modules/dev_tools/services/code_quality_service.py index 30bf4ae0..42234de3 100644 --- a/app/modules/dev_tools/services/code_quality_service.py +++ b/app/modules/dev_tools/services/code_quality_service.py @@ -7,22 +7,22 @@ Supports multiple validator types: architecture, security, performance import json import logging import subprocess -from datetime import datetime, UTC +from datetime import UTC, datetime from sqlalchemy import desc, func from sqlalchemy.orm import Session -from app.modules.monitoring.exceptions import ( - ScanParseException, - ScanTimeoutException, - ViolationNotFoundException, -) from app.modules.dev_tools.models import ( ArchitectureScan, ArchitectureViolation, ViolationAssignment, ViolationComment, ) +from app.modules.monitoring.exceptions import ( + ScanParseException, + ScanTimeoutException, + ViolationNotFoundException, +) logger = logging.getLogger(__name__) @@ -565,7 +565,7 @@ class CodeQualityService: .group_by(ArchitectureViolation.status) .all() ) - status_dict = {status: count for status, count in status_counts} + status_dict = dict(status_counts) # Get violations by severity severity_counts = ( @@ -576,7 +576,7 @@ class CodeQualityService: .group_by(ArchitectureViolation.severity) .all() ) - by_severity = {sev: count for sev, count in severity_counts} + by_severity = dict(severity_counts) # Get violations by rule rule_counts = ( @@ -587,10 +587,7 @@ class CodeQualityService: .group_by(ArchitectureViolation.rule_id) .all() ) - by_rule = { - rule: count - for rule, count in sorted(rule_counts, key=lambda x: x[1], reverse=True)[:10] - } + by_rule = dict(sorted(rule_counts, key=lambda x: x[1], reverse=True)[:10]) # Get top violating files file_counts = ( @@ -667,7 +664,7 @@ class CodeQualityService: .group_by(ArchitectureViolation.status) .all() ) - status_dict = {status: count for status, count in status_counts} + status_dict = dict(status_counts) # Get violations by severity severity_counts = ( @@ -678,7 +675,7 @@ class CodeQualityService: .group_by(ArchitectureViolation.severity) .all() ) - by_severity = {sev: count for sev, count in severity_counts} + by_severity = dict(severity_counts) # Get violations by rule (across all validators) rule_counts = ( @@ -689,10 +686,7 @@ class CodeQualityService: .group_by(ArchitectureViolation.rule_id) .all() ) - by_rule = { - rule: count - for rule, count in sorted(rule_counts, key=lambda x: x[1], reverse=True)[:10] - } + by_rule = dict(sorted(rule_counts, key=lambda x: x[1], reverse=True)[:10]) # Get top violating files file_counts = ( @@ -761,10 +755,7 @@ class CodeQualityService: for v in violations: path_parts = v.file_path.split("/") - if len(path_parts) >= 2: - module = "/".join(path_parts[:2]) - else: - module = path_parts[0] + module = "/".join(path_parts[:2]) if len(path_parts) >= 2 else path_parts[0] by_module[module] = by_module.get(module, 0) + 1 return dict(sorted(by_module.items(), key=lambda x: x[1], reverse=True)[:10]) diff --git a/app/modules/dev_tools/services/test_runner_service.py b/app/modules/dev_tools/services/test_runner_service.py index ed3116c5..59a782ab 100644 --- a/app/modules/dev_tools/services/test_runner_service.py +++ b/app/modules/dev_tools/services/test_runner_service.py @@ -3,6 +3,7 @@ Test Runner Service Service for running pytest and storing results """ +import contextlib import json import logging import re @@ -123,10 +124,8 @@ class TestRunnerService: self._parse_pytest_output(test_run, result.stdout, result.stderr) finally: # Clean up temp file - try: + with contextlib.suppress(Exception): Path(json_report_path).unlink() - except Exception: - pass # Set final status if test_run.failed > 0 or test_run.errors > 0: @@ -428,7 +427,7 @@ class TestRunnerService: ) as f: json_report_path = f.name - result = subprocess.run( + subprocess.run( [ "python", "-m", diff --git a/app/modules/dev_tools/tasks/code_quality.py b/app/modules/dev_tools/tasks/code_quality.py index aa3b1b7b..3d2ad2d4 100644 --- a/app/modules/dev_tools/tasks/code_quality.py +++ b/app/modules/dev_tools/tasks/code_quality.py @@ -13,9 +13,11 @@ import subprocess from datetime import UTC, datetime from app.core.celery_config import celery_app -from app.modules.messaging.services.admin_notification_service import admin_notification_service -from app.modules.task_base import ModuleTask from app.modules.dev_tools.models import ArchitectureScan, ArchitectureViolation +from app.modules.messaging.services.admin_notification_service import ( + admin_notification_service, +) +from app.modules.task_base import ModuleTask logger = logging.getLogger(__name__) diff --git a/app/modules/dev_tools/tasks/test_runner.py b/app/modules/dev_tools/tasks/test_runner.py index 810dbbb0..1519a58c 100644 --- a/app/modules/dev_tools/tasks/test_runner.py +++ b/app/modules/dev_tools/tasks/test_runner.py @@ -10,9 +10,9 @@ for backward compatibility. import logging from app.core.celery_config import celery_app +from app.modules.dev_tools.models import TestRun from app.modules.dev_tools.services.test_runner_service import test_runner_service from app.modules.task_base import ModuleTask -from app.modules.dev_tools.models import TestRun logger = logging.getLogger(__name__) diff --git a/app/modules/events.py b/app/modules/events.py index 255f308e..f9c82868 100644 --- a/app/modules/events.py +++ b/app/modules/events.py @@ -25,10 +25,11 @@ Usage: """ import logging +from collections.abc import Callable from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime from enum import Enum -from typing import Any, Callable +from typing import Any logger = logging.getLogger(__name__) @@ -70,7 +71,7 @@ class ModuleEventData: user_id: int | None = None config: dict[str, Any] | None = None metadata: dict[str, Any] = field(default_factory=dict) - timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + timestamp: datetime = field(default_factory=lambda: datetime.now(UTC)) # Type alias for event handlers diff --git a/app/modules/inventory/__init__.py b/app/modules/inventory/__init__.py index 95337dad..dbee49df 100644 --- a/app/modules/inventory/__init__.py +++ b/app/modules/inventory/__init__.py @@ -26,7 +26,7 @@ def __getattr__(name: str): from app.modules.inventory.definition import inventory_module return inventory_module - elif name == "get_inventory_module_with_routers": + if name == "get_inventory_module_with_routers": from app.modules.inventory.definition import get_inventory_module_with_routers return get_inventory_module_with_routers diff --git a/app/modules/inventory/definition.py b/app/modules/inventory/definition.py index 97e5c10c..994627cf 100644 --- a/app/modules/inventory/definition.py +++ b/app/modules/inventory/definition.py @@ -31,14 +31,18 @@ def _get_store_router(): def _get_metrics_provider(): """Lazy import of metrics provider to avoid circular imports.""" - from app.modules.inventory.services.inventory_metrics import inventory_metrics_provider + from app.modules.inventory.services.inventory_metrics import ( + inventory_metrics_provider, + ) return inventory_metrics_provider def _get_feature_provider(): """Lazy import of feature provider to avoid circular imports.""" - from app.modules.inventory.services.inventory_features import inventory_feature_provider + from app.modules.inventory.services.inventory_features import ( + inventory_feature_provider, + ) return inventory_feature_provider diff --git a/app/modules/inventory/migrations/versions/inventory_001_initial.py b/app/modules/inventory/migrations/versions/inventory_001_initial.py index 8c93cca8..6e48f285 100644 --- a/app/modules/inventory/migrations/versions/inventory_001_initial.py +++ b/app/modules/inventory/migrations/versions/inventory_001_initial.py @@ -4,9 +4,10 @@ Revision ID: inventory_001 Revises: orders_001 Create Date: 2026-02-07 """ -from alembic import op import sqlalchemy as sa +from alembic import op + revision = "inventory_001" down_revision = "orders_001" branch_labels = None diff --git a/app/modules/inventory/models/inventory_transaction.py b/app/modules/inventory/models/inventory_transaction.py index 055f1f9b..e6dab462 100644 --- a/app/modules/inventory/models/inventory_transaction.py +++ b/app/modules/inventory/models/inventory_transaction.py @@ -16,13 +16,15 @@ from enum import Enum from sqlalchemy import ( Column, DateTime, - Enum as SQLEnum, ForeignKey, Index, Integer, String, Text, ) +from sqlalchemy import ( + Enum as SQLEnum, +) from sqlalchemy.orm import relationship from app.core.database import Base diff --git a/app/modules/inventory/routes/__init__.py b/app/modules/inventory/routes/__init__.py index f8afa849..3ea9a68a 100644 --- a/app/modules/inventory/routes/__init__.py +++ b/app/modules/inventory/routes/__init__.py @@ -19,7 +19,7 @@ def __getattr__(name: str): if name == "admin_router": from app.modules.inventory.routes.api import admin_router return admin_router - elif name == "store_router": + if name == "store_router": from app.modules.inventory.routes.api import store_router return store_router raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/app/modules/inventory/routes/api/__init__.py b/app/modules/inventory/routes/api/__init__.py index f0a9fc5c..f0e9c5ff 100644 --- a/app/modules/inventory/routes/api/__init__.py +++ b/app/modules/inventory/routes/api/__init__.py @@ -15,7 +15,7 @@ def __getattr__(name: str): if name == "admin_router": from app.modules.inventory.routes.api.admin import admin_router return admin_router - elif name == "store_router": + if name == "store_router": from app.modules.inventory.routes.api.store import store_router return store_router raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/app/modules/inventory/routes/api/admin.py b/app/modules/inventory/routes/api/admin.py index c78d8c2e..59764fb8 100644 --- a/app/modules/inventory/routes/api/admin.py +++ b/app/modules/inventory/routes/api/admin.py @@ -21,10 +21,6 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api, require_module_access from app.core.database import get_db from app.modules.enums import FrontendType -from app.modules.inventory.services.inventory_import_service import inventory_import_service -from app.modules.inventory.services.inventory_service import inventory_service -from app.modules.inventory.services.inventory_transaction_service import inventory_transaction_service -from models.schema.auth import UserContext from app.modules.inventory.schemas import ( AdminInventoryAdjust, AdminInventoryCreate, @@ -34,8 +30,8 @@ from app.modules.inventory.schemas import ( AdminInventoryTransactionItem, AdminInventoryTransactionListResponse, AdminLowStockItem, - AdminTransactionStatsResponse, AdminStoresWithInventoryResponse, + AdminTransactionStatsResponse, InventoryAdjust, InventoryCreate, InventoryMessageResponse, @@ -43,6 +39,14 @@ from app.modules.inventory.schemas import ( InventoryUpdate, ProductInventorySummary, ) +from app.modules.inventory.services.inventory_import_service import ( + inventory_import_service, +) +from app.modules.inventory.services.inventory_service import inventory_service +from app.modules.inventory.services.inventory_transaction_service import ( + inventory_transaction_service, +) +from models.schema.auth import UserContext admin_router = APIRouter( prefix="/inventory", diff --git a/app/modules/inventory/routes/api/store.py b/app/modules/inventory/routes/api/store.py index 9be0d257..bf954789 100644 --- a/app/modules/inventory/routes/api/store.py +++ b/app/modules/inventory/routes/api/store.py @@ -14,9 +14,6 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_api, require_module_access from app.core.database import get_db from app.modules.enums import FrontendType -from app.modules.inventory.services.inventory_service import inventory_service -from app.modules.inventory.services.inventory_transaction_service import inventory_transaction_service -from models.schema.auth import UserContext from app.modules.inventory.schemas import ( InventoryAdjust, InventoryCreate, @@ -31,6 +28,11 @@ from app.modules.inventory.schemas import ( ProductInventorySummary, ProductTransactionHistoryResponse, ) +from app.modules.inventory.services.inventory_service import inventory_service +from app.modules.inventory.services.inventory_transaction_service import ( + inventory_transaction_service, +) +from models.schema.auth import UserContext store_router = APIRouter( prefix="/inventory", diff --git a/app/modules/inventory/routes/pages/admin.py b/app/modules/inventory/routes/pages/admin.py index 7998325a..b80140e1 100644 --- a/app/modules/inventory/routes/pages/admin.py +++ b/app/modules/inventory/routes/pages/admin.py @@ -12,9 +12,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access from app.modules.core.utils.page_context import get_admin_context -from app.templates_config import templates from app.modules.enums import FrontendType from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/inventory/routes/pages/store.py b/app/modules/inventory/routes/pages/store.py index e667ecd0..42c37388 100644 --- a/app/modules/inventory/routes/pages/store.py +++ b/app/modules/inventory/routes/pages/store.py @@ -12,8 +12,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_from_cookie_or_header, get_db from app.modules.core.utils.page_context import get_store_context -from app.templates_config import templates from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/inventory/schemas/__init__.py b/app/modules/inventory/schemas/__init__.py index a19fe84c..16434d77 100644 --- a/app/modules/inventory/schemas/__init__.py +++ b/app/modules/inventory/schemas/__init__.py @@ -6,39 +6,39 @@ This module contains the canonical implementations of inventory-related schemas. """ from app.modules.inventory.schemas.inventory import ( - # Base schemas - InventoryBase, - InventoryCreate, - InventoryAdjust, - InventoryUpdate, - InventoryReserve, - # Response schemas - InventoryResponse, - InventoryLocationResponse, - ProductInventorySummary, - InventoryListResponse, - InventoryMessageResponse, - InventorySummaryResponse, + AdminInventoryAdjust, # Admin schemas AdminInventoryCreate, - AdminInventoryAdjust, AdminInventoryItem, AdminInventoryListResponse, - AdminInventoryStats, - AdminLowStockItem, - AdminStoreWithInventory, - AdminStoresWithInventoryResponse, AdminInventoryLocationsResponse, - # Transaction schemas - InventoryTransactionResponse, - InventoryTransactionWithProduct, - InventoryTransactionListResponse, - ProductTransactionHistoryResponse, - OrderTransactionHistoryResponse, + AdminInventoryStats, # Admin transaction schemas AdminInventoryTransactionItem, AdminInventoryTransactionListResponse, + AdminLowStockItem, + AdminStoresWithInventoryResponse, + AdminStoreWithInventory, AdminTransactionStatsResponse, + InventoryAdjust, + # Base schemas + InventoryBase, + InventoryCreate, + InventoryListResponse, + InventoryLocationResponse, + InventoryMessageResponse, + InventoryReserve, + # Response schemas + InventoryResponse, + InventorySummaryResponse, + InventoryTransactionListResponse, + # Transaction schemas + InventoryTransactionResponse, + InventoryTransactionWithProduct, + InventoryUpdate, + OrderTransactionHistoryResponse, + ProductInventorySummary, + ProductTransactionHistoryResponse, ) __all__ = [ diff --git a/app/modules/inventory/services/__init__.py b/app/modules/inventory/services/__init__.py index 0217c1e2..0edcba7b 100644 --- a/app/modules/inventory/services/__init__.py +++ b/app/modules/inventory/services/__init__.py @@ -5,18 +5,18 @@ Inventory module services. This module contains the canonical implementations of inventory-related services. """ +from app.modules.inventory.services.inventory_import_service import ( + ImportResult, + InventoryImportService, + inventory_import_service, +) from app.modules.inventory.services.inventory_service import ( - inventory_service, InventoryService, + inventory_service, ) from app.modules.inventory.services.inventory_transaction_service import ( - inventory_transaction_service, InventoryTransactionService, -) -from app.modules.inventory.services.inventory_import_service import ( - inventory_import_service, - InventoryImportService, - ImportResult, + inventory_transaction_service, ) __all__ = [ diff --git a/app/modules/inventory/services/inventory_features.py b/app/modules/inventory/services/inventory_features.py index 18d11fb8..9021026c 100644 --- a/app/modules/inventory/services/inventory_features.py +++ b/app/modules/inventory/services/inventory_features.py @@ -13,7 +13,6 @@ from typing import TYPE_CHECKING from app.modules.contracts.features import ( FeatureDeclaration, - FeatureProviderProtocol, FeatureScope, FeatureType, FeatureUsage, diff --git a/app/modules/inventory/services/inventory_import_service.py b/app/modules/inventory/services/inventory_import_service.py index 8ba70593..b1de02a1 100644 --- a/app/modules/inventory/services/inventory_import_service.py +++ b/app/modules/inventory/services/inventory_import_service.py @@ -23,8 +23,8 @@ from dataclasses import dataclass, field from sqlalchemy.orm import Session -from app.modules.inventory.models.inventory import Inventory from app.modules.catalog.models import Product +from app.modules.inventory.models.inventory import Inventory logger = logging.getLogger(__name__) @@ -227,7 +227,7 @@ class InventoryImportService: ImportResult with summary and errors """ try: - with open(file_path, "r", encoding="utf-8") as f: + with open(file_path, encoding="utf-8") as f: content = f.read() except Exception as e: return ImportResult(success=False, errors=[f"Failed to read file: {e}"]) diff --git a/app/modules/inventory/services/inventory_metrics.py b/app/modules/inventory/services/inventory_metrics.py index c25cbbf9..eb43cfaf 100644 --- a/app/modules/inventory/services/inventory_metrics.py +++ b/app/modules/inventory/services/inventory_metrics.py @@ -15,9 +15,8 @@ from sqlalchemy import func from sqlalchemy.orm import Session from app.modules.contracts.metrics import ( - MetricValue, MetricsContext, - MetricsProviderProtocol, + MetricValue, ) if TYPE_CHECKING: diff --git a/app/modules/inventory/services/inventory_service.py b/app/modules/inventory/services/inventory_service.py index 7151e86f..2366392b 100644 --- a/app/modules/inventory/services/inventory_service.py +++ b/app/modules/inventory/services/inventory_service.py @@ -6,14 +6,14 @@ from sqlalchemy import func from sqlalchemy.orm import Session from app.exceptions import ValidationException +from app.modules.catalog.exceptions import ProductNotFoundException +from app.modules.catalog.models import Product from app.modules.inventory.exceptions import ( InsufficientInventoryException, InvalidQuantityException, InventoryNotFoundException, InventoryValidationException, ) -from app.modules.catalog.exceptions import ProductNotFoundException -from app.modules.tenancy.exceptions import StoreNotFoundException from app.modules.inventory.models.inventory import Inventory from app.modules.inventory.schemas.inventory import ( AdminInventoryItem, @@ -30,7 +30,7 @@ from app.modules.inventory.schemas.inventory import ( InventoryUpdate, ProductInventorySummary, ) -from app.modules.catalog.models import Product +from app.modules.tenancy.exceptions import StoreNotFoundException from app.modules.tenancy.models import Store logger = logging.getLogger(__name__) @@ -217,7 +217,7 @@ class InventoryService: """ try: # Validate product - product = self._get_store_product(db, store_id, reserve_data.product_id) + self._get_store_product(db, store_id, reserve_data.product_id) # Validate location and quantity location = self._validate_location(reserve_data.location) @@ -279,7 +279,7 @@ class InventoryService: """ try: # Validate product - product = self._get_store_product(db, store_id, reserve_data.product_id) + self._get_store_product(db, store_id, reserve_data.product_id) location = self._validate_location(reserve_data.location) self._validate_quantity(reserve_data.quantity, allow_zero=False) @@ -338,7 +338,7 @@ class InventoryService: Updated Inventory object """ try: - product = self._get_store_product(db, store_id, reserve_data.product_id) + self._get_store_product(db, store_id, reserve_data.product_id) location = self._validate_location(reserve_data.location) self._validate_quantity(reserve_data.quantity, allow_zero=False) diff --git a/app/modules/inventory/services/inventory_transaction_service.py b/app/modules/inventory/services/inventory_transaction_service.py index ae48479e..21a68d10 100644 --- a/app/modules/inventory/services/inventory_transaction_service.py +++ b/app/modules/inventory/services/inventory_transaction_service.py @@ -8,15 +8,16 @@ This service handles transaction READS for reporting and auditing. """ import logging + from sqlalchemy import func from sqlalchemy.orm import Session from app.modules.catalog.exceptions import ProductNotFoundException -from app.modules.orders.exceptions import OrderNotFoundException +from app.modules.catalog.models import Product from app.modules.inventory.models.inventory import Inventory from app.modules.inventory.models.inventory_transaction import InventoryTransaction +from app.modules.orders.exceptions import OrderNotFoundException from app.modules.orders.models import Order -from app.modules.catalog.models import Product logger = logging.getLogger(__name__) @@ -411,7 +412,7 @@ class InventoryTransactionService: total = db.query(sql_func.count(InventoryTransaction.id)).scalar() or 0 # Transactions today - from datetime import UTC, datetime, timedelta + from datetime import UTC, datetime today_start = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0) today_count = ( diff --git a/app/modules/loyalty/__init__.py b/app/modules/loyalty/__init__.py index d7afa54c..61cfc06e 100644 --- a/app/modules/loyalty/__init__.py +++ b/app/modules/loyalty/__init__.py @@ -41,7 +41,7 @@ def __getattr__(name: str): from app.modules.loyalty.definition import loyalty_module return loyalty_module - elif name == "get_loyalty_module_with_routers": + if name == "get_loyalty_module_with_routers": from app.modules.loyalty.definition import get_loyalty_module_with_routers return get_loyalty_module_with_routers diff --git a/app/modules/loyalty/definition.py b/app/modules/loyalty/definition.py index 2da48c54..08f2d748 100644 --- a/app/modules/loyalty/definition.py +++ b/app/modules/loyalty/definition.py @@ -6,7 +6,13 @@ Defines the loyalty module including its features, menu items, route configurations, and scheduled tasks. """ -from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition, ScheduledTask +from app.modules.base import ( + MenuItemDefinition, + MenuSectionDefinition, + ModuleDefinition, + PermissionDefinition, + ScheduledTask, +) from app.modules.enums import FrontendType diff --git a/app/modules/loyalty/migrations/versions/loyalty_001_initial.py b/app/modules/loyalty/migrations/versions/loyalty_001_initial.py index 7a9af5c3..ab269c2e 100644 --- a/app/modules/loyalty/migrations/versions/loyalty_001_initial.py +++ b/app/modules/loyalty/migrations/versions/loyalty_001_initial.py @@ -4,9 +4,10 @@ Revision ID: loyalty_001 Revises: messaging_001 Create Date: 2026-02-07 """ -from alembic import op import sqlalchemy as sa +from alembic import op + revision = "loyalty_001" down_revision = "messaging_001" branch_labels = None diff --git a/app/modules/loyalty/migrations/versions/loyalty_002_add_total_points_voided.py b/app/modules/loyalty/migrations/versions/loyalty_002_add_total_points_voided.py index 66e22d57..b6eaad23 100644 --- a/app/modules/loyalty/migrations/versions/loyalty_002_add_total_points_voided.py +++ b/app/modules/loyalty/migrations/versions/loyalty_002_add_total_points_voided.py @@ -4,9 +4,10 @@ Revision ID: loyalty_002 Revises: loyalty_001 Create Date: 2026-02-08 """ -from alembic import op import sqlalchemy as sa +from alembic import op + revision = "loyalty_002" down_revision = "loyalty_001" branch_labels = None diff --git a/app/modules/loyalty/models/__init__.py b/app/modules/loyalty/models/__init__.py index 9b1b79e7..cb8b2250 100644 --- a/app/modules/loyalty/models/__init__.py +++ b/app/modules/loyalty/models/__init__.py @@ -19,36 +19,36 @@ Usage: ) """ -from app.modules.loyalty.models.loyalty_program import ( - # Enums - LoyaltyType, +from app.modules.loyalty.models.apple_device import ( # Model - LoyaltyProgram, + AppleDeviceRegistration, ) from app.modules.loyalty.models.loyalty_card import ( # Model LoyaltyCard, ) -from app.modules.loyalty.models.loyalty_transaction import ( +from app.modules.loyalty.models.loyalty_program import ( + # Model + LoyaltyProgram, # Enums - TransactionType, + LoyaltyType, +) +from app.modules.loyalty.models.loyalty_transaction import ( # Model LoyaltyTransaction, + # Enums + TransactionType, +) +from app.modules.loyalty.models.merchant_settings import ( + # Model + MerchantLoyaltySettings, + # Enums + StaffPinPolicy, ) from app.modules.loyalty.models.staff_pin import ( # Model StaffPin, ) -from app.modules.loyalty.models.apple_device import ( - # Model - AppleDeviceRegistration, -) -from app.modules.loyalty.models.merchant_settings import ( - # Enums - StaffPinPolicy, - # Model - MerchantLoyaltySettings, -) __all__ = [ # Enums diff --git a/app/modules/loyalty/routes/api/__init__.py b/app/modules/loyalty/routes/api/__init__.py index b96a5c93..c6da0d3f 100644 --- a/app/modules/loyalty/routes/api/__init__.py +++ b/app/modules/loyalty/routes/api/__init__.py @@ -10,8 +10,8 @@ Provides REST API endpoints for: """ from app.modules.loyalty.routes.api.admin import admin_router -from app.modules.loyalty.routes.api.store import store_router from app.modules.loyalty.routes.api.platform import platform_router +from app.modules.loyalty.routes.api.store import store_router from app.modules.loyalty.routes.api.storefront import storefront_router __all__ = ["admin_router", "store_router", "platform_router", "storefront_router"] diff --git a/app/modules/loyalty/routes/api/admin.py b/app/modules/loyalty/routes/api/admin.py index 43ccfdb2..fb958adf 100644 --- a/app/modules/loyalty/routes/api/admin.py +++ b/app/modules/loyalty/routes/api/admin.py @@ -17,12 +17,12 @@ from app.api.deps import get_current_admin_api, require_module_access from app.core.database import get_db from app.modules.enums import FrontendType from app.modules.loyalty.schemas import ( + MerchantSettingsResponse, + MerchantSettingsUpdate, + MerchantStatsResponse, ProgramListResponse, ProgramResponse, ProgramStatsResponse, - MerchantStatsResponse, - MerchantSettingsResponse, - MerchantSettingsUpdate, ) from app.modules.loyalty.services import program_service from app.modules.tenancy.models import User @@ -182,7 +182,6 @@ def update_merchant_settings( db: Session = Depends(get_db), ): """Update merchant loyalty settings (admin only).""" - from app.modules.loyalty.models import MerchantLoyaltySettings settings = program_service.get_or_create_merchant_settings(db, merchant_id) @@ -211,7 +210,11 @@ def get_platform_stats( """Get platform-wide loyalty statistics.""" from sqlalchemy import func - from app.modules.loyalty.models import LoyaltyCard, LoyaltyProgram, LoyaltyTransaction + from app.modules.loyalty.models import ( + LoyaltyCard, + LoyaltyProgram, + LoyaltyTransaction, + ) # Program counts total_programs = db.query(func.count(LoyaltyProgram.id)).scalar() or 0 diff --git a/app/modules/loyalty/routes/api/platform.py b/app/modules/loyalty/routes/api/platform.py index 57a952b7..f448b92d 100644 --- a/app/modules/loyalty/routes/api/platform.py +++ b/app/modules/loyalty/routes/api/platform.py @@ -9,21 +9,18 @@ Platform endpoints for: """ import logging -from datetime import UTC, datetime +from datetime import datetime from fastapi import APIRouter, Depends, Header, HTTPException, Path, Response from sqlalchemy.orm import Session from app.core.database import get_db from app.modules.loyalty.exceptions import ( - LoyaltyCardNotFoundException, LoyaltyException, - LoyaltyProgramNotFoundException, ) -from app.modules.loyalty.models import LoyaltyCard, LoyaltyProgram +from app.modules.loyalty.models import LoyaltyCard from app.modules.loyalty.services import ( apple_wallet_service, - card_service, program_service, ) diff --git a/app/modules/loyalty/routes/api/store.py b/app/modules/loyalty/routes/api/store.py index 203803e7..4418c653 100644 --- a/app/modules/loyalty/routes/api/store.py +++ b/app/modules/loyalty/routes/api/store.py @@ -20,34 +20,33 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_api, require_module_access from app.core.database import get_db +from app.modules.enums import FrontendType from app.modules.loyalty.exceptions import ( LoyaltyCardNotFoundException, LoyaltyException, - LoyaltyProgramNotFoundException, ) from app.modules.loyalty.schemas import ( - CardDetailResponse, CardEnrollRequest, CardListResponse, CardLookupResponse, CardResponse, + MerchantStatsResponse, PinCreate, PinListResponse, PinResponse, PinUpdate, + PointsAdjustRequest, + PointsAdjustResponse, PointsEarnRequest, PointsEarnResponse, PointsRedeemRequest, PointsRedeemResponse, PointsVoidRequest, PointsVoidResponse, - PointsAdjustRequest, - PointsAdjustResponse, ProgramCreate, ProgramResponse, ProgramStatsResponse, ProgramUpdate, - MerchantStatsResponse, StampRedeemRequest, StampRedeemResponse, StampRequest, @@ -63,10 +62,8 @@ from app.modules.loyalty.services import ( points_service, program_service, stamp_service, - wallet_service, ) -from app.modules.enums import FrontendType -from app.modules.tenancy.models import User, Store +from app.modules.tenancy.models import Store, User logger = logging.getLogger(__name__) @@ -466,7 +463,7 @@ def get_card_transactions( # Verify card belongs to this merchant try: - card = card_service.lookup_card_for_store(db, store_id, card_id=card_id) + card_service.lookup_card_for_store(db, store_id, card_id=card_id) except LoyaltyCardNotFoundException: raise HTTPException(status_code=404, detail="Card not found") diff --git a/app/modules/loyalty/routes/api/storefront.py b/app/modules/loyalty/routes/api/storefront.py index b4fd5750..9af7b6bc 100644 --- a/app/modules/loyalty/routes/api/storefront.py +++ b/app/modules/loyalty/routes/api/storefront.py @@ -19,14 +19,12 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_customer_api from app.core.database import get_db from app.modules.customers.schemas import CustomerContext -from app.modules.loyalty.services import card_service, program_service from app.modules.loyalty.schemas import ( - CardResponse, CardEnrollRequest, - TransactionListResponse, - TransactionResponse, + CardResponse, ProgramResponse, ) +from app.modules.loyalty.services import card_service, program_service from app.modules.tenancy.exceptions import StoreNotFoundException storefront_router = APIRouter() @@ -212,7 +210,7 @@ def get_my_transactions( for tx in transactions: tx_data = { "id": tx.id, - "transaction_type": tx.transaction_type.value if hasattr(tx.transaction_type, 'value') else str(tx.transaction_type), + "transaction_type": tx.transaction_type.value if hasattr(tx.transaction_type, "value") else str(tx.transaction_type), "points_delta": tx.points_delta, "stamps_delta": tx.stamps_delta, "points_balance_after": tx.points_balance_after, diff --git a/app/modules/loyalty/routes/pages/admin.py b/app/modules/loyalty/routes/pages/admin.py index 3cbf50c1..aac21656 100644 --- a/app/modules/loyalty/routes/pages/admin.py +++ b/app/modules/loyalty/routes/pages/admin.py @@ -13,9 +13,9 @@ from fastapi.responses import HTMLResponse from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access -from app.templates_config import templates from app.modules.enums import FrontendType from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/loyalty/routes/pages/store.py b/app/modules/loyalty/routes/pages/store.py index 3f36e243..12b63306 100644 --- a/app/modules/loyalty/routes/pages/store.py +++ b/app/modules/loyalty/routes/pages/store.py @@ -16,9 +16,11 @@ from fastapi.responses import HTMLResponse from sqlalchemy.orm import Session from app.api.deps import get_current_store_from_cookie_or_header, get_db -from app.modules.core.services.platform_settings_service import platform_settings_service +from app.modules.core.services.platform_settings_service import ( + platform_settings_service, +) +from app.modules.tenancy.models import Store, User from app.templates_config import templates -from app.modules.tenancy.models import User, Store logger = logging.getLogger(__name__) diff --git a/app/modules/loyalty/schemas/__init__.py b/app/modules/loyalty/schemas/__init__.py index 7df675c3..71f8a7d5 100644 --- a/app/modules/loyalty/schemas/__init__.py +++ b/app/modules/loyalty/schemas/__init__.py @@ -25,46 +25,29 @@ Usage: ) """ -from app.modules.loyalty.schemas.program import ( - # Program CRUD - ProgramCreate, - ProgramUpdate, - ProgramResponse, - ProgramListResponse, - # Points rewards - PointsRewardConfig, - TierConfig, - # Stats - ProgramStatsResponse, - MerchantStatsResponse, - # Merchant settings - MerchantSettingsResponse, - MerchantSettingsUpdate, -) - from app.modules.loyalty.schemas.card import ( + CardDetailResponse, # Card operations CardEnrollRequest, - CardResponse, - CardDetailResponse, CardListResponse, CardLookupResponse, + CardResponse, + TransactionListResponse, # Transactions TransactionResponse, - TransactionListResponse, ) - -from app.modules.loyalty.schemas.stamp import ( - # Stamp operations - StampRequest, - StampResponse, - StampRedeemRequest, - StampRedeemResponse, - StampVoidRequest, - StampVoidResponse, +from app.modules.loyalty.schemas.pin import ( + # Staff PIN + PinCreate, + PinListResponse, + PinResponse, + PinUpdate, + PinVerifyRequest, + PinVerifyResponse, ) - from app.modules.loyalty.schemas.points import ( + PointsAdjustRequest, + PointsAdjustResponse, # Points operations PointsEarnRequest, PointsEarnResponse, @@ -72,18 +55,31 @@ from app.modules.loyalty.schemas.points import ( PointsRedeemResponse, PointsVoidRequest, PointsVoidResponse, - PointsAdjustRequest, - PointsAdjustResponse, ) - -from app.modules.loyalty.schemas.pin import ( - # Staff PIN - PinCreate, - PinUpdate, - PinResponse, - PinListResponse, - PinVerifyRequest, - PinVerifyResponse, +from app.modules.loyalty.schemas.program import ( + # Merchant settings + MerchantSettingsResponse, + MerchantSettingsUpdate, + MerchantStatsResponse, + # Points rewards + PointsRewardConfig, + # Program CRUD + ProgramCreate, + ProgramListResponse, + ProgramResponse, + # Stats + ProgramStatsResponse, + ProgramUpdate, + TierConfig, +) +from app.modules.loyalty.schemas.stamp import ( + StampRedeemRequest, + StampRedeemResponse, + # Stamp operations + StampRequest, + StampResponse, + StampVoidRequest, + StampVoidResponse, ) __all__ = [ diff --git a/app/modules/loyalty/services/__init__.py b/app/modules/loyalty/services/__init__.py index 2af5c3f2..e3980f5f 100644 --- a/app/modules/loyalty/services/__init__.py +++ b/app/modules/loyalty/services/__init__.py @@ -6,38 +6,38 @@ Provides loyalty program management, card operations, stamp/points handling, and wallet integration. """ -from app.modules.loyalty.services.program_service import ( - ProgramService, - program_service, +from app.modules.loyalty.services.apple_wallet_service import ( + AppleWalletService, + apple_wallet_service, ) from app.modules.loyalty.services.card_service import ( CardService, card_service, ) -from app.modules.loyalty.services.stamp_service import ( - StampService, - stamp_service, -) -from app.modules.loyalty.services.points_service import ( - PointsService, - points_service, +from app.modules.loyalty.services.google_wallet_service import ( + GoogleWalletService, + google_wallet_service, ) from app.modules.loyalty.services.pin_service import ( PinService, pin_service, ) +from app.modules.loyalty.services.points_service import ( + PointsService, + points_service, +) +from app.modules.loyalty.services.program_service import ( + ProgramService, + program_service, +) +from app.modules.loyalty.services.stamp_service import ( + StampService, + stamp_service, +) from app.modules.loyalty.services.wallet_service import ( WalletService, wallet_service, ) -from app.modules.loyalty.services.google_wallet_service import ( - GoogleWalletService, - google_wallet_service, -) -from app.modules.loyalty.services.apple_wallet_service import ( - AppleWalletService, - apple_wallet_service, -) __all__ = [ "ProgramService", diff --git a/app/modules/loyalty/services/apple_wallet_service.py b/app/modules/loyalty/services/apple_wallet_service.py index efd81e5e..0479158b 100644 --- a/app/modules/loyalty/services/apple_wallet_service.py +++ b/app/modules/loyalty/services/apple_wallet_service.py @@ -22,7 +22,11 @@ from app.modules.loyalty.exceptions import ( AppleWalletNotConfiguredException, WalletIntegrationException, ) -from app.modules.loyalty.models import AppleDeviceRegistration, LoyaltyCard, LoyaltyProgram +from app.modules.loyalty.models import ( + AppleDeviceRegistration, + LoyaltyCard, + LoyaltyProgram, +) logger = logging.getLogger(__name__) diff --git a/app/modules/loyalty/services/google_wallet_service.py b/app/modules/loyalty/services/google_wallet_service.py index 8606fa6d..7d3e9b96 100644 --- a/app/modules/loyalty/services/google_wallet_service.py +++ b/app/modules/loyalty/services/google_wallet_service.py @@ -9,7 +9,6 @@ Handles Google Wallet integration including: - Generating "Add to Wallet" URLs """ -import json import logging from typing import Any @@ -135,17 +134,16 @@ class GoogleWalletService: logger.info(f"Created Google Wallet class {class_id} for program {program.id}") return class_id - elif response.status_code == 409: + if response.status_code == 409: # Class already exists program.google_class_id = class_id db.commit() return class_id - else: - error = response.json() if response.text else {} - raise WalletIntegrationException( - "google", - f"Failed to create class: {response.status_code} - {error}", - ) + error = response.json() if response.text else {} + raise WalletIntegrationException( + "google", + f"Failed to create class: {response.status_code} - {error}", + ) except WalletIntegrationException: raise except Exception as e: @@ -223,17 +221,16 @@ class GoogleWalletService: logger.info(f"Created Google Wallet object {object_id} for card {card.id}") return object_id - elif response.status_code == 409: + if response.status_code == 409: # Object already exists card.google_object_id = object_id db.commit() return object_id - else: - error = response.json() if response.text else {} - raise WalletIntegrationException( - "google", - f"Failed to create object: {response.status_code} - {error}", - ) + error = response.json() if response.text else {} + raise WalletIntegrationException( + "google", + f"Failed to create object: {response.status_code} - {error}", + ) except WalletIntegrationException: raise except Exception as e: @@ -330,9 +327,10 @@ class GoogleWalletService: # Generate JWT for save link try: - import jwt from datetime import datetime, timedelta + import jwt + credentials = self._get_credentials() claims = { diff --git a/app/modules/loyalty/services/pin_service.py b/app/modules/loyalty/services/pin_service.py index 1aa3d5dd..1e4f87fb 100644 --- a/app/modules/loyalty/services/pin_service.py +++ b/app/modules/loyalty/services/pin_service.py @@ -14,7 +14,6 @@ Handles PIN operations including: """ import logging -from datetime import UTC, datetime from sqlalchemy.orm import Session @@ -218,7 +217,6 @@ class PinService: def delete_pin(self, db: Session, pin_id: int) -> None: """Delete a staff PIN.""" pin = self.require_pin(db, pin_id) - program_id = pin.program_id store_id = pin.store_id db.delete(pin) diff --git a/app/modules/loyalty/services/points_service.py b/app/modules/loyalty/services/points_service.py index c584d83e..5c2218d8 100644 --- a/app/modules/loyalty/services/points_service.py +++ b/app/modules/loyalty/services/points_service.py @@ -27,7 +27,7 @@ from app.modules.loyalty.exceptions import ( LoyaltyProgramInactiveException, StaffPinRequiredException, ) -from app.modules.loyalty.models import LoyaltyCard, LoyaltyTransaction, TransactionType +from app.modules.loyalty.models import LoyaltyTransaction, TransactionType from app.modules.loyalty.services.card_service import card_service from app.modules.loyalty.services.pin_service import pin_service diff --git a/app/modules/loyalty/services/program_service.py b/app/modules/loyalty/services/program_service.py index aea336ef..e38f5225 100644 --- a/app/modules/loyalty/services/program_service.py +++ b/app/modules/loyalty/services/program_service.py @@ -25,7 +25,6 @@ from app.modules.loyalty.exceptions import ( ) from app.modules.loyalty.models import ( LoyaltyProgram, - LoyaltyType, MerchantLoyaltySettings, ) from app.modules.loyalty.schemas.program import ( @@ -512,7 +511,7 @@ class ProgramService: "id": program.id, "display_name": program.display_name, "card_name": program.card_name, - "loyalty_type": program.loyalty_type.value if hasattr(program.loyalty_type, 'value') else str(program.loyalty_type), + "loyalty_type": program.loyalty_type.value if hasattr(program.loyalty_type, "value") else str(program.loyalty_type), "points_per_euro": program.points_per_euro, "welcome_bonus_points": program.welcome_bonus_points, "minimum_redemption_points": program.minimum_redemption_points, diff --git a/app/modules/loyalty/services/stamp_service.py b/app/modules/loyalty/services/stamp_service.py index 6773d9a7..946d2b02 100644 --- a/app/modules/loyalty/services/stamp_service.py +++ b/app/modules/loyalty/services/stamp_service.py @@ -19,16 +19,15 @@ from datetime import UTC, datetime, timedelta from sqlalchemy.orm import Session -from app.modules.loyalty.config import config from app.modules.loyalty.exceptions import ( DailyStampLimitException, InsufficientStampsException, LoyaltyCardInactiveException, LoyaltyProgramInactiveException, - StampCooldownException, StaffPinRequiredException, + StampCooldownException, ) -from app.modules.loyalty.models import LoyaltyCard, LoyaltyTransaction, TransactionType +from app.modules.loyalty.models import LoyaltyTransaction, TransactionType from app.modules.loyalty.services.card_service import card_service from app.modules.loyalty.services.pin_service import pin_service diff --git a/app/modules/loyalty/services/wallet_service.py b/app/modules/loyalty/services/wallet_service.py index e9093bad..38417f57 100644 --- a/app/modules/loyalty/services/wallet_service.py +++ b/app/modules/loyalty/services/wallet_service.py @@ -10,7 +10,7 @@ import logging from sqlalchemy.orm import Session -from app.modules.loyalty.models import LoyaltyCard, LoyaltyProgram +from app.modules.loyalty.models import LoyaltyCard logger = logging.getLogger(__name__) @@ -33,8 +33,12 @@ class WalletService: Returns: Dict with google_wallet_url and apple_wallet_url """ - from app.modules.loyalty.services.apple_wallet_service import apple_wallet_service - from app.modules.loyalty.services.google_wallet_service import google_wallet_service + from app.modules.loyalty.services.apple_wallet_service import ( + apple_wallet_service, + ) + from app.modules.loyalty.services.google_wallet_service import ( + google_wallet_service, + ) urls = { "google_wallet_url": None, @@ -72,15 +76,18 @@ class WalletService: Returns: Dict with success status for each wallet """ - from app.modules.loyalty.services.apple_wallet_service import apple_wallet_service - from app.modules.loyalty.services.google_wallet_service import google_wallet_service + from app.modules.loyalty.services.apple_wallet_service import ( + apple_wallet_service, + ) + from app.modules.loyalty.services.google_wallet_service import ( + google_wallet_service, + ) results = { "google_wallet": False, "apple_wallet": False, } - program = card.program # Sync to Google Wallet if card.google_object_id: @@ -113,7 +120,9 @@ class WalletService: Returns: Dict with success status for each wallet """ - from app.modules.loyalty.services.google_wallet_service import google_wallet_service + from app.modules.loyalty.services.google_wallet_service import ( + google_wallet_service, + ) results = { "google_wallet": False, diff --git a/app/modules/marketplace/__init__.py b/app/modules/marketplace/__init__.py index 3f4aa08e..10c45fe8 100644 --- a/app/modules/marketplace/__init__.py +++ b/app/modules/marketplace/__init__.py @@ -41,7 +41,9 @@ def __getattr__(name: str): if name == "marketplace_module": from app.modules.marketplace.definition import marketplace_module return marketplace_module - elif name == "get_marketplace_module_with_routers": - from app.modules.marketplace.definition import get_marketplace_module_with_routers + if name == "get_marketplace_module_with_routers": + from app.modules.marketplace.definition import ( + get_marketplace_module_with_routers, + ) return get_marketplace_module_with_routers raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/app/modules/marketplace/definition.py b/app/modules/marketplace/definition.py index 0a7b5c37..5231e30a 100644 --- a/app/modules/marketplace/definition.py +++ b/app/modules/marketplace/definition.py @@ -8,7 +8,13 @@ dependencies, route configurations, and scheduled tasks. Note: This module requires the inventory module to be enabled. """ -from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition, ScheduledTask +from app.modules.base import ( + MenuItemDefinition, + MenuSectionDefinition, + ModuleDefinition, + PermissionDefinition, + ScheduledTask, +) from app.modules.enums import FrontendType @@ -28,21 +34,27 @@ def _get_store_router(): def _get_metrics_provider(): """Lazy import of metrics provider to avoid circular imports.""" - from app.modules.marketplace.services.marketplace_metrics import marketplace_metrics_provider + from app.modules.marketplace.services.marketplace_metrics import ( + marketplace_metrics_provider, + ) return marketplace_metrics_provider def _get_widget_provider(): """Lazy import of widget provider to avoid circular imports.""" - from app.modules.marketplace.services.marketplace_widgets import marketplace_widget_provider + from app.modules.marketplace.services.marketplace_widgets import ( + marketplace_widget_provider, + ) return marketplace_widget_provider def _get_feature_provider(): """Lazy import of feature provider to avoid circular imports.""" - from app.modules.marketplace.services.marketplace_features import marketplace_feature_provider + from app.modules.marketplace.services.marketplace_features import ( + marketplace_feature_provider, + ) return marketplace_feature_provider diff --git a/app/modules/marketplace/migrations/versions/marketplace_001_initial.py b/app/modules/marketplace/migrations/versions/marketplace_001_initial.py index cb526e73..06c84c20 100644 --- a/app/modules/marketplace/migrations/versions/marketplace_001_initial.py +++ b/app/modules/marketplace/migrations/versions/marketplace_001_initial.py @@ -4,9 +4,10 @@ Revision ID: marketplace_001 Revises: billing_001 Create Date: 2026-02-07 """ -from alembic import op import sqlalchemy as sa +from alembic import op + revision = "marketplace_001" down_revision = "billing_001" branch_labels = None diff --git a/app/modules/marketplace/models/__init__.py b/app/modules/marketplace/models/__init__.py index b9a62d52..81bf2f76 100644 --- a/app/modules/marketplace/models/__init__.py +++ b/app/modules/marketplace/models/__init__.py @@ -25,37 +25,36 @@ Usage: # # NOTE: This module owns the relationships to tenancy models (User, Store). # Core models should NOT have back-references to optional module models. -from app.modules.orders.models import Order # noqa: F401 -from app.modules.tenancy.models.user import User # noqa: F401 -from app.modules.tenancy.models.store import Store # noqa: F401 - +from app.modules.marketplace.models.letzshop import ( + LetzshopFulfillmentQueue, + # Import jobs + LetzshopHistoricalImportJob, + LetzshopStoreCache, + LetzshopSyncLog, + # Letzshop credentials and sync + StoreLetzshopCredentials, +) +from app.modules.marketplace.models.marketplace_import_job import ( + MarketplaceImportError, + MarketplaceImportJob, +) from app.modules.marketplace.models.marketplace_product import ( + DigitalDeliveryMethod, MarketplaceProduct, ProductType, - DigitalDeliveryMethod, ) from app.modules.marketplace.models.marketplace_product_translation import ( MarketplaceProductTranslation, ) -from app.modules.marketplace.models.marketplace_import_job import ( - MarketplaceImportJob, - MarketplaceImportError, -) -from app.modules.marketplace.models.letzshop import ( - # Letzshop credentials and sync - StoreLetzshopCredentials, - LetzshopFulfillmentQueue, - LetzshopStoreCache, - LetzshopSyncLog, - # Import jobs - LetzshopHistoricalImportJob, -) from app.modules.marketplace.models.onboarding import ( + STEP_ORDER, OnboardingStatus, OnboardingStep, - STEP_ORDER, StoreOnboarding, ) +from app.modules.orders.models import Order # noqa: F401 +from app.modules.tenancy.models.store import Store # noqa: F401 +from app.modules.tenancy.models.user import User # noqa: F401 __all__ = [ # Marketplace products diff --git a/app/modules/marketplace/routes/api/admin.py b/app/modules/marketplace/routes/api/admin.py index 369da1c0..6a1fe9ce 100644 --- a/app/modules/marketplace/routes/api/admin.py +++ b/app/modules/marketplace/routes/api/admin.py @@ -14,9 +14,9 @@ Includes: from fastapi import APIRouter -from .admin_products import admin_products_router -from .admin_marketplace import admin_marketplace_router from .admin_letzshop import admin_letzshop_router +from .admin_marketplace import admin_marketplace_router +from .admin_products import admin_products_router # Create aggregate router for auto-discovery # The router is named 'admin_router' for auto-discovery compatibility diff --git a/app/modules/marketplace/routes/api/admin_letzshop.py b/app/modules/marketplace/routes/api/admin_letzshop.py index 85201753..ece06c30 100644 --- a/app/modules/marketplace/routes/api/admin_letzshop.py +++ b/app/modules/marketplace/routes/api/admin_letzshop.py @@ -20,19 +20,6 @@ from app.api.deps import get_current_admin_api, require_module_access from app.core.database import get_db from app.exceptions import ResourceNotFoundException, ValidationException from app.modules.enums import FrontendType -from app.modules.orders.exceptions import OrderHasUnresolvedExceptionsException -from app.modules.orders.services.order_item_exception_service import order_item_exception_service -from app.modules.marketplace.services.letzshop import ( - CredentialsNotFoundError, - LetzshopClientError, - LetzshopCredentialsService, - LetzshopOrderService, - LetzshopStoreSyncService, - OrderNotFoundError, - StoreNotFoundError, -) -from app.modules.marketplace.tasks import process_historical_import -from models.schema.auth import UserContext from app.modules.marketplace.schemas import ( FulfillmentOperationResponse, LetzshopCachedStoreDetail, @@ -56,15 +43,28 @@ from app.modules.marketplace.schemas import ( LetzshopOrderListResponse, LetzshopOrderResponse, LetzshopOrderStats, + LetzshopStoreDirectoryStats, + LetzshopStoreDirectoryStatsResponse, + LetzshopStoreListResponse, + LetzshopStoreOverview, LetzshopSuccessResponse, LetzshopSyncTriggerRequest, LetzshopSyncTriggerResponse, - LetzshopStoreDirectoryStats, - LetzshopStoreDirectoryStatsResponse, - LetzshopStoreDirectorySyncResponse, - LetzshopStoreListResponse, - LetzshopStoreOverview, ) +from app.modules.marketplace.services.letzshop import ( + CredentialsNotFoundError, + LetzshopClientError, + LetzshopCredentialsService, + LetzshopOrderService, + LetzshopStoreSyncService, + OrderNotFoundError, + StoreNotFoundError, +) +from app.modules.orders.exceptions import OrderHasUnresolvedExceptionsException +from app.modules.orders.services.order_item_exception_service import ( + order_item_exception_service, +) +from models.schema.auth import UserContext admin_letzshop_router = APIRouter( prefix="/letzshop", @@ -1557,7 +1557,9 @@ def export_store_products_letzshop( """ from fastapi.responses import Response - from app.modules.marketplace.services.letzshop_export_service import letzshop_export_service + from app.modules.marketplace.services.letzshop_export_service import ( + letzshop_export_service, + ) order_service = get_order_service(db) @@ -1616,7 +1618,9 @@ def export_store_products_letzshop_to_folder( from pathlib import Path as FilePath from app.core.config import settings - from app.modules.marketplace.services.letzshop_export_service import letzshop_export_service + from app.modules.marketplace.services.letzshop_export_service import ( + letzshop_export_service, + ) order_service = get_order_service(db) diff --git a/app/modules/marketplace/routes/api/admin_marketplace.py b/app/modules/marketplace/routes/api/admin_marketplace.py index 644fcaa2..78d5ca1b 100644 --- a/app/modules/marketplace/routes/api/admin_marketplace.py +++ b/app/modules/marketplace/routes/api/admin_marketplace.py @@ -12,11 +12,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api, require_module_access from app.core.database import get_db -from app.modules.enums import FrontendType -from app.modules.marketplace.services.marketplace_import_job_service import marketplace_import_job_service +from app.modules.analytics.schemas import ImportStatsResponse from app.modules.analytics.services.stats_service import stats_service -from app.modules.tenancy.services.store_service import store_service -from models.schema.auth import UserContext +from app.modules.enums import FrontendType from app.modules.marketplace.schemas import ( AdminMarketplaceImportJobListResponse, AdminMarketplaceImportJobRequest, @@ -26,7 +24,11 @@ from app.modules.marketplace.schemas import ( MarketplaceImportJobRequest, MarketplaceImportJobResponse, ) -from app.modules.analytics.schemas import ImportStatsResponse +from app.modules.marketplace.services.marketplace_import_job_service import ( + marketplace_import_job_service, +) +from app.modules.tenancy.services.store_service import store_service +from models.schema.auth import UserContext admin_marketplace_router = APIRouter( prefix="/marketplace-import-jobs", diff --git a/app/modules/marketplace/routes/api/admin_products.py b/app/modules/marketplace/routes/api/admin_products.py index 87d55708..e22fb3f0 100644 --- a/app/modules/marketplace/routes/api/admin_products.py +++ b/app/modules/marketplace/routes/api/admin_products.py @@ -21,7 +21,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api, require_module_access from app.core.database import get_db from app.modules.enums import FrontendType -from app.modules.marketplace.services.marketplace_product_service import marketplace_product_service +from app.modules.marketplace.services.marketplace_product_service import ( + marketplace_product_service, +) from models.schema.auth import UserContext admin_products_router = APIRouter( diff --git a/app/modules/marketplace/routes/api/platform.py b/app/modules/marketplace/routes/api/platform.py index 0be65e02..b4cec282 100644 --- a/app/modules/marketplace/routes/api/platform.py +++ b/app/modules/marketplace/routes/api/platform.py @@ -18,8 +18,8 @@ from sqlalchemy.orm import Session from app.core.database import get_db from app.exceptions import ResourceNotFoundException -from app.modules.marketplace.services.letzshop import LetzshopStoreSyncService from app.modules.marketplace.models import LetzshopStoreCache +from app.modules.marketplace.services.letzshop import LetzshopStoreSyncService router = APIRouter(prefix="/letzshop-stores") logger = logging.getLogger(__name__) diff --git a/app/modules/marketplace/routes/api/store.py b/app/modules/marketplace/routes/api/store.py index 5f88ccd4..b0723e57 100644 --- a/app/modules/marketplace/routes/api/store.py +++ b/app/modules/marketplace/routes/api/store.py @@ -13,8 +13,8 @@ Includes: from fastapi import APIRouter -from .store_marketplace import store_marketplace_router from .store_letzshop import store_letzshop_router +from .store_marketplace import store_marketplace_router from .store_onboarding import store_onboarding_router # Create aggregate router for auto-discovery diff --git a/app/modules/marketplace/routes/api/store_letzshop.py b/app/modules/marketplace/routes/api/store_letzshop.py index 770845e0..d0d441f9 100644 --- a/app/modules/marketplace/routes/api/store_letzshop.py +++ b/app/modules/marketplace/routes/api/store_letzshop.py @@ -21,17 +21,7 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_api, require_module_access from app.core.database import get_db from app.exceptions import ResourceNotFoundException, ValidationException -from app.modules.orders.exceptions import OrderHasUnresolvedExceptionsException -from app.modules.orders.services.order_item_exception_service import order_item_exception_service -from app.modules.marketplace.services.letzshop import ( - CredentialsNotFoundError, - LetzshopClientError, - LetzshopCredentialsService, - LetzshopOrderService, - OrderNotFoundError, -) from app.modules.enums import FrontendType -from models.schema.auth import UserContext from app.modules.marketplace.schemas import ( FulfillmentConfirmRequest, FulfillmentOperationResponse, @@ -54,6 +44,18 @@ from app.modules.marketplace.schemas import ( LetzshopSyncTriggerRequest, LetzshopSyncTriggerResponse, ) +from app.modules.marketplace.services.letzshop import ( + CredentialsNotFoundError, + LetzshopClientError, + LetzshopCredentialsService, + LetzshopOrderService, + OrderNotFoundError, +) +from app.modules.orders.exceptions import OrderHasUnresolvedExceptionsException +from app.modules.orders.services.order_item_exception_service import ( + order_item_exception_service, +) +from models.schema.auth import UserContext store_letzshop_router = APIRouter( prefix="/letzshop", @@ -767,7 +769,9 @@ def export_products_letzshop( """ from fastapi.responses import Response - from app.modules.marketplace.services.letzshop_export_service import letzshop_export_service + from app.modules.marketplace.services.letzshop_export_service import ( + letzshop_export_service, + ) from app.modules.tenancy.services.store_service import store_service store_id = current_user.token_store_id diff --git a/app/modules/marketplace/routes/api/store_marketplace.py b/app/modules/marketplace/routes/api/store_marketplace.py index bd586001..826d9953 100644 --- a/app/modules/marketplace/routes/api/store_marketplace.py +++ b/app/modules/marketplace/routes/api/store_marketplace.py @@ -16,14 +16,16 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_api, require_module_access from app.core.database import get_db from app.modules.enums import FrontendType -from app.modules.marketplace.services.marketplace_import_job_service import marketplace_import_job_service -from app.modules.tenancy.services.store_service import store_service -from middleware.decorators import rate_limit -from models.schema.auth import UserContext from app.modules.marketplace.schemas import ( MarketplaceImportJobRequest, MarketplaceImportJobResponse, ) +from app.modules.marketplace.services.marketplace_import_job_service import ( + marketplace_import_job_service, +) +from app.modules.tenancy.services.store_service import store_service +from middleware.decorators import rate_limit +from models.schema.auth import UserContext store_marketplace_router = APIRouter( prefix="/marketplace", diff --git a/app/modules/marketplace/routes/api/store_onboarding.py b/app/modules/marketplace/routes/api/store_onboarding.py index 0c2d7265..13613bab 100644 --- a/app/modules/marketplace/routes/api/store_onboarding.py +++ b/app/modules/marketplace/routes/api/store_onboarding.py @@ -21,15 +21,13 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_api, require_module_access from app.core.database import get_db from app.modules.enums import FrontendType -from app.modules.marketplace.services.onboarding_service import OnboardingService -from models.schema.auth import UserContext from app.modules.marketplace.schemas import ( - MerchantProfileRequest, - MerchantProfileResponse, LetzshopApiConfigRequest, LetzshopApiConfigResponse, LetzshopApiTestRequest, LetzshopApiTestResponse, + MerchantProfileRequest, + MerchantProfileResponse, OnboardingStatusResponse, OrderSyncCompleteRequest, OrderSyncCompleteResponse, @@ -39,6 +37,8 @@ from app.modules.marketplace.schemas import ( ProductImportConfigRequest, ProductImportConfigResponse, ) +from app.modules.marketplace.services.onboarding_service import OnboardingService +from models.schema.auth import UserContext store_onboarding_router = APIRouter( prefix="/onboarding", diff --git a/app/modules/marketplace/routes/pages/admin.py b/app/modules/marketplace/routes/pages/admin.py index 247ef383..f8447304 100644 --- a/app/modules/marketplace/routes/pages/admin.py +++ b/app/modules/marketplace/routes/pages/admin.py @@ -17,9 +17,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access from app.core.config import settings from app.modules.core.utils.page_context import get_admin_context -from app.templates_config import templates from app.modules.enums import FrontendType from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/marketplace/routes/pages/store.py b/app/modules/marketplace/routes/pages/store.py index c7e8d356..ef52ec47 100644 --- a/app/modules/marketplace/routes/pages/store.py +++ b/app/modules/marketplace/routes/pages/store.py @@ -16,8 +16,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_from_cookie_or_header, get_db from app.modules.core.utils.page_context import get_store_context from app.modules.marketplace.services.onboarding_service import OnboardingService -from app.templates_config import templates from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/marketplace/schemas/__init__.py b/app/modules/marketplace/schemas/__init__.py index 9e9039bc..a9ac95cd 100644 --- a/app/modules/marketplace/schemas/__init__.py +++ b/app/modules/marketplace/schemas/__init__.py @@ -12,113 +12,113 @@ Usage: ) """ +from app.modules.marketplace.schemas.letzshop import ( + # Fulfillment + FulfillmentConfirmRequest, + FulfillmentOperationResponse, + FulfillmentQueueItemResponse, + FulfillmentQueueListResponse, + FulfillmentRejectRequest, + FulfillmentTrackingRequest, + LetzshopCachedStoreDetail, + LetzshopCachedStoreDetailResponse, + # Store Directory + LetzshopCachedStoreItem, + LetzshopCachedStoreListResponse, + # Connection + LetzshopConnectionTestRequest, + LetzshopConnectionTestResponse, + LetzshopCreateStoreFromCacheResponse, + # Credentials + LetzshopCredentialsCreate, + LetzshopCredentialsResponse, + LetzshopCredentialsStatus, + LetzshopCredentialsUpdate, + LetzshopExportFileInfo, + # Product Export + LetzshopExportRequest, + LetzshopExportResponse, + # Historical Import + LetzshopHistoricalImportJobResponse, + LetzshopHistoricalImportStartResponse, + # Jobs + LetzshopJobItem, + LetzshopJobsListResponse, + LetzshopOrderDetailResponse, + # Orders + LetzshopOrderItemResponse, + LetzshopOrderListResponse, + LetzshopOrderResponse, + LetzshopOrderStats, + LetzshopStoreDirectoryStats, + LetzshopStoreDirectoryStatsResponse, + LetzshopStoreDirectorySyncResponse, + LetzshopStoreListResponse, + # Admin + LetzshopStoreOverview, + LetzshopSuccessResponse, + LetzshopSyncLogListResponse, + # Sync + LetzshopSyncLogResponse, + LetzshopSyncTriggerRequest, + LetzshopSyncTriggerResponse, +) from app.modules.marketplace.schemas.marketplace_import_job import ( - MarketplaceImportJobRequest, - AdminMarketplaceImportJobRequest, - MarketplaceImportJobResponse, - MarketplaceImportJobListResponse, - MarketplaceImportErrorResponse, - MarketplaceImportErrorListResponse, - AdminMarketplaceImportJobResponse, AdminMarketplaceImportJobListResponse, + AdminMarketplaceImportJobRequest, + AdminMarketplaceImportJobResponse, + MarketplaceImportErrorListResponse, + MarketplaceImportErrorResponse, + MarketplaceImportJobListResponse, + MarketplaceImportJobRequest, + MarketplaceImportJobResponse, MarketplaceImportJobStatusUpdate, ) from app.modules.marketplace.schemas.marketplace_product import ( - # Translation schemas - MarketplaceProductTranslationSchema, + # Import schemas + MarketplaceImportRequest, + MarketplaceImportResponse, # Base schemas MarketplaceProductBase, # CRUD schemas MarketplaceProductCreate, - MarketplaceProductUpdate, + MarketplaceProductDetailResponse, + MarketplaceProductListResponse, # Response schemas MarketplaceProductResponse, - MarketplaceProductListResponse, - MarketplaceProductDetailResponse, - # Import schemas - MarketplaceImportRequest, - MarketplaceImportResponse, -) -from app.modules.marketplace.schemas.letzshop import ( - # Credentials - LetzshopCredentialsCreate, - LetzshopCredentialsUpdate, - LetzshopCredentialsResponse, - LetzshopCredentialsStatus, - # Orders - LetzshopOrderItemResponse, - LetzshopOrderResponse, - LetzshopOrderDetailResponse, - LetzshopOrderStats, - LetzshopOrderListResponse, - # Fulfillment - FulfillmentConfirmRequest, - FulfillmentRejectRequest, - FulfillmentTrackingRequest, - FulfillmentQueueItemResponse, - FulfillmentQueueListResponse, - FulfillmentOperationResponse, - # Sync - LetzshopSyncLogResponse, - LetzshopSyncLogListResponse, - LetzshopSyncTriggerRequest, - LetzshopSyncTriggerResponse, - # Connection - LetzshopConnectionTestRequest, - LetzshopConnectionTestResponse, - LetzshopSuccessResponse, - # Admin - LetzshopStoreOverview, - LetzshopStoreListResponse, - # Jobs - LetzshopJobItem, - LetzshopJobsListResponse, - # Historical Import - LetzshopHistoricalImportJobResponse, - LetzshopHistoricalImportStartResponse, - # Store Directory - LetzshopCachedStoreItem, - LetzshopCachedStoreDetail, - LetzshopStoreDirectoryStats, - LetzshopStoreDirectoryStatsResponse, - LetzshopCachedStoreListResponse, - LetzshopCachedStoreDetailResponse, - LetzshopStoreDirectorySyncResponse, - LetzshopCreateStoreFromCacheResponse, - # Product Export - LetzshopExportRequest, - LetzshopExportFileInfo, - LetzshopExportResponse, + # Translation schemas + MarketplaceProductTranslationSchema, + MarketplaceProductUpdate, ) from app.modules.marketplace.schemas.onboarding import ( - # Step status - StepStatus, - MerchantProfileStepStatus, + # Step 2 + LetzshopApiConfigRequest, + LetzshopApiConfigResponse, LetzshopApiStepStatus, - ProductImportStepStatus, - OrderSyncStepStatus, - # Main status - OnboardingStatusResponse, + LetzshopApiTestRequest, + LetzshopApiTestResponse, # Step 1 MerchantProfileRequest, MerchantProfileResponse, - # Step 2 - LetzshopApiConfigRequest, - LetzshopApiTestRequest, - LetzshopApiTestResponse, - LetzshopApiConfigResponse, - # Step 3 - ProductImportConfigRequest, - ProductImportConfigResponse, - # Step 4 - OrderSyncTriggerRequest, - OrderSyncTriggerResponse, - OrderSyncProgressResponse, - OrderSyncCompleteRequest, - OrderSyncCompleteResponse, + MerchantProfileStepStatus, # Admin OnboardingSkipRequest, OnboardingSkipResponse, + # Main status + OnboardingStatusResponse, + OrderSyncCompleteRequest, + OrderSyncCompleteResponse, + OrderSyncProgressResponse, + OrderSyncStepStatus, + # Step 4 + OrderSyncTriggerRequest, + OrderSyncTriggerResponse, + # Step 3 + ProductImportConfigRequest, + ProductImportConfigResponse, + ProductImportStepStatus, + # Step status + StepStatus, ) __all__ = [ diff --git a/app/modules/marketplace/schemas/onboarding.py b/app/modules/marketplace/schemas/onboarding.py index 80b4e267..5054478e 100644 --- a/app/modules/marketplace/schemas/onboarding.py +++ b/app/modules/marketplace/schemas/onboarding.py @@ -15,7 +15,6 @@ from datetime import datetime from pydantic import BaseModel, ConfigDict, Field - # ============================================================================= # STEP STATUS MODELS # ============================================================================= diff --git a/app/modules/marketplace/services/__init__.py b/app/modules/marketplace/services/__init__.py index 321c5ce6..0cfb8477 100644 --- a/app/modules/marketplace/services/__init__.py +++ b/app/modules/marketplace/services/__init__.py @@ -6,6 +6,18 @@ This module contains the canonical implementations of marketplace-related servic """ # Main marketplace services +# Letzshop submodule services +from app.modules.marketplace.services.letzshop import ( + LetzshopClient, + LetzshopClientError, +) +from app.modules.marketplace.services.letzshop.credentials_service import ( + LetzshopCredentialsService, +) +from app.modules.marketplace.services.letzshop.order_service import LetzshopOrderService +from app.modules.marketplace.services.letzshop.store_sync_service import ( + LetzshopStoreSyncService, +) from app.modules.marketplace.services.letzshop_export_service import ( LetzshopExportService, letzshop_export_service, @@ -23,24 +35,11 @@ from app.modules.marketplace.services.onboarding_service import ( get_onboarding_service, ) from app.modules.marketplace.services.platform_signup_service import ( - PlatformSignupService, - platform_signup_service, - SignupSessionData, AccountCreationResult, + PlatformSignupService, SignupCompletionResult, -) - -# Letzshop submodule services -from app.modules.marketplace.services.letzshop import ( - LetzshopClient, - LetzshopClientError, -) -from app.modules.marketplace.services.letzshop.credentials_service import ( - LetzshopCredentialsService, -) -from app.modules.marketplace.services.letzshop.order_service import LetzshopOrderService -from app.modules.marketplace.services.letzshop.store_sync_service import ( - LetzshopStoreSyncService, + SignupSessionData, + platform_signup_service, ) __all__ = [ diff --git a/app/modules/marketplace/services/letzshop/client_service.py b/app/modules/marketplace/services/letzshop/client_service.py index 1eb9be14..3096cfa6 100644 --- a/app/modules/marketplace/services/letzshop/client_service.py +++ b/app/modules/marketplace/services/letzshop/client_service.py @@ -8,7 +8,8 @@ for all Letzshop API operations. import logging import time -from typing import Any, Callable +from collections.abc import Callable +from typing import Any import requests diff --git a/app/modules/marketplace/services/letzshop/credentials_service.py b/app/modules/marketplace/services/letzshop/credentials_service.py index 14799be0..247068ec 100644 --- a/app/modules/marketplace/services/letzshop/credentials_service.py +++ b/app/modules/marketplace/services/letzshop/credentials_service.py @@ -10,8 +10,8 @@ from datetime import UTC, datetime from sqlalchemy.orm import Session -from app.utils.encryption import decrypt_value, encrypt_value, mask_api_key from app.modules.marketplace.models import StoreLetzshopCredentials +from app.utils.encryption import decrypt_value, encrypt_value, mask_api_key from .client_service import LetzshopClient diff --git a/app/modules/marketplace/services/letzshop/order_service.py b/app/modules/marketplace/services/letzshop/order_service.py index d3f805f8..19c57c5c 100644 --- a/app/modules/marketplace/services/letzshop/order_service.py +++ b/app/modules/marketplace/services/letzshop/order_service.py @@ -8,14 +8,15 @@ with `channel='letzshop'`. """ import logging +from collections.abc import Callable from datetime import UTC, datetime -from typing import Any, Callable +from typing import Any -from sqlalchemy import String, and_, func, or_ +from sqlalchemy import func, or_ from sqlalchemy.orm import Session -from app.modules.orders.services.order_service import order_service as unified_order_service from app.modules.billing.services.subscription_service import subscription_service +from app.modules.catalog.models import Product from app.modules.marketplace.models import ( LetzshopFulfillmentQueue, LetzshopHistoricalImportJob, @@ -24,7 +25,9 @@ from app.modules.marketplace.models import ( StoreLetzshopCredentials, ) from app.modules.orders.models import Order, OrderItem -from app.modules.catalog.models import Product +from app.modules.orders.services.order_service import ( + order_service as unified_order_service, +) from app.modules.tenancy.models import Store logger = logging.getLogger(__name__) @@ -1049,7 +1052,7 @@ class LetzshopOrderService: return { "total_orders": total_orders, "unique_customers": unique_customers, - "orders_by_status": {status: count for status, count in status_counts}, + "orders_by_status": dict(status_counts), "orders_by_locale": { locale or "unknown": count for locale, count in locale_counts }, diff --git a/app/modules/marketplace/services/letzshop/store_sync_service.py b/app/modules/marketplace/services/letzshop/store_sync_service.py index a973a32a..8e5a064f 100644 --- a/app/modules/marketplace/services/letzshop/store_sync_service.py +++ b/app/modules/marketplace/services/letzshop/store_sync_service.py @@ -7,16 +7,17 @@ in the letzshop_store_cache table for fast lookups during signup. """ import logging +from collections.abc import Callable from datetime import UTC, datetime -from typing import Any, Callable +from typing import Any from sqlalchemy import func -from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.orm import Session -from .client_service import LetzshopClient from app.modules.marketplace.models import LetzshopStoreCache +from .client_service import LetzshopClient + logger = logging.getLogger(__name__) @@ -146,14 +147,13 @@ class LetzshopStoreSyncService: setattr(existing, key, value) existing.last_synced_at = datetime.now(UTC) return "updated" - else: - # Create new record - cache_entry = LetzshopStoreCache( - **parsed, - last_synced_at=datetime.now(UTC), - ) - self.db.add(cache_entry) - return "created" + # Create new record + cache_entry = LetzshopStoreCache( + **parsed, + last_synced_at=datetime.now(UTC), + ) + self.db.add(cache_entry) + return "created" def _parse_store_data(self, data: dict[str, Any]) -> dict[str, Any]: """ @@ -436,10 +436,9 @@ class LetzshopStoreSyncService: from sqlalchemy import func - from app.modules.tenancy.services.admin_service import admin_service - from app.modules.tenancy.models import Merchant - from app.modules.tenancy.models import Store + from app.modules.tenancy.models import Merchant, Store from app.modules.tenancy.schemas.store import StoreCreate + from app.modules.tenancy.services.admin_service import admin_service # Get cache entry cache_entry = self.get_cached_store(letzshop_slug) diff --git a/app/modules/marketplace/services/letzshop_export_service.py b/app/modules/marketplace/services/letzshop_export_service.py index 585879bb..27fb7890 100644 --- a/app/modules/marketplace/services/letzshop_export_service.py +++ b/app/modules/marketplace/services/letzshop_export_service.py @@ -8,12 +8,12 @@ Generates Google Shopping compatible CSV files for Letzshop marketplace. import csv import io import logging -from datetime import UTC, datetime +from datetime import datetime from sqlalchemy.orm import Session, joinedload -from app.modules.marketplace.models import LetzshopSyncLog, MarketplaceProduct from app.modules.catalog.models import Product +from app.modules.marketplace.models import LetzshopSyncLog, MarketplaceProduct logger = logging.getLogger(__name__) diff --git a/app/modules/marketplace/services/marketplace_features.py b/app/modules/marketplace/services/marketplace_features.py index 0327738d..f389c26f 100644 --- a/app/modules/marketplace/services/marketplace_features.py +++ b/app/modules/marketplace/services/marketplace_features.py @@ -13,7 +13,6 @@ from typing import TYPE_CHECKING from app.modules.contracts.features import ( FeatureDeclaration, - FeatureProviderProtocol, FeatureScope, FeatureType, FeatureUsage, diff --git a/app/modules/marketplace/services/marketplace_import_job_service.py b/app/modules/marketplace/services/marketplace_import_job_service.py index 7051309b..124a696e 100644 --- a/app/modules/marketplace/services/marketplace_import_job_service.py +++ b/app/modules/marketplace/services/marketplace_import_job_service.py @@ -12,13 +12,12 @@ from app.modules.marketplace.models import ( MarketplaceImportError, MarketplaceImportJob, ) -from app.modules.tenancy.models import User -from app.modules.tenancy.models import Store from app.modules.marketplace.schemas import ( AdminMarketplaceImportJobResponse, MarketplaceImportJobRequest, MarketplaceImportJobResponse, ) +from app.modules.tenancy.models import Store, User logger = logging.getLogger(__name__) diff --git a/app/modules/marketplace/services/marketplace_metrics.py b/app/modules/marketplace/services/marketplace_metrics.py index e995a3f8..0c36f389 100644 --- a/app/modules/marketplace/services/marketplace_metrics.py +++ b/app/modules/marketplace/services/marketplace_metrics.py @@ -16,9 +16,8 @@ from sqlalchemy import func from sqlalchemy.orm import Session from app.modules.contracts.metrics import ( - MetricValue, MetricsContext, - MetricsProviderProtocol, + MetricValue, ) if TYPE_CHECKING: @@ -51,7 +50,10 @@ class MarketplaceMetricsProvider: - Imported products (staging) - Import job statistics """ - from app.modules.marketplace.models import MarketplaceImportJob, MarketplaceProduct + from app.modules.marketplace.models import ( + MarketplaceImportJob, + MarketplaceProduct, + ) from app.modules.tenancy.models import Store try: @@ -194,7 +196,10 @@ class MarketplaceMetricsProvider: Aggregates import and staging data across all stores. """ - from app.modules.marketplace.models import MarketplaceImportJob, MarketplaceProduct + from app.modules.marketplace.models import ( + MarketplaceImportJob, + MarketplaceProduct, + ) from app.modules.tenancy.models import StorePlatform try: diff --git a/app/modules/marketplace/services/marketplace_product_service.py b/app/modules/marketplace/services/marketplace_product_service.py index 71570d98..cc315f9d 100644 --- a/app/modules/marketplace/services/marketplace_product_service.py +++ b/app/modules/marketplace/services/marketplace_product_service.py @@ -23,23 +23,26 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session, joinedload from app.exceptions import ValidationException +from app.modules.inventory.models import Inventory +from app.modules.inventory.schemas import ( + InventoryLocationResponse, + InventorySummaryResponse, +) from app.modules.marketplace.exceptions import ( InvalidMarketplaceProductDataException, MarketplaceProductAlreadyExistsException, MarketplaceProductNotFoundException, MarketplaceProductValidationException, ) -from app.utils.data_processing import GTINProcessor, PriceProcessor -from app.modules.inventory.models import Inventory from app.modules.marketplace.models import ( MarketplaceProduct, MarketplaceProductTranslation, ) -from app.modules.inventory.schemas import InventoryLocationResponse, InventorySummaryResponse from app.modules.marketplace.schemas import ( MarketplaceProductCreate, MarketplaceProductUpdate, ) +from app.utils.data_processing import GTINProcessor, PriceProcessor logger = logging.getLogger(__name__) @@ -859,8 +862,7 @@ class MarketplaceProductService: Returns: Dict with copied, skipped, failed counts and details """ - from app.modules.catalog.models import Product - from app.modules.catalog.models import ProductTranslation + from app.modules.catalog.models import Product, ProductTranslation from app.modules.tenancy.models import Store store = db.query(Store).filter(Store.id == store_id).first() @@ -880,9 +882,10 @@ class MarketplaceProductService: raise MarketplaceProductNotFoundException("No marketplace products found") # Check product limit from subscription - from app.modules.billing.services.feature_service import feature_service from sqlalchemy import func + from app.modules.billing.services.feature_service import feature_service + current_products = ( db.query(func.count(Product.id)) .filter(Product.store_id == store_id) diff --git a/app/modules/marketplace/services/marketplace_widgets.py b/app/modules/marketplace/services/marketplace_widgets.py index 2e449508..f74c8a2c 100644 --- a/app/modules/marketplace/services/marketplace_widgets.py +++ b/app/modules/marketplace/services/marketplace_widgets.py @@ -16,7 +16,6 @@ from sqlalchemy.orm import Session from app.modules.contracts.widgets import ( BreakdownWidget, DashboardWidget, - DashboardWidgetProviderProtocol, ListWidget, WidgetBreakdownItem, WidgetContext, @@ -140,7 +139,7 @@ class MarketplaceWidgetProvider: from sqlalchemy.orm import joinedload from app.modules.marketplace.models import MarketplaceImportJob - from app.modules.tenancy.models import Store, StorePlatform + from app.modules.tenancy.models import StorePlatform limit = context.limit if context else 5 diff --git a/app/modules/marketplace/services/onboarding_service.py b/app/modules/marketplace/services/onboarding_service.py index a1e18dd2..0d4c2ab2 100644 --- a/app/modules/marketplace/services/onboarding_service.py +++ b/app/modules/marketplace/services/onboarding_service.py @@ -14,7 +14,6 @@ from datetime import UTC, datetime from sqlalchemy.orm import Session -from app.modules.tenancy.exceptions import StoreNotFoundException from app.modules.marketplace.exceptions import ( OnboardingCsvUrlRequiredException, OnboardingNotFoundException, @@ -22,15 +21,16 @@ from app.modules.marketplace.exceptions import ( OnboardingSyncJobNotFoundException, OnboardingSyncNotCompleteException, ) -from app.modules.marketplace.services.letzshop import ( - LetzshopCredentialsService, - LetzshopOrderService, -) from app.modules.marketplace.models import ( OnboardingStatus, OnboardingStep, StoreOnboarding, ) +from app.modules.marketplace.services.letzshop import ( + LetzshopCredentialsService, + LetzshopOrderService, +) +from app.modules.tenancy.exceptions import StoreNotFoundException from app.modules.tenancy.models import Store logger = logging.getLogger(__name__) @@ -295,14 +295,13 @@ class OnboardingService: "store_id": None, "shop_slug": shop_slug, } - else: - return { - "success": False, - "message": error or "Connection failed", - "store_name": None, - "store_id": None, - "shop_slug": None, - } + return { + "success": False, + "message": error or "Connection failed", + "store_name": None, + "store_id": None, + "shop_slug": None, + } def complete_letzshop_api( self, diff --git a/app/modules/marketplace/services/platform_signup_service.py b/app/modules/marketplace/services/platform_signup_service.py index 22b5672b..f3ca3045 100644 --- a/app/modules/marketplace/services/platform_signup_service.py +++ b/app/modules/marketplace/services/platform_signup_service.py @@ -11,8 +11,8 @@ Handles all database operations for the platform signup flow: import logging import secrets -from datetime import UTC, datetime, timedelta from dataclasses import dataclass +from datetime import UTC, datetime, timedelta from sqlalchemy.orm import Session @@ -22,20 +22,26 @@ from app.exceptions import ( ResourceNotFoundException, ValidationException, ) -from app.modules.messaging.services.email_service import EmailService -from app.modules.marketplace.services.onboarding_service import OnboardingService -from app.modules.billing.services.stripe_service import stripe_service -from middleware.auth import AuthManager -from app.modules.tenancy.models import Merchant from app.modules.billing.models import ( - SubscriptionStatus, SubscriptionTier, TierCode, ) -from app.modules.billing.services.subscription_service import subscription_service as sub_service -from app.modules.tenancy.models import User -from app.modules.tenancy.models import Store, StorePlatform, StoreUser, StoreUserType -from app.modules.tenancy.models import Platform +from app.modules.billing.services.stripe_service import stripe_service +from app.modules.billing.services.subscription_service import ( + subscription_service as sub_service, +) +from app.modules.marketplace.services.onboarding_service import OnboardingService +from app.modules.messaging.services.email_service import EmailService +from app.modules.tenancy.models import ( + Merchant, + Platform, + Store, + StorePlatform, + StoreUser, + StoreUserType, + User, +) +from middleware.auth import AuthManager logger = logging.getLogger(__name__) @@ -221,7 +227,7 @@ class PlatformSignupService: ResourceNotFoundException: If session not found ConflictException: If store already claimed """ - session = self.get_session_or_raise(session_id) + self.get_session_or_raise(session_id) # Check if store is already claimed if self.check_store_claimed(db, letzshop_slug): diff --git a/app/modules/marketplace/tasks/__init__.py b/app/modules/marketplace/tasks/__init__.py index b1c04e6d..df67b612 100644 --- a/app/modules/marketplace/tasks/__init__.py +++ b/app/modules/marketplace/tasks/__init__.py @@ -9,17 +9,17 @@ Tasks for: - Product export to Letzshop CSV format """ +from app.modules.marketplace.tasks.export_tasks import ( + export_marketplace_products, + export_store_products_to_folder, +) from app.modules.marketplace.tasks.import_tasks import ( - process_marketplace_import, process_historical_import, + process_marketplace_import, ) from app.modules.marketplace.tasks.sync_tasks import ( sync_store_directory, ) -from app.modules.marketplace.tasks.export_tasks import ( - export_store_products_to_folder, - export_marketplace_products, -) __all__ = [ # Import tasks diff --git a/app/modules/marketplace/tasks/export_tasks.py b/app/modules/marketplace/tasks/export_tasks.py index 1a49354d..5faf5113 100644 --- a/app/modules/marketplace/tasks/export_tasks.py +++ b/app/modules/marketplace/tasks/export_tasks.py @@ -40,7 +40,9 @@ def export_store_products_to_folder( Returns: dict: Export results per language with file paths """ - from app.modules.marketplace.services.letzshop_export_service import letzshop_export_service + from app.modules.marketplace.services.letzshop_export_service import ( + letzshop_export_service, + ) languages = ["en", "fr", "de"] results = {} @@ -149,7 +151,9 @@ def export_marketplace_products( Returns: dict: Export result with file path """ - from app.modules.marketplace.services.letzshop_export_service import letzshop_export_service + from app.modules.marketplace.services.letzshop_export_service import ( + letzshop_export_service, + ) with self.get_db() as db: started_at = datetime.now(UTC) diff --git a/app/modules/marketplace/tasks/import_tasks.py b/app/modules/marketplace/tasks/import_tasks.py index 2958f8cc..b75383f1 100644 --- a/app/modules/marketplace/tasks/import_tasks.py +++ b/app/modules/marketplace/tasks/import_tasks.py @@ -9,20 +9,25 @@ Includes: import asyncio import logging +from collections.abc import Callable from datetime import UTC, datetime -from typing import Callable from app.core.celery_config import celery_app -from app.modules.marketplace.models import MarketplaceImportJob, LetzshopHistoricalImportJob -from app.modules.messaging.services.admin_notification_service import admin_notification_service +from app.modules.marketplace.models import ( + LetzshopHistoricalImportJob, + MarketplaceImportJob, +) from app.modules.marketplace.services.letzshop import ( LetzshopClientError, LetzshopCredentialsService, LetzshopOrderService, ) +from app.modules.messaging.services.admin_notification_service import ( + admin_notification_service, +) from app.modules.task_base import ModuleTask -from app.utils.csv_processor import CSVProcessor from app.modules.tenancy.models import Store +from app.utils.csv_processor import CSVProcessor logger = logging.getLogger(__name__) diff --git a/app/modules/marketplace/tasks/sync_tasks.py b/app/modules/marketplace/tasks/sync_tasks.py index 09458271..2b4b80dd 100644 --- a/app/modules/marketplace/tasks/sync_tasks.py +++ b/app/modules/marketplace/tasks/sync_tasks.py @@ -9,9 +9,11 @@ import logging from typing import Any from app.core.celery_config import celery_app -from app.modules.task_base import ModuleTask -from app.modules.messaging.services.admin_notification_service import admin_notification_service from app.modules.marketplace.services.letzshop import LetzshopStoreSyncService +from app.modules.messaging.services.admin_notification_service import ( + admin_notification_service, +) +from app.modules.task_base import ModuleTask logger = logging.getLogger(__name__) diff --git a/app/modules/marketplace/tests/unit/test_marketplace_product_service.py b/app/modules/marketplace/tests/unit/test_marketplace_product_service.py index 98b68260..c22e814a 100644 --- a/app/modules/marketplace/tests/unit/test_marketplace_product_service.py +++ b/app/modules/marketplace/tests/unit/test_marketplace_product_service.py @@ -16,16 +16,11 @@ import uuid import pytest -from app.exceptions import ValidationException from app.modules.marketplace.exceptions import ( InvalidMarketplaceProductDataException, MarketplaceProductNotFoundException, MarketplaceProductValidationException, ) -from app.modules.marketplace.services.marketplace_product_service import ( - MarketplaceProductService, - marketplace_product_service, -) from app.modules.marketplace.models import ( MarketplaceProduct, MarketplaceProductTranslation, @@ -34,6 +29,10 @@ from app.modules.marketplace.schemas import ( MarketplaceProductCreate, MarketplaceProductUpdate, ) +from app.modules.marketplace.services.marketplace_product_service import ( + MarketplaceProductService, + marketplace_product_service, +) @pytest.mark.unit diff --git a/app/modules/marketplace/tests/unit/test_onboarding_service.py b/app/modules/marketplace/tests/unit/test_onboarding_service.py index e31309b4..1436724b 100644 --- a/app/modules/marketplace/tests/unit/test_onboarding_service.py +++ b/app/modules/marketplace/tests/unit/test_onboarding_service.py @@ -15,8 +15,12 @@ from unittest.mock import MagicMock, patch import pytest +from app.modules.marketplace.models import ( + OnboardingStatus, + OnboardingStep, + StoreOnboarding, +) from app.modules.marketplace.services.onboarding_service import OnboardingService -from app.modules.marketplace.models import OnboardingStatus, OnboardingStep, StoreOnboarding @pytest.mark.unit @@ -525,7 +529,9 @@ class TestOnboardingServiceStep4: def test_complete_order_sync_raises_for_missing_job(self, db, test_store): """Test complete_order_sync raises for non-existent job""" - from app.modules.marketplace.exceptions import OnboardingSyncJobNotFoundException + from app.modules.marketplace.exceptions import ( + OnboardingSyncJobNotFoundException, + ) onboarding = StoreOnboarding( store_id=test_store.id, @@ -550,7 +556,9 @@ class TestOnboardingServiceStep4: def test_complete_order_sync_raises_if_not_complete(self, db, test_store): """Test complete_order_sync raises if job still running""" - from app.modules.marketplace.exceptions import OnboardingSyncNotCompleteException + from app.modules.marketplace.exceptions import ( + OnboardingSyncNotCompleteException, + ) onboarding = StoreOnboarding( store_id=test_store.id, diff --git a/app/modules/marketplace/tests/unit/test_product_service.py b/app/modules/marketplace/tests/unit/test_product_service.py index 18404c31..bcdc2e0e 100644 --- a/app/modules/marketplace/tests/unit/test_product_service.py +++ b/app/modules/marketplace/tests/unit/test_product_service.py @@ -7,11 +7,13 @@ from app.modules.marketplace.exceptions import ( MarketplaceProductNotFoundException, MarketplaceProductValidationException, ) -from app.modules.marketplace.services.marketplace_product_service import MarketplaceProductService from app.modules.marketplace.schemas import ( MarketplaceProductCreate, MarketplaceProductUpdate, ) +from app.modules.marketplace.services.marketplace_product_service import ( + MarketplaceProductService, +) @pytest.mark.unit diff --git a/app/modules/messaging/__init__.py b/app/modules/messaging/__init__.py index c616b8ec..a3ecfa60 100644 --- a/app/modules/messaging/__init__.py +++ b/app/modules/messaging/__init__.py @@ -26,7 +26,7 @@ def __getattr__(name: str): from app.modules.messaging.definition import messaging_module return messaging_module - elif name == "get_messaging_module_with_routers": + if name == "get_messaging_module_with_routers": from app.modules.messaging.definition import get_messaging_module_with_routers return get_messaging_module_with_routers diff --git a/app/modules/messaging/definition.py b/app/modules/messaging/definition.py index f7d41043..e5e34a3d 100644 --- a/app/modules/messaging/definition.py +++ b/app/modules/messaging/definition.py @@ -6,7 +6,12 @@ Defines the messaging module including its features, menu items, route configurations, and self-contained module settings. """ -from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition +from app.modules.base import ( + MenuItemDefinition, + MenuSectionDefinition, + ModuleDefinition, + PermissionDefinition, +) from app.modules.enums import FrontendType @@ -26,7 +31,9 @@ def _get_store_router(): def _get_feature_provider(): """Lazy import of feature provider to avoid circular imports.""" - from app.modules.messaging.services.messaging_features import messaging_feature_provider + from app.modules.messaging.services.messaging_features import ( + messaging_feature_provider, + ) return messaging_feature_provider diff --git a/app/modules/messaging/migrations/versions/messaging_001_initial.py b/app/modules/messaging/migrations/versions/messaging_001_initial.py index 9621033b..7013bf10 100644 --- a/app/modules/messaging/migrations/versions/messaging_001_initial.py +++ b/app/modules/messaging/migrations/versions/messaging_001_initial.py @@ -4,9 +4,10 @@ Revision ID: messaging_001 Revises: cart_001 Create Date: 2026-02-07 """ -from alembic import op import sqlalchemy as sa +from alembic import op + revision = "messaging_001" down_revision = "cart_001" branch_labels = None diff --git a/app/modules/messaging/models/__init__.py b/app/modules/messaging/models/__init__.py index 8b73f8b9..15a973e1 100644 --- a/app/modules/messaging/models/__init__.py +++ b/app/modules/messaging/models/__init__.py @@ -8,6 +8,13 @@ This module contains the canonical implementations of messaging-related models: - Email templates and settings: Email system """ +from app.modules.messaging.models.admin_notification import AdminNotification +from app.modules.messaging.models.email import ( + EmailCategory, + EmailLog, + EmailStatus, + EmailTemplate, +) from app.modules.messaging.models.message import ( Conversation, ConversationParticipant, @@ -16,16 +23,9 @@ from app.modules.messaging.models.message import ( MessageAttachment, ParticipantType, ) -from app.modules.messaging.models.admin_notification import AdminNotification -from app.modules.messaging.models.email import ( - EmailCategory, - EmailLog, - EmailStatus, - EmailTemplate, -) from app.modules.messaging.models.store_email_settings import ( - EmailProvider, PREMIUM_EMAIL_PROVIDERS, + EmailProvider, StoreEmailSettings, ) from app.modules.messaging.models.store_email_template import StoreEmailTemplate diff --git a/app/modules/messaging/models/admin_notification.py b/app/modules/messaging/models/admin_notification.py index 72532a54..10911f52 100644 --- a/app/modules/messaging/models/admin_notification.py +++ b/app/modules/messaging/models/admin_notification.py @@ -6,12 +6,12 @@ This model handles admin-specific notifications for system alerts and warnings. """ from sqlalchemy import ( + JSON, Boolean, Column, DateTime, ForeignKey, Integer, - JSON, String, Text, ) diff --git a/app/modules/messaging/models/message.py b/app/modules/messaging/models/message.py index 59754a8b..c21408a5 100644 --- a/app/modules/messaging/models/message.py +++ b/app/modules/messaging/models/message.py @@ -12,7 +12,6 @@ involving customers. """ import enum -from datetime import datetime from sqlalchemy import ( Boolean, diff --git a/app/modules/messaging/routes/__init__.py b/app/modules/messaging/routes/__init__.py index 17df68ee..9e39fabc 100644 --- a/app/modules/messaging/routes/__init__.py +++ b/app/modules/messaging/routes/__init__.py @@ -22,13 +22,13 @@ def __getattr__(name: str): if name == "admin_router": from app.modules.messaging.routes.admin import admin_router return admin_router - elif name == "admin_notifications_router": + if name == "admin_notifications_router": from app.modules.messaging.routes.admin import admin_notifications_router return admin_notifications_router - elif name == "store_router": + if name == "store_router": from app.modules.messaging.routes.store import store_router return store_router - elif name == "store_notifications_router": + if name == "store_notifications_router": from app.modules.messaging.routes.store import store_notifications_router return store_notifications_router raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/app/modules/messaging/routes/api/__init__.py b/app/modules/messaging/routes/api/__init__.py index 7bf93b39..7b02b581 100644 --- a/app/modules/messaging/routes/api/__init__.py +++ b/app/modules/messaging/routes/api/__init__.py @@ -17,8 +17,8 @@ Storefront routes: """ from app.modules.messaging.routes.api.admin import admin_router -from app.modules.messaging.routes.api.storefront import router as storefront_router from app.modules.messaging.routes.api.store import store_router +from app.modules.messaging.routes.api.storefront import router as storefront_router # Tag for OpenAPI documentation STOREFRONT_TAG = "Messages (Storefront)" diff --git a/app/modules/messaging/routes/api/admin.py b/app/modules/messaging/routes/api/admin.py index 0dd85c28..383b00de 100644 --- a/app/modules/messaging/routes/api/admin.py +++ b/app/modules/messaging/routes/api/admin.py @@ -13,9 +13,9 @@ from fastapi import APIRouter, Depends from app.api.deps import require_module_access from app.modules.enums import FrontendType +from .admin_email_templates import admin_email_templates_router from .admin_messages import admin_messages_router from .admin_notifications import admin_notifications_router -from .admin_email_templates import admin_email_templates_router admin_router = APIRouter( dependencies=[Depends(require_module_access("messaging", FrontendType.ADMIN))], diff --git a/app/modules/messaging/routes/api/admin_email_templates.py b/app/modules/messaging/routes/api/admin_email_templates.py index 56cd9df3..719050b8 100644 --- a/app/modules/messaging/routes/api/admin_email_templates.py +++ b/app/modules/messaging/routes/api/admin_email_templates.py @@ -239,11 +239,10 @@ def send_test_email( "success": True, "message": f"Test email sent to {test_data.to_email}", } - else: - return { - "success": False, - "message": email_log.error_message or "Failed to send email", - } + return { + "success": False, + "message": email_log.error_message or "Failed to send email", + } except Exception as e: logger.exception(f"Failed to send test email: {e}") return { diff --git a/app/modules/messaging/routes/api/admin_messages.py b/app/modules/messaging/routes/api/admin_messages.py index 7716182f..5343c5f5 100644 --- a/app/modules/messaging/routes/api/admin_messages.py +++ b/app/modules/messaging/routes/api/admin_messages.py @@ -25,8 +25,6 @@ from app.modules.messaging.exceptions import ( InvalidRecipientTypeException, MessageAttachmentException, ) -from app.modules.messaging.services.message_attachment_service import message_attachment_service -from app.modules.messaging.services.messaging_service import messaging_service from app.modules.messaging.models import ConversationType, ParticipantType from app.modules.messaging.schemas import ( AdminConversationListResponse, @@ -36,7 +34,6 @@ from app.modules.messaging.schemas import ( ConversationCreate, ConversationDetailResponse, MarkReadResponse, - MessageCreate, MessageResponse, NotificationPreferencesUpdate, ParticipantInfo, @@ -46,6 +43,10 @@ from app.modules.messaging.schemas import ( ReopenConversationResponse, UnreadCountResponse, ) +from app.modules.messaging.services.message_attachment_service import ( + message_attachment_service, +) +from app.modules.messaging.services.messaging_service import messaging_service from models.schema.auth import UserContext admin_messages_router = APIRouter(prefix="/messages") diff --git a/app/modules/messaging/routes/api/admin_notifications.py b/app/modules/messaging/routes/api/admin_notifications.py index c257cd93..ff632f3c 100644 --- a/app/modules/messaging/routes/api/admin_notifications.py +++ b/app/modules/messaging/routes/api/admin_notifications.py @@ -15,11 +15,15 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db +from app.modules.messaging.schemas import ( + AlertStatisticsResponse, + MessageResponse, + UnreadCountResponse, +) from app.modules.messaging.services.admin_notification_service import ( admin_notification_service, platform_alert_service, ) -from models.schema.auth import UserContext from app.modules.tenancy.schemas.admin import ( AdminNotificationCreate, AdminNotificationListResponse, @@ -29,11 +33,7 @@ from app.modules.tenancy.schemas.admin import ( PlatformAlertResolve, PlatformAlertResponse, ) -from app.modules.messaging.schemas import ( - AlertStatisticsResponse, - MessageResponse, - UnreadCountResponse, -) +from models.schema.auth import UserContext admin_notifications_router = APIRouter(prefix="/notifications") logger = logging.getLogger(__name__) diff --git a/app/modules/messaging/routes/api/store.py b/app/modules/messaging/routes/api/store.py index 85f5eca6..e3dd0d76 100644 --- a/app/modules/messaging/routes/api/store.py +++ b/app/modules/messaging/routes/api/store.py @@ -14,10 +14,10 @@ from fastapi import APIRouter, Depends from app.api.deps import require_module_access from app.modules.enums import FrontendType -from .store_messages import store_messages_router -from .store_notifications import store_notifications_router from .store_email_settings import store_email_settings_router from .store_email_templates import store_email_templates_router +from .store_messages import store_messages_router +from .store_notifications import store_notifications_router store_router = APIRouter( dependencies=[Depends(require_module_access("messaging", FrontendType.STORE))], diff --git a/app/modules/messaging/routes/api/store_email_settings.py b/app/modules/messaging/routes/api/store_email_settings.py index 6da695b8..683b6c1e 100644 --- a/app/modules/messaging/routes/api/store_email_settings.py +++ b/app/modules/messaging/routes/api/store_email_settings.py @@ -20,8 +20,10 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_api from app.core.database import get_db -from app.modules.cms.services.store_email_settings_service import store_email_settings_service from app.modules.billing.services.subscription_service import subscription_service +from app.modules.cms.services.store_email_settings_service import ( + store_email_settings_service, +) from models.schema.auth import UserContext store_email_settings_router = APIRouter(prefix="/email-settings") diff --git a/app/modules/messaging/routes/api/store_email_templates.py b/app/modules/messaging/routes/api/store_email_templates.py index c32390a8..111f2c75 100644 --- a/app/modules/messaging/routes/api/store_email_templates.py +++ b/app/modules/messaging/routes/api/store_email_templates.py @@ -236,11 +236,10 @@ def send_test_email( "success": True, "message": f"Test email sent to {test_data.to_email}", } - else: - return { - "success": False, - "message": email_log.error_message or "Failed to send email", - } + return { + "success": False, + "message": email_log.error_message or "Failed to send email", + } except Exception as e: logger.exception(f"Failed to send test email: {e}") return { diff --git a/app/modules/messaging/routes/api/store_messages.py b/app/modules/messaging/routes/api/store_messages.py index 923f9c54..0ea1045a 100644 --- a/app/modules/messaging/routes/api/store_messages.py +++ b/app/modules/messaging/routes/api/store_messages.py @@ -27,8 +27,6 @@ from app.modules.messaging.exceptions import ( InvalidRecipientTypeException, MessageAttachmentException, ) -from app.modules.messaging.services.message_attachment_service import message_attachment_service -from app.modules.messaging.services.messaging_service import messaging_service from app.modules.messaging.models import ConversationType, ParticipantType from app.modules.messaging.schemas import ( AttachmentResponse, @@ -47,6 +45,10 @@ from app.modules.messaging.schemas import ( ReopenConversationResponse, UnreadCountResponse, ) +from app.modules.messaging.services.message_attachment_service import ( + message_attachment_service, +) +from app.modules.messaging.services.messaging_service import messaging_service from models.schema.auth import UserContext store_messages_router = APIRouter(prefix="/messages") diff --git a/app/modules/messaging/routes/api/store_notifications.py b/app/modules/messaging/routes/api/store_notifications.py index ed725e6a..a36b60b8 100644 --- a/app/modules/messaging/routes/api/store_notifications.py +++ b/app/modules/messaging/routes/api/store_notifications.py @@ -13,8 +13,6 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_api from app.core.database import get_db -from app.modules.tenancy.services.store_service import store_service -from models.schema.auth import UserContext from app.modules.messaging.schemas import ( MessageResponse, NotificationListResponse, @@ -25,6 +23,8 @@ from app.modules.messaging.schemas import ( TestNotificationRequest, UnreadCountResponse, ) +from app.modules.tenancy.services.store_service import store_service +from models.schema.auth import UserContext store_notifications_router = APIRouter(prefix="/notifications") logger = logging.getLogger(__name__) diff --git a/app/modules/messaging/routes/api/storefront.py b/app/modules/messaging/routes/api/storefront.py index f1aac7b1..13d3ed9c 100644 --- a/app/modules/messaging/routes/api/storefront.py +++ b/app/modules/messaging/routes/api/storefront.py @@ -18,7 +18,6 @@ Customers can only: """ import logging -from typing import List, Optional from fastapi import APIRouter, Depends, File, Form, Path, Query, Request, UploadFile from fastapi.responses import FileResponse @@ -27,13 +26,12 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_customer_api from app.core.database import get_db +from app.modules.customers.schemas import CustomerContext from app.modules.messaging.exceptions import ( AttachmentNotFoundException, ConversationClosedException, ConversationNotFoundException, ) -from app.modules.tenancy.exceptions import StoreNotFoundException -from app.modules.customers.schemas import CustomerContext from app.modules.messaging.models.message import ConversationType, ParticipantType from app.modules.messaging.schemas import ( ConversationDetailResponse, @@ -46,6 +44,7 @@ from app.modules.messaging.services import ( message_attachment_service, messaging_service, ) +from app.modules.tenancy.exceptions import StoreNotFoundException router = APIRouter() logger = logging.getLogger(__name__) @@ -73,7 +72,7 @@ def list_conversations( request: Request, skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=100), - status: Optional[str] = Query(None, pattern="^(open|closed)$"), + status: str | None = Query(None, pattern="^(open|closed)$"), customer: CustomerContext = Depends(get_current_customer_api), db: Session = Depends(get_db), ): @@ -260,7 +259,7 @@ async def send_message( request: Request, conversation_id: int = Path(..., description="Conversation ID", gt=0), content: str = Form(..., min_length=1, max_length=10000), - attachments: List[UploadFile] = File(default=[]), + attachments: list[UploadFile] = File(default=[]), customer: CustomerContext = Depends(get_current_customer_api), db: Session = Depends(get_db), ): @@ -513,7 +512,7 @@ def _get_sender_name(message) -> str: if customer: return f"{customer.first_name} {customer.last_name}" return "Customer" - elif message.sender_type == ParticipantType.STORE: + if message.sender_type == ParticipantType.STORE: from app.modules.tenancy.models import User user = ( @@ -524,6 +523,6 @@ def _get_sender_name(message) -> str: if user: return f"{user.first_name} {user.last_name}" return "Shop Support" - elif message.sender_type == ParticipantType.ADMIN: + if message.sender_type == ParticipantType.ADMIN: return "Platform Support" return "Unknown" diff --git a/app/modules/messaging/routes/pages/admin.py b/app/modules/messaging/routes/pages/admin.py index ef8253d9..e6cb4474 100644 --- a/app/modules/messaging/routes/pages/admin.py +++ b/app/modules/messaging/routes/pages/admin.py @@ -15,9 +15,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access from app.modules.core.utils.page_context import get_admin_context -from app.templates_config import templates from app.modules.enums import FrontendType from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/messaging/routes/pages/store.py b/app/modules/messaging/routes/pages/store.py index cd8f3e4d..0d59ffb5 100644 --- a/app/modules/messaging/routes/pages/store.py +++ b/app/modules/messaging/routes/pages/store.py @@ -14,8 +14,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_from_cookie_or_header, get_db from app.modules.core.utils.page_context import get_store_context -from app.templates_config import templates from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/messaging/schemas/__init__.py b/app/modules/messaging/schemas/__init__.py index 51042843..23f8c8e1 100644 --- a/app/modules/messaging/schemas/__init__.py +++ b/app/modules/messaging/schemas/__init__.py @@ -5,58 +5,6 @@ Messaging module Pydantic schemas. This module contains the canonical implementations of messaging-related schemas. """ -from app.modules.messaging.schemas.message import ( - # Attachment schemas - AttachmentResponse, - # Message schemas - MessageCreate, - MessageResponse, - # Participant schemas - ParticipantInfo, - ParticipantResponse, - # Conversation schemas - ConversationCreate, - ConversationSummary, - ConversationDetailResponse, - ConversationListResponse, - ConversationResponse, - # Unread count - UnreadCountResponse, - # Notification preferences - NotificationPreferencesUpdate, - # Conversation actions - CloseConversationResponse, - ReopenConversationResponse, - MarkReadResponse, - # Recipient selection - RecipientOption, - RecipientListResponse, - # Admin schemas - AdminConversationSummary, - AdminConversationListResponse, - AdminMessageStats, -) - -from app.modules.messaging.schemas.notification import ( - # Response schemas - MessageResponse as NotificationMessageResponse, - UnreadCountResponse as NotificationUnreadCountResponse, - # Notification schemas - NotificationResponse, - NotificationListResponse, - # Settings schemas - NotificationSettingsResponse, - NotificationSettingsUpdate, - # Template schemas - NotificationTemplateResponse, - NotificationTemplateListResponse, - NotificationTemplateUpdate, - # Test notification - TestNotificationRequest, - # Alert statistics - AlertStatisticsResponse, -) - # Email template schemas from app.modules.messaging.schemas.email import ( EmailPreviewRequest, @@ -73,6 +21,60 @@ from app.modules.messaging.schemas.email import ( StoreEmailTemplateResponse, StoreEmailTemplateUpdate, ) +from app.modules.messaging.schemas.message import ( + AdminConversationListResponse, + # Admin schemas + AdminConversationSummary, + AdminMessageStats, + # Attachment schemas + AttachmentResponse, + # Conversation actions + CloseConversationResponse, + # Conversation schemas + ConversationCreate, + ConversationDetailResponse, + ConversationListResponse, + ConversationResponse, + ConversationSummary, + MarkReadResponse, + # Message schemas + MessageCreate, + MessageResponse, + # Notification preferences + NotificationPreferencesUpdate, + # Participant schemas + ParticipantInfo, + ParticipantResponse, + RecipientListResponse, + # Recipient selection + RecipientOption, + ReopenConversationResponse, + # Unread count + UnreadCountResponse, +) +from app.modules.messaging.schemas.notification import ( + # Alert statistics + AlertStatisticsResponse, + NotificationListResponse, + # Notification schemas + NotificationResponse, + # Settings schemas + NotificationSettingsResponse, + NotificationSettingsUpdate, + NotificationTemplateListResponse, + # Template schemas + NotificationTemplateResponse, + NotificationTemplateUpdate, + # Test notification + TestNotificationRequest, +) +from app.modules.messaging.schemas.notification import ( + # Response schemas + MessageResponse as NotificationMessageResponse, +) +from app.modules.messaging.schemas.notification import ( + UnreadCountResponse as NotificationUnreadCountResponse, +) __all__ = [ # Attachment schemas diff --git a/app/modules/messaging/schemas/message.py b/app/modules/messaging/schemas/message.py index 1f34fe8e..03e74ba7 100644 --- a/app/modules/messaging/schemas/message.py +++ b/app/modules/messaging/schemas/message.py @@ -14,7 +14,6 @@ from pydantic import BaseModel, ConfigDict, Field from app.modules.messaging.models.message import ConversationType, ParticipantType - # ============================================================================ # Attachment Schemas # ============================================================================ @@ -41,10 +40,9 @@ class AttachmentResponse(BaseModel): """Human-readable file size.""" if self.file_size < 1024: return f"{self.file_size} B" - elif self.file_size < 1024 * 1024: + if self.file_size < 1024 * 1024: return f"{self.file_size / 1024:.1f} KB" - else: - return f"{self.file_size / 1024 / 1024:.1f} MB" + return f"{self.file_size / 1024 / 1024:.1f} MB" # ============================================================================ diff --git a/app/modules/messaging/services/__init__.py b/app/modules/messaging/services/__init__.py index 595f6fd7..2c9525b1 100644 --- a/app/modules/messaging/services/__init__.py +++ b/app/modules/messaging/services/__init__.py @@ -5,64 +5,64 @@ Messaging module services. This module contains the canonical implementations of messaging-related services. """ -from app.modules.messaging.services.messaging_service import ( - messaging_service, - MessagingService, -) -from app.modules.messaging.services.message_attachment_service import ( - message_attachment_service, - MessageAttachmentService, -) from app.modules.messaging.services.admin_notification_service import ( - admin_notification_service, AdminNotificationService, - platform_alert_service, - PlatformAlertService, + AlertType, # Constants NotificationType, + PlatformAlertService, Priority, - AlertType, Severity, + admin_notification_service, + platform_alert_service, ) from app.modules.messaging.services.email_service import ( - EmailService, - EmailProvider, - ResolvedTemplate, - BrandingContext, - send_email, - get_provider, - get_platform_provider, - get_store_provider, - get_platform_email_config, - # Provider classes - SMTPProvider, - SendGridProvider, - MailgunProvider, - SESProvider, - DebugProvider, - # Configurable provider classes - ConfigurableSMTPProvider, - ConfigurableSendGridProvider, - ConfigurableMailgunProvider, - ConfigurableSESProvider, - # Store provider classes - StoreSMTPProvider, - StoreSendGridProvider, - StoreMailgunProvider, - StoreSESProvider, + PLATFORM_DEFAULT_LANGUAGE, # Constants PLATFORM_NAME, PLATFORM_SUPPORT_EMAIL, - PLATFORM_DEFAULT_LANGUAGE, - SUPPORTED_LANGUAGES, - WHITELABEL_TIERS, POWERED_BY_FOOTER_HTML, POWERED_BY_FOOTER_TEXT, + SUPPORTED_LANGUAGES, + WHITELABEL_TIERS, + BrandingContext, + ConfigurableMailgunProvider, + ConfigurableSendGridProvider, + ConfigurableSESProvider, + # Configurable provider classes + ConfigurableSMTPProvider, + DebugProvider, + EmailProvider, + EmailService, + MailgunProvider, + ResolvedTemplate, + SendGridProvider, + SESProvider, + # Provider classes + SMTPProvider, + StoreMailgunProvider, + StoreSendGridProvider, + StoreSESProvider, + # Store provider classes + StoreSMTPProvider, + get_platform_email_config, + get_platform_provider, + get_provider, + get_store_provider, + send_email, ) from app.modules.messaging.services.email_template_service import ( EmailTemplateService, - TemplateData, StoreOverrideData, + TemplateData, +) +from app.modules.messaging.services.message_attachment_service import ( + MessageAttachmentService, + message_attachment_service, +) +from app.modules.messaging.services.messaging_service import ( + MessagingService, + messaging_service, ) __all__ = [ diff --git a/app/modules/messaging/services/admin_notification_service.py b/app/modules/messaging/services/admin_notification_service.py index 0e0c1e3b..b99b7709 100644 --- a/app/modules/messaging/services/admin_notification_service.py +++ b/app/modules/messaging/services/admin_notification_service.py @@ -12,12 +12,15 @@ import logging from datetime import datetime, timedelta from typing import Any -from sqlalchemy import and_, case, func +from sqlalchemy import and_, case from sqlalchemy.orm import Session from app.modules.messaging.models.admin_notification import AdminNotification from app.modules.tenancy.models import PlatformAlert -from app.modules.tenancy.schemas.admin import AdminNotificationCreate, PlatformAlertCreate +from app.modules.tenancy.schemas.admin import ( + AdminNotificationCreate, + PlatformAlertCreate, +) logger = logging.getLogger(__name__) diff --git a/app/modules/messaging/services/email_service.py b/app/modules/messaging/services/email_service.py index 509ca1c0..4d387fcc 100644 --- a/app/modules/messaging/services/email_service.py +++ b/app/modules/messaging/services/email_service.py @@ -37,12 +37,16 @@ from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from typing import Any -from jinja2 import Environment, BaseLoader +from jinja2 import BaseLoader, Environment from sqlalchemy.orm import Session from app.core.config import settings -from app.modules.messaging.models import EmailLog, EmailStatus, EmailTemplate -from app.modules.messaging.models import StoreEmailTemplate +from app.modules.messaging.models import ( + EmailLog, + EmailStatus, + EmailTemplate, + StoreEmailTemplate, +) logger = logging.getLogger(__name__) @@ -118,7 +122,6 @@ class EmailProvider(ABC): Returns: tuple: (success, provider_message_id, error_message) """ - pass class SMTPProvider(EmailProvider): @@ -190,7 +193,7 @@ class SendGridProvider(EmailProvider): ) -> tuple[bool, str | None, str | None]: try: from sendgrid import SendGridAPIClient - from sendgrid.helpers.mail import Mail, Email, To, Content + from sendgrid.helpers.mail import Content, Email, Mail, To message = Mail( from_email=Email(from_email, from_name), @@ -211,8 +214,7 @@ class SendGridProvider(EmailProvider): if response.status_code in (200, 201, 202): message_id = response.headers.get("X-Message-Id") return True, message_id, None - else: - return False, None, f"SendGrid error: {response.status_code}" + return False, None, f"SendGrid error: {response.status_code}" except ImportError: return False, None, "SendGrid library not installed. Run: pip install sendgrid" @@ -263,8 +265,7 @@ class MailgunProvider(EmailProvider): if response.status_code == 200: result = response.json() return True, result.get("id"), None - else: - return False, None, f"Mailgun error: {response.status_code} - {response.text}" + return False, None, f"Mailgun error: {response.status_code} - {response.text}" except Exception as e: logger.error(f"Mailgun send error: {e}") @@ -519,7 +520,7 @@ class ConfigurableSendGridProvider(EmailProvider): ) -> tuple[bool, str | None, str | None]: try: from sendgrid import SendGridAPIClient - from sendgrid.helpers.mail import Mail, Email, To, Content + from sendgrid.helpers.mail import Content, Email, Mail, To message = Mail( from_email=Email(from_email, from_name), @@ -540,8 +541,7 @@ class ConfigurableSendGridProvider(EmailProvider): if response.status_code in (200, 201, 202): message_id = response.headers.get("X-Message-Id") return True, message_id, None - else: - return False, None, f"SendGrid error: {response.status_code}" + return False, None, f"SendGrid error: {response.status_code}" except ImportError: return False, None, "SendGrid library not installed" @@ -595,8 +595,7 @@ class ConfigurableMailgunProvider(EmailProvider): if response.status_code == 200: result = response.json() return True, result.get("id"), None - else: - return False, None, f"Mailgun error: {response.status_code} - {response.text}" + return False, None, f"Mailgun error: {response.status_code} - {response.text}" except Exception as e: logger.error(f"Configurable Mailgun send error: {e}") @@ -765,7 +764,7 @@ class StoreSendGridProvider(EmailProvider): ) -> tuple[bool, str | None, str | None]: try: from sendgrid import SendGridAPIClient - from sendgrid.helpers.mail import Mail, Email, To, Content + from sendgrid.helpers.mail import Content, Email, Mail, To message = Mail( from_email=Email(from_email, from_name), @@ -786,8 +785,7 @@ class StoreSendGridProvider(EmailProvider): if response.status_code in (200, 201, 202): message_id = response.headers.get("X-Message-Id") return True, message_id, None - else: - return False, None, f"SendGrid error: {response.status_code}" + return False, None, f"SendGrid error: {response.status_code}" except ImportError: return False, None, "SendGrid library not installed" @@ -841,8 +839,7 @@ class StoreMailgunProvider(EmailProvider): if response.status_code == 200: result = response.json() return True, result.get("id"), None - else: - return False, None, f"Mailgun error: {response.status_code} - {response.text}" + return False, None, f"Mailgun error: {response.status_code} - {response.text}" except Exception as e: logger.error(f"Store Mailgun send error: {e}") @@ -1038,7 +1035,9 @@ class EmailService: def _get_store_tier(self, store_id: int) -> str | None: """Get store's subscription tier with caching.""" if store_id not in self._store_tier_cache: - from app.modules.billing.services.subscription_service import subscription_service + from app.modules.billing.services.subscription_service import ( + subscription_service, + ) tier = subscription_service.get_current_tier(self.db, store_id) self._store_tier_cache[store_id] = tier.value if tier else None @@ -1169,16 +1168,15 @@ class EmailService: store_logo_url=store.get_logo_url(), is_whitelabel=True, ) - else: - # Standard: Wizamart branding with store details - return BrandingContext( - platform_name=PLATFORM_NAME, - platform_logo_url=None, # Use default platform logo - support_email=PLATFORM_SUPPORT_EMAIL, - store_name=store.name if store else None, - store_logo_url=store.get_logo_url() if store else None, - is_whitelabel=False, - ) + # Standard: Wizamart branding with store details + return BrandingContext( + platform_name=PLATFORM_NAME, + platform_logo_url=None, # Use default platform logo + support_email=PLATFORM_SUPPORT_EMAIL, + store_name=store.name if store else None, + store_logo_url=store.get_logo_url() if store else None, + is_whitelabel=False, + ) def resolve_template( self, diff --git a/app/modules/messaging/services/email_template_service.py b/app/modules/messaging/services/email_template_service.py index 19fecde3..5f57351e 100644 --- a/app/modules/messaging/services/email_template_service.py +++ b/app/modules/messaging/services/email_template_service.py @@ -24,8 +24,12 @@ from app.exceptions.base import ( ResourceNotFoundException, ValidationException, ) -from app.modules.messaging.models import EmailCategory, EmailLog, EmailTemplate -from app.modules.messaging.models import StoreEmailTemplate +from app.modules.messaging.models import ( + EmailCategory, + EmailLog, + EmailTemplate, + StoreEmailTemplate, +) logger = logging.getLogger(__name__) @@ -498,7 +502,7 @@ class EmailTemplateService: "body_html": platform_version.body_html, } if platform_version else None, } - elif platform_version: + if platform_version: return { "code": code, "language": language, @@ -510,8 +514,7 @@ class EmailTemplateService: "variables": self._parse_required_variables(platform_template.required_variables), "platform_template": None, } - else: - raise ResourceNotFoundException(f"No template found for language: {language}") + raise ResourceNotFoundException(f"No template found for language: {language}") def create_or_update_store_override( self, diff --git a/app/modules/messaging/services/message_attachment_service.py b/app/modules/messaging/services/message_attachment_service.py index 6c6269a6..09e6c6da 100644 --- a/app/modules/messaging/services/message_attachment_service.py +++ b/app/modules/messaging/services/message_attachment_service.py @@ -156,9 +156,10 @@ class MessageAttachmentService: def _create_thumbnail(self, content: bytes, original_path: str) -> dict: """Create thumbnail for image attachments.""" try: - from PIL import Image import io + from PIL import Image + img = Image.open(io.BytesIO(content)) width, height = img.size diff --git a/app/modules/messaging/services/messaging_features.py b/app/modules/messaging/services/messaging_features.py index f4f999e1..f368f106 100644 --- a/app/modules/messaging/services/messaging_features.py +++ b/app/modules/messaging/services/messaging_features.py @@ -13,7 +13,6 @@ from typing import TYPE_CHECKING from app.modules.contracts.features import ( FeatureDeclaration, - FeatureProviderProtocol, FeatureScope, FeatureType, FeatureUsage, diff --git a/app/modules/messaging/services/messaging_service.py b/app/modules/messaging/services/messaging_service.py index 2b9a336b..b93be9c4 100644 --- a/app/modules/messaging/services/messaging_service.py +++ b/app/modules/messaging/services/messaging_service.py @@ -17,6 +17,7 @@ from typing import Any from sqlalchemy import and_, func, or_ from sqlalchemy.orm import Session, joinedload +from app.modules.customers.models.customer import Customer from app.modules.messaging.models.message import ( Conversation, ConversationParticipant, @@ -25,7 +26,6 @@ from app.modules.messaging.models.message import ( MessageAttachment, ParticipantType, ) -from app.modules.customers.models.customer import Customer from app.modules.tenancy.models import User logger = logging.getLogger(__name__) diff --git a/app/modules/messaging/tests/unit/test_email_service.py b/app/modules/messaging/tests/unit/test_email_service.py index d947987f..927b174f 100644 --- a/app/modules/messaging/tests/unit/test_email_service.py +++ b/app/modules/messaging/tests/unit/test_email_service.py @@ -6,14 +6,18 @@ from unittest.mock import MagicMock, patch import pytest +from app.modules.messaging.models import ( + EmailCategory, + EmailLog, + EmailStatus, + EmailTemplate, +) from app.modules.messaging.services.email_service import ( DebugProvider, - EmailProvider, EmailService, SMTPProvider, get_provider, ) -from app.modules.messaging.models import EmailCategory, EmailLog, EmailStatus, EmailTemplate @pytest.mark.unit diff --git a/app/modules/messaging/tests/unit/test_message_attachment_service.py b/app/modules/messaging/tests/unit/test_message_attachment_service.py index a5f40919..705d6f6a 100644 --- a/app/modules/messaging/tests/unit/test_message_attachment_service.py +++ b/app/modules/messaging/tests/unit/test_message_attachment_service.py @@ -11,7 +11,6 @@ import pytest from fastapi import UploadFile from app.modules.messaging.services.message_attachment_service import ( - ALLOWED_MIME_TYPES, DEFAULT_MAX_FILE_SIZE_MB, IMAGE_MIME_TYPES, MessageAttachmentService, diff --git a/app/modules/messaging/tests/unit/test_messaging_service.py b/app/modules/messaging/tests/unit/test_messaging_service.py index 82b7cb84..6a558d6c 100644 --- a/app/modules/messaging/tests/unit/test_messaging_service.py +++ b/app/modules/messaging/tests/unit/test_messaging_service.py @@ -3,14 +3,12 @@ import pytest -from app.modules.messaging.services.messaging_service import MessagingService from app.modules.messaging.models import ( - Conversation, ConversationParticipant, ConversationType, - Message, ParticipantType, ) +from app.modules.messaging.services.messaging_service import MessagingService @pytest.fixture diff --git a/app/modules/monitoring/__init__.py b/app/modules/monitoring/__init__.py index 41a32efe..03a8546a 100644 --- a/app/modules/monitoring/__init__.py +++ b/app/modules/monitoring/__init__.py @@ -26,7 +26,7 @@ def __getattr__(name: str): from app.modules.monitoring.definition import monitoring_module return monitoring_module - elif name == "get_monitoring_module_with_routers": + if name == "get_monitoring_module_with_routers": from app.modules.monitoring.definition import get_monitoring_module_with_routers return get_monitoring_module_with_routers diff --git a/app/modules/monitoring/routes/api/admin.py b/app/modules/monitoring/routes/api/admin.py index 34f80e19..7db0b574 100644 --- a/app/modules/monitoring/routes/api/admin.py +++ b/app/modules/monitoring/routes/api/admin.py @@ -16,12 +16,12 @@ from fastapi import APIRouter, Depends from app.api.deps import require_module_access from app.modules.enums import FrontendType +from .admin_audit import admin_audit_router +from .admin_code_quality import admin_code_quality_router from .admin_logs import admin_logs_router +from .admin_platform_health import admin_platform_health_router from .admin_tasks import admin_tasks_router from .admin_tests import admin_tests_router -from .admin_code_quality import admin_code_quality_router -from .admin_audit import admin_audit_router -from .admin_platform_health import admin_platform_health_router admin_router = APIRouter( dependencies=[Depends(require_module_access("monitoring", FrontendType.ADMIN))], diff --git a/app/modules/monitoring/routes/api/admin_audit.py b/app/modules/monitoring/routes/api/admin_audit.py index 61a2f329..febf7f5a 100644 --- a/app/modules/monitoring/routes/api/admin_audit.py +++ b/app/modules/monitoring/routes/api/admin_audit.py @@ -17,12 +17,12 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db from app.modules.monitoring.services.admin_audit_service import admin_audit_service -from models.schema.auth import UserContext from app.modules.tenancy.schemas.admin import ( AdminAuditLogFilters, AdminAuditLogListResponse, AdminAuditLogResponse, ) +from models.schema.auth import UserContext admin_audit_router = APIRouter(prefix="/audit") logger = logging.getLogger(__name__) diff --git a/app/modules/monitoring/routes/api/admin_code_quality.py b/app/modules/monitoring/routes/api/admin_code_quality.py index 8f2a9a57..af201446 100644 --- a/app/modules/monitoring/routes/api/admin_code_quality.py +++ b/app/modules/monitoring/routes/api/admin_code_quality.py @@ -14,14 +14,17 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db -from app.modules.monitoring.exceptions import ScanNotFoundException, ViolationNotFoundException +from app.modules.analytics.schemas import CodeQualityDashboardStatsResponse +from app.modules.dev_tools.models import ArchitectureScan from app.modules.dev_tools.services.code_quality_service import ( VALID_VALIDATOR_TYPES, code_quality_service, ) -from app.modules.dev_tools.models import ArchitectureScan +from app.modules.monitoring.exceptions import ( + ScanNotFoundException, + ViolationNotFoundException, +) from models.schema.auth import UserContext -from app.modules.analytics.schemas import CodeQualityDashboardStatsResponse admin_code_quality_router = APIRouter(prefix="/code-quality") diff --git a/app/modules/monitoring/routes/api/admin_logs.py b/app/modules/monitoring/routes/api/admin_logs.py index 8515f571..74f27aa1 100644 --- a/app/modules/monitoring/routes/api/admin_logs.py +++ b/app/modules/monitoring/routes/api/admin_logs.py @@ -19,11 +19,10 @@ from app.api.deps import get_current_admin_api from app.core.database import get_db from app.core.logging import reload_log_level from app.exceptions import ResourceNotFoundException -from app.modules.tenancy.exceptions import ConfirmationRequiredException -from app.modules.monitoring.services.admin_audit_service import admin_audit_service from app.modules.core.services.admin_settings_service import admin_settings_service +from app.modules.monitoring.services.admin_audit_service import admin_audit_service from app.modules.monitoring.services.log_service import log_service -from models.schema.auth import UserContext +from app.modules.tenancy.exceptions import ConfirmationRequiredException from app.modules.tenancy.schemas.admin import ( ApplicationLogFilters, ApplicationLogListResponse, @@ -36,6 +35,7 @@ from app.modules.tenancy.schemas.admin import ( LogSettingsUpdateResponse, LogStatistics, ) +from models.schema.auth import UserContext admin_logs_router = APIRouter(prefix="/logs") logger = logging.getLogger(__name__) diff --git a/app/modules/monitoring/routes/api/admin_platform_health.py b/app/modules/monitoring/routes/api/admin_platform_health.py index 60a6a090..0d9c2391 100644 --- a/app/modules/monitoring/routes/api/admin_platform_health.py +++ b/app/modules/monitoring/routes/api/admin_platform_health.py @@ -16,7 +16,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db -from app.modules.monitoring.services.platform_health_service import platform_health_service +from app.modules.monitoring.services.platform_health_service import ( + platform_health_service, +) from models.schema.auth import UserContext admin_platform_health_router = APIRouter(prefix="/platform") @@ -170,7 +172,9 @@ async def get_growth_trends( Returns growth rates and projections for key metrics. """ - from app.modules.billing.services.capacity_forecast_service import capacity_forecast_service + from app.modules.billing.services.capacity_forecast_service import ( + capacity_forecast_service, + ) return capacity_forecast_service.get_growth_trends(db, days=days) @@ -185,7 +189,9 @@ async def get_scaling_recommendations( Returns prioritized list of recommendations. """ - from app.modules.billing.services.capacity_forecast_service import capacity_forecast_service + from app.modules.billing.services.capacity_forecast_service import ( + capacity_forecast_service, + ) return capacity_forecast_service.get_scaling_recommendations(db) @@ -200,7 +206,9 @@ async def capture_snapshot( Normally run automatically by daily background job. """ - from app.modules.billing.services.capacity_forecast_service import capacity_forecast_service + from app.modules.billing.services.capacity_forecast_service import ( + capacity_forecast_service, + ) snapshot = capacity_forecast_service.capture_daily_snapshot(db) db.commit() diff --git a/app/modules/monitoring/routes/api/admin_tasks.py b/app/modules/monitoring/routes/api/admin_tasks.py index 67dfcc19..4c3590d3 100644 --- a/app/modules/monitoring/routes/api/admin_tasks.py +++ b/app/modules/monitoring/routes/api/admin_tasks.py @@ -12,7 +12,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db -from app.modules.monitoring.services.background_tasks_service import background_tasks_service +from app.modules.monitoring.services.background_tasks_service import ( + background_tasks_service, +) from models.schema.auth import UserContext admin_tasks_router = APIRouter(prefix="/tasks") diff --git a/app/modules/monitoring/routes/pages/admin.py b/app/modules/monitoring/routes/pages/admin.py index 14dedc0b..fb4b4ce2 100644 --- a/app/modules/monitoring/routes/pages/admin.py +++ b/app/modules/monitoring/routes/pages/admin.py @@ -13,9 +13,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access from app.modules.core.utils.page_context import get_admin_context -from app.templates_config import templates from app.modules.enums import FrontendType from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/monitoring/services/__init__.py b/app/modules/monitoring/services/__init__.py index 275e6914..62d84ff3 100644 --- a/app/modules/monitoring/services/__init__.py +++ b/app/modules/monitoring/services/__init__.py @@ -6,20 +6,20 @@ This module contains the canonical implementations of monitoring-related service """ from app.modules.monitoring.services.admin_audit_service import ( - admin_audit_service, AdminAuditService, + admin_audit_service, ) from app.modules.monitoring.services.background_tasks_service import ( - background_tasks_service, BackgroundTasksService, + background_tasks_service, ) from app.modules.monitoring.services.log_service import ( - log_service, LogService, + log_service, ) from app.modules.monitoring.services.platform_health_service import ( - platform_health_service, PlatformHealthService, + platform_health_service, ) __all__ = [ diff --git a/app/modules/monitoring/services/admin_audit_service.py b/app/modules/monitoring/services/admin_audit_service.py index 07a92ccd..afe3703d 100644 --- a/app/modules/monitoring/services/admin_audit_service.py +++ b/app/modules/monitoring/services/admin_audit_service.py @@ -15,9 +15,11 @@ from sqlalchemy import and_ from sqlalchemy.orm import Session from app.modules.tenancy.exceptions import AdminOperationException -from app.modules.tenancy.models import AdminAuditLog -from app.modules.tenancy.models import User -from app.modules.tenancy.schemas.admin import AdminAuditLogFilters, AdminAuditLogResponse +from app.modules.tenancy.models import AdminAuditLog, User +from app.modules.tenancy.schemas.admin import ( + AdminAuditLogFilters, + AdminAuditLogResponse, +) logger = logging.getLogger(__name__) diff --git a/app/modules/monitoring/services/audit_provider.py b/app/modules/monitoring/services/audit_provider.py index 4f4cdfc5..f7333c94 100644 --- a/app/modules/monitoring/services/audit_provider.py +++ b/app/modules/monitoring/services/audit_provider.py @@ -12,7 +12,7 @@ from typing import TYPE_CHECKING from sqlalchemy.orm import Session -from app.modules.contracts.audit import AuditEvent, AuditProviderProtocol +from app.modules.contracts.audit import AuditEvent from app.modules.tenancy.models import AdminAuditLog if TYPE_CHECKING: diff --git a/app/modules/monitoring/services/background_tasks_service.py b/app/modules/monitoring/services/background_tasks_service.py index 166dba84..abbb0e8d 100644 --- a/app/modules/monitoring/services/background_tasks_service.py +++ b/app/modules/monitoring/services/background_tasks_service.py @@ -9,9 +9,8 @@ from datetime import UTC, datetime from sqlalchemy import case, desc, func from sqlalchemy.orm import Session -from app.modules.dev_tools.models import ArchitectureScan +from app.modules.dev_tools.models import ArchitectureScan, TestRun from app.modules.marketplace.models import MarketplaceImportJob -from app.modules.dev_tools.models import TestRun class BackgroundTasksService: diff --git a/app/modules/monitoring/services/log_service.py b/app/modules/monitoring/services/log_service.py index a32836ac..e0a56dd0 100644 --- a/app/modules/monitoring/services/log_service.py +++ b/app/modules/monitoring/services/log_service.py @@ -174,7 +174,7 @@ class LogService: .group_by(ApplicationLog.level) .all() ) - by_level = {level: count for level, count in by_level_raw} + by_level = dict(by_level_raw) # Count by module (top 10) by_module_raw = ( @@ -186,7 +186,7 @@ class LogService: .limit(10) .all() ) - by_module = {module: count for module, count in by_module_raw} + by_module = dict(by_module_raw) # Recent errors (last 5) recent_errors = ( @@ -286,10 +286,7 @@ class LogService: try: # Determine log directory log_file_path = settings.log_file - if log_file_path: - log_dir = Path(log_file_path).parent - else: - log_dir = Path("logs") + log_dir = Path(log_file_path).parent if log_file_path else Path("logs") if not log_dir.exists(): return [] diff --git a/app/modules/monitoring/services/platform_health_service.py b/app/modules/monitoring/services/platform_health_service.py index 9ec2b76e..c4d43c8c 100644 --- a/app/modules/monitoring/services/platform_health_service.py +++ b/app/modules/monitoring/services/platform_health_service.py @@ -16,10 +16,10 @@ import psutil from sqlalchemy import func, text from sqlalchemy.orm import Session +from app.modules.catalog.models import Product from app.modules.cms.services.media_service import media_service from app.modules.inventory.models import Inventory from app.modules.orders.models import Order -from app.modules.catalog.models import Product from app.modules.tenancy.models import Store logger = logging.getLogger(__name__) @@ -172,7 +172,7 @@ class PlatformHealthService: Returns aggregated limits and current usage for capacity planning. """ - from app.modules.billing.models import MerchantSubscription, TierFeatureLimit + from app.modules.billing.models import MerchantSubscription from app.modules.tenancy.models import StoreUser # Get all active subscriptions with tier + feature limits @@ -247,7 +247,7 @@ class PlatformHealthService: "utilization_percent": None, "has_unlimited": True, } - elif limit > 0: + if limit > 0: return { "actual": actual, "theoretical_limit": limit, @@ -256,14 +256,13 @@ class PlatformHealthService: "headroom": limit - actual, "has_unlimited": False, } - else: - return { - "actual": actual, - "theoretical_limit": 0, - "unlimited_count": 0, - "utilization_percent": 0, - "has_unlimited": False, - } + return { + "actual": actual, + "theoretical_limit": 0, + "unlimited_count": 0, + "utilization_percent": 0, + "has_unlimited": False, + } return { "total_subscriptions": len(subscriptions), @@ -527,10 +526,9 @@ class PlatformHealthService: if "critical" in statuses: return "critical" - elif "warning" in statuses: + if "warning" in statuses: return "degraded" - else: - return "healthy" + return "healthy" # Create service instance diff --git a/app/modules/monitoring/tasks/capacity.py b/app/modules/monitoring/tasks/capacity.py index fc229770..071c3562 100644 --- a/app/modules/monitoring/tasks/capacity.py +++ b/app/modules/monitoring/tasks/capacity.py @@ -27,7 +27,9 @@ def capture_capacity_snapshot(self): Returns: dict: Snapshot summary with store and product counts. """ - from app.modules.billing.services.capacity_forecast_service import capacity_forecast_service + from app.modules.billing.services.capacity_forecast_service import ( + capacity_forecast_service, + ) with self.get_db() as db: snapshot = capacity_forecast_service.capture_daily_snapshot(db) diff --git a/app/modules/orders/__init__.py b/app/modules/orders/__init__.py index 3411787d..6681516b 100644 --- a/app/modules/orders/__init__.py +++ b/app/modules/orders/__init__.py @@ -26,7 +26,7 @@ def __getattr__(name: str): from app.modules.orders.definition import orders_module return orders_module - elif name == "get_orders_module_with_routers": + if name == "get_orders_module_with_routers": from app.modules.orders.definition import get_orders_module_with_routers return get_orders_module_with_routers diff --git a/app/modules/orders/migrations/versions/orders_001_initial.py b/app/modules/orders/migrations/versions/orders_001_initial.py index 203a4205..13a9c88e 100644 --- a/app/modules/orders/migrations/versions/orders_001_initial.py +++ b/app/modules/orders/migrations/versions/orders_001_initial.py @@ -4,9 +4,10 @@ Revision ID: orders_001 Revises: customers_001 Create Date: 2026-02-07 """ -from alembic import op import sqlalchemy as sa +from alembic import op + revision = "orders_001" down_revision = "customers_001" branch_labels = None diff --git a/app/modules/orders/models/__init__.py b/app/modules/orders/models/__init__.py index b5ba4a02..51f48966 100644 --- a/app/modules/orders/models/__init__.py +++ b/app/modules/orders/models/__init__.py @@ -5,14 +5,14 @@ Orders module database models. This module contains the canonical implementations of order-related models. """ -from app.modules.orders.models.order import Order, OrderItem -from app.modules.orders.models.order_item_exception import OrderItemException from app.modules.orders.models.invoice import ( Invoice, InvoiceStatus, - VATRegime, StoreInvoiceSettings, + VATRegime, ) +from app.modules.orders.models.order import Order, OrderItem +from app.modules.orders.models.order_item_exception import OrderItemException __all__ = [ "Order", diff --git a/app/modules/orders/models/order.py b/app/modules/orders/models/order.py index a45532bc..761bfbe2 100644 --- a/app/modules/orders/models/order.py +++ b/app/modules/orders/models/order.py @@ -16,6 +16,8 @@ Money values are stored as integer cents (e.g., €105.91 = 10591). See docs/architecture/money-handling.md for details. """ +from typing import TYPE_CHECKING + from sqlalchemy import ( Boolean, Column, @@ -27,10 +29,9 @@ from sqlalchemy import ( String, Text, ) -from typing import TYPE_CHECKING if TYPE_CHECKING: - from app.modules.orders.models.order_item_exception import OrderItemException + pass from sqlalchemy.dialects.sqlite import JSON from sqlalchemy.orm import relationship diff --git a/app/modules/orders/routes/__init__.py b/app/modules/orders/routes/__init__.py index ba549278..a47ba0b3 100644 --- a/app/modules/orders/routes/__init__.py +++ b/app/modules/orders/routes/__init__.py @@ -24,13 +24,13 @@ def __getattr__(name: str): if name == "admin_router": from app.modules.orders.routes.api import admin_router return admin_router - elif name == "admin_exceptions_router": + if name == "admin_exceptions_router": from app.modules.orders.routes.api import admin_exceptions_router return admin_exceptions_router - elif name == "store_router": + if name == "store_router": from app.modules.orders.routes.api import store_router return store_router - elif name == "store_exceptions_router": + if name == "store_exceptions_router": from app.modules.orders.routes.api import store_exceptions_router return store_exceptions_router raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/app/modules/orders/routes/api/__init__.py b/app/modules/orders/routes/api/__init__.py index 7d10c356..4376ffad 100644 --- a/app/modules/orders/routes/api/__init__.py +++ b/app/modules/orders/routes/api/__init__.py @@ -29,7 +29,7 @@ def __getattr__(name: str): if name == "admin_router": from app.modules.orders.routes.api.admin import admin_router return admin_router - elif name == "store_router": + if name == "store_router": from app.modules.orders.routes.api.store import store_router return store_router raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/app/modules/orders/routes/api/admin.py b/app/modules/orders/routes/api/admin.py index edbe28c8..bcf9cf0c 100644 --- a/app/modules/orders/routes/api/admin.py +++ b/app/modules/orders/routes/api/admin.py @@ -22,8 +22,6 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api, require_module_access from app.core.database import get_db from app.modules.enums import FrontendType -from app.modules.orders.services.order_service import order_service -from models.schema.auth import UserContext from app.modules.orders.schemas import ( AdminOrderItem, AdminOrderListResponse, @@ -34,6 +32,8 @@ from app.modules.orders.schemas import ( OrderDetailResponse, ShippingLabelInfo, ) +from app.modules.orders.services.order_service import order_service +from models.schema.auth import UserContext # Base router for orders _orders_router = APIRouter( diff --git a/app/modules/orders/routes/api/admin_exceptions.py b/app/modules/orders/routes/api/admin_exceptions.py index ce9f1eb8..3450dd40 100644 --- a/app/modules/orders/routes/api/admin_exceptions.py +++ b/app/modules/orders/routes/api/admin_exceptions.py @@ -17,8 +17,6 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api, require_module_access from app.core.database import get_db from app.modules.enums import FrontendType -from app.modules.orders.services.order_item_exception_service import order_item_exception_service -from models.schema.auth import UserContext from app.modules.orders.schemas import ( BulkResolveRequest, BulkResolveResponse, @@ -28,6 +26,10 @@ from app.modules.orders.schemas import ( OrderItemExceptionStats, ResolveExceptionRequest, ) +from app.modules.orders.services.order_item_exception_service import ( + order_item_exception_service, +) +from models.schema.auth import UserContext logger = logging.getLogger(__name__) diff --git a/app/modules/orders/routes/api/store.py b/app/modules/orders/routes/api/store.py index 58b4aac5..5b145e36 100644 --- a/app/modules/orders/routes/api/store.py +++ b/app/modules/orders/routes/api/store.py @@ -17,15 +17,15 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_api, require_module_access from app.core.database import get_db from app.modules.enums import FrontendType -from app.modules.orders.services.order_inventory_service import order_inventory_service -from app.modules.orders.services.order_service import order_service -from models.schema.auth import UserContext from app.modules.orders.schemas import ( OrderDetailResponse, OrderListResponse, OrderResponse, OrderUpdate, ) +from app.modules.orders.services.order_inventory_service import order_inventory_service +from app.modules.orders.services.order_service import order_service +from models.schema.auth import UserContext # Base router for orders _orders_router = APIRouter( diff --git a/app/modules/orders/routes/api/store_exceptions.py b/app/modules/orders/routes/api/store_exceptions.py index da8631cf..6a03c2c4 100644 --- a/app/modules/orders/routes/api/store_exceptions.py +++ b/app/modules/orders/routes/api/store_exceptions.py @@ -16,8 +16,6 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_api, require_module_access from app.core.database import get_db from app.modules.enums import FrontendType -from app.modules.orders.services.order_item_exception_service import order_item_exception_service -from models.schema.auth import UserContext from app.modules.orders.schemas import ( BulkResolveRequest, BulkResolveResponse, @@ -27,6 +25,10 @@ from app.modules.orders.schemas import ( OrderItemExceptionStats, ResolveExceptionRequest, ) +from app.modules.orders.services.order_item_exception_service import ( + order_item_exception_service, +) +from models.schema.auth import UserContext logger = logging.getLogger(__name__) diff --git a/app/modules/orders/routes/api/store_invoices.py b/app/modules/orders/routes/api/store_invoices.py index 81bfc30c..d225c77a 100644 --- a/app/modules/orders/routes/api/store_invoices.py +++ b/app/modules/orders/routes/api/store_invoices.py @@ -38,8 +38,6 @@ from app.modules.enums import FrontendType from app.modules.orders.exceptions import ( InvoicePDFNotFoundException, ) -from app.modules.orders.services.invoice_service import invoice_service -from models.schema.auth import UserContext from app.modules.orders.schemas import ( InvoiceCreate, InvoiceListPaginatedResponse, @@ -52,6 +50,8 @@ from app.modules.orders.schemas import ( StoreInvoiceSettingsResponse, StoreInvoiceSettingsUpdate, ) +from app.modules.orders.services.invoice_service import invoice_service +from models.schema.auth import UserContext store_invoices_router = APIRouter( prefix="/invoices", diff --git a/app/modules/orders/routes/api/storefront.py b/app/modules/orders/routes/api/storefront.py index 25048ea3..8e174cb7 100644 --- a/app/modules/orders/routes/api/storefront.py +++ b/app/modules/orders/routes/api/storefront.py @@ -20,17 +20,21 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_customer_api from app.core.database import get_db -from app.modules.orders.exceptions import OrderNotFoundException -from app.modules.tenancy.exceptions import StoreNotFoundException -from app.modules.orders.exceptions import InvoicePDFNotFoundException from app.modules.customers.schemas import CustomerContext -from app.modules.orders.services import order_service -from app.modules.orders.services.invoice_service import invoice_service # noqa: MOD-004 - Core invoice service +from app.modules.orders.exceptions import ( + InvoicePDFNotFoundException, + OrderNotFoundException, +) from app.modules.orders.schemas import ( OrderDetailResponse, OrderListResponse, OrderResponse, ) +from app.modules.orders.services import order_service +from app.modules.orders.services.invoice_service import ( + invoice_service, # noqa: MOD-004 - Core invoice service +) +from app.modules.tenancy.exceptions import StoreNotFoundException router = APIRouter() logger = logging.getLogger(__name__) diff --git a/app/modules/orders/routes/pages/admin.py b/app/modules/orders/routes/pages/admin.py index 825eb353..1d7318e9 100644 --- a/app/modules/orders/routes/pages/admin.py +++ b/app/modules/orders/routes/pages/admin.py @@ -12,9 +12,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access from app.modules.core.utils.page_context import get_admin_context -from app.templates_config import templates from app.modules.enums import FrontendType from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/orders/routes/pages/store.py b/app/modules/orders/routes/pages/store.py index a745fcd3..0634ece2 100644 --- a/app/modules/orders/routes/pages/store.py +++ b/app/modules/orders/routes/pages/store.py @@ -13,8 +13,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_from_cookie_or_header, get_db from app.modules.core.utils.page_context import get_store_context -from app.templates_config import templates from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/orders/schemas/__init__.py b/app/modules/orders/schemas/__init__.py index 31d82667..aa9970b6 100644 --- a/app/modules/orders/schemas/__init__.py +++ b/app/modules/orders/schemas/__init__.py @@ -5,82 +5,80 @@ Orders module Pydantic schemas. This module contains the canonical implementations of order-related schemas. """ +from app.modules.orders.schemas.invoice import ( + InvoiceBuyerDetails, + # Invoice CRUD schemas + InvoiceCreate, + # Line item schemas + InvoiceLineItem, + InvoiceLineItemResponse, + # Pagination + InvoiceListPaginatedResponse, + InvoiceListResponse, + InvoiceManualCreate, + # PDF + InvoicePDFGeneratedResponse, + InvoiceResponse, + # Address schemas + InvoiceSellerDetails, + # Backward compatibility + InvoiceSettingsCreate, + InvoiceSettingsResponse, + InvoiceSettingsUpdate, + InvoiceStatsResponse, + InvoiceStatusUpdate, + # Invoice settings schemas + StoreInvoiceSettingsCreate, + StoreInvoiceSettingsResponse, + StoreInvoiceSettingsUpdate, +) from app.modules.orders.schemas.order import ( # Address schemas AddressSnapshot, AddressSnapshotResponse, - # Order item schemas - OrderItemCreate, - OrderItemExceptionBrief, - OrderItemResponse, - # Customer schemas - CustomerSnapshot, - CustomerSnapshotResponse, - # Order CRUD schemas - OrderCreate, - OrderUpdate, - OrderTrackingUpdate, - OrderItemStateUpdate, - # Order response schemas - OrderResponse, - OrderDetailResponse, - OrderListResponse, - OrderListItem, # Admin schemas AdminOrderItem, AdminOrderListResponse, AdminOrderStats, AdminOrderStatusUpdate, - AdminStoreWithOrders, AdminStoresWithOrdersResponse, + AdminStoreWithOrders, + # Customer schemas + CustomerSnapshot, + CustomerSnapshotResponse, + LetzshopOrderConfirmItem, + LetzshopOrderConfirmRequest, # Letzshop schemas LetzshopOrderImport, LetzshopShippingInfo, - LetzshopOrderConfirmItem, - LetzshopOrderConfirmRequest, # Shipping schemas MarkAsShippedRequest, + # Order CRUD schemas + OrderCreate, + OrderDetailResponse, + # Order item schemas + OrderItemCreate, + OrderItemExceptionBrief, + OrderItemResponse, + OrderItemStateUpdate, + OrderListItem, + OrderListResponse, + # Order response schemas + OrderResponse, + OrderTrackingUpdate, + OrderUpdate, ShippingLabelInfo, ) - from app.modules.orders.schemas.order_item_exception import ( - OrderItemExceptionResponse, - OrderItemExceptionBriefResponse, - OrderItemExceptionListResponse, - OrderItemExceptionStats, - ResolveExceptionRequest, - IgnoreExceptionRequest, + AutoMatchResult, BulkResolveRequest, BulkResolveResponse, - AutoMatchResult, -) - -from app.modules.orders.schemas.invoice import ( - # Invoice settings schemas - StoreInvoiceSettingsCreate, - StoreInvoiceSettingsUpdate, - StoreInvoiceSettingsResponse, - # Line item schemas - InvoiceLineItem, - InvoiceLineItemResponse, - # Address schemas - InvoiceSellerDetails, - InvoiceBuyerDetails, - # Invoice CRUD schemas - InvoiceCreate, - InvoiceManualCreate, - InvoiceResponse, - InvoiceListResponse, - InvoiceStatusUpdate, - # Pagination - InvoiceListPaginatedResponse, - # PDF - InvoicePDFGeneratedResponse, - InvoiceStatsResponse, - # Backward compatibility - InvoiceSettingsCreate, - InvoiceSettingsUpdate, - InvoiceSettingsResponse, + IgnoreExceptionRequest, + OrderItemExceptionBriefResponse, + OrderItemExceptionListResponse, + OrderItemExceptionResponse, + OrderItemExceptionStats, + ResolveExceptionRequest, ) __all__ = [ diff --git a/app/modules/orders/schemas/order_item_exception.py b/app/modules/orders/schemas/order_item_exception.py index 15fdb111..4948048f 100644 --- a/app/modules/orders/schemas/order_item_exception.py +++ b/app/modules/orders/schemas/order_item_exception.py @@ -9,7 +9,6 @@ from datetime import datetime from pydantic import BaseModel, ConfigDict, Field - # ============================================================================ # Exception Response Schemas # ============================================================================ diff --git a/app/modules/orders/services/__init__.py b/app/modules/orders/services/__init__.py index ceb5df0e..994ad9e6 100644 --- a/app/modules/orders/services/__init__.py +++ b/app/modules/orders/services/__init__.py @@ -5,29 +5,29 @@ Orders module services. This module contains the canonical implementations of order-related services. """ -from app.modules.orders.services.order_service import ( - order_service, - OrderService, -) -from app.modules.orders.services.order_inventory_service import ( - order_inventory_service, - OrderInventoryService, -) -from app.modules.orders.services.order_item_exception_service import ( - order_item_exception_service, - OrderItemExceptionService, -) -from app.modules.orders.services.invoice_service import ( - invoice_service, - InvoiceService, +from app.modules.orders.services.customer_order_service import ( + CustomerOrderService, + customer_order_service, ) from app.modules.orders.services.invoice_pdf_service import ( - invoice_pdf_service, InvoicePDFService, + invoice_pdf_service, ) -from app.modules.orders.services.customer_order_service import ( - customer_order_service, - CustomerOrderService, +from app.modules.orders.services.invoice_service import ( + InvoiceService, + invoice_service, +) +from app.modules.orders.services.order_inventory_service import ( + OrderInventoryService, + order_inventory_service, +) +from app.modules.orders.services.order_item_exception_service import ( + OrderItemExceptionService, + order_item_exception_service, +) +from app.modules.orders.services.order_service import ( + OrderService, + order_service, ) __all__ = [ diff --git a/app/modules/orders/services/invoice_service.py b/app/modules/orders/services/invoice_service.py index d685c3f6..b1a8b95b 100644 --- a/app/modules/orders/services/invoice_service.py +++ b/app/modules/orders/services/invoice_service.py @@ -20,15 +20,15 @@ from sqlalchemy.orm import Session from app.exceptions import ValidationException from app.modules.orders.exceptions import ( - InvoiceSettingsNotFoundException, InvoiceNotFoundException, + InvoiceSettingsNotFoundException, OrderNotFoundException, ) from app.modules.orders.models.invoice import ( Invoice, InvoiceStatus, - VATRegime, StoreInvoiceSettings, + VATRegime, ) from app.modules.orders.models.order import Order from app.modules.orders.schemas.invoice import ( @@ -126,9 +126,8 @@ class InvoiceService: if seller_oss_registered: vat_rate = self.get_vat_rate_for_country(buyer_country) return VATRegime.OSS, vat_rate, buyer_country - else: - vat_rate = self.get_vat_rate_for_country(seller_country) - return VATRegime.ORIGIN, vat_rate, buyer_country + vat_rate = self.get_vat_rate_for_country(seller_country) + return VATRegime.ORIGIN, vat_rate, buyer_country return VATRegime.EXEMPT, Decimal("0.00"), buyer_country diff --git a/app/modules/orders/services/order_features.py b/app/modules/orders/services/order_features.py index d3b869fd..359b368a 100644 --- a/app/modules/orders/services/order_features.py +++ b/app/modules/orders/services/order_features.py @@ -15,7 +15,6 @@ from sqlalchemy import func from app.modules.contracts.features import ( FeatureDeclaration, - FeatureProviderProtocol, FeatureScope, FeatureType, FeatureUsage, diff --git a/app/modules/orders/services/order_inventory_service.py b/app/modules/orders/services/order_inventory_service.py index e341d83c..e447d54a 100644 --- a/app/modules/orders/services/order_inventory_service.py +++ b/app/modules/orders/services/order_inventory_service.py @@ -11,6 +11,7 @@ All operations are logged to the inventory_transactions table for audit trail. """ import logging + from sqlalchemy.orm import Session from app.exceptions import ValidationException @@ -18,7 +19,6 @@ from app.modules.inventory.exceptions import ( InsufficientInventoryException, InventoryNotFoundException, ) -from app.modules.orders.exceptions import OrderNotFoundException from app.modules.inventory.models.inventory import Inventory from app.modules.inventory.models.inventory_transaction import ( InventoryTransaction, @@ -26,6 +26,7 @@ from app.modules.inventory.models.inventory_transaction import ( ) from app.modules.inventory.schemas.inventory import InventoryReserve from app.modules.inventory.services.inventory_service import inventory_service +from app.modules.orders.exceptions import OrderNotFoundException from app.modules.orders.models.order import Order, OrderItem logger = logging.getLogger(__name__) @@ -136,10 +137,9 @@ class OrderInventoryService: "reason": "no_inventory", }) continue - else: - raise InventoryNotFoundException( - f"No inventory found for product {item.product_id}" - ) + raise InventoryNotFoundException( + f"No inventory found for product {item.product_id}" + ) try: reserve_data = InventoryReserve( @@ -237,10 +237,9 @@ class OrderInventoryService: "reason": "no_inventory", }) continue - else: - raise InventoryNotFoundException( - f"No inventory found for product {item.product_id}" - ) + raise InventoryNotFoundException( + f"No inventory found for product {item.product_id}" + ) try: reserve_data = InventoryReserve( @@ -367,10 +366,9 @@ class OrderInventoryService: "fulfilled_quantity": 0, "message": "No inventory found", } - else: - raise InventoryNotFoundException( - f"No inventory found for product {item.product_id}" - ) + raise InventoryNotFoundException( + f"No inventory found for product {item.product_id}" + ) try: reserve_data = InventoryReserve( @@ -420,8 +418,7 @@ class OrderInventoryService: "fulfilled_quantity": 0, "message": str(e), } - else: - raise + raise def release_order_reservation( self, @@ -461,10 +458,9 @@ class OrderInventoryService: "reason": "no_inventory", }) continue - else: - raise InventoryNotFoundException( - f"No inventory found for product {item.product_id}" - ) + raise InventoryNotFoundException( + f"No inventory found for product {item.product_id}" + ) try: reserve_data = InventoryReserve( diff --git a/app/modules/orders/services/order_item_exception_service.py b/app/modules/orders/services/order_item_exception_service.py index 85f3340a..0500eef0 100644 --- a/app/modules/orders/services/order_item_exception_service.py +++ b/app/modules/orders/services/order_item_exception_service.py @@ -15,15 +15,15 @@ from datetime import UTC, datetime from sqlalchemy import and_, func, or_ from sqlalchemy.orm import Session, joinedload +from app.modules.catalog.exceptions import ProductNotFoundException +from app.modules.catalog.models import Product from app.modules.orders.exceptions import ( ExceptionAlreadyResolvedException, InvalidProductForExceptionException, + OrderItemExceptionNotFoundException, ) -from app.modules.catalog.exceptions import ProductNotFoundException -from app.modules.orders.exceptions import OrderItemExceptionNotFoundException from app.modules.orders.models.order import Order, OrderItem from app.modules.orders.models.order_item_exception import OrderItemException -from app.modules.catalog.models import Product logger = logging.getLogger(__name__) diff --git a/app/modules/orders/services/order_metrics.py b/app/modules/orders/services/order_metrics.py index 4275bb78..441cfe33 100644 --- a/app/modules/orders/services/order_metrics.py +++ b/app/modules/orders/services/order_metrics.py @@ -16,9 +16,8 @@ from sqlalchemy import func from sqlalchemy.orm import Session from app.modules.contracts.metrics import ( - MetricValue, MetricsContext, - MetricsProviderProtocol, + MetricValue, ) if TYPE_CHECKING: diff --git a/app/modules/orders/services/order_service.py b/app/modules/orders/services/order_service.py index d51fa5e3..75d52f6e 100644 --- a/app/modules/orders/services/order_service.py +++ b/app/modules/orders/services/order_service.py @@ -25,31 +25,31 @@ from sqlalchemy import and_, func, or_ from sqlalchemy.orm import Session from app.exceptions import ValidationException +from app.modules.billing.services.subscription_service import ( + TierLimitExceededException, + subscription_service, +) +from app.modules.catalog.models import Product from app.modules.customers.exceptions import CustomerNotFoundException -from app.modules.inventory.exceptions import InsufficientInventoryException -from app.modules.orders.exceptions import OrderNotFoundException from app.modules.customers.models.customer import Customer +from app.modules.inventory.exceptions import InsufficientInventoryException +from app.modules.marketplace.models import ( + MarketplaceProduct, + MarketplaceProductTranslation, +) +from app.modules.orders.exceptions import OrderNotFoundException from app.modules.orders.models.order import Order, OrderItem from app.modules.orders.schemas.order import ( - AddressSnapshot, - CustomerSnapshot, OrderCreate, - OrderItemCreate, OrderUpdate, ) -from app.modules.billing.services.subscription_service import ( - subscription_service, - TierLimitExceededException, -) +from app.modules.tenancy.models import Store from app.utils.money import Money, cents_to_euros, euros_to_cents from app.utils.vat import ( VATResult, calculate_vat_amount, determine_vat_regime, ) -from app.modules.marketplace.models import MarketplaceProduct, MarketplaceProductTranslation -from app.modules.catalog.models import Product -from app.modules.tenancy.models import Store # Placeholder product constants PLACEHOLDER_GTIN = "0000000000000" @@ -372,7 +372,7 @@ class OrderService: store_id=store_id, subtotal_cents=subtotal_cents, billing_country_iso=billing.country_iso, - buyer_vat_number=getattr(billing, 'vat_number', None), + buyer_vat_number=getattr(billing, "vat_number", None), ) # Calculate amounts in cents @@ -1291,7 +1291,9 @@ class OrderService: order_id: int, ) -> dict[str, Any]: """Get shipping label information for an order (admin only).""" - from app.modules.core.services.admin_settings_service import admin_settings_service # noqa: MOD-004 + from app.modules.core.services.admin_settings_service import ( + admin_settings_service, # noqa: MOD-004 + ) order = db.query(Order).filter(Order.id == order_id).first() diff --git a/app/modules/orders/tests/unit/test_invoice_service.py b/app/modules/orders/tests/unit/test_invoice_service.py index 6afacb3f..d506ec9f 100644 --- a/app/modules/orders/tests/unit/test_invoice_service.py +++ b/app/modules/orders/tests/unit/test_invoice_service.py @@ -1,7 +1,6 @@ # tests/unit/services/test_invoice_service.py """Unit tests for InvoiceService.""" -import uuid from decimal import Decimal import pytest @@ -11,21 +10,19 @@ from app.modules.orders.exceptions import ( InvoiceNotFoundException, InvoiceSettingsNotFoundException, ) -from app.modules.orders.services.invoice_service import ( - EU_VAT_RATES, - InvoiceService, - LU_VAT_RATES, -) from app.modules.orders.models import ( Invoice, InvoiceStatus, - VATRegime, StoreInvoiceSettings, + VATRegime, ) from app.modules.orders.schemas import ( StoreInvoiceSettingsCreate, StoreInvoiceSettingsUpdate, ) +from app.modules.orders.services.invoice_service import ( + InvoiceService, +) @pytest.mark.unit diff --git a/app/modules/orders/tests/unit/test_order_item_exception_model.py b/app/modules/orders/tests/unit/test_order_item_exception_model.py index 849f343b..3724c125 100644 --- a/app/modules/orders/tests/unit/test_order_item_exception_model.py +++ b/app/modules/orders/tests/unit/test_order_item_exception_model.py @@ -1,6 +1,8 @@ # tests/unit/models/database/test_order_item_exception.py """Unit tests for OrderItemException database model.""" +from datetime import UTC + import pytest from sqlalchemy.exc import IntegrityError @@ -153,7 +155,7 @@ class TestOrderItemExceptionModel: def test_exception_resolution(self, db, test_order_item, test_store, test_product, test_user): """Test resolving an exception with a product.""" - from datetime import datetime, timezone + from datetime import datetime exception = OrderItemException( order_item_id=test_order_item.id, @@ -166,7 +168,7 @@ class TestOrderItemExceptionModel: db.commit() # Resolve the exception - now = datetime.now(timezone.utc) + now = datetime.now(UTC) exception.status = "resolved" exception.resolved_product_id = test_product.id exception.resolved_at = now diff --git a/app/modules/orders/tests/unit/test_order_model.py b/app/modules/orders/tests/unit/test_order_model.py index 498dd7f4..a54b6a0a 100644 --- a/app/modules/orders/tests/unit/test_order_model.py +++ b/app/modules/orders/tests/unit/test_order_model.py @@ -1,7 +1,7 @@ # tests/unit/models/database/test_order.py """Unit tests for Order and OrderItem database models.""" -from datetime import datetime, timezone +from datetime import UTC, datetime import pytest from sqlalchemy.exc import IntegrityError @@ -33,7 +33,7 @@ def create_order_with_snapshots( subtotal=subtotal, total_amount=total_amount, currency="EUR", - order_date=datetime.now(timezone.utc), + order_date=datetime.now(UTC), # Customer snapshot customer_first_name=customer.first_name, customer_last_name=customer.last_name, diff --git a/app/modules/orders/tests/unit/test_order_schema.py b/app/modules/orders/tests/unit/test_order_schema.py index 7fc3854d..2cecb12f 100644 --- a/app/modules/orders/tests/unit/test_order_schema.py +++ b/app/modules/orders/tests/unit/test_order_schema.py @@ -1,14 +1,13 @@ # tests/unit/models/schema/test_order.py """Unit tests for order Pydantic schemas.""" -from datetime import datetime, timezone +from datetime import UTC, datetime import pytest from pydantic import ValidationError from app.modules.orders.schemas import ( AddressSnapshot, - AddressSnapshotResponse, CustomerSnapshot, CustomerSnapshotResponse, OrderCreate, @@ -385,7 +384,7 @@ class TestOrderResponseSchema: def test_from_dict(self): """Test creating response from dict.""" - now = datetime.now(timezone.utc) + now = datetime.now(UTC) data = { "id": 1, "store_id": 1, @@ -448,7 +447,7 @@ class TestOrderResponseSchema: def test_is_marketplace_order(self): """Test is_marketplace_order property.""" - now = datetime.now(timezone.utc) + now = datetime.now(UTC) # Direct order direct_order = OrderResponse( id=1, store_id=1, customer_id=1, order_number="ORD-001", @@ -499,7 +498,7 @@ class TestOrderItemResponseSchema: def test_from_dict(self): """Test creating response from dict.""" - now = datetime.now(timezone.utc) + now = datetime.now(UTC) data = { "id": 1, "order_id": 1, @@ -525,7 +524,7 @@ class TestOrderItemResponseSchema: def test_has_unresolved_exception(self): """Test has_unresolved_exception property.""" - now = datetime.now(timezone.utc) + now = datetime.now(UTC) base_data = { "id": 1, "order_id": 1, "product_id": 1, "product_name": "Test", "product_sku": "SKU-001", diff --git a/app/modules/payments/exceptions.py b/app/modules/payments/exceptions.py index abd7bc97..8872a603 100644 --- a/app/modules/payments/exceptions.py +++ b/app/modules/payments/exceptions.py @@ -14,7 +14,6 @@ from app.exceptions.base import ( ValidationException, ) - # ============================================================================= # Webhook Exceptions # ============================================================================= diff --git a/app/modules/payments/routes/api/store.py b/app/modules/payments/routes/api/store.py index 86ae5dd3..832b1491 100644 --- a/app/modules/payments/routes/api/store.py +++ b/app/modules/payments/routes/api/store.py @@ -22,21 +22,25 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_api, require_module_access from app.core.database import get_db from app.modules.enums import FrontendType -from app.modules.tenancy.services.store_service import store_service -from models.schema.auth import UserContext from app.modules.payments.schemas import ( PaymentBalanceResponse, PaymentConfigResponse, PaymentConfigUpdate, PaymentConfigUpdateResponse, PaymentMethodsResponse, - PaymentRefundRequest as RefundRequest, - PaymentRefundResponse as RefundResponse, StripeConnectRequest, StripeConnectResponse, StripeDisconnectResponse, TransactionsResponse, ) +from app.modules.payments.schemas import ( + PaymentRefundRequest as RefundRequest, +) +from app.modules.payments.schemas import ( + PaymentRefundResponse as RefundResponse, +) +from app.modules.tenancy.services.store_service import store_service +from models.schema.auth import UserContext store_router = APIRouter( prefix="/payments", diff --git a/app/modules/payments/routes/api/webhooks.py b/app/modules/payments/routes/api/webhooks.py index 33148c35..cfce8e7e 100644 --- a/app/modules/payments/routes/api/webhooks.py +++ b/app/modules/payments/routes/api/webhooks.py @@ -13,9 +13,12 @@ import logging from fastapi import APIRouter, Header, Request from app.core.database import get_db -from app.modules.payments.exceptions import InvalidWebhookSignatureException, WebhookMissingSignatureException -from app.modules.billing.services.stripe_service import stripe_service from app.handlers.stripe_webhook import stripe_webhook_handler +from app.modules.billing.services.stripe_service import stripe_service +from app.modules.payments.exceptions import ( + InvalidWebhookSignatureException, + WebhookMissingSignatureException, +) router = APIRouter(prefix="/stripe") logger = logging.getLogger(__name__) diff --git a/app/modules/payments/schemas/__init__.py b/app/modules/payments/schemas/__init__.py index d9edcb21..d7a4b023 100644 --- a/app/modules/payments/schemas/__init__.py +++ b/app/modules/payments/schemas/__init__.py @@ -9,25 +9,25 @@ from typing import Any from pydantic import BaseModel, Field from app.modules.payments.schemas.payment import ( + # Balance + PaymentBalanceResponse, # Configuration PaymentConfigResponse, PaymentConfigUpdate, PaymentConfigUpdateResponse, + # Methods + PaymentMethodInfo, + PaymentMethodsResponse, + # Refunds (config version) + PaymentRefundRequest, + PaymentRefundResponse, # Stripe StripeConnectRequest, StripeConnectResponse, StripeDisconnectResponse, - # Methods - PaymentMethodInfo, - PaymentMethodsResponse, # Transactions TransactionInfo, TransactionsResponse, - # Balance - PaymentBalanceResponse, - # Refunds (config version) - PaymentRefundRequest, - PaymentRefundResponse, ) diff --git a/app/modules/payments/services/__init__.py b/app/modules/payments/services/__init__.py index 48b158fb..43009835 100644 --- a/app/modules/payments/services/__init__.py +++ b/app/modules/payments/services/__init__.py @@ -7,7 +7,7 @@ Provides: - GatewayService: Gateway abstraction layer """ -from app.modules.payments.services.payment_service import PaymentService from app.modules.payments.services.gateway_service import GatewayService +from app.modules.payments.services.payment_service import PaymentService __all__ = ["PaymentService", "GatewayService"] diff --git a/app/modules/payments/services/gateway_service.py b/app/modules/payments/services/gateway_service.py index fcae5e97..35cc1671 100644 --- a/app/modules/payments/services/gateway_service.py +++ b/app/modules/payments/services/gateway_service.py @@ -97,13 +97,11 @@ class BaseGateway(ABC): @abstractmethod def code(self) -> str: """Gateway code identifier.""" - pass @property @abstractmethod def name(self) -> str: """Gateway display name.""" - pass @abstractmethod async def process_payment( @@ -114,7 +112,6 @@ class BaseGateway(ABC): **kwargs: Any, ) -> dict[str, Any]: """Process a payment.""" - pass @abstractmethod async def refund( @@ -123,7 +120,6 @@ class BaseGateway(ABC): amount: int | None = None, ) -> dict[str, Any]: """Issue a refund.""" - pass async def health_check(self) -> bool: """Check if gateway is operational.""" diff --git a/app/modules/payments/services/payment_service.py b/app/modules/payments/services/payment_service.py index 140129ac..313351a4 100644 --- a/app/modules/payments/services/payment_service.py +++ b/app/modules/payments/services/payment_service.py @@ -27,8 +27,7 @@ Usage: import logging from dataclasses import dataclass -from datetime import datetime, timezone -from decimal import Decimal +from datetime import UTC, datetime from enum import Enum from typing import Any @@ -147,7 +146,7 @@ class PaymentService: status=PaymentStatus.SUCCEEDED, amount=amount, currency=currency, - created_at=datetime.now(timezone.utc), + created_at=datetime.now(UTC), ) async def refund( diff --git a/app/modules/registry.py b/app/modules/registry.py index c5efcc31..d148949a 100644 --- a/app/modules/registry.py +++ b/app/modules/registry.py @@ -65,11 +65,11 @@ def __getattr__(name: str): """Lazy module-level attribute access for backward compatibility.""" if name == "MODULES": return _get_all_modules() - elif name == "CORE_MODULES": + if name == "CORE_MODULES": return _get_modules_by_tier()["core"] - elif name == "OPTIONAL_MODULES": + if name == "OPTIONAL_MODULES": return _get_modules_by_tier()["optional"] - elif name == "INTERNAL_MODULES": + if name == "INTERNAL_MODULES": return _get_modules_by_tier()["internal"] raise AttributeError(f"module {__name__!r} has no attribute {name!r}") @@ -216,9 +216,9 @@ def get_module_tier(code: str) -> str | None: by_tier = _get_modules_by_tier() if code in by_tier["core"]: return "core" - elif code in by_tier["optional"]: + if code in by_tier["optional"]: return "optional" - elif code in by_tier["internal"]: + if code in by_tier["internal"]: return "internal" return None diff --git a/app/modules/routes.py b/app/modules/routes.py index 5ee7c909..6c88a5da 100644 --- a/app/modules/routes.py +++ b/app/modules/routes.py @@ -41,7 +41,7 @@ Route Configuration: import importlib import logging -from dataclasses import dataclass, field +from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING @@ -176,12 +176,12 @@ def _discover_routes_in_dir( "store": { "api_prefix": "/api/v1/store", "pages_prefix": "/store", - "include_in_schema": True if route_type == "api" else False, + "include_in_schema": route_type == "api", }, "storefront": { "api_prefix": "/api/v1/storefront", "pages_prefix": "/storefront", - "include_in_schema": True if route_type == "api" else False, + "include_in_schema": route_type == "api", }, "platform": { "api_prefix": "/api/v1/platform", @@ -247,10 +247,7 @@ def _discover_routes_in_dir( prefix = config["pages_prefix"] # Build tags - use custom tags if provided, otherwise default - if custom_tags: - tags = custom_tags - else: - tags = [f"{frontend}-{module_code}"] + tags = custom_tags if custom_tags else [f"{frontend}-{module_code}"] route_info = RouteInfo( router=router, diff --git a/app/modules/service.py b/app/modules/service.py index c395e858..f15ab17f 100644 --- a/app/modules/service.py +++ b/app/modules/service.py @@ -13,21 +13,19 @@ If neither is configured, all modules are enabled (backwards compatibility). """ import logging -from datetime import datetime, timezone -from functools import lru_cache +from datetime import UTC, datetime from sqlalchemy.orm import Session from app.modules.base import ModuleDefinition +from app.modules.enums import FrontendType from app.modules.registry import ( MODULES, get_core_module_codes, get_menu_item_module, get_module, ) -from app.modules.enums import FrontendType -from app.modules.tenancy.models import Platform -from app.modules.tenancy.models import PlatformModule +from app.modules.tenancy.models import Platform, PlatformModule logger = logging.getLogger(__name__) @@ -231,10 +229,10 @@ class ModuleService: else: enabled_codes = set(enabled_modules) | get_core_module_codes() - now = datetime.now(timezone.utc) + now = datetime.now(UTC) # Create junction table records for all known modules - for code in MODULES.keys(): + for code in MODULES: is_enabled = code in enabled_codes pm = PlatformModule( platform_id=platform_id, @@ -516,10 +514,10 @@ class ModuleService: # Resolve dependencies enabled_set = self._resolve_dependencies(enabled_set) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) # Update junction table for all modules - for code in MODULES.keys(): + for code in MODULES: platform_module = ( db.query(PlatformModule) .filter( @@ -594,7 +592,7 @@ class ModuleService: # Migrate JSON settings to junction table if needed self._migrate_json_to_junction_table(db, platform_id, user_id) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) # Enable this module and its dependencies modules_to_enable = {module_code} @@ -674,7 +672,7 @@ class ModuleService: # Migrate JSON settings to junction table if needed self._migrate_json_to_junction_table(db, platform_id, user_id) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) # Get modules to disable (this one + dependents) modules_to_disable = {module_code} diff --git a/app/modules/tenancy/exceptions.py b/app/modules/tenancy/exceptions.py index 47386340..355d8f5c 100644 --- a/app/modules/tenancy/exceptions.py +++ b/app/modules/tenancy/exceptions.py @@ -18,7 +18,6 @@ from app.exceptions.base import ( WizamartException, ) - # ============================================================================= # Authentication Exceptions # ============================================================================= diff --git a/app/modules/tenancy/migrations/versions/tenancy_001_add_merchant_domains.py b/app/modules/tenancy/migrations/versions/tenancy_001_add_merchant_domains.py index f5f86917..e6b8ad4f 100644 --- a/app/modules/tenancy/migrations/versions/tenancy_001_add_merchant_domains.py +++ b/app/modules/tenancy/migrations/versions/tenancy_001_add_merchant_domains.py @@ -4,9 +4,10 @@ Revision ID: tenancy_001 Revises: dev_tools_001 Create Date: 2026-02-09 """ -from alembic import op import sqlalchemy as sa +from alembic import op + revision = "tenancy_001" down_revision = "dev_tools_001" branch_labels = None diff --git a/app/modules/tenancy/models/__init__.py b/app/modules/tenancy/models/__init__.py index 71cff76f..ad007553 100644 --- a/app/modules/tenancy/models/__init__.py +++ b/app/modules/tenancy/models/__init__.py @@ -20,7 +20,6 @@ This is the canonical location for tenancy module models including: # NOTE: MarketplaceImportJob relationships have been moved to the marketplace module. # Optional modules own their relationships to core models, not vice versa. from app.modules.core.models import AdminMenuConfig # noqa: F401 - from app.modules.tenancy.models.admin import ( AdminAuditLog, AdminSession, @@ -30,13 +29,13 @@ from app.modules.tenancy.models.admin import ( ) from app.modules.tenancy.models.admin_platform import AdminPlatform from app.modules.tenancy.models.merchant import Merchant +from app.modules.tenancy.models.merchant_domain import MerchantDomain from app.modules.tenancy.models.platform import Platform from app.modules.tenancy.models.platform_module import PlatformModule -from app.modules.tenancy.models.user import User, UserRole from app.modules.tenancy.models.store import Role, Store, StoreUser, StoreUserType -from app.modules.tenancy.models.merchant_domain import MerchantDomain from app.modules.tenancy.models.store_domain import StoreDomain from app.modules.tenancy.models.store_platform import StorePlatform +from app.modules.tenancy.models.user import User, UserRole __all__ = [ # Admin models diff --git a/app/modules/tenancy/routes/api/admin.py b/app/modules/tenancy/routes/api/admin.py index 6129994b..cf47514b 100644 --- a/app/modules/tenancy/routes/api/admin.py +++ b/app/modules/tenancy/routes/api/admin.py @@ -19,15 +19,15 @@ The tenancy module owns identity and organizational hierarchy. from fastapi import APIRouter from .admin_auth import admin_auth_router -from .admin_users import admin_users_router -from .admin_platform_users import admin_platform_users_router -from .admin_merchants import admin_merchants_router -from .admin_platforms import admin_platforms_router -from .admin_stores import admin_stores_router -from .admin_store_domains import admin_store_domains_router from .admin_merchant_domains import admin_merchant_domains_router -from .admin_modules import router as admin_modules_router +from .admin_merchants import admin_merchants_router from .admin_module_config import router as admin_module_config_router +from .admin_modules import router as admin_modules_router +from .admin_platform_users import admin_platform_users_router +from .admin_platforms import admin_platforms_router +from .admin_store_domains import admin_store_domains_router +from .admin_stores import admin_stores_router +from .admin_users import admin_users_router admin_router = APIRouter() diff --git a/app/modules/tenancy/routes/api/admin_auth.py b/app/modules/tenancy/routes/api/admin_auth.py index 7c74cf7e..3cbe64d6 100644 --- a/app/modules/tenancy/routes/api/admin_auth.py +++ b/app/modules/tenancy/routes/api/admin_auth.py @@ -17,13 +17,20 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api, get_current_admin_from_cookie_or_header from app.core.database import get_db from app.core.environment import should_use_secure_cookies -from app.modules.tenancy.exceptions import InsufficientPermissionsException, InvalidCredentialsException -from app.modules.tenancy.services.admin_platform_service import admin_platform_service from app.modules.core.services.auth_service import auth_service +from app.modules.tenancy.exceptions import ( + InvalidCredentialsException, +) +from app.modules.tenancy.services.admin_platform_service import admin_platform_service from middleware.auth import AuthManager -from app.modules.tenancy.models import Platform # noqa: API-007 - Admin needs to query platforms -from models.schema.auth import UserContext -from models.schema.auth import LoginResponse, LogoutResponse, PlatformSelectResponse, UserLogin, UserResponse +from models.schema.auth import ( + LoginResponse, + LogoutResponse, + PlatformSelectResponse, + UserContext, + UserLogin, + UserResponse, +) admin_auth_router = APIRouter(prefix="/auth") logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/routes/api/admin_merchant_domains.py b/app/modules/tenancy/routes/api/admin_merchant_domains.py index 74a843d8..f44cba52 100644 --- a/app/modules/tenancy/routes/api/admin_merchant_domains.py +++ b/app/modules/tenancy/routes/api/admin_merchant_domains.py @@ -16,10 +16,6 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db -from app.modules.tenancy.services.merchant_domain_service import ( - merchant_domain_service, -) -from models.schema.auth import UserContext from app.modules.tenancy.schemas.merchant_domain import ( MerchantDomainCreate, MerchantDomainDeletionResponse, @@ -31,6 +27,10 @@ from app.modules.tenancy.schemas.store_domain import ( DomainVerificationInstructions, DomainVerificationResponse, ) +from app.modules.tenancy.services.merchant_domain_service import ( + merchant_domain_service, +) +from models.schema.auth import UserContext admin_merchant_domains_router = APIRouter(prefix="/merchants") logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/routes/api/admin_merchants.py b/app/modules/tenancy/routes/api/admin_merchants.py index c4c41e7c..26538505 100644 --- a/app/modules/tenancy/routes/api/admin_merchants.py +++ b/app/modules/tenancy/routes/api/admin_merchants.py @@ -11,9 +11,10 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db -from app.modules.tenancy.exceptions import MerchantHasStoresException, ConfirmationRequiredException -from app.modules.tenancy.services.merchant_service import merchant_service -from models.schema.auth import UserContext +from app.modules.tenancy.exceptions import ( + ConfirmationRequiredException, + MerchantHasStoresException, +) from app.modules.tenancy.schemas.merchant import ( MerchantCreate, MerchantCreateResponse, @@ -24,6 +25,8 @@ from app.modules.tenancy.schemas.merchant import ( MerchantTransferOwnershipResponse, MerchantUpdate, ) +from app.modules.tenancy.services.merchant_service import merchant_service +from models.schema.auth import UserContext admin_merchants_router = APIRouter(prefix="/merchants") logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/routes/api/admin_modules.py b/app/modules/tenancy/routes/api/admin_modules.py index 5a1112ee..4f7a5b4e 100644 --- a/app/modules/tenancy/routes/api/admin_modules.py +++ b/app/modules/tenancy/routes/api/admin_modules.py @@ -136,7 +136,7 @@ async def list_all_modules( Super admin only. """ modules = [] - for code in MODULES.keys(): + for code in MODULES: # All modules shown as enabled in the global list modules.append(_build_module_response(code, is_enabled=True)) @@ -172,7 +172,7 @@ async def get_platform_modules( enabled_codes = module_service.get_enabled_module_codes(db, platform_id) modules = [] - for code in MODULES.keys(): + for code in MODULES: is_enabled = code in enabled_codes modules.append(_build_module_response(code, is_enabled)) @@ -222,7 +222,7 @@ async def update_platform_modules( enabled_codes = module_service.get_enabled_module_codes(db, platform_id) modules = [] - for code in MODULES.keys(): + for code in MODULES: is_enabled = code in enabled_codes modules.append(_build_module_response(code, is_enabled)) diff --git a/app/modules/tenancy/routes/api/admin_platform_users.py b/app/modules/tenancy/routes/api/admin_platform_users.py index 9440823e..428ad699 100644 --- a/app/modules/tenancy/routes/api/admin_platform_users.py +++ b/app/modules/tenancy/routes/api/admin_platform_users.py @@ -16,10 +16,10 @@ from app.api.deps import get_current_admin_api from app.core.database import get_db from app.modules.core.services.stats_aggregator import stats_aggregator from app.modules.tenancy.services.admin_service import admin_service -from models.schema.auth import UserContext from models.schema.auth import ( OwnedMerchantSummary, StoreMembershipSummary, + UserContext, UserCreate, UserDeleteResponse, UserDetailResponse, diff --git a/app/modules/tenancy/routes/api/admin_store_domains.py b/app/modules/tenancy/routes/api/admin_store_domains.py index 01e813a6..9b22b7d7 100644 --- a/app/modules/tenancy/routes/api/admin_store_domains.py +++ b/app/modules/tenancy/routes/api/admin_store_domains.py @@ -16,9 +16,6 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db -from app.modules.tenancy.services.store_domain_service import store_domain_service -from app.modules.tenancy.services.store_service import store_service -from models.schema.auth import UserContext from app.modules.tenancy.schemas.store_domain import ( DomainDeletionResponse, DomainVerificationInstructions, @@ -28,6 +25,9 @@ from app.modules.tenancy.schemas.store_domain import ( StoreDomainResponse, StoreDomainUpdate, ) +from app.modules.tenancy.services.store_domain_service import store_domain_service +from app.modules.tenancy.services.store_service import store_service +from models.schema.auth import UserContext admin_store_domains_router = APIRouter(prefix="/stores") logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/routes/api/admin_stores.py b/app/modules/tenancy/routes/api/admin_stores.py index 6c2822b9..a6d84b00 100644 --- a/app/modules/tenancy/routes/api/admin_stores.py +++ b/app/modules/tenancy/routes/api/admin_stores.py @@ -16,9 +16,6 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db from app.modules.tenancy.exceptions import ConfirmationRequiredException -from app.modules.tenancy.services.admin_service import admin_service -from app.modules.tenancy.services.store_service import store_service -from models.schema.auth import UserContext from app.modules.tenancy.schemas.store import ( StoreCreate, StoreCreateResponse, @@ -27,6 +24,9 @@ from app.modules.tenancy.schemas.store import ( StoreStatsResponse, StoreUpdate, ) +from app.modules.tenancy.services.admin_service import admin_service +from app.modules.tenancy.services.store_service import store_service +from models.schema.auth import UserContext admin_stores_router = APIRouter(prefix="/stores") logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/routes/api/admin_users.py b/app/modules/tenancy/routes/api/admin_users.py index 9436e00f..8610ec7f 100644 --- a/app/modules/tenancy/routes/api/admin_users.py +++ b/app/modules/tenancy/routes/api/admin_users.py @@ -13,7 +13,6 @@ This module provides endpoints for: import logging from datetime import datetime -from typing import Optional from fastapi import APIRouter, Body, Depends, Path, Query from pydantic import BaseModel, EmailStr @@ -22,8 +21,10 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_super_admin, get_current_super_admin_api from app.core.database import get_db from app.exceptions import ValidationException +from app.modules.tenancy.models import ( + User, # noqa: API-007 - Internal helper uses User model +) from app.modules.tenancy.services.admin_platform_service import admin_platform_service -from app.modules.tenancy.models import User # noqa: API-007 - Internal helper uses User model from models.schema.auth import UserContext admin_users_router = APIRouter(prefix="/admin-users") @@ -53,14 +54,14 @@ class AdminUserResponse(BaseModel): id: int email: str username: str - first_name: Optional[str] = None - last_name: Optional[str] = None + first_name: str | None = None + last_name: str | None = None is_active: bool is_super_admin: bool platform_assignments: list[PlatformAssignmentResponse] = [] created_at: datetime updated_at: datetime - last_login: Optional[datetime] = None + last_login: datetime | None = None class Config: from_attributes = True @@ -79,8 +80,8 @@ class CreateAdminUserRequest(BaseModel): email: EmailStr username: str password: str - first_name: Optional[str] = None - last_name: Optional[str] = None + first_name: str | None = None + last_name: str | None = None is_super_admin: bool = False platform_ids: list[int] = [] @@ -204,22 +205,21 @@ def create_admin_user( is_super_admin=user.is_super_admin, platform_assignments=[], ) - else: - # Create platform admin with assignments using service - user, assignments = admin_platform_service.create_platform_admin( - db=db, - email=request.email, - username=request.username, - password=request.password, - platform_ids=request.platform_ids, - created_by_user_id=current_admin.id, - first_name=request.first_name, - last_name=request.last_name, - ) - db.commit() - db.refresh(user) + # Create platform admin with assignments using service + user, assignments = admin_platform_service.create_platform_admin( + db=db, + email=request.email, + username=request.username, + password=request.password, + platform_ids=request.platform_ids, + created_by_user_id=current_admin.id, + first_name=request.first_name, + last_name=request.last_name, + ) + db.commit() + db.refresh(user) - return _build_admin_response(user) + return _build_admin_response(user) @admin_users_router.get("/{user_id}", response_model=AdminUserResponse) diff --git a/app/modules/tenancy/routes/api/merchant.py b/app/modules/tenancy/routes/api/merchant.py index a3d33585..dbc9f7a0 100644 --- a/app/modules/tenancy/routes/api/merchant.py +++ b/app/modules/tenancy/routes/api/merchant.py @@ -10,15 +10,18 @@ Auto-discovered by the route system (merchant.py in routes/api/). """ import logging -from typing import Optional -from fastapi import APIRouter, Depends, HTTPException, Request -from pydantic import BaseModel, EmailStr +from fastapi import APIRouter, Depends, Query, Request from sqlalchemy.orm import Session -from app.api.deps import get_current_merchant_from_cookie_or_header +from app.api.deps import get_current_merchant_api, get_merchant_for_current_user from app.core.database import get_db -from app.modules.tenancy.models import Merchant +from app.modules.tenancy.schemas import ( + MerchantPortalProfileResponse, + MerchantPortalProfileUpdate, + MerchantPortalStoreListResponse, +) +from app.modules.tenancy.services.merchant_service import merchant_service from models.schema.auth import UserContext from .merchant_auth import merchant_auth_router @@ -34,95 +37,40 @@ router.include_router(merchant_auth_router, tags=["merchant-auth"]) _account_router = APIRouter(prefix="/account") -# ============================================================================ -# SCHEMAS -# ============================================================================ - - -class MerchantProfileUpdate(BaseModel): - """Schema for updating merchant profile information.""" - - name: str | None = None - contact_email: EmailStr | None = None - contact_phone: str | None = None - website: str | None = None - business_address: str | None = None - tax_number: str | None = None - - -# ============================================================================ -# HELPERS -# ============================================================================ - - -def _get_user_merchant(db: Session, user_context: UserContext) -> Merchant: - """ - Get the first active merchant owned by the authenticated user. - - Args: - db: Database session - user_context: Authenticated user context - - Returns: - Merchant: The user's active merchant - - Raises: - HTTPException: 404 if user does not own any active merchant - """ - merchant = ( - db.query(Merchant) - .filter( - Merchant.owner_user_id == user_context.id, - Merchant.is_active == True, # noqa: E712 - ) - .first() - ) - - if not merchant: - raise HTTPException(status_code=404, detail="Merchant not found") - - return merchant - - # ============================================================================ # ACCOUNT ENDPOINTS # ============================================================================ -@_account_router.get("/stores") +@_account_router.get("/stores", response_model=MerchantPortalStoreListResponse) async def merchant_stores( request: Request, - current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header), + skip: int = Query(0, ge=0, description="Number of records to skip"), + limit: int = Query(100, ge=1, le=200, description="Max records to return"), + merchant=Depends(get_merchant_for_current_user), db: Session = Depends(get_db), ): """ List all stores belonging to the merchant. - Returns a list of store summary dicts with basic info for each store - owned by the authenticated merchant. + Returns a paginated list of store summaries for the authenticated merchant. """ - merchant = _get_user_merchant(db, current_user) + stores, total = merchant_service.get_merchant_stores( + db, merchant.id, skip=skip, limit=limit + ) - stores = [] - for store in merchant.stores: - stores.append( - { - "id": store.id, - "name": store.name, - "store_code": store.store_code, - "is_active": store.is_active, - "created_at": store.created_at.isoformat() if store.created_at else None, - } - ) - - return {"stores": stores} + return MerchantPortalStoreListResponse( + stores=stores, + total=total, + skip=skip, + limit=limit, + ) -@_account_router.get("/profile") +@_account_router.get("/profile", response_model=MerchantPortalProfileResponse) async def merchant_profile( request: Request, - current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header), - db: Session = Depends(get_db), + merchant=Depends(get_merchant_for_current_user), ): """ Get the authenticated merchant's profile information. @@ -130,25 +78,15 @@ async def merchant_profile( Returns merchant details including contact info, business details, and verification status. """ - merchant = _get_user_merchant(db, current_user) - - return { - "id": merchant.id, - "name": merchant.name, - "contact_email": merchant.contact_email, - "contact_phone": merchant.contact_phone, - "website": merchant.website, - "business_address": merchant.business_address, - "tax_number": merchant.tax_number, - "is_verified": merchant.is_verified, - } + return merchant -@_account_router.put("/profile") +@_account_router.put("/profile", response_model=MerchantPortalProfileResponse) async def update_merchant_profile( request: Request, - profile_data: MerchantProfileUpdate, - current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header), + profile_data: MerchantPortalProfileUpdate, + current_user: UserContext = Depends(get_current_merchant_api), + merchant=Depends(get_merchant_for_current_user), db: Session = Depends(get_db), ): """ @@ -156,8 +94,6 @@ async def update_merchant_profile( Accepts partial updates - only provided fields are changed. """ - merchant = _get_user_merchant(db, current_user) - # Apply only the fields that were explicitly provided update_data = profile_data.model_dump(exclude_unset=True) for field_name, value in update_data.items(): @@ -171,16 +107,7 @@ async def update_merchant_profile( f"user={current_user.username}, fields={list(update_data.keys())}" ) - return { - "id": merchant.id, - "name": merchant.name, - "contact_email": merchant.contact_email, - "contact_phone": merchant.contact_phone, - "website": merchant.website, - "business_address": merchant.business_address, - "tax_number": merchant.tax_number, - "is_verified": merchant.is_verified, - } + return merchant # Include account routes in main router diff --git a/app/modules/tenancy/routes/api/merchant_auth.py b/app/modules/tenancy/routes/api/merchant_auth.py index cf907ace..79837d0c 100644 --- a/app/modules/tenancy/routes/api/merchant_auth.py +++ b/app/modules/tenancy/routes/api/merchant_auth.py @@ -18,7 +18,13 @@ from app.api.deps import get_current_merchant_from_cookie_or_header from app.core.database import get_db from app.core.environment import should_use_secure_cookies from app.modules.core.services.auth_service import auth_service -from models.schema.auth import LoginResponse, LogoutResponse, UserLogin, UserResponse, UserContext +from models.schema.auth import ( + LoginResponse, + LogoutResponse, + UserContext, + UserLogin, + UserResponse, +) merchant_auth_router = APIRouter(prefix="/auth") logger = logging.getLogger(__name__) @@ -106,12 +112,6 @@ def merchant_logout(response: Response): path="/merchants", ) - # Also clear legacy cookie with path=/ (from before path isolation was added) - response.delete_cookie( - key="merchant_token", - path="/", - ) - - logger.debug("Deleted merchant_token cookies (both /merchants and / paths)") + logger.debug("Deleted merchant_token cookie (path=/merchants)") return LogoutResponse(message="Logged out successfully") diff --git a/app/modules/tenancy/routes/api/store.py b/app/modules/tenancy/routes/api/store.py index d9afe22b..cf4db2f0 100644 --- a/app/modules/tenancy/routes/api/store.py +++ b/app/modules/tenancy/routes/api/store.py @@ -17,8 +17,8 @@ from fastapi import APIRouter, Depends, Path from sqlalchemy.orm import Session from app.core.database import get_db -from app.modules.tenancy.services.store_service import store_service # noqa: mod-004 from app.modules.tenancy.schemas.store import StoreDetailResponse +from app.modules.tenancy.services.store_service import store_service # noqa: mod-004 store_router = APIRouter() logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/routes/api/store_auth.py b/app/modules/tenancy/routes/api/store_auth.py index e132908d..0339d907 100644 --- a/app/modules/tenancy/routes/api/store_auth.py +++ b/app/modules/tenancy/routes/api/store_auth.py @@ -21,11 +21,10 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_api from app.core.database import get_db from app.core.environment import should_use_secure_cookies -from app.modules.tenancy.exceptions import InvalidCredentialsException from app.modules.core.services.auth_service import auth_service +from app.modules.tenancy.exceptions import InvalidCredentialsException from middleware.store_context import get_current_store -from models.schema.auth import UserContext -from models.schema.auth import LogoutResponse, UserLogin, StoreUserResponse +from models.schema.auth import LogoutResponse, StoreUserResponse, UserContext, UserLogin store_auth_router = APIRouter(prefix="/auth") logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/routes/api/store_profile.py b/app/modules/tenancy/routes/api/store_profile.py index 8b63af50..545d4990 100644 --- a/app/modules/tenancy/routes/api/store_profile.py +++ b/app/modules/tenancy/routes/api/store_profile.py @@ -13,9 +13,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_store_api from app.core.database import get_db +from app.modules.tenancy.schemas.store import StoreResponse, StoreUpdate from app.modules.tenancy.services.store_service import store_service from models.schema.auth import UserContext -from app.modules.tenancy.schemas.store import StoreResponse, StoreUpdate store_profile_router = APIRouter(prefix="/profile") logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/routes/api/store_team.py b/app/modules/tenancy/routes/api/store_team.py index e1332dcb..60138545 100644 --- a/app/modules/tenancy/routes/api/store_team.py +++ b/app/modules/tenancy/routes/api/store_team.py @@ -22,10 +22,6 @@ from app.api.deps import ( require_store_permission, ) from app.core.database import get_db -# Permission IDs are now defined in module definition.py files -# and discovered by PermissionDiscoveryService -from app.modules.tenancy.services.store_team_service import store_team_service -from models.schema.auth import UserContext from app.modules.tenancy.schemas.team import ( BulkRemoveRequest, BulkRemoveResponse, @@ -41,6 +37,11 @@ from app.modules.tenancy.schemas.team import ( UserPermissionsResponse, ) +# Permission IDs are now defined in module definition.py files +# and discovered by PermissionDiscoveryService +from app.modules.tenancy.services.store_team_service import store_team_service +from models.schema.auth import UserContext + store_team_router = APIRouter(prefix="/team") logger = logging.getLogger(__name__) @@ -268,7 +269,7 @@ def update_team_member( """ store = request.state.store - store_user = store_team_service.update_member_role( + store_team_service.update_member_role( db=db, store=store, user_id=user_id, diff --git a/app/modules/tenancy/routes/pages/admin.py b/app/modules/tenancy/routes/pages/admin.py index c804db86..bac5c4b1 100644 --- a/app/modules/tenancy/routes/pages/admin.py +++ b/app/modules/tenancy/routes/pages/admin.py @@ -17,9 +17,9 @@ from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access from app.modules.core.utils.page_context import get_admin_context -from app.templates_config import templates from app.modules.enums import FrontendType from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/tenancy/routes/pages/store.py b/app/modules/tenancy/routes/pages/store.py index a4218842..da7d74fd 100644 --- a/app/modules/tenancy/routes/pages/store.py +++ b/app/modules/tenancy/routes/pages/store.py @@ -20,8 +20,8 @@ from app.api.deps import ( get_db, ) from app.modules.core.utils.page_context import get_store_context -from app.templates_config import templates from app.modules.tenancy.models import User +from app.templates_config import templates router = APIRouter() diff --git a/app/modules/tenancy/schemas/__init__.py b/app/modules/tenancy/schemas/__init__.py index 4eaf7cb2..c3861189 100644 --- a/app/modules/tenancy/schemas/__init__.py +++ b/app/modules/tenancy/schemas/__init__.py @@ -6,30 +6,6 @@ Request/response schemas for platform, merchant, store, admin user, and team man """ # Merchant schemas -from app.modules.tenancy.schemas.merchant import ( - MerchantBase, - MerchantCreate, - MerchantCreateResponse, - MerchantDetailResponse, - MerchantListResponse, - MerchantResponse, - MerchantSummary, - MerchantTransferOwnership, - MerchantTransferOwnershipResponse, - MerchantUpdate, -) - -# Store schemas -from app.modules.tenancy.schemas.store import ( - StoreCreate, - StoreCreateResponse, - StoreDetailResponse, - StoreListResponse, - StoreResponse, - StoreSummary, - StoreUpdate, -) - # Admin schemas from app.modules.tenancy.schemas.admin import ( AdminAuditLogFilters, @@ -50,10 +26,10 @@ from app.modules.tenancy.schemas.admin import ( ApplicationLogFilters, ApplicationLogListResponse, ApplicationLogResponse, - BulkUserAction, - BulkUserActionResponse, BulkStoreAction, BulkStoreActionResponse, + BulkUserAction, + BulkUserActionResponse, ComponentHealthStatus, FileLogResponse, LogCleanupResponse, @@ -73,6 +49,43 @@ from app.modules.tenancy.schemas.admin import ( RowsPerPageUpdateResponse, SystemHealthResponse, ) +from app.modules.tenancy.schemas.merchant import ( + MerchantBase, + MerchantCreate, + MerchantCreateResponse, + MerchantDetailResponse, + MerchantListResponse, + MerchantPortalProfileResponse, + MerchantPortalProfileUpdate, + MerchantPortalStoreListResponse, + MerchantResponse, + MerchantSummary, + MerchantTransferOwnership, + MerchantTransferOwnershipResponse, + MerchantUpdate, +) + +# Store schemas +from app.modules.tenancy.schemas.store import ( + StoreCreate, + StoreCreateResponse, + StoreDetailResponse, + StoreListResponse, + StoreResponse, + StoreSummary, + StoreUpdate, +) + +# Store domain schemas +from app.modules.tenancy.schemas.store_domain import ( + DomainDeletionResponse, + DomainVerificationInstructions, + DomainVerificationResponse, + StoreDomainCreate, + StoreDomainListResponse, + StoreDomainResponse, + StoreDomainUpdate, +) # Team schemas from app.modules.tenancy.schemas.team import ( @@ -98,17 +111,6 @@ from app.modules.tenancy.schemas.team import ( UserPermissionsResponse, ) -# Store domain schemas -from app.modules.tenancy.schemas.store_domain import ( - DomainDeletionResponse, - DomainVerificationInstructions, - DomainVerificationResponse, - StoreDomainCreate, - StoreDomainListResponse, - StoreDomainResponse, - StoreDomainUpdate, -) - __all__ = [ # Merchant "MerchantBase", @@ -116,6 +118,9 @@ __all__ = [ "MerchantCreateResponse", "MerchantDetailResponse", "MerchantListResponse", + "MerchantPortalProfileResponse", + "MerchantPortalProfileUpdate", + "MerchantPortalStoreListResponse", "MerchantResponse", "MerchantSummary", "MerchantTransferOwnership", diff --git a/app/modules/tenancy/schemas/merchant.py b/app/modules/tenancy/schemas/merchant.py index b3a3aab2..61f55289 100644 --- a/app/modules/tenancy/schemas/merchant.py +++ b/app/modules/tenancy/schemas/merchant.py @@ -10,6 +10,8 @@ from typing import Any from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator +from app.modules.tenancy.schemas.store import StoreSummary + class MerchantBase(BaseModel): """Base schema for merchant with common fields.""" @@ -214,3 +216,46 @@ class MerchantTransferOwnershipResponse(BaseModel): transferred_at: datetime transfer_reason: str | None + + +# ============================================================================ +# Merchant Portal Schemas (for merchant-facing routes) +# ============================================================================ + + +class MerchantPortalProfileResponse(BaseModel): + """Merchant profile as seen by the merchant owner.""" + + model_config = ConfigDict(from_attributes=True) + + id: int + name: str + description: str | None + contact_email: str + contact_phone: str | None + website: str | None + business_address: str | None + tax_number: str | None + is_verified: bool + + +class MerchantPortalProfileUpdate(BaseModel): + """Merchant profile update from the merchant portal. + Excludes admin-only fields (is_active, is_verified).""" + + name: str | None = Field(None, min_length=2, max_length=200) + description: str | None = None + contact_email: EmailStr | None = None + contact_phone: str | None = None + website: str | None = None + business_address: str | None = None + tax_number: str | None = None + + +class MerchantPortalStoreListResponse(BaseModel): + """Paginated store list for the merchant portal.""" + + stores: list[StoreSummary] + total: int + skip: int + limit: int diff --git a/app/modules/tenancy/schemas/team.py b/app/modules/tenancy/schemas/team.py index 5859addd..a18241c0 100644 --- a/app/modules/tenancy/schemas/team.py +++ b/app/modules/tenancy/schemas/team.py @@ -84,7 +84,7 @@ class TeamMemberInvite(TeamMemberBase): ) @field_validator("role_name") - def validate_role_name(cls, v): + def validate_role_name(self, v): """Validate role name is in allowed presets.""" if v is not None: allowed_roles = ["manager", "staff", "support", "viewer", "marketing"] @@ -95,7 +95,7 @@ class TeamMemberInvite(TeamMemberBase): return v.lower() if v else v @field_validator("custom_permissions") - def validate_custom_permissions(cls, v, values): + def validate_custom_permissions(self, v, values): """Ensure either role_id/role_name OR custom_permissions is provided.""" if v is not None and len(v) > 0: # If custom permissions provided, role_name should be provided too @@ -170,7 +170,7 @@ class InvitationAccept(BaseModel): last_name: str = Field(..., min_length=1, max_length=100) @field_validator("password") - def validate_password_strength(cls, v): + def validate_password_strength(self, v): """Validate password meets minimum requirements.""" if len(v) < 8: raise ValueError("Password must be at least 8 characters long") diff --git a/app/modules/tenancy/services/__init__.py b/app/modules/tenancy/services/__init__.py index 2dca1325..a98a1521 100644 --- a/app/modules/tenancy/services/__init__.py +++ b/app/modules/tenancy/services/__init__.py @@ -20,13 +20,15 @@ from app.modules.tenancy.services.admin_platform_service import ( admin_platform_service, ) from app.modules.tenancy.services.admin_service import AdminService, admin_service -from app.modules.tenancy.services.merchant_service import MerchantService, merchant_service +from app.modules.tenancy.services.merchant_service import ( + MerchantService, + merchant_service, +) from app.modules.tenancy.services.platform_service import ( PlatformService, PlatformStats, platform_service, ) -from app.modules.tenancy.services.team_service import TeamService, team_service from app.modules.tenancy.services.store_domain_service import ( StoreDomainService, store_domain_service, @@ -36,6 +38,7 @@ from app.modules.tenancy.services.store_team_service import ( StoreTeamService, store_team_service, ) +from app.modules.tenancy.services.team_service import TeamService, team_service __all__ = [ # Store diff --git a/app/modules/tenancy/services/admin_platform_service.py b/app/modules/tenancy/services/admin_platform_service.py index 67d737f7..79716569 100644 --- a/app/modules/tenancy/services/admin_platform_service.py +++ b/app/modules/tenancy/services/admin_platform_service.py @@ -20,9 +20,7 @@ from app.modules.tenancy.exceptions import ( AdminOperationException, CannotModifySelfException, ) -from app.modules.tenancy.models import AdminPlatform -from app.modules.tenancy.models import Platform -from app.modules.tenancy.models import User +from app.modules.tenancy.models import AdminPlatform, Platform, User from models.schema.auth import UserContext logger = logging.getLogger(__name__) @@ -344,7 +342,6 @@ class AdminPlatformService: field="user_id", ) - old_status = user.is_super_admin user.is_super_admin = is_super_admin user.updated_at = datetime.now(UTC) db.flush() diff --git a/app/modules/tenancy/services/admin_service.py b/app/modules/tenancy/services/admin_service.py index c23f784b..e3964a4b 100644 --- a/app/modules/tenancy/services/admin_service.py +++ b/app/modules/tenancy/services/admin_service.py @@ -23,21 +23,18 @@ from app.exceptions import ValidationException from app.modules.tenancy.exceptions import ( AdminOperationException, CannotModifySelfException, + StoreAlreadyExistsException, + StoreNotFoundException, + StoreVerificationException, UserAlreadyExistsException, UserCannotBeDeletedException, UserNotFoundException, UserRoleChangeException, UserStatusChangeException, - StoreAlreadyExistsException, - StoreNotFoundException, - StoreVerificationException, ) -from middleware.auth import AuthManager -from app.modules.tenancy.models import Merchant -from app.modules.tenancy.models import Platform -from app.modules.tenancy.models import User -from app.modules.tenancy.models import Role, Store +from app.modules.tenancy.models import Merchant, Platform, Role, Store, User from app.modules.tenancy.schemas.store import StoreCreate +from middleware.auth import AuthManager logger = logging.getLogger(__name__) @@ -564,7 +561,6 @@ class AdminService: store = self._get_store_by_id_or_raise(db, store_id) try: - original_status = store.is_active store.is_active = not store.is_active store.updated_at = datetime.now(UTC) db.flush() diff --git a/app/modules/tenancy/services/merchant_domain_service.py b/app/modules/tenancy/services/merchant_domain_service.py index 5f126e58..cf1e986b 100644 --- a/app/modules/tenancy/services/merchant_domain_service.py +++ b/app/modules/tenancy/services/merchant_domain_service.py @@ -80,7 +80,7 @@ class MerchantDomainService: """ try: # Verify merchant exists - merchant = self._get_merchant_by_id_or_raise(db, merchant_id) + self._get_merchant_by_id_or_raise(db, merchant_id) # Check domain limit self._check_domain_limit(db, merchant_id) diff --git a/app/modules/tenancy/services/merchant_service.py b/app/modules/tenancy/services/merchant_service.py index b0d07c8e..bc7314e3 100644 --- a/app/modules/tenancy/services/merchant_service.py +++ b/app/modules/tenancy/services/merchant_service.py @@ -12,10 +12,16 @@ import string from sqlalchemy import func, select from sqlalchemy.orm import Session, joinedload -from app.modules.tenancy.exceptions import MerchantNotFoundException, UserNotFoundException -from app.modules.tenancy.models import Merchant -from app.modules.tenancy.models import User -from app.modules.tenancy.schemas.merchant import MerchantCreate, MerchantTransferOwnership, MerchantUpdate +from app.modules.tenancy.exceptions import ( + MerchantNotFoundException, + UserNotFoundException, +) +from app.modules.tenancy.models import Merchant, Store, User +from app.modules.tenancy.schemas.merchant import ( + MerchantCreate, + MerchantTransferOwnership, + MerchantUpdate, +) logger = logging.getLogger(__name__) @@ -320,6 +326,15 @@ class MerchantService: return merchant, old_owner, new_owner + def get_merchant_stores( + self, db: Session, merchant_id: int, skip: int = 0, limit: int = 100 + ) -> tuple[list, int]: + """Get paginated stores for a merchant.""" + query = db.query(Store).filter(Store.merchant_id == merchant_id) + total = query.count() + stores = query.order_by(Store.id).offset(skip).limit(limit).all() + return stores, total + def _generate_temp_password(self, length: int = 12) -> str: """Generate secure temporary password.""" alphabet = string.ascii_letters + string.digits + "!@#$%^&*" diff --git a/app/modules/tenancy/services/permission_discovery_service.py b/app/modules/tenancy/services/permission_discovery_service.py index 5bb913d0..8fc72003 100644 --- a/app/modules/tenancy/services/permission_discovery_service.py +++ b/app/modules/tenancy/services/permission_discovery_service.py @@ -32,8 +32,6 @@ Usage: import logging from dataclasses import dataclass, field -from app.modules.base import PermissionDefinition - logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/services/platform_service.py b/app/modules/tenancy/services/platform_service.py index 08319f9a..f9e47201 100644 --- a/app/modules/tenancy/services/platform_service.py +++ b/app/modules/tenancy/services/platform_service.py @@ -17,12 +17,11 @@ from dataclasses import dataclass from sqlalchemy import func from sqlalchemy.orm import Session +from app.modules.cms.models import ContentPage from app.modules.tenancy.exceptions import ( PlatformNotFoundException, ) -from app.modules.cms.models import ContentPage -from app.modules.tenancy.models import Platform -from app.modules.tenancy.models import StorePlatform +from app.modules.tenancy.models import Platform, StorePlatform logger = logging.getLogger(__name__) @@ -159,7 +158,7 @@ class PlatformService: db.query(func.count(ContentPage.id)) .filter( ContentPage.platform_id == platform_id, - ContentPage.store_id == None, + ContentPage.store_id is None, ContentPage.is_platform_page == True, ) .scalar() @@ -182,7 +181,7 @@ class PlatformService: db.query(func.count(ContentPage.id)) .filter( ContentPage.platform_id == platform_id, - ContentPage.store_id == None, + ContentPage.store_id is None, ContentPage.is_platform_page == False, ) .scalar() @@ -205,7 +204,7 @@ class PlatformService: db.query(func.count(ContentPage.id)) .filter( ContentPage.platform_id == platform_id, - ContentPage.store_id != None, + ContentPage.store_id is not None, ) .scalar() or 0 diff --git a/app/modules/tenancy/services/store_domain_service.py b/app/modules/tenancy/services/store_domain_service.py index 46c15ac3..3c8c8708 100644 --- a/app/modules/tenancy/services/store_domain_service.py +++ b/app/modules/tenancy/services/store_domain_service.py @@ -29,9 +29,11 @@ from app.modules.tenancy.exceptions import ( StoreDomainNotFoundException, StoreNotFoundException, ) -from app.modules.tenancy.models import Store -from app.modules.tenancy.models import StoreDomain -from app.modules.tenancy.schemas.store_domain import StoreDomainCreate, StoreDomainUpdate +from app.modules.tenancy.models import Store, StoreDomain +from app.modules.tenancy.schemas.store_domain import ( + StoreDomainCreate, + StoreDomainUpdate, +) logger = logging.getLogger(__name__) @@ -74,7 +76,7 @@ class StoreDomainService: """ try: # Verify store exists - store = self._get_store_by_id_or_raise(db, store_id) + self._get_store_by_id_or_raise(db, store_id) # Check domain limit self._check_domain_limit(db, store_id) diff --git a/app/modules/tenancy/services/store_service.py b/app/modules/tenancy/services/store_service.py index f8910b21..c89e51aa 100644 --- a/app/modules/tenancy/services/store_service.py +++ b/app/modules/tenancy/services/store_service.py @@ -18,12 +18,11 @@ from sqlalchemy.orm import Session from app.exceptions import ValidationException from app.modules.tenancy.exceptions import ( InvalidStoreDataException, - UnauthorizedStoreAccessException, StoreAlreadyExistsException, StoreNotFoundException, + UnauthorizedStoreAccessException, ) -from app.modules.tenancy.models import User -from app.modules.tenancy.models import Store +from app.modules.tenancy.models import Store, User from app.modules.tenancy.schemas.store import StoreCreate logger = logging.getLogger(__name__) @@ -489,10 +488,7 @@ class StoreService: return True # Check if user is owner via StoreUser relationship - if user.is_owner_of(store.id): - return True - - return False + return bool(user.is_owner_of(store.id)) def update_store( self, diff --git a/app/modules/tenancy/services/store_team_service.py b/app/modules/tenancy/services/store_team_service.py index fc24147d..a3105a0b 100644 --- a/app/modules/tenancy/services/store_team_service.py +++ b/app/modules/tenancy/services/store_team_service.py @@ -24,6 +24,7 @@ from app.modules.tenancy.services.permission_discovery_service import ( def get_preset_permissions(preset_name: str) -> set[str]: """Get permissions for a preset role.""" return permission_discovery_service.get_preset_permissions(preset_name) +from app.modules.billing.exceptions import TierLimitExceededException from app.modules.tenancy.exceptions import ( CannotRemoveOwnerException, InvalidInvitationTokenException, @@ -31,10 +32,8 @@ from app.modules.tenancy.exceptions import ( TeamMemberAlreadyExistsException, UserNotFoundException, ) -from app.modules.billing.exceptions import TierLimitExceededException +from app.modules.tenancy.models import Role, Store, StoreUser, StoreUserType, User from middleware.auth import AuthManager -from app.modules.tenancy.models import User -from app.modules.tenancy.models import Role, Store, StoreUser, StoreUserType logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/services/team_service.py b/app/modules/tenancy/services/team_service.py index 67c81763..c436aa0c 100644 --- a/app/modules/tenancy/services/team_service.py +++ b/app/modules/tenancy/services/team_service.py @@ -15,8 +15,7 @@ from typing import Any from sqlalchemy.orm import Session from app.exceptions import ValidationException -from app.modules.tenancy.models import User -from app.modules.tenancy.models import Role, StoreUser +from app.modules.tenancy.models import Role, StoreUser, User logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/services/tenancy_features.py b/app/modules/tenancy/services/tenancy_features.py index b73a15fb..50371dae 100644 --- a/app/modules/tenancy/services/tenancy_features.py +++ b/app/modules/tenancy/services/tenancy_features.py @@ -16,7 +16,6 @@ from sqlalchemy import func from app.modules.contracts.features import ( FeatureDeclaration, - FeatureProviderProtocol, FeatureScope, FeatureType, FeatureUsage, @@ -113,9 +112,9 @@ class TenancyFeatureProvider: merchant_id: int, platform_id: int, ) -> list[FeatureUsage]: - from app.modules.tenancy.models.user import User from app.modules.tenancy.models.store import Store, StoreUser from app.modules.tenancy.models.store_platform import StorePlatform + from app.modules.tenancy.models.user import User # Count active users associated with stores owned by this merchant count = ( diff --git a/app/modules/tenancy/services/tenancy_metrics.py b/app/modules/tenancy/services/tenancy_metrics.py index 6766103e..fbaf2634 100644 --- a/app/modules/tenancy/services/tenancy_metrics.py +++ b/app/modules/tenancy/services/tenancy_metrics.py @@ -16,9 +16,8 @@ from sqlalchemy import func from sqlalchemy.orm import Session from app.modules.contracts.metrics import ( - MetricValue, MetricsContext, - MetricsProviderProtocol, + MetricValue, ) if TYPE_CHECKING: @@ -124,7 +123,14 @@ class TenancyMetricsProvider: - Total users - Active users """ - from app.modules.tenancy.models import AdminPlatform, Merchant, StoreUser, User, Store, StorePlatform + from app.modules.tenancy.models import ( + AdminPlatform, + Merchant, + Store, + StorePlatform, + StoreUser, + User, + ) try: # Store metrics - using StorePlatform junction table diff --git a/app/modules/tenancy/services/tenancy_widgets.py b/app/modules/tenancy/services/tenancy_widgets.py index d2c67ae4..749ac4eb 100644 --- a/app/modules/tenancy/services/tenancy_widgets.py +++ b/app/modules/tenancy/services/tenancy_widgets.py @@ -15,7 +15,6 @@ from sqlalchemy.orm import Session from app.modules.contracts.widgets import ( DashboardWidget, - DashboardWidgetProviderProtocol, ListWidget, WidgetContext, WidgetListItem, diff --git a/app/modules/tenancy/tests/integration/test_merchant_auth_routes.py b/app/modules/tenancy/tests/integration/test_merchant_auth_routes.py new file mode 100644 index 00000000..69da91c7 --- /dev/null +++ b/app/modules/tenancy/tests/integration/test_merchant_auth_routes.py @@ -0,0 +1,317 @@ +# app/modules/tenancy/tests/integration/test_merchant_auth_routes.py +""" +Integration tests for merchant authentication API routes. + +Tests the merchant auth endpoints at: + /api/v1/merchants/auth/* + +Uses real login flow (no dependency overrides) to verify JWT generation, +cookie setting, and token validation end-to-end. +""" + +import uuid + +import pytest + +from app.modules.tenancy.models import Merchant, User + +# ============================================================================ +# Fixtures +# ============================================================================ + +BASE = "/api/v1/merchants/auth" + + +@pytest.fixture +def ma_owner(db): + """Create a merchant owner user with known credentials.""" + from middleware.auth import AuthManager + + auth = AuthManager() + uid = uuid.uuid4().hex[:8] + user = User( + email=f"maowner_{uid}@test.com", + username=f"maowner_{uid}", + hashed_password=auth.hash_password("mapass123"), + role="store", + is_active=True, + ) + db.add(user) + db.commit() + db.refresh(user) + return user + + +@pytest.fixture +def ma_merchant(db, ma_owner): + """Create a merchant owned by ma_owner.""" + merchant = Merchant( + name="Auth Test Merchant", + owner_user_id=ma_owner.id, + contact_email=ma_owner.email, + is_active=True, + is_verified=True, + ) + db.add(merchant) + db.commit() + db.refresh(merchant) + return merchant + + +@pytest.fixture +def ma_non_merchant_user(db): + """Create a user who does NOT own any merchants.""" + from middleware.auth import AuthManager + + auth = AuthManager() + uid = uuid.uuid4().hex[:8] + user = User( + email=f"nonmerch_{uid}@test.com", + username=f"nonmerch_{uid}", + hashed_password=auth.hash_password("nonmerch123"), + role="store", + is_active=True, + ) + db.add(user) + db.commit() + db.refresh(user) + return user + + +@pytest.fixture +def ma_inactive_user(db): + """Create an inactive user who owns a merchant.""" + from middleware.auth import AuthManager + + auth = AuthManager() + uid = uuid.uuid4().hex[:8] + user = User( + email=f"inactive_{uid}@test.com", + username=f"inactive_{uid}", + hashed_password=auth.hash_password("inactive123"), + role="store", + is_active=False, + ) + db.add(user) + db.commit() + db.refresh(user) + + merchant = Merchant( + name="Inactive Owner Merchant", + owner_user_id=user.id, + contact_email=user.email, + is_active=True, + is_verified=True, + ) + db.add(merchant) + db.commit() + + return user + + +# ============================================================================ +# Login Tests +# ============================================================================ + + +class TestMerchantLogin: + """Tests for POST /api/v1/merchants/auth/login.""" + + def test_login_success(self, client, ma_owner, ma_merchant): + response = client.post( + f"{BASE}/login", + json={ + "email_or_username": ma_owner.username, + "password": "mapass123", + }, + ) + assert response.status_code == 200 + data = response.json() + assert "access_token" in data + assert data["token_type"] == "bearer" + assert "expires_in" in data + assert "user" in data + + def test_login_sets_cookie(self, client, ma_owner, ma_merchant): + response = client.post( + f"{BASE}/login", + json={ + "email_or_username": ma_owner.username, + "password": "mapass123", + }, + ) + assert response.status_code == 200 + # Check Set-Cookie header + cookies = response.headers.get_list("set-cookie") if hasattr(response.headers, "get_list") else [ + v for k, v in response.headers.items() if k.lower() == "set-cookie" + ] + merchant_cookies = [c for c in cookies if "merchant_token" in c] + assert len(merchant_cookies) > 0 + # Verify path restriction + assert "path=/merchants" in merchant_cookies[0].lower() or "Path=/merchants" in merchant_cookies[0] + + def test_login_wrong_password(self, client, ma_owner, ma_merchant): + response = client.post( + f"{BASE}/login", + json={ + "email_or_username": ma_owner.username, + "password": "wrong_password", + }, + ) + assert response.status_code == 401 + + def test_login_non_merchant_user(self, client, ma_non_merchant_user): + response = client.post( + f"{BASE}/login", + json={ + "email_or_username": ma_non_merchant_user.username, + "password": "nonmerch123", + }, + ) + # Should fail because user doesn't own any merchants + assert response.status_code in (401, 403) + + def test_login_inactive_user(self, client, ma_inactive_user): + response = client.post( + f"{BASE}/login", + json={ + "email_or_username": ma_inactive_user.username, + "password": "inactive123", + }, + ) + assert response.status_code in (401, 403) + + +# ============================================================================ +# Me Tests +# ============================================================================ + + +class TestMerchantMe: + """Tests for GET /api/v1/merchants/auth/me.""" + + def test_me_success(self, client, ma_owner, ma_merchant): + # Login first to get token + login_resp = client.post( + f"{BASE}/login", + json={ + "email_or_username": ma_owner.username, + "password": "mapass123", + }, + ) + assert login_resp.status_code == 200 + token = login_resp.json()["access_token"] + + # Call /me with the token + response = client.get( + f"{BASE}/me", + headers={"Authorization": f"Bearer {token}"}, + ) + assert response.status_code == 200 + data = response.json() + assert data["username"] == ma_owner.username + assert data["email"] == ma_owner.email + + def test_me_no_token(self, client): + response = client.get(f"{BASE}/me") + assert response.status_code in (401, 403) + + +# ============================================================================ +# Logout Tests +# ============================================================================ + + +class TestMerchantLogout: + """Tests for POST /api/v1/merchants/auth/logout.""" + + def test_logout_clears_cookie(self, client, ma_owner, ma_merchant): + # Login first + login_resp = client.post( + f"{BASE}/login", + json={ + "email_or_username": ma_owner.username, + "password": "mapass123", + }, + ) + assert login_resp.status_code == 200 + + # Logout + response = client.post(f"{BASE}/logout") + assert response.status_code == 200 + data = response.json() + assert "message" in data + + # Check that cookie deletion is in response + cookies = [ + v for k, v in response.headers.items() if k.lower() == "set-cookie" + ] + merchant_cookies = [c for c in cookies if "merchant_token" in c] + assert len(merchant_cookies) > 0 + + +# ============================================================================ +# Auth Failure & Isolation Tests +# ============================================================================ + + +class TestMerchantAuthFailures: + """Tests for authentication edge cases and cross-portal isolation.""" + + def test_expired_token_rejected(self, client, ma_owner, ma_merchant): + """An expired JWT should be rejected.""" + from datetime import UTC, datetime, timedelta + + from jose import jwt + + # Create an already-expired token + payload = { + "sub": str(ma_owner.id), + "username": ma_owner.username, + "email": ma_owner.email, + "role": ma_owner.role, + "exp": datetime.now(UTC) - timedelta(hours=1), + "iat": datetime.now(UTC) - timedelta(hours=2), + } + import os + secret = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-in-production-please") + expired_token = jwt.encode(payload, secret, algorithm="HS256") + + response = client.get( + f"{BASE}/me", + headers={"Authorization": f"Bearer {expired_token}"}, + ) + assert response.status_code in (401, 403) + + def test_invalid_token_rejected(self, client): + """A completely invalid token should be rejected.""" + response = client.get( + f"{BASE}/me", + headers={"Authorization": "Bearer invalid.token.here"}, + ) + assert response.status_code in (401, 403) + + def test_store_token_not_accepted(self, client, db, ma_owner, ma_merchant): + """A store-context token should not grant merchant /me access. + + Store tokens include token_type=store which the merchant auth + dependency does not accept. + """ + from middleware.auth import AuthManager + + auth = AuthManager() + # Create a real token for the user, but with store context + token_data = auth.create_access_token( + user=ma_owner, + store_id=999, + store_code="FAKE_STORE", + store_role="owner", + ) + + response = client.get( + f"{BASE}/me", + headers={"Authorization": f"Bearer {token_data['access_token']}"}, + ) + # Store tokens should be rejected at merchant endpoints + # (they have store context which merchant auth doesn't accept) + assert response.status_code in (401, 403) diff --git a/app/modules/tenancy/tests/integration/test_merchant_routes.py b/app/modules/tenancy/tests/integration/test_merchant_routes.py new file mode 100644 index 00000000..bd79c79d --- /dev/null +++ b/app/modules/tenancy/tests/integration/test_merchant_routes.py @@ -0,0 +1,233 @@ +# app/modules/tenancy/tests/integration/test_merchant_routes.py +""" +Integration tests for merchant portal tenancy API routes. + +Tests the merchant portal endpoints at: + /api/v1/merchants/account/* + +Authentication: Overrides get_merchant_for_current_user and +get_current_merchant_api with mocks that return the test merchant/user. +""" + +import uuid + +import pytest + +from app.api.deps import get_current_merchant_api, get_merchant_for_current_user +from app.modules.tenancy.models import Merchant, Store, User +from main import app +from models.schema.auth import UserContext + +# ============================================================================ +# Fixtures +# ============================================================================ + +BASE = "/api/v1/merchants/account" + + +@pytest.fixture +def mt_owner(db): + """Create a merchant owner user.""" + from middleware.auth import AuthManager + + auth = AuthManager() + uid = uuid.uuid4().hex[:8] + user = User( + email=f"mtowner_{uid}@test.com", + username=f"mtowner_{uid}", + hashed_password=auth.hash_password("mtpass123"), + role="store", + is_active=True, + ) + db.add(user) + db.commit() + db.refresh(user) + return user + + +@pytest.fixture +def mt_merchant(db, mt_owner): + """Create a merchant owned by mt_owner.""" + merchant = Merchant( + name="Merchant Portal Test", + description="A test merchant for portal routes", + owner_user_id=mt_owner.id, + contact_email=mt_owner.email, + contact_phone="+352 123 456", + website="https://example.com", + business_address="1 Rue Test, Luxembourg", + tax_number="LU12345678", + is_active=True, + is_verified=True, + ) + db.add(merchant) + db.commit() + db.refresh(merchant) + return merchant + + +@pytest.fixture +def mt_stores(db, mt_merchant): + """Create stores for the merchant.""" + stores = [] + for i in range(3): + uid = uuid.uuid4().hex[:8] + store = Store( + merchant_id=mt_merchant.id, + store_code=f"MT_{uid.upper()}", + subdomain=f"mtstore{uid}", + name=f"MT Store {i}", + is_active=i < 2, # Third store inactive + is_verified=True, + ) + db.add(store) + stores.append(store) + db.commit() + for s in stores: + db.refresh(s) + return stores + + +@pytest.fixture +def mt_auth(mt_owner, mt_merchant): + """Override auth dependencies to return the test merchant/user.""" + user_context = UserContext( + id=mt_owner.id, + email=mt_owner.email, + username=mt_owner.username, + role="store", + is_active=True, + ) + + def _override_merchant(): + return mt_merchant + + def _override_user(): + return user_context + + app.dependency_overrides[get_merchant_for_current_user] = _override_merchant + app.dependency_overrides[get_current_merchant_api] = _override_user + yield {"Authorization": "Bearer fake-token"} + app.dependency_overrides.pop(get_merchant_for_current_user, None) + app.dependency_overrides.pop(get_current_merchant_api, None) + + +# ============================================================================ +# Store Endpoints +# ============================================================================ + + +class TestMerchantStoresList: + """Tests for GET /api/v1/merchants/account/stores.""" + + def test_list_stores_success(self, client, mt_auth, mt_stores): + response = client.get(f"{BASE}/stores", headers=mt_auth) + assert response.status_code == 200 + data = response.json() + assert "stores" in data + assert "total" in data + assert data["total"] == 3 + + def test_list_stores_response_shape(self, client, mt_auth, mt_stores): + response = client.get(f"{BASE}/stores", headers=mt_auth) + assert response.status_code == 200 + store = response.json()["stores"][0] + assert "id" in store + assert "name" in store + assert "store_code" in store + assert "is_active" in store + assert "subdomain" in store + + def test_list_stores_pagination(self, client, mt_auth, mt_stores): + response = client.get( + f"{BASE}/stores", + params={"skip": 0, "limit": 2}, + headers=mt_auth, + ) + assert response.status_code == 200 + data = response.json() + assert len(data["stores"]) == 2 + assert data["total"] == 3 + assert data["skip"] == 0 + assert data["limit"] == 2 + + def test_list_stores_empty(self, client, mt_auth, mt_merchant): + response = client.get(f"{BASE}/stores", headers=mt_auth) + assert response.status_code == 200 + data = response.json() + assert data["stores"] == [] + assert data["total"] == 0 + + +# ============================================================================ +# Profile Endpoints +# ============================================================================ + + +class TestMerchantProfile: + """Tests for GET/PUT /api/v1/merchants/account/profile.""" + + def test_get_profile_success(self, client, mt_auth, mt_merchant): + response = client.get(f"{BASE}/profile", headers=mt_auth) + assert response.status_code == 200 + data = response.json() + assert data["id"] == mt_merchant.id + assert data["name"] == "Merchant Portal Test" + assert data["description"] == "A test merchant for portal routes" + assert data["contact_email"] == mt_merchant.contact_email + assert data["contact_phone"] == "+352 123 456" + assert data["website"] == "https://example.com" + assert data["business_address"] == "1 Rue Test, Luxembourg" + assert data["tax_number"] == "LU12345678" + assert data["is_verified"] is True + + def test_get_profile_all_fields_present(self, client, mt_auth, mt_merchant): + response = client.get(f"{BASE}/profile", headers=mt_auth) + assert response.status_code == 200 + data = response.json() + expected_fields = { + "id", "name", "description", "contact_email", "contact_phone", + "website", "business_address", "tax_number", "is_verified", + } + assert expected_fields.issubset(set(data.keys())) + + def test_update_profile_partial(self, client, mt_auth, mt_merchant): + response = client.put( + f"{BASE}/profile", + json={"name": "Updated Merchant Name"}, + headers=mt_auth, + ) + assert response.status_code == 200 + data = response.json() + assert data["name"] == "Updated Merchant Name" + # Other fields should remain unchanged + assert data["contact_phone"] == "+352 123 456" + + def test_update_profile_email_validation(self, client, mt_auth, mt_merchant): + response = client.put( + f"{BASE}/profile", + json={"contact_email": "not-an-email"}, + headers=mt_auth, + ) + assert response.status_code == 422 + + def test_update_profile_cannot_set_admin_fields(self, client, mt_auth, mt_merchant): + """Admin-only fields (is_active, is_verified) are not accepted.""" + response = client.put( + f"{BASE}/profile", + json={"is_active": False, "is_verified": False}, + headers=mt_auth, + ) + # Should succeed but ignore unknown fields (Pydantic extra="ignore" by default) + assert response.status_code == 200 + data = response.json() + # is_verified should remain True (not changed) + assert data["is_verified"] is True + + def test_update_profile_name_too_short(self, client, mt_auth, mt_merchant): + response = client.put( + f"{BASE}/profile", + json={"name": "A"}, + headers=mt_auth, + ) + assert response.status_code == 422 diff --git a/app/modules/tenancy/tests/unit/test_admin_platform_service.py b/app/modules/tenancy/tests/unit/test_admin_platform_service.py index d84e1caf..dce120ce 100644 --- a/app/modules/tenancy/tests/unit/test_admin_platform_service.py +++ b/app/modules/tenancy/tests/unit/test_admin_platform_service.py @@ -8,7 +8,10 @@ Tests the admin platform assignment service operations. import pytest from app.exceptions import ValidationException -from app.modules.tenancy.exceptions import AdminOperationException, CannotModifySelfException +from app.modules.tenancy.exceptions import ( + AdminOperationException, + CannotModifySelfException, +) from app.modules.tenancy.services.admin_platform_service import AdminPlatformService @@ -242,8 +245,7 @@ class TestAdminPlatformServiceQueries: self, db, test_platform_admin, test_platform, test_super_admin, auth_manager ): """Test getting admins for a platform.""" - from app.modules.tenancy.models import AdminPlatform - from app.modules.tenancy.models import User + from app.modules.tenancy.models import AdminPlatform, User service = AdminPlatformService() diff --git a/app/modules/tenancy/tests/unit/test_merchant_domain.py b/app/modules/tenancy/tests/unit/test_merchant_domain.py index 80ab4183..7020303e 100644 --- a/app/modules/tenancy/tests/unit/test_merchant_domain.py +++ b/app/modules/tenancy/tests/unit/test_merchant_domain.py @@ -9,7 +9,6 @@ import pytest from app.modules.tenancy.models.merchant_domain import MerchantDomain from app.modules.tenancy.models.store_domain import StoreDomain - # ============================================================================= # MODEL TESTS # ============================================================================= diff --git a/app/modules/tenancy/tests/unit/test_merchant_domain_service.py b/app/modules/tenancy/tests/unit/test_merchant_domain_service.py index eda49477..54d9b67c 100644 --- a/app/modules/tenancy/tests/unit/test_merchant_domain_service.py +++ b/app/modules/tenancy/tests/unit/test_merchant_domain_service.py @@ -6,11 +6,9 @@ from datetime import UTC, datetime from unittest.mock import MagicMock, patch import pytest - from pydantic import ValidationError from app.modules.tenancy.exceptions import ( - DNSVerificationException, DomainAlreadyVerifiedException, DomainNotVerifiedException, DomainVerificationFailedException, @@ -29,7 +27,6 @@ from app.modules.tenancy.services.merchant_domain_service import ( merchant_domain_service, ) - # ============================================================================= # ADD DOMAIN TESTS # ============================================================================= diff --git a/app/modules/tenancy/tests/unit/test_store_domain_service.py b/app/modules/tenancy/tests/unit/test_store_domain_service.py index a9dd9597..e9a4c466 100644 --- a/app/modules/tenancy/tests/unit/test_store_domain_service.py +++ b/app/modules/tenancy/tests/unit/test_store_domain_service.py @@ -6,11 +6,9 @@ from datetime import UTC, datetime from unittest.mock import MagicMock, patch import pytest - from pydantic import ValidationError from app.modules.tenancy.exceptions import ( - DNSVerificationException, DomainAlreadyVerifiedException, DomainNotVerifiedException, DomainVerificationFailedException, @@ -20,10 +18,12 @@ from app.modules.tenancy.exceptions import ( StoreNotFoundException, ) from app.modules.tenancy.models import StoreDomain -from app.modules.tenancy.schemas.store_domain import StoreDomainCreate, StoreDomainUpdate +from app.modules.tenancy.schemas.store_domain import ( + StoreDomainCreate, + StoreDomainUpdate, +) from app.modules.tenancy.services.store_domain_service import store_domain_service - # ============================================================================= # FIXTURES # ============================================================================= diff --git a/app/modules/tenancy/tests/unit/test_store_service.py b/app/modules/tenancy/tests/unit/test_store_service.py index c16c4200..f44079f6 100644 --- a/app/modules/tenancy/tests/unit/test_store_service.py +++ b/app/modules/tenancy/tests/unit/test_store_service.py @@ -12,14 +12,13 @@ import pytest from app.exceptions import ValidationException from app.modules.tenancy.exceptions import ( InvalidStoreDataException, - UnauthorizedStoreAccessException, StoreAlreadyExistsException, StoreNotFoundException, + UnauthorizedStoreAccessException, ) -from app.modules.tenancy.services.store_service import StoreService -from app.modules.tenancy.models import Merchant -from app.modules.tenancy.models import Store +from app.modules.tenancy.models import Merchant, Store from app.modules.tenancy.schemas.store import StoreCreate +from app.modules.tenancy.services.store_service import StoreService @pytest.fixture diff --git a/app/modules/tenancy/tests/unit/test_store_team_service.py b/app/modules/tenancy/tests/unit/test_store_team_service.py index 9f593318..10b4ec85 100644 --- a/app/modules/tenancy/tests/unit/test_store_team_service.py +++ b/app/modules/tenancy/tests/unit/test_store_team_service.py @@ -3,21 +3,17 @@ import uuid from datetime import datetime, timedelta -from unittest.mock import patch import pytest from app.modules.tenancy.exceptions import ( CannotRemoveOwnerException, InvalidInvitationTokenException, - TeamInvitationAlreadyAcceptedException, - TeamMemberAlreadyExistsException, UserNotFoundException, ) -from app.modules.tenancy.models import Role, User, Store, StoreUser, StoreUserType +from app.modules.tenancy.models import Role, Store, StoreUser, StoreUserType, User from app.modules.tenancy.services.store_team_service import store_team_service - # ============================================================================= # FIXTURES # ============================================================================= diff --git a/app/modules/tenancy/tests/unit/test_team_model.py b/app/modules/tenancy/tests/unit/test_team_model.py index dcf9502c..0f306094 100644 --- a/app/modules/tenancy/tests/unit/test_team_model.py +++ b/app/modules/tenancy/tests/unit/test_team_model.py @@ -3,7 +3,7 @@ import pytest -from app.modules.tenancy.models import Role, Store, StoreUser +from app.modules.tenancy.models import Role, StoreUser @pytest.mark.unit diff --git a/app/modules/tenancy/tests/unit/test_team_service.py b/app/modules/tenancy/tests/unit/test_team_service.py index 99f6909c..1d76a153 100644 --- a/app/modules/tenancy/tests/unit/test_team_service.py +++ b/app/modules/tenancy/tests/unit/test_team_service.py @@ -10,14 +10,12 @@ Tests cover: - Get store roles """ -from datetime import UTC, datetime -from unittest.mock import MagicMock import pytest from app.exceptions import ValidationException -from app.modules.tenancy.services.team_service import TeamService, team_service from app.modules.tenancy.models import Role, StoreUser +from app.modules.tenancy.services.team_service import TeamService, team_service @pytest.mark.unit diff --git a/app/tasks/__init__.py b/app/tasks/__init__.py index 16986004..d1992796 100644 --- a/app/tasks/__init__.py +++ b/app/tasks/__init__.py @@ -14,6 +14,6 @@ Use the task_dispatcher for dispatching tasks from API routes: task_id = task_dispatcher.dispatch_marketplace_import(...) """ -from app.tasks.dispatcher import task_dispatcher, TaskDispatcher +from app.tasks.dispatcher import TaskDispatcher, task_dispatcher __all__ = ["task_dispatcher", "TaskDispatcher"] diff --git a/app/templates_config.py b/app/templates_config.py index b327a04b..dd9641f3 100644 --- a/app/templates_config.py +++ b/app/templates_config.py @@ -21,10 +21,10 @@ from fastapi.templating import Jinja2Templates from jinja2 import ChoiceLoader, FileSystemLoader from app.utils.i18n import ( + DEFAULT_LANGUAGE, LANGUAGE_FLAGS, LANGUAGE_NAMES, SUPPORTED_LANGUAGES, - DEFAULT_LANGUAGE, create_translation_context, ) diff --git a/app/utils/csv_processor.py b/app/utils/csv_processor.py index 24f182df..caa19c6e 100644 --- a/app/utils/csv_processor.py +++ b/app/utils/csv_processor.py @@ -18,12 +18,12 @@ import requests from sqlalchemy import literal from sqlalchemy.orm import Session -from app.utils.money import euros_to_cents from app.modules.marketplace.models import ( MarketplaceImportError, MarketplaceProduct, MarketplaceProductTranslation, ) +from app.utils.money import euros_to_cents logger = logging.getLogger(__name__) @@ -426,7 +426,7 @@ class CSVProcessor: f"{marketplace} -> {store_name}" ) - for batch_idx, (index, row) in enumerate(batch_df.iterrows()): + for batch_idx, (_index, row) in enumerate(batch_df.iterrows()): row_number = base_row_num + batch_idx row_dict = row.to_dict() diff --git a/app/utils/money.py b/app/utils/money.py index 85c3a354..7edd1ca6 100644 --- a/app/utils/money.py +++ b/app/utils/money.py @@ -29,7 +29,6 @@ See docs/architecture/money-handling.md for full documentation. import re from decimal import ROUND_HALF_UP, Decimal -from typing import Union # Type alias for clarity Cents = int @@ -47,7 +46,7 @@ CURRENCY_DECIMALS = { DEFAULT_CURRENCY = "EUR" -def euros_to_cents(euros: Union[float, str, Decimal, int, None]) -> Cents: +def euros_to_cents(euros: float | str | Decimal | int | None) -> Cents: """ Convert a euro amount to cents. @@ -93,7 +92,7 @@ def euros_to_cents(euros: Union[float, str, Decimal, int, None]) -> Cents: return int(cents.quantize(Decimal("1"), rounding=ROUND_HALF_UP)) -def cents_to_euros(cents: Union[int, None]) -> Euros: +def cents_to_euros(cents: int | None) -> Euros: """ Convert cents to euros. @@ -119,7 +118,7 @@ def cents_to_euros(cents: Union[int, None]) -> Euros: def parse_price_to_cents( - price_str: Union[str, float, int, None], + price_str: str | float | int | None, default_currency: str = DEFAULT_CURRENCY, ) -> tuple[Cents, str]: """ @@ -149,7 +148,7 @@ def parse_price_to_cents( if price_str is None: return (0, default_currency) - if isinstance(price_str, (int, float)): + if isinstance(price_str, int | float): return (euros_to_cents(price_str), default_currency) # Parse string @@ -202,23 +201,23 @@ class Money: """ @staticmethod - def from_euros(euros: Union[float, str, Decimal, None]) -> Cents: + def from_euros(euros: float | str | Decimal | None) -> Cents: """Create cents from euro amount.""" return euros_to_cents(euros) @staticmethod - def from_cents(cents: Union[int, None]) -> Cents: + def from_cents(cents: int | None) -> Cents: """Passthrough for cents (for consistency).""" return cents if cents is not None else 0 @staticmethod - def to_euros(cents: Union[int, None]) -> Euros: + def to_euros(cents: int | None) -> Euros: """Convert cents to euros.""" return cents_to_euros(cents) @staticmethod def format( - cents: Union[int, None], + cents: int | None, currency: str = "", locale: str = "en", ) -> str: @@ -259,7 +258,7 @@ class Money: return formatted @staticmethod - def parse(price_str: Union[str, float, int, None]) -> Cents: + def parse(price_str: str | float | int | None) -> Cents: """ Parse a price value to cents. @@ -273,7 +272,7 @@ class Money: return cents @staticmethod - def add(*amounts: Union[int, None]) -> Cents: + def add(*amounts: int | None) -> Cents: """ Add multiple cent amounts. @@ -286,7 +285,7 @@ class Money: return sum(a for a in amounts if a is not None) @staticmethod - def subtract(amount: int, *deductions: Union[int, None]) -> Cents: + def subtract(amount: int, *deductions: int | None) -> Cents: """ Subtract amounts from a base amount. diff --git a/app/utils/vat.py b/app/utils/vat.py index 546ffa45..48a9c9ea 100644 --- a/app/utils/vat.py +++ b/app/utils/vat.py @@ -203,16 +203,15 @@ def determine_vat_regime( destination_country=buyer_country, label=label, ) - else: - # No OSS: use origin country VAT - vat_rate = get_vat_rate_for_country(seller_country) - label = get_vat_rate_label(seller_country, vat_rate) - return VATResult( - regime=VATRegime.ORIGIN, - rate=vat_rate, - destination_country=buyer_country, - label=label, - ) + # No OSS: use origin country VAT + vat_rate = get_vat_rate_for_country(seller_country) + label = get_vat_rate_label(seller_country, vat_rate) + return VATResult( + regime=VATRegime.ORIGIN, + rate=vat_rate, + destination_country=buyer_country, + label=label, + ) # Non-EU = VAT exempt return VATResult( diff --git a/docs/deployment/hetzner-server-setup.md b/docs/deployment/hetzner-server-setup.md index 9690b8d9..c042c1ad 100644 --- a/docs/deployment/hetzner-server-setup.md +++ b/docs/deployment/hetzner-server-setup.md @@ -13,25 +13,30 @@ Complete step-by-step guide for deploying Wizamart on a Hetzner Cloud VPS. - **Auth**: SSH key (configured via Hetzner Console) - **Setup date**: 2026-02-11 -!!! success "Progress — 2026-02-11" - **Completed (Steps 1–12):** +!!! success "Progress — 2026-02-12" + **Completed (Steps 1–15):** - Non-root user `samir` with SSH key - Server hardened (UFW firewall, SSH root login disabled, fail2ban) - Docker 29.2.1 & Docker Compose 5.0.2 installed - - Gitea running at `http://91.99.65.229:3000` (user: `sboulahtit`, repo: `orion`) + - Gitea running at `https://git.wizard.lu` (user: `sboulahtit`, repo: `orion`) - Repository cloned to `~/apps/orion` - Production `.env` configured with generated secrets - Full Docker stack deployed (API, PostgreSQL, Redis, Celery worker/beat, Flower) - Database migrated (76 tables) and seeded (admin, platforms, CMS, email templates) - - API verified at `http://91.99.65.229:8001/docs` and `/admin/login` + - API verified at `https://api.wizard.lu/health` + - DNS A records configured and propagated for `wizard.lu` and subdomains + - Caddy 2.10.2 reverse proxy with auto-SSL (Let's Encrypt) + - Temporary firewall rules removed (ports 3000, 8001) + - Gitea Actions runner v0.2.13 registered and running as systemd service - **Remaining (Steps 13–15):** + **Remaining:** - - [ ] DNS: Point domain A records to `91.99.65.229` - - [ ] Caddy reverse proxy with auto-SSL - - [ ] Gitea Actions runner for CI/CD - - [ ] Remove temporary firewall rules (ports 3000, 8001) + - [ ] DNS A records for additional platform domains (`oms.lu`, `loyaltyplus.lu`) + - [ ] Uncomment platform domains in Caddyfile after DNS propagation + - [ ] AAAA (IPv6) records for all domains + - [ ] Update `platforms` table `domain` column to match production domains + - [ ] Verify CI pipeline runs successfully on push ## Installed Software Versions @@ -45,6 +50,8 @@ Complete step-by-step guide for deploying Wizamart on a Hetzner Cloud VPS. | Redis | 7-alpine (container) | | Python | 3.11-slim (container) | | Gitea | latest (container) | +| Caddy | 2.10.2 | +| act_runner | 0.2.13 | --- @@ -231,14 +238,45 @@ Then create a repository (e.g. `orion`). ## Step 8: Push Repository to Gitea +### Add SSH Key to Gitea + +Before pushing via SSH, add your local machine's public key to Gitea: + +1. Copy your public key: + + ```bash + cat ~/.ssh/id_ed25519.pub + # Or if using RSA: cat ~/.ssh/id_rsa.pub + ``` + +2. In the Gitea web UI: click your avatar → **Settings** → **SSH / GPG Keys** → **Add Key** → paste the key. + +3. Add the Gitea SSH host to known hosts: + + ```bash + ssh-keyscan -p 2222 git.wizard.lu >> ~/.ssh/known_hosts + ``` + +### Add Remote and Push + From your **local machine**: ```bash cd /home/samir/Documents/PycharmProjects/letzshop-product-import -git remote add gitea http://91.99.65.229:3000/sboulahtit/orion.git +git remote add gitea ssh://git@git.wizard.lu:2222/sboulahtit/orion.git git push gitea master ``` +!!! note "Remote URL updated" + The remote was initially set to `http://91.99.65.229:3000/...` during setup. + After Caddy was configured, it was updated to use the domain with SSH: + `ssh://git@git.wizard.lu:2222/sboulahtit/orion.git` + + To update an existing remote: + ```bash + git remote set-url gitea ssh://git@git.wizard.lu:2222/sboulahtit/orion.git + ``` + ## Step 9: Clone Repository on Server ```bash @@ -337,25 +375,58 @@ docker compose --profile full exec -e PYTHONPATH=/app api python scripts/seed/se --- -## Step 13: DNS Configuration (TODO) +## Step 13: DNS Configuration -Before setting up Caddy, point your domain's DNS to the server. In your domain registrar's DNS settings, create **A records**: +Before setting up Caddy, point your domain's DNS to the server. + +### wizard.lu (Main Platform) — Completed | Type | Name | Value | TTL | |---|---|---|---| | A | `@` | `91.99.65.229` | 300 | +| A | `www` | `91.99.65.229` | 300 | | A | `api` | `91.99.65.229` | 300 | | A | `git` | `91.99.65.229` | 300 | | A | `flower` | `91.99.65.229` | 300 | + +### oms.lu (OMS Platform) — TODO + +| Type | Name | Value | TTL | +|---|---|---|---| +| A | `@` | `91.99.65.229` | 300 | +| A | `www` | `91.99.65.229` | 300 | + +### loyaltyplus.lu (Loyalty+ Platform) — TODO + +| Type | Name | Value | TTL | +|---|---|---|---| +| A | `@` | `91.99.65.229` | 300 | +| A | `www` | `91.99.65.229` | 300 | + +### IPv6 (AAAA) Records — TODO + +Optional but recommended. Add AAAA records for all domains above, pointing to the server's IPv6 address. Verify your IPv6 address first: + +```bash +ip -6 addr show eth0 | grep 'scope global' +``` + +It should match the value in the Hetzner Cloud Console (Networking tab). Then create AAAA records mirroring each A record above, e.g.: + +| Type | Name (wizard.lu) | Value | TTL | +|---|---|---|---| | AAAA | `@` | `2a01:4f8:1c1a:b39c::1` | 300 | +| AAAA | `www` | `2a01:4f8:1c1a:b39c::1` | 300 | | AAAA | `api` | `2a01:4f8:1c1a:b39c::1` | 300 | | AAAA | `git` | `2a01:4f8:1c1a:b39c::1` | 300 | | AAAA | `flower` | `2a01:4f8:1c1a:b39c::1` | 300 | -!!! tip "DNS propagation" - Set TTL to 300 (5 minutes) initially. DNS changes can take up to 24 hours to propagate globally, but usually complete within 30 minutes. Verify with: `dig api.wizard.lu` +Repeat for `oms.lu` and `loyaltyplus.lu`. -## Step 14: Reverse Proxy with Caddy (TODO) +!!! tip "DNS propagation" + Set TTL to 300 (5 minutes) initially. DNS changes can take up to 24 hours to propagate globally, but usually complete within 30 minutes. Verify with: `dig api.wizard.lu +short` + +## Step 14: Reverse Proxy with Caddy Install Caddy: @@ -368,9 +439,41 @@ curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \ sudo apt update && sudo apt install caddy ``` -Configure `/etc/caddy/Caddyfile` (replace `wizard.lu` with your actual domain): +### Caddyfile Configuration + +Edit `/etc/caddy/Caddyfile`: ```caddy +# ─── Platform 1: Main (wizard.lu) ─────────────────────────── +wizard.lu { + reverse_proxy localhost:8001 +} + +www.wizard.lu { + redir https://wizard.lu{uri} permanent +} + +# ─── Platform 2: OMS (oms.lu) ─────────────────────────────── +# Uncomment after DNS is configured for oms.lu +# oms.lu { +# reverse_proxy localhost:8001 +# } +# +# www.oms.lu { +# redir https://oms.lu{uri} permanent +# } + +# ─── Platform 3: Loyalty+ (loyaltyplus.lu) ────────────────── +# Uncomment after DNS is configured for loyaltyplus.lu +# loyaltyplus.lu { +# reverse_proxy localhost:8001 +# } +# +# www.loyaltyplus.lu { +# redir https://loyaltyplus.lu{uri} permanent +# } + +# ─── Services ─────────────────────────────────────────────── api.wizard.lu { reverse_proxy localhost:8001 } @@ -384,11 +487,32 @@ flower.wizard.lu { } ``` +!!! info "How multi-platform routing works" + All platform domains (`wizard.lu`, `oms.lu`, `loyaltyplus.lu`) point to the **same FastAPI backend** on port 8001. The `PlatformContextMiddleware` reads the `Host` header to detect which platform the request is for. Caddy preserves the Host header by default, so no extra configuration is needed. + + The `domain` column in the `platforms` database table must match: + + | Platform | code | domain | + |---|---|---| + | Main | `main` | `wizard.lu` | + | OMS | `oms` | `oms.lu` | + | Loyalty+ | `loyalty` | `loyaltyplus.lu` | + +Start Caddy: + ```bash sudo systemctl restart caddy ``` -Caddy automatically provisions Let's Encrypt SSL certificates. +Caddy automatically provisions Let's Encrypt SSL certificates for all configured domains. + +Verify: + +```bash +curl -I https://wizard.lu +curl -I https://api.wizard.lu/health +curl -I https://git.wizard.lu +``` After Caddy is working, remove the temporary firewall rules: @@ -397,56 +521,175 @@ sudo ufw delete allow 3000/tcp sudo ufw delete allow 8001/tcp ``` -## Step 15: Gitea Actions Runner (TODO) +Update Gitea's configuration to use its new domain. In `~/gitea/docker-compose.yml`, change: + +```yaml +- GITEA__server__ROOT_URL=https://git.wizard.lu/ +- GITEA__server__SSH_DOMAIN=git.wizard.lu +- GITEA__server__DOMAIN=git.wizard.lu +``` + +Then restart Gitea: + +```bash +cd ~/gitea && docker compose up -d gitea +``` + +### Future: Multi-Tenant Store Routing + +Stores on each platform use two routing modes: + +- **Standard (subdomain)**: `acme.oms.lu` — included in the base subscription +- **Premium (custom domain)**: `acme.lu` — available with premium subscription tiers + +Both modes are handled by the `StoreContextMiddleware` which reads the `Host` header, so Caddy just needs to forward requests and preserve the header. + +#### Wildcard Subdomains (for store subdomains) + +When stores start using subdomains like `acme.oms.lu`, add wildcard blocks: + +```caddy +*.oms.lu { + reverse_proxy localhost:8001 +} + +*.loyaltyplus.lu { + reverse_proxy localhost:8001 +} + +*.wizard.lu { + reverse_proxy localhost:8001 +} +``` + +!!! warning "Wildcard SSL requires DNS challenge" + Let's Encrypt cannot issue wildcard certificates via HTTP challenge. Wildcard certs require a **DNS challenge**, which means installing a Caddy DNS provider plugin (e.g. `caddy-dns/cloudflare`) and configuring API credentials for your DNS provider. See [Caddy DNS challenge docs](https://caddyserver.com/docs/automatic-https#dns-challenge). + +#### Custom Store Domains (for premium stores) + +When premium stores bring their own domains (e.g. `acme.lu`), use Caddy's **on-demand TLS**: + +```caddy +https:// { + tls { + on_demand + } + reverse_proxy localhost:8001 +} +``` + +On-demand TLS auto-provisions SSL certificates when a new domain connects. Add an `ask` endpoint to validate that the domain is registered in the `store_domains` table, preventing abuse: + +```caddy +tls { + on_demand + ask http://localhost:8001/api/v1/internal/verify-domain +} +``` + +!!! note "Not needed yet" + Wildcard subdomains and custom domains are future work. The current Caddyfile handles all platform root domains and service subdomains. + +## Step 15: Gitea Actions Runner !!! warning "ARM64 architecture" This server is ARM64. Download the `arm64` binary, not `amd64`. +Download and install: + ```bash mkdir -p ~/gitea-runner && cd ~/gitea-runner -# Download act_runner (ARM64 version) -wget https://gitea.com/gitea/act_runner/releases/latest/download/act_runner-linux-arm64 -chmod +x act_runner-linux-arm64 - -# Register (get token from Gitea Site Administration > Runners) -./act_runner-linux-arm64 register \ - --instance http://localhost:3000 \ - --token YOUR_RUNNER_TOKEN - -# Start daemon -./act_runner-linux-arm64 daemon & +# Download act_runner v0.2.13 (ARM64) +wget https://gitea.com/gitea/act_runner/releases/download/v0.2.13/act_runner-0.2.13-linux-arm64 +chmod +x act_runner-0.2.13-linux-arm64 +ln -s act_runner-0.2.13-linux-arm64 act_runner ``` -## Step 16: Verify Full Deployment (TODO) +Register the runner (get token from **Site Administration > Actions > Runners > Create new Runner**): ```bash -# All containers running -docker compose --profile full ps +./act_runner register \ + --instance https://git.wizard.lu \ + --token YOUR_RUNNER_TOKEN +``` -# API health -curl http://localhost:8001/health +Accept the default runner name and labels when prompted. -# Caddy proxy with SSL +Create a systemd service for persistent operation: + +```bash +sudo nano /etc/systemd/system/gitea-runner.service +``` + +```ini +[Unit] +Description=Gitea Actions Runner +After=network.target + +[Service] +Type=simple +User=samir +WorkingDirectory=/home/samir/gitea-runner +ExecStart=/home/samir/gitea-runner/act_runner daemon +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +Enable and start: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now gitea-runner +sudo systemctl status gitea-runner +``` + +Verify the runner shows as **Online** in Gitea: **Site Administration > Actions > Runners**. + +## Step 16: Verify Full Deployment + +```bash +# All app containers running +cd ~/apps/orion && docker compose --profile full ps + +# API health (via Caddy with SSL) curl https://api.wizard.lu/health +# Main platform +curl -I https://wizard.lu + # Gitea -curl https://git.wizard.lu +curl -I https://git.wizard.lu + +# Flower +curl -I https://flower.wizard.lu + +# Gitea runner status +sudo systemctl status gitea-runner ``` --- -## Port Reference +## Domain & Port Reference -| Service | Internal | External | Domain (via Caddy) | +| Service | Internal Port | External Port | Domain (via Caddy) | |---|---|---|---| | Wizamart API | 8000 | 8001 | `api.wizard.lu` | +| Main Platform | 8000 | 8001 | `wizard.lu` | +| OMS Platform | 8000 | 8001 | `oms.lu` (TODO) | +| Loyalty+ Platform | 8000 | 8001 | `loyaltyplus.lu` (TODO) | | PostgreSQL | 5432 | 5432 | (internal only) | | Redis | 6379 | 6380 | (internal only) | | Flower | 5555 | 5555 | `flower.wizard.lu` | | Gitea | 3000 | 3000 | `git.wizard.lu` | | Caddy | — | 80, 443 | (reverse proxy) | +!!! note "Single backend, multiple domains" + All platform domains route to the same FastAPI backend. The `PlatformContextMiddleware` identifies the platform from the `Host` header. See [Multi-Platform Architecture](../architecture/multi-platform-cms.md) for details. + ## Directory Structure on Server ``` @@ -460,7 +703,10 @@ curl https://git.wizard.lu │ ├── logs/ # Application logs │ ├── uploads/ # User uploads │ └── exports/ # Export files -└── gitea-runner/ # (TODO) CI/CD runner +└── gitea-runner/ # CI/CD runner (act_runner v0.2.13) + ├── act_runner # symlink → act_runner-0.2.13-linux-arm64 + ├── act_runner-0.2.13-linux-arm64 + └── .runner # registration config ``` ## Troubleshooting @@ -528,13 +774,26 @@ docker compose --profile full up -d --build docker compose --profile full exec -e PYTHONPATH=/app api python -m alembic upgrade heads ``` -### Quick access URLs (current — no domain yet) +### Quick access URLs + +After Caddy is configured: | Service | URL | |---|---| -| API Swagger docs | `http://91.99.65.229:8001/docs` | -| API ReDoc | `http://91.99.65.229:8001/redoc` | -| Admin panel | `http://91.99.65.229:8001/admin/login` | -| Health check | `http://91.99.65.229:8001/health` | +| Main Platform | `https://wizard.lu` | +| API Swagger docs | `https://api.wizard.lu/docs` | +| API ReDoc | `https://api.wizard.lu/redoc` | +| Admin panel | `https://wizard.lu/admin/login` | +| Health check | `https://api.wizard.lu/health` | +| Gitea | `https://git.wizard.lu` | +| Flower | `https://flower.wizard.lu` | +| OMS Platform | `https://oms.lu` (after DNS) | +| Loyalty+ Platform | `https://loyaltyplus.lu` (after DNS) | + +Direct IP access (temporary, until firewall rules are removed): + +| Service | URL | +|---|---| +| API | `http://91.99.65.229:8001/docs` | | Gitea | `http://91.99.65.229:3000` | | Flower | `http://91.99.65.229:5555` | diff --git a/main.py b/main.py index 2bf7caaf..68a31d5a 100644 --- a/main.py +++ b/main.py @@ -26,7 +26,6 @@ from fastapi import Depends, FastAPI, HTTPException, Request, Response from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates from sentry_sdk.integrations.fastapi import FastApiIntegration from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration from sqlalchemy import text @@ -66,18 +65,17 @@ from app.modules.routes import ( get_admin_page_routes, get_merchant_page_routes, get_platform_page_routes, - get_storefront_page_routes, get_store_page_routes, + get_storefront_page_routes, ) -from app.utils.i18n import get_jinja2_globals from middleware.frontend_type import FrontendTypeMiddleware from middleware.language import LanguageMiddleware from middleware.logging import LoggingMiddleware -from middleware.theme_context import ThemeContextMiddleware # Import REFACTORED class-based middleware from middleware.platform_context import PlatformContextMiddleware from middleware.store_context import StoreContextMiddleware +from middleware.theme_context import ThemeContextMiddleware logger = logging.getLogger(__name__) @@ -442,8 +440,8 @@ async def store_root_path( if not store: raise HTTPException(status_code=404, detail=f"Store '{store_code}' not found") - from app.modules.core.utils.page_context import get_storefront_context from app.modules.cms.services import content_page_service + from app.modules.core.utils.page_context import get_storefront_context # Get platform_id (use platform from context or default to 1 for OMS) platform_id = platform.id if platform else 1 diff --git a/middleware/language.py b/middleware/language.py index 3d6d1816..71da2221 100644 --- a/middleware/language.py +++ b/middleware/language.py @@ -23,8 +23,8 @@ from app.utils.i18n import ( DEFAULT_LANGUAGE, SUPPORTED_LANGUAGES, parse_accept_language, - resolve_storefront_language, resolve_store_dashboard_language, + resolve_storefront_language, ) logger = logging.getLogger(__name__) diff --git a/middleware/platform_context.py b/middleware/platform_context.py index 411a30fe..d462e50f 100644 --- a/middleware/platform_context.py +++ b/middleware/platform_context.py @@ -23,7 +23,6 @@ from sqlalchemy.orm import Session from app.core.database import get_db from app.core.frontend_detector import FrontendDetector -from app.modules.enums import FrontendType from app.modules.tenancy.models import Platform # Note: We use pure ASGI middleware (not BaseHTTPMiddleware) to enable path rewriting diff --git a/middleware/store_context.py b/middleware/store_context.py index 8b218b65..715f0a3d 100644 --- a/middleware/store_context.py +++ b/middleware/store_context.py @@ -24,8 +24,7 @@ from starlette.middleware.base import BaseHTTPMiddleware from app.core.config import settings from app.core.database import get_db from app.core.frontend_detector import FrontendDetector -from app.modules.tenancy.models import Store -from app.modules.tenancy.models import StoreDomain +from app.modules.tenancy.models import Store, StoreDomain logger = logging.getLogger(__name__) diff --git a/pyproject.toml b/pyproject.toml index 1a118bc7..4d716f4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ exclude = [ "build", "dist", "alembic/versions", + "scripts/rename_terminology.py", ] [tool.ruff.lint] @@ -51,10 +52,24 @@ select = [ # Ignore specific rules ignore = [ + "E402", # module level import not at top — late imports used for circular import avoidance "E501", # line too long (handled by formatter) + "E711", # comparison to None — intentional in SQLAlchemy filters + "E712", # comparison to True/False — intentional in SQLAlchemy filters (e.g. col == True) + "E722", # bare except — TODO: fix incrementally "B008", # do not perform function calls in argument defaults (FastAPI Depends) + "B904", # raise from — TODO: fix incrementally "RET504", # unnecessary variable assignment before return "SIM102", # use a single if statement instead of nested if (sometimes less readable) + "SIM105", # use contextlib.suppress — less readable in some cases + "SIM108", # use ternary operator — less readable in some cases + "SIM117", # combine with statements — less readable in some cases + "N806", # variable in function should be lowercase — intentional for constants + "N817", # CamelCase imported as acronym — intentional shorthand + "N803", # argument name should be lowercase — required by external APIs (e.g. Apple Wallet) + "N818", # exception name should end with Error — existing convention + "F821", # undefined name — false positives on forward-reference type hints + "F822", # undefined name in __all__ — lazy-loaded module attributes ] # Allow autofix for all rules @@ -67,11 +82,14 @@ unfixable = [] "__init__.py" = ["F401", "F403"] # Base files that re-export (re-export Base from core.database) "models/database/base.py" = ["F401"] -# Ignore specific rules in test files -"tests/**/*.py" = ["S101", "PLR2004"] -"app/modules/*/tests/**/*.py" = ["S101", "PLR2004"] +# Test files: late imports, unused imports, naming, assertions +"tests/**/*.py" = ["S101", "PLR2004", "E402", "F401", "F821", "F822", "N806", "N817", "N818", "B007", "B015", "B017"] +"app/modules/*/tests/**/*.py" = ["S101", "PLR2004", "E402", "F401", "F821", "F822", "N806", "N817", "N818", "B007", "B015", "B017"] # Alembic migrations can have longer lines and specific patterns "alembic/versions/*.py" = ["E501", "F401"] +# Scripts: late imports, intentional import-for-side-effect checks, loop vars, syntax +"scripts/**/*.py" = ["E402", "F401", "B007", "B015", "B024", "B027"] +"scripts/rename_terminology.py" = ["ALL"] # Import sorting configuration (replaces isort) [tool.ruff.lint.isort] diff --git a/scripts/add_i18n_module_loading.py b/scripts/add_i18n_module_loading.py index 3d829575..8db5c524 100644 --- a/scripts/add_i18n_module_loading.py +++ b/scripts/add_i18n_module_loading.py @@ -6,7 +6,6 @@ This ensures module translations are loaded before use. import re from pathlib import Path -from collections import defaultdict PROJECT_ROOT = Path(__file__).parent.parent MODULES_DIR = PROJECT_ROOT / "app" / "modules" diff --git a/scripts/check_letzshop_shipment.py b/scripts/check_letzshop_shipment.py index a2533b24..ecb0b639 100644 --- a/scripts/check_letzshop_shipment.py +++ b/scripts/check_letzshop_shipment.py @@ -10,8 +10,9 @@ Example: python scripts/check_letzshop_shipment.py abc123 R532332163 """ -import sys import json +import sys + import requests ENDPOINT = "https://letzshop.lu/graphql" @@ -132,52 +133,48 @@ def search_shipment(api_key: str, search_term: str): order_number = order.get("number", "") # Check if this matches our search term - if (search_term in shipment_id or - search_term in shipment_number or - search_term in order_number or - search_term == shipment_id or - search_term == order_number): + if (search_term in (shipment_id, order_number) or search_term in shipment_id or search_term in shipment_number or search_term in order_number): print(f"\n{'=' * 60}") - print(f"FOUND SHIPMENT!") + print("FOUND SHIPMENT!") print(f"{'=' * 60}") - print(f"\n--- Shipment Info ---") + print("\n--- Shipment Info ---") print(f" ID: {shipment.get('id')}") print(f" Number: {shipment.get('number')}") print(f" State: {shipment.get('state')}") - print(f"\n--- Order Info ---") + print("\n--- Order Info ---") print(f" Order ID: {order.get('id')}") print(f" Order Number: {order.get('number')}") print(f" Email: {order.get('email')}") print(f" Total: {order.get('total')}") print(f" Completed At: {order.get('completedAt')}") - ship_addr = order.get('shipAddress', {}) + ship_addr = order.get("shipAddress", {}) if ship_addr: - print(f"\n--- Shipping Address ---") + print("\n--- Shipping Address ---") print(f" Name: {ship_addr.get('firstName')} {ship_addr.get('lastName')}") print(f" Street: {ship_addr.get('streetName')} {ship_addr.get('streetNumber')}") print(f" City: {ship_addr.get('zipCode')} {ship_addr.get('city')}") - country = ship_addr.get('country', {}) + country = ship_addr.get("country", {}) print(f" Country: {country.get('iso')}") - print(f"\n--- Inventory Units ---") - units = shipment.get('inventoryUnits', []) + print("\n--- Inventory Units ---") + units = shipment.get("inventoryUnits", []) for i, unit in enumerate(units, 1): print(f" Unit {i}:") print(f" ID: {unit.get('id')}") print(f" State: {unit.get('state')}") - variant = unit.get('variant', {}) + variant = unit.get("variant", {}) print(f" SKU: {variant.get('sku')}") - trade_id = variant.get('tradeId', {}) + trade_id = variant.get("tradeId", {}) print(f" GTIN: {trade_id.get('number')}") - product = variant.get('product', {}) - name = product.get('name', {}) + product = variant.get("product", {}) + name = product.get("name", {}) print(f" Product: {name.get('en')}") - print(f"\n--- Raw Response ---") + print("\n--- Raw Response ---") print(json.dumps(shipment, indent=2, default=str)) return shipment diff --git a/scripts/check_letzshop_tracking.py b/scripts/check_letzshop_tracking.py index 643a2aed..2b8eba12 100644 --- a/scripts/check_letzshop_tracking.py +++ b/scripts/check_letzshop_tracking.py @@ -9,8 +9,9 @@ Example: python scripts/check_letzshop_tracking.py abc123 nvDv5RQEmCwbjo """ -import sys import json +import sys + import requests ENDPOINT = "https://letzshop.lu/graphql" @@ -80,13 +81,13 @@ def get_tracking_info(api_key: str, target_shipment_id: str): if response.status_code != 200: print(f"Error: HTTP {response.status_code}") print(response.text) - return + return None data = response.json() if "errors" in data: print(f"GraphQL errors: {json.dumps(data['errors'], indent=2)}") - return + return None result = data.get("data", {}).get("shipments", {}) nodes = result.get("nodes", []) @@ -100,29 +101,29 @@ def get_tracking_info(api_key: str, target_shipment_id: str): print("FOUND SHIPMENT!") print(f"{'=' * 60}") - print(f"\n--- Shipment Info ---") + print("\n--- Shipment Info ---") print(f" ID: {shipment.get('id')}") print(f" Number: {shipment.get('number')}") print(f" State: {shipment.get('state')}") - print(f"\n--- Tracking Info ---") - tracking = shipment.get('tracking') + print("\n--- Tracking Info ---") + tracking = shipment.get("tracking") if tracking: print(f" Tracking Number: {tracking.get('number')}") print(f" Tracking URL: {tracking.get('url')}") - carrier = tracking.get('carrier', {}) + carrier = tracking.get("carrier", {}) if carrier: print(f" Carrier Name: {carrier.get('name')}") print(f" Carrier Code: {carrier.get('code')}") else: print(" No tracking object returned") - print(f"\n--- Order Info ---") - order = shipment.get('order', {}) + print("\n--- Order Info ---") + order = shipment.get("order", {}) print(f" Order Number: {order.get('number')}") print(f" Email: {order.get('email')}") - print(f"\n--- Raw Response ---") + print("\n--- Raw Response ---") print(json.dumps(shipment, indent=2, default=str)) return shipment diff --git a/scripts/check_tracking_schema.py b/scripts/check_tracking_schema.py index ec9b671b..b3255425 100644 --- a/scripts/check_tracking_schema.py +++ b/scripts/check_tracking_schema.py @@ -7,7 +7,7 @@ Usage: """ import sys -import json + import requests ENDPOINT = "https://letzshop.lu/graphql" @@ -177,7 +177,7 @@ def main(): node = result.get("data", {}).get("node", {}) tracking = node.get("tracking") if tracking: - print(f" Tracking via node query:") + print(" Tracking via node query:") print(f" Code: {tracking.get('code')}") print(f" Provider: {tracking.get('provider')}") else: diff --git a/scripts/create_dummy_letzshop_order.py b/scripts/create_dummy_letzshop_order.py index fd82a79c..2ea6010c 100755 --- a/scripts/create_dummy_letzshop_order.py +++ b/scripts/create_dummy_letzshop_order.py @@ -15,16 +15,15 @@ import argparse import random import string import sys -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path # Add project root to path sys.path.insert(0, str(Path(__file__).parent.parent)) from app.core.database import SessionLocal -from app.utils.money import cents_to_euros, euros_to_cents -from app.modules.orders.models import Order, OrderItem from app.modules.catalog.models import Product +from app.modules.orders.models import Order, OrderItem from app.modules.tenancy.models import Store @@ -41,7 +40,7 @@ def generate_shipment_number(): def generate_hash_id(): """Generate a realistic hash ID like nvDv5RQEmCwbjo.""" chars = string.ascii_letters + string.digits - return ''.join(random.choice(chars) for _ in range(14)) + return "".join(random.choice(chars) for _ in range(14)) def create_dummy_order( @@ -97,7 +96,7 @@ def create_dummy_order( order_number = generate_order_number() shipment_number = generate_shipment_number() hash_id = generate_hash_id() - order_date = datetime.now(timezone.utc) - timedelta(days=random.randint(0, 7)) + order_date = datetime.now(UTC) - timedelta(days=random.randint(0, 7)) # Customer data first_names = ["Jean", "Marie", "Pierre", "Sophie", "Michel", "Anne", "Thomas", "Claire"] @@ -180,7 +179,7 @@ def create_dummy_order( db.flush() # Create order items with prices in cents - for i, product in enumerate(products[:items_count]): + for _i, product in enumerate(products[:items_count]): quantity = random.randint(1, 3) unit_price_cents = product.price_cents or 0 product_name = product.get_title("en") or f"Product {product.id}" diff --git a/scripts/create_inventory.py b/scripts/create_inventory.py index f6cb9f4d..67925b47 100755 --- a/scripts/create_inventory.py +++ b/scripts/create_inventory.py @@ -33,7 +33,7 @@ cursor = conn.cursor() # Get products without inventory cursor.execute( """ - SELECT p.id, p.store_id, p.product_id + SELECT p.id, p.store_id, p.product_id FROM products p LEFT JOIN inventory i ON p.id = i.product_id WHERE i.id IS NULL @@ -49,7 +49,7 @@ if not products_without_inventory: print(f"📦 Creating inventory for {len(products_without_inventory)} products...") # Create inventory entries -for product_id, store_id, sku in products_without_inventory: +for product_id, store_id, _sku in products_without_inventory: cursor.execute( """ INSERT INTO inventory ( diff --git a/scripts/investigate_order.py b/scripts/investigate_order.py index 76ab787c..2faabfdb 100644 --- a/scripts/investigate_order.py +++ b/scripts/investigate_order.py @@ -2,6 +2,7 @@ """Debug script to investigate order shipping information.""" import sys + sys.path.insert(0, ".") from app.core.database import SessionLocal @@ -82,15 +83,15 @@ def investigate_order(order_number: str): print(f" Keys: {list(ext.keys())}") # Check for tracking info in external data - if 'tracking' in ext: + if "tracking" in ext: print(f" tracking: {ext['tracking']}") - if 'trackingNumber' in ext: + if "trackingNumber" in ext: print(f" trackingNumber: {ext['trackingNumber']}") - if 'carrier' in ext: + if "carrier" in ext: print(f" carrier: {ext['carrier']}") - if 'state' in ext: + if "state" in ext: print(f" state: {ext['state']}") - if 'shipmentState' in ext: + if "shipmentState" in ext: print(f" shipmentState: {ext['shipmentState']}") # Print full external data (truncated if too long) diff --git a/scripts/letzshop_introspect.py b/scripts/letzshop_introspect.py index 4028ae99..8a55fbdd 100644 --- a/scripts/letzshop_introspect.py +++ b/scripts/letzshop_introspect.py @@ -9,9 +9,10 @@ Usage: python scripts/letzshop_introspect.py YOUR_API_KEY """ -import sys -import requests import json +import sys + +import requests ENDPOINT = "https://letzshop.lu/graphql" @@ -227,10 +228,9 @@ def format_type(type_info: dict) -> str: if kind == "NON_NULL": return f"{format_type(of_type)}!" - elif kind == "LIST": + if kind == "LIST": return f"[{format_type(of_type)}]" - else: - return name or kind + return name or kind def print_fields(type_data: dict, highlight_terms: list[str] = None): @@ -490,7 +490,7 @@ def test_shipment_query(api_key: str, state: str = "unconfirmed"): result = run_query(api_key, query) if "errors" in result: - print(f"\n❌ QUERY FAILED!") + print("\n❌ QUERY FAILED!") print(f"Errors: {json.dumps(result['errors'], indent=2)}") return False @@ -503,7 +503,7 @@ def test_shipment_query(api_key: str, state: str = "unconfirmed"): order = shipment.get("order", {}) units = shipment.get("inventoryUnits", []) - print(f"\nExample shipment:") + print("\nExample shipment:") print(f" Shipment #: {shipment.get('number')}") print(f" Order #: {order.get('number')}") print(f" Customer: {order.get('email')}") diff --git a/scripts/migrate_js_i18n.py b/scripts/migrate_js_i18n.py index 8ee4d1d4..60caef75 100644 --- a/scripts/migrate_js_i18n.py +++ b/scripts/migrate_js_i18n.py @@ -6,8 +6,8 @@ Extracts messages, creates translation keys, and updates both JS and locale file import json import re -from pathlib import Path from collections import defaultdict +from pathlib import Path PROJECT_ROOT = Path(__file__).parent.parent MODULES_DIR = PROJECT_ROOT / "app" / "modules" @@ -42,11 +42,11 @@ def message_to_key(message: str) -> str: """Convert a message string to a translation key.""" # Remove special characters and convert to snake_case key = message.lower() - key = re.sub(r'[^\w\s]', '', key) - key = re.sub(r'\s+', '_', key) + key = re.sub(r"[^\w\s]", "", key) + key = re.sub(r"\s+", "_", key) # Truncate if too long if len(key) > 40: - key = key[:40].rstrip('_') + key = key[:40].rstrip("_") return key @@ -120,7 +120,7 @@ def process_module(module_name: str, js_files: list[Path]) -> dict[str, str]: # Extract all unique messages for js_file in js_files: messages = extract_messages_from_file(js_file) - for message, msg_type in messages: + for message, _msg_type in messages: if message not in all_messages: all_messages[message] = message_to_key(message) @@ -158,7 +158,7 @@ def main(): print(f" {key}: {msg[:50]}{'...' if len(msg) > 50 else ''}") # Update locale files for all languages - print(f"\n Updating locale files...") + print("\n Updating locale files...") for lang in LANGUAGES: locale_data = load_locale_file(module_path, lang) @@ -177,7 +177,7 @@ def main(): print(f" Updated: {lang}.json") # Update JS files - print(f"\n Updating JS files...") + print("\n Updating JS files...") for js_file in js_files: if update_js_file(js_file, module_name, message_keys): rel_path = js_file.relative_to(PROJECT_ROOT) diff --git a/scripts/module_dependency_graph.py b/scripts/module_dependency_graph.py index 2326359b..710c25b5 100755 --- a/scripts/module_dependency_graph.py +++ b/scripts/module_dependency_graph.py @@ -20,8 +20,8 @@ import json import re import sys from collections import defaultdict -from pathlib import Path from dataclasses import dataclass, field +from pathlib import Path @dataclass @@ -136,7 +136,7 @@ class ModuleDependencyAnalyzer: in_type_checking = False type_checking_indent = 0 - for i, line in enumerate(lines, 1): + for _i, line in enumerate(lines, 1): stripped = line.strip() if stripped.startswith("#") or not stripped: continue @@ -167,7 +167,7 @@ class ModuleDependencyAnalyzer: # Look for import statements import_match = re.match( - r'^\s*(?:from\s+(app\.modules\.(\w+))|import\s+(app\.modules\.(\w+)))', + r"^\s*(?:from\s+(app\.modules\.(\w+))|import\s+(app\.modules\.(\w+)))", line ) @@ -291,13 +291,13 @@ def format_mermaid(analyzer: ModuleDependencyAnalyzer) -> str: lines = [] lines.append("```mermaid") lines.append("flowchart TD") - lines.append(" subgraph core[\"Core Modules\"]") + lines.append(' subgraph core["Core Modules"]') for module in sorted(analyzer.CORE_MODULES): if module in analyzer.modules: lines.append(f" {module}[{module}]") lines.append(" end") lines.append("") - lines.append(" subgraph optional[\"Optional Modules\"]") + lines.append(' subgraph optional["Optional Modules"]') for module in sorted(analyzer.OPTIONAL_MODULES): if module in analyzer.modules: lines.append(f" {module}[{module}]") @@ -306,7 +306,7 @@ def format_mermaid(analyzer: ModuleDependencyAnalyzer) -> str: # Add edges for source, deps in analyzer.dependencies.items(): - for target, dep in deps.items(): + for target, _dep in deps.items(): # Style violations differently source_is_core = source in analyzer.CORE_MODULES target_is_optional = target in analyzer.OPTIONAL_MODULES @@ -323,43 +323,43 @@ def format_dot(analyzer: ModuleDependencyAnalyzer) -> str: """Format output as GraphViz DOT.""" lines = [] lines.append("digraph ModuleDependencies {") - lines.append(' rankdir=LR;') - lines.append(' node [shape=box];') - lines.append('') + lines.append(" rankdir=LR;") + lines.append(" node [shape=box];") + lines.append("") # Core modules cluster - lines.append(' subgraph cluster_core {') + lines.append(" subgraph cluster_core {") lines.append(' label="Core Modules";') - lines.append(' style=filled;') - lines.append(' color=lightblue;') + lines.append(" style=filled;") + lines.append(" color=lightblue;") for module in sorted(analyzer.CORE_MODULES): if module in analyzer.modules: - lines.append(f' {module};') - lines.append(' }') - lines.append('') + lines.append(f" {module};") + lines.append(" }") + lines.append("") # Optional modules cluster - lines.append(' subgraph cluster_optional {') + lines.append(" subgraph cluster_optional {") lines.append(' label="Optional Modules";') - lines.append(' style=filled;') - lines.append(' color=lightyellow;') + lines.append(" style=filled;") + lines.append(" color=lightyellow;") for module in sorted(analyzer.OPTIONAL_MODULES): if module in analyzer.modules: - lines.append(f' {module};') - lines.append(' }') - lines.append('') + lines.append(f" {module};") + lines.append(" }") + lines.append("") # Edges for source, deps in analyzer.dependencies.items(): - for target, dep in deps.items(): + for target, _dep in deps.items(): source_is_core = source in analyzer.CORE_MODULES target_is_optional = target in analyzer.OPTIONAL_MODULES if source_is_core and target_is_optional: lines.append(f' {source} -> {target} [color=red, style=dashed, label="violation"];') else: - lines.append(f' {source} -> {target};') + lines.append(f" {source} -> {target};") - lines.append('}') + lines.append("}") return "\n".join(lines) diff --git a/scripts/seed/create_default_content_pages.py b/scripts/seed/create_default_content_pages.py index a6815a51..62faf528 100755 --- a/scripts/seed/create_default_content_pages.py +++ b/scripts/seed/create_default_content_pages.py @@ -33,6 +33,8 @@ from pathlib import Path project_root = Path(__file__).parent.parent sys.path.insert(0, str(project_root)) +import contextlib + from sqlalchemy import select from sqlalchemy.orm import Session @@ -49,10 +51,8 @@ for _mod in [ "app.modules.marketplace.models", "app.modules.cms.models", ]: - try: + with contextlib.suppress(ImportError): __import__(_mod) - except ImportError: - pass from app.core.database import SessionLocal from app.modules.cms.models import ContentPage @@ -505,7 +505,7 @@ def create_default_pages(db: Session) -> None: # Check if page already exists (platform default with this slug) existing = db.execute( select(ContentPage).where( - ContentPage.store_id == None, ContentPage.slug == page_data["slug"] + ContentPage.store_id is None, ContentPage.slug == page_data["slug"] ) ).scalar_one_or_none() diff --git a/scripts/seed/create_platform_pages.py b/scripts/seed/create_platform_pages.py index a5e3b26a..04abf0c5 100755 --- a/scripts/seed/create_platform_pages.py +++ b/scripts/seed/create_platform_pages.py @@ -16,6 +16,7 @@ Usage: python scripts/create_platform_pages.py """ +import contextlib import sys from pathlib import Path @@ -36,10 +37,8 @@ for _mod in [ "app.modules.marketplace.models", "app.modules.cms.models", ]: - try: + with contextlib.suppress(ImportError): __import__(_mod) - except ImportError: - pass from sqlalchemy import select diff --git a/scripts/seed/init_log_settings.py b/scripts/seed/init_log_settings.py index 8edfbb1e..2c10a7b8 100644 --- a/scripts/seed/init_log_settings.py +++ b/scripts/seed/init_log_settings.py @@ -6,6 +6,8 @@ Run this script to create default logging configuration settings. """ # Register all models with SQLAlchemy so string-based relationships resolve +import contextlib + for _mod in [ "app.modules.billing.models", "app.modules.inventory.models", @@ -18,10 +20,8 @@ for _mod in [ "app.modules.marketplace.models", "app.modules.cms.models", ]: - try: + with contextlib.suppress(ImportError): __import__(_mod) - except ImportError: - pass from app.core.database import SessionLocal from app.modules.tenancy.models import AdminSetting diff --git a/scripts/seed/init_production.py b/scripts/seed/init_production.py index 2d1b6379..b9201d71 100644 --- a/scripts/seed/init_production.py +++ b/scripts/seed/init_production.py @@ -24,6 +24,8 @@ from pathlib import Path project_root = Path(__file__).parent.parent sys.path.insert(0, str(project_root)) +import contextlib + from sqlalchemy import select from sqlalchemy.orm import Session @@ -34,13 +36,12 @@ from app.core.config import ( ) from app.core.database import SessionLocal from app.core.environment import is_production +from app.modules.billing.models.subscription import SubscriptionTier +from app.modules.tenancy.models import AdminSetting, Platform, User from app.modules.tenancy.services.permission_discovery_service import ( permission_discovery_service, ) from middleware.auth import AuthManager -from app.modules.tenancy.models import AdminSetting, Platform -from app.modules.tenancy.models import User -from app.modules.billing.models.subscription import SubscriptionTier # Register all models with SQLAlchemy so string-based relationships resolve for _mod in [ @@ -55,10 +56,8 @@ for _mod in [ "app.modules.marketplace.models", "app.modules.cms.models", ]: - try: + with contextlib.suppress(ImportError): __import__(_mod) - except ImportError: - pass # ============================================================================= # HELPER FUNCTIONS @@ -510,7 +509,7 @@ def initialize_production(db: Session, auth_manager: AuthManager): # Step 2: Create admin user print_step(2, "Creating platform admin...") - admin = create_admin_user(db, auth_manager) + create_admin_user(db, auth_manager) # Step 3: Create default platforms print_step(3, "Creating default platforms...") @@ -518,7 +517,7 @@ def initialize_production(db: Session, auth_manager: AuthManager): # Step 4: Set up default role templates print_step(4, "Setting up role templates...") - role_templates = create_default_role_templates(db) + create_default_role_templates(db) # Step 5: Create admin settings print_step(5, "Creating admin settings...") diff --git a/scripts/seed/install.py b/scripts/seed/install.py index 5abceb81..2e7a3dd2 100755 --- a/scripts/seed/install.py +++ b/scripts/seed/install.py @@ -18,7 +18,6 @@ Usage: This script is idempotent - safe to run multiple times. """ -import os import subprocess import sys from pathlib import Path @@ -107,9 +106,8 @@ def check_env_file() -> tuple[bool, dict]: print_info("Copy .env.example to .env and configure it:") print_info(" cp .env.example .env") return False, {} - else: - print_warning("Neither .env nor .env.example found") - return False, {} + print_warning("Neither .env nor .env.example found") + return False, {} print_success(".env file found") @@ -451,11 +449,10 @@ def run_migrations() -> bool: if result.returncode == 0: print_success("Migrations completed successfully") return True - else: - print_error("Migration failed") - if result.stderr: - print_info(result.stderr[:500]) - return False + print_error("Migration failed") + if result.stderr: + print_info(result.stderr[:500]) + return False except Exception as e: print_error(f"Failed to run migrations: {e}") return False @@ -474,11 +471,10 @@ def run_init_script(script_name: str, description: str) -> bool: if result.returncode == 0: print_success(description) return True - else: - print_error(f"Failed: {description}") - if result.stderr: - print_info(result.stderr[:300]) - return False + print_error(f"Failed: {description}") + if result.stderr: + print_info(result.stderr[:300]) + return False except Exception as e: print_error(f"Error running {script_name}: {e}") return False @@ -578,7 +574,7 @@ def main(): print(f"\n {Colors.BOLD}Admin Login:{Colors.ENDC}") admin_email = env_vars.get("ADMIN_EMAIL", "admin@wizamart.com") - print(f" URL: /admin/login") + print(" URL: /admin/login") print(f" Email: {admin_email}") print(f" Password: {'(configured in .env)' if env_vars.get('ADMIN_PASSWORD') else 'admin123'}") diff --git a/scripts/seed/seed_demo.py b/scripts/seed/seed_demo.py index 75b3c1b2..a43912ed 100644 --- a/scripts/seed/seed_demo.py +++ b/scripts/seed/seed_demo.py @@ -42,6 +42,7 @@ sys.path.insert(0, str(project_root)) # ============================================================================= # MODE DETECTION (from environment variable set by Makefile) # ============================================================================= +import contextlib import os from sqlalchemy import delete, select @@ -50,25 +51,33 @@ from sqlalchemy.orm import Session from app.core.config import settings from app.core.database import SessionLocal from app.core.environment import get_environment, is_production -from middleware.auth import AuthManager +from app.modules.catalog.models import Product +from app.modules.cms.models import ContentPage, StoreTheme +from app.modules.customers.models.customer import Customer, CustomerAddress +from app.modules.marketplace.models import ( + MarketplaceImportJob, + MarketplaceProduct, + MarketplaceProductTranslation, +) +from app.modules.orders.models import Order, OrderItem + # ============================================================================= # MODEL IMPORTS # ============================================================================= # ALL models must be imported before any ORM query so SQLAlchemy can resolve # cross-module string relationships (e.g. Store→StoreEmailTemplate, # Platform→SubscriptionTier, Product→Inventory). - # Core modules -from app.modules.tenancy.models import Merchant, PlatformAlert, User, Role, Store, StoreUser, StoreDomain -from app.modules.cms.models import ContentPage, StoreTheme -from app.modules.catalog.models import Product -from app.modules.customers.models.customer import Customer, CustomerAddress -from app.modules.orders.models import Order, OrderItem -from app.modules.marketplace.models import ( - MarketplaceImportJob, - MarketplaceProduct, - MarketplaceProductTranslation, +from app.modules.tenancy.models import ( + Merchant, + PlatformAlert, + Role, + Store, + StoreDomain, + StoreUser, + User, ) +from middleware.auth import AuthManager # Optional modules — import to register models with SQLAlchemy for _mod in [ @@ -78,10 +87,8 @@ for _mod in [ "app.modules.messaging.models", "app.modules.loyalty.models", ]: - try: + with contextlib.suppress(ImportError): __import__(_mod) - except ImportError: - pass SEED_MODE = os.getenv("SEED_MODE", "normal") # normal, minimal, reset FORCE_RESET = os.getenv("FORCE_RESET", "false").lower() in ("true", "1", "yes") @@ -562,7 +569,7 @@ def reset_all_data(db: Session): for table in tables_to_clear: if table == ContentPage: # Only delete store content pages, keep platform defaults - db.execute(delete(ContentPage).where(ContentPage.store_id != None)) + db.execute(delete(ContentPage).where(ContentPage.store_id is not None)) else: db.execute(delete(table)) @@ -1104,8 +1111,8 @@ def print_summary(db: Session): team_member_count = db.query(StoreUser).filter(StoreUser.user_type == "member").count() customer_count = db.query(Customer).count() product_count = db.query(Product).count() - platform_pages = db.query(ContentPage).filter(ContentPage.store_id == None).count() - store_pages = db.query(ContentPage).filter(ContentPage.store_id != None).count() + platform_pages = db.query(ContentPage).filter(ContentPage.store_id is None).count() + store_pages = db.query(ContentPage).filter(ContentPage.store_id is not None).count() print("\n📊 Database Status:") print(f" Merchants: {merchant_count}") diff --git a/scripts/seed/seed_email_templates.py b/scripts/seed/seed_email_templates.py index ad1f6306..70fee8c9 100644 --- a/scripts/seed/seed_email_templates.py +++ b/scripts/seed/seed_email_templates.py @@ -5,6 +5,7 @@ Seed default email templates. Run: python scripts/seed_email_templates.py """ +import contextlib import json import sys from pathlib import Path @@ -25,15 +26,12 @@ for _mod in [ "app.modules.marketplace.models", "app.modules.cms.models", ]: - try: + with contextlib.suppress(ImportError): __import__(_mod) - except ImportError: - pass from app.core.database import get_db from app.modules.messaging.models import EmailCategory, EmailTemplate - # ============================================================================= # EMAIL TEMPLATES # ============================================================================= diff --git a/scripts/show_structure.py b/scripts/show_structure.py index 2246c374..a41ce93e 100644 --- a/scripts/show_structure.py +++ b/scripts/show_structure.py @@ -23,7 +23,7 @@ def count_files(directory: str, pattern: str) -> int: return 0 count = 0 - for root, dirs, files in os.walk(directory): + for _root, dirs, files in os.walk(directory): # Skip __pycache__ and other cache directories dirs[:] = [ d diff --git a/scripts/show_urls.py b/scripts/show_urls.py index 5f328c70..9c675f93 100644 --- a/scripts/show_urls.py +++ b/scripts/show_urls.py @@ -21,7 +21,6 @@ from sqlalchemy import text from app.core.config import settings from app.core.database import SessionLocal - DEV_BASE = "http://localhost:9999" SEPARATOR = "─" * 72 diff --git a/scripts/squash_migrations.py b/scripts/squash_migrations.py index 165748d2..e828da40 100644 --- a/scripts/squash_migrations.py +++ b/scripts/squash_migrations.py @@ -100,10 +100,9 @@ def backup_migrations(): if total_backed_up > 0: print(f"Backed up {total_backed_up} migrations to {backup_dir.name}/") return backup_dir - else: - print("No migration files found to backup") - backup_dir.rmdir() - return None + print("No migration files found to backup") + backup_dir.rmdir() + return None def create_fresh_migration(): @@ -180,7 +179,7 @@ def main(): # Confirm with user response = input("This will backup and replace all migrations. Continue? [y/N] ") - if response.lower() != 'y': + if response.lower() != "y": print("Aborted") sys.exit(0) diff --git a/scripts/test_historical_import.py b/scripts/test_historical_import.py index fb8a582c..6db89da0 100644 --- a/scripts/test_historical_import.py +++ b/scripts/test_historical_import.py @@ -461,10 +461,9 @@ def test_query(api_key: str, query_name: str, query: str, page_size: int = 5, sh else: print("OK - no node returned") return True - else: - shipments = data.get("data", {}).get("shipments", {}).get("nodes", []) - print(f"OK - {len(shipments)} shipments") - return True + shipments = data.get("data", {}).get("shipments", {}).get("nodes", []) + print(f"OK - {len(shipments)} shipments") + return True except Exception as e: print(f"ERROR - {e}") @@ -570,7 +569,7 @@ def main(): failing = [name for name, success in results.items() if success is False] if failing: - print(f"\n⚠️ Problem detected!") + print("\n⚠️ Problem detected!") print(f" Working queries: {', '.join(working) if working else 'none'}") print(f" Failing queries: {', '.join(failing)}") @@ -664,11 +663,11 @@ def main(): print(f"Total items: {total_items}") print(f"Unique EANs: {len(eans)}") - print(f"\nOrders by locale:") + print("\nOrders by locale:") for locale, count in sorted(locales.items(), key=lambda x: -x[1]): print(f" {locale}: {count}") - print(f"\nOrders by country:") + print("\nOrders by country:") for country, count in sorted(countries.items(), key=lambda x: -x[1]): print(f" {country}: {count}") diff --git a/scripts/test_logging_system.py b/scripts/test_logging_system.py index ff7e8bfb..7be505d4 100644 --- a/scripts/test_logging_system.py +++ b/scripts/test_logging_system.py @@ -90,7 +90,9 @@ def test_logging_endpoints(): print("\n[4] Testing log settings...") try: from app.core.database import SessionLocal - from app.modules.core.services.admin_settings_service import admin_settings_service + from app.modules.core.services.admin_settings_service import ( + admin_settings_service, + ) db = SessionLocal() try: diff --git a/scripts/test_store_management.py b/scripts/test_store_management.py index 3007f767..60f69e94 100644 --- a/scripts/test_store_management.py +++ b/scripts/test_store_management.py @@ -240,7 +240,7 @@ def main(): store_id_1 = test_create_store_with_both_emails() # Test 2: Create with single email - store_id_2 = test_create_store_single_email() + test_create_store_single_email() if store_id_1: # Test 3: Update contact email diff --git a/scripts/validate/base_validator.py b/scripts/validate/base_validator.py index 88e2bbcb..139fe60b 100755 --- a/scripts/validate/base_validator.py +++ b/scripts/validate/base_validator.py @@ -4,7 +4,7 @@ Base Validator Class Shared functionality for all validators. """ -from abc import ABC, abstractmethod +from abc import ABC from dataclasses import dataclass, field from enum import Enum from pathlib import Path @@ -100,7 +100,7 @@ class BaseValidator(ABC): Subclasses should implement validate_all() instead. """ result = self.validate_all() - return not result.has_errors() if hasattr(result, 'has_errors') else True + return not result.has_errors() if hasattr(result, "has_errors") else True def validate_all(self, target_path: Path | None = None) -> ValidationResult: """Run all validations. Override in subclasses.""" @@ -178,10 +178,7 @@ class BaseValidator(ABC): def _should_ignore_file(self, file_path: Path) -> bool: """Check if a file should be ignored based on patterns.""" path_str = str(file_path) - for pattern in self.IGNORE_PATTERNS: - if pattern in path_str: - return True - return False + return any(pattern in path_str for pattern in self.IGNORE_PATTERNS) def _add_violation( self, @@ -224,7 +221,6 @@ class BaseValidator(ABC): def _validate_file_content(self, file_path: Path, content: str, lines: list[str]): """Validate file content. Override in subclasses.""" - pass def output_results(self, json_output: bool = False, errors_only: bool = False) -> None: """Output validation results.""" diff --git a/scripts/validate/validate_all.py b/scripts/validate/validate_all.py index 7356a6fa..318e40a3 100755 --- a/scripts/validate/validate_all.py +++ b/scripts/validate/validate_all.py @@ -129,10 +129,10 @@ def run_audit_validator(verbose: bool = False) -> tuple[int, dict]: 1 if has_errors else 0, { "name": "Audit", - "files_checked": len(validator.files_checked) if hasattr(validator, 'files_checked') else 0, + "files_checked": len(validator.files_checked) if hasattr(validator, "files_checked") else 0, "errors": len(validator.errors), "warnings": len(validator.warnings), - "info": len(validator.info) if hasattr(validator, 'info') else 0, + "info": len(validator.info) if hasattr(validator, "info") else 0, } ) except ImportError as e: diff --git a/scripts/validate/validate_architecture.py b/scripts/validate/validate_architecture.py index 7f23c332..80953b77 100755 --- a/scripts/validate/validate_architecture.py +++ b/scripts/validate/validate_architecture.py @@ -484,7 +484,7 @@ class ArchitectureValidator: stripped = line.strip() # Skip comments - if stripped.startswith("//") or stripped.startswith("/*"): + if stripped.startswith(("//", "/*")): continue # Skip lines with inline noqa comment @@ -583,7 +583,7 @@ class ArchitectureValidator: if re.search(r"\bfetch\s*\(", line): # Skip if it's a comment stripped = line.strip() - if stripped.startswith("//") or stripped.startswith("/*"): + if stripped.startswith(("//", "/*")): continue # Check if it's calling an API endpoint (contains /api/) @@ -757,10 +757,6 @@ class ArchitectureValidator: has_loading_state = ( "loading:" in component_region or "loading :" in component_region ) - has_loading_assignment = ( - "this.loading = " in component_region - or "loading = true" in component_region - ) if has_api_calls and not has_loading_state: line_num = content[:func_start].count("\n") + 1 @@ -1119,7 +1115,6 @@ class ArchitectureValidator: return # Valid admin template blocks - valid_blocks = {"title", "extra_head", "alpine_data", "content", "extra_scripts"} # Common invalid block names that developers might mistakenly use invalid_blocks = { @@ -1200,7 +1195,6 @@ class ArchitectureValidator: # Track multi-line copyCode template literals with double-quoted outer attribute in_copycode_template = False - copycode_start_line = 0 for i, line in enumerate(lines, 1): if "noqa: tpl-012" in line.lower(): @@ -1208,9 +1202,8 @@ class ArchitectureValidator: # Check for start of copyCode with double-quoted attribute and template literal # Pattern: @click="copyCode(` where the backtick doesn't close on same line - if '@click="copyCode(`' in line and '`)' not in line: + if '@click="copyCode(`' in line and "`)" not in line: in_copycode_template = True - copycode_start_line = i continue # Check for end of copyCode template (backtick followed by )" or )') @@ -1272,7 +1265,7 @@ class ArchitectureValidator: line_number=i, message=f"Old pagination API with '{param_name}' parameter", context=line.strip()[:80], - suggestion="Use: {{ pagination(show_condition=\"!loading && pagination.total > 0\") }}", + suggestion='Use: {{ pagination(show_condition="!loading && pagination.total > 0") }}', ) break # Only report once per line @@ -1433,7 +1426,7 @@ class ArchitectureValidator: if pattern in line: # Skip if it's in a comment stripped = line.strip() - if stripped.startswith("{#") or stripped.startswith("