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:
@@ -933,6 +933,43 @@ class InvalidInvitationTokenException(ValidationException):
|
||||
# =============================================================================
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Merchant Domain Exceptions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class MerchantDomainNotFoundException(ResourceNotFoundException):
|
||||
"""Raised when a merchant domain is not found."""
|
||||
|
||||
def __init__(self, domain_identifier: str, identifier_type: str = "ID"):
|
||||
if identifier_type.lower() == "domain":
|
||||
message = f"Merchant domain '{domain_identifier}' not found"
|
||||
else:
|
||||
message = f"Merchant domain with ID '{domain_identifier}' not found"
|
||||
|
||||
super().__init__(
|
||||
resource_type="MerchantDomain",
|
||||
identifier=domain_identifier,
|
||||
message=message,
|
||||
error_code="MERCHANT_DOMAIN_NOT_FOUND",
|
||||
)
|
||||
|
||||
|
||||
class MerchantDomainAlreadyExistsException(ConflictException):
|
||||
"""Raised when trying to add a domain that already exists."""
|
||||
|
||||
def __init__(self, domain: str, existing_merchant_id: int | None = None):
|
||||
details = {"domain": domain}
|
||||
if existing_merchant_id:
|
||||
details["existing_merchant_id"] = existing_merchant_id
|
||||
|
||||
super().__init__(
|
||||
message=f"Domain '{domain}' is already registered",
|
||||
error_code="MERCHANT_DOMAIN_ALREADY_EXISTS",
|
||||
details=details,
|
||||
)
|
||||
|
||||
|
||||
class StoreDomainNotFoundException(ResourceNotFoundException):
|
||||
"""Raised when a store domain is not found."""
|
||||
|
||||
@@ -1129,6 +1166,9 @@ __all__ = [
|
||||
"TeamValidationException",
|
||||
"InvalidInvitationDataException",
|
||||
"InvalidInvitationTokenException",
|
||||
# Merchant Domain
|
||||
"MerchantDomainNotFoundException",
|
||||
"MerchantDomainAlreadyExistsException",
|
||||
# Store Domain
|
||||
"StoreDomainNotFoundException",
|
||||
"StoreDomainAlreadyExistsException",
|
||||
|
||||
Reference in New Issue
Block a user