diff --git a/backend/handlers/bookings/bookings.go b/backend/handlers/bookings/bookings.go index f99aee9..8f560aa 100644 --- a/backend/handlers/bookings/bookings.go +++ b/backend/handlers/bookings/bookings.go @@ -1179,6 +1179,63 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) { return } + // Check patch test requirements for all services + for _, serviceID := range req.ServiceIDs { + // Find patch test for this service + var patchTestID string + var noticeHours int + err := db.DB.QueryRow(r.Context(), ` + SELECT id, notice_duration_hours + FROM patch_tests + WHERE $1 = ANY(service_ids) + `, serviceID).Scan(&patchTestID, ¬iceHours) + + if err == nil { + // Service requires a patch test - check if user has valid record + var testedAt time.Time + err = db.DB.QueryRow(r.Context(), ` + SELECT tested_at + FROM user_patch_tests + WHERE user_id = $1 AND patch_test_id = $2 + `, userID, patchTestID).Scan(&testedAt) + + if err != nil { + // No valid patch test record + http.Error(w, "Patch test required for this service. Please complete a patch test first.", http.StatusBadRequest) + return + } + + // Check if notice period has passed + eligibleFrom := testedAt.Add(time.Duration(noticeHours) * time.Hour) + if time.Now().Before(eligibleFrom) { + hoursLeft := time.Until(eligibleFrom).Hours() + http.Error(w, fmt.Sprintf("You must wait %.0f hours after your patch test before booking this service.", hoursLeft), http.StatusBadRequest) + return + } + + // Check if patch test has expired + var expiryMonths int + err = db.DB.QueryRow(r.Context(), `SELECT expiry_months FROM patch_tests WHERE id = $1`, patchTestID).Scan(&expiryMonths) + if err == nil { + expiresAt := testedAt.AddDate(0, expiryMonths, 0) + if time.Now().After(expiresAt) { + http.Error(w, "Your patch test has expired. Please complete a new patch test.", http.StatusBadRequest) + return + } + } + } + } + + // Validate start time is not in the past + if req.StartTime.IsZero() { + http.Error(w, "Start time is required", http.StatusBadRequest) + return + } + if len(req.ServiceIDs) == 0 { + http.Error(w, "At least one service is required", http.StatusBadRequest) + return + } + // Validate start time is not in the past if req.StartTime.Before(time.Now()) { http.Error(w, "Start time cannot be in the past", http.StatusBadRequest) @@ -1543,30 +1600,36 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) { } if req.Status == "completed" { + // When a booking is completed, extend patch test validity for any related patch tests + // Get all services in this booking rows, err := db.DB.Query(r.Context(), ` - SELECT bs.service_id, s.patch_test_duration_hours - FROM booking_services bs - JOIN services s ON bs.service_id = s.id - WHERE bs.booking_id = $1 AND s.patch_test_duration_hours > 0 + SELECT DISTINCT pt.id + FROM patch_tests pt + JOIN booking_services bs ON bs.booking_id = $1 + WHERE pt.id IN ( + SELECT pt_inner.id + FROM patch_tests pt_inner + WHERE bs.service_id = ANY(pt_inner.service_ids) + ) `, bookingID) if err != nil { - log.Printf("Failed to fetch services for patch test: %v", err) + log.Printf("Failed to fetch patch tests for booking %s: %v", bookingID, err) } else { defer rows.Close() for rows.Next() { - var serviceID string - var patchTestHours int - if err := rows.Scan(&serviceID, &patchTestHours); err != nil { - log.Printf("Failed to scan service: %v", err) + var patchTestID string + if err := rows.Scan(&patchTestID); err != nil { + log.Printf("Failed to scan patch test: %v", err) continue } + // Update or insert user_patch_tests record _, err := db.DB.Exec(r.Context(), ` - INSERT INTO user_service_patch_tests (user_id, service_id, last_time) + INSERT INTO user_patch_tests (user_id, patch_test_id, tested_at) VALUES ($1, $2, NOW()) - ON CONFLICT (user_id, service_id) DO UPDATE SET last_time = NOW() - `, booking.User.ID, serviceID) + ON CONFLICT (user_id, patch_test_id) DO UPDATE SET tested_at = NOW() + `, booking.User.ID, patchTestID) if err != nil { - log.Printf("Failed to record patch test: %v", err) + log.Printf("Failed to update patch test validity for user %s, patch test %s: %v", booking.User.ID, patchTestID, err) } } } @@ -1599,6 +1662,7 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) { // Return updated booking w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(booking); err != nil { log.Printf("Failed to encode booking response: %v", err) diff --git a/backend/handlers/bookings/manage.go b/backend/handlers/bookings/manage.go index d611462..8de3db4 100644 --- a/backend/handlers/bookings/manage.go +++ b/backend/handlers/bookings/manage.go @@ -429,6 +429,67 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) { return } + // Check patch test requirements for all services + for _, serviceID := range req.ServiceIDs { + // Find patch test for this service + var patchTestID string + var noticeHours int + err := db.DB.QueryRow(r.Context(), ` + SELECT id, notice_duration_hours + FROM patch_tests + WHERE $1 = ANY(service_ids) + `, serviceID).Scan(&patchTestID, ¬iceHours) + + if err == nil { + // Service requires a patch test - check if user has valid record + var testedAt time.Time + err = db.DB.QueryRow(r.Context(), ` + SELECT tested_at + FROM user_patch_tests + WHERE user_id = $1 AND patch_test_id = $2 + `, req.UserID, patchTestID).Scan(&testedAt) + + if err != nil { + // No valid patch test record + http.Error(w, "Patch test required for this service. Please complete a patch test first.", http.StatusBadRequest) + return + } + + // Check if notice period has passed + eligibleFrom := testedAt.Add(time.Duration(noticeHours) * time.Hour) + if req.StartTime.Before(eligibleFrom) { + hoursNeeded := time.Until(eligibleFrom).Hours() + http.Error(w, fmt.Sprintf("Booking time is before the %.0f hour notice period after patch test. Earliest booking: %s", hoursNeeded, eligibleFrom.Format("2006-01-02 15:04")), http.StatusBadRequest) + return + } + + // Check if patch test has expired + var expiryMonths int + err = db.DB.QueryRow(r.Context(), `SELECT expiry_months FROM patch_tests WHERE id = $1`, patchTestID).Scan(&expiryMonths) + if err == nil { + expiresAt := testedAt.AddDate(0, expiryMonths, 0) + if req.StartTime.After(expiresAt) { + http.Error(w, "Your patch test has expired. Please complete a new patch test.", http.StatusBadRequest) + return + } + } + } + } + + // Validate overrides + if req.UserID == "" { + http.Error(w, "User ID is required", http.StatusBadRequest) + return + } + if req.StartTime.IsZero() { + http.Error(w, "Start time is required", http.StatusBadRequest) + return + } + if len(req.ServiceIDs) == 0 { + http.Error(w, "At least one service is required", http.StatusBadRequest) + return + } + // Validate overrides for _, override := range req.ServiceOverrides { if override.ServiceID == "" { diff --git a/init-scripts/init-script.sql b/init-scripts/init-script.sql index 556623a..d988f6e 100644 --- a/init-scripts/init-script.sql +++ b/init-scripts/init-script.sql @@ -155,6 +155,13 @@ CREATE INDEX idx_verification_codes_expires ON verification_codes (expires_at) W -- ======================================= CREATE TABLE patch_tests ( + id CHAR(12) PRIMARY KEY DEFAULT generate_service_id(), + name VARCHAR(100) NOT NULL, + description TEXT, + notice_duration_hours INT NOT NULL DEFAULT 24, + expiry_months INT NOT NULL DEFAULT 6, + service_ids CHAR(12)[] DEFAULT '{}' +); id CHAR(12) PRIMARY KEY DEFAULT generate_service_id(), name VARCHAR(100) NOT NULL, description TEXT, diff --git a/local-dev-2.sh b/local-dev-2.sh index 6e7e424..3836fed 100755 --- a/local-dev-2.sh +++ b/local-dev-2.sh @@ -290,14 +290,14 @@ sleep 1 # 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,"patch_test_duration_hours":0,"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,"patch_test_duration_hours":0,"minimum_age_required":0}' - '{"name":"Luxury Pedicure","description":"Foot soak, scrub, mask, extended massage, and polish.","price":45.00,"duration_minutes":75,"patch_test_duration_hours":0,"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,"patch_test_duration_hours":0,"minimum_age_required":0}' - '{"name":"Gel Polish Removal","description":"Safe removal of existing gel polish.","price":10.00,"duration_minutes":20,"patch_test_duration_hours":0,"minimum_age_required":0}' - '{"name":"Nail Art Add-on","description":"Custom nail art, per two fingers.","price":5.00,"duration_minutes":15,"patch_test_duration_hours":0,"minimum_age_required":0}' - '{"name":"Gel Polish Full Set","description":"Full gel polish application - requires patch test 48h before.","price":45.00,"duration_minutes":60,"patch_test_duration_hours":48,"minimum_age_required":0}' - '{"name":"Luxury Gel Manicure","description":"Premium gel polish with extended massage - requires patch test 48h before.","price":55.00,"duration_minutes":75,"patch_test_duration_hours":48,"minimum_age_required":0}' + '{"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":"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}' ) SERVICE_IDS=() @@ -315,6 +315,26 @@ for svc in "${SERVICES[@]}"; do done echo "${C_GREEN}โœ… Created $success/$total Services${C_RESET}" +# 3b. Create Patch Tests (for gel services that require testing) +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]}") + +# 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 + +echo "${C_GREEN}โœ… Created patch test for gel services${C_RESET}" + # 4. Create Bookings echo -e "\n${C_BLUE}๐Ÿ“… Creating Bookings...${C_RESET}" @@ -485,12 +505,6 @@ if api_post "$BASE_URL/scheduling/exceptional-groups" "$NOV_BREAK" "November Bre 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}" -# --- UPDATE DB PANE STATS --- -if [ -n "$SESSION_NAME" ]; then - echo -e "\nโณ Updating DB pane stats..." - tmux send-keys -t "$SESSION_NAME:0.0" 'SELECT relname AS table_name, n_live_tup AS row_count FROM pg_stat_user_tables ORDER BY table_name\;' -fi - echo -e "\n${C_GREEN}๐ŸŽ‰ Seeding Complete!${C_RESET}" echo -e "${C_YELLOW}Press ENTER to run tests...${C_RESET}" read -r diff --git a/local-dev-2.sh.txt b/local-dev-2.sh.txt deleted file mode 100755 index 56de4ae..0000000 --- a/local-dev-2.sh.txt +++ /dev/null @@ -1,416 +0,0 @@ -#!/usr/bin/env zsh - -SESSION_NAME="crussell-dev" -SEED_SCRIPT="/tmp/seed_data.sh" - -setopt NO_UNSET -setopt PIPE_FAIL -setopt ERR_EXIT - -# --- UI Helpers --- -log_info() { echo "๐Ÿ”น $1" } -log_success() { echo "โœ… $1" } -log_error() { echo "โŒ $1" } -log_step() { echo "โ–ถ๏ธ $1" } - -# --- 1. Environment Setup --- -if [ -f .env ]; then - set -a - source .env - set +a - log_success "Loaded environment variables" -else - log_error ".env file not found!" - exit 1 -fi - -# --- 2. Docker Checks --- -if ! docker info > /dev/null 2>&1; then - log_info "Docker daemon not running. Starting..." - sudo systemctl start docker - sleep 2 - if ! docker info > /dev/null 2>&1; then - log_error "Failed to start Docker." - exit 1 - fi - log_success "Docker started" -fi - -# --- 3. Database Reset --- -log_step "Resetting PostgreSQL..." -docker compose down -v postgres > /dev/null 2>&1 -docker compose up postgres -d > /dev/null 2>&1 -log_success "PostgreSQL reset complete" -sleep 3 - -# --- 4. Tmux Session Setup --- -if tmux has-session -t $SESSION_NAME 2>/dev/null; then - log_info "Killing existing tmux session..." - tmux kill-session -t $SESSION_NAME -fi - -log_step "Starting tmux session '$SESSION_NAME'..." -tmux new-session -d -s $SESSION_NAME -n "Workspace" - -# Pane 0: Database -# Start interactive shell only. Stats will be shown after seeding. -tmux send-keys -t $SESSION_NAME "docker exec -it postgres psql -U myuser -d mydb" Enter -tmux select-pane -t $SESSION_NAME:0.0 -T "DB" - -# Pane 1: Backend (Split Horizontally) -tmux split-window -v -t $SESSION_NAME -tmux send-keys -t $SESSION_NAME "cd backend && go run -tags dev ./main.go" Enter -tmux select-pane -t $SESSION_NAME:0.1 -T "Backend" - -# Pane 2: Frontend (Split Vertically from Backend) -tmux split-window -h -t $SESSION_NAME:0.1 -tmux send-keys -t $SESSION_NAME "cd frontend && npm run dev -- --host" Enter -tmux select-pane -t $SESSION_NAME:0.2 -T "Frontend" - -# Layout configuration -tmux select-layout -t $SESSION_NAME even-vertical -tmux select-pane -t $SESSION_NAME:0.0 - -# --- 5. Seed Script Generation --- -log_step "Generating seed script..." - -cat > $SEED_SCRIPT << 'SEED_EOF' -#!/bin/bash - -# --- Config --- -ADMIN_EMAIL="admin@example.com" -ADMIN_PASS="password" -USER_EMAIL="user@example.com" -USER_PASS="password" -BASE_URL="http://localhost:8080/api" - -# --- Formatting --- -C_RESET=$'\033[0m' -C_GREEN=$'\033[32m' -C_RED=$'\033[31m' -C_BLUE=$'\033[34m' -C_YELLOW=$'\033[33m' - -# --- Global for ID Capture --- -LAST_BOOKING_ID="" - -# --- Helper: API Request --- -# --- Helper: API Request --- -api_post() { - local url="$1" - local data="$2" - local desc="$3" - local token="$4" - - local curl_opts=(-s -w "\n%{http_code}" -X POST -H 'Content-Type: application/json') - [[ -n "$token" ]] && curl_opts+=(-H "Authorization: Bearer $token") - [[ -n "$data" ]] && curl_opts+=(-d "$data") - - local response=$(curl "${curl_opts[@]}" "$url") - local http_code=$(echo "$response" | tail -n1) - 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' - return 0 - else - printf "${C_RED}โŒ Failed: %s (HTTP %s)${C_RESET}\n" "$desc" "$http_code" - printf " Request: %s\n" "$data" - printf " Response: %s\n" "$body" - return 1 - fi -} - -wait_for_backend() { - echo -ne "โณ Waiting for backend..." - for ((i=1; i<=60; i++)); do - if curl -s --connect-timeout 2 http://localhost:8080/api/register > /dev/null 2>&1; then - echo -e "\rโณ Waiting for backend... ${C_GREEN}Ready!${C_RESET}" - return 0 - fi - echo -n "." - sleep 1 - done - echo -e "\rโณ Waiting for backend... ${C_RED}Timed out${C_RESET}" - exit 1 -} - -# --- Main Execution --- -wait_for_backend - -# 1. Register Users -echo -e "\n${C_BLUE}๐Ÿ‘ค Registering Users...${C_RESET}" -success=0 -total=18 - -# 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 - -# 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 - -# 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 - -# 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 -echo "${C_GREEN}โœ… Registered $success/$total Users${C_RESET}" - -# 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 - -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') - - -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 -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,"patch_test_duration_hours":0,"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,"patch_test_duration_hours":0,"minimum_age_required":0}' - '{"name":"Luxury Pedicure","description":"Foot soak, scrub, mask, extended massage, and polish.","price":45.00,"duration_minutes":75,"patch_test_duration_hours":0,"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,"patch_test_duration_hours":0,"minimum_age_required":0}' - '{"name":"Gel Polish Removal","description":"Safe removal of existing gel polish.","price":10.00,"duration_minutes":20,"patch_test_duration_hours":0,"minimum_age_required":0}' - '{"name":"Nail Art Add-on","description":"Custom nail art, per two fingers.","price":5.00,"duration_minutes":15,"patch_test_duration_hours":0,"minimum_age_required":0}' -) - -SERVICE_IDS=() -success=0 -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:]-') - SERVICE_IDS+=("$ID") - success=$((success+1)) - fi -done -echo "${C_GREEN}โœ… Created $success/$total Services${C_RESET}" - -# 4. Create 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_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:]-') - LAST_BOOKING_ID="$id" - return 0 - else - LAST_BOOKING_ID="" - return 1 - fi -} - -get_svc() { echo "${SERVICE_IDS[$1]}"; } - -# Counters -count_past=0 -count_today=0 -count_tomorrow=0 -count_future=0 - -# Array to hold IDs of upcoming bookings for confirmation step -UPCOMING_BOOKING_IDS=() -UPCOMING_BOOKING_NAMES=() - -# --- 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) - - # 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"); - 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}" - -# 5. Confirm Random Half of Upcoming Bookings -echo -e "\n${C_BLUE}๐Ÿ”’ Confirming Random Upcoming Bookings...${C_RESET}" -confirmed_count=0 -total_upcoming=${#UPCOMING_BOOKING_IDS[@]} - -# Small pause to ensure backend is ready after bulk creation -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]}" - - # 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 - # Send proper JSON with empty serviceOverrides array - RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $ADMIN_TOKEN" \ - -d '{"serviceOverrides":[]}' \ - "$BASE_URL/admin/bookings/$id/confirm") - HTTP_CODE=$(echo "$RESPONSE" | tail -n1) - BODY=$(echo "$RESPONSE" | sed '$d') - - if [ "$HTTP_CODE" = "200" ]; then - echo " โœ… Confirmed: $name" - confirmed_count=$((confirmed_count+1)) - else - echo " โš ๏ธ Failed to confirm: $name (HTTP $HTTP_CODE)" - echo " Response: $BODY" - fi - # Small sleep to prevent overwhelming the server - sleep 0.1 - fi -done -echo "${C_GREEN}โœ… Confirmed $confirmed_count upcoming bookings${C_RESET}" - -# 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"]}' - -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}" - -# --- UPDATE DB PANE STATS --- -if [ -n "$SESSION_NAME" ]; then - echo -e "\nโณ Updating DB pane stats..." - tmux send-keys -t "$SESSION_NAME:0.0" 'SELECT relname AS table_name, n_live_tup AS row_count FROM pg_stat_user_tables ORDER BY table_name\;' -fi - -echo -e "\n${C_GREEN}๐ŸŽ‰ Seeding Complete!${C_RESET}" -read -n1 -s -p "Press any key to close this window..." -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 --- -trap 'rm -f $SEED_SCRIPT' EXIT -log_success "Environment ready. Attaching to session..." -tmux attach-session -t $SESSION_NAME