Files
orion/app/modules/tenancy/routes/api/admin_merchant_domains.py
Samir Boulahtit 4aa6f76e46
Some checks failed
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled
CI / ruff (push) Successful in 10s
refactor(arch): move auth schemas to tenancy module and add cross-module service methods
Move all auth schemas (UserContext, UserLogin, LoginResponse, etc.) from
legacy models/schema/auth.py to app/modules/tenancy/schemas/auth.py per
MOD-019. Update 84 import sites across 14 modules. Legacy file now
re-exports for backwards compatibility.

Add missing tenancy service methods for cross-module consumers:
- merchant_service.get_merchant_by_owner_id()
- merchant_service.get_merchant_count_for_owner()
- admin_service.get_user_by_id() (public, was private-only)
- platform_service.get_active_store_count()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 23:57:04 +01:00

298 lines
9.1 KiB
Python

# app/modules/tenancy/routes/api/admin_merchant_domains.py
"""
Admin endpoints for managing merchant-level custom domains.
Follows the same pattern as admin_store_domains.py:
- Endpoints only handle HTTP layer
- Business logic in service layer
- Domain exceptions bubble up to global handler
- Pydantic schemas for validation
"""
import logging
from fastapi import APIRouter, Body, Depends, Path
from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api
from app.core.database import get_db
from app.modules.tenancy.schemas.auth import UserContext
from app.modules.tenancy.schemas.merchant_domain import (
MerchantDomainCreate,
MerchantDomainDeletionResponse,
MerchantDomainListResponse,
MerchantDomainResponse,
MerchantDomainUpdate,
)
from app.modules.tenancy.schemas.store_domain import (
DomainVerificationInstructions,
DomainVerificationResponse,
)
from app.modules.tenancy.services.merchant_domain_service import (
merchant_domain_service,
)
admin_merchant_domains_router = APIRouter(prefix="/merchants")
logger = logging.getLogger(__name__)
@admin_merchant_domains_router.post(
"/{merchant_id}/domains", response_model=MerchantDomainResponse
)
def add_merchant_domain(
merchant_id: int = Path(..., description="Merchant ID", gt=0),
domain_data: MerchantDomainCreate = Body(...),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Register a merchant-level domain (Admin only).
The domain serves as the default for all the merchant's stores.
Individual stores can override with their own StoreDomain.
**Domain Resolution Priority:**
1. Store-specific custom domain (StoreDomain) -> highest priority
2. Merchant domain (MerchantDomain) -> inherited default
3. Store subdomain ({store.subdomain}.platform.lu) -> fallback
**Raises:**
- 404: Merchant not found
- 409: Domain already registered
- 422: Invalid domain format or reserved subdomain
"""
domain = merchant_domain_service.add_domain(
db=db, merchant_id=merchant_id, domain_data=domain_data
)
db.commit()
return MerchantDomainResponse(
id=domain.id,
merchant_id=domain.merchant_id,
domain=domain.domain,
is_primary=domain.is_primary,
is_active=domain.is_active,
is_verified=domain.is_verified,
ssl_status=domain.ssl_status,
platform_id=domain.platform_id,
verification_token=domain.verification_token,
verified_at=domain.verified_at,
ssl_verified_at=domain.ssl_verified_at,
created_at=domain.created_at,
updated_at=domain.updated_at,
)
@admin_merchant_domains_router.get(
"/{merchant_id}/domains", response_model=MerchantDomainListResponse
)
def list_merchant_domains(
merchant_id: int = Path(..., description="Merchant ID", gt=0),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
List all domains for a merchant (Admin only).
Returns domains ordered by:
1. Primary domains first
2. Creation date (newest first)
**Raises:**
- 404: Merchant not found
"""
domains = merchant_domain_service.get_merchant_domains(db, merchant_id)
return MerchantDomainListResponse(
domains=[
MerchantDomainResponse(
id=d.id,
merchant_id=d.merchant_id,
domain=d.domain,
is_primary=d.is_primary,
is_active=d.is_active,
is_verified=d.is_verified,
ssl_status=d.ssl_status,
platform_id=d.platform_id,
verification_token=d.verification_token if not d.is_verified else None,
verified_at=d.verified_at,
ssl_verified_at=d.ssl_verified_at,
created_at=d.created_at,
updated_at=d.updated_at,
)
for d in domains
],
total=len(domains),
)
@admin_merchant_domains_router.get(
"/domains/merchant/{domain_id}", response_model=MerchantDomainResponse
)
def get_merchant_domain_details(
domain_id: int = Path(..., description="Domain ID", gt=0),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Get detailed information about a specific merchant domain (Admin only).
**Raises:**
- 404: Domain not found
"""
domain = merchant_domain_service.get_domain_by_id(db, domain_id)
return MerchantDomainResponse(
id=domain.id,
merchant_id=domain.merchant_id,
domain=domain.domain,
is_primary=domain.is_primary,
is_active=domain.is_active,
is_verified=domain.is_verified,
ssl_status=domain.ssl_status,
platform_id=domain.platform_id,
verification_token=(
domain.verification_token if not domain.is_verified else None
),
verified_at=domain.verified_at,
ssl_verified_at=domain.ssl_verified_at,
created_at=domain.created_at,
updated_at=domain.updated_at,
)
@admin_merchant_domains_router.put(
"/domains/merchant/{domain_id}", response_model=MerchantDomainResponse
)
def update_merchant_domain(
domain_id: int = Path(..., description="Domain ID", gt=0),
domain_update: MerchantDomainUpdate = Body(...),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Update merchant domain settings (Admin only).
**Can update:**
- `is_primary`: Set as primary domain for merchant
- `is_active`: Activate or deactivate domain
**Important:**
- Cannot activate unverified domains
- Setting a domain as primary will unset other primary domains
**Raises:**
- 404: Domain not found
- 400: Cannot activate unverified domain
"""
domain = merchant_domain_service.update_domain(
db=db, domain_id=domain_id, domain_update=domain_update
)
db.commit()
return MerchantDomainResponse(
id=domain.id,
merchant_id=domain.merchant_id,
domain=domain.domain,
is_primary=domain.is_primary,
is_active=domain.is_active,
is_verified=domain.is_verified,
ssl_status=domain.ssl_status,
platform_id=domain.platform_id,
verification_token=None,
verified_at=domain.verified_at,
ssl_verified_at=domain.ssl_verified_at,
created_at=domain.created_at,
updated_at=domain.updated_at,
)
@admin_merchant_domains_router.delete(
"/domains/merchant/{domain_id}",
response_model=MerchantDomainDeletionResponse,
)
def delete_merchant_domain(
domain_id: int = Path(..., description="Domain ID", gt=0),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Delete a merchant domain (Admin only).
**Warning:** This is permanent and cannot be undone.
**Raises:**
- 404: Domain not found
"""
domain = merchant_domain_service.get_domain_by_id(db, domain_id)
merchant_id = domain.merchant_id
domain_name = domain.domain
message = merchant_domain_service.delete_domain(db, domain_id)
db.commit()
return MerchantDomainDeletionResponse(
message=message, domain=domain_name, merchant_id=merchant_id
)
@admin_merchant_domains_router.post(
"/domains/merchant/{domain_id}/verify",
response_model=DomainVerificationResponse,
)
def verify_merchant_domain_ownership(
domain_id: int = Path(..., description="Domain ID", gt=0),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Verify merchant domain ownership via DNS TXT record (Admin only).
**Verification Process:**
1. Queries DNS for TXT record: `_orion-verify.{domain}`
2. Checks if verification token matches
3. If found, marks domain as verified
**Raises:**
- 404: Domain not found
- 400: Already verified, or verification failed
- 502: DNS query failed
"""
domain, message = merchant_domain_service.verify_domain(db, domain_id)
db.commit()
return DomainVerificationResponse(
message=message,
domain=domain.domain,
verified_at=domain.verified_at,
is_verified=domain.is_verified,
)
@admin_merchant_domains_router.get(
"/domains/merchant/{domain_id}/verification-instructions",
response_model=DomainVerificationInstructions,
)
def get_merchant_domain_verification_instructions(
domain_id: int = Path(..., description="Domain ID", gt=0),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Get DNS verification instructions for merchant domain (Admin only).
**Raises:**
- 404: Domain not found
"""
instructions = merchant_domain_service.get_verification_instructions(
db, domain_id
)
return DomainVerificationInstructions(
domain=instructions["domain"],
verification_token=instructions["verification_token"],
instructions=instructions["instructions"],
txt_record=instructions["txt_record"],
common_registrars=instructions["common_registrars"],
)