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>
298 lines
9.1 KiB
Python
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.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,
|
|
)
|
|
from models.schema.auth import UserContext
|
|
|
|
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"],
|
|
)
|