diff --git a/backend/handlers/bookings/manage.go b/backend/handlers/bookings/manage.go index 7a039e9..d9f9c14 100644 --- a/backend/handlers/bookings/manage.go +++ b/backend/handlers/bookings/manage.go @@ -23,107 +23,6 @@ import ( "github.com/go-chi/chi/v5" ) -// UserCancelBookingHandler allows an authenticated user to cancel a booking they own. -// The update is performed in a transaction with notification handling. -func UserCancelBookingHandler(w http.ResponseWriter, r *http.Request) { - bookingID := chi.URLParam(r, "id") - if bookingID == "" || !validators.IsValidID(bookingID) { - http.Error(w, "Booking not found", http.StatusNotFound) - return - } - - userID, ok := r.Context().Value(mw.UserIDKey).(string) - if !ok || userID == "" { - http.Error(w, "Authentication required", http.StatusUnauthorized) - return - } - - tx, err := db.Conn.Begin(r.Context()) - if err != nil { - log.Printf("Failed to start transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - defer func() { - if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { - slog.Error("failed to rollback transaction", "err", err) - } -}() - - // Get current status before updating - var originalStatus string - err = tx.QueryRow(r.Context(), "SELECT status FROM bookings WHERE id = $1 AND user_id = $2", bookingID, userID).Scan(&originalStatus) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - http.Error(w, "Booking not cancellable", http.StatusNotFound) - return - } - log.Printf("Failed to get booking status %s: %v", bookingID, err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - res, err := tx.Exec(r.Context(), ` - UPDATE bookings - SET status = 'client_cancelled', updated_at = $1 - WHERE id = $2 AND user_id = $3 AND status IN ('pending', 'confirmed', 'in_progress') - `, clock.Now(), bookingID, userID) - if err != nil { - log.Printf("Failed to cancel booking %s: %v", bookingID, err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - rowsAffected := res.RowsAffected() - if rowsAffected == 0 { - http.Error(w, "Booking not cancellable", http.StatusNotFound) - return - } - - // Acknowledge pending notification if exists - if err := notifications.AcknowledgePendingBookingNotification(tx, r.Context(), bookingID); err != nil { - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - if _, err := tx.Exec(r.Context(), ` - DELETE FROM booking_edit_requests WHERE booking_id = $1 - `, bookingID); err != nil { - log.Printf("ALERT: failed to delete edit requests: %v", err) - } - if _, err := tx.Exec(r.Context(), ` - DELETE FROM time_blockers WHERE description = $1 - `, fmt.Sprintf("RESERVATION:edit_request:%s", bookingID)); err != nil { - log.Printf("ALERT: failed to delete time_blocker: %v", err) - } - if _, err := tx.Exec(r.Context(), ` - DELETE FROM admin_notifications WHERE booking_id = $1 AND reason = 'edit_requested' - `, bookingID); err != nil { - log.Printf("ALERT: failed to delete edit_requested notification: %v", err) - } - - // Only notify on cancellation if booking was not pending (e.g. confirmed, in_progress) - if originalStatus != "pending" { - notificationQuery := ` - INSERT INTO admin_notifications (reason, booking_id, user_id) - VALUES ($1, $2, $3) - ` - _, err = tx.Exec(r.Context(), notificationQuery, "cancelled_booking", bookingID, userID) - if err != nil { - log.Printf("Failed to create admin notification for booking %s: %v", bookingID, err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - } - - if err := tx.Commit(r.Context()); err != nil { - log.Printf("Failed to commit user cancel: %v, %v", bookingID, err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusNoContent) -} - type AdminCancelBookingRequest struct { ForgiveFees *bool `json:"forgive_fees,omitempty"` ForgiveNoShow *bool `json:"forgive_noshow,omitempty"` @@ -305,63 +204,6 @@ func AdminCancelBookingHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } -// AdminGetInProgressBookingHandler returns the booking that is currently in progress. -// It joins the bookings table with users to populate the UserSummary in the returned Booking. -func AdminGetInProgressBookingHandler(w http.ResponseWriter, r *http.Request) { - var b Booking - var userID, fullName string - - err := db.Conn.QueryRow(r.Context(), ` - SELECT - b.id, - b.start_time, - b.status, - b.notes, - b.created_at, - b.updated_at, - b.created_by, - u.id, - u.fn - FROM bookings b - LEFT JOIN users u ON b.user_id = u.id - WHERE b.status = 'in_progress' - ORDER BY b.start_time - LIMIT 1 - `).Scan( - &b.ID, - &b.StartTime, - &b.Status, - &b.Notes, - &b.CreatedAt, - &b.UpdatedAt, - &b.CreatedBy, - &userID, - &fullName, - ) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - http.Error(w, "No in-progress booking found", http.StatusNotFound) - return - } - log.Printf("Failed to fetch in-progress booking: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - // Populate the UserSummary field - b.User = &UserSummary{ - ID: userID, - FullName: fullName, - FirstName: "", // not available here - LastName: "", // not available here - } - - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(b); err != nil { - log.Printf("Failed to encode booking response: %v", err) - } -} - type AdminCreateBookingForUserRequest struct { UserID string `json:"user_id" validate:"required"` StartTime time.Time `json:"start_time" validate:"required"` diff --git a/backend/handlers/portfolio/images.go b/backend/handlers/portfolio/images.go index 8fcd588..a429533 100644 --- a/backend/handlers/portfolio/images.go +++ b/backend/handlers/portfolio/images.go @@ -24,7 +24,6 @@ import ( "time" "github.com/go-chi/chi/v5" - "github.com/kovidgoyal/imaging" ) const MaxInputLength = 256 @@ -44,26 +43,6 @@ func mimeTypeForField(fieldName string) string { } } -// processImage strips metadata and auto-orients the image - //nolint:unused -func processImage(data []byte, quality int) ([]byte, error) { - // Decode the image - this automatically applies EXIF orientation - // and strips all metadata (EXIF, GPS, etc.) - img, err := imaging.Decode(bytes.NewReader(data), imaging.AutoOrientation(true)) - if err != nil { - return nil, fmt.Errorf("failed to decode image: %w", err) - } - - // Encode to JPEG without any metadata - var buf bytes.Buffer - err = imaging.Encode(&buf, img, imaging.JPEG, imaging.JPEGQuality(quality)) - if err != nil { - return nil, fmt.Errorf("failed to encode image: %w", err) - } - - return buf.Bytes(), nil -} - // validateInputLength returns an error if input exceeds max length func validateInputLength(input string) error {