Publish curated MobilityOps source snapshot
This commit is contained in:
commit
7adc072f5e
317 files changed
+48851
No files matched your search
@@ -0,0 +1,650 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Annotated, Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, StringConstraints
|
||||
|
||||
Role = Literal["operations_manager", "rental_employee"]
|
||||
NonBlankOperationalReason = Annotated[
|
||||
str,
|
||||
StringConstraints(strip_whitespace=True, min_length=3, max_length=500),
|
||||
]
|
||||
|
||||
|
||||
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
|
||||
attention_reason: str | None = None
|
||||
service_remaining_km: int
|
||||
next_booking_ref: str | None = None
|
||||
next_booking_at: datetime | None = None
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class CompleteBookingRequirementsRequest(BaseModel):
|
||||
confirmation: NonBlankOperationalReason
|
||||
|
||||
|
||||
class RescheduleBookingRequest(BaseModel):
|
||||
starts_at: datetime
|
||||
ends_at: datetime
|
||||
reason: NonBlankOperationalReason
|
||||
|
||||
|
||||
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: NonBlankOperationalReason
|
||||
|
||||
|
||||
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
|
||||
correlation_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: NonBlankOperationalReason
|
||||
|
||||
|
||||
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
|
||||
due_at: datetime | None = None
|
||||
assigned_to_ref: str | None = None
|
||||
assigned_to_name: str | None = None
|
||||
overdue: bool = False
|
||||
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 BulkDataQualityWorkRequest(BaseModel):
|
||||
issue_refs: list[str] = Field(min_length=1, max_length=25)
|
||||
assigned_to_ref: str | None = Field(default=None, min_length=3, max_length=20)
|
||||
clear_assignment: bool = False
|
||||
due_at: datetime | None = None
|
||||
clear_due_at: bool = False
|
||||
|
||||
|
||||
class BulkDataQualityWorkResult(BaseModel):
|
||||
updated: list[DataQualityIssueOut]
|
||||
|
||||
|
||||
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: uuid.UUID | 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 OidcStatusOut(BaseModel):
|
||||
enabled: bool
|
||||
provider_name: str | None = None
|
||||
|
||||
|
||||
class PrivacyRetentionOut(BaseModel):
|
||||
minimum_booking_retention_days: int
|
||||
audit_retention_days: int
|
||||
customers_total: int
|
||||
customers_anonymized: int
|
||||
customers_eligible: int
|
||||
|
||||
|
||||
class CustomerAnonymizeRequest(BaseModel):
|
||||
confirmation: str = Field(min_length=1, max_length=20)
|
||||
reason: str = Field(min_length=8, max_length=500)
|
||||
|
||||
|
||||
class CustomerAnonymizeResult(BaseModel):
|
||||
public_ref: str
|
||||
anonymized_at: datetime
|
||||
status: Literal["anonymized", "already_anonymized"]
|
||||
|
||||
|
||||
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 ReturnCallbackIn(BaseModel):
|
||||
"""Body of the n8n return follow-up callback.
|
||||
|
||||
n8n forwards its whole item (``JSON.stringify($json)``), so unknown keys are ignored;
|
||||
only the fields we persist are validated and bounded.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
event_id: uuid.UUID
|
||||
correlation_id: uuid.UUID
|
||||
follow_up: str | None = Field(default=None, max_length=200)
|
||||
summary: str | None = Field(default=None, max_length=2000)
|
||||
|
||||
|
||||
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
|
||||
Reference in new issue
Block a user