# 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"], )