M39: harden application and acceptance gates
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime, timedelta
|
||||
@@ -171,6 +173,33 @@ def preview_vehicle_return(
|
||||
return booking, vehicle, evaluation
|
||||
|
||||
|
||||
def request_fingerprint(body: RegisterReturnRequest) -> str:
|
||||
canonical = json.dumps(body.model_dump(mode="json"), sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _replay_or_reject(
|
||||
db: Session,
|
||||
existing: IdempotencyRecord,
|
||||
booking_ref: str,
|
||||
fingerprint: str,
|
||||
) -> tuple[int, dict]:
|
||||
booking = db.get(Booking, existing.booking_id)
|
||||
if booking is None or booking.public_ref != booking_ref:
|
||||
raise AppError(
|
||||
"IDEMPOTENCY_KEY_REUSED",
|
||||
"This idempotency key was already used for a different booking.",
|
||||
status_code=409,
|
||||
)
|
||||
if existing.request_fingerprint is not None and existing.request_fingerprint != fingerprint:
|
||||
raise AppError(
|
||||
"IDEMPOTENCY_KEY_REUSED",
|
||||
"This idempotency key was already used with a different request body.",
|
||||
status_code=409,
|
||||
)
|
||||
return existing.response_status, existing.response_body
|
||||
|
||||
|
||||
def register_vehicle_return(
|
||||
db: Session,
|
||||
booking_ref: str,
|
||||
@@ -178,18 +207,12 @@ def register_vehicle_return(
|
||||
idempotency_key: str,
|
||||
actor: CurrentUser,
|
||||
) -> tuple[int, dict]:
|
||||
fingerprint = request_fingerprint(body)
|
||||
existing = db.scalar(
|
||||
select(IdempotencyRecord).where(IdempotencyRecord.idempotency_key == idempotency_key)
|
||||
)
|
||||
if existing is not None:
|
||||
booking = db.get(Booking, existing.booking_id)
|
||||
if booking is None or booking.public_ref != booking_ref:
|
||||
raise AppError(
|
||||
"IDEMPOTENCY_KEY_REUSED",
|
||||
"This idempotency key was already used for a different booking.",
|
||||
status_code=409,
|
||||
)
|
||||
return existing.response_status, existing.response_body
|
||||
return _replay_or_reject(db, existing, booking_ref, fingerprint)
|
||||
|
||||
booking, vehicle = _load_active_booking_and_vehicle(db, booking_ref, lock=True)
|
||||
|
||||
@@ -199,7 +222,7 @@ def register_vehicle_return(
|
||||
select(IdempotencyRecord).where(IdempotencyRecord.idempotency_key == idempotency_key)
|
||||
)
|
||||
if existing is not None:
|
||||
return existing.response_status, existing.response_body
|
||||
return _replay_or_reject(db, existing, booking_ref, fingerprint)
|
||||
|
||||
if booking.status != "active":
|
||||
raise AppError(
|
||||
@@ -336,6 +359,7 @@ def register_vehicle_return(
|
||||
IdempotencyRecord(
|
||||
idempotency_key=idempotency_key,
|
||||
booking_id=booking.id,
|
||||
request_fingerprint=fingerprint,
|
||||
response_status=201,
|
||||
response_body=response_body,
|
||||
)
|
||||
@@ -349,7 +373,7 @@ def register_vehicle_return(
|
||||
select(IdempotencyRecord).where(IdempotencyRecord.idempotency_key == idempotency_key)
|
||||
)
|
||||
if existing is not None:
|
||||
return existing.response_status, existing.response_body
|
||||
return _replay_or_reject(db, existing, booking_ref, fingerprint)
|
||||
raise
|
||||
|
||||
return 201, response_body
|
||||
|
||||
Reference in New Issue
Block a user