# 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 pydantic import BaseModel 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.schemas.template import TemplateListResponse, TemplateResponse from app.modules.hosting.services.hosted_site_service import hosted_site_service from app.modules.hosting.services.poc_builder_service import poc_builder_service from app.modules.hosting.services.template_service import template_service from app.modules.tenancy.schemas.auth import UserContext router = APIRouter(prefix="/sites") logger = logging.getLogger(__name__) @router.get("/templates", response_model=TemplateListResponse) def list_templates( current_admin: UserContext = Depends(get_current_admin_api), ): """List available industry templates for POC site generation.""" templates = template_service.list_templates() return TemplateListResponse( templates=[TemplateResponse(**t) for t in templates], ) class PreviewUrlResponse(BaseModel): """Response with signed preview URL.""" preview_url: str expires_in_hours: int = 24 @router.get("/sites/{site_id}/preview-url", response_model=PreviewUrlResponse) def get_preview_url( site_id: int = Path(...), db: Session = Depends(get_db), current_admin: UserContext = Depends(get_current_admin_api), ): """Generate a signed preview URL for a hosted site.""" from app.core.preview_token import create_preview_token site = hosted_site_service.get_by_id(db, site_id) store = site.store subdomain = store.subdomain or store.store_code token = create_preview_token(store.id, subdomain, site.id) return PreviewUrlResponse( preview_url=f"/storefront/{subdomain}/?_preview={token}", ) class BuildPocRequest(BaseModel): """Request to build a POC site from prospect + template.""" prospect_id: int template_id: str merchant_id: int | None = None class BuildPocResponse(BaseModel): """Response from POC builder.""" hosted_site_id: int store_id: int pages_created: int theme_applied: bool template_id: str subdomain: str | None = None @router.post("/poc/build", response_model=BuildPocResponse) def build_poc( data: BuildPocRequest, db: Session = Depends(get_db), current_admin: UserContext = Depends(get_current_admin_api), ): """Build a POC site from prospect data + industry template.""" result = poc_builder_service.build_poc( db, prospect_id=data.prospect_id, template_id=data.template_id, merchant_id=data.merchant_id, ) db.commit() return BuildPocResponse(**result) 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.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)