From c7c10bc177cccae8095a7b09a5278a8c031825ce Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Sat, 7 Mar 2026 21:00:16 +0000 Subject: [PATCH] Fixed user cancelation logic --- backend/handlers/admin/bookings_test.go | 279 +++++++++ backend/main.go | 2 +- local-dev-2.sh | 783 +++++++++++++++++------- 3 files changed, 835 insertions(+), 229 deletions(-) diff --git a/backend/handlers/admin/bookings_test.go b/backend/handlers/admin/bookings_test.go index 95bb4cb..9a0d488 100644 --- a/backend/handlers/admin/bookings_test.go +++ b/backend/handlers/admin/bookings_test.go @@ -816,6 +816,285 @@ func TestAdminBookings_Cancel_NotFound(t *testing.T) { } } +// TestAdminBookings_Cancel_PendingStatus verifies that admin cancellations of pending bookings +// do NOT create admin notifications (pending cancellations don't require staff attention). +func TestAdminBookings_Cancel_PendingStatus(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + adminID, err := fixtures.CreateTestAdminUser(db.DB) + if err != nil { + t.Fatalf("failed to create admin user: %v", err) + } + defer fixtures.DeleteUser(db.DB, adminID) + + userID, err := fixtures.CreateTestUser(db.DB) + if err != nil { + t.Fatalf("failed to create test user: %v", err) + } + defer fixtures.DeleteUser(db.DB, userID) + + serviceID, err := fixtures.CreateTestService(db.DB) + if err != nil { + t.Fatalf("failed to create test service: %v", err) + } + defer fixtures.DeleteService(db.DB, serviceID) + + ctx := context.Background() + bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID) + if err != nil { + t.Fatalf("failed to create test booking: %v", err) + } + defer fixtures.DeleteBooking(db.DB, bookingID) + + // Booking stays in 'pending' status (no confirmation) + + handler := http.HandlerFunc(bookings.AdminCancelBookingHandler) + w := makeAdminRequest(handler, "POST", "/api/admin/bookings/"+bookingID+"/cancel", nil) + + if w.Code != http.StatusNoContent { + t.Errorf("expected status 204, got %d. body: %s", w.Code, w.Body.String()) + } + + // Verify status changed to we_cancelled + var dbStatus string + err = db.DB.QueryRow(ctx, + "SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&dbStatus) + if err != nil { + t.Fatalf("failed to query booking: %v", err) + } + if dbStatus != "we_cancelled" { + t.Errorf("expected status 'we_cancelled' in DB, got %s", dbStatus) + } + + // Verify NO admin notification was created for pending cancellations + var notifCount int + err = db.DB.QueryRow(ctx, + `SELECT COUNT(*) FROM admin_notifications WHERE booking_id = $1`, + bookingID).Scan(¬ifCount) + if err != nil { + t.Fatalf("failed to query notifications: %v", err) + } + if notifCount != 0 { + t.Errorf("expected 0 admin notifications for pending cancel, got %d", notifCount) + } +} + +// TestAdminBookings_Cancel_ConfirmedCreatesNotification verifies that cancelling a confirmed +// booking creates an admin notification for staff awareness. +func TestAdminBookings_Cancel_ConfirmedCreatesNotification(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + adminID, err := fixtures.CreateTestAdminUser(db.DB) + if err != nil { + t.Fatalf("failed to create admin user: %v", err) + } + defer fixtures.DeleteUser(db.DB, adminID) + + userID, err := fixtures.CreateTestUser(db.DB) + if err != nil { + t.Fatalf("failed to create test user: %v", err) + } + defer fixtures.DeleteUser(db.DB, userID) + + serviceID, err := fixtures.CreateTestService(db.DB) + if err != nil { + t.Fatalf("failed to create test service: %v", err) + } + defer fixtures.DeleteService(db.DB, serviceID) + + ctx := context.Background() + bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID) + if err != nil { + t.Fatalf("failed to create test booking: %v", err) + } + defer fixtures.DeleteBooking(db.DB, bookingID) + + // Confirm the booking + _, err = db.DB.Exec(ctx, "UPDATE bookings SET status = 'confirmed' WHERE id = $1", bookingID) + if err != nil { + t.Fatalf("failed to confirm booking: %v", err) + } + + handler := http.HandlerFunc(bookings.AdminCancelBookingHandler) + w := makeAdminRequest(handler, "POST", "/api/admin/bookings/"+bookingID+"/cancel", nil) + + if w.Code != http.StatusNoContent { + t.Errorf("expected status 204, got %d. body: %s", w.Code, w.Body.String()) + } + + // Verify status changed to we_cancelled + var dbStatus string + err = db.DB.QueryRow(ctx, + "SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&dbStatus) + if err != nil { + t.Fatalf("failed to query booking: %v", err) + } + if dbStatus != "we_cancelled" { + t.Errorf("expected status 'we_cancelled' in DB, got %s", dbStatus) + } + + // Verify admin notification WAS created for confirmed->cancelled + var notifCount int + err = db.DB.QueryRow(ctx, + `SELECT COUNT(*) FROM admin_notifications WHERE booking_id = $1 AND reason = 'cancelled_booking'`, + bookingID).Scan(¬ifCount) + if err != nil { + t.Fatalf("failed to query notifications: %v", err) + } + if notifCount != 1 { + t.Errorf("expected 1 admin notification for confirmed cancel, got %d", notifCount) + } +} + +// TestAdminBookings_Cancel_InProgressStatus verifies cancellation of in-progress bookings. +func TestAdminBookings_Cancel_InProgressStatus(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + adminID, err := fixtures.CreateTestAdminUser(db.DB) + if err != nil { + t.Fatalf("failed to create admin user: %v", err) + } + defer fixtures.DeleteUser(db.DB, adminID) + + userID, err := fixtures.CreateTestUser(db.DB) + if err != nil { + t.Fatalf("failed to create test user: %v", err) + } + defer fixtures.DeleteUser(db.DB, userID) + + serviceID, err := fixtures.CreateTestService(db.DB) + if err != nil { + t.Fatalf("failed to create test service: %v", err) + } + defer fixtures.DeleteService(db.DB, serviceID) + + ctx := context.Background() + bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID) + if err != nil { + t.Fatalf("failed to create test booking: %v", err) + } + defer fixtures.DeleteBooking(db.DB, bookingID) + + // Set booking to in-progress status + _, err = db.DB.Exec(ctx, "UPDATE bookings SET status = 'in_progress' WHERE id = $1", bookingID) + if err != nil { + t.Fatalf("failed to set in_progress status: %v", err) + } + + handler := http.HandlerFunc(bookings.AdminCancelBookingHandler) + w := makeAdminRequest(handler, "POST", "/api/admin/bookings/"+bookingID+"/cancel", nil) + + if w.Code != http.StatusNoContent { + t.Errorf("expected status 204, got %d. body: %s", w.Code, w.Body.String()) + } + + // Verify status changed to we_cancelled + var dbStatus string + err = db.DB.QueryRow(ctx, + "SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&dbStatus) + if err != nil { + t.Fatalf("failed to query booking: %v", err) + } + if dbStatus != "we_cancelled" { + t.Errorf("expected status 'we_cancelled' in DB, got %s", dbStatus) + } +} + +// TestAdminBookings_Cancel_AlreadyCancelledRejectsCancellation verifies that attempting to +// cancel an already-cancelled booking returns 404 Not Found (idempotency guard). +func TestAdminBookings_Cancel_AlreadyCancelledRejectsCancellation(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + adminID, err := fixtures.CreateTestAdminUser(db.DB) + if err != nil { + t.Fatalf("failed to create admin user: %v", err) + } + defer fixtures.DeleteUser(db.DB, adminID) + + userID, err := fixtures.CreateTestUser(db.DB) + if err != nil { + t.Fatalf("failed to create test user: %v", err) + } + defer fixtures.DeleteUser(db.DB, userID) + + serviceID, err := fixtures.CreateTestService(db.DB) + if err != nil { + t.Fatalf("failed to create test service: %v", err) + } + defer fixtures.DeleteService(db.DB, serviceID) + + ctx := context.Background() + bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID) + if err != nil { + t.Fatalf("failed to create test booking: %v", err) + } + defer fixtures.DeleteBooking(db.DB, bookingID) + + // Set to already cancelled + _, err = db.DB.Exec(ctx, "UPDATE bookings SET status = 'we_cancelled' WHERE id = $1", bookingID) + if err != nil { + t.Fatalf("failed to set we_cancelled status: %v", err) + } + + // Try to cancel again + handler := http.HandlerFunc(bookings.AdminCancelBookingHandler) + w := makeAdminRequest(handler, "POST", "/api/admin/bookings/"+bookingID+"/cancel", nil) + + if w.Code != http.StatusNotFound { + t.Errorf("expected status 404 for already-cancelled booking, got %d", w.Code) + } +} + +// TestAdminBookings_Cancel_CompletedRejectsCancellation verifies that attempting to cancel +// a completed booking returns 404 Not Found (cannot cancel finished appointments). +func TestAdminBookings_Cancel_CompletedRejectsCancellation(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + adminID, err := fixtures.CreateTestAdminUser(db.DB) + if err != nil { + t.Fatalf("failed to create admin user: %v", err) + } + defer fixtures.DeleteUser(db.DB, adminID) + + userID, err := fixtures.CreateTestUser(db.DB) + if err != nil { + t.Fatalf("failed to create test user: %v", err) + } + defer fixtures.DeleteUser(db.DB, userID) + + serviceID, err := fixtures.CreateTestService(db.DB) + if err != nil { + t.Fatalf("failed to create test service: %v", err) + } + defer fixtures.DeleteService(db.DB, serviceID) + + ctx := context.Background() + bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID) + if err != nil { + t.Fatalf("failed to create test booking: %v", err) + } + defer fixtures.DeleteBooking(db.DB, bookingID) + + // Set to completed + _, err = db.DB.Exec(ctx, "UPDATE bookings SET status = 'completed' WHERE id = $1", bookingID) + if err != nil { + t.Fatalf("failed to set completed status: %v", err) + } + + // Try to cancel + handler := http.HandlerFunc(bookings.AdminCancelBookingHandler) + w := makeAdminRequest(handler, "POST", "/api/admin/bookings/"+bookingID+"/cancel", nil) + + if w.Code != http.StatusNotFound { + t.Errorf("expected status 404 for completed booking, got %d", w.Code) + } +} + // ============================================================================= // Non-Admin Tests // ============================================================================= diff --git a/backend/main.go b/backend/main.go index 2e0117f..bf9173c 100644 --- a/backend/main.go +++ b/backend/main.go @@ -185,7 +185,7 @@ func main() { r.Get("/{id}/overlapping", bookings.GetOverlappingBookingsHandler) r.Put("/{id}/progress", bookings.ProgressBookingHandler) r.Post("/{id}/confirm", bookings.ConfirmBookingHandler) - r.Post("/{id}/cancel", bookings.CancelBookingHandler) + r.Post("/{id}/cancel", bookings.AdminCancelBookingHandler) // Edit request endpoints r.Get("/{id}/edit-requests", bookings.AdminListEditRequestsHandler) r.Post("/{id}/edit-requests/{request_id}/approve", bookings.AdminApproveEditRequestHandler) diff --git a/local-dev-2.sh b/local-dev-2.sh index 068f8c2..9d65f0e 100755 --- a/local-dev-2.sh +++ b/local-dev-2.sh @@ -8,7 +8,6 @@ setopt PIPE_FAIL setopt ERR_EXIT # --- UI Helpers --- -# Color codes for output C_RESET=$'\033[0m' C_GREEN=$'\033[32m' C_RED=$'\033[31m' @@ -137,12 +136,10 @@ tmux set-environment -t $SESSION_NAME AWS_REGION "$AWS_REGION" tmux set-environment -t $SESSION_NAME VITE_BACKEND_URL "$VITE_BACKEND_URL" # Pane 0: Database -# Start with tables + row count query tmux send-keys -t $SESSION_NAME 'docker exec -it postgres psql -U myuser -d mydb -c "SELECT relname AS table_name, n_live_tup AS row_count FROM pg_stat_user_tables ORDER BY table_name;"' tmux select-pane -t $SESSION_NAME:0.0 -T "DB" # Pane 1: Backend (Split Horizontally) -# Source .env to ensure all env vars are available to Go tmux split-window -v -t $SESSION_NAME tmux send-keys -t $SESSION_NAME "set -a; source .env > /dev/null 2>&1; cd backend && go run -tags dev ./main.go" Enter tmux select-pane -t $SESSION_NAME:0.1 -T "Backend" @@ -174,6 +171,14 @@ USER_EMAIL="user@example.com" USER_PASS="password" BASE_URL="http://localhost:8080/api" +# Safe booking times โ€” mid-morning to early afternoon to avoid closing-hour conflicts. +# Adjust these if your business hours differ. +SLOT_A="10:00:00" +SLOT_B="11:30:00" +SLOT_C="13:00:00" +SLOT_D="14:30:00" +SLOT_E="12:15:00" # Used only for pending demo bookings โ€” avoids conflicts with A-D rotation + # --- Formatting --- C_RESET=$'\033[0m' C_GREEN=$'\033[32m' @@ -185,6 +190,7 @@ C_YELLOW=$'\033[33m' LAST_BOOKING_ID="" # --- Helper: API Request --- +# Returns the root-level "id" from the response body on success. api_post() { local url="$1" local data="$2" @@ -200,12 +206,13 @@ api_post() { local body=$(echo "$response" | sed '$d') if [[ "$http_code" =~ ^2 ]]; then - # FIX: - # 1. tr -d '\n': Ensure JSON is treated as a single line (handles pretty-printing). - # 2. sed 's/"user":{[^}]*}//': Remove the "user" object entirely. - # [^}]* matches everything up to the first closing brace, which is safe for UserSummary (flat object). - # 3. grep/cut: Extract the remaining root 'id' (which is now the Booking ID). - echo "$body" | tr -d '\n' | sed 's/"user":{[^}]*}//' | grep -o '"id":"[^"]*' | cut -d'"' -f4 | tr -d '\r\n' + # Strip nested "user" object so we reliably get the root "id" (UUID) + local extracted + extracted=$(echo "$body" | tr -d '\n' | sed 's/"user":{[^}]*}//' | grep -o '"id":"[^"]*' | cut -d'"' -f4 | tr -d '\r\n ') + # Only emit the value if it looks like a UUID (hex + hyphens, 8+ chars) + if [[ "$extracted" =~ ^[0-9a-f-]{8,}$ ]]; then + echo "$extracted" + fi return 0 else printf "${C_RED}โŒ Failed: %s (HTTP %s)${C_RESET}\n" "$desc" "$http_code" @@ -215,6 +222,16 @@ api_post() { fi } +# --- Helper: Login and return token --- +login() { + local email="$1" pass="$2" + curl -s -X POST -H 'Content-Type: application/json' \ + -d "{\"email\":\"$email\",\"password\":\"$pass\"}" \ + "$BASE_URL/login" \ + | tr -d '\r\n\t ' \ + | sed -n 's/.*"token":"\([^"]*\)".*/\1/p' +} + wait_for_backend() { echo -ne "โณ Waiting for backend..." for ((i=1; i<=60; i++)); do @@ -232,81 +249,128 @@ wait_for_backend() { # --- Main Execution --- wait_for_backend -# 1. Register Users +# =========================================================================== +# 1. REGISTER USERS +# =========================================================================== echo -e "\n${C_BLUE}๐Ÿ‘ค Registering Users...${C_RESET}" success=0 -total=18 +total=20 -# Admin user -if api_post "$BASE_URL/register" "{\"firstName\":\"Admin\",\"lastName\":\"User\",\"email\":\"$ADMIN_EMAIL\",\"password\":\"$ADMIN_PASS\",\"phone\":\"+447000000000\",\"dateOfBirth\":\"1985-01-01\",\"agreedToPolicy\":true}" "Register Admin" "" > /dev/null; then success=$((success+1)); fi +# Admin +if api_post "$BASE_URL/register" '{"firstName":"Admin","lastName":"User","email":"admin@example.com","password":"password","phone":"+447000000000","dateOfBirth":"1985-01-01","agreedToPolicy":true}' "Register Admin" "" > /dev/null; then success=$((success+1)); fi -# Original test user -if api_post "$BASE_URL/register" "{\"firstName\":\"Regular\",\"lastName\":\"User\",\"email\":\"$USER_EMAIL\",\"password\":\"$USER_PASS\",\"phone\":\"+447000000001\",\"dateOfBirth\":\"1990-05-15\",\"agreedToPolicy\":true}" "Register User" "" > /dev/null; then success=$((success+1)); fi +# Primary test user (no-deposit, easy to book with) +if api_post "$BASE_URL/register" '{"firstName":"Regular","lastName":"User","email":"user@example.com","password":"password","phone":"+447000000001","dateOfBirth":"1990-05-15","agreedToPolicy":true}' "Register User" "" > /dev/null; then success=$((success+1)); fi -# Additional test users (16 more) -if api_post "$BASE_URL/register" "{\"firstName\":\"Emma\",\"lastName\":\"Johnson\",\"email\":\"emma.johnson@example.com\",\"password\":\"password\",\"phone\":\"+447000000002\",\"dateOfBirth\":\"1988-03-22\",\"agreedToPolicy\":true}" "Register Emma Johnson" "" > /dev/null; then success=$((success+1)); fi -if api_post "$BASE_URL/register" "{\"firstName\":\"Oliver\",\"lastName\":\"Smith\",\"email\":\"oliver.smith@example.com\",\"password\":\"password\",\"phone\":\"+447000000003\",\"dateOfBirth\":\"1992-07-14\",\"agreedToPolicy\":true}" "Register Oliver Smith" "" > /dev/null; then success=$((success+1)); fi -if api_post "$BASE_URL/register" "{\"firstName\":\"Sophie\",\"lastName\":\"Williams\",\"email\":\"sophie.williams@example.com\",\"password\":\"password\",\"phone\":\"+447000000004\",\"dateOfBirth\":\"1995-11-08\",\"agreedToPolicy\":true}" "Register Sophie Williams" "" > /dev/null; then success=$((success+1)); fi -if api_post "$BASE_URL/register" "{\"firstName\":\"Harry\",\"lastName\":\"Brown\",\"email\":\"harry.brown@example.com\",\"password\":\"password\",\"phone\":\"+447000000005\",\"dateOfBirth\":\"1987-02-19\",\"agreedToPolicy\":true}" "Register Harry Brown" "" > /dev/null; then success=$((success+1)); fi -if api_post "$BASE_URL/register" "{\"firstName\":\"Amelia\",\"lastName\":\"Jones\",\"email\":\"amelia.jones@example.com\",\"password\":\"password\",\"phone\":\"+447000000006\",\"dateOfBirth\":\"1993-09-30\",\"agreedToPolicy\":true}" "Register Amelia Jones" "" > /dev/null; then success=$((success+1)); fi -if api_post "$BASE_URL/register" "{\"firstName\":\"Jack\",\"lastName\":\"Taylor\",\"email\":\"jack.taylor@example.com\",\"password\":\"password\",\"phone\":\"+447000000007\",\"dateOfBirth\":\"1991-05-12\",\"agreedToPolicy\":true}" "Register Jack Taylor" "" > /dev/null; then success=$((success+1)); fi -if api_post "$BASE_URL/register" "{\"firstName\":\"Isla\",\"lastName\":\"Davies\",\"email\":\"isla.davies@example.com\",\"password\":\"password\",\"phone\":\"+447000000008\",\"dateOfBirth\":\"1989-12-25\",\"agreedToPolicy\":true}" "Register Isla Davies" "" > /dev/null; then success=$((success+1)); fi -if api_post "$BASE_URL/register" "{\"firstName\":\"Thomas\",\"lastName\":\"Evans\",\"email\":\"thomas.evans@example.com\",\"password\":\"password\",\"phone\":\"+447000000009\",\"dateOfBirth\":\"1994-04-17\",\"agreedToPolicy\":true}" "Register Thomas Evans" "" > /dev/null; then success=$((success+1)); fi -if api_post "$BASE_URL/register" "{\"firstName\":\"Lily\",\"lastName\":\"Wilson\",\"email\":\"lily.wilson@example.com\",\"password\":\"password\",\"phone\":\"+447000000010\",\"dateOfBirth\":\"1996-08-03\",\"agreedToPolicy\":true}" "Register Lily Wilson" "" > /dev/null; then success=$((success+1)); fi -if api_post "$BASE_URL/register" "{\"firstName\":\"George\",\"lastName\":\"Roberts\",\"email\":\"george.roberts@example.com\",\"password\":\"password\",\"phone\":\"+447000000011\",\"dateOfBirth\":\"1986-01-29\",\"agreedToPolicy\":true}" "Register George Roberts" "" > /dev/null; then success=$((success+1)); fi -if api_post "$BASE_URL/register" "{\"firstName\":\"Poppy\",\"lastName\":\"Thompson\",\"email\":\"poppy.thompson@example.com\",\"password\":\"password\",\"phone\":\"+447000000012\",\"dateOfBirth\":\"1997-06-21\",\"agreedToPolicy\":true}" "Register Poppy Thompson" "" > /dev/null; then success=$((success+1)); fi -if api_post "$BASE_URL/register" "{\"firstName\":\"Charlie\",\"lastName\":\"Wright\",\"email\":\"charlie.wright@example.com\",\"password\":\"password\",\"phone\":\"+447000000013\",\"dateOfBirth\":\"1990-10-11\",\"agreedToPolicy\":true}" "Register Charlie Wright" "" > /dev/null; then success=$((success+1)); fi -if api_post "$BASE_URL/register" "{\"firstName\":\"Ava\",\"lastName\":\"Walker\",\"email\":\"ava.walker@example.com\",\"password\":\"password\",\"phone\":\"+447000000014\",\"dateOfBirth\":\"1993-03-07\",\"agreedToPolicy\":true}" "Register Ava Walker" "" > /dev/null; then success=$((success+1)); fi -if api_post "$BASE_URL/register" "{\"firstName\":\"Noah\",\"lastName\":\"Robinson\",\"email\":\"noah.robinson@example.com\",\"password\":\"password\",\"phone\":\"+447000000015\",\"dateOfBirth\":\"1988-11-16\",\"agreedToPolicy\":true}" "Register Noah Robinson" "" > /dev/null; then success=$((success+1)); fi -if api_post "$BASE_URL/register" "{\"firstName\":\"Mia\",\"lastName\":\"White\",\"email\":\"mia.white@example.com\",\"password\":\"password\",\"phone\":\"+447000000016\",\"dateOfBirth\":\"1995-07-28\",\"agreedToPolicy\":true}" "Register Mia White" "" > /dev/null; then success=$((success+1)); fi -if api_post "$BASE_URL/register" "{\"firstName\":\"Oscar\",\"lastName\":\"Hughes\",\"email\":\"oscar.hughes@example.com\",\"password\":\"password\",\"phone\":\"+447000000017\",\"dateOfBirth\":\"1991-02-04\",\"agreedToPolicy\":true}" "Register Oscar Hughes" "" > /dev/null; then success=$((success+1)); fi +# Loyal regulars +if api_post "$BASE_URL/register" '{"firstName":"Emma","lastName":"Johnson","email":"emma.johnson@example.com","password":"password","phone":"+447000000002","dateOfBirth":"1988-03-22","agreedToPolicy":true}' "Register Emma Johnson" "" > /dev/null; then success=$((success+1)); fi +if api_post "$BASE_URL/register" '{"firstName":"Sophie","lastName":"Williams","email":"sophie.williams@example.com","password":"password","phone":"+447000000003","dateOfBirth":"1995-11-08","agreedToPolicy":true}' "Register Sophie Williams" "" > /dev/null; then success=$((success+1)); fi +if api_post "$BASE_URL/register" '{"firstName":"Amelia","lastName":"Jones","email":"amelia.jones@example.com","password":"password","phone":"+447000000004","dateOfBirth":"1993-09-30","agreedToPolicy":true}' "Register Amelia Jones" "" > /dev/null; then success=$((success+1)); fi +if api_post "$BASE_URL/register" '{"firstName":"Isla","lastName":"Davies","email":"isla.davies@example.com","password":"password","phone":"+447000000005","dateOfBirth":"1989-12-25","agreedToPolicy":true}' "Register Isla Davies" "" > /dev/null; then success=$((success+1)); fi +if api_post "$BASE_URL/register" '{"firstName":"Lily","lastName":"Wilson","email":"lily.wilson@example.com","password":"password","phone":"+447000000006","dateOfBirth":"1996-08-03","agreedToPolicy":true}' "Register Lily Wilson" "" > /dev/null; then success=$((success+1)); fi +if api_post "$BASE_URL/register" '{"firstName":"Ava","lastName":"Walker","email":"ava.walker@example.com","password":"password","phone":"+447000000007","dateOfBirth":"1993-03-07","agreedToPolicy":true}' "Register Ava Walker" "" > /dev/null; then success=$((success+1)); fi -# Promote Admin -docker exec postgres psql -U myuser -d mydb -c "UPDATE users SET account_role = 'admin' WHERE email = '$ADMIN_EMAIL'" > /dev/null 2>&1 +# Occasional clients +if api_post "$BASE_URL/register" '{"firstName":"Oliver","lastName":"Smith","email":"oliver.smith@example.com","password":"password","phone":"+447000000008","dateOfBirth":"1992-07-14","agreedToPolicy":true}' "Register Oliver Smith" "" > /dev/null; then success=$((success+1)); fi +if api_post "$BASE_URL/register" '{"firstName":"Harry","lastName":"Brown","email":"harry.brown@example.com","password":"password","phone":"+447000000009","dateOfBirth":"1987-02-19","agreedToPolicy":true}' "Register Harry Brown" "" > /dev/null; then success=$((success+1)); fi +if api_post "$BASE_URL/register" '{"firstName":"George","lastName":"Roberts","email":"george.roberts@example.com","password":"password","phone":"+447000000010","dateOfBirth":"1986-01-29","agreedToPolicy":true}' "Register George Roberts" "" > /dev/null; then success=$((success+1)); fi +if api_post "$BASE_URL/register" '{"firstName":"Charlie","lastName":"Wright","email":"charlie.wright@example.com","password":"password","phone":"+447000000011","dateOfBirth":"1990-10-11","agreedToPolicy":true}' "Register Charlie Wright" "" > /dev/null; then success=$((success+1)); fi +if api_post "$BASE_URL/register" '{"firstName":"Noah","lastName":"Robinson","email":"noah.robinson@example.com","password":"password","phone":"+447000000012","dateOfBirth":"1988-11-16","agreedToPolicy":true}' "Register Noah Robinson" "" > /dev/null; then success=$((success+1)); fi +if api_post "$BASE_URL/register" '{"firstName":"Thomas","lastName":"Evans","email":"thomas.evans@example.com","password":"password","phone":"+447000000013","dateOfBirth":"1994-04-17","agreedToPolicy":true}' "Register Thomas Evans" "" > /dev/null; then success=$((success+1)); fi +if api_post "$BASE_URL/register" '{"firstName":"Oscar","lastName":"Hughes","email":"oscar.hughes@example.com","password":"password","phone":"+447000000014","dateOfBirth":"1991-02-04","agreedToPolicy":true}' "Register Oscar Hughes" "" > /dev/null; then success=$((success+1)); fi + +# Deposit-required users (demonstrate deposit snapshot behaviour) +if api_post "$BASE_URL/register" '{"firstName":"Poppy","lastName":"Thompson","email":"poppy.thompson@example.com","password":"password","phone":"+447000000015","dateOfBirth":"1997-06-21","agreedToPolicy":true}' "Register Poppy Thompson" "" > /dev/null; then success=$((success+1)); fi +if api_post "$BASE_URL/register" '{"firstName":"Mia","lastName":"White","email":"mia.white@example.com","password":"password","phone":"+447000000016","dateOfBirth":"1995-07-28","agreedToPolicy":true}' "Register Mia White" "" > /dev/null; then success=$((success+1)); fi + +# Young client (for age-restricted service testing) +if api_post "$BASE_URL/register" '{"firstName":"Chloe","lastName":"Park","email":"chloe.park@example.com","password":"password","phone":"+447000000017","dateOfBirth":"2009-04-12","agreedToPolicy":true}' "Register Chloe Park" "" > /dev/null; then success=$((success+1)); fi + +# Patch-test-complete user (will have gel allergy test recorded) +if api_post "$BASE_URL/register" '{"firstName":"Grace","lastName":"Fletcher","email":"grace.fletcher@example.com","password":"password","phone":"+447000000018","dateOfBirth":"1991-08-30","agreedToPolicy":true}' "Register Grace Fletcher" "" > /dev/null; then success=$((success+1)); fi + +# No-show history user (to demonstrate 48h booking restriction) +if api_post "$BASE_URL/register" '{"firstName":"Liam","lastName":"Caldwell","email":"liam.caldwell@example.com","password":"password","phone":"+447000000019","dateOfBirth":"1989-03-14","agreedToPolicy":true}' "Register Liam Caldwell" "" > /dev/null; then success=$((success+1)); fi + +# --- Promote admin and set deposit flags via DB --- +docker exec postgres psql -U myuser -d mydb -c "UPDATE users SET account_role = 'admin' WHERE email = 'admin@example.com'" > /dev/null 2>&1 +# Primary test user: zero deposit requirement for easy booking +docker exec postgres psql -U myuser -d mydb -c "UPDATE users SET deposits_required = 0 WHERE email = 'user@example.com'" > /dev/null 2>&1 +# Loyal regulars: zero deposit requirement +docker exec postgres psql -U myuser -d mydb -c "UPDATE users SET deposits_required = 0 WHERE email IN ('emma.johnson@example.com','sophie.williams@example.com','amelia.jones@example.com','isla.davies@example.com','lily.wilson@example.com','ava.walker@example.com','grace.fletcher@example.com')" > /dev/null 2>&1 +# Deposit-required users: 3 no-shows on record +docker exec postgres psql -U myuser -d mydb -c "UPDATE users SET deposits_required = 3 WHERE email IN ('poppy.thompson@example.com','mia.white@example.com')" > /dev/null 2>&1 +# Liam: 1 no-show, in 48h restriction window +docker exec postgres psql -U myuser -d mydb -c "UPDATE users SET deposits_required = 1 WHERE email = 'liam.caldwell@example.com'" > /dev/null 2>&1 -# Set deposits_required=0 for main test user (so bookings can be created without 48h restriction) -# Set deposits_required=3 for some users to demonstrate deposit snapshot behavior -docker exec postgres psql -U myuser -d mydb -c "UPDATE users SET deposits_required = 0 WHERE email = '$USER_EMAIL'" > /dev/null 2>&1 -docker exec postgres psql -U myuser -d mydb -c "UPDATE users SET deposits_required = 3 WHERE email IN ('poppy.thompson@example.com', 'mia.white@example.com')" > /dev/null 2>&1 echo "${C_GREEN}โœ… Registered $success/$total Users${C_RESET}" +user_success=$success +user_total=$total -# 2. Login +# =========================================================================== +# 2. LOGIN +# =========================================================================== echo -e "\n${C_BLUE}๐Ÿ”‘ Authenticating...${C_RESET}" -LOGIN_RESP=$(curl -s -X POST -H 'Content-Type: application/json' -d "{\"email\":\"$ADMIN_EMAIL\",\"password\":\"$ADMIN_PASS\"}" "$BASE_URL/login") -# FIX 2: Clean token extraction -ADMIN_TOKEN=$(echo "$LOGIN_RESP" \ - | tr -d '\r\n\t ' \ - | sed -n 's/.*"token":"\([^"]*\)".*/\1/p') -if [ -z "$ADMIN_TOKEN" ]; then - echo "โŒ Admin auth failed. Response: $LOGIN_RESP" - exit 1 -fi +ADMIN_TOKEN=$(login "$ADMIN_EMAIL" "$ADMIN_PASS") +if [ -z "$ADMIN_TOKEN" ]; then echo "โŒ Admin auth failed"; exit 1; fi -USER_LOGIN_RESP=$(curl -s -X POST -H 'Content-Type: application/json' -d "{\"email\":\"$USER_EMAIL\",\"password\":\"$USER_PASS\"}" "$BASE_URL/login") -USER_TOKEN=$(echo "$USER_LOGIN_RESP" \ - | tr -d '\r\n\t ' \ - | sed -n 's/.*"token":"\([^"]*\)".*/\1/p') +USER_TOKEN=$(login "$USER_EMAIL" "$USER_PASS") +if [ -z "$USER_TOKEN" ]; then echo "โŒ User auth failed"; exit 1; fi +EMMA_TOKEN=$(login "emma.johnson@example.com" "password") +SOPHIE_TOKEN=$(login "sophie.williams@example.com" "password") +AMELIA_TOKEN=$(login "amelia.jones@example.com" "password") +ISLA_TOKEN=$(login "isla.davies@example.com" "password") +LILY_TOKEN=$(login "lily.wilson@example.com" "password") +AVA_TOKEN=$(login "ava.walker@example.com" "password") +POPPY_TOKEN=$(login "poppy.thompson@example.com" "password") +MIA_TOKEN=$(login "mia.white@example.com" "password") +GRACE_TOKEN=$(login "grace.fletcher@example.com" "password") + +# Look up user IDs for admin booking creation +get_user_id() { + docker exec postgres psql -U myuser -d mydb -tAc "SELECT id FROM users WHERE email='$1';" 2>/dev/null | tr -d '\r\n\t ' +} + +ADMIN_USER_ID=$(get_user_id "admin@example.com") +USER_USER_ID=$(get_user_id "user@example.com") +EMMA_ID=$(get_user_id "emma.johnson@example.com") +SOPHIE_ID=$(get_user_id "sophie.williams@example.com") +AMELIA_ID=$(get_user_id "amelia.jones@example.com") +ISLA_ID=$(get_user_id "isla.davies@example.com") +LILY_ID=$(get_user_id "lily.wilson@example.com") +AVA_ID=$(get_user_id "ava.walker@example.com") +OLIVER_ID=$(get_user_id "oliver.smith@example.com") +HARRY_ID=$(get_user_id "harry.brown@example.com") +CHARLIE_ID=$(get_user_id "charlie.wright@example.com") +NOAH_ID=$(get_user_id "noah.robinson@example.com") +POPPY_ID=$(get_user_id "poppy.thompson@example.com") +MIA_ID=$(get_user_id "mia.white@example.com") +GRACE_ID=$(get_user_id "grace.fletcher@example.com") +LIAM_ID=$(get_user_id "liam.caldwell@example.com") -if [ -z "$USER_TOKEN" ]; then - echo "โŒ User auth failed. Response: $USER_LOGIN_RESP" - exit 1 -fi echo "${C_GREEN}โœ… Authentication successful${C_RESET}" sleep 1 -# 3. Create Services +# =========================================================================== +# 3. CREATE SERVICES +# =========================================================================== echo -e "\n${C_BLUE}๐Ÿ’… Creating Services...${C_RESET}" + SERVICES=( '{"name":"Classic Manicure","description":"Nail shaping, cuticle care, hand massage, and polish.","price":25.00,"duration_minutes":45,"minimum_age_required":0}' - '{"name":"Gel Manicure (BIAB)","description":"Hard-wearing gel polish with Builder In A Bottle base.","price":35.00,"duration_minutes":60,"minimum_age_required":0}' + '{"name":"Gel Manicure (BIAB)","description":"Hard-wearing gel polish with Builder In A Bottle base. Long-lasting with added nail strength.","price":35.00,"duration_minutes":60,"minimum_age_required":0}' '{"name":"Luxury Pedicure","description":"Foot soak, scrub, mask, extended massage, and polish.","price":45.00,"duration_minutes":75,"minimum_age_required":0}' '{"name":"Express Mani & Pedi","description":"Quick file, shape, and polish for both hands and feet.","price":40.00,"duration_minutes":60,"minimum_age_required":0}' - '{"name":"Gel Polish Removal","description":"Safe removal of existing gel polish.","price":10.00,"duration_minutes":20,"minimum_age_required":0}' - '{"name":"Nail Art Add-on","description":"Custom nail art, per two fingers.","price":5.00,"duration_minutes":15,"minimum_age_required":0}' - '{"name":"Gel Polish Full Set","description":"Full gel polish application - requires patch test 24h before.","price":45.00,"duration_minutes":60,"minimum_age_required":0}' - '{"name":"Luxury Gel Manicure","description":"Premium gel polish with extended massage - requires patch test 24h before.","price":55.00,"duration_minutes":75,"minimum_age_required":0}' + '{"name":"Gel Polish Removal","description":"Safe soak-off removal of existing gel polish, including aftercare oil treatment.","price":10.00,"duration_minutes":20,"minimum_age_required":0}' + '{"name":"Nail Art Add-on","description":"Custom nail art per two fingers โ€” French tips, florals, gems, and more.","price":5.00,"duration_minutes":15,"minimum_age_required":0}' + '{"name":"Gel Polish Full Set","description":"Full gel polish application over natural nails. Requires patch test 24h before first appointment.","price":45.00,"duration_minutes":60,"minimum_age_required":0}' + '{"name":"Luxury Gel Manicure","description":"Premium gel polish with extended hand massage and cuticle treatment. Requires patch test 24h before first appointment.","price":55.00,"duration_minutes":75,"minimum_age_required":0}' + '{"name":"Acrylic Full Set","description":"Full set of acrylic extensions, shaped and polished to your preference.","price":55.00,"duration_minutes":90,"minimum_age_required":16}' + '{"name":"Acrylic Infill","description":"Maintenance infill for existing acrylic extensions.","price":35.00,"duration_minutes":60,"minimum_age_required":16}' + '{"name":"Paraffin Wax Treatment","description":"Deeply moisturising paraffin wax hand or foot treatment, great as an add-on.","price":12.00,"duration_minutes":20,"minimum_age_required":0}' + '{"name":"Bridal Nail Package","description":"Luxury manicure and pedicure with nail art, paraffin wax, and extended massage for the big day.","price":120.00,"duration_minutes":150,"minimum_age_required":0}' ) SERVICE_IDS=() @@ -316,52 +380,64 @@ total=${#SERVICES[@]} for svc in "${SERVICES[@]}"; do NAME=$(echo "$svc" | grep -o '"name":"[^"]*' | cut -d'"' -f4) ID=$(api_post "$BASE_URL/admin/services" "$svc" "Create $NAME" "$ADMIN_TOKEN") - if [[ -n "$ID" ]]; then - ID=$(echo "$ID" | tr -cd '[:alnum:]-') + if [[ -n "$ID" && "$ID" =~ ^[0-9a-f-]{8,}$ ]]; then SERVICE_IDS+=("$ID") success=$((success+1)) fi done echo "${C_GREEN}โœ… Created $success/$total Services${C_RESET}" -# 3b. Create Patch Tests (for gel services that require testing) +get_svc() { echo "${SERVICE_IDS[$1]}"; } + +# =========================================================================== +# 3b. CREATE PATCH TESTS (seeded directly via SQL โ€” no creation API endpoint) +# =========================================================================== echo -e "\n${C_BLUE}๐Ÿงช Creating Patch Tests...${C_RESET}" -# Get the gel service IDs (indices 6 and 7) -GEL_SERVICE_IDS=("${SERVICE_IDS[6]}" "${SERVICE_IDS[7]}") +# Gel Allergy Test covers: Gel Polish Full Set (idx 6), Luxury Gel Manicure (idx 7) +GEL_SVC_1="${SERVICE_IDS[6]}" +GEL_SVC_2="${SERVICE_IDS[7]}" -# Create patch test linking to gel services -docker exec postgres psql -U myuser -d mydb -c " - INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids) - VALUES ( - 'Gel Allergy Test', - 'Patch test for gel polish products - must be completed 24h before first gel service', - 24, - 6, - ARRAY['${GEL_SERVICE_IDS[0]}', '${GEL_SERVICE_IDS[1]}'] - ); -" > /dev/null 2>&1 +# Insert patch test directly via SQL (no creation API endpoint exists) +# service_ids cast explicitly to uuid[] to handle typed columns +PATCH_TEST_RESULT=$(docker exec postgres psql -U myuser -d mydb -tAc \ + "INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids) VALUES ('Gel Allergy Test', 'Mandatory patch test for all gel polish services. Must be completed at least 24 hours before your first gel appointment.', 24, 6, ARRAY['${GEL_SVC_1}','${GEL_SVC_2}']) RETURNING id;" 2>&1) +PATCH_TEST_ID=$(echo "$PATCH_TEST_RESULT" | head -1 | tr -d '\r\t ') -echo "${C_GREEN}โœ… Created patch test for gel services${C_RESET}" +# Validate it looks like a UUID; print raw output if not to help diagnose +if [[ -z "$PATCH_TEST_ID" || ! "$PATCH_TEST_ID" =~ ^[0-9a-f-]{8,}$ ]]; then + echo "${C_YELLOW}โš ๏ธ Patch test SQL failed. Raw output: $PATCH_TEST_RESULT${C_RESET}" + PATCH_TEST_ID="" +fi -# 4. Create Bookings +# Record that Grace has already completed her patch test via the API +if [[ -n "$PATCH_TEST_ID" && -n "$GRACE_ID" ]]; then + api_post "$BASE_URL/admin/users/$GRACE_ID/patch-tests" \ + "{\"patch_test_id\":\"$PATCH_TEST_ID\"}" \ + "Record patch test for Grace" "$ADMIN_TOKEN" > /dev/null + echo "${C_GREEN}โœ… Patch test seeded (SQL) and recorded for Grace Fletcher${C_RESET}" +else + echo "${C_YELLOW}โš ๏ธ Patch test seeding failed (check DB connection or patch_tests table)${C_RESET}" +fi + +# =========================================================================== +# 4. BOOKINGS +# =========================================================================== echo -e "\n${C_BLUE}๐Ÿ“… Creating Bookings...${C_RESET}" format_london_time() { TZ=Europe/London date -d "$1 $2" +"%Y-%m-%dT%H:%M:%S%:z" } +# Create a booking as a regular user, capture LAST_BOOKING_ID create_booking() { local token=$1 time=$2 services=$3 notes=$4 name=$5 local json="{\"start_time\":\"$time\",\"service_ids\":$services" [[ -n "$notes" ]] && json="$json,\"notes\":\"$notes\"" json="$json}" - # Call API and capture ID local id=$(api_post "$BASE_URL/bookings" "$json" "Book: $name" "$token") - - if [[ -n "$id" ]]; then - id=$(echo "$id" | tr -cd '[:alnum:]-') + if [[ -n "$id" && "$id" =~ ^[0-9a-f-]{8,}$ ]]; then LAST_BOOKING_ID="$id" return 0 else @@ -370,186 +446,441 @@ create_booking() { fi } -get_svc() { echo "${SERVICE_IDS[$1]}"; } +# Create a booking as admin (no advance restriction, always confirmed) +create_admin_booking() { + local user_id=$1 time=$2 services=$3 notes=$4 name=$5 + local json="{\"user_id\":\"$user_id\",\"start_time\":\"$time\",\"service_ids\":$services,\"service_overrides\":[],\"enforce_deposits\":false" + [[ -n "$notes" ]] && json="$json,\"notes\":\"$notes\"" + json="$json}" -# Counters + local id=$(api_post "$BASE_URL/admin/bookings" "$json" "Admin Book: $name" "$ADMIN_TOKEN") + if [[ -n "$id" && "$id" =~ ^[0-9a-f-]{8,}$ ]]; then + LAST_BOOKING_ID="$id" + return 0 + else + LAST_BOOKING_ID="" + return 1 + fi +} + +# Returns the nearest OPEN business day at or after the given date. +# Schema: Monday=0 closed, Sunday=6 closed. Tue-Sat open. +# dow: date's day-of-week as 0=Sun,1=Mon,...,6=Sat (GNU date %w) +open_day() { + local d="$1" + local max=7 + for ((i=0; i /dev/null 2>&1 + PENDING_BOOKING_IDS+=("$LAST_BOOKING_ID"); PENDING_BOOKING_NAMES+=("Isla - Gel + Nail Art (pending)") + fi +fi +if create_admin_booking "$AVA_ID" "$(format_london_time "$TOMORROW" "$SLOT_E")" "[\"$(get_svc 0)\"]" "Could I get a specific nail shape โ€” stiletto if possible?" "Ava - Classic Manicure (tomorrow, pending)"; then + count_tomorrow=$((count_tomorrow+1)) + if [[ -n "$LAST_BOOKING_ID" ]]; then + docker exec postgres psql -U myuser -d mydb -c "UPDATE bookings SET status = 'pending' WHERE id = '${LAST_BOOKING_ID}'" > /dev/null 2>&1 + PENDING_BOOKING_IDS+=("$LAST_BOOKING_ID"); PENDING_BOOKING_NAMES+=("Ava - Classic Manicure (pending)") + fi +fi + +echo "${C_GREEN}โœ… Created $count_tomorrow Tomorrow's Bookings${C_RESET}" + +# --------------------------------------------------------------------------- +# UPCOMING โ€” next 14 days, all via admin to avoid booking_status enum bug. +# Bridal package uses user token (notes โ†’ pending) as the one exception. +# --------------------------------------------------------------------------- +echo -e "\n${C_YELLOW}๐Ÿ“… Creating Upcoming Bookings (next 14 days)...${C_RESET}" count_future=0 -# Array to hold IDs of upcoming bookings for confirmation step -UPCOMING_BOOKING_IDS=() -UPCOMING_BOOKING_NAMES=() +UP_IDS=( "$EMMA_ID" "$SOPHIE_ID" "$AMELIA_ID" "$ISLA_ID" "$LILY_ID" "$AVA_ID" "$GRACE_ID" "$USER_USER_ID" ) +UP_SVCS=( "1" "2" "0" "3" "1" "2" "7" "0" ) +UP_SLOTS=( "$SLOT_A" "$SLOT_C" "$SLOT_D" "$SLOT_B" "$SLOT_C" "$SLOT_A" "$SLOT_D" "$SLOT_B" ) +UP_NAMES=( "Gel Manicure" "Luxury Pedicure" "Classic Manicure" "Express Mani & Pedi" "Gel Manicure" "Luxury Pedicure" "Luxury Gel Manicure" "Classic Manicure" ) -# --- PAST BOOKINGS (8 Total) --- -echo -e "\n${C_YELLOW}๐Ÿ“… Creating 8 Past Bookings (Last 8 days)...${C_RESET}" -for day_offset in {1..8}; do - PAST_DATE=$(TZ=Europe/London date -d "today -$day_offset days" +%Y-%m-%d) +for day_offset in {2..15}; do + RAW_DATE=$(TZ=Europe/London date -d "$TODAY +$day_offset days" +%Y-%m-%d) + FUTURE_DATE=$(open_day "$RAW_DATE") + idx=$(( (day_offset - 2) % ${#UP_IDS[@]} )) + uid="${UP_IDS[$idx]}" + svc_idx="${UP_SVCS[$idx]}" + slot="${UP_SLOTS[$idx]}" + label="${UP_NAMES[$idx]}" - # Alternate times - if [ $((day_offset % 2)) -eq 0 ]; then - TIME="14:00:00" - else - TIME="10:00:00" - fi - - # Alternate services - SVC_IDX=$(( (day_offset % 2) )) - NAME="Past ($PAST_DATE) - $(echo "${SERVICES[$SVC_IDX]}" | grep -o '"name":"[^"]*' | cut -d'"' -f4)" - - if create_booking "$USER_TOKEN" "$(format_london_time "$PAST_DATE" "$TIME")" "[\"$(get_svc $SVC_IDX)\"]" "" "$NAME"; then - count_past=$((count_past+1)) - fi -done -echo "${C_GREEN}โœ… Created $count_past/8 Past Bookings${C_RESET}" - -# --- TODAY (3) --- -TODAY=$(TZ=Europe/London date +%Y-%m-%d) -TOMORROW=$(TZ=Europe/London date -d "tomorrow" +%Y-%m-%d) - -if create_booking "$USER_TOKEN" "$(format_london_time "$TODAY" "09:30:00")" "[\"$(get_svc 0)\"]" "" "Today - Classic Manicure"; then - count_today=$((count_today+1)); UPCOMING_BOOKING_IDS+=("$LAST_BOOKING_ID"); UPCOMING_BOOKING_NAMES+=("Today - Classic Manicure"); -fi -if create_booking "$USER_TOKEN" "$(format_london_time "$TODAY" "11:30:00")" "[\"$(get_svc 1)\"]" "" "Today - Gel Manicure"; then - count_today=$((count_today+1)); UPCOMING_BOOKING_IDS+=("$LAST_BOOKING_ID"); UPCOMING_BOOKING_NAMES+=("Today - Gel Manicure"); -fi -if create_booking "$USER_TOKEN" "$(format_london_time "$TODAY" "14:00:00")" "[\"$(get_svc 2)\"]" "" "Today - Luxury Pedicure"; then - count_today=$((count_today+1)); UPCOMING_BOOKING_IDS+=("$LAST_BOOKING_ID"); UPCOMING_BOOKING_NAMES+=("Today - Luxury Pedicure"); -fi - -# --- TOMORROW (4) --- -if create_booking "$USER_TOKEN" "$(format_london_time "$TOMORROW" "09:00:00")" "[\"$(get_svc 3)\"]" "" "Tomorrow - Express Mani & Pedi"; then - count_tomorrow=$((count_tomorrow+1)); UPCOMING_BOOKING_IDS+=("$LAST_BOOKING_ID"); UPCOMING_BOOKING_NAMES+=("Tomorrow - Express Mani & Pedi"); -fi -if create_booking "$USER_TOKEN" "$(format_london_time "$TOMORROW" "10:30:00")" "[\"$(get_svc 0)\",\"$(get_svc 5)\"]" "" "Tomorrow - Classic + Nail Art"; then - count_tomorrow=$((count_tomorrow+1)); UPCOMING_BOOKING_IDS+=("$LAST_BOOKING_ID"); UPCOMING_BOOKING_NAMES+=("Tomorrow - Classic + Nail Art"); -fi -if create_booking "$USER_TOKEN" "$(format_london_time "$TOMORROW" "13:00:00")" "[\"$(get_svc 1)\"]" "" "Tomorrow - Gel Manicure"; then - count_tomorrow=$((count_tomorrow+1)); UPCOMING_BOOKING_IDS+=("$LAST_BOOKING_ID"); UPCOMING_BOOKING_NAMES+=("Tomorrow - Gel Manicure"); -fi -if create_booking "$USER_TOKEN" "$(format_london_time "$TOMORROW" "15:30:00")" "[\"$(get_svc 2)\"]" "" "Tomorrow - Luxury Pedicure"; then - count_tomorrow=$((count_tomorrow+1)); UPCOMING_BOOKING_IDS+=("$LAST_BOOKING_ID"); UPCOMING_BOOKING_NAMES+=("Tomorrow - Luxury Pedicure"); -fi - -# --- FUTURE (30) --- -# Spread over the next 15 days (Day +2 to Day +16) -for day_offset in {2..16}; do - FUTURE_DATE=$(TZ=Europe/London date -d "$TODAY +$day_offset days" +%Y-%m-%d) - - # Morning Slot (10:00) - if create_booking "$USER_TOKEN" "$(format_london_time "$FUTURE_DATE" "10:00:00")" "[\"$(get_svc 0)\"]" "" "Future ($FUTURE_DATE) - Classic Manicure"; then - count_future=$((count_future+1)); UPCOMING_BOOKING_IDS+=("$LAST_BOOKING_ID"); UPCOMING_BOOKING_NAMES+=("Future ($FUTURE_DATE) AM"); - fi - - # Afternoon Slot (14:30) - if create_booking "$USER_TOKEN" "$(format_london_time "$FUTURE_DATE" "14:30:00")" "[\"$(get_svc 1)\"]" "" "Future ($FUTURE_DATE) - Gel Manicure"; then - count_future=$((count_future+1)); UPCOMING_BOOKING_IDS+=("$LAST_BOOKING_ID"); UPCOMING_BOOKING_NAMES+=("Future ($FUTURE_DATE) PM"); + if create_admin_booking "$uid" "$(format_london_time "$FUTURE_DATE" "$slot")" "[\"$(get_svc $svc_idx)\"]" "" "$label ($FUTURE_DATE)"; then + count_future=$((count_future+1)) + # NOTE: do NOT add admin bookings to PENDING_BOOKING_IDS โ€” they're already confirmed fi done -# Summary -TOTAL=$((count_today + count_tomorrow + count_future + count_past)) -echo "${C_GREEN}โœ… Created $count_today/3 Bookings (Today)${C_RESET}" -echo "${C_GREEN}โœ… Created $count_tomorrow/4 Bookings (Tomorrow)${C_RESET}" -echo "${C_GREEN}โœ… Created $count_future/30 Bookings (Future)${C_RESET}" -echo "${C_GREEN}โœ… Created $count_past/8 Bookings (Past)${C_RESET}" -echo "${C_GREEN}โœ… Created $TOTAL/45 Bookings (Total)${C_RESET}" - -# 4b. Create Booking for Deposit-Required User -echo -e "\n${C_BLUE}๐Ÿ’ฐ Creating Booking for Deposit-Required User...${C_RESET}" - -# Login as Poppy (deposits_required=3) -POPPY_LOGIN_RESP=$(curl -s -X POST -H 'Content-Type: application/json' -d '{"email":"poppy.thompson@example.com","password":"password"}' "$BASE_URL/login") -POPPY_TOKEN=$(echo "$POPPY_LOGIN_RESP" | tr -d '\r\n\t ' | sed -n 's/.*"token":"\([^"]*\).*/\1/p') - -if [[ -n "$POPPY_TOKEN" ]]; then - # Book 3 days ahead (50h+ to satisfy 48h requirement for deposit-required users) - DEPOSIT_TIME=$(TZ=Europe/London date -d "3 days 10:00" +"%Y-%m-%dT%H:%M:%S%:z") - if create_booking "$POPPY_TOKEN" "$DEPOSIT_TIME" "[\"$(get_svc 0)\"]" "" "Poppy (deposit required)"; then - echo "${C_GREEN}โœ… Created deposit-required booking (deposit_required=true snapshotted)${C_RESET}" - else - echo "${C_YELLOW}โš ๏ธ Could not create deposit-required booking${C_RESET}" - fi -else - echo "${C_YELLOW}โš ๏ธ Could not login as Poppy to create deposit-required booking${C_RESET}" +# Multi-service admin bookings for variety +D2=$(open_day "$(TZ=Europe/London date -d "$TODAY +3 days" +%Y-%m-%d)") +if create_admin_booking "$USER_USER_ID" "$(format_london_time "$D2" "$SLOT_B")" "[\"$(get_svc 0)\",\"$(get_svc 5)\"]" "" "User - Classic + Nail Art (+3 days)"; then + count_future=$((count_future+1)) fi -# 5. Confirm Random Half of Upcoming Bookings -echo -e "\n${C_BLUE}๐Ÿ”’ Confirming Random Upcoming Bookings...${C_RESET}" +D3=$(open_day "$(TZ=Europe/London date -d "$TODAY +6 days" +%Y-%m-%d)") +if create_admin_booking "$EMMA_ID" "$(format_london_time "$D3" "$SLOT_A")" "[\"$(get_svc 1)\",\"$(get_svc 4)\"]" "" "Emma - Gel Manicure + Removal (+6 days)"; then + count_future=$((count_future+1)) +fi + +# Bridal package โ€” admin-created with notes, then forced to pending via DB. +D4=$(open_day "$(TZ=Europe/London date -d "$TODAY +12 days" +%Y-%m-%d)") +if create_admin_booking "$SOPHIE_ID" "$(format_london_time "$D4" "$SLOT_D")" "[\"$(get_svc 11)\"]" "Bride-to-be โ€” please can we discuss nail art options beforehand?" "Sophie - Bridal Package (+12 days, pending)"; then + count_future=$((count_future+1)) + if [[ -n "$LAST_BOOKING_ID" ]]; then + docker exec postgres psql -U myuser -d mydb -c "UPDATE bookings SET status = 'pending' WHERE id = '${LAST_BOOKING_ID}'" > /dev/null 2>&1 + PENDING_BOOKING_IDS+=("$LAST_BOOKING_ID"); PENDING_BOOKING_NAMES+=("Sophie - Bridal Package (pending)") + fi +fi + +# Deposit-required user โ€” admin-created to bypass deposit check +D5=$(open_day "$(TZ=Europe/London date -d "$TODAY +4 days" +%Y-%m-%d)") +if create_admin_booking "$POPPY_ID" "$(format_london_time "$D5" "$SLOT_C")" "[\"$(get_svc 0)\"]" "" "Poppy - Future (deposit snapshotted)"; then + count_future=$((count_future+1)) +fi + +echo "${C_GREEN}โœ… Created $count_future Upcoming Bookings${C_RESET}" + +# --------------------------------------------------------------------------- +# CONFIRM roughly 2/3 of pending bookings, leave ~1/3 for admin review +# --------------------------------------------------------------------------- +echo -e "\n${C_BLUE}๐Ÿ”’ Confirming Upcoming Bookings (approx. 2/3)...${C_RESET}" confirmed_count=0 -rejected_count=0 -attempted_count=0 -total_upcoming=${#UPCOMING_BOOKING_IDS[@]} - -# Small pause to ensure backend is ready after bulk creation +skipped_count=0 sleep 1 -for ((i=0; i<${#UPCOMING_BOOKING_IDS[@]}; i++)); do - # FIX 3: Sanitize ID again just before use to ensure no hidden characters broke the array - id="${UPCOMING_BOOKING_IDS[$i]}" - name="${UPCOMING_BOOKING_NAMES[$i]}" +for ((i=0; i<${#PENDING_BOOKING_IDS[@]}; i++)); do + id="${PENDING_BOOKING_IDS[$i]}" + [[ -z "$id" ]] && continue + [[ ! "$id" =~ ^[0-9a-f-]{8,}$ ]] && continue - # Ensure ID is not empty - if [ -z "$id" ]; then - continue - fi - - # Random coin flip (0 or 1). If 1, confirm. - if [ $((RANDOM % 2)) -eq 1 ]; then - attempted_count=$((attempted_count+1)) - # Send proper JSON with empty serviceOverrides array + if [ $((RANDOM % 3)) -ne 0 ]; then RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $ADMIN_TOKEN" \ - -d '{"serviceOverrides":[]}' \ + -d '{"service_overrides":[],"notes":""}' \ "$BASE_URL/admin/bookings/$id/confirm") HTTP_CODE=$(echo "$RESPONSE" | tail -n1) - BODY=$(echo "$RESPONSE" | sed '$d') - - if [ "$HTTP_CODE" = "200" ]; then - confirmed_count=$((confirmed_count+1)) - else - rejected_count=$((rejected_count+1)) - fi - # Small sleep to prevent overwhelming the server - sleep 0.1 + [ "$HTTP_CODE" = "200" ] && confirmed_count=$((confirmed_count+1)) + sleep 0.05 + else + skipped_count=$((skipped_count+1)) fi done -echo "${C_GREEN}โœ… Confirmed $confirmed_count/$attempted_count Bookings${C_RESET}" -if [ "$rejected_count" -gt 0 ]; then - echo "${C_YELLOW}โš ๏ธ Rejected $rejected_count/$attempted_count (deposit/time restrictions)${C_RESET}" +echo "${C_GREEN}โœ… Confirmed $confirmed_count bookings, left $skipped_count pending for admin review${C_RESET}" + +# --------------------------------------------------------------------------- +# CANCELLATIONS +# Both POST /admin/bookings/{id}/cancel and DELETE /bookings/{id} have backend +# bugs (enum value mismatch + FK violation). Patching status directly in DB. +# Auto-detects the actual enum values from pg_enum to avoid hardcoding. +# --------------------------------------------------------------------------- +echo -e "\n${C_BLUE}๐Ÿšซ Simulating Cancellations...${C_RESET}" +cancel_count=0 + +# Discover actual booking_status enum values +ENUM_VALUES=$(docker exec postgres psql -U myuser -d mydb -tAc \ + "SELECT string_agg(enumlabel, ',' ORDER BY enumsortorder) FROM pg_enum e JOIN pg_type t ON e.enumtypid = t.oid WHERE t.typname = 'booking_status';" 2>/dev/null | tr -d '\r\n\t ') +echo " โ„น๏ธ booking_status enum: $ENUM_VALUES" + +CLIENT_CANCEL_STATUS=$(echo "$ENUM_VALUES" | tr ',' '\n' | grep -i 'client' | head -1) +ADMIN_CANCEL_STATUS=$(echo "$ENUM_VALUES" | tr ',' '\n' | grep -iE 'we_|admin' | head -1) +# If no distinct admin status, fall back to the first cancel-looking value +[[ -z "$CLIENT_CANCEL_STATUS" ]] && CLIENT_CANCEL_STATUS=$(echo "$ENUM_VALUES" | tr ',' '\n' | grep -i 'cancel' | head -1) +[[ -z "$ADMIN_CANCEL_STATUS" ]] && ADMIN_CANCEL_STATUS="$CLIENT_CANCEL_STATUS" +echo " โ„น๏ธ Cancel statuses โ€” client: '$CLIENT_CANCEL_STATUS' admin: '$ADMIN_CANCEL_STATUS'" + +db_cancel() { + local booking_id="$1" status="$2" reason="$3" + [[ -z "$booking_id" || -z "$status" ]] && return 1 + # Discover the cancel reason column name (varies by schema) + local reason_col + reason_col=$(docker exec postgres psql -U myuser -d mydb -tAc \ + "SELECT column_name FROM information_schema.columns WHERE table_name='bookings' AND column_name IN ('cancel_reason','cancellation_reason','reason') LIMIT 1;" 2>/dev/null | head -1 | tr -d '\r\t ') + local sql + if [[ -n "$reason_col" ]]; then + sql="UPDATE bookings SET status = '$status', $reason_col = '$reason' WHERE id = '$booking_id' RETURNING id;" + else + sql="UPDATE bookings SET status = '$status' WHERE id = '$booking_id' RETURNING id;" + fi + local result + result=$(docker exec postgres psql -U myuser -d mydb -tAc "$sql" 2>&1) + local rows + rows=$(echo "$result" | head -1 | tr -d '\r\t ') + if [[ -n "$rows" && "$rows" =~ ^[0-9a-f-]{8,}$ ]]; then + return 0 + else + echo "${C_RED} SQL: $sql${C_RESET}" + echo "${C_RED} Result: $result${C_RESET}" + return 1 + fi +} + +D_CANCEL=$(open_day "$(TZ=Europe/London date -d "$TODAY +5 days" +%Y-%m-%d)") +if create_admin_booking "$AVA_ID" "$(format_london_time "$D_CANCEL" "$SLOT_B")" "[\"$(get_svc 0)\"]" "" "Ava - to be cancelled"; then + if db_cancel "$LAST_BOOKING_ID" "$CLIENT_CANCEL_STATUS" "Something came up, really sorry!"; then + cancel_count=$((cancel_count+1)) + else + echo "${C_RED}โŒ DB cancel failed for Ava${C_RESET}" + fi fi +D_CANCEL2=$(open_day "$(TZ=Europe/London date -d "$TODAY +7 days" +%Y-%m-%d)") +if create_admin_booking "$USER_USER_ID" "$(format_london_time "$D_CANCEL2" "$SLOT_C")" "[\"$(get_svc 2)\"]" "" "User - to be cancelled"; then + if db_cancel "$LAST_BOOKING_ID" "$CLIENT_CANCEL_STATUS" "Plans changed, apologies for the late notice."; then + cancel_count=$((cancel_count+1)) + else + echo "${C_RED}โŒ DB cancel failed for User${C_RESET}" + fi +fi -# 6. Exceptional Groups (2 Total) -echo -e "\n${C_BLUE}๐Ÿ—“๏ธ Creating Exceptional Groups...${C_RESET}" -NOV_BREAK='{"name":"November Break","description":"Short break period in November","hours":[{"weekday":0,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false},{"weekday":1,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false},{"weekday":2,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false},{"weekday":3,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false},{"weekday":4,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false},{"weekday":5,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false},{"weekday":6,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false}],"weekStarts":["2025-11-10"]}' -XMAS_BREAK='{"name":"Christmas Holiday Period","description":"Reduced hours for Christmas and New Year","hours":[{"weekday":0,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false},{"weekday":1,"startTime":"10:00:00","endTime":"15:00:00","isOpen":true},{"weekday":2,"startTime":"10:00:00","endTime":"15:00:00","isOpen":true},{"weekday":3,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false},{"weekday":4,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false},{"weekday":5,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false},{"weekday":6,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false}],"weekStarts":["2025-12-22","2025-12-29"]}' +D_ADMIN_CANCEL=$(open_day "$(TZ=Europe/London date -d "$TODAY +9 days" +%Y-%m-%d)") +if create_admin_booking "$NOAH_ID" "$(format_london_time "$D_ADMIN_CANCEL" "$SLOT_B")" "[\"$(get_svc 1)\"]" "" "Noah - to be admin-cancelled"; then + if db_cancel "$LAST_BOOKING_ID" "$ADMIN_CANCEL_STATUS" "Slot no longer available due to schedule change."; then + cancel_count=$((cancel_count+1)) + else + echo "${C_RED}โŒ DB cancel failed for Noah${C_RESET}" + fi +fi -success=0 -total=2 -if api_post "$BASE_URL/scheduling/exceptional-groups" "$NOV_BREAK" "November Break" "$ADMIN_TOKEN" > /dev/null; then success=$((success+1)); fi -if api_post "$BASE_URL/scheduling/exceptional-groups" "$XMAS_BREAK" "Christmas Holiday" "$ADMIN_TOKEN" > /dev/null; then success=$((success+1)); fi -echo "${C_GREEN}โœ… Created $success/$total Exceptional Groups${C_RESET}" +echo "${C_GREEN}โœ… Simulated $cancel_count cancellations${C_RESET}" -echo -e "\n${C_GREEN}๐ŸŽ‰ Seeding Complete!${C_RESET}" -echo -e "${C_YELLOW}Press ENTER to run tests...${C_RESET}" +# =========================================================================== +# 5. EXCEPTIONAL SCHEDULING GROUPS +# =========================================================================== +echo -e "\n${C_BLUE}๐Ÿ—“๏ธ Creating Exceptional Schedule Groups...${C_RESET}" +sched_success=0 + +# Easter break โ€” fully closed +EASTER_BREAK='{ + "name":"Easter Break", + "description":"Closed for Easter weekend", + "hours":[ + {"weekday":0,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false}, + {"weekday":1,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false}, + {"weekday":2,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false}, + {"weekday":3,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false}, + {"weekday":4,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false}, + {"weekday":5,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false}, + {"weekday":6,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false} + ], + "weekStarts":["2026-04-06"] +}' + +# Summer half-term โ€” reduced hours (mornings only, Tue-Sat) +HALF_TERM='{ + "name":"Spring Half-Term", + "description":"Reduced hours during half-term week โ€” mornings only", + "hours":[ + {"weekday":0,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false}, + {"weekday":1,"startTime":"09:30:00","endTime":"13:00:00","isOpen":true}, + {"weekday":2,"startTime":"09:30:00","endTime":"13:00:00","isOpen":true}, + {"weekday":3,"startTime":"09:30:00","endTime":"13:00:00","isOpen":true}, + {"weekday":4,"startTime":"09:30:00","endTime":"13:00:00","isOpen":true}, + {"weekday":5,"startTime":"09:30:00","endTime":"13:00:00","isOpen":true}, + {"weekday":6,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false} + ], + "weekStarts":["2026-06-01"] +}' + +# Christmas holiday โ€” fully closed across two weeks +XMAS_BREAK='{ + "name":"Christmas Holiday Period", + "description":"Closed for Christmas and New Year", + "hours":[ + {"weekday":0,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false}, + {"weekday":1,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false}, + {"weekday":2,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false}, + {"weekday":3,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false}, + {"weekday":4,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false}, + {"weekday":5,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false}, + {"weekday":6,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false} + ], + "weekStarts":["2026-12-21","2026-12-28"] +}' + +if api_post "$BASE_URL/scheduling/exceptional-groups" "$EASTER_BREAK" "Easter Break" "$ADMIN_TOKEN" > /dev/null; then sched_success=$((sched_success+1)); fi +if api_post "$BASE_URL/scheduling/exceptional-groups" "$HALF_TERM" "Spring Half-Term" "$ADMIN_TOKEN" > /dev/null; then sched_success=$((sched_success+1)); fi +if api_post "$BASE_URL/scheduling/exceptional-groups" "$XMAS_BREAK" "Christmas Break" "$ADMIN_TOKEN" > /dev/null; then sched_success=$((sched_success+1)); fi + +echo "${C_GREEN}โœ… Created $sched_success/3 Exceptional Schedule Groups${C_RESET}" + +# =========================================================================== +# SUMMARY +# =========================================================================== +TOTAL_BOOKINGS=$((count_past + count_today + count_tomorrow + count_future)) +echo "" +echo -e "${C_GREEN}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${C_RESET}" +echo -e "${C_GREEN}๐ŸŽ‰ Seeding Complete!${C_RESET}" +echo -e "${C_GREEN}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${C_RESET}" +echo -e " Users registered : $user_success/$user_total" +echo -e " Services created : ${#SERVICE_IDS[@]}/${#SERVICES[@]}" +if [[ -n "$PATCH_TEST_ID" ]]; then + echo -e " Patch tests : 1 (Gel Allergy Test, recorded for Grace)" +else + echo -e " Patch tests : โš ๏ธ failed โ€” see output above" +fi +echo -e " Bookings โ€” past : $count_past" +echo -e " Bookings โ€” today : $count_today" +echo -e " Bookings โ€” tomorrow: $count_tomorrow" +echo -e " Bookings โ€” future : $count_future" +echo -e " Bookings โ€” total : $TOTAL_BOOKINGS" +echo -e " Cancellations : $cancel_count" +echo -e " Confirmed : $confirmed_count | Still pending: $skipped_count" +echo -e " Schedule groups : $sched_success/3" +echo "" +echo -e " Quick login creds (all pass: ${C_YELLOW}password${C_RESET})" +echo -e " Admin : ${C_YELLOW}admin@example.com${C_RESET}" +echo -e " User : ${C_YELLOW}user@example.com${C_RESET}" +echo -e " Deposit: ${C_YELLOW}poppy.thompson@example.com${C_RESET}" +echo -e " Gel โœ“ : ${C_YELLOW}grace.fletcher@example.com${C_RESET} (patch test done)" +echo -e "${C_GREEN}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${C_RESET}" + +echo -e "\n${C_YELLOW}Press ENTER to run tests...${C_RESET}" read -r echo -e "${C_GREEN}โณ Running tests...${C_RESET}" -# Run tests in current pane with full output + cd /home/popertots/Crussell/backend export POSTGRES_USER POSTGRES_PASSWORD POSTGRES_HOST POSTGRES_DB GO_TESTING=1 TEST_OUTPUT=$(go test -tags test -v -p 1 -count=1 ./... 2>&1 || true) cd .. -# Show test summary - sanitize grep output to handle edge cases TOTAL_TESTS=$(echo "$TEST_OUTPUT" | grep "^=== RUN" | grep -cv "/" || echo "0") FAILED_TESTS=$(echo "$TEST_OUTPUT" | grep -c "^--- FAIL" || echo "0") SKIPPED_TESTS=$(echo "$TEST_OUTPUT" | grep -c "^--- SKIP" || echo "0") PASSED_TESTS=$(echo "$TEST_OUTPUT" | grep -c "^--- PASS" || echo "0") - -# Fallback: if counts are empty or invalid, default to 0 TOTAL_TESTS=${TOTAL_TESTS:-0} PASSED_TESTS=${PASSED_TESTS:-0} FAILED_TESTS=${FAILED_TESTS:-0} @@ -572,19 +903,15 @@ echo "" echo -e "${C_YELLOW}Press ENTER to restore 4-pane layout...${C_RESET}" read -r -# Switch back to the original workspace window (window 0) -# The original 4 panes (DB, Backend, Frontend, Rustfs) are still there tmux select-window -t $SESSION_NAME:0 tmux select-pane -t $SESSION_NAME:0.0 - SEED_EOF chmod +x $SEED_SCRIPT # --- 6. Execute Seed Script in Tmux --- log_step "Starting seeding process in new window..." -# Pass SESSION_NAME to the seed script tmux new-window -t $SESSION_NAME -n "Seeding" "SESSION_NAME=$SESSION_NAME $SEED_SCRIPT" # --- 7. Finalize ---