Files
orion/app/api/v1/admin/vendor_domains.py
Samir Boulahtit 3520bcb069 refactor: move transaction management from services to API endpoints
- Services now use db.flush() instead of db.commit() for database operations
- API endpoints handle transaction commit after service calls
- Remove db.rollback() from services (let exception handlers manage this)
- Ensures consistent transaction boundaries at API layer

This pattern gives API endpoints full control over when to commit,
allowing for better error handling and potential multi-operation transactions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-06 18:34:41 +01:00

310 lines
9.3 KiB
Python

# app/api/v1/admin/vendor_domains.py
"""
Admin endpoints for managing vendor custom domains.
Follows the architecture pattern:
- 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.services.vendor_domain_service import vendor_domain_service
from app.services.vendor_service import vendor_service
from models.database.user import User
from models.schema.vendor_domain import (
DomainDeletionResponse,
DomainVerificationInstructions,
DomainVerificationResponse,
VendorDomainCreate,
VendorDomainListResponse,
VendorDomainResponse,
VendorDomainUpdate,
)
router = APIRouter(prefix="/vendors")
logger = logging.getLogger(__name__)
@router.post("/{vendor_id}/domains", response_model=VendorDomainResponse)
def add_vendor_domain(
vendor_id: int = Path(..., description="Vendor ID", gt=0),
domain_data: VendorDomainCreate = Body(...),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""
Add a custom domain to vendor (Admin only).
This endpoint:
1. Validates the domain format
2. Checks if domain is already registered
3. Generates verification token
4. Creates domain record (unverified, inactive)
5. Returns domain with verification instructions
**Domain Examples:**
- myshop.com
- shop.mybrand.com
- customstore.net
**Next Steps:**
1. Vendor adds DNS TXT record
2. Admin clicks "Verify Domain" to confirm ownership
3. Once verified, domain can be activated
**Raises:**
- 404: Vendor not found
- 409: Domain already registered
- 422: Invalid domain format or reserved subdomain
"""
domain = vendor_domain_service.add_domain(
db=db, vendor_id=vendor_id, domain_data=domain_data
)
db.commit()
return VendorDomainResponse(
id=domain.id,
vendor_id=domain.vendor_id,
domain=domain.domain,
is_primary=domain.is_primary,
is_active=domain.is_active,
is_verified=domain.is_verified,
ssl_status=domain.ssl_status,
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,
)
@router.get("/{vendor_id}/domains", response_model=VendorDomainListResponse)
def list_vendor_domains(
vendor_id: int = Path(..., description="Vendor ID", gt=0),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""
List all domains for a vendor (Admin only).
Returns domains ordered by:
1. Primary domains first
2. Creation date (newest first)
**Raises:**
- 404: Vendor not found
"""
# Verify vendor exists (raises VendorNotFoundException if not found)
vendor_service.get_vendor_by_id(db, vendor_id)
domains = vendor_domain_service.get_vendor_domains(db, vendor_id)
return VendorDomainListResponse(
domains=[
VendorDomainResponse(
id=d.id,
vendor_id=d.vendor_id,
domain=d.domain,
is_primary=d.is_primary,
is_active=d.is_active,
is_verified=d.is_verified,
ssl_status=d.ssl_status,
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),
)
@router.get("/domains/{domain_id}", response_model=VendorDomainResponse)
def get_domain_details(
domain_id: int = Path(..., description="Domain ID", gt=0),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""
Get detailed information about a specific domain (Admin only).
**Raises:**
- 404: Domain not found
"""
domain = vendor_domain_service.get_domain_by_id(db, domain_id)
return VendorDomainResponse(
id=domain.id,
vendor_id=domain.vendor_id,
domain=domain.domain,
is_primary=domain.is_primary,
is_active=domain.is_active,
is_verified=domain.is_verified,
ssl_status=domain.ssl_status,
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,
)
@router.put("/domains/{domain_id}", response_model=VendorDomainResponse)
def update_vendor_domain(
domain_id: int = Path(..., description="Domain ID", gt=0),
domain_update: VendorDomainUpdate = Body(...),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""
Update domain settings (Admin only).
**Can update:**
- `is_primary`: Set as primary domain for vendor
- `is_active`: Activate or deactivate domain
**Important:**
- Cannot activate unverified domains
- Setting a domain as primary will unset other primary domains
- Cannot modify domain name (delete and recreate instead)
**Raises:**
- 404: Domain not found
- 400: Cannot activate unverified domain
"""
domain = vendor_domain_service.update_domain(
db=db, domain_id=domain_id, domain_update=domain_update
)
db.commit()
return VendorDomainResponse(
id=domain.id,
vendor_id=domain.vendor_id,
domain=domain.domain,
is_primary=domain.is_primary,
is_active=domain.is_active,
is_verified=domain.is_verified,
ssl_status=domain.ssl_status,
verification_token=None, # Don't expose token after updates
verified_at=domain.verified_at,
ssl_verified_at=domain.ssl_verified_at,
created_at=domain.created_at,
updated_at=domain.updated_at,
)
@router.delete("/domains/{domain_id}", response_model=DomainDeletionResponse)
def delete_vendor_domain(
domain_id: int = Path(..., description="Domain ID", gt=0),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""
Delete a custom domain (Admin only).
**Warning:** This is permanent and cannot be undone.
**Raises:**
- 404: Domain not found
"""
# Get domain details before deletion
domain = vendor_domain_service.get_domain_by_id(db, domain_id)
vendor_id = domain.vendor_id
domain_name = domain.domain
# Delete domain
message = vendor_domain_service.delete_domain(db, domain_id)
db.commit()
return DomainDeletionResponse(
message=message, domain=domain_name, vendor_id=vendor_id
)
@router.post("/domains/{domain_id}/verify", response_model=DomainVerificationResponse)
def verify_domain_ownership(
domain_id: int = Path(..., description="Domain ID", gt=0),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""
Verify domain ownership via DNS TXT record (Admin only).
**Verification Process:**
1. Queries DNS for TXT record: `_wizamart-verify.{domain}`
2. Checks if verification token matches
3. If found, marks domain as verified
**Requirements:**
- Vendor must have added TXT record to their DNS
- DNS propagation may take 5-15 minutes
- Record format: `_wizamart-verify.domain.com` TXT `{token}`
**After verification:**
- Domain can be activated
- Domain will be available for routing
**Raises:**
- 404: Domain not found
- 400: Already verified, or verification failed
- 502: DNS query failed
"""
domain, message = vendor_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,
)
@router.get(
"/domains/{domain_id}/verification-instructions",
response_model=DomainVerificationInstructions,
)
def get_domain_verification_instructions(
domain_id: int = Path(..., description="Domain ID", gt=0),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""
Get DNS verification instructions for domain (Admin only).
Returns step-by-step instructions for:
1. Where to add DNS records
2. What TXT record to create
3. Links to common registrars
4. Verification token
**Use this endpoint to:**
- Show vendors how to verify their domain
- Get the exact TXT record values
- Access registrar links
**Raises:**
- 404: Domain not found
"""
instructions = vendor_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"],
)