Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart with Orion/orion/ORION across 184 files. This includes database identifiers, email addresses, domain references, R2 bucket names, DNS prefixes, encryption salt, Celery app name, config defaults, Docker configs, CI configs, documentation, seed data, and templates. Renames homepage-wizamart.html template to homepage-orion.html. Fixes duplicate file_pattern key in api.yaml architecture rule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
118 lines
3.7 KiB
Python
118 lines
3.7 KiB
Python
# app/modules/tenancy/models/merchant_domain.py
|
|
"""
|
|
Merchant Domain Model - Maps custom domains to merchants for merchant-level domain routing.
|
|
|
|
When a merchant subscribes to a platform (e.g., loyalty), they can register a domain
|
|
(e.g., myloyaltyprogram.lu) that serves as the default for all their stores.
|
|
Individual stores can optionally override this with their own custom StoreDomain.
|
|
|
|
Domain Resolution Priority:
|
|
1. Store-specific custom domain (StoreDomain) -> highest priority
|
|
2. Merchant domain (MerchantDomain) -> inherited default
|
|
3. Store subdomain ({store.subdomain}.loyalty.lu) -> fallback
|
|
"""
|
|
|
|
from sqlalchemy import (
|
|
Boolean,
|
|
Column,
|
|
DateTime,
|
|
ForeignKey,
|
|
Index,
|
|
Integer,
|
|
String,
|
|
UniqueConstraint,
|
|
)
|
|
from sqlalchemy.orm import relationship
|
|
|
|
from app.core.database import Base
|
|
from models.database.base import TimestampMixin
|
|
|
|
|
|
class MerchantDomain(Base, TimestampMixin):
|
|
"""
|
|
Maps custom domains to merchants for merchant-level domain routing.
|
|
|
|
Examples:
|
|
- myloyaltyprogram.lu -> Merchant "WizaCorp" (all stores inherit)
|
|
- Store ORION overrides with StoreDomain -> mysuperloyaltyprogram.lu
|
|
- Store WIZAGADGETS -> inherits myloyaltyprogram.lu
|
|
"""
|
|
|
|
__tablename__ = "merchant_domains"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
merchant_id = Column(
|
|
Integer, ForeignKey("merchants.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
|
|
# Domain configuration
|
|
domain = Column(String(255), nullable=False, unique=True, index=True)
|
|
is_primary = Column(Boolean, default=True, nullable=False)
|
|
is_active = Column(Boolean, default=True, nullable=False)
|
|
|
|
# SSL/TLS status (for monitoring)
|
|
ssl_status = Column(
|
|
String(50), default="pending"
|
|
) # pending, active, expired, error
|
|
ssl_verified_at = Column(DateTime(timezone=True), nullable=True)
|
|
|
|
# DNS verification (to confirm domain ownership)
|
|
verification_token = Column(String(100), unique=True, nullable=True)
|
|
is_verified = Column(Boolean, default=False, nullable=False)
|
|
verified_at = Column(DateTime(timezone=True), nullable=True)
|
|
|
|
# Platform association (for platform context resolution from custom domains)
|
|
platform_id = Column(
|
|
Integer,
|
|
ForeignKey("platforms.id", ondelete="SET NULL"),
|
|
nullable=True,
|
|
index=True,
|
|
comment="Platform this domain is associated with (for platform context resolution)",
|
|
)
|
|
|
|
# Relationships
|
|
merchant = relationship("Merchant", back_populates="merchant_domains")
|
|
platform = relationship("Platform")
|
|
|
|
# Constraints
|
|
__table_args__ = (
|
|
UniqueConstraint("merchant_id", "platform_id", name="uq_merchant_domain_platform"),
|
|
Index("idx_merchant_domain_active", "domain", "is_active"),
|
|
Index("idx_merchant_domain_primary", "merchant_id", "is_primary"),
|
|
Index("idx_merchant_domain_platform", "platform_id"),
|
|
)
|
|
|
|
def __repr__(self):
|
|
return f"<MerchantDomain(domain='{self.domain}', merchant_id={self.merchant_id})>"
|
|
|
|
@property
|
|
def full_url(self):
|
|
"""Return full URL with https"""
|
|
return f"https://{self.domain}"
|
|
|
|
@classmethod
|
|
def normalize_domain(cls, domain: str) -> str:
|
|
"""
|
|
Normalize domain for consistent storage.
|
|
|
|
Reuses the same logic as StoreDomain.normalize_domain().
|
|
|
|
Examples:
|
|
- https://example.com -> example.com
|
|
- www.example.com -> example.com
|
|
- EXAMPLE.COM -> example.com
|
|
"""
|
|
# Remove protocol
|
|
domain = domain.replace("https://", "").replace("http://", "") # SEC-034
|
|
|
|
# Remove trailing slash
|
|
domain = domain.rstrip("/")
|
|
|
|
# Convert to lowercase
|
|
domain = domain.lower()
|
|
|
|
return domain
|
|
|
|
|
|
__all__ = ["MerchantDomain"]
|