Files
orion/app/modules/hosting/routes/api/admin_sites.py
Samir Boulahtit 8b147f53c6
Some checks failed
CI / pytest (push) Failing after 49m20s
CI / validate (push) Successful in 24s
CI / dependency-scanning (push) Successful in 33s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
CI / ruff (push) Successful in 10s
feat(hosting): add HostWizard platform module and fix migration chain
- Add complete hosting module (models, routes, schemas, services, templates, migrations)
- Add HostWizard platform to init_production seed (code=hosting, domain=hostwizard.lu)
- Fix cms_002 migration down_revision to z_unique_subdomain_domain
- Fix prospecting_001 migration to chain after cms_002 (remove branch label)
- Add hosting/prospecting version_locations to alembic.ini
- Fix admin_services delete endpoint to use proper response model
- Add hostwizard.lu to deployment docs (DNS, Caddy, Cloudflare)
- Add hosting and prospecting user journey docs to mkdocs nav

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 19:34:56 +01:00

211 lines
6.6 KiB
Python

# app/modules/hosting/routes/api/admin_sites.py
"""
Admin API routes for hosted site management.
"""
import logging
from math import ceil
from fastapi import APIRouter, Depends, Path, Query
from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api
from app.core.database import get_db
from app.modules.hosting.schemas.hosted_site import (
AcceptProposalRequest,
GoLiveRequest,
HostedSiteCreate,
HostedSiteDeleteResponse,
HostedSiteDetailResponse,
HostedSiteListResponse,
HostedSiteResponse,
HostedSiteUpdate,
SendProposalRequest,
)
from app.modules.hosting.services.hosted_site_service import hosted_site_service
from app.modules.tenancy.schemas.auth import UserContext
router = APIRouter(prefix="/sites")
logger = logging.getLogger(__name__)
def _to_response(site) -> HostedSiteResponse:
"""Convert a hosted site model to response schema."""
return HostedSiteResponse(
id=site.id,
store_id=site.store_id,
prospect_id=site.prospect_id,
status=site.status.value if hasattr(site.status, "value") else str(site.status),
business_name=site.business_name,
contact_name=site.contact_name,
contact_email=site.contact_email,
contact_phone=site.contact_phone,
proposal_sent_at=site.proposal_sent_at,
proposal_accepted_at=site.proposal_accepted_at,
went_live_at=site.went_live_at,
proposal_notes=site.proposal_notes,
live_domain=site.live_domain,
internal_notes=site.internal_notes,
created_at=site.created_at,
updated_at=site.updated_at,
)
@router.get("", response_model=HostedSiteListResponse)
def list_sites(
page: int = Query(1, ge=1),
per_page: int = Query(20, ge=1, le=100),
search: str | None = Query(None),
status: str | None = Query(None),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""List hosted sites with filters and pagination."""
sites, total = hosted_site_service.get_all(
db, page=page, per_page=per_page, search=search, status=status,
)
return HostedSiteListResponse(
items=[_to_response(s) for s in sites],
total=total,
page=page,
per_page=per_page,
pages=ceil(total / per_page) if per_page else 0,
)
@router.get("/{site_id}", response_model=HostedSiteDetailResponse)
def get_site(
site_id: int = Path(...),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""Get full hosted site detail with services."""
site = hosted_site_service.get_by_id(db, site_id)
return site
@router.post("", response_model=HostedSiteResponse)
def create_site(
data: HostedSiteCreate,
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""Create a new hosted site (auto-creates Store)."""
site = hosted_site_service.create(db, data.model_dump(exclude_none=True))
db.commit()
return _to_response(site)
@router.post("/from-prospect/{prospect_id}", response_model=HostedSiteResponse)
def create_from_prospect(
prospect_id: int = Path(...),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""Create a hosted site pre-filled from prospect data."""
site = hosted_site_service.create_from_prospect(db, prospect_id)
db.commit()
return _to_response(site)
@router.put("/{site_id}", response_model=HostedSiteResponse)
def update_site(
data: HostedSiteUpdate,
site_id: int = Path(...),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""Update a hosted site."""
site = hosted_site_service.update(db, site_id, data.model_dump(exclude_none=True))
db.commit()
return _to_response(site)
@router.delete("/{site_id}", response_model=HostedSiteDeleteResponse)
def delete_site(
site_id: int = Path(...),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""Delete a hosted site."""
hosted_site_service.delete(db, site_id)
db.commit()
return HostedSiteDeleteResponse(message="Hosted site deleted")
# ── Lifecycle endpoints ────────────────────────────────────────────────
@router.post("/{site_id}/mark-poc-ready", response_model=HostedSiteResponse)
def mark_poc_ready(
site_id: int = Path(...),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""Mark site as POC ready."""
site = hosted_site_service.mark_poc_ready(db, site_id)
db.commit()
return _to_response(site)
@router.post("/{site_id}/send-proposal", response_model=HostedSiteResponse)
def send_proposal(
data: SendProposalRequest,
site_id: int = Path(...),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""Send proposal to prospect."""
site = hosted_site_service.send_proposal(db, site_id, notes=data.notes)
db.commit()
return _to_response(site)
@router.post("/{site_id}/accept", response_model=HostedSiteResponse)
def accept_proposal(
data: AcceptProposalRequest,
site_id: int = Path(...),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""Accept proposal: create/link merchant, create subscription."""
site = hosted_site_service.accept_proposal(db, site_id, merchant_id=data.merchant_id)
db.commit()
return _to_response(site)
@router.post("/{site_id}/go-live", response_model=HostedSiteResponse)
def go_live(
data: GoLiveRequest,
site_id: int = Path(...),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""Go live with a domain."""
site = hosted_site_service.go_live(db, site_id, domain=data.domain)
db.commit()
return _to_response(site)
@router.post("/{site_id}/suspend", response_model=HostedSiteResponse)
def suspend_site(
site_id: int = Path(...),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""Suspend a site."""
site = hosted_site_service.suspend(db, site_id)
db.commit()
return _to_response(site)
@router.post("/{site_id}/cancel", response_model=HostedSiteResponse)
def cancel_site(
site_id: int = Path(...),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""Cancel a site."""
site = hosted_site_service.cancel(db, site_id)
db.commit()
return _to_response(site)