# app/modules/hosting/services/poc_builder_service.py """ POC Builder Service — creates a near-final multi-page website from a prospect + industry template. Flow: 1. Load prospect data (scraped content, contacts) 2. Load industry template (pages, theme) 3. Create HostedSite + Store via hosted_site_service 4. Populate CMS ContentPages from template, replacing {{placeholders}} with prospect data 5. Apply StoreTheme from template 6. Result: a previewable site at {subdomain}.hostwizard.lu """ import json import logging import re from datetime import UTC, datetime from sqlalchemy.orm import Session from app.modules.hosting.services.hosted_site_service import hosted_site_service from app.modules.hosting.services.template_service import template_service logger = logging.getLogger(__name__) class PocBuilderService: """Builds POC sites from prospect data + industry templates.""" def build_poc( self, db: Session, prospect_id: int, template_id: str, merchant_id: int | None = None, ) -> dict: """Build a complete POC site from prospect data and a template. Returns dict with hosted_site, store, pages_created, theme_applied. """ from app.modules.prospecting.models import Prospect # 1. Load prospect prospect = db.query(Prospect).filter(Prospect.id == prospect_id).first() if not prospect: from app.modules.prospecting.exceptions import ProspectNotFoundException raise ProspectNotFoundException(str(prospect_id)) # 2. Load template template = template_service.get_template(template_id) if not template: raise ValueError(f"Template '{template_id}' not found") # 3. Build placeholder context from prospect data context = self._build_context(prospect) # 4. Create HostedSite + Store site_data = { "business_name": context["business_name"], "domain_name": prospect.domain_name, # used for clean subdomain slug "prospect_id": prospect_id, "contact_email": context.get("email"), "contact_phone": context.get("phone"), } if merchant_id: site_data["merchant_id"] = merchant_id site = hosted_site_service.create(db, site_data) # 5. Get the hosting platform_id from the store from app.modules.tenancy.models import StorePlatform store_platform = ( db.query(StorePlatform) .filter(StorePlatform.store_id == site.store_id) .first() ) platform_id = store_platform.platform_id if store_platform else None if not platform_id: logger.warning("No platform found for store %d", site.store_id) # 6. Populate CMS ContentPages from template pages_created = 0 if platform_id: pages_created = self._create_pages(db, site.store_id, platform_id, template, context) # 7. Apply StoreTheme theme_applied = self._apply_theme(db, site.store_id, template) # 8. Mark POC ready hosted_site_service.mark_poc_ready(db, site.id) db.flush() logger.info( "POC built for prospect %d: site=%d, store=%d, %d pages, template=%s", prospect_id, site.id, site.store_id, pages_created, template_id, ) return { "hosted_site_id": site.id, "store_id": site.store_id, "pages_created": pages_created, "theme_applied": theme_applied, "template_id": template_id, "subdomain": site.store.subdomain if site.store else None, } def _build_context(self, prospect) -> dict: """Build placeholder replacement context from prospect data.""" # Base context context = { "business_name": prospect.business_name or prospect.domain_name or "My Business", "domain": prospect.domain_name or "", "city": prospect.city or "", "address": "", "email": "", "phone": "", "meta_description": "", "about_paragraph": "", } # From contacts contacts = prospect.contacts or [] for c in contacts: if c.contact_type == "email" and not context["email"]: context["email"] = c.value elif c.contact_type == "phone" and not context["phone"]: context["phone"] = c.value elif c.contact_type == "address" and not context["address"]: context["address"] = c.value # From scraped content if prospect.scraped_content_json: try: scraped = json.loads(prospect.scraped_content_json) if scraped.get("meta_description"): context["meta_description"] = scraped["meta_description"] if scraped.get("paragraphs"): context["about_paragraph"] = scraped["paragraphs"][0] if scraped.get("headings") and not prospect.business_name: context["business_name"] = scraped["headings"][0] except (json.JSONDecodeError, KeyError): pass # From prospect fields if prospect.city: context["city"] = prospect.city elif context["address"]: # Try to extract city from address (last word after postal code) parts = context["address"].split() if len(parts) >= 2: context["city"] = parts[-1] return context def _replace_placeholders(self, text: str, context: dict) -> str: """Replace {{placeholder}} variables in text with context values.""" if not text: return text def replacer(match): key = match.group(1).strip() return context.get(key, match.group(0)) return re.sub(r"\{\{(\w+)\}\}", replacer, text) def _replace_in_structure(self, data, context: dict): """Recursively replace placeholders in a nested dict/list structure.""" if isinstance(data, str): return self._replace_placeholders(data, context) if isinstance(data, dict): return {k: self._replace_in_structure(v, context) for k, v in data.items()} if isinstance(data, list): return [self._replace_in_structure(item, context) for item in data] return data def _create_pages(self, db: Session, store_id: int, platform_id: int, template: dict, context: dict) -> int: """Create CMS ContentPages from template page definitions.""" from app.modules.cms.models.content_page import ContentPage count = 0 for page_def in template.get("pages", []): slug = page_def.get("slug", "") if not slug: continue # Replace placeholders in all text fields page_data = self._replace_in_structure(page_def, context) # Build content from content_translations if present content = page_data.get("content", "") content_translations = page_data.get("content_translations") if content_translations and not content: content = next(iter(content_translations.values()), "") page = ContentPage( platform_id=platform_id, store_id=store_id, is_platform_page=False, slug=slug, title=page_data.get("title", slug.title()), content=content or f"

{slug.title()} page content

", content_format="html", template=page_data.get("template", "default"), sections=page_data.get("sections"), title_translations=page_data.get("title_translations"), content_translations=content_translations, meta_description=context.get("meta_description"), is_published=page_data.get("is_published", True), published_at=datetime.now(UTC) if page_data.get("is_published", True) else None, show_in_header=page_data.get("show_in_header", False), show_in_footer=page_data.get("show_in_footer", False), ) db.add(page) count += 1 db.flush() return count def _apply_theme(self, db: Session, store_id: int, template: dict) -> bool: """Apply the template's theme to the store.""" from app.modules.cms.models.store_theme import StoreTheme theme_data = template.get("theme") if not theme_data: return False # Check if store already has a theme existing = db.query(StoreTheme).filter(StoreTheme.store_id == store_id).first() if existing: # Update existing theme = existing else: theme = StoreTheme(store_id=store_id) db.add(theme) colors = theme_data.get("colors", {}) theme.theme_name = theme_data.get("theme_name", "default") theme.colors = colors theme.font_family_heading = theme_data.get("font_family_heading", "Inter") theme.font_family_body = theme_data.get("font_family_body", "Inter") theme.layout_style = theme_data.get("layout_style", "grid") theme.header_style = theme_data.get("header_style", "fixed") db.flush() return True poc_builder_service = PocBuilderService()