from __future__ import annotations from datetime import datetime from sqlalchemy import select from sqlalchemy.orm import Session from app.core.config import get_settings from app.models.audit import AuditEvent from app.models.booking import Booking from app.models.data_quality import DataQualityIssue from app.models.outbox import OutboxEvent from app.schemas import DemoIntegrationSummaryOut, DemoManifestOut, DemoScenarioOut from app.services.integration_status import derive_n8n_status from app.services.knowledge import get_knowledge_provider settings = get_settings() # The default name matches the project's locked fictitious tenant (see PROJECT_STATE.md # "Locked decisions"; the same slug already backs `ragcore_tenant`) — this surfaces that # existing decision in the UI rather than inventing a new one. Configurable via # DEMO_ORGANIZATION_NAME so a redeployment can rebrand the fictional org without a code change. ORGANIZATION_DESCRIPTION = ( "MobilityOps brengt voertuig-, boekings- en operationele gegevens samen, " "ondersteunt verhuurprocessen, detecteert datakwaliteitsproblemen en " "automatiseert gecontroleerde vervolgstappen." ) _FAILED_DEMO_EVENT_ID = "00000000-0000-4000-8000-000000000020" _N8N_STATE_LABELS = { "disabled": "Niet gekoppeld", "unavailable": "Verwerking mislukt", "degraded": "Opnieuw proberen mogelijk", "operational": "Operationeel", "no_evidence": "Voorbereid", } def _last_reset(db: Session) -> tuple[datetime | None, str | None]: marker = db.scalar( select(AuditEvent) .where(AuditEvent.action == "demo_data_seeded") .order_by(AuditEvent.occurred_at.desc()) ) if marker is None: return None, None metadata = marker.metadata_json or {} return marker.occurred_at, metadata.get("anchor_date") def _scenarios(db: Session) -> list[DemoScenarioOut]: booking = db.scalar(select(Booking).where(Booking.public_ref == "BK-DEMO-RETURN")) duplicate_issue = db.scalar( select(DataQualityIssue).where(DataQualityIssue.public_ref == "DQ-DEMO-DUPLICATE") ) overlap_issue = db.scalar( select(DataQualityIssue).where(DataQualityIssue.public_ref == "DQ-DEMO-OVERLAP") ) failed_run = db.scalar( select(OutboxEvent).where(OutboxEvent.event_id == _FAILED_DEMO_EVENT_ID) ) knowledge_health = get_knowledge_provider().health() reset_hint = "Reset de demo-data om dit scenario opnieuw beschikbaar te maken." return [ DemoScenarioOut( id="return-anomaly", title="Retour met afwijkende kilometerstand", operational_problem=( "Een voertuig komt terug met een kilometerstand die lager ligt dan de " "laatst geregistreerde stand — een teken van een foutieve invoer of een " "verwisseld voertuig." ), estimated_minutes=3, required_roles=["rental_employee", "operations_manager"], start_path=f"/bookings/{booking.public_ref}" if booking else "/bookings", demonstrates=( "Retourverwerking, automatische detectie van datakwaliteitsproblemen en de " "audit trail die daaruit ontstaat." ), ready=bool( booking and booking.status == "active" and booking.end_odometer_km is None ), blocked_reason=( None if booking and booking.status == "active" and booking.end_odometer_km is None else ( f"Demoboeking BK-DEMO-RETURN niet gevonden. {reset_hint}" if booking is None else f"Deze boeking is al verwerkt sinds de laatste reset. {reset_hint}" ) ), ), DemoScenarioOut( id="duplicate-customer", title="Mogelijke dubbele klant samenvoegen", operational_problem=( "Twee klantprofielen delen hetzelfde e-mailadres en telefoonnummer — " "waarschijnlijk dezelfde persoon, twee keer geregistreerd." ), estimated_minutes=3, required_roles=["operations_manager"], start_path=( f"/data-quality/{duplicate_issue.public_ref}" if duplicate_issue else "/data-quality" ), demonstrates=( "Samenvoegen van klanten met behoud van boekingsgeschiedenis en audit trail." ), ready=bool(duplicate_issue and duplicate_issue.status == "open"), blocked_reason=( None if duplicate_issue and duplicate_issue.status == "open" else ( f"Demo-issue DQ-DEMO-DUPLICATE niet gevonden. {reset_hint}" if duplicate_issue is None else f"Dit issue is al opgelost sinds de laatste reset. {reset_hint}" ) ), ), DemoScenarioOut( id="booking-overlap", title="Overlappende boekingen herstellen", operational_problem=( "Eén voertuig staat dubbel gereserveerd voor overlappende periodes — een " "planningsfout die vóór vertrek moet worden opgelost." ), estimated_minutes=2, required_roles=["operations_manager"], start_path=( f"/data-quality/{overlap_issue.public_ref}" if overlap_issue else "/data-quality" ), demonstrates="Detectie en gecontroleerde oplossing van planningsconflicten.", ready=bool(overlap_issue and overlap_issue.status == "open"), blocked_reason=( None if overlap_issue and overlap_issue.status == "open" else ( f"Demo-issue DQ-DEMO-OVERLAP niet gevonden. {reset_hint}" if overlap_issue is None else f"Dit issue is al opgelost sinds de laatste reset. {reset_hint}" ) ), ), DemoScenarioOut( id="automation-retry", title="Mislukte automatisering opnieuw proberen", operational_problem=( "Eén eerdere gebeurtenis kon niet worden afgeleverd aan de automatisering " "door een gesimuleerde verbindingsfout." ), estimated_minutes=2, required_roles=["operations_manager"], start_path="/automation", demonstrates=( "Betrouwbare aflevering met begrensde herpogingen en zichtbare foutstatus." ), ready=bool(failed_run and failed_run.delivery_status == "failed"), blocked_reason=( None if failed_run and failed_run.delivery_status == "failed" else ( f"Gesimuleerde mislukte gebeurtenis niet gevonden. {reset_hint}" if failed_run is None else f"Deze gebeurtenis is al hersteld sinds de laatste reset. {reset_hint}" ) ), ), DemoScenarioOut( id="knowledge-question", title="Een procedurevraag stellen", operational_problem=( "Een medewerker weet niet zeker welke procedure van toepassing is bij een " "specifieke operationele situatie." ), estimated_minutes=2, required_roles=["rental_employee", "operations_manager"], start_path="/knowledge", demonstrates=( "Antwoorden met brongebaseerde onderbouwing uit een afgebakende demokennisbank." ), ready=knowledge_health.available, blocked_reason=( None if knowledge_health.available else "De demokennisbank is momenteel niet beschikbaar." ), ), ] def _integrations(db: Session) -> list[DemoIntegrationSummaryOut]: n8n = derive_n8n_status(db) knowledge_health = get_knowledge_provider().health() return [ DemoIntegrationSummaryOut( key="n8n", label="Automatisering (n8n)", status_label=_N8N_STATE_LABELS.get(n8n.state, n8n.state), detail=f"{n8n.succeeded} geslaagd, {n8n.failed} mislukt, {n8n.pending} in wachtrij.", ), DemoIntegrationSummaryOut( key="ragcore", label="Kennisassistent (RAGcore)", status_label=( "Demomodus — lokale kennisprovider" if knowledge_health.provider != "ragcore" else "Operationeel" ), detail=knowledge_health.detail, ), DemoIntegrationSummaryOut( key="mcp_hub", label="ITWorx MCP Hub", status_label=( "Operationeel" if settings.mcp_hub_registration_enabled else "Niet gekoppeld" ), detail="Voorbereid voor toekomstige, gecontroleerde tool-aanroepen vanuit de Hub.", ), ] def build_demo_manifest(db: Session) -> DemoManifestOut: last_reset_at, anchor_date = _last_reset(db) return DemoManifestOut( demo_mode=settings.mobilityops_demo_mode, organization_name=settings.demo_organization_name, organization_description=ORGANIZATION_DESCRIPTION, timezone=settings.demo_timezone, synthetic_data=True, allow_reset=settings.demo_allow_reset, last_reset_at=last_reset_at, anchor_date=anchor_date, guide_available=True, required_roles=["operations_manager", "rental_employee"], scenarios=_scenarios(db), integrations=_integrations(db), )