feat(tenancy): add merchant-level domain with store override
Merchants can now register domains (e.g., myloyaltyprogram.lu) that all their stores inherit. Individual stores can override with their own custom domain. Resolution priority: StoreDomain > MerchantDomain > subdomain. - Add MerchantDomain model, schema, service, and admin API endpoints - Add merchant domain fallback in platform and store context middleware - Add Merchant.primary_domain and Store.effective_domain properties - Add Alembic migration for merchant_domains table - Update loyalty user journey docs with subscription & domain setup flow - Add unit tests (50 passing) and integration tests (15 passing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
0
app/modules/tenancy/migrations/__init__.py
Normal file
0
app/modules/tenancy/migrations/__init__.py
Normal file
0
app/modules/tenancy/migrations/versions/__init__.py
Normal file
0
app/modules/tenancy/migrations/versions/__init__.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""tenancy: add merchant_domains table for merchant-level domain routing
|
||||
|
||||
Revision ID: tenancy_001
|
||||
Revises: dev_tools_001
|
||||
Create Date: 2026-02-09
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "tenancy_001"
|
||||
down_revision = "dev_tools_001"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"merchant_domains",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column(
|
||||
"merchant_id",
|
||||
sa.Integer(),
|
||||
sa.ForeignKey("merchants.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("domain", sa.String(255), nullable=False, unique=True, index=True),
|
||||
sa.Column("is_primary", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("ssl_status", sa.String(50), server_default="pending"),
|
||||
sa.Column("ssl_verified_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("verification_token", sa.String(100), unique=True, nullable=True),
|
||||
sa.Column("is_verified", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("verified_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column(
|
||||
"platform_id",
|
||||
sa.Integer(),
|
||||
sa.ForeignKey("platforms.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
comment="Platform this domain is associated with (for platform context resolution)",
|
||||
),
|
||||
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_domain_platform"),
|
||||
)
|
||||
|
||||
op.create_index(
|
||||
"idx_merchant_domain_active", "merchant_domains", ["domain", "is_active"]
|
||||
)
|
||||
op.create_index(
|
||||
"idx_merchant_domain_primary", "merchant_domains", ["merchant_id", "is_primary"]
|
||||
)
|
||||
op.create_index(
|
||||
"idx_merchant_domain_platform", "merchant_domains", ["platform_id"]
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("idx_merchant_domain_platform", table_name="merchant_domains")
|
||||
op.drop_index("idx_merchant_domain_primary", table_name="merchant_domains")
|
||||
op.drop_index("idx_merchant_domain_active", table_name="merchant_domains")
|
||||
op.drop_table("merchant_domains")
|
||||
Reference in New Issue
Block a user