Final acceptance audit: fix all mypy defects, verify full journey matrix and degraded modes

Ran a dedicated post-M7 release-readiness audit. Found and fixed the one real gap: mypy
was a declared dev dependency but had never been run in any milestone's validation loop.
Fixed all 43 pre-existing type errors it surfaced, including two genuine defensive-
programming gaps (unguarded Optional vehicle/customer lookups that could have crashed
with unhandled 500s instead of clean 404/401 responses) rather than suppressing them.
make lint now runs ruff + mypy; mypy reports zero errors across 44 source files.

Re-verified end to end against a genuinely wiped-volumes clean checkout: automatic
migrations, deterministic seed, 66/66 backend tests, and the full user-journey matrix
(login, dashboard, vehicle/booking detail, return workflow, invalid-mileage rejection,
data-quality review, duplicate-customer merge, audit trail, Knowledge Assistant, n8n,
MCP Hub) via curl and Playwright.

Live-verified both external-dependency degraded modes, not just unit tests: stopped n8n
mid-flow and confirmed a return still commits with the outbox event staying pending and
retrying with backoff, then self-healing to succeeded with zero manual intervention once
n8n came back; verified RAGcore's unavailable-degradation path against an unreachable
host. Added frontend/e2e/interactive-elements.spec.ts (11 tests covering every nav item,
filter, tab, and role boundary) alongside the existing demo script test — 12/12 e2e tests
passing.

Verified no secrets are committed (.env never tracked, clean git history scan) and
.env.example covers every operator-configurable setting. Confirmed no placeholders,
TODOs, fake responses, hardcoded metrics, or dead routes anywhere in the codebase.

