Five rule scanners (duplicate customers, missing fields, odometer regression, booking overlap, status conflict) run automatically after seed and via an explicit scan endpoint. Issue defer/reject/merge-customers endpoints with transactional customer merge (booking rewiring, tombstone, audit). Data Quality nav + workbench UI with two-column duplicate comparison and inline (non-native) confirm. Dashboard attention items now link to issues. 35 backend tests passing, ruff clean. Fixed a real false-positive bug in odometer-regression detection found through iteration on seed data, and two TS narrowing errors. Verified end-to-end via browser: S2 merge and S4 overlap scenarios.
191 lines
4.1 KiB
Python
191 lines
4.1 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Any, Literal
|
|
|
|
from pydantic import BaseModel, Field, conint
|
|
|
|
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: conint(ge=0)
|
|
fuel_level_percent: conint(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 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 AuditEventOut(BaseModel):
|
|
id: str
|
|
actor_type: str
|
|
actor_label: str
|
|
action: str
|
|
entity_type: str
|
|
entity_id: str | None
|
|
correlation_id: str
|
|
occurred_at: datetime
|
|
metadata: dict[str, Any] | None = None
|