From e00740a8c9c68924012a37e55b15563ce415653a Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Fri, 27 Feb 2026 16:07:46 +0000 Subject: [PATCH] fix edit requests --- backend/go.mod | 1 + backend/go.sum | 2 ++ backend/handlers/bookings/manage.go | 54 ++++++++++++++++++----------- init-scripts/init-script.sql | 23 ++---------- 4 files changed, 38 insertions(+), 42 deletions(-) diff --git a/backend/go.mod b/backend/go.mod index a6e7091..1a684cb 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -33,6 +33,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/kovidgoyal/go-parallel v1.1.1 // indirect github.com/kovidgoyal/go-shm v1.0.0 // indirect + github.com/lib/pq v1.11.2 // indirect github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect golang.org/x/image v0.36.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index b101c4c..972cbd4 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -75,6 +75,8 @@ github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVf github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= +github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/nyaruka/phonenumbers v1.6.10 h1:kGTxTzd320dUamRB/MPeZSIwKNLn4vHlysOt5Cp8uoU= github.com/nyaruka/phonenumbers v1.6.10/go.mod h1:IUu45lj2bSeYXQuxDyyuzOrdV10tyRa1YSsfH8EKN5c= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/backend/handlers/bookings/manage.go b/backend/handlers/bookings/manage.go index fb3590b..83c4056 100644 --- a/backend/handlers/bookings/manage.go +++ b/backend/handlers/bookings/manage.go @@ -15,6 +15,7 @@ import ( "time" "github.com/go-chi/chi/v5" + "github.com/lib/pq" ) // UserCancelBookingHandler allows an authenticated user to cancel a booking they own. @@ -726,8 +727,17 @@ func DeleteEditRequestHandler(w http.ResponseWriter, r *http.Request) { return } + // Use transaction to delete edit request and associated admin notification + tx, err := db.DB.Begin(r.Context()) + if err != nil { + log.Printf("Failed to start transaction: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + defer tx.Rollback(r.Context()) + // Delete the edit request for this booking - res, err := db.DB.Exec(r.Context(), ` + res, err := tx.Exec(r.Context(), ` DELETE FROM booking_edit_requests WHERE booking_id = $1 AND requested_by = $2 `, bookingID, userID) @@ -743,6 +753,23 @@ func DeleteEditRequestHandler(w http.ResponseWriter, r *http.Request) { return } + // Delete the admin notification for this edit request + _, err = tx.Exec(r.Context(), ` + DELETE FROM admin_notifications + WHERE booking_id = $1 AND reason = 'edit_request' AND user_id = $2 + `, bookingID, userID) + if err != nil { + log.Printf("Failed to delete 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 delete edit request: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) } @@ -951,14 +978,14 @@ func AdminListEditRequestsHandler(w http.ResponseWriter, r *http.Request) { var origStartTime time.Time var bookingStatus string var userName string - var newServicesJSON []byte + var newServices []string err := rows.Scan( &req.ID, &req.BookingID, &req.RequestedBy, &req.NewStartTime, - &newServicesJSON, + pq.Array(&newServices), &req.Notes, &req.HasOverrides, &req.UpdatedAt, @@ -971,12 +998,7 @@ func AdminListEditRequestsHandler(w http.ResponseWriter, r *http.Request) { continue } - // Unmarshal JSON services - if len(newServicesJSON) > 0 { - if err := json.Unmarshal(newServicesJSON, &req.NewServices); err != nil { - log.Printf("Failed to unmarshal new_services: %v", err) - } - } + req.NewServices = newServices req.Booking = &Booking{ ID: req.BookingID, @@ -1021,14 +1043,14 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) { // Get the edit request var bookingID string var newStartTime *time.Time - var newServicesJSON []byte + var newServices []string var notes *string var hasOverrides bool err = tx.QueryRow(r.Context(), ` SELECT booking_id, new_start_time, new_services, notes, has_overrides FROM booking_edit_requests WHERE id = $1 - `, requestID).Scan(&bookingID, &newStartTime, &newServicesJSON, ¬es, &hasOverrides) + `, requestID).Scan(&bookingID, &newStartTime, pq.Array(&newServices), ¬es, &hasOverrides) if err != nil { if errors.Is(err, sql.ErrNoRows) { http.Error(w, "Edit request not found", http.StatusNotFound) @@ -1039,16 +1061,6 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) { return } - // Parse new_services - var newServices []string - if len(newServicesJSON) > 0 { - if err := json.Unmarshal(newServicesJSON, &newServices); err != nil { - log.Printf("Failed to unmarshal new_services: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - } - // If new_services provided and has_overrides is true, block with error if len(newServices) > 0 && hasOverrides { http.Error(w, "Cannot change services on a booking that has overrides. Please update services manually.", http.StatusForbidden) diff --git a/init-scripts/init-script.sql b/init-scripts/init-script.sql index 686946f..d33c79a 100644 --- a/init-scripts/init-script.sql +++ b/init-scripts/init-script.sql @@ -241,37 +241,18 @@ CREATE TABLE booking_edit_requests ( booking_id CHAR(12) NOT NULL REFERENCES bookings(id) ON DELETE CASCADE, requested_by CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE, new_start_time TIMESTAMPTZ, - new_services JSONB DEFAULT '[]'::jsonb, -- Array of service IDs to replace booking_services + new_services CHAR(12)[] DEFAULT '{}', -- Array of service IDs to replace booking_services notes TEXT, has_overrides BOOLEAN NOT NULL DEFAULT FALSE, -- If TRUE, cannot change services, use existing overrides for duration updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_at_least_one_field CHECK ( new_start_time IS NOT NULL OR - new_services IS NOT NULL OR + array_length(new_services, 1) IS NOT NULL OR notes IS NOT NULL ) ); CREATE INDEX idx_booking_edit_requests_booking ON booking_edit_requests(booking_id); - id CHAR(12) PRIMARY KEY DEFAULT generate_booking_id(), - booking_id CHAR(12) NOT NULL REFERENCES bookings(id) ON DELETE CASCADE, - requested_by CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE, - new_start_time TIMESTAMPTZ, - new_services JSONB DEFAULT '[]'::jsonb, - new_notes TEXT, - status VARCHAR(20) NOT NULL DEFAULT 'pending', - admin_notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - CONSTRAINT chk_at_least_one_field CHECK ( - new_start_time IS NOT NULL OR - new_services IS NOT NULL OR - new_notes IS NOT NULL - ) -); - -CREATE INDEX idx_booking_edit_requests_booking ON booking_edit_requests(booking_id); -CREATE INDEX idx_booking_edit_requests_status ON booking_edit_requests(status); CREATE TABLE user_referrals ( referrer_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,