Files
orion/app/modules/tenancy/models/merchant.py
Samir Boulahtit 0984ff7d17 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>
2026-02-09 22:04:49 +01:00

125 lines
4.4 KiB
Python

# app/modules/tenancy/models/merchant.py
"""
Merchant model representing the business entity that owns one or more store brands.
A Merchant represents the legal/business entity with contact information,
while Stores represent the individual brands/storefronts operated by that merchant.
"""
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Text
from sqlalchemy.orm import relationship
from app.core.database import Base
from models.database.base import TimestampMixin
class Merchant(Base, TimestampMixin):
"""
Represents a merchant (business entity) in the system.
A merchant owns one or more store brands. All business/contact information
is stored at the merchant level to avoid duplication.
"""
__tablename__ = "merchants"
# ========================================================================
# Basic Information
# ========================================================================
id = Column(Integer, primary_key=True, index=True)
"""Unique identifier for the merchant."""
name = Column(String, nullable=False, index=True)
"""Merchant legal/business name."""
description = Column(Text)
"""Optional description of the merchant."""
# ========================================================================
# Ownership
# ========================================================================
owner_user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
"""Foreign key to the user who owns this merchant."""
# ========================================================================
# Contact Information
# ========================================================================
contact_email = Column(String, nullable=False)
"""Primary business contact email."""
contact_phone = Column(String)
"""Business phone number."""
website = Column(String)
"""Merchant website URL."""
# ========================================================================
# Business Details
# ========================================================================
business_address = Column(Text)
"""Physical business address."""
tax_number = Column(String)
"""Tax/VAT registration number."""
# ========================================================================
# Status Flags
# ========================================================================
is_active = Column(Boolean, default=True, nullable=False)
"""Whether the merchant is active. Affects all associated stores."""
is_verified = Column(Boolean, default=False, nullable=False)
"""Whether the merchant has been verified by platform admins."""
# ========================================================================
# Relationships
# ========================================================================
owner = relationship("User", back_populates="owned_merchants")
"""The user who owns this merchant."""
stores = relationship(
"Store",
back_populates="merchant",
cascade="all, delete-orphan",
order_by="Store.name",
)
"""All store brands operated by this merchant."""
merchant_domains = relationship(
"MerchantDomain",
back_populates="merchant",
cascade="all, delete-orphan",
)
"""Custom domains registered at the merchant level (inherited by all stores)."""
def __repr__(self):
"""String representation of the Merchant object."""
return f"<Merchant(id={self.id}, name='{self.name}', stores={len(self.stores) if self.stores else 0})>"
# ========================================================================
# Helper Properties
# ========================================================================
@property
def store_count(self) -> int:
"""Get the number of stores belonging to this merchant."""
return len(self.stores) if self.stores else 0
@property
def primary_domain(self) -> str | None:
"""Get the primary active and verified merchant domain."""
for md in self.merchant_domains:
if md.is_primary and md.is_active and md.is_verified:
return md.domain
return None
@property
def active_store_count(self) -> int:
"""Get the number of active stores belonging to this merchant."""
if not self.stores:
return 0
return sum(1 for v in self.stores if v.is_active)
__all__ = ["Merchant"]