Files
MobilityOps/backend/app/schemas.py
T
NuklearRabbit f5212959b4 feat(returns): add authoritative return preview
The return-review step predicted operational consequences independently in
the frontend, and got it wrong: damage or a technical warning was described
as routing to "maintenance" when the actual domain rule (returns.py) routes
it to "blocked", and the no-contradiction case was described as becoming
"available" when the vehicle actually always goes to "cleaning" first
(only reaching "maintenance" if the service threshold was crossed).

Extract the evaluation returns.py already performed inline into a pure
evaluate_return() function with no writes -- resulting status (with an
explanation), odometer regression, would-create-quality-issue,
next-booking-risk -- and share it between a new non-mutating
POST /bookings/{ref}/return-preview endpoint and the existing commit path,
so preview and commit can never drift apart again. The result screen also
now distinguishes local commit success from n8n delivery (still queued/
unconfirmed) instead of implying both succeeded, and links to any created
quality issue for Operations Manager.
2026-08-02 05:33:12 +02:00

240 lines
5.3 KiB
Python

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 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 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 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
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 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 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 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 AttentionItem(BaseModel):
kind: Literal["quality_issue", "vehicle"]
severity: str
title: str
detail: str
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
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)
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