One-click POC site generation from prospect data + industry template:
PocBuilderService.build_poc():
1. Loads prospect (scraped content, contacts, business info)
2. Loads industry template (pages, theme, sections)
3. Creates HostedSite + Store via hosted_site_service
4. Populates CMS ContentPages from template, replacing {{placeholders}}
(business_name, city, phone, email, address, meta_description,
about_paragraph) with prospect data
5. Applies StoreTheme (colors, fonts, layout) from template
6. Auto-transitions to POC_READY status
API: POST /admin/hosting/sites/poc/build
Body: {prospect_id, template_id, merchant_id?}
Tested: prospect 1 (batirenovation-strasbourg.fr) + "construction"
template → 4 pages created, theme applied, subdomain assigned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
251 lines
7.7 KiB
Python
251 lines
7.7 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 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 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)
|