Updated README.md with an honest integration-status section and PROJECT_STATE.md with
the full audit findings. Added artifacts/final-acceptance/summary.md as the authoritative
final evidence document (commands, results, URLs, demo access, integration status per
external dependency, known limitations, deployment instructions, five-minute demo flow).
This commit is contained in:
NuklearRabbit
2026-08-02 01:27:01 +02:00
parent 108b5d04fc
commit 4bf9afbeff
15 changed files with 604 additions and 48 deletions
+5 -5
View File
@@ -8,9 +8,10 @@ from sqlalchemy.orm import Session
from app.core.config import get_settings
from app.core.db import SessionLocal
from app.core.security import SessionPayload, read_session_token
from app.schemas import CurrentUser
from app.schemas import CurrentUser, Role
settings = get_settings()
_VALID_ROLES = frozenset(Role.__args__) # type: ignore[attr-defined]
def get_db() -> Generator[Session, None, None]:
@@ -24,11 +25,10 @@ def get_db() -> Generator[Session, None, None]:
def get_current_user(request: Request) -> CurrentUser:
token = request.cookies.get(settings.session_cookie_name)
payload: SessionPayload | None = read_session_token(token) if token else None
if payload is None:
if payload is None or payload.role not in _VALID_ROLES:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
return CurrentUser(
public_ref=payload.public_ref, display_name=payload.display_name, role=payload.role
)
role: Role = payload.role # type: ignore[assignment]
return CurrentUser(public_ref=payload.public_ref, display_name=payload.display_name, role=role)
def require_operations_manager(
+2
View File
@@ -61,6 +61,8 @@ def get_booking(
raise HTTPException(status_code=404, detail="Booking not found")
customer = db.get(Customer, booking.customer_id)
vehicle = db.get(Vehicle, booking.vehicle_id)
if customer is None or vehicle is None:
raise HTTPException(status_code=500, detail="Booking references a missing record")
return _to_out(booking, customer, vehicle)
+3
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
from datetime import date, datetime
from typing import Literal
from fastapi import APIRouter, Depends
from sqlalchemy import select
@@ -49,6 +50,8 @@ def get_dashboard(
).all()
attention_items = []
for issue in issues:
entity: Vehicle | Customer | None
link_type: Literal["vehicle", "customer"]
if issue.entity_type == "vehicle":
entity = vehicles_by_id.get(issue.entity_id)
link_type = "vehicle"
+17 -17
View File
@@ -56,28 +56,28 @@ def list_issues(
def _snapshot(entity_type: str, ref: str, db: Session) -> dict | None:
if entity_type == "customer":
obj = db.scalar(select(Customer).where(Customer.public_ref == ref))
if obj is None:
customer = db.scalar(select(Customer).where(Customer.public_ref == ref))
if customer is None:
return None
return {
"public_ref": obj.public_ref,
"first_name": obj.first_name,
"last_name": obj.last_name,
"email": obj.email,
"phone": obj.phone,
"postal_code": obj.postal_code,
"city": obj.city,
"public_ref": customer.public_ref,
"first_name": customer.first_name,
"last_name": customer.last_name,
"email": customer.email,
"phone": customer.phone,
"postal_code": customer.postal_code,
"city": customer.city,
}
obj = db.scalar(select(Vehicle).where(Vehicle.public_ref == ref))
if obj is None:
vehicle = db.scalar(select(Vehicle).where(Vehicle.public_ref == ref))
if vehicle is None:
return None
return {
"public_ref": obj.public_ref,
"make": obj.make,
"model": obj.model,
"location": obj.location,
"operational_status": obj.operational_status,
"odometer_km": obj.odometer_km,
"public_ref": vehicle.public_ref,
"make": vehicle.make,
"model": vehicle.model,
"location": vehicle.location,
"operational_status": vehicle.operational_status,
"odometer_km": vehicle.odometer_km,
}
+1 -1
View File
@@ -53,7 +53,7 @@ def demo_login(
entity_id=user.id,
)
db.commit()
return CurrentUser(public_ref=user.public_ref, display_name=user.display_name, role=user.role)
return CurrentUser(public_ref=user.public_ref, display_name=user.display_name, role=body.role)
@router.post("/reset")
+4 -4
View File
@@ -1,9 +1,9 @@
from __future__ import annotations
from datetime import datetime
from typing import Any, Literal
from typing import Annotated, Any, Literal
from pydantic import BaseModel, Field, conint
from pydantic import BaseModel, Field
Role = Literal["operations_manager", "rental_employee"]
@@ -49,8 +49,8 @@ class BookingOut(BookingSummaryOut):
class RegisterReturnRequest(BaseModel):
end_odometer_km: conint(ge=0)
fuel_level_percent: conint(ge=0, le=100)
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
+2 -2
View File
@@ -6,7 +6,7 @@ from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from sqlalchemy import delete, insert
from sqlalchemy import delete, insert, update
from sqlalchemy.orm import Session
from app.core.config import get_settings
@@ -114,7 +114,7 @@ def load_seed(db: Session) -> SeedResult:
merged_ref = row.get("merged_into") or ""
if merged_ref:
db.execute(
Customer.__table__.update()
update(Customer)
.where(Customer.id == customer_id_by_ref[row["public_ref"]])
.values(merged_into_customer_id=customer_id_by_ref[merged_ref])
)
+11 -9
View File
@@ -5,7 +5,7 @@ from dataclasses import dataclass, field
from datetime import UTC, datetime
from difflib import SequenceMatcher
from sqlalchemy import select
from sqlalchemy import select, update
from sqlalchemy.orm import Session
from app.core.errors import AppError
@@ -94,9 +94,9 @@ def _open_issue(
def _scan_duplicate_customers(db: Session, scan: ScanResult) -> None:
customers = db.scalars(
select(Customer).where(Customer.merged_into_customer_id.is_(None))
).all()
customers = list(
db.scalars(select(Customer).where(Customer.merged_into_customer_id.is_(None))).all()
)
customers.sort(key=lambda c: c.public_ref)
for i, a in enumerate(customers):
@@ -257,6 +257,9 @@ def _scan_odometer_regressions(db: Session, scan: ScanResult) -> None:
for vehicle_id, bookings in bookings_by_vehicle.items():
bookings.sort(key=lambda b: b.ends_at)
for earlier, later in zip(bookings, bookings[1:], strict=False):
# The query above filters end_odometer_km IS NOT NULL, so both are ints here.
assert earlier.end_odometer_km is not None
assert later.end_odometer_km is not None
if later.end_odometer_km < earlier.end_odometer_km:
vehicle = vehicles[vehicle_id]
_open_issue(
@@ -391,10 +394,9 @@ def merge_customers(
setattr(survivor, field_name, value)
rewired = db.execute(
Booking.__table__.update()
.where(Booking.customer_id == loser.id)
.values(customer_id=survivor.id)
update(Booking).where(Booking.customer_id == loser.id).values(customer_id=survivor.id)
)
rewired_count: int = rewired.rowcount # type: ignore[attr-defined]
loser.merged_into_customer_id = survivor.id
issue.status = "resolved"
@@ -413,7 +415,7 @@ def merge_customers(
metadata={
"loser_ref": loser_ref,
"survivor_ref": survivor_ref,
"rewired_bookings": rewired.rowcount,
"rewired_bookings": rewired_count,
},
)
db.commit()
@@ -422,5 +424,5 @@ def merge_customers(
"issue_ref": issue.public_ref,
"survivor_ref": survivor_ref,
"loser_ref": loser_ref,
"rewired_bookings": rewired.rowcount,
"rewired_bookings": rewired_count,
}
+2 -2
View File
@@ -16,10 +16,10 @@ SEVERITY_ORDER = {"high": 0, "medium": 1, "low": 2}
def compute_metrics(db: Session) -> DashboardMetrics:
"""Shared operations-summary computation used by both the dashboard and the MCP
provider API, so the two never drift out of sync with two copies of the same query."""
status_counts = dict(
status_counts: dict[str, int] = dict(
db.execute(
select(Vehicle.operational_status, func.count()).group_by(Vehicle.operational_status)
).all()
).all() # type: ignore[arg-type]
)
open_issues = db.scalar(
select(func.count()).select_from(DataQualityIssue).where(DataQualityIssue.status == "open")
+4
View File
@@ -57,6 +57,10 @@ def register_vehicle_return(
if booking is None:
raise AppError("BOOKING_NOT_FOUND", "Booking not found.", status_code=404)
vehicle = db.scalar(select(Vehicle).where(Vehicle.id == booking.vehicle_id).with_for_update())
if vehicle is None:
raise AppError(
"VEHICLE_NOT_FOUND", "The vehicle for this booking could not be found.", status_code=404
)
# Re-check after acquiring the row lock: a concurrent identical-key request may have
# just committed while we were waiting.