from __future__ import annotations from datetime import datetime from typing import Annotated, Any, Literal from pydantic import BaseModel, Field Role = Literal["operations_manager", "rental_employee"] class DemoLoginRequest(BaseModel): role: Role class PasswordLoginRequest(BaseModel): email: str = Field(min_length=3, max_length=320) password: str = Field(min_length=8, max_length=256) class UserOut(BaseModel): public_ref: str email: str | None display_name: str role: Role active: bool class CreateUserRequest(BaseModel): email: str = Field(min_length=3, max_length=320) display_name: str = Field(min_length=2, max_length=120) role: Role password: str = Field(min_length=8, max_length=256) class UpdateUserRequest(BaseModel): display_name: str | None = Field(default=None, min_length=2, max_length=120) role: Role | None = None active: bool | None = None password: str | None = Field(default=None, min_length=8, max_length=256) class CurrentUser(BaseModel): public_ref: str display_name: str role: Role class VehicleOut(BaseModel): public_ref: str make: str model: str model_year: int registration_number: str location: str operational_status: str odometer_km: int next_service_km: int active: bool attention: bool = False class VehiclePageOut(BaseModel): items: list[VehicleOut] page: int page_size: int total: int total_pages: int class BookingSummaryOut(BaseModel): public_ref: str customer_ref: str vehicle_ref: str starts_at: datetime ends_at: datetime status: str class BookingOut(BookingSummaryOut): start_odometer_km: int | None end_odometer_km: int | None requirements_complete: bool customer_name: str class CreateBookingRequest(BaseModel): customer_ref: str = Field(min_length=3, max_length=20) vehicle_ref: str = Field(min_length=3, max_length=20) starts_at: datetime ends_at: datetime requirements_complete: bool = True class CustomerOptionOut(BaseModel): public_ref: str display_name: str email: str | None class AvailableVehicleOut(BaseModel): public_ref: str make: str model: str registration_number: str location: str operational_status: str class CancelBookingRequest(BaseModel): reason: str = Field(min_length=3, max_length=500) class CheckoutBookingRequest(BaseModel): start_odometer_km: Annotated[int, Field(ge=0)] fuel_level_percent: Annotated[int, Field(ge=0, le=100)] cleanliness_ok: bool damage_reported: bool = False technical_warning: bool = False notes: str | None = Field(default=None, max_length=2000) class CheckoutBookingResult(BaseModel): booking_ref: str vehicle_ref: str inspection_ref: str booking_status: str resulting_vehicle_status: str activated: bool attention_reasons: list[str] class BookingPageOut(BaseModel): items: list[BookingOut] page: int page_size: int total: int total_pages: int class RegisterReturnRequest(BaseModel): end_odometer_km: Annotated[int, Field(ge=0)] fuel_level_percent: Annotated[int, Field(ge=0, le=100)] cleanliness_ok: bool damage_reported: bool technical_warning: bool notes: str | None = Field(default=None, max_length=2000) class NextBookingRisk(BaseModel): booking_ref: str starts_at: datetime at_risk: bool class RegisterReturnResult(BaseModel): booking_ref: str vehicle_ref: str inspection_ref: str resulting_vehicle_status: str odometer_regression: bool quality_issue_ref: str | None workflow_event_id: str next_booking_risk: NextBookingRisk | None class ReturnPreviewResult(BaseModel): booking_ref: str vehicle_ref: str canonical_odometer_km: int submitted_odometer_km: int odometer_regression: bool resulting_odometer_km: int resulting_vehicle_status: str status_reason: str status_reason_code: str status_reason_params: dict[str, str | int] = {} would_create_quality_issue: bool attention_reasons: list[str] next_booking_risk: NextBookingRisk | None class InspectionOut(BaseModel): public_ref: str booking_ref: str type: str fuel_level_percent: int cleanliness_ok: bool damage_reported: bool technical_warning: bool odometer_km: int completed_at: datetime class MaintenanceOut(BaseModel): public_ref: str occurred_at: datetime odometer_km: int category: str summary: str class CreateMaintenanceRequest(BaseModel): occurred_at: datetime odometer_km: Annotated[int, Field(ge=0)] category: Literal["periodic_service", "repair", "inspection", "tyres", "other"] summary: str = Field(min_length=3, max_length=2000) next_service_km: Annotated[int | None, Field(default=None, ge=0)] mark_maintenance: bool = True class ReleaseVehicleRequest(BaseModel): reason: str = Field(min_length=3, max_length=500) class DataQualityIssueOut(BaseModel): public_ref: str rule_type: str entity_type: str entity_ref: str severity: str status: str evidence: dict[str, Any] detected_at: datetime resolved_at: datetime | None = None class DataQualityIssuePageOut(BaseModel): items: list[DataQualityIssueOut] page: int page_size: int total: int total_pages: int class DataQualityIssueDetailOut(DataQualityIssueOut): entity_snapshot: dict[str, Any] | None = None related_snapshots: list[dict[str, Any]] = Field(default_factory=list) class MergeCustomersRequest(BaseModel): survivor_ref: str field_overrides: dict[str, str] | None = None class MergeCustomersResult(BaseModel): issue_ref: str survivor_ref: str loser_ref: str rewired_bookings: int class ScanResultOut(BaseModel): created: dict[str, int] class WorkflowErrorReportIn(BaseModel): workflow_id: str = Field(max_length=120) workflow_name: str = Field(max_length=200) execution_id: str = Field(max_length=120) failed_at: datetime error_category: Literal[ "timeout", "authError", "connectionError", "httpError", "validationError", "unknown" ] error_summary: str = Field(max_length=500) trigger_context: str | None = Field(default=None, max_length=200) correlation_id: str | None = None attempt: int = Field(default=1, ge=1, le=1000) retry_action: str | None = Field(default=None, max_length=200) class WorkflowErrorReportResult(BaseModel): status: Literal["registered", "already_registered"] execution_id: str occurred_at: datetime class ProcedureDocumentOut(BaseModel): id: str language: str document_id: str title: str version: str content: str content_hash: str class ProcedureListOut(BaseModel): documents: list[ProcedureDocumentOut] class ProcedureSyncResultIn(BaseModel): execution_id: str = Field(max_length=120) synced: int = Field(ge=0) failed: int = Field(default=0, ge=0) class ProcedureSyncResultResult(BaseModel): status: Literal["registered", "already_registered"] execution_id: str occurred_at: datetime class ProvideFieldsRequest(BaseModel): fields: dict[str, str] class ResolveOdometerRegressionRequest(BaseModel): decision: Literal["retain_canonical", "correct_reading"] booking_ref: str | None = None corrected_odometer_km: Annotated[int, Field(ge=0)] | None = None note: str | None = Field(default=None, max_length=500) class ResolveOverlapRequest(BaseModel): booking_ref: str note: str | None = Field(default=None, max_length=500) class VehicleStatusFactsOut(BaseModel): active_booking_refs: list[str] overlapping_booking_pairs: list[list[str]] service_threshold_reached: bool odometer_km: int next_service_km: int open_booking_overlap_issue_ref: str | None = None class StatusRecommendationOut(BaseModel): current_status: str recommended_status: str | None recommendation_code: str safe_to_apply: bool manual_review_required: bool facts: VehicleStatusFactsOut blocking_reasons: list[str] recommendation_token: str class ApplyRecommendedStatusRequest(BaseModel): recommendation_token: str class ApplyRecommendedStatusResult(BaseModel): issue: DataQualityIssueOut applied_status: str reason_code: str class SearchResultItem(BaseModel): type: Literal["vehicle", "booking", "data_quality_issue", "section"] label: str detail_code: str detail_params: dict[str, str] = {} link: str class SearchResponse(BaseModel): query: str results: list[SearchResultItem] class N8nWorkflowEvidence(BaseModel): name: str built: bool last_seen_at: datetime | None state: Literal["no_evidence", "healthy", "stale", "failed"] = "no_evidence" last_status: Literal["succeeded", "failed"] | None = None last_execution_id: str | None = None class N8nHeartbeatIn(BaseModel): workflow_id: str = Field(min_length=1, max_length=120) workflow_name: str = Field(min_length=1, max_length=200) execution_id: str = Field(min_length=1, max_length=120) status: Literal["succeeded", "failed"] class N8nHeartbeatResult(BaseModel): status: Literal["registered", "already_registered"] execution_id: str occurred_at: datetime class N8nErrorHandlerStatus(BaseModel): total_failures_registered: int latest_failure_at: datetime | None latest_failure_workflow: str | None class N8nIntegrationStatus(BaseModel): configured: bool dispatch_enabled: bool state: Literal["disabled", "unavailable", "degraded", "operational", "no_evidence"] pending: int #: Every failed delivery, staged and real together -- the number a viewer sees in #: the run list. failed: int #: Failures that were not planted by the demo seed. This is the only failure count #: that may influence `state`. unexpected_failed: int = 0 #: Prepared demo failures (see `app.models.outbox.DEMO_SCENARIO_ERROR_CODE`). #: Present so the UI can label them instead of implying the automation is broken. demo_scenario_failed: int = 0 delivering: int succeeded: int latest_success_at: datetime | None #: Most recent *real* failure; a staged one never sets this. latest_failure_at: datetime | None latest_demo_scenario_at: datetime | None = None expected_workflow_count: int known_workflow_count: int workflows: list[N8nWorkflowEvidence] error_handler: N8nErrorHandlerStatus class McpHubIntegrationStatus(BaseModel): registration_enabled: bool state: Literal["not_configured", "no_evidence", "operational"] total_calls: int last_tool: str | None = None last_client: str | None = None last_called_at: datetime | None = None hub_reachable: bool | None = None class IntegrationStatusOut(BaseModel): n8n: N8nIntegrationStatus mcp_hub: McpHubIntegrationStatus class DemoScenarioOut(BaseModel): id: str estimated_minutes: int required_roles: list[Role] start_path: str ready: bool blocked_reason_code: str | None = None blocked_reason_params: dict[str, str] = {} class DemoIntegrationSummaryOut(BaseModel): key: Literal["n8n", "ragcore", "mcp_hub"] status_code: str detail_code: str detail_params: dict[str, str | int] = {} class DemoManifestOut(BaseModel): demo_mode: bool organization_name: str timezone: str synthetic_data: bool allow_reset: bool last_reset_at: datetime | None anchor_date: str | None guide_available: bool required_roles: list[Role] scenarios: list[DemoScenarioOut] integrations: list[DemoIntegrationSummaryOut] class VehicleDetailOut(VehicleOut): bookings: list[BookingSummaryOut] = Field(default_factory=list) inspections: list[InspectionOut] = Field(default_factory=list) maintenance: list[MaintenanceOut] = Field(default_factory=list) quality_issues: list[DataQualityIssueOut] = Field(default_factory=list) class DashboardMetrics(BaseModel): available: int rented: int cleaning: int maintenance: int blocked: int open_quality_issues: int pending_or_failed_workflows: int class EvidenceSignalOut(BaseModel): code: str params: dict[str, Any] = Field(default_factory=dict) class AttentionItem(BaseModel): kind: Literal["quality_issue", "vehicle"] severity: str rule_type: str evidence_signals: list[EvidenceSignalOut] = Field(default_factory=list) link_type: Literal["vehicle", "booking", "customer"] link_ref: str issue_ref: str | None = None class TodayItem(BaseModel): kind: Literal["departure", "return"] booking_ref: str vehicle_ref: str scheduled_at: datetime class AutomationRunOut(BaseModel): event_id: str event_type: str aggregate_ref: str status: str attempts: int last_error: str | None last_error_code: str | None #: True for the deliberately seeded demo failure. The UI uses this to label the run #: as a prepared scenario and to offer the demo retry, instead of presenting it as #: an unexplained production error. is_demo_scenario: bool = False occurred_at: datetime class DashboardOut(BaseModel): metrics: DashboardMetrics attention_items: list[AttentionItem] today: list[TodayItem] recent_automation: list[AutomationRunOut] class OperationsSummaryOut(BaseModel): tenant: str metrics: DashboardMetrics class AttentionVehicleOut(BaseModel): vehicle_ref: str severity: Literal["low", "medium", "high"] rule_type: str summary: str detected_at: datetime class McpVehicleDetailOut(BaseModel): public_ref: str make: str model: str model_year: int location: str operational_status: str odometer_km: int next_service_km: int open_quality_issue_count: int current_booking_ref: str | None class McpKnowledgeSearchRequest(BaseModel): question: str = Field(min_length=3, max_length=1000) max_sources: int = Field(default=4, ge=1, le=8) locale: Literal["nl-BE", "en-GB", "fr-BE"] = "en-GB" class AuditEventOut(BaseModel): id: str actor_type: str actor_label: str action: str entity_type: str entity_id: str | None entity_ref: str | None = None entity_link: str | None = None correlation_id: str occurred_at: datetime before: dict[str, Any] | None = None after: dict[str, Any] | None = None metadata: dict[str, Any] | None = None class AuditEventPageOut(BaseModel): items: list[AuditEventOut] page: int page_size: int total: int total_pages: int