{"openapi":"3.1.0","info":{"title":"FastAPI","version":"0.1.0"},"paths":{"/api/v1/register/":{"post":{"tags":["Auth"],"summary":"Register","operationId":"register_api_v1_register__post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCreate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/token":{"post":{"tags":["Auth"],"summary":"Login","operationId":"login_api_v1_token_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_login_api_v1_token_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Token"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/logout":{"post":{"tags":["Auth"],"summary":"Logout","operationId":"logout_api_v1_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/me":{"get":{"tags":["Auth"],"summary":"Read Current User","operationId":"read_current_user_api_v1_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRead"}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"delete":{"tags":["Auth"],"summary":"Delete Account","description":"Deletes (soft) the current user's own account - requires the current\npassword, same reasoning as change-password. Booking/payment history\nis deliberately preserved; see crud/users.py's delete_user_account.","operationId":"delete_account_api_v1_me_delete","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAccountRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/forgot-password":{"post":{"tags":["Auth"],"summary":"Forgot Password","description":"Always returns the same generic acknowledgement whether or not the\nemail is registered - the response itself must not reveal which\nemails exist in the system (user enumeration).","operationId":"forgot_password_api_v1_forgot_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgotPasswordRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reset-password":{"post":{"tags":["Auth"],"summary":"Reset Password","operationId":"reset_password_api_v1_reset_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/change-password":{"post":{"tags":["Auth"],"summary":"Change Password","description":"Self-service password change for a logged-in user - distinct from\n/reset-password, which proves identity via an emailed token instead.\nRequires the current password so a hijacked session can't change it\nwithout knowing it.","operationId":"change_password_api_v1_change_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/shopping/flight-offers":{"post":{"tags":["Flights"],"summary":"Search Flights","description":"Search for flights by creating a Duffel offer request.\n\nAccepts slices (origin/destination/departure date), passengers and an\noptional cabin class, and returns one page of matching offers (grouped\nby route) plus facets and pagination info. The full result is cached in\nRedis for one minute per unique search request; `params` (sort,\nairlines, max_stops, price_max, limit, offset) slice/filter that cached\nresult and don't affect the cache key.","operationId":"search_flights_api_v1_shopping_flight_offers_post","parameters":[{"name":"sort","in":"query","required":false,"schema":{"$ref":"#/components/schemas/OfferSortKey","default":"price"}},{"name":"airlines","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Owner IATA codes to keep","title":"Airlines"},"description":"Owner IATA codes to keep"},{"name":"max_stops","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":0},{"type":"null"}],"title":"Max Stops"}},{"name":"price_max","in":"query","required":false,"schema":{"anyOf":[{"type":"number","minimum":0},{"type":"null"}],"title":"Price Max"}},{"name":"depart_windows","in":"query","required":false,"schema":{"type":"array","items":{"$ref":"#/components/schemas/DepartureWindow"},"description":"Keep offers whose outbound leg departs in any of these windows","title":"Depart Windows"},"description":"Keep offers whose outbound leg departs in any of these windows"},{"name":"max_duration_minutes","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":0},{"type":"null"}],"title":"Max Duration Minutes"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfferRequestCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlightSearchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Flights"],"summary":"Search Flights 2","description":"Simple flight search via query parameters.\n\nDuffel has no GET search endpoint, so the parameters are translated into\nan offer request (slices + passengers) and posted to Duffel. Shares its\ncache namespace with the POST endpoint, since an equivalent request via\neither path resolves to the same offer request body. `params` combines\nboth the shopping fields (origin/destination/...) and the sort/filter/\npagination fields (FastAPI only flattens one Query() model per route -\nsee FlightSearchAndListQueryParams's docstring) - only the former go\ninto the offer request itself, the latter are applied afterwards, same\nas search_flights.","operationId":"search_flights_2_api_v1_shopping_flight_offers_get","parameters":[{"name":"sort","in":"query","required":false,"schema":{"$ref":"#/components/schemas/OfferSortKey","default":"price"}},{"name":"airlines","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Owner IATA codes to keep","title":"Airlines"},"description":"Owner IATA codes to keep"},{"name":"max_stops","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":0},{"type":"null"}],"title":"Max Stops"}},{"name":"price_max","in":"query","required":false,"schema":{"anyOf":[{"type":"number","minimum":0},{"type":"null"}],"title":"Price Max"}},{"name":"depart_windows","in":"query","required":false,"schema":{"type":"array","items":{"$ref":"#/components/schemas/DepartureWindow"},"description":"Keep offers whose outbound leg departs in any of these windows","title":"Depart Windows"},"description":"Keep offers whose outbound leg departs in any of these windows"},{"name":"max_duration_minutes","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":0},{"type":"null"}],"title":"Max Duration Minutes"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"origin","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":3,"title":"Origin"}},{"name":"destination","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":3,"title":"Destination"}},{"name":"departure_date","in":"query","required":true,"schema":{"type":"string","format":"date","title":"Departure Date"}},{"name":"return_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Return Date"}},{"name":"adults","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Adults"}},{"name":"children","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Children"}},{"name":"infants","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Infants"}},{"name":"cabin_class","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/CabinClass"},{"type":"null"}],"title":"Cabin Class"}},{"name":"max_connections","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":2,"minimum":0},{"type":"null"}],"title":"Max Connections"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlightSearchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/shopping/flight-offers/pricing":{"post":{"tags":["Flights"],"summary":"Confirm Price","description":"Confirm the live price of a selected offer.\n\nDuffel has no separate pricing API: fetching the single offer again\nreturns its up-to-date total_amount/total_currency (plus available\nservices). Offers expire, so always confirm shortly before booking.","operationId":"confirm_price_api_v1_shopping_flight_offers_pricing_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfferPriceRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfferResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/shopping/flight-offers/{offer_id}/passengers/{offer_passenger_id}":{"patch":{"tags":["Flights"],"summary":"Update Offer Passenger","description":"Attach loyalty programme accounts to a passenger on an already-priced\noffer, then return the offer re-fetched (and re-marked-up) - Duffel\nmay reveal a loyalty-discounted fare, only reflected by re-fetching\nafter the update. Call this after collecting passenger names/loyalty\nnumbers but before final checkout, so any discount is reflected in\nwhat the customer is actually charged.","operationId":"update_offer_passenger_api_v1_shopping_flight_offers__offer_id__passengers__offer_passenger_id__patch","parameters":[{"name":"offer_id","in":"path","required":true,"schema":{"type":"string","title":"Offer Id"}},{"name":"offer_passenger_id","in":"path","required":true,"schema":{"type":"string","title":"Offer Passenger Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfferPassengerUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfferResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/shopping/seatmaps":{"get":{"tags":["Flights"],"summary":"View Seat Map Get","description":"Get seat maps for an offer.\n\nDuffel exposes seat maps per offer (before booking), not per order,\nso this takes an offer_id instead of the old flightOrderId. Cached\nbriefly per offer_id, since it's a read-only, offer-scoped lookup.","operationId":"view_seat_map_get_api_v1_shopping_seatmaps_get","parameters":[{"name":"offer_id","in":"query","required":true,"schema":{"type":"string","title":"Offer Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/shopping/places":{"get":{"tags":["Flights"],"summary":"Search Places","description":"Search for airports and cities.\n\nGive either `query` (a free-text name or IATA code, for autocomplete)\nor `lat`+`lng`+`rad` (a geographic radius in meters), not both. Cached\nfor a day, since airport/city reference data rarely changes.","operationId":"search_places_api_v1_shopping_places_get","parameters":[{"name":"query","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}],"description":"Free-text name or IATA code","title":"Query"},"description":"Free-text name or IATA code"},{"name":"lat","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":90,"minimum":-90},{"type":"null"}],"title":"Lat"}},{"name":"lng","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":180,"minimum":-180},{"type":"null"}],"title":"Lng"}},{"name":"rad","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"Radius in meters","title":"Rad"},"description":"Radius in meters"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaceSuggestionsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/flights/popular-destinations":{"get":{"tags":["Flights"],"summary":"Popular Destinations","description":"Public, no auth - aggregated from real BookingSlice data (our own\nDB, not Duffel), with a real-signal threshold\n(PUBLIC_POPULAR_ROUTE_MIN_BOOKINGS) so this app's early, thin booking\nvolume never surfaces a single booking as a fabricated 'popular\ndestination'. An empty list is a completely normal response - the\nfrontend hides the whole section rather than showing an empty box.","operationId":"popular_destinations_api_v1_flights_popular_destinations_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":20,"minimum":1,"default":8,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PopularRoute"},"title":"Response Popular Destinations Api V1 Flights Popular Destinations Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/booking/flight-orders":{"post":{"tags":["Bookings"],"summary":"Flight Order","description":"Create an order (booking) from a selected, freshly priced offer.\n\nIMPORTANT:\n\n- selected_offers must contain an offer ID from a RECENT search\n- Offers expire quickly (typically within minutes), so call\n  /shopping/flight-offers/pricing first and use its amounts\n- passengers must use the IDs issued by the offer request and the\n  payment amount/currency must match the offer's total","operationId":"flight_order_api_v1_booking_flight_orders_post","security":[{"OAuth2PasswordBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Bookings"],"summary":"List Flight Orders","description":"List the current user's bookings, most recent first.\n\nBacked by our own DB (not a live Duffel call), since Duffel's\n/air/orders isn't scoped per end-user - it lists every order in the\nwhole Duffel account. Only bookings made through this app appear here.\n\nCursor-paginated (?cursor=&size=): a page is found by seeking straight\nto the cursor's position in the sort key, so cost doesn't grow with\nhow deep into the list you are, the way OFFSET's row-skipping does.\n\nThe four filters are spelled out here rather than grouped into one\nAnnotated[Model, Query()] parameter, which is what this route used to\ndo: fastapi-pagination's pagination_ctx dependency stops FastAPI from\nexpanding such a model into its individual query params, so /docs\nadvertised a single required `params` object instead of the four real\nfilters. Requests still parsed correctly - only the published schema\nwas wrong - but that schema is the API contract, so the filters are\ndeclared in the form that documents itself accurately (and matches\nhow the admin list routes declare theirs).","operationId":"list_flight_orders_api_v1_booking_flight_orders_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"booking_reference","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Booking Reference"}},{"name":"origin","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":3,"maxLength":3},{"type":"null"}],"title":"Origin"}},{"name":"destination","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":3,"maxLength":3},{"type":"null"}],"title":"Destination"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/BookingStatus"},{"type":"null"}],"title":"Status"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Cursor for the next page","title":"Cursor"},"description":"Cursor for the next page"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":0,"description":"Page size","default":50,"title":"Size"},"description":"Page size"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CursorPage_BookingPublic_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/booking/flight-orders/by-id/{booking_id}":{"get":{"tags":["Bookings"],"summary":"Get Flight Order By Id","description":"Get one of the current user's bookings by OUR OWN id (not Duffel's\norder_id, which flight_order_management below uses) - backed by our\nDB, so this includes ticket numbers (BookingPassengerPublic.tickets)\nthat Duffel's raw order response doesn't carry in the same shape.","operationId":"get_flight_order_by_id_api_v1_booking_flight_orders_by_id__booking_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"booking_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Booking Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingPublic"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/booking/flight-orders/by-ticket/{ticket_number}":{"get":{"tags":["Bookings"],"summary":"Get Flight Order By Ticket Number","description":"Look up one of the current user's bookings by an airline-issued ticket\nnumber alone, for a traveler who has the ticket number but not our\ninternal booking id handy.","operationId":"get_flight_order_by_ticket_number_api_v1_booking_flight_orders_by_ticket__ticket_number__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"ticket_number","in":"path","required":true,"schema":{"type":"string","title":"Ticket Number"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingPublic"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/booking/flight-orders/by-id/{booking_id}/qr.png":{"get":{"tags":["Bookings"],"summary":"Get Booking Qr","description":"The booking's verification QR as a PNG, for the on-screen ticket\n(components/tickets/TicketDocument.tsx).\n\nServed rather than generated in the browser so the code on screen is\nbyte-identical to the one on the printed PDF - both call\nutils/itinerary_pdf.py's booking_qr_png. Owner-scoped like every other\nbooking route here: a QR resolves to a real booking, so it is not\npublic just because it looks like an image.","operationId":"get_booking_qr_api_v1_booking_flight_orders_by_id__booking_id__qr_png_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"booking_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Booking Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/booking/flight-orders/by-id/{booking_id}/itinerary.pdf":{"get":{"tags":["Bookings"],"summary":"Get Booking Itinerary Pdf","description":"Downloadable e-itinerary/receipt PDF for one of the current user's\nbookings - generated fresh on every request (see utils/itinerary_pdf.py)\nso it always reflects the booking's current state, rather than a\nstale snapshot attached to the confirmation email at send time.","operationId":"get_booking_itinerary_pdf_api_v1_booking_flight_orders_by_id__booking_id__itinerary_pdf_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"booking_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Booking Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/booking/flight-orders/{order_id}":{"get":{"tags":["Bookings"],"summary":"Flight Order Management","description":"Get flight order details by order ID.\n\nAlso useful to fetch the order's up-to-date price before paying a held\norder, since re-fetching avoids a `price_changed` error on payment.","operationId":"flight_order_management_api_v1_booking_flight_orders__order_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","title":"Order Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/booking/flight-orders/{order_id}/cancellations":{"post":{"tags":["Bookings"],"summary":"Request Order Cancellation","description":"Request an order cancellation quote.\n\nIMPORTANT:\n\n- This does NOT cancel the order; it only creates an unconfirmed quote\n  with the refund amount and an expiry\n- Check the order's available_actions includes \"cancel\" before calling\n- Confirm the quote via the /confirm endpoint before it expires,\n  otherwise a new quote must be requested\n\nReturns Duffel's quote alongside `customer_refund` - what the person\nactually gets back. Those are different numbers (Duffel refunds\nflyt's balance, and its figure can exceed what the customer paid once\na discount code is involved), and the customer-facing UI must quote\nthe latter or it promises money that will never arrive.","operationId":"request_order_cancellation_api_v1_booking_flight_orders__order_id__cancellations_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","title":"Order Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderCancellationQuoteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/booking/flight-orders/by-id/{booking_id}/refund":{"get":{"tags":["Bookings"],"summary":"Get Booking Refund","description":"The traveller's own view of the refund owed on a cancelled\nbooking. 404s when there isn't one - a booking that was never\ncancelled, or a fully non-refundable fare where nothing is owed.","operationId":"get_booking_refund_api_v1_booking_flight_orders_by_id__booking_id__refund_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"booking_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Booking Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerRefundRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/booking/flight-orders/{order_id}/cancellations/{order_cancellation_id}/confirm":{"post":{"tags":["Bookings"],"summary":"Confirm Order Cancellation","description":"Confirm a previously requested order cancellation quote.\n\nFinalizes the cancellation and initiates the refund to the original\nform of payment. order_id is accepted for a predictable, RESTful URL,\nthough Duffel only requires the cancellation ID to confirm.","operationId":"confirm_order_cancellation_api_v1_booking_flight_orders__order_id__cancellations__order_cancellation_id__confirm_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","title":"Order Id"}},{"name":"order_cancellation_id","in":"path","required":true,"schema":{"type":"string","title":"Order Cancellation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderCancellationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/booking/flight-orders/{order_id}/change-requests":{"post":{"tags":["Bookings"],"summary":"Create Order Change Request","description":"Step 1: describe which slice(s) to remove and what new slice(s) to\nsearch for in their place. Doesn't touch the order yet - review the\nresulting offers (step 2) before committing to anything.","operationId":"create_order_change_request_api_v1_booking_flight_orders__order_id__change_requests_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","title":"Order Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderChangeSlices"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderChangeRequestResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/booking/flight-orders/{order_id}/change-requests/{order_change_request_id}/offers":{"get":{"tags":["Bookings"],"summary":"List Order Change Offers","description":"Step 2: the priced ways to satisfy a change request - the customer\npicks one to proceed with (step 3).","operationId":"list_order_change_offers_api_v1_booking_flight_orders__order_id__change_requests__order_change_request_id__offers_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","title":"Order Id"}},{"name":"order_change_request_id","in":"path","required":true,"schema":{"type":"string","title":"Order Change Request Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderChangeOffersResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/booking/flight-orders/{order_id}/changes":{"post":{"tags":["Bookings"],"summary":"Create Order Change","description":"Step 3: create a pending change from a chosen offer - still not\nconfirmed/charged. Review the returned change_total_amount/\npenalty_total_amount with the customer before confirming (step 4).","operationId":"create_order_change_api_v1_booking_flight_orders__order_id__changes_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","title":"Order Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderChangeCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderChangeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/booking/flight-orders/{order_id}/changes/{order_change_id}/confirm":{"post":{"tags":["Bookings"],"summary":"Confirm Order Change","description":"Step 4: pay for and finalize the pending change. On success,\nre-fetches the order and re-syncs our persisted booking's flights/\ntotal, since they may now differ from what was recorded at booking\ntime.","operationId":"confirm_order_change_api_v1_booking_flight_orders__order_id__changes__order_change_id__confirm_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","title":"Order Id"}},{"name":"order_change_id","in":"path","required":true,"schema":{"type":"string","title":"Order Change Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderChangeConfirm"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderChangeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/payments/discounts/preview":{"post":{"tags":["Payments"],"summary":"Preview Discount","description":"Non-persisting check so the customer sees whether a code works (and\nroughly what it saves) before committing to checkout - see\nDiscountPreviewRequest's docstring for why this previews the base\nfare only, not the final total with seats/baggage folded in. The real\ncheckout call re-validates and applies the code for real.","operationId":"preview_discount_api_v1_payments_discounts_preview_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscountPreviewRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscountPreviewResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/payments/checkout":{"post":{"tags":["Payments"],"summary":"Checkout","description":"Starts a purchase: re-confirms the offer's live price, stores the\npassenger details for later, and hands back a Pesapal redirect URL.\n\nDuffel is NOT contacted to create an order here - that only happens\nonce Pesapal confirms payment (see GET /payments/{payment_id}/status\nand /payments/ipn), so no airline-side hold is ever created for a\npurchase the customer doesn't complete.","operationId":"checkout_api_v1_payments_checkout_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/payments/checkout/card":{"post":{"tags":["Payments"],"summary":"Checkout Card","description":"Card-payment alternative to /checkout: re-confirms price the same way,\nthen starts a Duffel Payments PaymentIntent instead of redirecting to\nPesapal. Returns a client_token for the frontend's DuffelPayments\ncomponent to collect the card directly with Duffel - card details\nnever reach our backend. See POST /payments/{payment_id}/confirm-card\nfor what happens once the customer submits their card.","operationId":"checkout_card_api_v1_payments_checkout_card_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardCheckoutResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/payments/{payment_id}/confirm-card":{"post":{"tags":["Payments"],"summary":"Confirm Card","description":"Called by the frontend right after DuffelPayments reports a\nsuccessful card collection. Confirms the PaymentIntent (which tops up\nour Duffel Balance) and, on success, completes the booking - the\ncard-path equivalent of GET /payments/{payment_id}/status, returning\nthe same response shape so the frontend can reuse its result UI.","operationId":"confirm_card_api_v1_payments__payment_id__confirm_card_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"payment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Payment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/payments/{payment_id}/status":{"get":{"tags":["Payments"],"summary":"Payment Status","description":"Polled by the frontend right after Pesapal redirects the customer back\nto the callback URL - the primary confirmation path. Triggers a live\nPesapal status check (and, on success, the Duffel booking) if the\npayment is still `pending`; otherwise just returns its current state.","operationId":"payment_status_api_v1_payments__payment_id__status_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"payment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Payment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/payments/ipn":{"post":{"tags":["Payments"],"summary":"Pesapal Ipn","description":"Pesapal's server-to-server webhook. Not authenticated, since Pesapal\ncalls this directly; the payment is looked up by the tracking ID\nPesapal itself generated, not anything client-suppliable.\n\nAccepts both GET and POST regardless of which `ipn_notification_type`\nwas registered (see backend/scripts/register_pesapal_ipn.py) - Pesapal\nalways delivers OrderTrackingId/OrderMerchantReference/\nOrderNotificationType as query params either way (confirmed across\ntheir IPN, callback, and recurring-payment docs), so there's no\nparsing difference between the two methods, and accepting both is a\nfree hedge against the registered type ever drifting from what we\nexpect.\n\nA reliability backstop for the status-poll endpoint above, for cases\nwhere the customer closes their browser before returning from Pesapal.\nMust respond with this exact JSON echo per Pesapal's IPN spec.","operationId":"pesapal_ipn_api_v1_payments_ipn_post","parameters":[{"name":"OrderTrackingId","in":"query","required":true,"schema":{"type":"string","title":"Ordertrackingid"}},{"name":"OrderMerchantReference","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ordermerchantreference"}},{"name":"OrderNotificationType","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ordernotificationtype"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Payments"],"summary":"Pesapal Ipn","description":"Pesapal's server-to-server webhook. Not authenticated, since Pesapal\ncalls this directly; the payment is looked up by the tracking ID\nPesapal itself generated, not anything client-suppliable.\n\nAccepts both GET and POST regardless of which `ipn_notification_type`\nwas registered (see backend/scripts/register_pesapal_ipn.py) - Pesapal\nalways delivers OrderTrackingId/OrderMerchantReference/\nOrderNotificationType as query params either way (confirmed across\ntheir IPN, callback, and recurring-payment docs), so there's no\nparsing difference between the two methods, and accepting both is a\nfree hedge against the registered type ever drifting from what we\nexpect.\n\nA reliability backstop for the status-poll endpoint above, for cases\nwhere the customer closes their browser before returning from Pesapal.\nMust respond with this exact JSON echo per Pesapal's IPN spec.","operationId":"pesapal_ipn_api_v1_payments_ipn_post","parameters":[{"name":"OrderTrackingId","in":"query","required":true,"schema":{"type":"string","title":"Ordertrackingid"}},{"name":"OrderMerchantReference","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ordermerchantreference"}},{"name":"OrderNotificationType","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ordernotificationtype"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/stays/search":{"post":{"tags":["Stays"],"summary":"Search Stays","description":"Step 1 of 4: search accommodation near a location for given dates\nand guest counts. Returns Duffel's raw search response - each result's\nid is what fetch_rates below needs.","operationId":"search_stays_api_v1_stays_search_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StaysSearchRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/stays/search-results/{search_result_id}/rates":{"post":{"tags":["Stays"],"summary":"Fetch Stay Rates","description":"Step 2 of 4: the full list of bookable room rates for one\naccommodation from a search result. Each rate's id is what\ncreate_quote below needs.","operationId":"fetch_stay_rates_api_v1_stays_search_results__search_result_id__rates_post","parameters":[{"name":"search_result_id","in":"path","required":true,"schema":{"type":"string","title":"Search Result Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/stays/quotes":{"post":{"tags":["Stays"],"summary":"Create Stay Quote","description":"Step 3 of 4: re-validates a rate is still purchasable and locks its\nprice. Returns a quote_id, required to book and only valid briefly -\ncall this shortly before create_stay_booking.","operationId":"create_stay_quote_api_v1_stays_quotes_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StaysQuoteRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/stays/bookings":{"post":{"tags":["Stays"],"summary":"Create Stay Booking","description":"Step 4 of 4: finalizes the booking from a quote_id. NOT persisted\nanywhere in our own DB - see this module's docstring. The response is\nDuffel's own booking record; there is currently no way to look this\nbooking up again through this app after this call returns.","operationId":"create_stay_booking_api_v1_stays_bookings_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StaysBookingCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/cars/search":{"post":{"tags":["Cars"],"summary":"Search Cars","description":"Step 1 of 3: available vehicles for a pick-up location and period.\n\nReturns Duffel's raw search response - each result carries the\nquote id step 2 needs. Not modelled into a response schema on\npurpose (see schemas/duffel_cars.py): Duffel documents the request\nparameters but not the response shape, and a guessed model would\nsilently drop fields callers need.","operationId":"search_cars_api_v1_cars_search_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CarsSearchRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/cars/quotes/{quote_id}":{"get":{"tags":["Cars"],"summary":"Get Car Quote","description":"Step 2 of 3: re-read a quote to confirm it is still valid and\nstill priced as displayed.\n\nWorth doing rather than booking straight from a search result:\navailability and price move between the two, and finding out at\nbooking time means telling a customer no after they have committed.","operationId":"get_car_quote_api_v1_cars_quotes__quote_id__get","parameters":[{"name":"quote_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"quo_... from a search result","title":"Quote Id"},"description":"quo_... from a search result"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/cars/bookings":{"post":{"tags":["Cars"],"summary":"Create Car Booking","description":"Step 3 of 3: reserve the vehicle against a quote.\n\nNo payment object, unlike a flight order: a car reservation is\nsettled at the desk against the renter's own licence and card. What\nthis creates is the reservation - which is also why a package that\nincludes a car cannot simply charge for it up front without deciding\nwho carries the supplier relationship.","operationId":"create_car_booking_api_v1_cars_bookings_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CarsBookingRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/support/contact":{"post":{"tags":["Support"],"summary":"Contact Support","description":"Relays a support message and confirms receipt to the customer.\n\nNothing here reads from or depends on the DB row this request itself\ncreates (there isn't one - the form is stateless), so it's safe to\npublish the event immediately: the staff email, the customer\nautoreply, and the staff in-app notification (see\nbackend/queues/events.py) all happen out of the request path.","operationId":"contact_support_api_v1_support_contact_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/permissions":{"get":{"tags":["Admin - RBAC"],"summary":"List All Permissions","operationId":"list_all_permissions_api_v1_admin_permissions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/PermissionRead"},"type":"array","title":"Response List All Permissions Api V1 Admin Permissions Get"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/admin/groups":{"get":{"tags":["Admin - RBAC"],"summary":"List All Groups","operationId":"list_all_groups_api_v1_admin_groups_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/GroupRead"},"type":"array","title":"Response List All Groups Api V1 Admin Groups Get"}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"post":{"tags":["Admin - RBAC"],"summary":"Create New Group","operationId":"create_new_group_api_v1_admin_groups_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupCreate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/admin/groups/{group_id}/permissions":{"post":{"tags":["Admin - RBAC"],"summary":"Assign Group Permissions","operationId":"assign_group_permissions_api_v1_admin_groups__group_id__permissions_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"integer","title":"Group Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignPermissionsRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/groups/{group_id}/permissions/{codename}":{"delete":{"tags":["Admin - RBAC"],"summary":"Revoke Group Permission By Codename","operationId":"revoke_group_permission_by_codename_api_v1_admin_groups__group_id__permissions__codename__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"integer","title":"Group Id"}},{"name":"codename","in":"path","required":true,"schema":{"type":"string","title":"Codename"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/users/{user_id}/groups":{"post":{"tags":["Admin - RBAC"],"summary":"Assign User Groups","operationId":"assign_user_groups_api_v1_admin_users__user_id__groups_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignGroupsRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/users/{user_id}/groups/{group_id}":{"delete":{"tags":["Admin - RBAC"],"summary":"Remove User Group Route","operationId":"remove_user_group_route_api_v1_admin_users__user_id__groups__group_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}},{"name":"group_id","in":"path","required":true,"schema":{"type":"integer","title":"Group Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/bookings":{"get":{"tags":["Admin - Bookings"],"summary":"List All Bookings","description":"Every booking in the system, most recent first - the staff\ncounterpart of GET /booking/flight-orders (which is scoped to the\nlogged-in user). `search` matches booking reference or owner email.","operationId":"list_all_bookings_api_v1_admin_bookings_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Cursor for the next page","title":"Cursor"},"description":"Cursor for the next page"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":0,"description":"Page size","default":50,"title":"Size"},"description":"Page size"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CursorPage_AdminBookingRead_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Admin - Bookings"],"summary":"Create Admin Booking Route","description":"Admin-marked-paid booking on behalf of an existing customer - no\nreal payment collection, see crud/payments.py's create_admin_booking\nfor what \"marked-paid\" means and why. The customer must already have\na flyt account; there's no guest-booking concept here.","operationId":"create_admin_booking_route_api_v1_admin_bookings_post","security":[{"OAuth2PasswordBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCreateBookingRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminBookingRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/bookings/{booking_id}":{"get":{"tags":["Admin - Bookings"],"summary":"Get Booking Detail","operationId":"get_booking_detail_api_v1_admin_bookings__booking_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"booking_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Booking Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminBookingRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/bookings/{booking_id}/backfill-tickets":{"post":{"tags":["Admin - Bookings"],"summary":"Backfill Booking Tickets","description":"Manually re-checks Duffel for e-tickets on a booking that's still\nticket-less after _complete_booking's own short retry window\n(crud/payments.py) - closes that gap on demand. Safe to call more\nthan once: no-ops if the booking already has tickets (see\ncrud/tickets.py's backfill_tickets_from_duffel).","operationId":"backfill_booking_tickets_api_v1_admin_bookings__booking_id__backfill_tickets_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"booking_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Booking Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminBookingRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/bookings/{booking_id}/resend-confirmation":{"post":{"tags":["Admin - Bookings"],"summary":"Resend Booking Confirmation","description":"Re-sends the booking-confirmation email - for a customer who says\nthey never got it, or who wants it resent after a manual ticket\nbackfill above.","operationId":"resend_booking_confirmation_api_v1_admin_bookings__booking_id__resend_confirmation_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"booking_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Booking Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/dashboard/summary":{"get":{"tags":["Admin - Dashboard"],"summary":"Dashboard Summary","operationId":"dashboard_summary_api_v1_admin_dashboard_summary_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminDashboardSummary"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/admin/dashboard/popular-routes":{"get":{"tags":["Admin - Dashboard"],"summary":"Dashboard Popular Routes","description":"Staff see real signal from a single booking up - the public\ncounterpart (routers/flights.py's popular_destinations) uses a much\nhigher min_bookings so a lone booking never looks 'popular' to a\ncustomer.","operationId":"dashboard_popular_routes_api_v1_admin_dashboard_popular_routes_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":10,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PopularRoute"},"title":"Response Dashboard Popular Routes Api V1 Admin Dashboard Popular Routes Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/users":{"get":{"tags":["Admin - Users"],"summary":"List All Users","operationId":"list_all_users_api_v1_admin_users_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Cursor for the next page","title":"Cursor"},"description":"Cursor for the next page"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":0,"description":"Page size","default":50,"title":"Size"},"description":"Page size"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CursorPage_AdminUserRead_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/users/{user_id}/bookings":{"get":{"tags":["Admin - Users"],"summary":"Get User Bookings Admin","operationId":"get_user_bookings_admin_api_v1_admin_users__user_id__bookings_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Cursor for the next page","title":"Cursor"},"description":"Cursor for the next page"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":0,"description":"Page size","default":50,"title":"Size"},"description":"Page size"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CursorPage_BookingPublic_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/users/{user_id}/staff":{"post":{"tags":["Admin - Users"],"summary":"Update User Staff Status","description":"Granting admin-surface access is exactly the kind of\nbootstrapping-sensitive action group/permission management already\nreserves for superusers - see utils/rbac.py's require_superuser.","operationId":"update_user_staff_status_api_v1_admin_users__user_id__staff_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetStaffRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/users/{user_id}/deactivate":{"post":{"tags":["Admin - Users"],"summary":"Deactivate User","description":"A staff-initiated deactivation is the same soft-delete a\nself-service account deletion already is (crud/users.py's\ndelete_user_account) - booking/payment history is preserved\nunchanged, only identity is scrubbed. Blocks deactivating your own\naccount through this admin action - self-service deletion\n(DELETE /api/me) already covers that and requires re-entering your\npassword; this path shouldn't be a way to accidentally lock yourself\nout with one misclick.","operationId":"deactivate_user_api_v1_admin_users__user_id__deactivate_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/users/{user_id}":{"get":{"tags":["Admin - Users"],"summary":"Get User Detail","operationId":"get_user_detail_api_v1_admin_users__user_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/users/{user_id}/ban":{"post":{"tags":["Admin - Users"],"summary":"Ban User Route","description":"Reuses delete_user's permission (same as deactivate_user) - a ban\nis the same class of moderation action, and utils/rbac.py's fixed\n{add,change,delete,view}-per-model permission grid has no room for a\none-off 'ban' verb. Unlike deactivate_user this is reversible (see\ncrud/users.py's ban_user/unban_user) so there's no destructive\nself-lockout risk in letting an admin ban their own account, but\nblocking it anyway avoids a confusing self-ban with no obvious way\nback short of another admin unbanning you.","operationId":"ban_user_route_api_v1_admin_users__user_id__ban_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BanUserRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/users/{user_id}/unban":{"post":{"tags":["Admin - Users"],"summary":"Unban User Route","operationId":"unban_user_route_api_v1_admin_users__user_id__unban_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/pricing/sales":{"get":{"tags":["Admin - Pricing"],"summary":"List Pricing Sales Route","operationId":"list_pricing_sales_route_api_v1_admin_pricing_sales_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/PricingSaleRead"},"type":"array","title":"Response List Pricing Sales Route Api V1 Admin Pricing Sales Get"}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"post":{"tags":["Admin - Pricing"],"summary":"Create Pricing Sale Route","operationId":"create_pricing_sale_route_api_v1_admin_pricing_sales_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePricingSaleRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PricingSaleRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/admin/pricing/sales/{sale_id}":{"delete":{"tags":["Admin - Pricing"],"summary":"Delete Pricing Sale Route","operationId":"delete_pricing_sale_route_api_v1_admin_pricing_sales__sale_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"sale_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Sale Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/pricing/discount-codes":{"get":{"tags":["Admin - Pricing"],"summary":"List Discount Codes Route","operationId":"list_discount_codes_route_api_v1_admin_pricing_discount_codes_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/DiscountCodeRead"},"type":"array","title":"Response List Discount Codes Route Api V1 Admin Pricing Discount Codes Get"}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"post":{"tags":["Admin - Pricing"],"summary":"Create Discount Code Route","operationId":"create_discount_code_route_api_v1_admin_pricing_discount_codes_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDiscountCodeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscountCodeRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/admin/pricing/discount-codes/{discount_code_id}/active":{"post":{"tags":["Admin - Pricing"],"summary":"Set Discount Code Active Route","operationId":"set_discount_code_active_route_api_v1_admin_pricing_discount_codes__discount_code_id__active_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"discount_code_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Discount Code Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetDiscountCodeActiveRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscountCodeRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/refunds":{"get":{"tags":["Admin - Refunds"],"summary":"List Refunds Route","description":"Every customer refund, newest first. Filter by status=manual_required\nto find the ones Pesapal couldn't carry and a human still owes someone -\nsee crud/refunds.py for when that happens.","operationId":"list_refunds_route_api_v1_admin_refunds_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/RefundStatus"},{"type":"null"}],"title":"Status"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Cursor for the next page","title":"Cursor"},"description":"Cursor for the next page"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":0,"description":"Page size","default":50,"title":"Size"},"description":"Page size"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CursorPage_RefundRead_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/refunds/{refund_id}/retry":{"post":{"tags":["Admin - Refunds"],"summary":"Retry Refund Route","description":"Re-sends a FAILED refund to Pesapal - for a rejection that has since\nbeen fixed (or was transient). Deliberately re-sends the existing row\nrather than creating a second one: Pesapal accepts only one refund per\npayment, so a duplicate would be rejected outright.","operationId":"retry_refund_route_api_v1_admin_refunds__refund_id__retry_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"refund_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Refund Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefundRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/refunds/{refund_id}/complete":{"post":{"tags":["Admin - Refunds"],"summary":"Complete Refund Route","description":"Marks a refund as actually paid out. Pesapal exposes no way to learn\nthis (no webhook, no status lookup for refunds), so reconciliation is\nnecessarily manual - this is also how a manual_required refund gets\nclosed once someone has sent the money another way.","operationId":"complete_refund_route_api_v1_admin_refunds__refund_id__complete_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"refund_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Refund Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefundRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/concierge/chat":{"post":{"tags":["Concierge"],"summary":"Chat","description":"Streams the concierge's reply over the AG-UI protocol as SSE - the\nfrontend consumes it with @tanstack/ai-react's useChat, not client.ts's\nusual fetch-and-zod-parse pattern (see ConciergeWidget.tsx).\n\nAG-UI rather than the Vercel AI SDK protocol because it emits tool\ncalls as first-class, individually addressable events with their own\nlifecycle, which is what lets the UI show what the agent is doing\nwhile it does it (searching flights, and so on) instead of a silent\npause. Both are pydantic-ai adapters over the same agent, so this is a\nwire-format change only - no agent or tool code moved.\n\nOpen to signed-out visitors. Anyone can ask about routes and fares -\nthat is the question a first-time visitor actually has, and putting a\nlogin in front of it means the one feature meant to help someone\ndecide is unavailable until after they have committed.\n\nSigning in is still what unlocks acting: the booking tools refuse\nwithout an account (external_services/concierge.py's _require_user),\nand the agent relays that as \"sign in to do this\" rather than failing.\n\nThe cost of anonymous access is real and bounded deliberately: every\nreply is a paid model call, so this route keeps the same IP rate limit\nit had, which is now the only thing standing between a scraper and the\nOpenAI bill. Worth watching once traffic is real.","operationId":"chat_api_v1_concierge_chat_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/notifications/stream":{"get":{"tags":["Notifications"],"summary":"Stream Notifications","description":"Server-Sent-Events stream of the signed-in user's own notifications.\n\nNo user_id path/query param by design - the stream is always scoped\nto whoever the auth cookie/bearer token actually belongs to, never a\ncaller-supplied id (a bare `/notifications/{user_id}` would let any\nsigned-in user snoop anyone else's notifications by changing the URL).\nWorks identically for a regular customer and a staff/admin account;\nwhich events a given user receives is decided by who\npublish_notification() is called for (crud/notifications.py), not by\nthis endpoint. Live-only: a fresh page load still needs GET\n/notifications below for anything that arrived before the stream\nconnected.\n\nNo try/except here: by the time this returns, the 200 and SSE headers\nare already on the wire, so a failure inside the generator (a dropped\nRedis connection, say) can't be turned into a different HTTP status\nany more - notification_streamer's own finally block (utils/\nredis_client.py) handles cleanup instead.","operationId":"stream_notifications_api_v1_notifications_stream_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/notifications":{"get":{"tags":["Notifications"],"summary":"List My Notifications","description":"The signed-in user's own notifications, most recent first - backs\nthe bell icon's panel on initial load/refresh; the SSE stream above\nonly covers what arrives while it's connected.\n\nThe unread count is deliberately not here - GET /notifications/\nunread-count already serves it, and that is where every caller reads\nit from, so computing it again on each list call was pure waste.","operationId":"list_my_notifications_api_v1_notifications_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Cursor for the next page","title":"Cursor"},"description":"Cursor for the next page"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":0,"description":"Page size","default":50,"title":"Size"},"description":"Page size"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CursorPage_NotificationRead_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/notifications/unread-count":{"get":{"tags":["Notifications"],"summary":"Get Unread Count","description":"Cheap standalone poll for the bell icon's badge - avoids fetching\nthe full notification list just to know if the count changed.","operationId":"get_unread_count_api_v1_notifications_unread_count_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnreadCountResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/notifications/{notification_id}/read":{"post":{"tags":["Notifications"],"summary":"Mark Notification Read","operationId":"mark_notification_read_api_v1_notifications__notification_id__read_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"notification_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Notification Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotificationRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/notifications/read-all":{"post":{"tags":["Notifications"],"summary":"Mark All Notifications Read","operationId":"mark_all_notifications_read_api_v1_notifications_read_all_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/notifications/{notification_id}":{"delete":{"tags":["Notifications"],"summary":"Delete Notification Route","description":"Hard-deletes one of the signed-in user's own notifications. Scoped\nto the owner the same way mark_notification_read is - a 404 either\nway for an unknown id or someone else's, never a 403 that would\nconfirm the id exists at all.","operationId":"delete_notification_route_api_v1_notifications__notification_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"notification_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Notification Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/jobs/departure-reminders":{"post":{"tags":["Jobs"],"summary":"Run Departure Reminder Sweep","description":"One sweep. Returns how many legs were reminded, for the scheduler's\nown logs - it has no other way to tell a working sweep from a silent\none.","operationId":"run_departure_reminder_sweep_api_v1_jobs_departure_reminders_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"integer"},"title":"Response Run Departure Reminder Sweep Api V1 Jobs Departure Reminders Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/health/live":{"get":{"tags":["Health"],"summary":"Liveness","description":"Deliberately checks nothing. Reaching this handler already proves\nwhat liveness asks: the process is up, the event loop is turning and\nrouting works. Anything more would risk restarting a healthy container\nover an unhealthy dependency.","operationId":"liveness_api_v1_health_live_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/health/ready":{"get":{"tags":["Health"],"summary":"Readiness","description":"Returns 503 when a dependency a request needs is down, so the\norchestrator can stop routing to this instance. The body is the same\nshape as GET /health, so anything already parsing that keeps working.\n\nEvent publishing is excluded on purpose: it is fire-and-forget\n(utils/events.py), and the API serves every read and write whether or\nnot the queue is reachable, so a queue outage must not take the whole\nAPI out of rotation.","operationId":"readiness_api_v1_health_ready_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/api/v1/health":{"get":{"tags":["Health"],"summary":"Health Check","description":"A plain `def`, not `async def` - every check below is a blocking\nsynchronous client call (SQLModel's Session and redis-py both block\nthe caller). FastAPI runs a sync route\nhandler in its own threadpool automatically; wrapping the same\nblocking calls in `async def` would instead stall the event loop for\nevery other in-flight request each time this one polls its\ndependencies.","operationId":"health_check_api_v1_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/api/v1/auth/google/login":{"get":{"tags":["Auth"],"summary":"Google Login","operationId":"google_login_api_v1_auth_google_login_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/auth/google/callback":{"get":{"tags":["Auth"],"summary":"Google Callback","operationId":"google_callback_api_v1_auth_google_callback_get","parameters":[{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"error","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/health/live":{"get":{"tags":["Health"],"summary":"Liveness","description":"Deliberately checks nothing. Reaching this handler already proves\nwhat liveness asks: the process is up, the event loop is turning and\nrouting works. Anything more would risk restarting a healthy container\nover an unhealthy dependency.","operationId":"liveness_health_live_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health/ready":{"get":{"tags":["Health"],"summary":"Readiness","description":"Returns 503 when a dependency a request needs is down, so the\norchestrator can stop routing to this instance. The body is the same\nshape as GET /health, so anything already parsing that keeps working.\n\nEvent publishing is excluded on purpose: it is fire-and-forget\n(utils/events.py), and the API serves every read and write whether or\nnot the queue is reachable, so a queue outage must not take the whole\nAPI out of rotation.","operationId":"readiness_health_ready_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/health":{"get":{"tags":["Health"],"summary":"Health Check","description":"A plain `def`, not `async def` - every check below is a blocking\nsynchronous client call (SQLModel's Session and redis-py both block\nthe caller). FastAPI runs a sync route\nhandler in its own threadpool automatically; wrapping the same\nblocking calls in `async def` would instead stall the event loop for\nevery other in-flight request each time this one polls its\ndependencies.","operationId":"health_check_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/":{"get":{"summary":"Hello","operationId":"hello__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"AdminBookingRead":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"duffel_order_id":{"type":"string","title":"Duffel Order Id"},"booking_reference":{"type":"string","title":"Booking Reference"},"status":{"$ref":"#/components/schemas/BookingStatus"},"total_amount":{"type":"string","title":"Total Amount"},"total_currency":{"type":"string","title":"Total Currency"},"base_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Base Amount"},"base_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Base Currency"},"tax_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tax Amount"},"tax_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tax Currency"},"owner_iata_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Iata Code"},"owner_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Name"},"refund_allowed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Refund Allowed"},"refund_penalty_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refund Penalty Amount"},"refund_penalty_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refund Penalty Currency"},"change_allowed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Change Allowed"},"change_penalty_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Change Penalty Amount"},"change_penalty_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Change Penalty Currency"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"cancelled_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Cancelled At"},"airline_initiated_change_detected_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Airline Initiated Change Detected At"},"slices":{"items":{"$ref":"#/components/schemas/BookingSlicePublic"},"type":"array","title":"Slices","default":[]},"passengers":{"items":{"$ref":"#/components/schemas/BookingPassengerPublic"},"type":"array","title":"Passengers","default":[]},"user_id":{"type":"string","format":"uuid","title":"User Id"},"user_email":{"type":"string","title":"User Email"}},"type":"object","required":["id","duffel_order_id","booking_reference","status","total_amount","total_currency","created_at","user_id","user_email"],"title":"AdminBookingRead","description":"BookingPublic plus who it belongs to - BookingPublic itself has no\nowner field since a customer's own booking list is implicitly\n'yours'; staff need to see whose it is."},"AdminCreateBookingRequest":{"properties":{"selected_offers":{"items":{"type":"string"},"type":"array","maxItems":1,"minItems":1,"title":"Selected Offers","description":"A single offer ID to book"},"passengers":{"items":{"$ref":"#/components/schemas/OrderPassenger"},"type":"array","minItems":1,"title":"Passengers"},"discount_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Discount Code","description":"Validated and applied server-side (crud/pricing.py) - never trust a client-computed discounted amount. Not used by admin-created bookings (routers/admin.py's create_admin_booking_route), which never collects real payment in the first place."},"user_id":{"type":"string","format":"uuid","title":"User Id"}},"type":"object","required":["selected_offers","passengers","user_id"],"title":"AdminCreateBookingRequest","description":"CheckoutRequest plus which existing customer this booking belongs\nto - the customer must already have a flyt account, there's no\nguest-booking concept in this schema (see\ncrud/payments.py's create_admin_booking)."},"AdminDashboardSummary":{"properties":{"total_bookings":{"type":"integer","title":"Total Bookings"},"bookings_today":{"type":"integer","title":"Bookings Today"},"bookings_this_week":{"type":"integer","title":"Bookings This Week"},"total_users":{"type":"integer","title":"Total Users"},"active_users":{"type":"integer","title":"Active Users","description":"Distinct users with at least one Booking row (any status) - see crud/users.py's count_active_users docstring for why this definition was picked over a fuzzier 'recently active' signal flyt doesn't track anywhere."},"revenue":{"items":{"$ref":"#/components/schemas/CurrencyTotal"},"type":"array","title":"Revenue","description":"Grouped by currency, never summed across currencies - see crud/bookings.py's get_revenue_by_currency docstring."}},"type":"object","required":["total_bookings","bookings_today","bookings_this_week","total_users","active_users","revenue"],"title":"AdminDashboardSummary"},"AdminUserDetail":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"email":{"type":"string","title":"Email"},"is_staff":{"type":"boolean","title":"Is Staff"},"is_superuser":{"type":"boolean","title":"Is Superuser"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"deleted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Deleted At"},"banned_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Banned At"},"banned_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Banned Reason"},"group_ids":{"items":{"type":"integer"},"type":"array","title":"Group Ids"},"banned_by_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Banned By Email"}},"type":"object","required":["id","email","is_staff","is_superuser","created_at","group_ids"],"title":"AdminUserDetail","description":"Extends the list row with fields only worth fetching for one user\nat a time (routers/admin.py's get_user_detail) - group_ids in\nparticular would mean an extra query per row if it were on\nAdminUserRead instead."},"AdminUserRead":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"email":{"type":"string","title":"Email"},"is_staff":{"type":"boolean","title":"Is Staff"},"is_superuser":{"type":"boolean","title":"Is Superuser"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"deleted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Deleted At"},"banned_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Banned At"},"banned_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Banned Reason"}},"type":"object","required":["id","email","is_staff","is_superuser","created_at"],"title":"AdminUserRead"},"Aircraft":{"properties":{"iata_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Iata Code"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}},"type":"object","title":"Aircraft"},"AirlineFacet":{"properties":{"code":{"type":"string","title":"Code"},"name":{"type":"string","title":"Name"},"count":{"type":"integer","title":"Count"}},"type":"object","required":["code","name","count"],"title":"AirlineFacet"},"Airport":{"properties":{"iata_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Iata Code"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"city_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City Name"},"time_zone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Time Zone","description":"IANA name, e.g. Africa/Nairobi. Duffel's departing_at/arriving_at are LOCAL times at their airport with no offset attached, so this is the only thing that turns one into an actual instant - see utils/flight_times.py."}},"type":"object","title":"Airport"},"AssignGroupsRequest":{"properties":{"group_ids":{"items":{"type":"integer"},"type":"array","title":"Group Ids"}},"type":"object","required":["group_ids"],"title":"AssignGroupsRequest"},"AssignPermissionsRequest":{"properties":{"codenames":{"items":{"type":"string"},"type":"array","title":"Codenames"}},"type":"object","required":["codenames"],"title":"AssignPermissionsRequest"},"AvailableService":{"properties":{"id":{"type":"string","title":"Id"},"type":{"type":"string","title":"Type","description":"e.g. baggage"},"total_amount":{"type":"string","title":"Total Amount"},"total_currency":{"type":"string","title":"Total Currency"},"maximum_quantity":{"type":"integer","title":"Maximum Quantity","default":1},"passenger_ids":{"items":{"type":"string"},"type":"array","title":"Passenger Ids","default":[]},"segment_ids":{"items":{"type":"string"},"type":"array","title":"Segment Ids","default":[]},"metadata":{"additionalProperties":true,"type":"object","title":"Metadata","default":{}}},"type":"object","required":["id","type","total_amount","total_currency"],"title":"AvailableService","description":"A purchasable ancillary on a priced offer - today only baggage\n(Duffel's only currently-supported available_services type). Only\npresent when the offer was fetched with return_available_services=true\n(see external_services/flight.py's confirm_price). segment_ids/\npassenger_ids scope which segment(s) and passenger(s) this specific\nservice applies to - a multi-segment trip typically needs a separate\nservice purchased per segment for the same physical bag."},"Baggage":{"properties":{"type":{"type":"string","title":"Type","description":"e.g. checked or carry_on"},"quantity":{"type":"integer","title":"Quantity"}},"type":"object","required":["type","quantity"],"title":"Baggage"},"BanUserRequest":{"properties":{"reason":{"type":"string","minLength":1,"title":"Reason"}},"type":"object","required":["reason"],"title":"BanUserRequest"},"Body_login_api_v1_token_post":{"properties":{"grant_type":{"anyOf":[{"type":"string","pattern":"^password$"},{"type":"null"}],"title":"Grant Type"},"username":{"type":"string","title":"Username"},"password":{"type":"string","format":"password","title":"Password"},"scope":{"type":"string","title":"Scope","default":""},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"format":"password","title":"Client Secret"}},"type":"object","required":["username","password"],"title":"Body_login_api_v1_token_post"},"BookingPassengerPublic":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"duffel_passenger_id":{"type":"string","title":"Duffel Passenger Id"},"passenger_type":{"anyOf":[{"$ref":"#/components/schemas/PassengerType"},{"type":"null"}]},"given_name":{"type":"string","title":"Given Name"},"family_name":{"type":"string","title":"Family Name"},"born_on":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Born On"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number"},"seat_designator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Seat Designator"},"cabin_class":{"anyOf":[{"$ref":"#/components/schemas/CabinClass"},{"type":"null"}]},"checked_bags":{"type":"integer","title":"Checked Bags","default":0},"carry_on_bags":{"type":"integer","title":"Carry On Bags","default":0},"tickets":{"items":{"$ref":"#/components/schemas/TicketPublic"},"type":"array","title":"Tickets","default":[]}},"type":"object","required":["id","duffel_passenger_id","given_name","family_name"],"title":"BookingPassengerPublic"},"BookingPublic":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"duffel_order_id":{"type":"string","title":"Duffel Order Id"},"booking_reference":{"type":"string","title":"Booking Reference"},"status":{"$ref":"#/components/schemas/BookingStatus"},"total_amount":{"type":"string","title":"Total Amount"},"total_currency":{"type":"string","title":"Total Currency"},"base_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Base Amount"},"base_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Base Currency"},"tax_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tax Amount"},"tax_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tax Currency"},"owner_iata_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Iata Code"},"owner_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Name"},"refund_allowed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Refund Allowed"},"refund_penalty_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refund Penalty Amount"},"refund_penalty_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refund Penalty Currency"},"change_allowed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Change Allowed"},"change_penalty_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Change Penalty Amount"},"change_penalty_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Change Penalty Currency"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"cancelled_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Cancelled At"},"airline_initiated_change_detected_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Airline Initiated Change Detected At"},"slices":{"items":{"$ref":"#/components/schemas/BookingSlicePublic"},"type":"array","title":"Slices","default":[]},"passengers":{"items":{"$ref":"#/components/schemas/BookingPassengerPublic"},"type":"array","title":"Passengers","default":[]}},"type":"object","required":["id","duffel_order_id","booking_reference","status","total_amount","total_currency","created_at"],"title":"BookingPublic"},"BookingSlicePublic":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"duffel_slice_id":{"type":"string","title":"Duffel Slice Id"},"origin_iata_code":{"type":"string","title":"Origin Iata Code"},"origin_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin Name"},"origin_city_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin City Name"},"destination_iata_code":{"type":"string","title":"Destination Iata Code"},"destination_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Destination Name"},"destination_city_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Destination City Name"},"duration":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Duration"},"flights":{"items":{"$ref":"#/components/schemas/FlightPublic"},"type":"array","title":"Flights","default":[]}},"type":"object","required":["id","duffel_slice_id","origin_iata_code","destination_iata_code"],"title":"BookingSlicePublic"},"BookingStatus":{"type":"string","enum":["pending","confirmed","cancelled"],"title":"BookingStatus"},"Cabin":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"marketing_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marketing Name"},"amenities":{"anyOf":[{"$ref":"#/components/schemas/CabinAmenities"},{"type":"null"}]}},"type":"object","title":"Cabin"},"CabinAmenities":{"properties":{"seat":{"anyOf":[{"$ref":"#/components/schemas/CabinAmenitySeat"},{"type":"null"}]},"wifi":{"anyOf":[{"$ref":"#/components/schemas/CabinAmenityWifi"},{"type":"null"}]},"power":{"anyOf":[{"$ref":"#/components/schemas/CabinAmenityPower"},{"type":"null"}]}},"type":"object","title":"CabinAmenities"},"CabinAmenityPower":{"properties":{"available":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Available"}},"type":"object","title":"CabinAmenityPower"},"CabinAmenitySeat":{"properties":{"pitch":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pitch"},"legroom":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Legroom"},"type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Type"}},"type":"object","title":"CabinAmenitySeat"},"CabinAmenityWifi":{"properties":{"available":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Available"},"cost":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cost"}},"type":"object","title":"CabinAmenityWifi"},"CabinClass":{"type":"string","enum":["first","business","premium_economy","economy"],"title":"CabinClass"},"CancellationRefundPreview":{"properties":{"amount":{"type":"string","title":"Amount"},"currency":{"type":"string","title":"Currency"},"to_original_payment_method":{"type":"boolean","title":"To Original Payment Method"},"manual_payout_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Manual Payout Reason"}},"type":"object","required":["amount","currency","to_original_payment_method"],"title":"CancellationRefundPreview","description":"What the *customer* would get if this cancellation is confirmed.\n\nDeliberately not the same number as the Duffel quote it sits beside:\nthat one is what returns to flyt's balance, and it can exceed what\nthe customer actually paid once a discount code is involved. Computed\nby crud/refunds.py so this preview and the refund eventually paid can\nnever disagree - the frontend renders this rather than doing its own\narithmetic."},"CardCheckoutResponse":{"properties":{"payment_id":{"type":"string","format":"uuid","title":"Payment Id"},"client_token":{"type":"string","title":"Client Token","description":"Passed to the DuffelPayments React component to collect card details directly with Duffel - never touches our backend"}},"type":"object","required":["payment_id","client_token"],"title":"CardCheckoutResponse"},"Carrier":{"properties":{"iata_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Iata Code"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"logo_symbol_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Logo Symbol Url"}},"type":"object","title":"Carrier"},"CarsBookingRequest":{"properties":{"quote_id":{"type":"string","minLength":1,"title":"Quote Id","description":"From a search result, re-read via GET /cars/quotes/{id} to confirm it is still valid."},"renter_email":{"type":"string","format":"email","title":"Renter Email","description":"Where the supplier sends the confirmation. Not necessarily the flyt account holder's address."}},"type":"object","required":["quote_id","renter_email"],"title":"CarsBookingRequest","description":"Duffel `cars.bookings.create`. Only two documented fields.\n\nSparse compared with a flight order - no passenger manifest, no\npayment object - because the rental agreement is formed at the desk\nagainst the renter's own licence and card. What Duffel books is the\nreservation; the supplier handles identity on collection."},"CarsSearchRequest":{"properties":{"pick_up_location":{"type":"string","maxLength":100,"minLength":3,"title":"Pick Up Location","description":"Location code or name, e.g. 'LHR'. An airport code is what links a rental to an arriving or departing flight."},"drop_off_location":{"anyOf":[{"type":"string","maxLength":100,"minLength":3},{"type":"null"}],"title":"Drop Off Location","description":"Defaults to pick_up_location when omitted (one-way vs return hire)."},"pick_up_date":{"type":"string","format":"date","title":"Pick Up Date"},"pick_up_time":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","title":"Pick Up Time","description":"HH:mm, 24-hour. Validated here so a malformed time fails at the edge rather than as an opaque Duffel 422."},"drop_off_date":{"type":"string","format":"date","title":"Drop Off Date"},"drop_off_time":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","title":"Drop Off Time"},"driver_age":{"anyOf":[{"type":"integer","maximum":99.0,"minimum":18.0},{"type":"null"}],"title":"Driver Age","description":"Optional, but worth sending: most suppliers price a young-driver surcharge, so omitting it can quote a price the renter cannot get."}},"type":"object","required":["pick_up_location","pick_up_date","pick_up_time","drop_off_date","drop_off_time"],"title":"CarsSearchRequest","description":"Duffel `cars.search`. Four required parameters, three optional.\n\nTimes are separate from dates in Duffel's model rather than being\ndatetimes: rental desks quote by local clock time at the branch, so\nthere is no zone to attach - the same reason flight times arrive as\nnaive local values (see utils/flight_times.py)."},"ChangePasswordRequest":{"properties":{"current_password":{"type":"string","title":"Current Password"},"new_password":{"type":"string","minLength":8,"title":"New Password"}},"type":"object","required":["current_password","new_password"],"title":"ChangePasswordRequest","description":"Self-service change while logged in - distinct from\nResetPasswordRequest's forgot-password flow, which proves identity via\na emailed token instead of the current password."},"CheckoutRequest":{"properties":{"selected_offers":{"items":{"type":"string"},"type":"array","maxItems":1,"minItems":1,"title":"Selected Offers","description":"A single offer ID to book"},"passengers":{"items":{"$ref":"#/components/schemas/OrderPassenger"},"type":"array","minItems":1,"title":"Passengers"},"discount_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Discount Code","description":"Validated and applied server-side (crud/pricing.py) - never trust a client-computed discounted amount. Not used by admin-created bookings (routers/admin.py's create_admin_booking_route), which never collects real payment in the first place."}},"type":"object","required":["selected_offers","passengers"],"title":"CheckoutRequest","description":"Starts a purchase. Deliberately excludes payment details - unlike\nDuffel's OrderCreate, no `payments` field, since payment is collected\nby Pesapal, not supplied by the client."},"CheckoutResponse":{"properties":{"payment_id":{"type":"string","format":"uuid","title":"Payment Id"},"redirect_url":{"type":"string","title":"Redirect Url","description":"Send the browser here to complete payment"}},"type":"object","required":["payment_id","redirect_url"],"title":"CheckoutResponse"},"ConditionDetail":{"properties":{"allowed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Allowed"},"penalty_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Penalty Amount"},"penalty_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Penalty Currency"}},"type":"object","title":"ConditionDetail","description":"Shared shape for every refund/change condition Duffel returns - on\nan offer (pre-purchase), a slice, or an order (post-purchase); see\nOffer.conditions/OfferSlice.conditions/Order.conditions below. `None`\nmeans Duffel doesn't support/report conditions for that airline/offer,\nnot that the traveler doesn't have that right."},"Conditions":{"properties":{"refund_before_departure":{"anyOf":[{"$ref":"#/components/schemas/ConditionDetail"},{"type":"null"}]},"change_before_departure":{"anyOf":[{"$ref":"#/components/schemas/ConditionDetail"},{"type":"null"}]}},"type":"object","title":"Conditions"},"ContactRequest":{"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name"},"email":{"type":"string","format":"email","title":"Email"},"subject":{"type":"string","maxLength":200,"minLength":1,"title":"Subject"},"message":{"type":"string","maxLength":5000,"minLength":1,"title":"Message"},"booking_reference":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Booking Reference"}},"type":"object","required":["name","email","subject","message"],"title":"ContactRequest"},"CreateDiscountCodeRequest":{"properties":{"code":{"type":"string","minLength":1,"title":"Code"},"discount_percentage":{"type":"number","maximum":100.0,"exclusiveMinimum":0.0,"title":"Discount Percentage"},"max_redemptions":{"anyOf":[{"type":"integer","minimum":1.0},{"type":"null"}],"title":"Max Redemptions"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"}},"type":"object","required":["code","discount_percentage"],"title":"CreateDiscountCodeRequest"},"CreatePricingSaleRequest":{"properties":{"name":{"type":"string","minLength":1,"title":"Name"},"markup_rate":{"type":"number","maximum":1.0,"minimum":0.0,"title":"Markup Rate","description":"e.g. 0.03 for 3%"},"starts_at":{"type":"string","format":"date-time","title":"Starts At"},"ends_at":{"type":"string","format":"date-time","title":"Ends At"}},"type":"object","required":["name","markup_rate","starts_at","ends_at"],"title":"CreatePricingSaleRequest"},"CurrencyTotal":{"properties":{"currency":{"type":"string","title":"Currency"},"total_amount":{"type":"string","title":"Total Amount"}},"type":"object","required":["currency","total_amount"],"title":"CurrencyTotal"},"CursorPage_AdminBookingRead_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/AdminBookingRead"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"current_page":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Page","description":"Cursor to refetch the current page"},"current_page_backwards":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Page Backwards","description":"Cursor to refetch the current page starting from the last item"},"previous_page":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Previous Page","description":"Cursor for the previous page"},"next_page":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Page","description":"Cursor for the next page"}},"type":"object","required":["items","total"],"title":"CursorPage[AdminBookingRead]"},"CursorPage_AdminUserRead_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/AdminUserRead"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"current_page":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Page","description":"Cursor to refetch the current page"},"current_page_backwards":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Page Backwards","description":"Cursor to refetch the current page starting from the last item"},"previous_page":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Previous Page","description":"Cursor for the previous page"},"next_page":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Page","description":"Cursor for the next page"}},"type":"object","required":["items","total"],"title":"CursorPage[AdminUserRead]"},"CursorPage_BookingPublic_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/BookingPublic"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"current_page":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Page","description":"Cursor to refetch the current page"},"current_page_backwards":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Page Backwards","description":"Cursor to refetch the current page starting from the last item"},"previous_page":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Previous Page","description":"Cursor for the previous page"},"next_page":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Page","description":"Cursor for the next page"}},"type":"object","required":["items","total"],"title":"CursorPage[BookingPublic]"},"CursorPage_NotificationRead_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/NotificationRead"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"current_page":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Page","description":"Cursor to refetch the current page"},"current_page_backwards":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Page Backwards","description":"Cursor to refetch the current page starting from the last item"},"previous_page":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Previous Page","description":"Cursor for the previous page"},"next_page":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Page","description":"Cursor for the next page"}},"type":"object","required":["items","total"],"title":"CursorPage[NotificationRead]"},"CursorPage_RefundRead_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/RefundRead"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"current_page":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Page","description":"Cursor to refetch the current page"},"current_page_backwards":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Page Backwards","description":"Cursor to refetch the current page starting from the last item"},"previous_page":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Previous Page","description":"Cursor for the previous page"},"next_page":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Page","description":"Cursor for the next page"}},"type":"object","required":["items","total"],"title":"CursorPage[RefundRead]"},"CustomerRefundRead":{"properties":{"amount":{"type":"string","title":"Amount"},"currency":{"type":"string","title":"Currency"},"status":{"$ref":"#/components/schemas/CustomerRefundStatus"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["amount","currency","status","created_at"],"title":"CustomerRefundRead","description":"A traveller's own view of their refund, on their booking page."},"CustomerRefundStatus":{"type":"string","enum":["processing","paid"],"title":"CustomerRefundStatus","description":"What a traveller is shown, deliberately narrower than the internal\nRefundStatus. failed and manual_required are flyt's problem to fix,\nnot something the customer can act on - from their side the money\nsimply hasn't arrived yet, which is exactly what `processing` says.\nSurfacing \"failed\" would alarm someone whose refund is still coming."},"DeleteAccountRequest":{"properties":{"password":{"type":"string","title":"Password"}},"type":"object","required":["password"],"title":"DeleteAccountRequest","description":"Requires the current password so a hijacked session (cookie/token\nwithout the password) can't destroy the account outright."},"DepartureWindow":{"type":"string","enum":["morning","afternoon","evening","night"],"title":"DepartureWindow","description":"Local departure time of the outbound leg. Travellers shop by \"I\nwant to leave in the morning\" far more often than by an exact time,\nand it's the filter airline sites lead with."},"DiscountCodeRead":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"code":{"type":"string","title":"Code"},"discount_percentage":{"type":"number","title":"Discount Percentage"},"max_redemptions":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Redemptions"},"times_redeemed":{"type":"integer","title":"Times Redeemed"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"is_active":{"type":"boolean","title":"Is Active"},"created_by_user_id":{"type":"string","format":"uuid","title":"Created By User Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","code","discount_percentage","max_redemptions","times_redeemed","expires_at","is_active","created_by_user_id","created_at"],"title":"DiscountCodeRead"},"DiscountPreviewRequest":{"properties":{"offer_id":{"type":"string","title":"Offer Id"},"discount_code":{"type":"string","title":"Discount Code"}},"type":"object","required":["offer_id","discount_code"],"title":"DiscountPreviewRequest","description":"POST /discounts/preview - a lightweight, non-persisting check so\nthe customer sees whether a code works (and roughly what it saves)\nbefore committing to checkout. Deliberately doesn't take the full\nCheckoutRequest (passengers/seats/baggage) - it previews the base\nfare's discount only; the real checkout call re-validates and applies\nthe code against the actual final total, which is what's authoritative."},"DiscountPreviewResponse":{"properties":{"original_amount":{"type":"string","title":"Original Amount","description":"Base fare with the current markup, no discount"},"discounted_amount":{"type":"string","title":"Discounted Amount"},"currency":{"type":"string","title":"Currency"},"discount_percentage":{"type":"number","title":"Discount Percentage"}},"type":"object","required":["original_amount","discounted_amount","currency","discount_percentage"],"title":"DiscountPreviewResponse"},"FlightPublic":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"duffel_segment_id":{"type":"string","title":"Duffel Segment Id"},"origin_iata_code":{"type":"string","title":"Origin Iata Code"},"origin_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin Name"},"origin_terminal":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin Terminal"},"destination_iata_code":{"type":"string","title":"Destination Iata Code"},"destination_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Destination Name"},"destination_terminal":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Destination Terminal"},"departing_at":{"type":"string","format":"date-time","title":"Departing At"},"arriving_at":{"type":"string","format":"date-time","title":"Arriving At"},"duration":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Duration"},"marketing_carrier_iata_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marketing Carrier Iata Code"},"marketing_carrier_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marketing Carrier Name"},"marketing_carrier_logo_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marketing Carrier Logo Url"},"marketing_carrier_flight_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marketing Carrier Flight Number"},"operating_carrier_iata_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Operating Carrier Iata Code"},"operating_carrier_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Operating Carrier Name"},"operating_carrier_flight_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Operating Carrier Flight Number"},"aircraft_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Aircraft Name"}},"type":"object","required":["id","duffel_segment_id","origin_iata_code","destination_iata_code","departing_at","arriving_at"],"title":"FlightPublic"},"FlightSearchResponse":{"properties":{"data":{"$ref":"#/components/schemas/OfferRequest"},"groups":{"items":{"$ref":"#/components/schemas/OfferGroup"},"type":"array","title":"Groups"},"meta":{"$ref":"#/components/schemas/PaginationMeta"},"facets":{"$ref":"#/components/schemas/OfferFacets"}},"type":"object","required":["data","groups","meta","facets"],"title":"FlightSearchResponse","description":"The offer request (metadata only) under `data`, the current page of\ngrouped offers under `groups`, pagination info under `meta`, and\nfacets (computed pre-filter, pre-pagination) under `facets`."},"ForgotPasswordRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"}},"type":"object","required":["email"],"title":"ForgotPasswordRequest"},"GroupCreate":{"properties":{"name":{"type":"string","title":"Name"}},"type":"object","required":["name"],"title":"GroupCreate"},"GroupRead":{"properties":{"id":{"type":"integer","title":"Id"},"name":{"type":"string","title":"Name"},"permissions":{"items":{"type":"string"},"type":"array","title":"Permissions"}},"type":"object","required":["id","name","permissions"],"title":"GroupRead"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HealthResponse":{"properties":{"status":{"type":"string","enum":["healthy","degraded","down"],"title":"Status"},"checked_at":{"type":"string","format":"date-time","title":"Checked At"},"services":{"additionalProperties":{"$ref":"#/components/schemas/ServiceHealth"},"type":"object","title":"Services"}},"type":"object","required":["status","checked_at","services"],"title":"HealthResponse"},"IdentityDocument":{"properties":{"unique_identifier":{"type":"string","title":"Unique Identifier","description":"Document/passport number"},"type":{"type":"string","title":"Type","description":"passport or tax_id","default":"passport"},"issuing_country_code":{"type":"string","maxLength":2,"minLength":2,"title":"Issuing Country Code"},"expires_on":{"type":"string","format":"date","title":"Expires On"}},"type":"object","required":["unique_identifier","issuing_country_code","expires_on"],"title":"IdentityDocument","description":"A passport/tax-id required by some itineraries - see `Offer.\npassenger_identity_documents_required`. Passed straight through to\nDuffel's order-creation payload."},"LoyaltyProgrammeAccount":{"properties":{"airline_iata_code":{"type":"string","maxLength":2,"minLength":2,"title":"Airline Iata Code"},"account_number":{"type":"string","title":"Account Number"}},"type":"object","required":["airline_iata_code","account_number"],"title":"LoyaltyProgrammeAccount","description":"A frequent-flyer account to attach to an already-priced offer via\nPATCH /air/offers/{offer_id}/passengers/{offer_passenger_id} (see\nrouters/flights.py's update_offer_passenger) - may reveal a\nloyalty-discounted fare, reflected by re-fetching the offer\nafterward. Not sent again at order-creation time; Duffel carries it\nthrough automatically from the offer it was attached to."},"NotificationRead":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"type":{"$ref":"#/components/schemas/NotificationType"},"title":{"type":"string","title":"Title"},"body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body"},"link_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Link Url"},"read_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Read At"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","type","title","body","link_url","read_at","created_at"],"title":"NotificationRead"},"NotificationType":{"type":"string","enum":["booking_confirmed","booking_failed","airline_change","cancellation_confirmed","change_confirmed","departure_reminder","support_request","discount_redemption_failed"],"title":"NotificationType","description":"One value per producer site (see crud/notifications.py) - kept as\na real enum, not a free string, so a typo'd type can't silently\ncreate a notification the frontend has no icon/copy for."},"Offer":{"properties":{"id":{"type":"string","title":"Id"},"live_mode":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Live Mode"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"total_amount":{"type":"string","title":"Total Amount"},"total_currency":{"type":"string","title":"Total Currency"},"base_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Base Amount"},"base_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Base Currency"},"tax_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tax Amount"},"tax_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tax Currency"},"total_emissions_kg":{"anyOf":[{"type":"string"},{"type":"integer"},{"type":"number"},{"type":"null"}],"title":"Total Emissions Kg"},"partial":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Partial"},"owner":{"anyOf":[{"$ref":"#/components/schemas/Carrier"},{"type":"null"}]},"slices":{"items":{"$ref":"#/components/schemas/OfferSlice"},"type":"array","title":"Slices","default":[]},"passengers":{"items":{"$ref":"#/components/schemas/OfferPassenger"},"type":"array","title":"Passengers","default":[]},"passenger_identity_documents_required":{"type":"boolean","title":"Passenger Identity Documents Required","default":false},"supported_passenger_identity_document_types":{"items":{"type":"string"},"type":"array","title":"Supported Passenger Identity Document Types","default":[]},"conditions":{"anyOf":[{"$ref":"#/components/schemas/Conditions"},{"type":"null"}]},"payment_requirements":{"anyOf":[{"$ref":"#/components/schemas/PaymentRequirements"},{"type":"null"}]},"available_services":{"items":{"$ref":"#/components/schemas/AvailableService"},"type":"array","title":"Available Services","default":[]}},"type":"object","required":["id","total_amount","total_currency"],"title":"Offer"},"OfferFacets":{"properties":{"airlines":{"items":{"$ref":"#/components/schemas/AirlineFacet"},"type":"array","title":"Airlines"},"price_min":{"type":"number","title":"Price Min"},"price_max":{"type":"number","title":"Price Max"},"has_direct":{"type":"boolean","title":"Has Direct"},"has_one_stop":{"type":"boolean","title":"Has One Stop"},"has_multi_stop":{"type":"boolean","title":"Has Multi Stop"}},"type":"object","required":["airlines","price_min","price_max","has_direct","has_one_stop","has_multi_stop"],"title":"OfferFacets","description":"Always computed from the full, unfiltered offer list for this\nsearch, regardless of which filters/page were requested - so facet\ncounts stay stable as the user pages through or narrows results."},"OfferGroup":{"properties":{"primary":{"$ref":"#/components/schemas/Offer"},"alternates":{"items":{"$ref":"#/components/schemas/Offer"},"type":"array","title":"Alternates","default":[]}},"type":"object","required":["primary"],"title":"OfferGroup","description":"Offers that share an itinerary (same origin/destination per slice)\ncollapsed into one card: the cheapest as `primary`, the rest browsable\nas `alternates` (see backend/utils/offer_filtering.py:group_by_route)."},"OfferPassenger":{"properties":{"id":{"type":"string","title":"Id"},"type":{"anyOf":[{"$ref":"#/components/schemas/PassengerType"},{"type":"null"}]},"age":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Age"},"fare_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fare Type","description":"e.g. student, same_day_change - special fare eligibility"}},"type":"object","required":["id"],"title":"OfferPassenger"},"OfferPassengerUpdate":{"properties":{"given_name":{"type":"string","title":"Given Name"},"family_name":{"type":"string","title":"Family Name"},"loyalty_programme_accounts":{"items":{"$ref":"#/components/schemas/LoyaltyProgrammeAccount"},"type":"array","minItems":1,"title":"Loyalty Programme Accounts"}},"type":"object","required":["given_name","family_name","loyalty_programme_accounts"],"title":"OfferPassengerUpdate","description":"Request body for attaching loyalty programme accounts to a\nspecific passenger on an already-created offer. Duffel requires the\npassenger's name alongside the loyalty details."},"OfferPriceRequest":{"properties":{"offer_id":{"type":"string","title":"Offer Id","description":"Duffel offer ID (off_...)"}},"type":"object","required":["offer_id"],"title":"OfferPriceRequest","description":"Identifies the offer whose live price should be confirmed."},"OfferRequest":{"properties":{"id":{"type":"string","title":"Id"},"live_mode":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Live Mode"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"passengers":{"items":{"$ref":"#/components/schemas/OfferPassenger"},"type":"array","title":"Passengers","default":[]}},"type":"object","required":["id"],"title":"OfferRequest","description":"Offer-request metadata only - the offers themselves are paginated\nseparately, under `groups` on FlightSearchResponse."},"OfferRequestCreate":{"properties":{"slices":{"items":{"$ref":"#/components/schemas/SlicePlan"},"type":"array","minItems":1,"title":"Slices"},"passengers":{"items":{"$ref":"#/components/schemas/SearchPassenger"},"type":"array","minItems":1,"title":"Passengers"},"cabin_class":{"anyOf":[{"$ref":"#/components/schemas/CabinClass"},{"type":"null"}]},"max_connections":{"anyOf":[{"type":"integer","maximum":2.0,"minimum":0.0},{"type":"null"}],"title":"Max Connections"}},"type":"object","required":["slices","passengers"],"title":"OfferRequestCreate"},"OfferResponse":{"properties":{"data":{"$ref":"#/components/schemas/Offer"}},"type":"object","required":["data"],"title":"OfferResponse","description":"Duffel envelope for a single, freshly priced offer."},"OfferSegment":{"properties":{"id":{"type":"string","title":"Id"},"origin":{"$ref":"#/components/schemas/Airport"},"destination":{"$ref":"#/components/schemas/Airport"},"origin_terminal":{"anyOf":[{"type":"string"},{"type":"integer"},{"type":"null"}],"title":"Origin Terminal"},"destination_terminal":{"anyOf":[{"type":"string"},{"type":"integer"},{"type":"null"}],"title":"Destination Terminal"},"departing_at":{"type":"string","format":"date-time","title":"Departing At"},"arriving_at":{"type":"string","format":"date-time","title":"Arriving At"},"duration":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Duration"},"distance":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Distance"},"marketing_carrier":{"anyOf":[{"$ref":"#/components/schemas/Carrier"},{"type":"null"}]},"marketing_carrier_flight_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marketing Carrier Flight Number"},"operating_carrier":{"anyOf":[{"$ref":"#/components/schemas/Carrier"},{"type":"null"}]},"operating_carrier_flight_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Operating Carrier Flight Number"},"aircraft":{"anyOf":[{"$ref":"#/components/schemas/Aircraft"},{"type":"null"}]},"stops":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Stops","default":[]},"passengers":{"items":{"$ref":"#/components/schemas/OfferSegmentPassenger"},"type":"array","title":"Passengers","default":[]}},"type":"object","required":["id","origin","destination","departing_at","arriving_at"],"title":"OfferSegment"},"OfferSegmentPassenger":{"properties":{"passenger_id":{"type":"string","title":"Passenger Id"},"baggages":{"items":{"$ref":"#/components/schemas/Baggage"},"type":"array","title":"Baggages","default":[]},"cabin":{"anyOf":[{"$ref":"#/components/schemas/Cabin"},{"type":"null"}]},"cabin_class":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cabin Class"},"cabin_class_marketing_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cabin Class Marketing Name"},"fare_basis_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fare Basis Code"}},"type":"object","required":["passenger_id"],"title":"OfferSegmentPassenger","description":"Per-passenger details for one segment - baggages is the allowance\n*included* in the fare (purchasable extra baggage is a separate\nDuffel `available_services` ancillary-purchase flow, not modeled\nhere). cabin_class/cabin_class_marketing_name/fare_basis_code answer\n\"what am I actually getting\" more precisely than the offer-request's\nown cabin_class - e.g. a request for \"economy\" can be granted as the\nairline's \"Basic Economy\" on some segments and not others. `cabin` is\nthe richer, nested version of the same info (seat pitch, wifi, power)."},"OfferSlice":{"properties":{"id":{"type":"string","title":"Id"},"origin":{"$ref":"#/components/schemas/Airport"},"destination":{"$ref":"#/components/schemas/Airport"},"duration":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Duration"},"fare_brand_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fare Brand Name","description":"e.g. Basic, Standard, Flex - airline's own fare tier name"},"conditions":{"anyOf":[{"$ref":"#/components/schemas/OfferSliceConditions"},{"type":"null"}]},"segments":{"items":{"$ref":"#/components/schemas/OfferSegment"},"type":"array","title":"Segments","default":[]}},"type":"object","required":["id","origin","destination"],"title":"OfferSlice"},"OfferSliceConditions":{"properties":{"change_before_departure":{"anyOf":[{"$ref":"#/components/schemas/ConditionDetail"},{"type":"null"}]},"priority_check_in":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Priority Check In"},"priority_boarding":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Priority Boarding"},"advance_seat_selection":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Advance Seat Selection"}},"type":"object","title":"OfferSliceConditions","description":"Richer than the offer/order-level Conditions - per-slice perks\nbeyond just refund/change, e.g. whether this specific fare includes\npriority boarding."},"OfferSortKey":{"type":"string","enum":["best","price","duration","departure","arrival"],"title":"OfferSortKey"},"Order":{"properties":{"id":{"type":"string","title":"Id"},"booking_reference":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Booking Reference"},"live_mode":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Live Mode"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"cancelled_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Cancelled At"},"total_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Total Amount"},"total_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Total Currency"},"base_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Base Amount"},"base_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Base Currency"},"tax_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tax Amount"},"tax_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tax Currency"},"owner":{"anyOf":[{"$ref":"#/components/schemas/Carrier"},{"type":"null"}]},"slices":{"items":{"$ref":"#/components/schemas/OrderSlice"},"type":"array","title":"Slices","default":[]},"passengers":{"items":{"$ref":"#/components/schemas/OrderPassengerDetail"},"type":"array","title":"Passengers","default":[]},"payment_status":{"anyOf":[{"$ref":"#/components/schemas/backend__schemas__duffel_orders__PaymentStatus"},{"type":"null"}]},"conditions":{"anyOf":[{"$ref":"#/components/schemas/Conditions"},{"type":"null"}]},"documents":{"items":{"$ref":"#/components/schemas/OrderDocument"},"type":"array","title":"Documents","default":[]},"available_actions":{"items":{"type":"string"},"type":"array","title":"Available Actions","default":[]}},"type":"object","required":["id"],"title":"Order"},"OrderCancellationQuote":{"properties":{"id":{"type":"string","title":"Id"},"order_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Order Id"},"refund_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refund Amount"},"refund_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refund Currency"},"refund_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refund To"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"confirmed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Confirmed At"}},"type":"object","required":["id"],"title":"OrderCancellationQuote"},"OrderCancellationQuoteResponse":{"properties":{"data":{"$ref":"#/components/schemas/OrderCancellationQuote"},"customer_refund":{"$ref":"#/components/schemas/CancellationRefundPreview"}},"type":"object","required":["data","customer_refund"],"title":"OrderCancellationQuoteResponse","description":"Duffel's cancellation quote plus flyt's own customer-facing figure."},"OrderCancellationResponse":{"properties":{"data":{"$ref":"#/components/schemas/OrderCancellationQuote"}},"type":"object","required":["data"],"title":"OrderCancellationResponse","description":"Duffel envelope for an order cancellation quote or its confirmation."},"OrderChange":{"properties":{"id":{"type":"string","title":"Id"},"order_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Order Id"},"confirmed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Confirmed At"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"},"change_total_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Change Total Amount"},"change_total_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Change Total Currency"},"new_total_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"New Total Amount"},"new_total_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"New Total Currency"},"penalty_total_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Penalty Total Amount"},"penalty_total_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Penalty Total Currency"},"refund_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refund To"}},"type":"object","required":["id"],"title":"OrderChange"},"OrderChangeConfirm":{"properties":{"payment":{"$ref":"#/components/schemas/OrderChangePayment"}},"type":"object","required":["payment"],"title":"OrderChangeConfirm","description":"Step 4: pay for and finalize the pending change."},"OrderChangeCreate":{"properties":{"selected_order_change_offer":{"type":"string","title":"Selected Order Change Offer","description":"An order_change_offer id (oco_...)"}},"type":"object","required":["selected_order_change_offer"],"title":"OrderChangeCreate","description":"Step 3: create a pending change from a chosen offer - not\nconfirmed/charged yet."},"OrderChangeOffer":{"properties":{"id":{"type":"string","title":"Id"},"order_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Order Id"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"change_total_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Change Total Amount"},"change_total_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Change Total Currency"},"new_total_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"New Total Amount"},"new_total_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"New Total Currency"},"penalty_total_amount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Penalty Total Amount"},"penalty_total_currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Penalty Total Currency"},"refund_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refund To"},"live_mode":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Live Mode"}},"type":"object","required":["id"],"title":"OrderChangeOffer","description":"Step 2: one priced way to satisfy the change request - the\ncustomer picks one of these to proceed with."},"OrderChangeOffersData":{"properties":{"offers":{"items":{"$ref":"#/components/schemas/OrderChangeOffer"},"type":"array","title":"Offers","default":[]}},"type":"object","title":"OrderChangeOffersData"},"OrderChangeOffersResponse":{"properties":{"data":{"$ref":"#/components/schemas/OrderChangeOffersData"}},"type":"object","required":["data"],"title":"OrderChangeOffersResponse"},"OrderChangePayment":{"properties":{"type":{"type":"string","title":"Type","default":"balance"},"currency":{"type":"string","maxLength":3,"minLength":3,"title":"Currency"},"amount":{"type":"string","title":"Amount"}},"type":"object","required":["currency","amount"],"title":"OrderChangePayment"},"OrderChangeRequest":{"properties":{"id":{"type":"string","title":"Id"},"order_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Order Id"},"live_mode":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Live Mode"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"}},"type":"object","required":["id"],"title":"OrderChangeRequest"},"OrderChangeRequestResponse":{"properties":{"data":{"$ref":"#/components/schemas/OrderChangeRequest"}},"type":"object","required":["data"],"title":"OrderChangeRequestResponse"},"OrderChangeResponse":{"properties":{"data":{"$ref":"#/components/schemas/OrderChange"}},"type":"object","required":["data"],"title":"OrderChangeResponse"},"OrderChangeSliceAdd":{"properties":{"origin":{"type":"string","maxLength":3,"minLength":3,"title":"Origin"},"destination":{"type":"string","maxLength":3,"minLength":3,"title":"Destination"},"departure_date":{"type":"string","format":"date","title":"Departure Date"},"cabin_class":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cabin Class"}},"type":"object","required":["origin","destination","departure_date"],"title":"OrderChangeSliceAdd"},"OrderChangeSliceRemove":{"properties":{"slice_id":{"type":"string","title":"Slice Id"}},"type":"object","required":["slice_id"],"title":"OrderChangeSliceRemove"},"OrderChangeSlices":{"properties":{"remove":{"items":{"$ref":"#/components/schemas/OrderChangeSliceRemove"},"type":"array","minItems":1,"title":"Remove"},"add":{"items":{"$ref":"#/components/schemas/OrderChangeSliceAdd"},"type":"array","minItems":1,"title":"Add"}},"type":"object","required":["remove","add"],"title":"OrderChangeSlices","description":"Step 1 request body: describe what should change - which slice(s)\nto remove and what new slice(s) to search for in their place. The\norder_id itself comes from the URL path\n(routers/bookings.py's create_order_change_request), not this body.\nDoesn't touch the order yet; just returns candidate offers to\nreview."},"OrderCreate":{"properties":{"selected_offers":{"items":{"type":"string"},"type":"array","maxItems":1,"minItems":1,"title":"Selected Offers","description":"A single offer ID to book"},"passengers":{"items":{"$ref":"#/components/schemas/OrderPassenger"},"type":"array","minItems":1,"title":"Passengers"},"payments":{"items":{"$ref":"#/components/schemas/OrderPayment"},"type":"array","minItems":1,"title":"Payments"},"services":{"anyOf":[{"items":{"$ref":"#/components/schemas/OrderService"},"type":"array"},{"type":"null"}],"title":"Services"},"metadata":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Metadata","description":"Arbitrary key-value pairs Duffel stores on the order and echoes back on every future read - our own reconciliation hook back to Payment/Booking, independent of Duffel's own order/booking_reference IDs."}},"type":"object","required":["selected_offers","passengers","payments"],"title":"OrderCreate"},"OrderDocument":{"properties":{"unique_identifier":{"type":"string","title":"Unique Identifier"},"type":{"type":"string","title":"Type","description":"e.g. electronic_ticket"},"passenger_ids":{"items":{"type":"string"},"type":"array","title":"Passenger Ids","default":[]}},"type":"object","required":["unique_identifier","type"],"title":"OrderDocument","description":"An issued travel document (e-ticket, or an EMD for a service), only\npresent once an order has actually been paid for."},"OrderPassenger":{"properties":{"id":{"type":"string","title":"Id","description":"Passenger ID issued by the offer request"},"title":{"type":"string","title":"Title","description":"e.g. mr, ms, mrs, miss, dr"},"gender":{"type":"string","title":"Gender","description":"m or f, as required by airlines"},"given_name":{"type":"string","title":"Given Name"},"family_name":{"type":"string","title":"Family Name"},"born_on":{"type":"string","format":"date","title":"Born On"},"email":{"type":"string","format":"email","title":"Email"},"phone_number":{"type":"string","title":"Phone Number","description":"E.164 format, e.g. +442080160508"},"infant_passenger_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Infant Passenger Id","description":"For adults responsible for an infant: the infant's passenger ID"},"identity_documents":{"anyOf":[{"items":{"$ref":"#/components/schemas/IdentityDocument"},"type":"array"},{"type":"null"}],"title":"Identity Documents","description":"Required when the offer's passenger_identity_documents_required is true"},"seat_designator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Seat Designator","description":"Seat picked in our own seat-map UI (e.g. '14C'), stored on our booking record for display. Not itself sent to Duffel - seat_service_id below is what actually reserves it."},"seat_service_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Seat Service Id","description":"The seat's `available_services[].id` (ase_...) from GET /air/seat_maps, for THIS passenger. Stripped from the passenger payload before sending to Duffel (it's not a real passenger field) and instead collected into OrderCreate.services below, so the airline actually holds the seat."},"extra_baggage_service_ids":{"items":{"type":"string"},"type":"array","title":"Extra Baggage Service Ids","description":"Purchasable-baggage `available_services[].id` (ase_.../bag_...) picked for this passenger. Same treatment as seat_service_id - stripped from the passenger payload and collected into OrderCreate.services instead."}},"type":"object","required":["id","title","gender","given_name","family_name","born_on","email","phone_number"],"title":"OrderPassenger"},"OrderPassengerDetail":{"properties":{"id":{"type":"string","title":"Id"},"type":{"anyOf":[{"$ref":"#/components/schemas/PassengerType"},{"type":"null"}]},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"gender":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gender"},"given_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Given Name"},"family_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Family Name"},"born_on":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Born On"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number"},"infant_passenger_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Infant Passenger Id"}},"type":"object","required":["id"],"title":"OrderPassengerDetail"},"OrderPayment":{"properties":{"type":{"type":"string","title":"Type","description":"balance or arc_bsp_cash","default":"balance"},"currency":{"type":"string","maxLength":3,"minLength":3,"title":"Currency"},"amount":{"type":"string","title":"Amount","description":"Must match the offer's total_amount"}},"type":"object","required":["currency","amount"],"title":"OrderPayment"},"OrderResponse":{"properties":{"data":{"$ref":"#/components/schemas/Order"}},"type":"object","required":["data"],"title":"OrderResponse","description":"Duffel envelope for a single order."},"OrderSegment":{"properties":{"id":{"type":"string","title":"Id"},"origin":{"$ref":"#/components/schemas/Airport"},"destination":{"$ref":"#/components/schemas/Airport"},"origin_terminal":{"anyOf":[{"type":"string"},{"type":"integer"},{"type":"null"}],"title":"Origin Terminal"},"destination_terminal":{"anyOf":[{"type":"string"},{"type":"integer"},{"type":"null"}],"title":"Destination Terminal"},"departing_at":{"type":"string","format":"date-time","title":"Departing At"},"arriving_at":{"type":"string","format":"date-time","title":"Arriving At"},"duration":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Duration"},"distance":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Distance"},"marketing_carrier":{"anyOf":[{"$ref":"#/components/schemas/Carrier"},{"type":"null"}]},"marketing_carrier_flight_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marketing Carrier Flight Number"},"operating_carrier":{"anyOf":[{"$ref":"#/components/schemas/Carrier"},{"type":"null"}]},"operating_carrier_flight_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Operating Carrier Flight Number"},"aircraft":{"anyOf":[{"$ref":"#/components/schemas/Aircraft"},{"type":"null"}]},"stops":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Stops","default":[]},"passengers":{"items":{"$ref":"#/components/schemas/OfferSegmentPassenger"},"type":"array","title":"Passengers","default":[]}},"type":"object","required":["id","origin","destination","departing_at","arriving_at"],"title":"OrderSegment"},"OrderService":{"properties":{"id":{"type":"string","title":"Id"},"quantity":{"type":"integer","title":"Quantity","default":1}},"type":"object","required":["id"],"title":"OrderService","description":"A paid ancillary to attach to the order - seats\n(OrderPassenger.seat_service_id) and extra baggage\n(OrderPassenger.extra_baggage_service_ids), one entry per\nservice/passenger combination."},"OrderSlice":{"properties":{"id":{"type":"string","title":"Id"},"origin":{"$ref":"#/components/schemas/Airport"},"destination":{"$ref":"#/components/schemas/Airport"},"duration":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Duration"},"segments":{"items":{"$ref":"#/components/schemas/OrderSegment"},"type":"array","title":"Segments","default":[]}},"type":"object","required":["id","origin","destination"],"title":"OrderSlice"},"PaginationMeta":{"properties":{"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"total":{"type":"integer","title":"Total"},"has_more":{"type":"boolean","title":"Has More"}},"type":"object","required":["limit","offset","total","has_more"],"title":"PaginationMeta","description":"Offset pagination, now used only by flight search.\n\nEvery DB-backed list moved to cursor pagination (fastapi-pagination's\nCursorPage) because OFFSET makes the database walk and discard every\nrow it skips, so deep pages get steadily slower as data grows. Flight\nsearch deliberately stays here: it pages a Redis-cached Duffel result\nalready held in memory (utils/offer_filtering.py slices a Python\nlist), so there is no row-skipping to avoid, and its UI shows a\nresult count and lets you jump around - neither of which cursor\npagination gives you."},"PassengerType":{"type":"string","enum":["adult","child","infant_without_seat"],"title":"PassengerType"},"PaymentRequirements":{"properties":{"requires_instant_payment":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Requires Instant Payment"},"price_guarantee_expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Price Guarantee Expires At"},"payment_required_by":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Payment Required By"}},"type":"object","title":"PaymentRequirements","description":"Whether an offer must be paid immediately or can be held (see\nDuffel's holding-orders-and-paying-later guide) - we always pay\ninstantly today (see crud/payments.py's _complete_booking), so this is\nsurfaced but not yet acted on; an offer with requires_instant_payment\nFalse would still work, just without using the hold option."},"PaymentStatusResponse":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"status":{"$ref":"#/components/schemas/backend__models__payments__PaymentStatus"},"booking_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Booking Id"},"failure_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failure Reason"},"booking":{"anyOf":[{"$ref":"#/components/schemas/BookingPublic"},{"type":"null"}],"description":"Populated once status is completed - avoids a second round-trip from the payment-callback page"}},"type":"object","required":["id","status"],"title":"PaymentStatusResponse"},"PermissionRead":{"properties":{"codename":{"type":"string","title":"Codename"},"name":{"type":"string","title":"Name"},"content_type":{"type":"string","title":"Content Type"}},"type":"object","required":["codename","name","content_type"],"title":"PermissionRead"},"Place":{"properties":{"id":{"type":"string","title":"Id"},"type":{"$ref":"#/components/schemas/PlaceType"},"name":{"type":"string","title":"Name"},"iata_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Iata Code"},"iata_country_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Iata Country Code"},"iata_city_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Iata City Code"},"icao_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Icao Code"},"city_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City Name"},"latitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Latitude"},"longitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Longitude"},"time_zone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Time Zone"},"airports":{"anyOf":[{"items":{"$ref":"#/components/schemas/Place"},"type":"array"},{"type":"null"}],"title":"Airports","description":"Present on city-type places"}},"type":"object","required":["id","type","name"],"title":"Place"},"PlaceSuggestionsResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Place"},"type":"array","title":"Data"},"meta":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Meta"}},"type":"object","required":["data"],"title":"PlaceSuggestionsResponse","description":"Duffel envelope for a list of airport/city suggestions."},"PlaceType":{"type":"string","enum":["airport","city"],"title":"PlaceType"},"PopularRoute":{"properties":{"origin_iata_code":{"type":"string","title":"Origin Iata Code"},"origin_city_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin City Name"},"destination_iata_code":{"type":"string","title":"Destination Iata Code"},"destination_city_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Destination City Name"},"booking_count":{"type":"integer","title":"Booking Count"},"destination_image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Destination Image Url"},"destination_image_attribution_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Destination Image Attribution Name"},"destination_image_attribution_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Destination Image Attribution Url"}},"type":"object","required":["origin_iata_code","destination_iata_code","booking_count"],"title":"PopularRoute","description":"Aggregated from BookingSlice (crud/bookings.py's\nget_popular_routes) - shared by the staff dashboard\n(routers/admin.py) and the public destinations endpoint\n(routers/flights.py), which differ only in their min_bookings\nthreshold, not in shape."},"PricingSaleRead":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"name":{"type":"string","title":"Name"},"markup_rate":{"type":"number","title":"Markup Rate"},"starts_at":{"type":"string","format":"date-time","title":"Starts At"},"ends_at":{"type":"string","format":"date-time","title":"Ends At"},"created_by_user_id":{"type":"string","format":"uuid","title":"Created By User Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","name","markup_rate","starts_at","ends_at","created_by_user_id","created_at"],"title":"PricingSaleRead"},"RefundRead":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"payment_id":{"type":"string","format":"uuid","title":"Payment Id"},"booking_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Booking Id"},"amount":{"type":"string","title":"Amount"},"currency":{"type":"string","title":"Currency"},"status":{"$ref":"#/components/schemas/RefundStatus"},"failure_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failure Reason"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","payment_id","booking_id","amount","currency","status","failure_reason","created_at","updated_at"],"title":"RefundRead","description":"Admin-facing view of one customer refund (routers/admin.py)."},"RefundStatus":{"type":"string","enum":["requested","failed","manual_required","completed"],"title":"RefundStatus","description":"Pesapal never tells us a refund actually landed - its\nRefundRequest returns \"received, pending our finance team's\napproval\", with no webhook or status endpoint afterwards (unlike a\npayment, which has GetTransactionStatus). So REQUESTED is as far as\nthis app can get on its own; COMPLETED only ever comes from a human\nmarking it so in the admin UI after reconciling."},"ResetPasswordRequest":{"properties":{"token":{"type":"string","title":"Token"},"new_password":{"type":"string","minLength":8,"title":"New Password"}},"type":"object","required":["token","new_password"],"title":"ResetPasswordRequest"},"SearchPassenger":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/PassengerType"},{"type":"null"}]},"age":{"anyOf":[{"type":"integer","maximum":130.0,"minimum":0.0},{"type":"null"}],"title":"Age"}},"type":"object","title":"SearchPassenger","description":"A passenger in an offer request: give either a type or an exact age."},"ServiceHealth":{"properties":{"status":{"type":"string","enum":["healthy","unhealthy","not_configured"],"title":"Status"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"}},"type":"object","required":["status"],"title":"ServiceHealth"},"SetDiscountCodeActiveRequest":{"properties":{"is_active":{"type":"boolean","title":"Is Active"}},"type":"object","required":["is_active"],"title":"SetDiscountCodeActiveRequest"},"SetStaffRequest":{"properties":{"is_staff":{"type":"boolean","title":"Is Staff"}},"type":"object","required":["is_staff"],"title":"SetStaffRequest"},"SlicePlan":{"properties":{"origin":{"type":"string","maxLength":3,"minLength":3,"title":"Origin","description":"IATA code of the origin airport/city"},"destination":{"type":"string","maxLength":3,"minLength":3,"title":"Destination","description":"IATA code of the destination airport/city"},"departure_date":{"type":"string","format":"date","title":"Departure Date","description":"Departure date (YYYY-MM-DD)"}},"type":"object","required":["origin","destination","departure_date"],"title":"SlicePlan"},"StaysBookingCreate":{"properties":{"quote_id":{"type":"string","title":"Quote Id","description":"A quote id (stq_...) from create_quote"},"email":{"type":"string","format":"email","title":"Email"},"phone_number":{"type":"string","title":"Phone Number","description":"E.164 format, e.g. +442080160508"},"guests":{"items":{"$ref":"#/components/schemas/StaysBookingGuest"},"type":"array","minItems":1,"title":"Guests"},"accommodation_special_requests":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Accommodation Special Requests"}},"type":"object","required":["quote_id","email","phone_number","guests"],"title":"StaysBookingCreate"},"StaysBookingGuest":{"properties":{"given_name":{"type":"string","title":"Given Name"},"family_name":{"type":"string","title":"Family Name"},"born_on":{"type":"string","format":"date","title":"Born On"}},"type":"object","required":["given_name","family_name","born_on"],"title":"StaysBookingGuest"},"StaysGeographicCoordinates":{"properties":{"latitude":{"type":"number","maximum":90.0,"minimum":-90.0,"title":"Latitude"},"longitude":{"type":"number","maximum":180.0,"minimum":-180.0,"title":"Longitude"}},"type":"object","required":["latitude","longitude"],"title":"StaysGeographicCoordinates"},"StaysGuest":{"properties":{"type":{"type":"string","title":"Type","description":"adult or child","default":"adult"},"age":{"anyOf":[{"type":"integer","maximum":130.0,"minimum":0.0},{"type":"null"}],"title":"Age"}},"type":"object","title":"StaysGuest"},"StaysLocation":{"properties":{"radius":{"type":"number","exclusiveMinimum":0.0,"title":"Radius","description":"Search radius in km"},"geographic_coordinates":{"$ref":"#/components/schemas/StaysGeographicCoordinates"}},"type":"object","required":["radius","geographic_coordinates"],"title":"StaysLocation"},"StaysQuoteRequest":{"properties":{"rate_id":{"type":"string","title":"Rate Id","description":"A rate id (rat_...) from fetch_rates"}},"type":"object","required":["rate_id"],"title":"StaysQuoteRequest"},"StaysSearchRequest":{"properties":{"rooms":{"type":"integer","minimum":1.0,"title":"Rooms"},"check_in_date":{"type":"string","format":"date","title":"Check In Date"},"check_out_date":{"type":"string","format":"date","title":"Check Out Date"},"guests":{"items":{"$ref":"#/components/schemas/StaysGuest"},"type":"array","minItems":1,"title":"Guests"},"location":{"$ref":"#/components/schemas/StaysLocation"}},"type":"object","required":["rooms","check_in_date","check_out_date","guests","location"],"title":"StaysSearchRequest"},"TicketPublic":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"document_type":{"type":"string","title":"Document Type"},"ticket_number":{"type":"string","title":"Ticket Number"},"issued_at":{"type":"string","format":"date-time","title":"Issued At"}},"type":"object","required":["id","document_type","ticket_number","issued_at"],"title":"TicketPublic"},"Token":{"properties":{"access_token":{"type":"string","title":"Access Token"},"token_type":{"type":"string","title":"Token Type"}},"type":"object","required":["access_token","token_type"],"title":"Token"},"UnreadCountResponse":{"properties":{"unread_count":{"type":"integer","title":"Unread Count"}},"type":"object","required":["unread_count"],"title":"UnreadCountResponse"},"UserCreate":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","title":"Password"}},"type":"object","required":["email","password"],"title":"UserCreate"},"UserRead":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"email":{"type":"string","format":"email","title":"Email"},"is_staff":{"type":"boolean","title":"Is Staff"},"is_superuser":{"type":"boolean","title":"Is Superuser"}},"type":"object","required":["id","email","is_staff","is_superuser"],"title":"UserRead"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"backend__models__payments__PaymentStatus":{"type":"string","enum":["pending","completed","failed","booking_failed"],"title":"PaymentStatus"},"backend__schemas__duffel_orders__PaymentStatus":{"properties":{"awaiting_payment":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Awaiting Payment"},"payment_required_by":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Payment Required By"},"price_guarantee_expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Price Guarantee Expires At"}},"type":"object","title":"PaymentStatus"}},"securitySchemes":{"OAuth2PasswordBearer":{"type":"oauth2","flows":{"password":{"scopes":{},"tokenUrl":"token"}}}}}}