refactor(migrations): squash 75 migrations into 12 per-module initial migrations
The old migration chain was broken (downgrade path through vendor->merchant rename made rollbacks impossible). This squashes everything into fresh per-module migrations with zero schema drift, verified by autogenerate. Changes: - Replace 75 accumulated migrations with 12 per-module initial migrations (core, billing, catalog, marketplace, cms, customers, orders, inventory, cart, messaging, loyalty, dev_tools) in a linear chain - Fix make db-reset to use SQL DROP SCHEMA instead of alembic downgrade base - Enable migration autodiscovery for all modules (migrations_path in definitions) - Rewrite alembic/env.py to import all 75 model tables across 13 modules - Fix AdminNotification import (was incorrectly from tenancy, now from messaging) - Update squash_migrations.py to handle all module migration directories Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -233,6 +233,7 @@ billing_module = ModuleDefinition(
|
||||
schemas_path="app.modules.billing.schemas",
|
||||
exceptions_path="app.modules.billing.exceptions",
|
||||
tasks_path="app.modules.billing.tasks",
|
||||
migrations_path="migrations",
|
||||
# =========================================================================
|
||||
# Scheduled Tasks
|
||||
# =========================================================================
|
||||
|
||||
233
app/modules/billing/migrations/versions/billing_001_initial.py
Normal file
233
app/modules/billing/migrations/versions/billing_001_initial.py
Normal file
@@ -0,0 +1,233 @@
|
||||
"""billing initial - subscription tiers, addons, billing history
|
||||
|
||||
Revision ID: billing_001
|
||||
Revises: core_001
|
||||
Create Date: 2026-02-07
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "billing_001"
|
||||
down_revision = "core_001"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# --- subscription_tiers ---
|
||||
op.create_table(
|
||||
"subscription_tiers",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("platform_id", sa.Integer(), sa.ForeignKey("platforms.id", ondelete="CASCADE"), nullable=True, index=True, comment="Platform this tier belongs to (NULL = global tier)"),
|
||||
sa.Column("code", sa.String(30), nullable=False, index=True),
|
||||
sa.Column("name", sa.String(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("stripe_product_id", sa.String(100), nullable=True),
|
||||
sa.Column("stripe_price_monthly_id", sa.String(100), nullable=True),
|
||||
sa.Column("stripe_price_annual_id", sa.String(100), nullable=True),
|
||||
sa.Column("display_order", sa.Integer(), nullable=True),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("is_public", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_tier_platform_active", "subscription_tiers", ["platform_id", "is_active"])
|
||||
|
||||
# --- store_platforms (depends on subscription_tiers) ---
|
||||
op.create_table(
|
||||
"store_platforms",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id", ondelete="CASCADE"), nullable=False, index=True, comment="Reference to the store"),
|
||||
sa.Column("platform_id", sa.Integer(), sa.ForeignKey("platforms.id", ondelete="CASCADE"), nullable=False, index=True, comment="Reference to the platform"),
|
||||
sa.Column("tier_id", sa.Integer(), sa.ForeignKey("subscription_tiers.id", ondelete="SET NULL"), nullable=True, index=True, comment="Platform-specific subscription tier"),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true", comment="Whether the store is active on this platform"),
|
||||
sa.Column("is_primary", sa.Boolean(), nullable=False, server_default="false", comment="Whether this is the store's primary platform"),
|
||||
sa.Column("custom_subdomain", sa.String(100), nullable=True, comment="Platform-specific subdomain (if different from main subdomain)"),
|
||||
sa.Column("settings", sa.JSON(), nullable=True, server_default="{}", comment="Platform-specific store settings"),
|
||||
sa.Column("joined_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False, comment="When the store joined this platform"),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.UniqueConstraint("store_id", "platform_id", name="uq_store_platform"),
|
||||
)
|
||||
op.create_index("idx_store_platform_active", "store_platforms", ["store_id", "platform_id", "is_active"])
|
||||
op.create_index("idx_store_platform_primary", "store_platforms", ["store_id", "is_primary"])
|
||||
|
||||
# --- tier_feature_limits ---
|
||||
op.create_table(
|
||||
"tier_feature_limits",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("tier_id", sa.Integer(), sa.ForeignKey("subscription_tiers.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("feature_code", sa.String(80), nullable=False, index=True),
|
||||
sa.Column("limit_value", sa.Integer(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.UniqueConstraint("tier_id", "feature_code", name="uq_tier_feature_code"),
|
||||
)
|
||||
op.create_index("idx_tier_feature_lookup", "tier_feature_limits", ["tier_id", "feature_code"])
|
||||
|
||||
# --- addon_products ---
|
||||
op.create_table(
|
||||
"addon_products",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("code", sa.String(50), unique=True, nullable=False, index=True),
|
||||
sa.Column("name", sa.String(100), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("category", sa.String(50), nullable=False, index=True),
|
||||
sa.Column("price_cents", sa.Integer(), nullable=False),
|
||||
sa.Column("billing_period", sa.String(20), nullable=False, server_default="monthly"),
|
||||
sa.Column("quantity_unit", sa.String(50), nullable=True),
|
||||
sa.Column("quantity_value", sa.Integer(), nullable=True),
|
||||
sa.Column("stripe_product_id", sa.String(100), nullable=True),
|
||||
sa.Column("stripe_price_id", sa.String(100), nullable=True),
|
||||
sa.Column("display_order", sa.Integer(), nullable=True),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
|
||||
# --- store_addons ---
|
||||
op.create_table(
|
||||
"store_addons",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=False, index=True),
|
||||
sa.Column("addon_product_id", sa.Integer(), sa.ForeignKey("addon_products.id"), nullable=False, index=True),
|
||||
sa.Column("status", sa.String(20), nullable=False, server_default="active", index=True),
|
||||
sa.Column("domain_name", sa.String(255), nullable=True, index=True),
|
||||
sa.Column("quantity", sa.Integer(), nullable=False, server_default="1"),
|
||||
sa.Column("stripe_subscription_item_id", sa.String(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(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_store_addon_status", "store_addons", ["store_id", "status"])
|
||||
op.create_index("idx_store_addon_product", "store_addons", ["store_id", "addon_product_id"])
|
||||
|
||||
# --- merchant_subscriptions ---
|
||||
op.create_table(
|
||||
"merchant_subscriptions",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("merchant_id", sa.Integer(), sa.ForeignKey("merchants.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("platform_id", sa.Integer(), sa.ForeignKey("platforms.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("tier_id", sa.Integer(), sa.ForeignKey("subscription_tiers.id", ondelete="SET NULL"), nullable=True, index=True),
|
||||
sa.Column("status", sa.String(20), nullable=False, server_default="trial", index=True),
|
||||
sa.Column("is_annual", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("period_start", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("period_end", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("trial_ends_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("stripe_customer_id", sa.String(100), nullable=True, index=True),
|
||||
sa.Column("stripe_subscription_id", sa.String(100), nullable=True, index=True),
|
||||
sa.Column("stripe_payment_method_id", sa.String(100), nullable=True),
|
||||
sa.Column("payment_retry_count", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("last_payment_error", sa.Text(), nullable=True),
|
||||
sa.Column("cancelled_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("cancellation_reason", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.UniqueConstraint("merchant_id", "platform_id", name="uq_merchant_platform_subscription"),
|
||||
)
|
||||
op.create_index("idx_merchant_sub_status", "merchant_subscriptions", ["merchant_id", "status"])
|
||||
op.create_index("idx_merchant_sub_platform", "merchant_subscriptions", ["platform_id", "status"])
|
||||
|
||||
# --- merchant_feature_overrides ---
|
||||
op.create_table(
|
||||
"merchant_feature_overrides",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("merchant_id", sa.Integer(), sa.ForeignKey("merchants.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("platform_id", sa.Integer(), sa.ForeignKey("platforms.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("feature_code", sa.String(80), nullable=False, index=True),
|
||||
sa.Column("limit_value", sa.Integer(), nullable=True),
|
||||
sa.Column("is_enabled", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("reason", sa.String(255), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.UniqueConstraint("merchant_id", "platform_id", "feature_code", name="uq_merchant_platform_feature"),
|
||||
)
|
||||
op.create_index("idx_merchant_override_lookup", "merchant_feature_overrides", ["merchant_id", "platform_id", "feature_code"])
|
||||
|
||||
# --- stripe_webhook_events ---
|
||||
op.create_table(
|
||||
"stripe_webhook_events",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("event_id", sa.String(100), unique=True, nullable=False, index=True),
|
||||
sa.Column("event_type", sa.String(100), nullable=False, index=True),
|
||||
sa.Column("status", sa.String(20), nullable=False, server_default="pending", index=True),
|
||||
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("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=True, index=True),
|
||||
sa.Column("merchant_subscription_id", sa.Integer(), sa.ForeignKey("merchant_subscriptions.id"), nullable=True, index=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_webhook_event_type_status", "stripe_webhook_events", ["event_type", "status"])
|
||||
|
||||
# --- billing_history ---
|
||||
op.create_table(
|
||||
"billing_history",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=True, index=True),
|
||||
sa.Column("merchant_id", sa.Integer(), sa.ForeignKey("merchants.id"), nullable=True, index=True),
|
||||
sa.Column("stripe_invoice_id", sa.String(100), unique=True, nullable=True, index=True),
|
||||
sa.Column("stripe_payment_intent_id", sa.String(100), nullable=True),
|
||||
sa.Column("invoice_number", sa.String(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, server_default="0"),
|
||||
sa.Column("total_cents", sa.Integer(), nullable=False),
|
||||
sa.Column("amount_paid_cents", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("currency", sa.String(3), nullable=False, server_default="EUR"),
|
||||
sa.Column("status", sa.String(20), nullable=False, index=True),
|
||||
sa.Column("invoice_pdf_url", sa.String(500), nullable=True),
|
||||
sa.Column("hosted_invoice_url", sa.String(500), nullable=True),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("line_items", sa.JSON(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_billing_store_date", "billing_history", ["store_id", "invoice_date"])
|
||||
op.create_index("idx_billing_status", "billing_history", ["store_id", "status"])
|
||||
|
||||
# --- capacity_snapshots ---
|
||||
op.create_table(
|
||||
"capacity_snapshots",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("snapshot_date", sa.DateTime(timezone=True), unique=True, nullable=False, index=True),
|
||||
sa.Column("total_stores", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("active_stores", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("trial_stores", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("total_subscriptions", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("active_subscriptions", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("total_products", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("total_orders_month", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("total_team_members", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("storage_used_gb", sa.Numeric(10, 2), nullable=False, server_default="0"),
|
||||
sa.Column("db_size_mb", sa.Numeric(10, 2), nullable=False, server_default="0"),
|
||||
sa.Column("theoretical_products_limit", sa.Integer(), nullable=True),
|
||||
sa.Column("theoretical_orders_limit", sa.Integer(), nullable=True),
|
||||
sa.Column("theoretical_team_limit", sa.Integer(), nullable=True),
|
||||
sa.Column("tier_distribution", sa.JSON(), nullable=True),
|
||||
sa.Column("avg_response_ms", sa.Integer(), nullable=True),
|
||||
sa.Column("peak_cpu_percent", sa.Numeric(5, 2), nullable=True),
|
||||
sa.Column("peak_memory_percent", sa.Numeric(5, 2), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("ix_capacity_snapshots_date", "capacity_snapshots", ["snapshot_date"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("capacity_snapshots")
|
||||
op.drop_table("billing_history")
|
||||
op.drop_table("stripe_webhook_events")
|
||||
op.drop_table("merchant_feature_overrides")
|
||||
op.drop_table("merchant_subscriptions")
|
||||
op.drop_table("store_addons")
|
||||
op.drop_table("addon_products")
|
||||
op.drop_table("tier_feature_limits")
|
||||
op.drop_table("store_platforms")
|
||||
op.drop_table("subscription_tiers")
|
||||
@@ -1,179 +0,0 @@
|
||||
# app/modules/billing/migrations/versions/billing_001_merchant_subscriptions_and_feature_limits.py
|
||||
"""
|
||||
Merchant subscriptions and feature limits migration.
|
||||
|
||||
Creates:
|
||||
- merchant_subscriptions table (replaces store_subscriptions)
|
||||
- tier_feature_limits table (replaces hardcoded limit columns)
|
||||
- merchant_feature_overrides table (replaces custom_*_limit columns)
|
||||
|
||||
Drops:
|
||||
- store_subscriptions table
|
||||
- features table
|
||||
|
||||
Alters:
|
||||
- subscription_tiers: removes limit columns and features JSON
|
||||
|
||||
Revision ID: billing_001
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# Revision identifiers
|
||||
revision = "billing_001"
|
||||
down_revision = None
|
||||
branch_labels = ("billing",)
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ========================================================================
|
||||
# Create merchant_subscriptions table
|
||||
# ========================================================================
|
||||
op.create_table(
|
||||
"merchant_subscriptions",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("merchant_id", sa.Integer(), sa.ForeignKey("merchants.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("platform_id", sa.Integer(), sa.ForeignKey("platforms.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("tier_id", sa.Integer(), sa.ForeignKey("subscription_tiers.id", ondelete="SET NULL"), nullable=True, index=True),
|
||||
sa.Column("status", sa.String(20), nullable=False, server_default="trial", index=True),
|
||||
sa.Column("is_annual", sa.Boolean(), nullable=False, server_default="0"),
|
||||
sa.Column("period_start", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("period_end", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("trial_ends_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("stripe_customer_id", sa.String(100), nullable=True, index=True),
|
||||
sa.Column("stripe_subscription_id", sa.String(100), nullable=True, index=True),
|
||||
sa.Column("stripe_payment_method_id", sa.String(100), nullable=True),
|
||||
sa.Column("payment_retry_count", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("last_payment_error", sa.Text(), nullable=True),
|
||||
sa.Column("cancelled_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("cancellation_reason", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
|
||||
sa.UniqueConstraint("merchant_id", "platform_id", name="uq_merchant_platform_subscription"),
|
||||
)
|
||||
op.create_index("idx_merchant_sub_status", "merchant_subscriptions", ["merchant_id", "status"])
|
||||
op.create_index("idx_merchant_sub_platform", "merchant_subscriptions", ["platform_id", "status"])
|
||||
|
||||
# ========================================================================
|
||||
# Create tier_feature_limits table
|
||||
# ========================================================================
|
||||
op.create_table(
|
||||
"tier_feature_limits",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("tier_id", sa.Integer(), sa.ForeignKey("subscription_tiers.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("feature_code", sa.String(80), nullable=False, index=True),
|
||||
sa.Column("limit_value", sa.Integer(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
|
||||
sa.UniqueConstraint("tier_id", "feature_code", name="uq_tier_feature_code"),
|
||||
)
|
||||
op.create_index("idx_tier_feature_lookup", "tier_feature_limits", ["tier_id", "feature_code"])
|
||||
|
||||
# ========================================================================
|
||||
# Create merchant_feature_overrides table
|
||||
# ========================================================================
|
||||
op.create_table(
|
||||
"merchant_feature_overrides",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("merchant_id", sa.Integer(), sa.ForeignKey("merchants.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("platform_id", sa.Integer(), sa.ForeignKey("platforms.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("feature_code", sa.String(80), nullable=False, index=True),
|
||||
sa.Column("limit_value", sa.Integer(), nullable=True),
|
||||
sa.Column("is_enabled", sa.Boolean(), nullable=False, server_default="1"),
|
||||
sa.Column("reason", sa.String(255), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
|
||||
sa.UniqueConstraint("merchant_id", "platform_id", "feature_code", name="uq_merchant_platform_feature"),
|
||||
)
|
||||
op.create_index("idx_merchant_override_lookup", "merchant_feature_overrides", ["merchant_id", "platform_id", "feature_code"])
|
||||
|
||||
# ========================================================================
|
||||
# Drop legacy tables
|
||||
# ========================================================================
|
||||
op.drop_table("store_subscriptions")
|
||||
op.drop_table("features")
|
||||
|
||||
# ========================================================================
|
||||
# Remove legacy columns from subscription_tiers
|
||||
# ========================================================================
|
||||
with op.batch_alter_table("subscription_tiers") as batch_op:
|
||||
batch_op.drop_column("orders_per_month")
|
||||
batch_op.drop_column("products_limit")
|
||||
batch_op.drop_column("team_members")
|
||||
batch_op.drop_column("order_history_months")
|
||||
batch_op.drop_column("cms_pages_limit")
|
||||
batch_op.drop_column("cms_custom_pages_limit")
|
||||
batch_op.drop_column("features")
|
||||
|
||||
# ========================================================================
|
||||
# Update stripe_webhook_events FK to merchant_subscriptions
|
||||
# ========================================================================
|
||||
with op.batch_alter_table("stripe_webhook_events") as batch_op:
|
||||
batch_op.drop_column("subscription_id")
|
||||
batch_op.add_column(
|
||||
sa.Column("merchant_subscription_id", sa.Integer(),
|
||||
sa.ForeignKey("merchant_subscriptions.id"), nullable=True, index=True)
|
||||
)
|
||||
|
||||
# ========================================================================
|
||||
# Add merchant_id to billing_history
|
||||
# ========================================================================
|
||||
with op.batch_alter_table("billing_history") as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column("merchant_id", sa.Integer(),
|
||||
sa.ForeignKey("merchants.id"), nullable=True, index=True)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Remove merchant_id from billing_history
|
||||
with op.batch_alter_table("billing_history") as batch_op:
|
||||
batch_op.drop_column("merchant_id")
|
||||
|
||||
# Restore subscription_id on stripe_webhook_events
|
||||
with op.batch_alter_table("stripe_webhook_events") as batch_op:
|
||||
batch_op.drop_column("merchant_subscription_id")
|
||||
batch_op.add_column(
|
||||
sa.Column("subscription_id", sa.Integer(),
|
||||
sa.ForeignKey("store_subscriptions.id"), nullable=True, index=True)
|
||||
)
|
||||
|
||||
# Restore columns on subscription_tiers
|
||||
with op.batch_alter_table("subscription_tiers") as batch_op:
|
||||
batch_op.add_column(sa.Column("orders_per_month", sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column("products_limit", sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column("team_members", sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column("order_history_months", sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column("cms_pages_limit", sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column("cms_custom_pages_limit", sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column("features", sa.JSON(), nullable=True))
|
||||
|
||||
# Recreate features table
|
||||
op.create_table(
|
||||
"features",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("code", sa.String(50), unique=True, nullable=False),
|
||||
sa.Column("name", sa.String(100), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("category", sa.String(50), nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), server_default="1"),
|
||||
)
|
||||
|
||||
# Recreate store_subscriptions table
|
||||
op.create_table(
|
||||
"store_subscriptions",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), unique=True, nullable=False),
|
||||
sa.Column("tier", sa.String(20), nullable=False, server_default="essential"),
|
||||
sa.Column("status", sa.String(20), nullable=False, server_default="trial"),
|
||||
sa.Column("period_start", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("period_end", sa.DateTime(timezone=True), nullable=False),
|
||||
)
|
||||
|
||||
# Drop new tables
|
||||
op.drop_table("merchant_feature_overrides")
|
||||
op.drop_table("tier_feature_limits")
|
||||
op.drop_table("merchant_subscriptions")
|
||||
@@ -243,8 +243,8 @@ class StoreAddOn(Base, TimestampMixin):
|
||||
addon_product = relationship("AddOnProduct")
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_vendor_addon_status", "store_id", "status"),
|
||||
Index("idx_vendor_addon_product", "store_id", "addon_product_id"),
|
||||
Index("idx_store_addon_status", "store_id", "status"),
|
||||
Index("idx_store_addon_product", "store_id", "addon_product_id"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
@@ -29,6 +29,7 @@ cart_module = ModuleDefinition(
|
||||
version="1.0.0",
|
||||
is_self_contained=True,
|
||||
requires=["inventory"], # Checks inventory availability
|
||||
migrations_path="migrations",
|
||||
features=[
|
||||
"cart_management", # Basic cart CRUD operations
|
||||
"cart_persistence", # Session and database persistence
|
||||
|
||||
0
app/modules/cart/migrations/__init__.py
Normal file
0
app/modules/cart/migrations/__init__.py
Normal file
0
app/modules/cart/migrations/versions/__init__.py
Normal file
0
app/modules/cart/migrations/versions/__init__.py
Normal file
35
app/modules/cart/migrations/versions/cart_001_initial.py
Normal file
35
app/modules/cart/migrations/versions/cart_001_initial.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""cart initial - cart items
|
||||
|
||||
Revision ID: cart_001
|
||||
Revises: inventory_001
|
||||
Create Date: 2026-02-07
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "cart_001"
|
||||
down_revision = "inventory_001"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# --- cart_items ---
|
||||
op.create_table(
|
||||
"cart_items",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=False),
|
||||
sa.Column("product_id", sa.Integer(), sa.ForeignKey("products.id"), nullable=False),
|
||||
sa.Column("session_id", sa.String(255), nullable=False, index=True),
|
||||
sa.Column("quantity", sa.Integer(), nullable=False, server_default="1"),
|
||||
sa.Column("price_at_add_cents", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.UniqueConstraint("store_id", "session_id", "product_id", name="uq_cart_item"),
|
||||
)
|
||||
op.create_index("idx_cart_session", "cart_items", ["store_id", "session_id"])
|
||||
op.create_index("idx_cart_created", "cart_items", ["created_at"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("cart_items")
|
||||
@@ -58,6 +58,7 @@ catalog_module = ModuleDefinition(
|
||||
version="1.0.0",
|
||||
is_self_contained=True,
|
||||
requires=["inventory"],
|
||||
migrations_path="migrations",
|
||||
features=[
|
||||
"product_catalog", # Core product catalog functionality
|
||||
"product_search", # Search and filtering
|
||||
|
||||
1
app/modules/catalog/migrations/__init__.py
Normal file
1
app/modules/catalog/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Module migrations."""
|
||||
1
app/modules/catalog/migrations/versions/__init__.py
Normal file
1
app/modules/catalog/migrations/versions/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Module migrations."""
|
||||
@@ -0,0 +1,97 @@
|
||||
"""catalog initial - products, translations, product media
|
||||
|
||||
Revision ID: catalog_001
|
||||
Revises: cms_001
|
||||
Create Date: 2026-02-07
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "catalog_001"
|
||||
down_revision = "cms_001"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# --- products ---
|
||||
op.create_table(
|
||||
"products",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=False),
|
||||
sa.Column("marketplace_product_id", sa.Integer(), sa.ForeignKey("marketplace_products.id"), nullable=True),
|
||||
sa.Column("store_sku", sa.String(), nullable=True, index=True),
|
||||
sa.Column("gtin", sa.String(50), nullable=True, index=True),
|
||||
sa.Column("gtin_type", sa.String(20), nullable=True),
|
||||
sa.Column("price_cents", sa.Integer(), nullable=True),
|
||||
sa.Column("sale_price_cents", sa.Integer(), nullable=True),
|
||||
sa.Column("currency", sa.String(3), nullable=True, server_default="EUR"),
|
||||
sa.Column("brand", sa.String(), nullable=True),
|
||||
sa.Column("condition", sa.String(), nullable=True),
|
||||
sa.Column("availability", sa.String(), nullable=True),
|
||||
sa.Column("primary_image_url", sa.String(), nullable=True),
|
||||
sa.Column("additional_images", sa.JSON(), nullable=True),
|
||||
sa.Column("download_url", sa.String(), nullable=True),
|
||||
sa.Column("license_type", sa.String(50), nullable=True),
|
||||
sa.Column("tax_rate_percent", sa.Integer(), nullable=False, server_default="17"),
|
||||
sa.Column("supplier", sa.String(50), nullable=True),
|
||||
sa.Column("supplier_product_id", sa.String(), nullable=True),
|
||||
sa.Column("cost_cents", sa.Integer(), nullable=True),
|
||||
sa.Column("margin_percent_x100", sa.Integer(), nullable=True),
|
||||
sa.Column("is_digital", sa.Boolean(), nullable=True, server_default="false", index=True),
|
||||
sa.Column("product_type", sa.String(20), nullable=True, server_default="physical"),
|
||||
sa.Column("is_featured", sa.Boolean(), nullable=True, server_default="false"),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=True, server_default="true"),
|
||||
sa.Column("display_order", sa.Integer(), nullable=True, server_default="0"),
|
||||
sa.Column("min_quantity", sa.Integer(), nullable=True, server_default="1"),
|
||||
sa.Column("max_quantity", sa.Integer(), nullable=True),
|
||||
sa.Column("fulfillment_email_template", sa.String(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.UniqueConstraint("store_id", "marketplace_product_id", name="uq_store_marketplace_product"),
|
||||
)
|
||||
op.create_index("idx_product_store_active", "products", ["store_id", "is_active"])
|
||||
op.create_index("idx_product_store_featured", "products", ["store_id", "is_featured"])
|
||||
op.create_index("idx_product_store_sku", "products", ["store_id", "store_sku"])
|
||||
op.create_index("idx_product_supplier", "products", ["supplier", "supplier_product_id"])
|
||||
|
||||
# --- product_translations ---
|
||||
op.create_table(
|
||||
"product_translations",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("product_id", sa.Integer(), sa.ForeignKey("products.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("language", sa.String(5), nullable=False),
|
||||
sa.Column("title", sa.String(), nullable=True),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("short_description", sa.String(500), nullable=True),
|
||||
sa.Column("meta_title", sa.String(70), nullable=True),
|
||||
sa.Column("meta_description", sa.String(160), nullable=True),
|
||||
sa.Column("url_slug", sa.String(255), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.UniqueConstraint("product_id", "language", name="uq_product_translation"),
|
||||
)
|
||||
op.create_index("idx_pt_product_id", "product_translations", ["product_id"])
|
||||
op.create_index("idx_pt_product_language", "product_translations", ["product_id", "language"])
|
||||
|
||||
# --- product_media ---
|
||||
op.create_table(
|
||||
"product_media",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("product_id", sa.Integer(), sa.ForeignKey("products.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("media_id", sa.Integer(), sa.ForeignKey("media_files.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("usage_type", sa.String(50), nullable=False, server_default="gallery"),
|
||||
sa.Column("display_order", sa.Integer(), nullable=True, server_default="0"),
|
||||
sa.Column("variant_id", sa.Integer(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.UniqueConstraint("product_id", "media_id", "usage_type", name="uq_product_media_usage"),
|
||||
)
|
||||
op.create_index("idx_product_media_product", "product_media", ["product_id"])
|
||||
op.create_index("idx_product_media_media", "product_media", ["media_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("product_media")
|
||||
op.drop_table("product_translations")
|
||||
op.drop_table("products")
|
||||
@@ -121,11 +121,11 @@ class Product(Base, TimestampMixin):
|
||||
# === CONSTRAINTS & INDEXES ===
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"store_id", "marketplace_product_id", name="uq_vendor_marketplace_product"
|
||||
"store_id", "marketplace_product_id", name="uq_store_marketplace_product"
|
||||
),
|
||||
Index("idx_product_vendor_active", "store_id", "is_active"),
|
||||
Index("idx_product_vendor_featured", "store_id", "is_featured"),
|
||||
Index("idx_product_vendor_sku", "store_id", "store_sku"),
|
||||
Index("idx_product_store_active", "store_id", "is_active"),
|
||||
Index("idx_product_store_featured", "store_id", "is_featured"),
|
||||
Index("idx_product_store_sku", "store_id", "store_sku"),
|
||||
Index("idx_product_supplier", "supplier", "supplier_product_id"),
|
||||
)
|
||||
|
||||
|
||||
@@ -254,6 +254,7 @@ cms_module = ModuleDefinition(
|
||||
services_path="app.modules.cms.services",
|
||||
models_path="app.modules.cms.models",
|
||||
exceptions_path="app.modules.cms.exceptions",
|
||||
migrations_path="migrations",
|
||||
# Module templates (namespaced as cms/admin/*.html and cms/store/*.html)
|
||||
templates_path="templates",
|
||||
# Module-specific translations (accessible via cms.* keys)
|
||||
|
||||
107
app/modules/cms/migrations/versions/cms_001_initial.py
Normal file
107
app/modules/cms/migrations/versions/cms_001_initial.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""cms initial - content pages, store themes, media files
|
||||
|
||||
Revision ID: cms_001
|
||||
Revises: marketplace_001
|
||||
Create Date: 2026-02-07
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "cms_001"
|
||||
down_revision = "marketplace_001"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# --- content_pages ---
|
||||
op.create_table(
|
||||
"content_pages",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("platform_id", sa.Integer(), sa.ForeignKey("platforms.id", ondelete="CASCADE"), nullable=False, index=True, comment="Platform this page belongs to"),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id", ondelete="CASCADE"), nullable=True, index=True, comment="Store this page belongs to (NULL for platform/default pages)"),
|
||||
sa.Column("is_platform_page", sa.Boolean(), nullable=False, server_default="false", comment="True = platform marketing page (homepage, pricing); False = store default or override"),
|
||||
sa.Column("slug", sa.String(100), nullable=False, index=True),
|
||||
sa.Column("title", sa.String(200), nullable=False),
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column("content_format", sa.String(20), nullable=True, server_default="html"),
|
||||
sa.Column("template", sa.String(50), nullable=False, server_default="default"),
|
||||
sa.Column("sections", sa.JSON(), nullable=True, comment="Structured homepage sections (hero, features, pricing, cta) with i18n"),
|
||||
sa.Column("meta_description", sa.String(300), nullable=True),
|
||||
sa.Column("meta_keywords", sa.String(300), nullable=True),
|
||||
sa.Column("is_published", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("published_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("display_order", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("show_in_footer", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("show_in_header", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("show_in_legal", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("created_by", sa.Integer(), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("updated_by", sa.Integer(), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.UniqueConstraint("platform_id", "store_id", "slug", name="uq_platform_store_slug"),
|
||||
)
|
||||
op.create_index("idx_platform_store_published", "content_pages", ["platform_id", "store_id", "is_published"])
|
||||
op.create_index("idx_platform_slug_published", "content_pages", ["platform_id", "slug", "is_published"])
|
||||
op.create_index("idx_platform_page_type", "content_pages", ["platform_id", "is_platform_page"])
|
||||
|
||||
# --- store_themes ---
|
||||
op.create_table(
|
||||
"store_themes",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id", ondelete="CASCADE"), unique=True, nullable=False),
|
||||
sa.Column("theme_name", sa.String(100), nullable=True, server_default="default"),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=True, server_default="true"),
|
||||
sa.Column("colors", sa.JSON(), nullable=True),
|
||||
sa.Column("font_family_heading", sa.String(100), nullable=True, server_default="Inter, sans-serif"),
|
||||
sa.Column("font_family_body", sa.String(100), nullable=True, server_default="Inter, sans-serif"),
|
||||
sa.Column("logo_url", sa.String(500), nullable=True),
|
||||
sa.Column("logo_dark_url", sa.String(500), nullable=True),
|
||||
sa.Column("favicon_url", sa.String(500), nullable=True),
|
||||
sa.Column("banner_url", sa.String(500), nullable=True),
|
||||
sa.Column("layout_style", sa.String(50), nullable=True, server_default="grid"),
|
||||
sa.Column("header_style", sa.String(50), nullable=True, server_default="fixed"),
|
||||
sa.Column("product_card_style", sa.String(50), nullable=True, server_default="modern"),
|
||||
sa.Column("custom_css", sa.Text(), nullable=True),
|
||||
sa.Column("social_links", sa.JSON(), nullable=True),
|
||||
sa.Column("meta_title_template", sa.String(200), nullable=True),
|
||||
sa.Column("meta_description", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
|
||||
# --- media_files ---
|
||||
op.create_table(
|
||||
"media_files",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=False),
|
||||
sa.Column("filename", sa.String(255), nullable=False),
|
||||
sa.Column("original_filename", sa.String(255), nullable=True),
|
||||
sa.Column("file_path", sa.String(500), nullable=False),
|
||||
sa.Column("media_type", sa.String(20), nullable=False),
|
||||
sa.Column("mime_type", sa.String(100), nullable=True),
|
||||
sa.Column("file_size", sa.Integer(), nullable=True),
|
||||
sa.Column("width", sa.Integer(), nullable=True),
|
||||
sa.Column("height", sa.Integer(), nullable=True),
|
||||
sa.Column("thumbnail_path", sa.String(500), nullable=True),
|
||||
sa.Column("alt_text", sa.String(500), nullable=True),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("folder", sa.String(100), nullable=True, server_default="general"),
|
||||
sa.Column("tags", sa.JSON(), nullable=True),
|
||||
sa.Column("extra_metadata", sa.JSON(), nullable=True),
|
||||
sa.Column("is_optimized", sa.Boolean(), nullable=True, server_default="false"),
|
||||
sa.Column("optimized_size", sa.Integer(), nullable=True),
|
||||
sa.Column("usage_count", sa.Integer(), nullable=True, server_default="0"),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_media_store_id", "media_files", ["store_id"])
|
||||
op.create_index("idx_media_store_folder", "media_files", ["store_id", "folder"])
|
||||
op.create_index("idx_media_store_type", "media_files", ["store_id", "media_type"])
|
||||
op.create_index("idx_media_filename", "media_files", ["filename"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("media_files")
|
||||
op.drop_table("store_themes")
|
||||
op.drop_table("content_pages")
|
||||
@@ -138,6 +138,7 @@ customers_module = ModuleDefinition(
|
||||
models_path="app.modules.customers.models",
|
||||
schemas_path="app.modules.customers.schemas",
|
||||
exceptions_path="app.modules.customers.exceptions",
|
||||
migrations_path="migrations",
|
||||
# Metrics provider for dashboard statistics
|
||||
metrics_provider=_get_metrics_provider,
|
||||
# Feature provider for feature flags
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"""customers initial - customers, addresses, password reset tokens
|
||||
|
||||
Revision ID: customers_001
|
||||
Revises: catalog_001
|
||||
Create Date: 2026-02-07
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "customers_001"
|
||||
down_revision = "catalog_001"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# --- customers ---
|
||||
op.create_table(
|
||||
"customers",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=False),
|
||||
sa.Column("email", sa.String(255), nullable=False, index=True),
|
||||
sa.Column("hashed_password", sa.String(255), nullable=False),
|
||||
sa.Column("first_name", sa.String(100), nullable=True),
|
||||
sa.Column("last_name", sa.String(100), nullable=True),
|
||||
sa.Column("phone", sa.String(50), nullable=True),
|
||||
sa.Column("customer_number", sa.String(100), nullable=False, index=True),
|
||||
sa.Column("preferences", sa.JSON(), nullable=True),
|
||||
sa.Column("marketing_consent", sa.Boolean(), nullable=True, server_default="false"),
|
||||
sa.Column("last_order_date", sa.DateTime(), nullable=True),
|
||||
sa.Column("total_orders", sa.Integer(), nullable=True, server_default="0"),
|
||||
sa.Column("total_spent", sa.Numeric(10, 2), nullable=True, server_default="0"),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("preferred_language", sa.String(5), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
|
||||
# --- customer_addresses ---
|
||||
op.create_table(
|
||||
"customer_addresses",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=False),
|
||||
sa.Column("customer_id", sa.Integer(), sa.ForeignKey("customers.id"), nullable=False),
|
||||
sa.Column("address_type", sa.String(50), nullable=False),
|
||||
sa.Column("first_name", sa.String(100), nullable=False),
|
||||
sa.Column("last_name", sa.String(100), nullable=False),
|
||||
sa.Column("company", sa.String(200), nullable=True),
|
||||
sa.Column("address_line_1", sa.String(255), nullable=False),
|
||||
sa.Column("address_line_2", sa.String(255), nullable=True),
|
||||
sa.Column("city", sa.String(100), nullable=False),
|
||||
sa.Column("postal_code", sa.String(20), nullable=False),
|
||||
sa.Column("country_name", sa.String(100), nullable=False),
|
||||
sa.Column("country_iso", sa.String(5), nullable=False),
|
||||
sa.Column("is_default", sa.Boolean(), nullable=True, server_default="false"),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
|
||||
# --- password_reset_tokens ---
|
||||
op.create_table(
|
||||
"password_reset_tokens",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("customer_id", sa.Integer(), sa.ForeignKey("customers.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("token_hash", sa.String(64), nullable=False, index=True),
|
||||
sa.Column("expires_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("used_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("password_reset_tokens")
|
||||
op.drop_table("customer_addresses")
|
||||
op.drop_table("customers")
|
||||
@@ -85,6 +85,7 @@ dev_tools_module = ModuleDefinition(
|
||||
schemas_path="app.modules.dev_tools.schemas",
|
||||
exceptions_path="app.modules.dev_tools.exceptions",
|
||||
tasks_path="app.modules.dev_tools.tasks",
|
||||
migrations_path="migrations",
|
||||
# Note: Code quality and test tasks are on-demand, not scheduled.
|
||||
# If scheduled scans are desired, add ScheduledTask entries here.
|
||||
)
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
"""dev_tools initial - test runs, architecture scans, violations
|
||||
|
||||
Revision ID: dev_tools_001
|
||||
Revises: loyalty_001
|
||||
Create Date: 2026-02-07
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "dev_tools_001"
|
||||
down_revision = "loyalty_001"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# --- test_runs ---
|
||||
op.create_table(
|
||||
"test_runs",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("timestamp", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False, index=True),
|
||||
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(100), nullable=True),
|
||||
sa.Column("git_commit_hash", sa.String(40), nullable=True),
|
||||
sa.Column("git_branch", sa.String(100), nullable=True),
|
||||
sa.Column("test_path", sa.String(500), nullable=True),
|
||||
sa.Column("pytest_args", sa.String(500), nullable=True),
|
||||
sa.Column("status", sa.String(20), nullable=True, server_default="running", index=True),
|
||||
sa.Column("celery_task_id", sa.String(255), nullable=True, index=True),
|
||||
)
|
||||
|
||||
# --- test_results ---
|
||||
op.create_table(
|
||||
"test_results",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("run_id", sa.Integer(), sa.ForeignKey("test_runs.id"), nullable=False, index=True),
|
||||
sa.Column("node_id", sa.String(500), nullable=False, index=True),
|
||||
sa.Column("test_name", sa.String(200), nullable=False),
|
||||
sa.Column("test_file", sa.String(300), nullable=False),
|
||||
sa.Column("test_class", sa.String(200), nullable=True),
|
||||
sa.Column("outcome", sa.String(20), nullable=False, index=True),
|
||||
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.func.now(), nullable=False),
|
||||
)
|
||||
|
||||
# --- test_collections ---
|
||||
op.create_table(
|
||||
"test_collections",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
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.func.now(), nullable=False),
|
||||
)
|
||||
|
||||
# --- architecture_scans ---
|
||||
op.create_table(
|
||||
"architecture_scans",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("timestamp", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False, index=True),
|
||||
sa.Column("validator_type", sa.String(20), nullable=False, server_default="architecture", index=True),
|
||||
sa.Column("status", sa.String(30), nullable=False, server_default="pending", index=True),
|
||||
sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("error_message", sa.Text(), nullable=True),
|
||||
sa.Column("progress_message", sa.String(255), nullable=True),
|
||||
sa.Column("total_files", sa.Integer(), nullable=True),
|
||||
sa.Column("total_violations", sa.Integer(), nullable=True),
|
||||
sa.Column("errors", sa.Integer(), nullable=True),
|
||||
sa.Column("warnings", sa.Integer(), nullable=True),
|
||||
sa.Column("duration_seconds", sa.Float(), nullable=True),
|
||||
sa.Column("triggered_by", sa.String(100), nullable=True),
|
||||
sa.Column("git_commit_hash", sa.String(40), nullable=True),
|
||||
sa.Column("celery_task_id", sa.String(255), nullable=True, index=True),
|
||||
)
|
||||
|
||||
# --- architecture_violations ---
|
||||
op.create_table(
|
||||
"architecture_violations",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("scan_id", sa.Integer(), sa.ForeignKey("architecture_scans.id"), nullable=False, index=True),
|
||||
sa.Column("validator_type", sa.String(20), nullable=False, server_default="architecture", index=True),
|
||||
sa.Column("rule_id", sa.String(20), nullable=False, index=True),
|
||||
sa.Column("rule_name", sa.String(200), nullable=False),
|
||||
sa.Column("severity", sa.String(10), nullable=False, index=True),
|
||||
sa.Column("file_path", sa.String(500), nullable=False, index=True),
|
||||
sa.Column("line_number", sa.Integer(), nullable=False),
|
||||
sa.Column("message", sa.Text(), nullable=False),
|
||||
sa.Column("context", sa.Text(), nullable=True),
|
||||
sa.Column("suggestion", sa.Text(), nullable=True),
|
||||
sa.Column("status", sa.String(20), nullable=True, server_default="open", index=True),
|
||||
sa.Column("assigned_to", sa.Integer(), sa.ForeignKey("users.id"), nullable=True),
|
||||
sa.Column("resolved_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("resolved_by", sa.Integer(), sa.ForeignKey("users.id"), nullable=True),
|
||||
sa.Column("resolution_note", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
|
||||
# --- architecture_rules ---
|
||||
op.create_table(
|
||||
"architecture_rules",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("rule_id", sa.String(20), unique=True, nullable=False, index=True),
|
||||
sa.Column("validator_type", sa.String(20), nullable=False, server_default="architecture", index=True),
|
||||
sa.Column("category", sa.String(50), nullable=False),
|
||||
sa.Column("name", sa.String(200), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("severity", sa.String(10), nullable=False),
|
||||
sa.Column("enabled", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("custom_config", sa.JSON(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
|
||||
# --- violation_assignments ---
|
||||
op.create_table(
|
||||
"violation_assignments",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("violation_id", sa.Integer(), sa.ForeignKey("architecture_violations.id"), nullable=False, index=True),
|
||||
sa.Column("user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=False),
|
||||
sa.Column("assigned_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("assigned_by", sa.Integer(), sa.ForeignKey("users.id"), nullable=True),
|
||||
sa.Column("due_date", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("priority", sa.String(10), nullable=True),
|
||||
)
|
||||
|
||||
# --- violation_comments ---
|
||||
op.create_table(
|
||||
"violation_comments",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("violation_id", sa.Integer(), sa.ForeignKey("architecture_violations.id"), nullable=False, index=True),
|
||||
sa.Column("user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=False),
|
||||
sa.Column("comment", sa.Text(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("violation_comments")
|
||||
op.drop_table("violation_assignments")
|
||||
op.drop_table("architecture_rules")
|
||||
op.drop_table("architecture_violations")
|
||||
op.drop_table("architecture_scans")
|
||||
op.drop_table("test_collections")
|
||||
op.drop_table("test_results")
|
||||
op.drop_table("test_runs")
|
||||
@@ -145,6 +145,7 @@ inventory_module = ModuleDefinition(
|
||||
models_path="app.modules.inventory.models",
|
||||
schemas_path="app.modules.inventory.schemas",
|
||||
exceptions_path="app.modules.inventory.exceptions",
|
||||
migrations_path="migrations",
|
||||
# Metrics provider for dashboard statistics
|
||||
metrics_provider=_get_metrics_provider,
|
||||
# Feature provider for feature flags
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""inventory initial - inventory, inventory transactions
|
||||
|
||||
Revision ID: inventory_001
|
||||
Revises: orders_001
|
||||
Create Date: 2026-02-07
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "inventory_001"
|
||||
down_revision = "orders_001"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# --- inventory ---
|
||||
op.create_table(
|
||||
"inventory",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("product_id", sa.Integer(), sa.ForeignKey("products.id"), nullable=False, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=False, index=True),
|
||||
sa.Column("warehouse", sa.String(), nullable=False, server_default="strassen", index=True),
|
||||
sa.Column("bin_location", sa.String(), nullable=False, index=True),
|
||||
sa.Column("location", sa.String(), nullable=True, index=True),
|
||||
sa.Column("quantity", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("reserved_quantity", sa.Integer(), nullable=True, server_default="0"),
|
||||
sa.Column("gtin", sa.String(), nullable=True, index=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.UniqueConstraint("product_id", "warehouse", "bin_location", name="uq_inventory_product_warehouse_bin"),
|
||||
)
|
||||
op.create_index("idx_inventory_store_product", "inventory", ["store_id", "product_id"])
|
||||
op.create_index("idx_inventory_warehouse_bin", "inventory", ["warehouse", "bin_location"])
|
||||
|
||||
# --- inventory_transactions ---
|
||||
# Create the enum type for transaction_type
|
||||
transaction_type_enum = sa.Enum(
|
||||
"reserve", "fulfill", "release", "adjust", "set", "import", "return",
|
||||
name="transactiontype",
|
||||
)
|
||||
op.create_table(
|
||||
"inventory_transactions",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=False, index=True),
|
||||
sa.Column("product_id", sa.Integer(), sa.ForeignKey("products.id"), nullable=False, index=True),
|
||||
sa.Column("inventory_id", sa.Integer(), sa.ForeignKey("inventory.id"), nullable=True, index=True),
|
||||
sa.Column("transaction_type", transaction_type_enum, nullable=False, index=True),
|
||||
sa.Column("quantity_change", sa.Integer(), nullable=False),
|
||||
sa.Column("quantity_after", sa.Integer(), nullable=False),
|
||||
sa.Column("reserved_after", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("location", sa.String(), nullable=True),
|
||||
sa.Column("warehouse", sa.String(), nullable=True),
|
||||
sa.Column("order_id", sa.Integer(), sa.ForeignKey("orders.id"), nullable=True, index=True),
|
||||
sa.Column("order_number", sa.String(), nullable=True),
|
||||
sa.Column("reason", sa.Text(), nullable=True),
|
||||
sa.Column("created_by", sa.String(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False, index=True),
|
||||
)
|
||||
op.create_index("idx_inv_tx_store_product", "inventory_transactions", ["store_id", "product_id"])
|
||||
op.create_index("idx_inv_tx_store_created", "inventory_transactions", ["store_id", "created_at"])
|
||||
op.create_index("idx_inv_tx_order", "inventory_transactions", ["order_id"])
|
||||
op.create_index("idx_inv_tx_type_created", "inventory_transactions", ["transaction_type", "created_at"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("inventory_transactions")
|
||||
op.drop_table("inventory")
|
||||
sa.Enum(name="transactiontype").drop(op.get_bind(), checkfirst=True)
|
||||
@@ -1,650 +0,0 @@
|
||||
"""add loyalty module tables
|
||||
|
||||
Revision ID: 0fb5d6d6ff97
|
||||
Revises: zd3n4o5p6q7r8
|
||||
Create Date: 2026-01-28 22:55:34.074321
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.dialects import sqlite
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '0fb5d6d6ff97'
|
||||
down_revision: Union[str, None] = 'zd3n4o5p6q7r8'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('loyalty_programs',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('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_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_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',
|
||||
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',
|
||||
existing_nullable=True)
|
||||
op.alter_column('admin_menu_configs', 'user_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
comment='User scope - applies to this specific super admin (admin frontend only)',
|
||||
existing_comment='User scope - applies to this specific super admin',
|
||||
existing_nullable=True)
|
||||
op.alter_column('admin_menu_configs', 'created_at',
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('admin_menu_configs', 'updated_at',
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.drop_index('idx_admin_menu_configs_frontend_type', table_name='admin_menu_configs')
|
||||
op.drop_index('idx_admin_menu_configs_menu_item_id', table_name='admin_menu_configs')
|
||||
op.drop_index('idx_admin_menu_configs_platform_id', table_name='admin_menu_configs')
|
||||
op.drop_index('idx_admin_menu_configs_user_id', table_name='admin_menu_configs')
|
||||
op.create_index(op.f('ix_admin_menu_configs_frontend_type'), 'admin_menu_configs', ['frontend_type'], unique=False)
|
||||
op.create_index(op.f('ix_admin_menu_configs_id'), 'admin_menu_configs', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_admin_menu_configs_menu_item_id'), 'admin_menu_configs', ['menu_item_id'], unique=False)
|
||||
op.create_index(op.f('ix_admin_menu_configs_platform_id'), 'admin_menu_configs', ['platform_id'], unique=False)
|
||||
op.create_index(op.f('ix_admin_menu_configs_user_id'), 'admin_menu_configs', ['user_id'], unique=False)
|
||||
op.alter_column('admin_platforms', 'created_at',
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('admin_platforms', 'updated_at',
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.drop_index('idx_admin_platforms_platform_id', table_name='admin_platforms')
|
||||
op.drop_index('idx_admin_platforms_user_id', table_name='admin_platforms')
|
||||
op.create_index(op.f('ix_admin_platforms_id'), 'admin_platforms', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_admin_platforms_platform_id'), 'admin_platforms', ['platform_id'], unique=False)
|
||||
op.create_index(op.f('ix_admin_platforms_user_id'), 'admin_platforms', ['user_id'], unique=False)
|
||||
op.alter_column('content_pages', 'platform_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
comment='Platform this page belongs to',
|
||||
existing_nullable=False)
|
||||
op.alter_column('content_pages', 'store_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
comment='Store this page belongs to (NULL for platform/default pages)',
|
||||
existing_nullable=True)
|
||||
op.alter_column('content_pages', 'is_platform_page',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
comment='True = platform marketing page (homepage, pricing); False = store default or override',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('false'))
|
||||
op.alter_column('platform_modules', 'created_at',
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('platform_modules', 'updated_at',
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.create_index(op.f('ix_platform_modules_id'), 'platform_modules', ['id'], unique=False)
|
||||
op.alter_column('platforms', 'code',
|
||||
existing_type=sa.VARCHAR(length=50),
|
||||
comment="Unique platform identifier (e.g., 'oms', 'loyalty', 'sites')",
|
||||
existing_nullable=False)
|
||||
op.alter_column('platforms', 'name',
|
||||
existing_type=sa.VARCHAR(length=100),
|
||||
comment="Display name (e.g., 'Wizamart OMS')",
|
||||
existing_nullable=False)
|
||||
op.alter_column('platforms', 'description',
|
||||
existing_type=sa.TEXT(),
|
||||
comment='Platform description for admin/marketing purposes',
|
||||
existing_nullable=True)
|
||||
op.alter_column('platforms', 'domain',
|
||||
existing_type=sa.VARCHAR(length=255),
|
||||
comment="Production domain (e.g., 'oms.lu', 'loyalty.lu')",
|
||||
existing_nullable=True)
|
||||
op.alter_column('platforms', 'path_prefix',
|
||||
existing_type=sa.VARCHAR(length=50),
|
||||
comment="Development path prefix (e.g., 'oms' for localhost:9999/oms/*)",
|
||||
existing_nullable=True)
|
||||
op.alter_column('platforms', 'logo',
|
||||
existing_type=sa.VARCHAR(length=500),
|
||||
comment='Logo URL for light mode',
|
||||
existing_nullable=True)
|
||||
op.alter_column('platforms', 'logo_dark',
|
||||
existing_type=sa.VARCHAR(length=500),
|
||||
comment='Logo URL for dark mode',
|
||||
existing_nullable=True)
|
||||
op.alter_column('platforms', 'favicon',
|
||||
existing_type=sa.VARCHAR(length=500),
|
||||
comment='Favicon URL',
|
||||
existing_nullable=True)
|
||||
op.alter_column('platforms', 'theme_config',
|
||||
existing_type=postgresql.JSON(astext_type=sa.Text()),
|
||||
comment='Theme configuration (colors, fonts, etc.)',
|
||||
existing_nullable=True)
|
||||
op.alter_column('platforms', 'default_language',
|
||||
existing_type=sa.VARCHAR(length=5),
|
||||
comment="Default language code (e.g., 'fr', 'en', 'de')",
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'fr'::character varying"))
|
||||
op.alter_column('platforms', 'supported_languages',
|
||||
existing_type=postgresql.JSON(astext_type=sa.Text()),
|
||||
comment='List of supported language codes',
|
||||
existing_nullable=False)
|
||||
op.alter_column('platforms', 'is_active',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
comment='Whether the platform is active and accessible',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('true'))
|
||||
op.alter_column('platforms', 'is_public',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
comment='Whether the platform is visible in public listings',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('true'))
|
||||
op.alter_column('platforms', 'settings',
|
||||
existing_type=postgresql.JSON(astext_type=sa.Text()),
|
||||
comment='Platform-specific settings and feature flags',
|
||||
existing_nullable=True)
|
||||
op.alter_column('platforms', 'created_at',
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('platforms', 'updated_at',
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.create_index(op.f('ix_platforms_id'), 'platforms', ['id'], unique=False)
|
||||
op.alter_column('subscription_tiers', 'platform_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
comment='Platform this tier belongs to (NULL = global tier)',
|
||||
existing_nullable=True)
|
||||
op.alter_column('subscription_tiers', 'cms_pages_limit',
|
||||
existing_type=sa.INTEGER(),
|
||||
comment='Total CMS pages limit (NULL = unlimited)',
|
||||
existing_nullable=True)
|
||||
op.alter_column('subscription_tiers', 'cms_custom_pages_limit',
|
||||
existing_type=sa.INTEGER(),
|
||||
comment='Custom pages limit, excluding overrides (NULL = unlimited)',
|
||||
existing_nullable=True)
|
||||
op.drop_index('ix_subscription_tiers_code', table_name='subscription_tiers')
|
||||
op.create_index(op.f('ix_subscription_tiers_code'), 'subscription_tiers', ['code'], unique=False)
|
||||
op.alter_column('users', 'is_super_admin',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
comment=None,
|
||||
existing_comment='Whether this admin has access to all platforms (super admin)',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('false'))
|
||||
op.alter_column('store_platforms', 'store_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
comment='Reference to the store',
|
||||
existing_nullable=False)
|
||||
op.alter_column('store_platforms', 'platform_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
comment='Reference to the platform',
|
||||
existing_nullable=False)
|
||||
op.alter_column('store_platforms', 'tier_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
comment='Platform-specific subscription tier',
|
||||
existing_nullable=True)
|
||||
op.alter_column('store_platforms', 'is_active',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
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_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_type=sa.VARCHAR(length=100),
|
||||
comment='Platform-specific subdomain (if different from main subdomain)',
|
||||
existing_nullable=True)
|
||||
op.alter_column('store_platforms', 'settings',
|
||||
existing_type=postgresql.JSON(astext_type=sa.Text()),
|
||||
comment='Platform-specific store settings',
|
||||
existing_nullable=True)
|
||||
op.alter_column('store_platforms', 'joined_at',
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
comment='When the store joined this platform',
|
||||
existing_nullable=False,
|
||||
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_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)
|
||||
# ### 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',
|
||||
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_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_type=postgresql.TIMESTAMP(timezone=True),
|
||||
comment=None,
|
||||
existing_comment='When the store joined this platform',
|
||||
existing_nullable=False,
|
||||
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_nullable=True)
|
||||
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_nullable=True)
|
||||
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_type=sa.BOOLEAN(),
|
||||
comment=None,
|
||||
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_type=sa.INTEGER(),
|
||||
comment=None,
|
||||
existing_comment='Platform-specific subscription tier',
|
||||
existing_nullable=True)
|
||||
op.alter_column('store_platforms', 'platform_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
comment=None,
|
||||
existing_comment='Reference to the platform',
|
||||
existing_nullable=False)
|
||||
op.alter_column('store_platforms', 'store_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
comment=None,
|
||||
existing_comment='Reference to the store',
|
||||
existing_nullable=False)
|
||||
op.alter_column('users', 'is_super_admin',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
comment='Whether this admin has access to all platforms (super admin)',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('false'))
|
||||
op.drop_index(op.f('ix_subscription_tiers_code'), table_name='subscription_tiers')
|
||||
op.create_index('ix_subscription_tiers_code', 'subscription_tiers', ['code'], unique=True)
|
||||
op.alter_column('subscription_tiers', 'cms_custom_pages_limit',
|
||||
existing_type=sa.INTEGER(),
|
||||
comment=None,
|
||||
existing_comment='Custom pages limit, excluding overrides (NULL = unlimited)',
|
||||
existing_nullable=True)
|
||||
op.alter_column('subscription_tiers', 'cms_pages_limit',
|
||||
existing_type=sa.INTEGER(),
|
||||
comment=None,
|
||||
existing_comment='Total CMS pages limit (NULL = unlimited)',
|
||||
existing_nullable=True)
|
||||
op.alter_column('subscription_tiers', 'platform_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
comment=None,
|
||||
existing_comment='Platform this tier belongs to (NULL = global tier)',
|
||||
existing_nullable=True)
|
||||
op.drop_index(op.f('ix_platforms_id'), table_name='platforms')
|
||||
op.alter_column('platforms', 'updated_at',
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('platforms', 'created_at',
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('platforms', 'settings',
|
||||
existing_type=postgresql.JSON(astext_type=sa.Text()),
|
||||
comment=None,
|
||||
existing_comment='Platform-specific settings and feature flags',
|
||||
existing_nullable=True)
|
||||
op.alter_column('platforms', 'is_public',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
comment=None,
|
||||
existing_comment='Whether the platform is visible in public listings',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('true'))
|
||||
op.alter_column('platforms', 'is_active',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
comment=None,
|
||||
existing_comment='Whether the platform is active and accessible',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('true'))
|
||||
op.alter_column('platforms', 'supported_languages',
|
||||
existing_type=postgresql.JSON(astext_type=sa.Text()),
|
||||
comment=None,
|
||||
existing_comment='List of supported language codes',
|
||||
existing_nullable=False)
|
||||
op.alter_column('platforms', 'default_language',
|
||||
existing_type=sa.VARCHAR(length=5),
|
||||
comment=None,
|
||||
existing_comment="Default language code (e.g., 'fr', 'en', 'de')",
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'fr'::character varying"))
|
||||
op.alter_column('platforms', 'theme_config',
|
||||
existing_type=postgresql.JSON(astext_type=sa.Text()),
|
||||
comment=None,
|
||||
existing_comment='Theme configuration (colors, fonts, etc.)',
|
||||
existing_nullable=True)
|
||||
op.alter_column('platforms', 'favicon',
|
||||
existing_type=sa.VARCHAR(length=500),
|
||||
comment=None,
|
||||
existing_comment='Favicon URL',
|
||||
existing_nullable=True)
|
||||
op.alter_column('platforms', 'logo_dark',
|
||||
existing_type=sa.VARCHAR(length=500),
|
||||
comment=None,
|
||||
existing_comment='Logo URL for dark mode',
|
||||
existing_nullable=True)
|
||||
op.alter_column('platforms', 'logo',
|
||||
existing_type=sa.VARCHAR(length=500),
|
||||
comment=None,
|
||||
existing_comment='Logo URL for light mode',
|
||||
existing_nullable=True)
|
||||
op.alter_column('platforms', 'path_prefix',
|
||||
existing_type=sa.VARCHAR(length=50),
|
||||
comment=None,
|
||||
existing_comment="Development path prefix (e.g., 'oms' for localhost:9999/oms/*)",
|
||||
existing_nullable=True)
|
||||
op.alter_column('platforms', 'domain',
|
||||
existing_type=sa.VARCHAR(length=255),
|
||||
comment=None,
|
||||
existing_comment="Production domain (e.g., 'oms.lu', 'loyalty.lu')",
|
||||
existing_nullable=True)
|
||||
op.alter_column('platforms', 'description',
|
||||
existing_type=sa.TEXT(),
|
||||
comment=None,
|
||||
existing_comment='Platform description for admin/marketing purposes',
|
||||
existing_nullable=True)
|
||||
op.alter_column('platforms', 'name',
|
||||
existing_type=sa.VARCHAR(length=100),
|
||||
comment=None,
|
||||
existing_comment="Display name (e.g., 'Wizamart OMS')",
|
||||
existing_nullable=False)
|
||||
op.alter_column('platforms', 'code',
|
||||
existing_type=sa.VARCHAR(length=50),
|
||||
comment=None,
|
||||
existing_comment="Unique platform identifier (e.g., 'oms', 'loyalty', 'sites')",
|
||||
existing_nullable=False)
|
||||
op.drop_index(op.f('ix_platform_modules_id'), table_name='platform_modules')
|
||||
op.alter_column('platform_modules', 'updated_at',
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('platform_modules', 'created_at',
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('content_pages', 'is_platform_page',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
comment=None,
|
||||
existing_comment='True = platform marketing page (homepage, pricing); False = store default or override',
|
||||
existing_nullable=False,
|
||||
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_nullable=True)
|
||||
op.alter_column('content_pages', 'platform_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
comment=None,
|
||||
existing_comment='Platform this page belongs to',
|
||||
existing_nullable=False)
|
||||
op.drop_index(op.f('ix_admin_platforms_user_id'), table_name='admin_platforms')
|
||||
op.drop_index(op.f('ix_admin_platforms_platform_id'), table_name='admin_platforms')
|
||||
op.drop_index(op.f('ix_admin_platforms_id'), table_name='admin_platforms')
|
||||
op.create_index('idx_admin_platforms_user_id', 'admin_platforms', ['user_id'], unique=False)
|
||||
op.create_index('idx_admin_platforms_platform_id', 'admin_platforms', ['platform_id'], unique=False)
|
||||
op.alter_column('admin_platforms', 'updated_at',
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('admin_platforms', 'created_at',
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.drop_index(op.f('ix_admin_menu_configs_user_id'), table_name='admin_menu_configs')
|
||||
op.drop_index(op.f('ix_admin_menu_configs_platform_id'), table_name='admin_menu_configs')
|
||||
op.drop_index(op.f('ix_admin_menu_configs_menu_item_id'), table_name='admin_menu_configs')
|
||||
op.drop_index(op.f('ix_admin_menu_configs_id'), table_name='admin_menu_configs')
|
||||
op.drop_index(op.f('ix_admin_menu_configs_frontend_type'), table_name='admin_menu_configs')
|
||||
op.create_index('idx_admin_menu_configs_user_id', 'admin_menu_configs', ['user_id'], unique=False)
|
||||
op.create_index('idx_admin_menu_configs_platform_id', 'admin_menu_configs', ['platform_id'], unique=False)
|
||||
op.create_index('idx_admin_menu_configs_menu_item_id', 'admin_menu_configs', ['menu_item_id'], unique=False)
|
||||
op.create_index('idx_admin_menu_configs_frontend_type', 'admin_menu_configs', ['frontend_type'], unique=False)
|
||||
op.alter_column('admin_menu_configs', 'updated_at',
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('admin_menu_configs', 'created_at',
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('admin_menu_configs', 'user_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
comment='User scope - applies to this specific super admin',
|
||||
existing_comment='User scope - applies to this specific super admin (admin frontend only)',
|
||||
existing_nullable=True)
|
||||
op.alter_column('admin_menu_configs', 'platform_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
comment='Platform scope - applies to all platform admins of this platform',
|
||||
existing_comment='Platform scope - applies to users/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')
|
||||
# ### end Alembic commands ###
|
||||
174
app/modules/loyalty/migrations/versions/loyalty_001_initial.py
Normal file
174
app/modules/loyalty/migrations/versions/loyalty_001_initial.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""loyalty initial - programs, cards, transactions, staff pins, apple devices, settings
|
||||
|
||||
Revision ID: loyalty_001
|
||||
Revises: messaging_001
|
||||
Create Date: 2026-02-07
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "loyalty_001"
|
||||
down_revision = "messaging_001"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# --- loyalty_programs ---
|
||||
op.create_table(
|
||||
"loyalty_programs",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("merchant_id", sa.Integer(), sa.ForeignKey("merchants.id", ondelete="CASCADE"), unique=True, nullable=False, index=True, comment="Merchant that owns this program (chain-wide)"),
|
||||
sa.Column("loyalty_type", sa.String(20), nullable=False, server_default="points"),
|
||||
sa.Column("stamps_target", sa.Integer(), nullable=False, server_default="10", comment="Number of stamps needed for reward"),
|
||||
sa.Column("stamps_reward_description", sa.String(255), nullable=False, server_default="Free item", 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, server_default="1", comment="Points earned per euro spent (1 euro = X points)"),
|
||||
sa.Column("points_rewards", sa.JSON(), nullable=False, comment="List of point rewards: [{id, name, points_required, description}]"),
|
||||
sa.Column("points_expiration_days", sa.Integer(), nullable=True, comment="Days of inactivity before points expire (None = never expire)"),
|
||||
sa.Column("welcome_bonus_points", sa.Integer(), nullable=False, server_default="0", comment="Bonus points awarded on enrollment"),
|
||||
sa.Column("minimum_redemption_points", sa.Integer(), nullable=False, server_default="100", comment="Minimum points required for any redemption"),
|
||||
sa.Column("minimum_purchase_cents", sa.Integer(), nullable=False, server_default="0", comment="Minimum purchase amount (cents) to earn points (0 = no minimum)"),
|
||||
sa.Column("tier_config", sa.JSON(), nullable=True, comment='Future: Tier thresholds {"bronze": 0, "silver": 1000, "gold": 5000}'),
|
||||
sa.Column("cooldown_minutes", sa.Integer(), nullable=False, server_default="15", comment="Minutes between stamps for same card"),
|
||||
sa.Column("max_daily_stamps", sa.Integer(), nullable=False, server_default="5", comment="Maximum stamps per card per day"),
|
||||
sa.Column("require_staff_pin", sa.Boolean(), nullable=False, server_default="true", comment="Require staff PIN for stamp/points operations"),
|
||||
sa.Column("card_name", sa.String(100), nullable=True, comment="Display name for loyalty card"),
|
||||
sa.Column("card_color", sa.String(7), nullable=False, server_default="#4F46E5", comment="Primary color for card (hex)"),
|
||||
sa.Column("card_secondary_color", sa.String(7), nullable=True, comment="Secondary color for card (hex)"),
|
||||
sa.Column("logo_url", sa.String(500), nullable=True, comment="URL to merchant logo for card"),
|
||||
sa.Column("hero_image_url", sa.String(500), nullable=True, comment="URL to hero image for card"),
|
||||
sa.Column("google_issuer_id", sa.String(100), nullable=True, comment="Google Wallet Issuer ID"),
|
||||
sa.Column("google_class_id", sa.String(200), nullable=True, comment="Google Wallet Loyalty Class ID"),
|
||||
sa.Column("apple_pass_type_id", sa.String(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(500), nullable=True, comment="URL to privacy policy"),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true", index=True),
|
||||
sa.Column("activated_at", sa.DateTime(timezone=True), nullable=True, comment="When program was first activated"),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_loyalty_program_merchant_active", "loyalty_programs", ["merchant_id", "is_active"])
|
||||
|
||||
# --- staff_pins ---
|
||||
op.create_table(
|
||||
"staff_pins",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("merchant_id", sa.Integer(), sa.ForeignKey("merchants.id", ondelete="CASCADE"), nullable=False, index=True, comment="Merchant that owns the loyalty program"),
|
||||
sa.Column("program_id", sa.Integer(), sa.ForeignKey("loyalty_programs.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id", ondelete="CASCADE"), nullable=False, index=True, comment="Store (location) where this staff member works"),
|
||||
sa.Column("name", sa.String(100), nullable=False, comment="Staff member name"),
|
||||
sa.Column("staff_id", sa.String(50), nullable=True, index=True, comment="Optional staff ID/employee number"),
|
||||
sa.Column("pin_hash", sa.String(255), nullable=False, comment="bcrypt hash of PIN"),
|
||||
sa.Column("failed_attempts", sa.Integer(), nullable=False, server_default="0", 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, server_default="true", index=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_staff_pin_merchant_active", "staff_pins", ["merchant_id", "is_active"])
|
||||
op.create_index("idx_staff_pin_store_active", "staff_pins", ["store_id", "is_active"])
|
||||
op.create_index("idx_staff_pin_program_active", "staff_pins", ["program_id", "is_active"])
|
||||
|
||||
# --- loyalty_cards ---
|
||||
op.create_table(
|
||||
"loyalty_cards",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("merchant_id", sa.Integer(), sa.ForeignKey("merchants.id", ondelete="CASCADE"), nullable=False, index=True, comment="Merchant whose program this card belongs to"),
|
||||
sa.Column("customer_id", sa.Integer(), sa.ForeignKey("customers.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("program_id", sa.Integer(), sa.ForeignKey("loyalty_programs.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("enrolled_at_store_id", sa.Integer(), sa.ForeignKey("stores.id", ondelete="SET NULL"), nullable=True, index=True, comment="Store where customer enrolled (for analytics)"),
|
||||
sa.Column("card_number", sa.String(20), unique=True, nullable=False, index=True, comment="Human-readable card number (XXXX-XXXX-XXXX)"),
|
||||
sa.Column("qr_code_data", sa.String(50), unique=True, nullable=False, index=True, comment="Data encoded in QR code for scanning"),
|
||||
sa.Column("stamp_count", sa.Integer(), nullable=False, server_default="0", comment="Current stamps toward next reward"),
|
||||
sa.Column("total_stamps_earned", sa.Integer(), nullable=False, server_default="0", comment="Lifetime stamps earned"),
|
||||
sa.Column("stamps_redeemed", sa.Integer(), nullable=False, server_default="0", comment="Total rewards redeemed (stamps reset on redemption)"),
|
||||
sa.Column("points_balance", sa.Integer(), nullable=False, server_default="0", comment="Current available points"),
|
||||
sa.Column("total_points_earned", sa.Integer(), nullable=False, server_default="0", comment="Lifetime points earned"),
|
||||
sa.Column("points_redeemed", sa.Integer(), nullable=False, server_default="0", comment="Lifetime points redeemed"),
|
||||
sa.Column("google_object_id", sa.String(200), nullable=True, index=True, comment="Google Wallet Loyalty Object ID"),
|
||||
sa.Column("google_object_jwt", sa.String(2000), nullable=True, comment="JWT for Google Wallet 'Add to Wallet' button"),
|
||||
sa.Column("apple_serial_number", sa.String(100), unique=True, nullable=True, index=True, comment="Apple Wallet pass serial number"),
|
||||
sa.Column("apple_auth_token", sa.String(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 (for expiration tracking)"),
|
||||
sa.Column("last_redemption_at", sa.DateTime(timezone=True), nullable=True, comment="Last reward redemption"),
|
||||
sa.Column("last_activity_at", sa.DateTime(timezone=True), nullable=True, comment="Any activity (for expiration calculation)"),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true", index=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_loyalty_card_merchant_customer", "loyalty_cards", ["merchant_id", "customer_id"], unique=True)
|
||||
op.create_index("idx_loyalty_card_merchant_active", "loyalty_cards", ["merchant_id", "is_active"])
|
||||
op.create_index("idx_loyalty_card_customer_program", "loyalty_cards", ["customer_id", "program_id"], unique=True)
|
||||
|
||||
# --- loyalty_transactions ---
|
||||
op.create_table(
|
||||
"loyalty_transactions",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("merchant_id", sa.Integer(), sa.ForeignKey("merchants.id", ondelete="CASCADE"), nullable=False, index=True, comment="Merchant that owns the loyalty program"),
|
||||
sa.Column("card_id", sa.Integer(), sa.ForeignKey("loyalty_cards.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id", ondelete="SET NULL"), nullable=True, index=True, comment="Store (location) that processed this transaction"),
|
||||
sa.Column("staff_pin_id", sa.Integer(), sa.ForeignKey("staff_pins.id", ondelete="SET NULL"), nullable=True, index=True, comment="Staff PIN used for this operation"),
|
||||
sa.Column("related_transaction_id", sa.Integer(), sa.ForeignKey("loyalty_transactions.id", ondelete="SET NULL"), nullable=True, index=True, comment="Original transaction (for voids/returns)"),
|
||||
sa.Column("transaction_type", sa.String(30), nullable=False, index=True),
|
||||
sa.Column("stamps_delta", sa.Integer(), nullable=False, server_default="0", comment="Change in stamps (+1 for earn, -N for redeem)"),
|
||||
sa.Column("points_delta", sa.Integer(), nullable=False, server_default="0", 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(100), nullable=True, index=True, comment="Reference to order that triggered points"),
|
||||
sa.Column("reward_id", sa.String(50), nullable=True, comment="ID of redeemed reward (from program.points_rewards)"),
|
||||
sa.Column("reward_description", sa.String(255), nullable=True, comment="Description of redeemed reward"),
|
||||
sa.Column("ip_address", sa.String(45), nullable=True, comment="IP address of requester (IPv4 or IPv6)"),
|
||||
sa.Column("user_agent", sa.String(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, index=True, comment="When the transaction occurred (may differ from created_at)"),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_loyalty_tx_card_type", "loyalty_transactions", ["card_id", "transaction_type"])
|
||||
op.create_index("idx_loyalty_tx_store_date", "loyalty_transactions", ["store_id", "transaction_at"])
|
||||
op.create_index("idx_loyalty_tx_type_date", "loyalty_transactions", ["transaction_type", "transaction_at"])
|
||||
op.create_index("idx_loyalty_tx_merchant_date", "loyalty_transactions", ["merchant_id", "transaction_at"])
|
||||
op.create_index("idx_loyalty_tx_merchant_store", "loyalty_transactions", ["merchant_id", "store_id"])
|
||||
|
||||
# --- apple_device_registrations ---
|
||||
op.create_table(
|
||||
"apple_device_registrations",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("card_id", sa.Integer(), sa.ForeignKey("loyalty_cards.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("device_library_identifier", sa.String(100), nullable=False, index=True, comment="Unique identifier for the device/library"),
|
||||
sa.Column("push_token", sa.String(100), nullable=False, comment="APNs push token for this device"),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_apple_device_card", "apple_device_registrations", ["device_library_identifier", "card_id"], unique=True)
|
||||
|
||||
# --- merchant_loyalty_settings ---
|
||||
op.create_table(
|
||||
"merchant_loyalty_settings",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("merchant_id", sa.Integer(), sa.ForeignKey("merchants.id", ondelete="CASCADE"), unique=True, nullable=False, index=True, comment="Merchant these settings apply to"),
|
||||
sa.Column("staff_pin_policy", sa.String(20), nullable=False, server_default="required", comment="Staff PIN policy: required, optional, disabled"),
|
||||
sa.Column("staff_pin_lockout_attempts", sa.Integer(), nullable=False, server_default="5", comment="Max failed PIN attempts before lockout"),
|
||||
sa.Column("staff_pin_lockout_minutes", sa.Integer(), nullable=False, server_default="30", comment="Lockout duration in minutes"),
|
||||
sa.Column("allow_self_enrollment", sa.Boolean(), nullable=False, server_default="true", comment="Allow customers to self-enroll via QR code"),
|
||||
sa.Column("allow_void_transactions", sa.Boolean(), nullable=False, server_default="true", comment="Allow voiding points for returns"),
|
||||
sa.Column("allow_cross_location_redemption", sa.Boolean(), nullable=False, server_default="true", comment="Allow redemption at any merchant location"),
|
||||
sa.Column("require_order_reference", sa.Boolean(), nullable=False, server_default="false", comment="Require order reference when earning points"),
|
||||
sa.Column("log_ip_addresses", sa.Boolean(), nullable=False, server_default="true", comment="Log IP addresses for transactions"),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_merchant_loyalty_settings_merchant", "merchant_loyalty_settings", ["merchant_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("merchant_loyalty_settings")
|
||||
op.drop_table("apple_device_registrations")
|
||||
op.drop_table("loyalty_transactions")
|
||||
op.drop_table("loyalty_cards")
|
||||
op.drop_table("staff_pins")
|
||||
op.drop_table("loyalty_programs")
|
||||
@@ -1,560 +0,0 @@
|
||||
"""Phase 2: migrate loyalty module to merchant-based architecture
|
||||
|
||||
Revision ID: loyalty_003_phase2
|
||||
Revises: 0fb5d6d6ff97
|
||||
Create Date: 2026-02-06 20:30:00.000000
|
||||
|
||||
Phase 2 changes:
|
||||
- loyalty_programs: store_id -> merchant_id (one program per merchant)
|
||||
- loyalty_cards: add merchant_id, rename store_id -> enrolled_at_store_id
|
||||
- loyalty_transactions: add merchant_id, add related_transaction_id, store_id nullable
|
||||
- staff_pins: add merchant_id
|
||||
- NEW TABLE: merchant_loyalty_settings
|
||||
- NEW COLUMNS on loyalty_programs: points_expiration_days, welcome_bonus_points,
|
||||
minimum_redemption_points, minimum_purchase_cents, tier_config
|
||||
- NEW COLUMN on loyalty_cards: last_activity_at
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
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
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# =========================================================================
|
||||
# 1. Create merchant_loyalty_settings table
|
||||
# =========================================================================
|
||||
op.create_table(
|
||||
"merchant_loyalty_settings",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("merchant_id", sa.Integer(), nullable=False),
|
||||
sa.Column(
|
||||
"staff_pin_policy",
|
||||
sa.String(length=20),
|
||||
nullable=False,
|
||||
server_default="required",
|
||||
),
|
||||
sa.Column(
|
||||
"staff_pin_lockout_attempts",
|
||||
sa.Integer(),
|
||||
nullable=False,
|
||||
server_default="5",
|
||||
),
|
||||
sa.Column(
|
||||
"staff_pin_lockout_minutes",
|
||||
sa.Integer(),
|
||||
nullable=False,
|
||||
server_default="30",
|
||||
),
|
||||
sa.Column(
|
||||
"allow_self_enrollment",
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default=sa.text("true"),
|
||||
),
|
||||
sa.Column(
|
||||
"allow_void_transactions",
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default=sa.text("true"),
|
||||
),
|
||||
sa.Column(
|
||||
"allow_cross_location_redemption",
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default=sa.text("true"),
|
||||
),
|
||||
sa.Column(
|
||||
"require_order_reference",
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default=sa.text("false"),
|
||||
),
|
||||
sa.Column(
|
||||
"log_ip_addresses",
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default=sa.text("true"),
|
||||
),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["merchant_id"], ["merchants.id"], ondelete="CASCADE"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_merchant_loyalty_settings_id"),
|
||||
"merchant_loyalty_settings",
|
||||
["id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_merchant_loyalty_settings_merchant_id"),
|
||||
"merchant_loyalty_settings",
|
||||
["merchant_id"],
|
||||
unique=True,
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# 2. Modify loyalty_programs: store_id -> merchant_id + new columns
|
||||
# =========================================================================
|
||||
|
||||
# Add merchant_id (nullable first for data migration)
|
||||
op.add_column(
|
||||
"loyalty_programs", sa.Column("merchant_id", sa.Integer(), nullable=True)
|
||||
)
|
||||
|
||||
# Migrate existing data: derive merchant_id from store_id
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE loyalty_programs lp
|
||||
SET merchant_id = v.merchant_id
|
||||
FROM stores v
|
||||
WHERE v.id = lp.store_id
|
||||
"""
|
||||
)
|
||||
|
||||
# Make merchant_id non-nullable
|
||||
op.alter_column("loyalty_programs", "merchant_id", nullable=False)
|
||||
|
||||
# Add FK and indexes
|
||||
op.create_foreign_key(
|
||||
"fk_loyalty_programs_merchant_id",
|
||||
"loyalty_programs",
|
||||
"merchants",
|
||||
["merchant_id"],
|
||||
["id"],
|
||||
ondelete="CASCADE",
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_loyalty_programs_merchant_id"),
|
||||
"loyalty_programs",
|
||||
["merchant_id"],
|
||||
unique=True,
|
||||
)
|
||||
op.create_index(
|
||||
"idx_loyalty_program_merchant_active",
|
||||
"loyalty_programs",
|
||||
["merchant_id", "is_active"],
|
||||
)
|
||||
|
||||
# Add new Phase 2 columns
|
||||
op.add_column(
|
||||
"loyalty_programs",
|
||||
sa.Column("points_expiration_days", sa.Integer(), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"loyalty_programs",
|
||||
sa.Column(
|
||||
"welcome_bonus_points",
|
||||
sa.Integer(),
|
||||
nullable=False,
|
||||
server_default="0",
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
"loyalty_programs",
|
||||
sa.Column(
|
||||
"minimum_redemption_points",
|
||||
sa.Integer(),
|
||||
nullable=False,
|
||||
server_default="100",
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
"loyalty_programs",
|
||||
sa.Column(
|
||||
"minimum_purchase_cents",
|
||||
sa.Integer(),
|
||||
nullable=False,
|
||||
server_default="0",
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
"loyalty_programs",
|
||||
sa.Column("tier_config", sa.JSON(), nullable=True),
|
||||
)
|
||||
|
||||
# Drop old store_id column and indexes
|
||||
op.drop_index("idx_loyalty_program_store_active", table_name="loyalty_programs")
|
||||
op.drop_index(
|
||||
op.f("ix_loyalty_programs_store_id"), table_name="loyalty_programs"
|
||||
)
|
||||
op.drop_constraint(
|
||||
"loyalty_programs_store_id_fkey", "loyalty_programs", type_="foreignkey"
|
||||
)
|
||||
op.drop_column("loyalty_programs", "store_id")
|
||||
|
||||
# =========================================================================
|
||||
# 3. Modify loyalty_cards: add merchant_id, rename store_id
|
||||
# =========================================================================
|
||||
|
||||
# Add merchant_id
|
||||
op.add_column(
|
||||
"loyalty_cards", sa.Column("merchant_id", sa.Integer(), nullable=True)
|
||||
)
|
||||
|
||||
# Migrate data
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE loyalty_cards lc
|
||||
SET merchant_id = v.merchant_id
|
||||
FROM stores v
|
||||
WHERE v.id = lc.store_id
|
||||
"""
|
||||
)
|
||||
|
||||
op.alter_column("loyalty_cards", "merchant_id", nullable=False)
|
||||
|
||||
op.create_foreign_key(
|
||||
"fk_loyalty_cards_merchant_id",
|
||||
"loyalty_cards",
|
||||
"merchants",
|
||||
["merchant_id"],
|
||||
["id"],
|
||||
ondelete="CASCADE",
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_loyalty_cards_merchant_id"),
|
||||
"loyalty_cards",
|
||||
["merchant_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"idx_loyalty_card_merchant_active",
|
||||
"loyalty_cards",
|
||||
["merchant_id", "is_active"],
|
||||
)
|
||||
op.create_index(
|
||||
"idx_loyalty_card_merchant_customer",
|
||||
"loyalty_cards",
|
||||
["merchant_id", "customer_id"],
|
||||
unique=True,
|
||||
)
|
||||
|
||||
# Rename store_id -> enrolled_at_store_id, make nullable, change FK
|
||||
op.drop_index("idx_loyalty_card_store_active", table_name="loyalty_cards")
|
||||
op.drop_index(op.f("ix_loyalty_cards_store_id"), table_name="loyalty_cards")
|
||||
op.drop_constraint(
|
||||
"loyalty_cards_store_id_fkey", "loyalty_cards", type_="foreignkey"
|
||||
)
|
||||
op.alter_column(
|
||||
"loyalty_cards",
|
||||
"store_id",
|
||||
new_column_name="enrolled_at_store_id",
|
||||
nullable=True,
|
||||
)
|
||||
op.create_foreign_key(
|
||||
"fk_loyalty_cards_enrolled_store",
|
||||
"loyalty_cards",
|
||||
"stores",
|
||||
["enrolled_at_store_id"],
|
||||
["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_loyalty_cards_enrolled_at_store_id"),
|
||||
"loyalty_cards",
|
||||
["enrolled_at_store_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# Add last_activity_at
|
||||
op.add_column(
|
||||
"loyalty_cards",
|
||||
sa.Column("last_activity_at", sa.DateTime(timezone=True), nullable=True),
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# 4. Modify loyalty_transactions: add merchant_id, related_transaction_id
|
||||
# =========================================================================
|
||||
|
||||
# Add merchant_id
|
||||
op.add_column(
|
||||
"loyalty_transactions",
|
||||
sa.Column("merchant_id", sa.Integer(), nullable=True),
|
||||
)
|
||||
|
||||
# Migrate data (from card's merchant)
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE loyalty_transactions lt
|
||||
SET merchant_id = lc.merchant_id
|
||||
FROM loyalty_cards lc
|
||||
WHERE lc.id = lt.card_id
|
||||
"""
|
||||
)
|
||||
|
||||
op.alter_column("loyalty_transactions", "merchant_id", nullable=False)
|
||||
|
||||
op.create_foreign_key(
|
||||
"fk_loyalty_transactions_merchant_id",
|
||||
"loyalty_transactions",
|
||||
"merchants",
|
||||
["merchant_id"],
|
||||
["id"],
|
||||
ondelete="CASCADE",
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_loyalty_transactions_merchant_id"),
|
||||
"loyalty_transactions",
|
||||
["merchant_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"idx_loyalty_tx_merchant_date",
|
||||
"loyalty_transactions",
|
||||
["merchant_id", "transaction_at"],
|
||||
)
|
||||
op.create_index(
|
||||
"idx_loyalty_tx_merchant_store",
|
||||
"loyalty_transactions",
|
||||
["merchant_id", "store_id"],
|
||||
)
|
||||
|
||||
# Make store_id nullable and change FK to SET NULL
|
||||
op.drop_constraint(
|
||||
"loyalty_transactions_store_id_fkey",
|
||||
"loyalty_transactions",
|
||||
type_="foreignkey",
|
||||
)
|
||||
op.alter_column("loyalty_transactions", "store_id", nullable=True)
|
||||
op.create_foreign_key(
|
||||
"fk_loyalty_transactions_store_id",
|
||||
"loyalty_transactions",
|
||||
"stores",
|
||||
["store_id"],
|
||||
["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
|
||||
# Add related_transaction_id (for void linkage)
|
||||
op.add_column(
|
||||
"loyalty_transactions",
|
||||
sa.Column("related_transaction_id", sa.Integer(), nullable=True),
|
||||
)
|
||||
op.create_foreign_key(
|
||||
"fk_loyalty_tx_related",
|
||||
"loyalty_transactions",
|
||||
"loyalty_transactions",
|
||||
["related_transaction_id"],
|
||||
["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_loyalty_transactions_related_transaction_id"),
|
||||
"loyalty_transactions",
|
||||
["related_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# 5. Modify staff_pins: add merchant_id
|
||||
# =========================================================================
|
||||
|
||||
op.add_column(
|
||||
"staff_pins", sa.Column("merchant_id", sa.Integer(), nullable=True)
|
||||
)
|
||||
|
||||
# Migrate data (from store's merchant)
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE staff_pins sp
|
||||
SET merchant_id = v.merchant_id
|
||||
FROM stores v
|
||||
WHERE v.id = sp.store_id
|
||||
"""
|
||||
)
|
||||
|
||||
op.alter_column("staff_pins", "merchant_id", nullable=False)
|
||||
|
||||
op.create_foreign_key(
|
||||
"fk_staff_pins_merchant_id",
|
||||
"staff_pins",
|
||||
"merchants",
|
||||
["merchant_id"],
|
||||
["id"],
|
||||
ondelete="CASCADE",
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_staff_pins_merchant_id"),
|
||||
"staff_pins",
|
||||
["merchant_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"idx_staff_pin_merchant_active",
|
||||
"staff_pins",
|
||||
["merchant_id", "is_active"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# =========================================================================
|
||||
# 5. Revert staff_pins
|
||||
# =========================================================================
|
||||
op.drop_index("idx_staff_pin_merchant_active", table_name="staff_pins")
|
||||
op.drop_index(op.f("ix_staff_pins_merchant_id"), table_name="staff_pins")
|
||||
op.drop_constraint("fk_staff_pins_merchant_id", "staff_pins", type_="foreignkey")
|
||||
op.drop_column("staff_pins", "merchant_id")
|
||||
|
||||
# =========================================================================
|
||||
# 4. Revert loyalty_transactions
|
||||
# =========================================================================
|
||||
op.drop_index(
|
||||
op.f("ix_loyalty_transactions_related_transaction_id"),
|
||||
table_name="loyalty_transactions",
|
||||
)
|
||||
op.drop_constraint(
|
||||
"fk_loyalty_tx_related", "loyalty_transactions", type_="foreignkey"
|
||||
)
|
||||
op.drop_column("loyalty_transactions", "related_transaction_id")
|
||||
|
||||
op.drop_constraint(
|
||||
"fk_loyalty_transactions_store_id",
|
||||
"loyalty_transactions",
|
||||
type_="foreignkey",
|
||||
)
|
||||
op.alter_column("loyalty_transactions", "store_id", nullable=False)
|
||||
op.create_foreign_key(
|
||||
"loyalty_transactions_store_id_fkey",
|
||||
"loyalty_transactions",
|
||||
"stores",
|
||||
["store_id"],
|
||||
["id"],
|
||||
ondelete="CASCADE",
|
||||
)
|
||||
|
||||
op.drop_index(
|
||||
"idx_loyalty_tx_merchant_store", table_name="loyalty_transactions"
|
||||
)
|
||||
op.drop_index(
|
||||
"idx_loyalty_tx_merchant_date", table_name="loyalty_transactions"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_loyalty_transactions_merchant_id"),
|
||||
table_name="loyalty_transactions",
|
||||
)
|
||||
op.drop_constraint(
|
||||
"fk_loyalty_transactions_merchant_id",
|
||||
"loyalty_transactions",
|
||||
type_="foreignkey",
|
||||
)
|
||||
op.drop_column("loyalty_transactions", "merchant_id")
|
||||
|
||||
# =========================================================================
|
||||
# 3. Revert loyalty_cards
|
||||
# =========================================================================
|
||||
op.drop_column("loyalty_cards", "last_activity_at")
|
||||
|
||||
op.drop_index(
|
||||
op.f("ix_loyalty_cards_enrolled_at_store_id"), table_name="loyalty_cards"
|
||||
)
|
||||
op.drop_constraint(
|
||||
"fk_loyalty_cards_enrolled_store", "loyalty_cards", type_="foreignkey"
|
||||
)
|
||||
op.alter_column(
|
||||
"loyalty_cards",
|
||||
"enrolled_at_store_id",
|
||||
new_column_name="store_id",
|
||||
nullable=False,
|
||||
)
|
||||
op.create_foreign_key(
|
||||
"loyalty_cards_store_id_fkey",
|
||||
"loyalty_cards",
|
||||
"stores",
|
||||
["store_id"],
|
||||
["id"],
|
||||
ondelete="CASCADE",
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_loyalty_cards_store_id"),
|
||||
"loyalty_cards",
|
||||
["store_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"idx_loyalty_card_store_active",
|
||||
"loyalty_cards",
|
||||
["store_id", "is_active"],
|
||||
)
|
||||
|
||||
op.drop_index(
|
||||
"idx_loyalty_card_merchant_customer", table_name="loyalty_cards"
|
||||
)
|
||||
op.drop_index(
|
||||
"idx_loyalty_card_merchant_active", table_name="loyalty_cards"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_loyalty_cards_merchant_id"), table_name="loyalty_cards"
|
||||
)
|
||||
op.drop_constraint(
|
||||
"fk_loyalty_cards_merchant_id", "loyalty_cards", type_="foreignkey"
|
||||
)
|
||||
op.drop_column("loyalty_cards", "merchant_id")
|
||||
|
||||
# =========================================================================
|
||||
# 2. Revert loyalty_programs
|
||||
# =========================================================================
|
||||
op.add_column(
|
||||
"loyalty_programs",
|
||||
sa.Column("store_id", sa.Integer(), nullable=True),
|
||||
)
|
||||
# Note: data migration back not possible if merchant had multiple stores
|
||||
op.create_foreign_key(
|
||||
"loyalty_programs_store_id_fkey",
|
||||
"loyalty_programs",
|
||||
"stores",
|
||||
["store_id"],
|
||||
["id"],
|
||||
ondelete="CASCADE",
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_loyalty_programs_store_id"),
|
||||
"loyalty_programs",
|
||||
["store_id"],
|
||||
unique=True,
|
||||
)
|
||||
op.create_index(
|
||||
"idx_loyalty_program_store_active",
|
||||
"loyalty_programs",
|
||||
["store_id", "is_active"],
|
||||
)
|
||||
|
||||
op.drop_column("loyalty_programs", "tier_config")
|
||||
op.drop_column("loyalty_programs", "minimum_purchase_cents")
|
||||
op.drop_column("loyalty_programs", "minimum_redemption_points")
|
||||
op.drop_column("loyalty_programs", "welcome_bonus_points")
|
||||
op.drop_column("loyalty_programs", "points_expiration_days")
|
||||
|
||||
op.drop_index(
|
||||
"idx_loyalty_program_merchant_active", table_name="loyalty_programs"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_loyalty_programs_merchant_id"), table_name="loyalty_programs"
|
||||
)
|
||||
op.drop_constraint(
|
||||
"fk_loyalty_programs_merchant_id", "loyalty_programs", type_="foreignkey"
|
||||
)
|
||||
op.drop_column("loyalty_programs", "merchant_id")
|
||||
|
||||
# =========================================================================
|
||||
# 1. Drop merchant_loyalty_settings table
|
||||
# =========================================================================
|
||||
op.drop_index(
|
||||
op.f("ix_merchant_loyalty_settings_merchant_id"),
|
||||
table_name="merchant_loyalty_settings",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_merchant_loyalty_settings_id"),
|
||||
table_name="merchant_loyalty_settings",
|
||||
)
|
||||
op.drop_table("merchant_loyalty_settings")
|
||||
@@ -156,6 +156,7 @@ marketplace_module = ModuleDefinition(
|
||||
schemas_path="app.modules.marketplace.schemas",
|
||||
exceptions_path="app.modules.marketplace.exceptions",
|
||||
tasks_path="app.modules.marketplace.tasks",
|
||||
migrations_path="migrations",
|
||||
# =========================================================================
|
||||
# Scheduled Tasks
|
||||
# =========================================================================
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
"""marketplace initial - marketplace products, imports, letzshop integration
|
||||
|
||||
Revision ID: marketplace_001
|
||||
Revises: billing_001
|
||||
Create Date: 2026-02-07
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "marketplace_001"
|
||||
down_revision = "billing_001"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# --- marketplace_products ---
|
||||
op.create_table(
|
||||
"marketplace_products",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("marketplace_product_id", sa.String(), unique=True, nullable=False, index=True),
|
||||
sa.Column("gtin", sa.String(), nullable=True, index=True),
|
||||
sa.Column("mpn", sa.String(), nullable=True, index=True),
|
||||
sa.Column("sku", sa.String(), nullable=True, index=True),
|
||||
sa.Column("marketplace", sa.String(), nullable=True, server_default="letzshop", index=True),
|
||||
sa.Column("source_url", sa.String(), nullable=True),
|
||||
sa.Column("store_name", sa.String(), nullable=True, index=True),
|
||||
sa.Column("product_type_enum", sa.String(20), nullable=False, server_default="physical"),
|
||||
sa.Column("is_digital", sa.Boolean(), nullable=True, server_default="false", index=True),
|
||||
sa.Column("digital_delivery_method", sa.String(20), nullable=True),
|
||||
sa.Column("platform", sa.String(50), nullable=True, index=True),
|
||||
sa.Column("region_restrictions", sa.JSON(), nullable=True),
|
||||
sa.Column("license_type", sa.String(50), nullable=True),
|
||||
sa.Column("brand", sa.String(), nullable=True, index=True),
|
||||
sa.Column("google_product_category", sa.String(), nullable=True, index=True),
|
||||
sa.Column("category_path", sa.String(), nullable=True),
|
||||
sa.Column("condition", sa.String(), nullable=True),
|
||||
sa.Column("price", sa.String(), nullable=True),
|
||||
sa.Column("price_cents", sa.Integer(), nullable=True),
|
||||
sa.Column("sale_price", sa.String(), nullable=True),
|
||||
sa.Column("sale_price_cents", sa.Integer(), nullable=True),
|
||||
sa.Column("currency", sa.String(3), nullable=True, server_default="EUR"),
|
||||
sa.Column("tax_rate_percent", sa.Integer(), nullable=False, server_default="17"),
|
||||
sa.Column("image_link", sa.String(), nullable=True),
|
||||
sa.Column("additional_image_link", sa.String(), nullable=True),
|
||||
sa.Column("additional_images", sa.JSON(), nullable=True),
|
||||
sa.Column("attributes", sa.JSON(), nullable=True),
|
||||
sa.Column("weight_grams", sa.Integer(), nullable=True),
|
||||
sa.Column("weight_unit", sa.String(10), nullable=True, server_default="kg"),
|
||||
sa.Column("dimensions", sa.JSON(), nullable=True),
|
||||
sa.Column("link", sa.String(), nullable=True),
|
||||
sa.Column("availability", sa.String(), nullable=True, index=True),
|
||||
sa.Column("adult", sa.String(), nullable=True),
|
||||
sa.Column("multipack", sa.Integer(), nullable=True),
|
||||
sa.Column("is_bundle", sa.String(), nullable=True),
|
||||
sa.Column("age_group", sa.String(), nullable=True),
|
||||
sa.Column("color", sa.String(), nullable=True),
|
||||
sa.Column("gender", sa.String(), nullable=True),
|
||||
sa.Column("material", sa.String(), nullable=True),
|
||||
sa.Column("pattern", sa.String(), nullable=True),
|
||||
sa.Column("size", sa.String(), nullable=True),
|
||||
sa.Column("size_type", sa.String(), nullable=True),
|
||||
sa.Column("size_system", sa.String(), nullable=True),
|
||||
sa.Column("item_group_id", sa.String(), nullable=True),
|
||||
sa.Column("product_type_raw", sa.String(), nullable=True),
|
||||
sa.Column("custom_label_0", sa.String(), nullable=True),
|
||||
sa.Column("custom_label_1", sa.String(), nullable=True),
|
||||
sa.Column("custom_label_2", sa.String(), nullable=True),
|
||||
sa.Column("custom_label_3", sa.String(), nullable=True),
|
||||
sa.Column("custom_label_4", sa.String(), nullable=True),
|
||||
sa.Column("unit_pricing_measure", sa.String(), nullable=True),
|
||||
sa.Column("unit_pricing_base_measure", sa.String(), nullable=True),
|
||||
sa.Column("identifier_exists", sa.String(), nullable=True),
|
||||
sa.Column("shipping", sa.String(), nullable=True),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=True, server_default="true", index=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_marketplace_store", "marketplace_products", ["marketplace", "store_name"])
|
||||
op.create_index("idx_marketplace_brand", "marketplace_products", ["marketplace", "brand"])
|
||||
op.create_index("idx_mp_gtin_marketplace", "marketplace_products", ["gtin", "marketplace"])
|
||||
op.create_index("idx_mp_product_type", "marketplace_products", ["product_type_enum", "is_digital"])
|
||||
|
||||
# --- marketplace_product_translations ---
|
||||
op.create_table(
|
||||
"marketplace_product_translations",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("marketplace_product_id", sa.Integer(), sa.ForeignKey("marketplace_products.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("language", sa.String(5), nullable=False),
|
||||
sa.Column("title", sa.String(), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("short_description", sa.String(500), nullable=True),
|
||||
sa.Column("meta_title", sa.String(70), nullable=True),
|
||||
sa.Column("meta_description", sa.String(160), nullable=True),
|
||||
sa.Column("url_slug", sa.String(255), nullable=True),
|
||||
sa.Column("source_import_id", sa.Integer(), nullable=True),
|
||||
sa.Column("source_file", sa.String(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.UniqueConstraint("marketplace_product_id", "language", name="uq_marketplace_product_translation"),
|
||||
)
|
||||
op.create_index("idx_mpt_marketplace_product_id", "marketplace_product_translations", ["marketplace_product_id"])
|
||||
op.create_index("idx_mpt_language", "marketplace_product_translations", ["language"])
|
||||
|
||||
# --- marketplace_import_jobs ---
|
||||
op.create_table(
|
||||
"marketplace_import_jobs",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=False, index=True),
|
||||
sa.Column("user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=False),
|
||||
sa.Column("marketplace", sa.String(), nullable=False, server_default="Letzshop", index=True),
|
||||
sa.Column("source_url", sa.String(), nullable=False),
|
||||
sa.Column("language", sa.String(5), nullable=False, server_default="en"),
|
||||
sa.Column("status", sa.String(), nullable=False, server_default="pending"),
|
||||
sa.Column("imported_count", sa.Integer(), nullable=True, server_default="0"),
|
||||
sa.Column("updated_count", sa.Integer(), nullable=True, server_default="0"),
|
||||
sa.Column("error_count", sa.Integer(), nullable=True, server_default="0"),
|
||||
sa.Column("total_processed", sa.Integer(), nullable=True, server_default="0"),
|
||||
sa.Column("error_message", sa.Text(), nullable=True),
|
||||
sa.Column("celery_task_id", sa.String(255), nullable=True, index=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(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_import_store_status", "marketplace_import_jobs", ["store_id", "status"])
|
||||
op.create_index("idx_import_store_created", "marketplace_import_jobs", ["store_id", "created_at"])
|
||||
op.create_index("idx_import_user_marketplace", "marketplace_import_jobs", ["user_id", "marketplace"])
|
||||
|
||||
# --- marketplace_import_errors ---
|
||||
op.create_table(
|
||||
"marketplace_import_errors",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("import_job_id", sa.Integer(), sa.ForeignKey("marketplace_import_jobs.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("row_number", sa.Integer(), nullable=False),
|
||||
sa.Column("identifier", sa.String(), nullable=True),
|
||||
sa.Column("error_type", sa.String(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(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_import_error_job_id", "marketplace_import_errors", ["import_job_id"])
|
||||
op.create_index("idx_import_error_type", "marketplace_import_errors", ["error_type"])
|
||||
|
||||
# --- store_letzshop_credentials ---
|
||||
op.create_table(
|
||||
"store_letzshop_credentials",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), unique=True, nullable=False, index=True),
|
||||
sa.Column("api_key_encrypted", sa.Text(), nullable=False),
|
||||
sa.Column("api_endpoint", sa.String(255), nullable=True, server_default="https://letzshop.lu/graphql"),
|
||||
sa.Column("auto_sync_enabled", sa.Boolean(), nullable=True, server_default="false"),
|
||||
sa.Column("sync_interval_minutes", sa.Integer(), nullable=True, server_default="15"),
|
||||
sa.Column("test_mode_enabled", sa.Boolean(), nullable=True, server_default="false"),
|
||||
sa.Column("default_carrier", sa.String(50), nullable=True),
|
||||
sa.Column("carrier_greco_label_url", sa.String(500), nullable=True, server_default="https://dispatchweb.fr/Tracky/Home/"),
|
||||
sa.Column("carrier_colissimo_label_url", sa.String(500), nullable=True),
|
||||
sa.Column("carrier_xpresslogistics_label_url", sa.String(500), nullable=True),
|
||||
sa.Column("last_sync_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("last_sync_status", sa.String(50), nullable=True),
|
||||
sa.Column("last_sync_error", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
|
||||
# --- letzshop_sync_logs ---
|
||||
op.create_table(
|
||||
"letzshop_sync_logs",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=False, index=True),
|
||||
sa.Column("operation_type", sa.String(50), nullable=False),
|
||||
sa.Column("direction", sa.String(10), nullable=False),
|
||||
sa.Column("status", sa.String(50), nullable=False),
|
||||
sa.Column("records_processed", sa.Integer(), nullable=True, server_default="0"),
|
||||
sa.Column("records_succeeded", sa.Integer(), nullable=True, server_default="0"),
|
||||
sa.Column("records_failed", sa.Integer(), nullable=True, server_default="0"),
|
||||
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(100), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_sync_log_store_type", "letzshop_sync_logs", ["store_id", "operation_type"])
|
||||
op.create_index("idx_sync_log_store_date", "letzshop_sync_logs", ["store_id", "started_at"])
|
||||
|
||||
# --- letzshop_store_cache ---
|
||||
op.create_table(
|
||||
"letzshop_store_cache",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("letzshop_id", sa.String(50), unique=True, nullable=False, index=True),
|
||||
sa.Column("slug", sa.String(200), unique=True, nullable=False, index=True),
|
||||
sa.Column("name", sa.String(255), nullable=False),
|
||||
sa.Column("merchant_name", sa.String(255), nullable=True),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=True, server_default="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(255), nullable=True),
|
||||
sa.Column("phone", sa.String(50), nullable=True),
|
||||
sa.Column("fax", sa.String(50), nullable=True),
|
||||
sa.Column("website", sa.String(500), nullable=True),
|
||||
sa.Column("street", sa.String(255), nullable=True),
|
||||
sa.Column("street_number", sa.String(50), nullable=True),
|
||||
sa.Column("city", sa.String(100), nullable=True),
|
||||
sa.Column("zipcode", sa.String(20), nullable=True),
|
||||
sa.Column("country_iso", sa.String(5), nullable=True, server_default="LU"),
|
||||
sa.Column("latitude", sa.String(20), nullable=True),
|
||||
sa.Column("longitude", sa.String(20), nullable=True),
|
||||
sa.Column("categories", sa.JSON(), nullable=True),
|
||||
sa.Column("background_image_url", sa.String(500), nullable=True),
|
||||
sa.Column("social_media_links", sa.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(255), nullable=True),
|
||||
sa.Column("representative_title", sa.String(100), nullable=True),
|
||||
sa.Column("claimed_by_store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=True, index=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", sa.JSON(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_letzshop_cache_city", "letzshop_store_cache", ["city"])
|
||||
op.create_index("idx_letzshop_cache_claimed", "letzshop_store_cache", ["claimed_by_store_id"])
|
||||
op.create_index("idx_letzshop_cache_active", "letzshop_store_cache", ["is_active"])
|
||||
|
||||
# --- letzshop_historical_import_jobs ---
|
||||
op.create_table(
|
||||
"letzshop_historical_import_jobs",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=False, index=True),
|
||||
sa.Column("user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=False),
|
||||
sa.Column("status", sa.String(50), nullable=False, server_default="pending"),
|
||||
sa.Column("current_phase", sa.String(20), nullable=True),
|
||||
sa.Column("current_page", sa.Integer(), nullable=True, server_default="0"),
|
||||
sa.Column("total_pages", sa.Integer(), nullable=True),
|
||||
sa.Column("shipments_fetched", sa.Integer(), nullable=True, server_default="0"),
|
||||
sa.Column("orders_processed", sa.Integer(), nullable=True, server_default="0"),
|
||||
sa.Column("orders_imported", sa.Integer(), nullable=True, server_default="0"),
|
||||
sa.Column("orders_updated", sa.Integer(), nullable=True, server_default="0"),
|
||||
sa.Column("orders_skipped", sa.Integer(), nullable=True, server_default="0"),
|
||||
sa.Column("products_matched", sa.Integer(), nullable=True, server_default="0"),
|
||||
sa.Column("products_not_found", sa.Integer(), nullable=True, server_default="0"),
|
||||
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("celery_task_id", sa.String(255), nullable=True, index=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(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_historical_import_store", "letzshop_historical_import_jobs", ["store_id", "status"])
|
||||
|
||||
# --- store_onboarding ---
|
||||
op.create_table(
|
||||
"store_onboarding",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id", ondelete="CASCADE"), unique=True, nullable=False, index=True),
|
||||
sa.Column("status", sa.String(20), nullable=False, server_default="not_started", index=True),
|
||||
sa.Column("current_step", sa.String(30), nullable=False, server_default="merchant_profile"),
|
||||
sa.Column("step_merchant_profile_completed", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("step_merchant_profile_completed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("step_merchant_profile_data", sa.JSON(), nullable=True),
|
||||
sa.Column("step_letzshop_api_completed", sa.Boolean(), nullable=False, server_default="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="false"),
|
||||
sa.Column("step_product_import_completed", sa.Boolean(), nullable=False, server_default="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="false"),
|
||||
sa.Column("step_order_sync_completed", sa.Boolean(), nullable=False, server_default="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("started_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("skipped_by_admin", sa.Boolean(), nullable=False, server_default="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(), sa.ForeignKey("users.id"), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_onboarding_store_status", "store_onboarding", ["store_id", "status"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("store_onboarding")
|
||||
op.drop_table("letzshop_historical_import_jobs")
|
||||
op.drop_table("letzshop_store_cache")
|
||||
op.drop_table("letzshop_sync_logs")
|
||||
op.drop_table("store_letzshop_credentials")
|
||||
op.drop_table("marketplace_import_errors")
|
||||
op.drop_table("marketplace_import_jobs")
|
||||
op.drop_table("marketplace_product_translations")
|
||||
op.drop_table("marketplace_products")
|
||||
@@ -263,9 +263,9 @@ class LetzshopStoreCache(Base, TimestampMixin):
|
||||
claimed_store = relationship("Store", foreign_keys=[claimed_by_store_id])
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_vendor_cache_city", "city"),
|
||||
Index("idx_vendor_cache_claimed", "claimed_by_store_id"),
|
||||
Index("idx_vendor_cache_active", "is_active"),
|
||||
Index("idx_letzshop_cache_city", "city"),
|
||||
Index("idx_letzshop_cache_claimed", "claimed_by_store_id"),
|
||||
Index("idx_letzshop_cache_active", "is_active"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
@@ -176,6 +176,7 @@ messaging_module = ModuleDefinition(
|
||||
models_path="app.modules.messaging.models",
|
||||
schemas_path="app.modules.messaging.schemas",
|
||||
exceptions_path="app.modules.messaging.exceptions",
|
||||
migrations_path="migrations",
|
||||
# Feature provider for feature flags
|
||||
feature_provider=_get_feature_provider,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
"""messaging initial - email settings, templates, logs, conversations, notifications
|
||||
|
||||
Revision ID: messaging_001
|
||||
Revises: cart_001
|
||||
Create Date: 2026-02-07
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "messaging_001"
|
||||
down_revision = "cart_001"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# --- store_email_settings ---
|
||||
op.create_table(
|
||||
"store_email_settings",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id", ondelete="CASCADE"), unique=True, nullable=False, index=True),
|
||||
sa.Column("from_email", sa.String(255), nullable=False),
|
||||
sa.Column("from_name", sa.String(100), nullable=False),
|
||||
sa.Column("reply_to_email", sa.String(255), nullable=True),
|
||||
sa.Column("signature_text", sa.Text(), nullable=True),
|
||||
sa.Column("signature_html", sa.Text(), nullable=True),
|
||||
sa.Column("provider", sa.String(20), nullable=False, server_default="smtp"),
|
||||
sa.Column("smtp_host", sa.String(255), nullable=True),
|
||||
sa.Column("smtp_port", sa.Integer(), nullable=True, server_default="587"),
|
||||
sa.Column("smtp_username", sa.String(255), nullable=True),
|
||||
sa.Column("smtp_password", sa.String(500), nullable=True),
|
||||
sa.Column("smtp_use_tls", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("smtp_use_ssl", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("sendgrid_api_key", sa.String(500), nullable=True),
|
||||
sa.Column("mailgun_api_key", sa.String(500), nullable=True),
|
||||
sa.Column("mailgun_domain", sa.String(255), nullable=True),
|
||||
sa.Column("ses_access_key_id", sa.String(100), nullable=True),
|
||||
sa.Column("ses_secret_access_key", sa.String(500), nullable=True),
|
||||
sa.Column("ses_region", sa.String(50), nullable=True, server_default="eu-west-1"),
|
||||
sa.Column("is_configured", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("is_verified", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("last_verified_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("verification_error", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_store_email_settings_configured", "store_email_settings", ["store_id", "is_configured"])
|
||||
|
||||
# --- store_email_templates ---
|
||||
op.create_table(
|
||||
"store_email_templates",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("template_code", sa.String(100), nullable=False, index=True),
|
||||
sa.Column("language", sa.String(5), nullable=False, server_default="en"),
|
||||
sa.Column("name", sa.String(255), nullable=True),
|
||||
sa.Column("subject", sa.String(500), nullable=False),
|
||||
sa.Column("body_html", sa.Text(), nullable=False),
|
||||
sa.Column("body_text", sa.Text(), nullable=True),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.UniqueConstraint("store_id", "template_code", "language", name="uq_store_email_template_code_language"),
|
||||
)
|
||||
|
||||
# --- email_templates (system/platform templates) ---
|
||||
op.create_table(
|
||||
"email_templates",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("code", sa.String(100), nullable=False, index=True),
|
||||
sa.Column("language", sa.String(5), nullable=False, server_default="en"),
|
||||
sa.Column("name", sa.String(255), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("category", sa.String(50), nullable=False, server_default="system", index=True),
|
||||
sa.Column("subject", sa.String(500), nullable=False),
|
||||
sa.Column("body_html", sa.Text(), nullable=False),
|
||||
sa.Column("body_text", sa.Text(), nullable=True),
|
||||
sa.Column("variables", sa.Text(), nullable=True),
|
||||
sa.Column("required_variables", sa.Text(), nullable=True),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("is_platform_only", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("ix_email_templates_code_language", "email_templates", ["code", "language"], unique=True)
|
||||
|
||||
# --- email_logs ---
|
||||
op.create_table(
|
||||
"email_logs",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("template_code", sa.String(100), nullable=True, index=True),
|
||||
sa.Column("template_id", sa.Integer(), sa.ForeignKey("email_templates.id"), nullable=True),
|
||||
sa.Column("recipient_email", sa.String(255), nullable=False, index=True),
|
||||
sa.Column("recipient_name", sa.String(255), nullable=True),
|
||||
sa.Column("subject", sa.String(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(255), nullable=False),
|
||||
sa.Column("from_name", sa.String(255), nullable=True),
|
||||
sa.Column("reply_to", sa.String(255), nullable=True),
|
||||
sa.Column("status", sa.String(20), nullable=False, server_default="pending", index=True),
|
||||
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, server_default="0"),
|
||||
sa.Column("provider", sa.String(50), nullable=True),
|
||||
sa.Column("provider_message_id", sa.String(255), nullable=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=True, index=True),
|
||||
sa.Column("user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=True, index=True),
|
||||
sa.Column("related_type", sa.String(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(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
|
||||
# --- conversations ---
|
||||
conversation_type_enum = sa.Enum(
|
||||
"admin_store", "store_customer", "admin_customer",
|
||||
name="conversationtype",
|
||||
)
|
||||
participant_type_enum = sa.Enum(
|
||||
"admin", "store", "customer",
|
||||
name="participanttype",
|
||||
)
|
||||
op.create_table(
|
||||
"conversations",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("conversation_type", conversation_type_enum, nullable=False, index=True),
|
||||
sa.Column("subject", sa.String(500), nullable=False),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=True, index=True),
|
||||
sa.Column("is_closed", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("closed_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("closed_by_type", participant_type_enum, nullable=True),
|
||||
sa.Column("closed_by_id", sa.Integer(), nullable=True),
|
||||
sa.Column("last_message_at", sa.DateTime(), nullable=True, index=True),
|
||||
sa.Column("message_count", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("ix_conversations_type_store", "conversations", ["conversation_type", "store_id"])
|
||||
|
||||
# --- conversation_participants ---
|
||||
op.create_table(
|
||||
"conversation_participants",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("conversation_id", sa.Integer(), sa.ForeignKey("conversations.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("participant_type", sa.Enum("admin", "store", "customer", name="participanttype", create_type=False), nullable=False),
|
||||
sa.Column("participant_id", sa.Integer(), nullable=False, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=True),
|
||||
sa.Column("unread_count", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("last_read_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("email_notifications", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("muted", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.UniqueConstraint("conversation_id", "participant_type", "participant_id", name="uq_conversation_participant"),
|
||||
)
|
||||
op.create_index("ix_participant_lookup", "conversation_participants", ["participant_type", "participant_id"])
|
||||
|
||||
# --- messages ---
|
||||
op.create_table(
|
||||
"messages",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("conversation_id", sa.Integer(), sa.ForeignKey("conversations.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("sender_type", sa.Enum("admin", "store", "customer", name="participanttype", create_type=False), nullable=False),
|
||||
sa.Column("sender_id", sa.Integer(), nullable=False, index=True),
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column("is_system_message", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("deleted_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("deleted_by_type", sa.Enum("admin", "store", "customer", name="participanttype", create_type=False), nullable=True),
|
||||
sa.Column("deleted_by_id", sa.Integer(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("ix_messages_conversation_created", "messages", ["conversation_id", "created_at"])
|
||||
|
||||
# --- message_attachments ---
|
||||
op.create_table(
|
||||
"message_attachments",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("message_id", sa.Integer(), sa.ForeignKey("messages.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("filename", sa.String(255), nullable=False),
|
||||
sa.Column("original_filename", sa.String(255), nullable=False),
|
||||
sa.Column("file_path", sa.String(1000), nullable=False),
|
||||
sa.Column("file_size", sa.Integer(), nullable=False),
|
||||
sa.Column("mime_type", sa.String(100), nullable=False),
|
||||
sa.Column("is_image", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("image_width", sa.Integer(), nullable=True),
|
||||
sa.Column("image_height", sa.Integer(), nullable=True),
|
||||
sa.Column("thumbnail_path", sa.String(1000), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
|
||||
# --- admin_notifications ---
|
||||
op.create_table(
|
||||
"admin_notifications",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("type", sa.String(50), nullable=False, index=True),
|
||||
sa.Column("priority", sa.String(20), nullable=True, server_default="normal", index=True),
|
||||
sa.Column("title", sa.String(200), nullable=False),
|
||||
sa.Column("message", sa.Text(), nullable=False),
|
||||
sa.Column("is_read", sa.Boolean(), nullable=True, server_default="false", index=True),
|
||||
sa.Column("read_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("read_by_user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=True),
|
||||
sa.Column("action_required", sa.Boolean(), nullable=True, server_default="false", index=True),
|
||||
sa.Column("action_url", sa.String(500), nullable=True),
|
||||
sa.Column("notification_metadata", sa.JSON(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("admin_notifications")
|
||||
op.drop_table("message_attachments")
|
||||
op.drop_table("messages")
|
||||
op.drop_table("conversation_participants")
|
||||
op.drop_table("conversations")
|
||||
op.drop_table("email_logs")
|
||||
op.drop_table("email_templates")
|
||||
op.drop_table("store_email_templates")
|
||||
op.drop_table("store_email_settings")
|
||||
sa.Enum(name="conversationtype").drop(op.get_bind(), checkfirst=True)
|
||||
sa.Enum(name="participanttype").drop(op.get_bind(), checkfirst=True)
|
||||
@@ -136,7 +136,7 @@ class StoreEmailSettings(Base, TimestampMixin):
|
||||
# Indexes
|
||||
# =========================================================================
|
||||
__table_args__ = (
|
||||
Index("idx_vendor_email_settings_configured", "store_id", "is_configured"),
|
||||
Index("idx_store_email_settings_configured", "store_id", "is_configured"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
||||
@@ -75,7 +75,7 @@ class StoreEmailTemplate(Base, TimestampMixin):
|
||||
"store_id",
|
||||
"template_code",
|
||||
"language",
|
||||
name="uq_vendor_email_template_code_language",
|
||||
name="uq_store_email_template_code_language",
|
||||
),
|
||||
{"sqlite_autoincrement": True},
|
||||
)
|
||||
|
||||
@@ -147,6 +147,7 @@ orders_module = ModuleDefinition(
|
||||
models_path="app.modules.orders.models",
|
||||
schemas_path="app.modules.orders.schemas",
|
||||
exceptions_path="app.modules.orders.exceptions",
|
||||
migrations_path="migrations",
|
||||
# Metrics provider for dashboard statistics
|
||||
metrics_provider=_get_metrics_provider,
|
||||
feature_provider=_get_feature_provider,
|
||||
|
||||
216
app/modules/orders/migrations/versions/orders_001_initial.py
Normal file
216
app/modules/orders/migrations/versions/orders_001_initial.py
Normal file
@@ -0,0 +1,216 @@
|
||||
"""orders initial - orders, order items, exceptions, invoices, fulfillment queue
|
||||
|
||||
Revision ID: orders_001
|
||||
Revises: customers_001
|
||||
Create Date: 2026-02-07
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "orders_001"
|
||||
down_revision = "customers_001"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# --- orders ---
|
||||
op.create_table(
|
||||
"orders",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=False, index=True),
|
||||
sa.Column("customer_id", sa.Integer(), sa.ForeignKey("customers.id"), nullable=False, index=True),
|
||||
sa.Column("order_number", sa.String(100), unique=True, nullable=False, index=True),
|
||||
sa.Column("channel", sa.String(50), nullable=False, server_default="direct", index=True),
|
||||
sa.Column("external_order_id", sa.String(100), nullable=True, index=True),
|
||||
sa.Column("external_shipment_id", sa.String(100), nullable=True, index=True),
|
||||
sa.Column("external_order_number", sa.String(100), nullable=True),
|
||||
sa.Column("external_data", sa.JSON(), nullable=True),
|
||||
sa.Column("status", sa.String(50), nullable=False, server_default="pending", index=True),
|
||||
sa.Column("subtotal_cents", sa.Integer(), nullable=True),
|
||||
sa.Column("tax_amount_cents", sa.Integer(), nullable=True),
|
||||
sa.Column("shipping_amount_cents", sa.Integer(), nullable=True),
|
||||
sa.Column("discount_amount_cents", sa.Integer(), nullable=True),
|
||||
sa.Column("total_amount_cents", sa.Integer(), nullable=False),
|
||||
sa.Column("currency", sa.String(10), nullable=True, server_default="EUR"),
|
||||
sa.Column("vat_regime", sa.String(20), nullable=True),
|
||||
sa.Column("vat_rate", sa.Numeric(5, 2), nullable=True),
|
||||
sa.Column("vat_rate_label", sa.String(100), nullable=True),
|
||||
sa.Column("vat_destination_country", sa.String(2), nullable=True),
|
||||
sa.Column("customer_first_name", sa.String(100), nullable=False),
|
||||
sa.Column("customer_last_name", sa.String(100), nullable=False),
|
||||
sa.Column("customer_email", sa.String(255), nullable=False),
|
||||
sa.Column("customer_phone", sa.String(50), nullable=True),
|
||||
sa.Column("customer_locale", sa.String(10), nullable=True),
|
||||
sa.Column("ship_first_name", sa.String(100), nullable=False),
|
||||
sa.Column("ship_last_name", sa.String(100), nullable=False),
|
||||
sa.Column("ship_company", sa.String(200), nullable=True),
|
||||
sa.Column("ship_address_line_1", sa.String(255), nullable=False),
|
||||
sa.Column("ship_address_line_2", sa.String(255), nullable=True),
|
||||
sa.Column("ship_city", sa.String(100), nullable=False),
|
||||
sa.Column("ship_postal_code", sa.String(20), nullable=False),
|
||||
sa.Column("ship_country_iso", sa.String(5), nullable=False),
|
||||
sa.Column("bill_first_name", sa.String(100), nullable=False),
|
||||
sa.Column("bill_last_name", sa.String(100), nullable=False),
|
||||
sa.Column("bill_company", sa.String(200), nullable=True),
|
||||
sa.Column("bill_address_line_1", sa.String(255), nullable=False),
|
||||
sa.Column("bill_address_line_2", sa.String(255), nullable=True),
|
||||
sa.Column("bill_city", sa.String(100), nullable=False),
|
||||
sa.Column("bill_postal_code", sa.String(20), nullable=False),
|
||||
sa.Column("bill_country_iso", sa.String(5), nullable=False),
|
||||
sa.Column("shipping_method", sa.String(100), nullable=True),
|
||||
sa.Column("tracking_number", sa.String(100), nullable=True),
|
||||
sa.Column("tracking_provider", sa.String(100), nullable=True),
|
||||
sa.Column("tracking_url", sa.String(500), nullable=True),
|
||||
sa.Column("shipment_number", sa.String(100), nullable=True),
|
||||
sa.Column("shipping_carrier", sa.String(50), nullable=True),
|
||||
sa.Column("customer_notes", sa.Text(), nullable=True),
|
||||
sa.Column("internal_notes", sa.Text(), nullable=True),
|
||||
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(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_order_store_status", "orders", ["store_id", "status"])
|
||||
op.create_index("idx_order_store_channel", "orders", ["store_id", "channel"])
|
||||
op.create_index("idx_order_store_date", "orders", ["store_id", "order_date"])
|
||||
|
||||
# --- order_items ---
|
||||
op.create_table(
|
||||
"order_items",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("order_id", sa.Integer(), sa.ForeignKey("orders.id"), nullable=False, index=True),
|
||||
sa.Column("product_id", sa.Integer(), sa.ForeignKey("products.id"), nullable=False),
|
||||
sa.Column("product_name", sa.String(255), nullable=False),
|
||||
sa.Column("product_sku", sa.String(100), nullable=True),
|
||||
sa.Column("gtin", sa.String(50), nullable=True),
|
||||
sa.Column("gtin_type", sa.String(20), nullable=True),
|
||||
sa.Column("quantity", sa.Integer(), nullable=False),
|
||||
sa.Column("unit_price_cents", sa.Integer(), nullable=False),
|
||||
sa.Column("total_price_cents", sa.Integer(), nullable=False),
|
||||
sa.Column("external_item_id", sa.String(100), nullable=True),
|
||||
sa.Column("external_variant_id", sa.String(100), nullable=True),
|
||||
sa.Column("item_state", sa.String(50), nullable=True),
|
||||
sa.Column("inventory_reserved", sa.Boolean(), nullable=True, server_default="false"),
|
||||
sa.Column("inventory_fulfilled", sa.Boolean(), nullable=True, server_default="false"),
|
||||
sa.Column("shipped_quantity", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("needs_product_match", sa.Boolean(), nullable=True, server_default="false", index=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
|
||||
# --- order_item_exceptions ---
|
||||
op.create_table(
|
||||
"order_item_exceptions",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("order_item_id", sa.Integer(), sa.ForeignKey("order_items.id", ondelete="CASCADE"), unique=True, nullable=False),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=False, index=True),
|
||||
sa.Column("original_gtin", sa.String(50), nullable=True, index=True),
|
||||
sa.Column("original_product_name", sa.String(500), nullable=True),
|
||||
sa.Column("original_sku", sa.String(100), nullable=True),
|
||||
sa.Column("exception_type", sa.String(50), nullable=False, server_default="product_not_found"),
|
||||
sa.Column("status", sa.String(50), nullable=False, server_default="pending", index=True),
|
||||
sa.Column("resolved_product_id", sa.Integer(), sa.ForeignKey("products.id"), nullable=True),
|
||||
sa.Column("resolved_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("resolved_by", sa.Integer(), sa.ForeignKey("users.id"), nullable=True),
|
||||
sa.Column("resolution_notes", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_exception_store_status", "order_item_exceptions", ["store_id", "status"])
|
||||
op.create_index("idx_exception_gtin", "order_item_exceptions", ["store_id", "original_gtin"])
|
||||
|
||||
# --- store_invoice_settings ---
|
||||
op.create_table(
|
||||
"store_invoice_settings",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), unique=True, nullable=False, index=True),
|
||||
sa.Column("merchant_name", sa.String(255), nullable=False),
|
||||
sa.Column("merchant_address", sa.String(255), nullable=True),
|
||||
sa.Column("merchant_city", sa.String(100), nullable=True),
|
||||
sa.Column("merchant_postal_code", sa.String(20), nullable=True),
|
||||
sa.Column("merchant_country", sa.String(2), nullable=False, server_default="LU"),
|
||||
sa.Column("vat_number", sa.String(50), nullable=True),
|
||||
sa.Column("is_vat_registered", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("is_oss_registered", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("oss_registration_country", sa.String(2), nullable=True),
|
||||
sa.Column("invoice_prefix", sa.String(20), nullable=False, server_default="INV"),
|
||||
sa.Column("invoice_next_number", sa.Integer(), nullable=False, server_default="1"),
|
||||
sa.Column("invoice_number_padding", sa.Integer(), nullable=False, server_default="5"),
|
||||
sa.Column("payment_terms", sa.Text(), nullable=True),
|
||||
sa.Column("bank_name", sa.String(255), nullable=True),
|
||||
sa.Column("bank_iban", sa.String(50), nullable=True),
|
||||
sa.Column("bank_bic", sa.String(20), nullable=True),
|
||||
sa.Column("footer_text", sa.Text(), nullable=True),
|
||||
sa.Column("default_vat_rate", sa.Numeric(5, 2), nullable=False, server_default="17.00"),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
|
||||
# --- invoices ---
|
||||
op.create_table(
|
||||
"invoices",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=False, index=True),
|
||||
sa.Column("order_id", sa.Integer(), sa.ForeignKey("orders.id"), nullable=True, index=True),
|
||||
sa.Column("invoice_number", sa.String(50), nullable=False),
|
||||
sa.Column("invoice_date", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("status", sa.String(20), nullable=False, server_default="draft"),
|
||||
sa.Column("seller_details", sa.JSON(), nullable=False),
|
||||
sa.Column("buyer_details", sa.JSON(), nullable=False),
|
||||
sa.Column("line_items", sa.JSON(), nullable=False),
|
||||
sa.Column("vat_regime", sa.String(20), nullable=False, server_default="domestic"),
|
||||
sa.Column("destination_country", sa.String(2), nullable=True),
|
||||
sa.Column("vat_rate", sa.Numeric(5, 2), nullable=False),
|
||||
sa.Column("vat_rate_label", sa.String(50), nullable=True),
|
||||
sa.Column("currency", sa.String(3), nullable=False, server_default="EUR"),
|
||||
sa.Column("subtotal_cents", sa.Integer(), nullable=False),
|
||||
sa.Column("vat_amount_cents", sa.Integer(), nullable=False),
|
||||
sa.Column("total_cents", sa.Integer(), nullable=False),
|
||||
sa.Column("payment_terms", sa.Text(), nullable=True),
|
||||
sa.Column("bank_details", sa.JSON(), nullable=True),
|
||||
sa.Column("footer_text", sa.Text(), nullable=True),
|
||||
sa.Column("pdf_generated_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("pdf_path", sa.String(500), nullable=True),
|
||||
sa.Column("notes", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_invoice_store_number", "invoices", ["store_id", "invoice_number"], unique=True)
|
||||
op.create_index("idx_invoice_store_date", "invoices", ["store_id", "invoice_date"])
|
||||
op.create_index("idx_invoice_status", "invoices", ["store_id", "status"])
|
||||
|
||||
# --- letzshop_fulfillment_queue (depends on orders + stores) ---
|
||||
op.create_table(
|
||||
"letzshop_fulfillment_queue",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id"), nullable=False, index=True),
|
||||
sa.Column("order_id", sa.Integer(), sa.ForeignKey("orders.id"), nullable=False, index=True),
|
||||
sa.Column("operation", sa.String(50), nullable=False),
|
||||
sa.Column("payload", sa.JSON(), nullable=False),
|
||||
sa.Column("status", sa.String(50), nullable=True, server_default="pending"),
|
||||
sa.Column("attempts", sa.Integer(), nullable=True, server_default="0"),
|
||||
sa.Column("max_attempts", sa.Integer(), nullable=True, server_default="3"),
|
||||
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(), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("idx_fulfillment_queue_status", "letzshop_fulfillment_queue", ["status", "store_id"])
|
||||
op.create_index("idx_fulfillment_queue_retry", "letzshop_fulfillment_queue", ["status", "next_retry_at"])
|
||||
op.create_index("idx_fulfillment_queue_order", "letzshop_fulfillment_queue", ["order_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("letzshop_fulfillment_queue")
|
||||
op.drop_table("invoices")
|
||||
op.drop_table("store_invoice_settings")
|
||||
op.drop_table("order_item_exceptions")
|
||||
op.drop_table("order_items")
|
||||
op.drop_table("orders")
|
||||
@@ -57,9 +57,9 @@ class StoreDomain(Base, TimestampMixin):
|
||||
|
||||
# Constraints
|
||||
__table_args__ = (
|
||||
UniqueConstraint("store_id", "domain", name="uq_vendor_domain"),
|
||||
UniqueConstraint("store_id", "domain", name="uq_store_domain"),
|
||||
Index("idx_domain_active", "domain", "is_active"),
|
||||
Index("idx_vendor_primary", "store_id", "is_primary"),
|
||||
Index("idx_store_domain_primary", "store_id", "is_primary"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
@@ -150,17 +150,17 @@ class StorePlatform(Base, TimestampMixin):
|
||||
UniqueConstraint(
|
||||
"store_id",
|
||||
"platform_id",
|
||||
name="uq_vendor_platform",
|
||||
name="uq_store_platform",
|
||||
),
|
||||
# Performance indexes
|
||||
Index(
|
||||
"idx_vendor_platform_active",
|
||||
"idx_store_platform_active",
|
||||
"store_id",
|
||||
"platform_id",
|
||||
"is_active",
|
||||
),
|
||||
Index(
|
||||
"idx_vendor_platform_primary",
|
||||
"idx_store_platform_primary",
|
||||
"store_id",
|
||||
"is_primary",
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user