Some checks failed
- 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>
211 lines
6.6 KiB
Python
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)
|