package handlers import ( "encoding/json" "net/http" "net/mail" "strings" "github.com/google/uuid" "github.com/travelswitch/travelhub/backend/internal/models" ) // ListBookings handles GET /api/bookings, optionally filtered by status. func (h *Handler) ListBookings(w http.ResponseWriter, r *http.Request) { id, ok := h.identity(w, r) if !ok { return } status := strings.TrimSpace(r.URL.Query().Get("status")) if status != "" && status != "confirmed" && status != "cancelled" { writeError(w, http.StatusBadRequest, "status must be confirmed or cancelled") return } limit := intParam(r, "limit", 25, 1, 100) offset := intParam(r, "offset", 0, 0, 100000) bookings, total, err := h.Repo.ListBookings(r.Context(), id.Tenant.ID, status, limit, offset) if err != nil { h.fail(w, err, "list bookings") return } writeJSON(w, http.StatusOK, page[models.Booking]{ Items: bookings, Total: total, Limit: limit, Offset: offset, }) } // GetBooking handles GET /api/bookings/{id}. func (h *Handler) GetBooking(w http.ResponseWriter, r *http.Request) { id, ok := h.identity(w, r) if !ok { return } bookingID, err := uuid.Parse(r.PathValue("id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid booking id") return } booking, err := h.Repo.BookingByID(r.Context(), id.Tenant.ID, bookingID) if err != nil { h.fail(w, err, "get booking") return } writeJSON(w, http.StatusOK, booking) } // CreateBooking handles POST /api/bookings. func (h *Handler) CreateBooking(w http.ResponseWriter, r *http.Request) { id, ok := h.identity(w, r) if !ok { return } var req models.CreateBookingRequest dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)) dec.DisallowUnknownFields() if err := dec.Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON body: "+err.Error()) return } if msg := validateBooking(&req); msg != "" { writeError(w, http.StatusBadRequest, msg) return } booking, err := h.Repo.CreateBooking(r.Context(), id.Tenant.ID, id.User.ID, req) if err != nil { h.fail(w, err, "create booking") return } writeJSON(w, http.StatusCreated, booking) } // CancelBooking handles POST /api/bookings/{id}/cancel and returns the seats // to inventory. func (h *Handler) CancelBooking(w http.ResponseWriter, r *http.Request) { id, ok := h.identity(w, r) if !ok { return } bookingID, err := uuid.Parse(r.PathValue("id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid booking id") return } booking, err := h.Repo.CancelBooking(r.Context(), id.Tenant.ID, bookingID) if err != nil { h.fail(w, err, "cancel booking") return } writeJSON(w, http.StatusOK, booking) } // validateBooking returns an empty string when the payload is usable, or a // message naming the first problem. Normalisation happens here too, so the // repository only ever sees trimmed values. func validateBooking(req *models.CreateBookingRequest) string { if req.FlightID == uuid.Nil { return "flightId is required" } req.ContactEmail = strings.TrimSpace(req.ContactEmail) if req.ContactEmail == "" { return "contactEmail is required" } if _, err := mail.ParseAddress(req.ContactEmail); err != nil { return "contactEmail is not a valid email address" } req.ContactPhone = strings.TrimSpace(req.ContactPhone) if len(req.Passengers) == 0 { return "at least one passenger is required" } if len(req.Passengers) > 9 { return "a single booking may carry at most 9 passengers" } for i := range req.Passengers { p := &req.Passengers[i] p.FirstName = strings.TrimSpace(p.FirstName) p.LastName = strings.TrimSpace(p.LastName) p.PassportNumber = strings.TrimSpace(p.PassportNumber) p.DateOfBirth = strings.TrimSpace(p.DateOfBirth) if p.FirstName == "" || p.LastName == "" { return "every passenger needs a first and last name" } } return "" } // Dashboard handles GET /api/dashboard. func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { id, ok := h.identity(w, r) if !ok { return } stats, err := h.Repo.DashboardStats(r.Context(), id.Tenant.ID) if err != nil { h.fail(w, err, "dashboard stats") return } writeJSON(w, http.StatusOK, stats) }