M1: implement operational core
Demo auth, seed import/reset, dashboard, vehicle/booking list+detail, audit trail. Backend: 19 tests passing, ruff clean. Frontend: React Router shell, typed API client, responsive pages. Verified end-to-end via curl and browser.
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import delete, insert
|
||||
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.customer import Customer
|
||||
from app.models.data_quality import DataQualityIssue
|
||||
from app.models.inspection import Inspection
|
||||
from app.models.maintenance import MaintenanceRecord
|
||||
from app.models.outbox import OutboxEvent
|
||||
from app.models.user import User
|
||||
from app.models.vehicle import Vehicle
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
DEMO_USERS = [
|
||||
{
|
||||
"public_ref": "USR-OPS",
|
||||
"display_name": "Amelie De Ridder",
|
||||
"role": "operations_manager",
|
||||
},
|
||||
{
|
||||
"public_ref": "USR-EMP",
|
||||
"display_name": "Karim Boujaddaine",
|
||||
"role": "rental_employee",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _parse_dt(value: str) -> datetime:
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
|
||||
|
||||
def _parse_bool(value: str) -> bool:
|
||||
return value.strip().lower() == "true"
|
||||
|
||||
|
||||
def _parse_optional_int(value: str) -> int | None:
|
||||
value = value.strip()
|
||||
return int(value) if value else None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SeedResult:
|
||||
counts: dict[str, int]
|
||||
|
||||
|
||||
def _seed_dir() -> Path:
|
||||
return Path(settings.seed_dir)
|
||||
|
||||
|
||||
def _read_csv(name: str) -> list[dict[str, str]]:
|
||||
path = _seed_dir() / name
|
||||
with path.open(newline="", encoding="utf-8") as handle:
|
||||
return list(csv.DictReader(handle))
|
||||
|
||||
|
||||
def clear_all(db: Session) -> None:
|
||||
for model in (
|
||||
AuditEvent,
|
||||
OutboxEvent,
|
||||
DataQualityIssue,
|
||||
Inspection,
|
||||
MaintenanceRecord,
|
||||
Booking,
|
||||
Vehicle,
|
||||
Customer,
|
||||
User,
|
||||
):
|
||||
db.execute(delete(model))
|
||||
|
||||
|
||||
def load_seed(db: Session) -> SeedResult:
|
||||
counts: dict[str, int] = {}
|
||||
|
||||
user_rows = [
|
||||
{"id": uuid.uuid4(), **user, "active": True} for user in DEMO_USERS
|
||||
]
|
||||
db.execute(insert(User), user_rows)
|
||||
counts["users"] = len(user_rows)
|
||||
|
||||
customer_id_by_ref: dict[str, uuid.UUID] = {}
|
||||
customer_rows = []
|
||||
for row in _read_csv("customers.csv"):
|
||||
cid = uuid.uuid4()
|
||||
customer_id_by_ref[row["public_ref"]] = cid
|
||||
customer_rows.append(
|
||||
{
|
||||
"id": cid,
|
||||
"public_ref": row["public_ref"],
|
||||
"first_name": row["first_name"],
|
||||
"last_name": row["last_name"],
|
||||
"email": row["email"] or None,
|
||||
"phone": row["phone"] or None,
|
||||
"postal_code": row["postal_code"] or None,
|
||||
"city": row["city"] or None,
|
||||
}
|
||||
)
|
||||
db.execute(insert(Customer), customer_rows)
|
||||
counts["customers"] = len(customer_rows)
|
||||
# Second pass for merged_into (self-referencing FK) since target must exist first.
|
||||
for row in _read_csv("customers.csv"):
|
||||
merged_ref = row.get("merged_into") or ""
|
||||
if merged_ref:
|
||||
db.execute(
|
||||
Customer.__table__.update()
|
||||
.where(Customer.id == customer_id_by_ref[row["public_ref"]])
|
||||
.values(merged_into_customer_id=customer_id_by_ref[merged_ref])
|
||||
)
|
||||
|
||||
vehicle_id_by_ref: dict[str, uuid.UUID] = {}
|
||||
vehicle_rows = []
|
||||
for row in _read_csv("vehicles.csv"):
|
||||
vid = uuid.uuid4()
|
||||
vehicle_id_by_ref[row["public_ref"]] = vid
|
||||
vehicle_rows.append(
|
||||
{
|
||||
"id": vid,
|
||||
"public_ref": row["public_ref"],
|
||||
"make": row["make"],
|
||||
"model": row["model"],
|
||||
"model_year": int(row["model_year"]),
|
||||
"registration_number": row["registration_number"],
|
||||
"location": row["location"],
|
||||
"operational_status": row["operational_status"],
|
||||
"odometer_km": int(row["odometer_km"]),
|
||||
"next_service_km": int(row["next_service_km"]),
|
||||
"active": _parse_bool(row["active"]),
|
||||
"version": 1,
|
||||
}
|
||||
)
|
||||
db.execute(insert(Vehicle), vehicle_rows)
|
||||
counts["vehicles"] = len(vehicle_rows)
|
||||
|
||||
booking_id_by_ref: dict[str, uuid.UUID] = {}
|
||||
booking_rows = []
|
||||
for row in _read_csv("bookings.csv"):
|
||||
bid = uuid.uuid4()
|
||||
booking_id_by_ref[row["public_ref"]] = bid
|
||||
booking_rows.append(
|
||||
{
|
||||
"id": bid,
|
||||
"public_ref": row["public_ref"],
|
||||
"customer_id": customer_id_by_ref[row["customer_ref"]],
|
||||
"vehicle_id": vehicle_id_by_ref[row["vehicle_ref"]],
|
||||
"starts_at": _parse_dt(row["starts_at"]),
|
||||
"ends_at": _parse_dt(row["ends_at"]),
|
||||
"status": row["status"],
|
||||
"start_odometer_km": _parse_optional_int(row["start_odometer_km"]),
|
||||
"end_odometer_km": _parse_optional_int(row["end_odometer_km"]),
|
||||
"requirements_complete": _parse_bool(row["requirements_complete"]),
|
||||
}
|
||||
)
|
||||
db.execute(insert(Booking), booking_rows)
|
||||
counts["bookings"] = len(booking_rows)
|
||||
|
||||
inspection_rows = []
|
||||
for row in _read_csv("inspections.csv"):
|
||||
inspection_rows.append(
|
||||
{
|
||||
"id": uuid.uuid4(),
|
||||
"public_ref": row["public_ref"],
|
||||
"booking_id": booking_id_by_ref[row["booking_ref"]],
|
||||
"vehicle_id": vehicle_id_by_ref[row["vehicle_ref"]],
|
||||
"type": row["type"],
|
||||
"fuel_level_percent": int(row["fuel_level_percent"]),
|
||||
"cleanliness_ok": _parse_bool(row["cleanliness_ok"]),
|
||||
"damage_reported": _parse_bool(row["damage_reported"]),
|
||||
"technical_warning": _parse_bool(row["technical_warning"]),
|
||||
"odometer_km": int(row["odometer_km"]),
|
||||
"completed_at": _parse_dt(row["completed_at"]),
|
||||
"completed_by": None,
|
||||
}
|
||||
)
|
||||
db.execute(insert(Inspection), inspection_rows)
|
||||
counts["inspections"] = len(inspection_rows)
|
||||
|
||||
maintenance_rows = []
|
||||
for row in _read_csv("maintenance.csv"):
|
||||
maintenance_rows.append(
|
||||
{
|
||||
"id": uuid.uuid4(),
|
||||
"public_ref": row["public_ref"],
|
||||
"vehicle_id": vehicle_id_by_ref[row["vehicle_ref"]],
|
||||
"occurred_at": _parse_dt(row["occurred_at"]),
|
||||
"odometer_km": int(row["odometer_km"]),
|
||||
"category": row["category"],
|
||||
"summary": row["summary"],
|
||||
}
|
||||
)
|
||||
db.execute(insert(MaintenanceRecord), maintenance_rows)
|
||||
counts["maintenance"] = len(maintenance_rows)
|
||||
|
||||
def resolve_entity(entity_ref: str) -> tuple[str, uuid.UUID]:
|
||||
if entity_ref.startswith("CUS-"):
|
||||
return "customer", customer_id_by_ref[entity_ref]
|
||||
return "vehicle", vehicle_id_by_ref[entity_ref]
|
||||
|
||||
dq_rows = []
|
||||
now = datetime.now(UTC)
|
||||
for row in _read_csv("data_quality_issues.csv"):
|
||||
entity_type, entity_id = resolve_entity(row["entity_ref"])
|
||||
related_ref = row.get("related_ref") or ""
|
||||
dq_rows.append(
|
||||
{
|
||||
"id": uuid.uuid4(),
|
||||
"public_ref": row["public_ref"],
|
||||
"rule_type": row["rule_type"],
|
||||
"entity_type": entity_type,
|
||||
"entity_id": entity_id,
|
||||
"severity": row["severity"],
|
||||
"status": row["status"],
|
||||
"evidence_json": {
|
||||
"summary": row["evidence"],
|
||||
"entity_ref": row["entity_ref"],
|
||||
"related_refs": related_ref.split("|") if related_ref else [],
|
||||
},
|
||||
"proposed_action_json": {},
|
||||
"detected_at": now,
|
||||
"resolved_at": now if row["status"] == "resolved" else None,
|
||||
"resolved_by": "USR-OPS" if row["status"] == "resolved" else None,
|
||||
}
|
||||
)
|
||||
db.execute(insert(DataQualityIssue), dq_rows)
|
||||
counts["data_quality_issues"] = len(dq_rows)
|
||||
|
||||
outbox_rows = []
|
||||
for row in _read_csv("workflow_runs.csv"):
|
||||
booking_id = booking_id_by_ref.get(row["aggregate_ref"])
|
||||
outbox_rows.append(
|
||||
{
|
||||
"event_id": uuid.UUID(row["event_id"]),
|
||||
"event_type": row["event_type"],
|
||||
"aggregate_type": "booking",
|
||||
"aggregate_id": booking_id or uuid.uuid4(),
|
||||
"payload_json": {"aggregate_ref": row["aggregate_ref"]},
|
||||
"occurred_at": _parse_dt(row["occurred_at"]),
|
||||
"delivery_status": row["status"],
|
||||
"attempts": int(row["attempts"]),
|
||||
"next_attempt_at": None,
|
||||
"last_error": row["last_error"] or None,
|
||||
"external_run_id": None,
|
||||
}
|
||||
)
|
||||
db.execute(insert(OutboxEvent), outbox_rows)
|
||||
counts["workflow_runs"] = len(outbox_rows)
|
||||
|
||||
return SeedResult(counts=counts)
|
||||
|
||||
|
||||
def reset_and_seed(db: Session) -> SeedResult:
|
||||
clear_all(db)
|
||||
result = load_seed(db)
|
||||
db.commit()
|
||||
return result
|
||||
Reference in New Issue
Block a user