M54: harden operations and demo resilience
This commit is contained in:
@@ -30,6 +30,7 @@ from app.schemas import (
|
||||
ReturnPreviewResult,
|
||||
)
|
||||
from app.services.audit import record_audit_event
|
||||
from app.services.data_quality import open_odometer_regression_issue
|
||||
from app.services.returns import preview_vehicle_return, register_vehicle_return
|
||||
|
||||
router = APIRouter(prefix="/api/v1/bookings", tags=["bookings"])
|
||||
@@ -146,8 +147,14 @@ def create_booking(
|
||||
) -> BookingOut:
|
||||
if body.ends_at <= body.starts_at:
|
||||
raise HTTPException(status_code=422, detail="Booking end must be after its start")
|
||||
customer = db.scalar(select(Customer).where(Customer.public_ref == body.customer_ref))
|
||||
if customer is None or customer.merged_into_customer_id is not None:
|
||||
customer = db.scalar(
|
||||
select(Customer).where(Customer.public_ref == body.customer_ref).with_for_update()
|
||||
)
|
||||
if (
|
||||
customer is None
|
||||
or customer.merged_into_customer_id is not None
|
||||
or customer.anonymized_at is not None
|
||||
):
|
||||
raise HTTPException(status_code=422, detail="Customer is unavailable for booking")
|
||||
# Serialise booking creation per vehicle. The overlap check must run after
|
||||
# acquiring this lock, otherwise two concurrent requests can both pass it.
|
||||
@@ -179,7 +186,9 @@ def create_booking(
|
||||
status="reserved",
|
||||
start_odometer_km=None,
|
||||
end_odometer_km=None,
|
||||
requirements_complete=body.requirements_complete,
|
||||
# Requirements are intentionally confirmed in a separate, audited action.
|
||||
# Never allow booking creation to bypass that operational checkpoint.
|
||||
requirements_complete=False,
|
||||
)
|
||||
db.add(booking)
|
||||
db.flush()
|
||||
@@ -225,6 +234,15 @@ def checkout_booking(
|
||||
if active_conflict is not None:
|
||||
raise HTTPException(status_code=409, detail="Vehicle already has an active booking")
|
||||
|
||||
correlation_id = uuid.uuid4()
|
||||
before_booking = {
|
||||
"status": booking.status,
|
||||
"start_odometer_km": booking.start_odometer_km,
|
||||
}
|
||||
before_vehicle = {
|
||||
"operational_status": vehicle.operational_status,
|
||||
"odometer_km": vehicle.odometer_km,
|
||||
}
|
||||
attention_reasons: list[str] = []
|
||||
if body.start_odometer_km < vehicle.odometer_km:
|
||||
attention_reasons.append("odometer_regression")
|
||||
@@ -250,6 +268,18 @@ def checkout_booking(
|
||||
completed_by=user.display_name,
|
||||
)
|
||||
db.add(inspection)
|
||||
if "odometer_regression" in attention_reasons:
|
||||
open_odometer_regression_issue(
|
||||
db,
|
||||
vehicle=vehicle,
|
||||
reading_ref=inspection.public_ref,
|
||||
reading_km=body.start_odometer_km,
|
||||
canonical_km=vehicle.odometer_km,
|
||||
source_type="checkout",
|
||||
related_refs=[booking.public_ref, inspection.public_ref],
|
||||
actor_label=user.display_name,
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
if attention_reasons:
|
||||
booking.status = "blocked"
|
||||
vehicle.operational_status = (
|
||||
@@ -269,13 +299,31 @@ def checkout_booking(
|
||||
action="booking_checkout_recorded",
|
||||
entity_type="booking",
|
||||
entity_id=booking.id,
|
||||
correlation_id=correlation_id,
|
||||
before=before_booking,
|
||||
after={
|
||||
"inspection_ref": inspection.public_ref,
|
||||
"booking_status": booking.status,
|
||||
"start_odometer_km": booking.start_odometer_km,
|
||||
"vehicle_status": vehicle.operational_status,
|
||||
"attention_reasons": attention_reasons,
|
||||
},
|
||||
)
|
||||
record_audit_event(
|
||||
db,
|
||||
actor_type="user",
|
||||
actor_label=user.display_name,
|
||||
action="vehicle_status_changed",
|
||||
entity_type="vehicle",
|
||||
entity_id=vehicle.id,
|
||||
correlation_id=correlation_id,
|
||||
before=before_vehicle,
|
||||
after={
|
||||
"operational_status": vehicle.operational_status,
|
||||
"odometer_km": vehicle.odometer_km,
|
||||
},
|
||||
metadata={"booking_ref": booking.public_ref, "inspection_ref": inspection.public_ref},
|
||||
)
|
||||
db.commit()
|
||||
return CheckoutBookingResult(
|
||||
booking_ref=booking.public_ref,
|
||||
|
||||
Reference in New Issue
Block a user