"""Generation du graphe JSON-LD schema.org (hand-off pour injection dans les pages). Un noeud `Organization` (marque publique · #4) + un noeud `listing_type` (defaut `Residence`) par projet du master. ANTI-INVENTION (#6 · #10) : un projet n'expose une `offers`/`price` QUE s'il est `disponible` ET qu'une de ses typologies porte un `prix_depuis_usd` numerique sourcE dans le master. Pour tout statut de `STATUTS_SANS_PRIX` (en_developpement, bientot, en_processus) : AUCUN prix (jamais de chiffre fabrique). Devise = USD (devise primaire #10). Aucune horloge, tri stable -> sortie diffable. """ from __future__ import annotations from . import deps def _is_number(value) -> bool: return isinstance(value, (int, float)) and not isinstance(value, bool) def _offers_for(projet: dict) -> list[dict]: """Offres schema.org d'un projet — vide si non `disponible` ou sans prix sourcE.""" if projet["statut"] in deps.STATUTS_SANS_PRIX: return [] offers = [] for typo in projet.get("typologies", []): price = typo.get("prix_depuis_usd") if _is_number(price): offers.append({ "@type": "Offer", "name": typo.get("nom"), "price": price, "priceCurrency": deps.DEVISE_PRIMAIRE, "availability": "https://schema.org/InStock", }) return offers def generate(spec: dict, master: dict) -> dict: so = spec["schema_org"] base = spec["site"]["base_url"].rstrip("/") org_id = f"{base}/#organization" org = so["organization"] graph: list[dict] = [{ "@type": org["type"], "@id": org_id, "name": org["name"], "url": org["url"], }] for p in master["projets"]: slug = deps.slugify(p["nom"]) node = { "@type": so["listing_type"], "@id": f"{base}/projets/{slug}#residence", "name": p["nom"], "url": f"{base}/projets/{slug}", "brand": {"@id": org_id}, "address": { "@type": "PostalAddress", "addressCountry": so["country_code"], "addressLocality": p["localisation"], }, } offers = _offers_for(p) if offers: node["offers"] = offers graph.append(node) return {"@context": so["context"], "@graph": graph}