Files
Crussell/local-dev-2.sh
T

921 lines
48 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 ---
C_RESET=$'\033[0m'
C_GREEN=$'\033[32m'
C_RED=$'\033[31m'
C_BLUE=$'\033[34m'
C_YELLOW=$'\033[33m'
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"
# --- 3a. Wait for PostgreSQL to be ready ---
log_step "Waiting for PostgreSQL to initialize..."
MAX_STARTUP_ATTEMPTS=30
STARTUP_ATTEMPT=0
while [ $STARTUP_ATTEMPT -lt $MAX_STARTUP_ATTEMPTS ]; do
if docker exec postgres psql -U myuser -d mydb -c "SELECT 1;" > /dev/null 2>&1; then
log_success "PostgreSQL is ready"
break
fi
STARTUP_ATTEMPT=$((STARTUP_ATTEMPT+1))
sleep 1
done
if [ $STARTUP_ATTEMPT -eq $MAX_STARTUP_ATTEMPTS ]; then
log_error "PostgreSQL failed to start after $MAX_STARTUP_ATTEMPTS attempts"
exit 1
fi
# --- 3a-extra. Additional wait for full PostgreSQL initialization ---
log_step "Allowing PostgreSQL to fully initialize..."
sleep 5
# --- 3b. Create Test Database ---
log_step "Setting up test database (crussell_test)..."
docker exec postgres psql -U myuser -d mydb -c "CREATE DATABASE crussell_test;" 2>&1 || log_info "Test database may already exist"
sleep 2
# --- 3c. Seed Test Database Schema ---
log_step "Seeding test database schema..."
docker exec -i postgres psql -U myuser -d crussell_test < init-scripts/init-script.sql 2>&1 | head -5 || true
log_success "Test database schema seeded"
# --- 3d. Verify test database is ready ---
log_step "Verifying test database readiness..."
MAX_ATTEMPTS=10
ATTEMPT=0
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
if docker exec postgres psql -U myuser -d crussell_test -c "SELECT 1;" > /dev/null 2>&1; then
log_success "Test database ready"
break
fi
ATTEMPT=$((ATTEMPT+1))
sleep 1
done
if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
log_error "Test database failed to initialize after $MAX_ATTEMPTS attempts"
exit 1
fi
# --- 3e. Rustfs (wipe data for fresh start) ---
log_step "Wiping Rustfs (S3 storage)..."
docker compose stop rustfs > /dev/null 2>&1 || true
docker compose rm -f rustfs > /dev/null 2>&1 || true
docker volume rm crussell_rustfs_data > /dev/null 2>&1 || true
docker compose up rustfs -d > /dev/null 2>&1
log_success "Rustfs wiped and restarted"
sleep 2
# --- 3f. SabreDAV (start early to create tables) ---
log_step "Starting SabreDAV (CardDAV/CalDAV)..."
docker compose up sabredav -d > /dev/null 2>&1
log_success "SabreDAV started"
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"
# Pass env vars to tmux session (must be after session creation)
tmux set-environment -t $SESSION_NAME POSTGRES_USER "$POSTGRES_USER"
tmux set-environment -t $SESSION_NAME POSTGRES_PASSWORD "$POSTGRES_PASSWORD"
tmux set-environment -t $SESSION_NAME POSTGRES_DB "$POSTGRES_DB"
tmux set-environment -t $SESSION_NAME POSTGRES_HOST "$POSTGRES_HOST"
tmux set-environment -t $SESSION_NAME POSTGRES_PORT "$POSTGRES_PORT"
tmux set-environment -t $SESSION_NAME JWT_SECRET_KEY "$JWT_SECRET_KEY"
tmux set-environment -t $SESSION_NAME S3_ENDPOINT "$S3_ENDPOINT"
tmux set-environment -t $SESSION_NAME S3_PUBLIC_URL "$S3_PUBLIC_URL"
tmux set-environment -t $SESSION_NAME S3_ACCESS_KEY "$S3_ACCESS_KEY"
tmux set-environment -t $SESSION_NAME S3_SECRET_KEY "$S3_SECRET_KEY"
tmux set-environment -t $SESSION_NAME S3_BUCKET "$S3_BUCKET"
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
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)
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"
# Pane 2: Frontend (Split Vertically from Backend)
tmux split-window -h -t $SESSION_NAME:0.1
tmux send-keys -t $SESSION_NAME "set -a; source .env > /dev/null 2>&1; cd frontend && npm run dev -- --host" Enter
tmux select-pane -t $SESSION_NAME:0.2 -T "Frontend"
# Pane 3: Rustfs (S3-compatible storage) - Split from Frontend
tmux split-window -v -t $SESSION_NAME:0.2
tmux send-keys -t $SESSION_NAME "docker logs -f rustfs" Enter
tmux select-pane -t $SESSION_NAME:0.3 -T "Rustfs"
# 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"
# 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'
C_RED=$'\033[31m'
C_BLUE=$'\033[34m'
C_YELLOW=$'\033[33m'
# --- Global for ID Capture ---
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"
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
# 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"
printf " Request: %s\n" "$data"
printf " Response: %s\n" "$body"
return 1
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
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=20
# 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
# 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
# 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
# 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
echo "${C_GREEN}✅ Registered $success/$total Users${C_RESET}"
user_success=$success
user_total=$total
# ===========================================================================
# 2. LOGIN
# ===========================================================================
echo -e "\n${C_BLUE}🔑 Authenticating...${C_RESET}"
ADMIN_TOKEN=$(login "$ADMIN_EMAIL" "$ADMIN_PASS")
if [ -z "$ADMIN_TOKEN" ]; then echo "❌ Admin auth failed"; exit 1; fi
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")
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,"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 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=()
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" && "$ID" =~ ^[0-9a-f-]{8,}$ ]]; then
SERVICE_IDS+=("$ID")
success=$((success+1))
fi
done
echo "${C_GREEN}✅ Created $success/$total Services${C_RESET}"
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}"
# 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]}"
# 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 ')
# 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
# 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}"
local id=$(api_post "$BASE_URL/bookings" "$json" "Book: $name" "$token")
if [[ -n "$id" && "$id" =~ ^[0-9a-f-]{8,}$ ]]; then
LAST_BOOKING_ID="$id"
return 0
else
LAST_BOOKING_ID=""
return 1
fi
}
# 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}"
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<max; i++)); do
local dow
dow=$(TZ=Europe/London date -d "$d" +%w) # 0=Sun, 1=Mon, 6=Sat
# Closed: Sunday (0) or Monday (1)
if [[ "$dow" == "0" || "$dow" == "1" ]]; then
d=$(TZ=Europe/London date -d "$d +1 day" +%Y-%m-%d)
else
echo "$d"
return
fi
done
echo "$d" # fallback
}
# Same but shifts BACKWARDS to find an open day (for past bookings)
open_day_past() {
local d="$1"
local max=7
for ((i=0; i<max; i++)); do
local dow
dow=$(TZ=Europe/London date -d "$d" +%w)
if [[ "$dow" == "0" || "$dow" == "1" ]]; then
d=$(TZ=Europe/London date -d "$d -1 day" +%Y-%m-%d)
else
echo "$d"
return
fi
done
echo "$d"
}
TODAY=$(TZ=Europe/London date +%Y-%m-%d)
TOMORROW=$(open_day "$(TZ=Europe/London date -d "tomorrow" +%Y-%m-%d)")
# Arrays to collect upcoming booking IDs that need notes confirmed
PENDING_BOOKING_IDS=()
PENDING_BOOKING_NAMES=()
# ---------------------------------------------------------------------------
# PAST BOOKINGS — all via admin (bypasses past-time rejection)
# ---------------------------------------------------------------------------
echo -e "\n${C_YELLOW}📅 Creating Past Bookings...${C_RESET}"
count_past=0
# Emma — loyal weekly gel client
for day_offset in 7 14 21 28; do
D=$(open_day_past "$(TZ=Europe/London date -d "today -$day_offset days" +%Y-%m-%d)")
if create_admin_booking "$EMMA_ID" "$(format_london_time "$D" "$SLOT_A")" "[\"$(get_svc 1)\"]" "" "Emma - Gel Manicure $D"; then count_past=$((count_past+1)); fi
done
# Sophie — pedicure every few weeks
for day_offset in 6 20; do
D=$(open_day_past "$(TZ=Europe/London date -d "today -$day_offset days" +%Y-%m-%d)")
if create_admin_booking "$SOPHIE_ID" "$(format_london_time "$D" "$SLOT_C")" "[\"$(get_svc 2)\"]" "" "Sophie - Luxury Pedicure $D"; then count_past=$((count_past+1)); fi
done
# Amelia — classic manicure regular
for day_offset in 5 19; do
D=$(open_day_past "$(TZ=Europe/London date -d "today -$day_offset days" +%Y-%m-%d)")
if create_admin_booking "$AMELIA_ID" "$(format_london_time "$D" "$SLOT_B")" "[\"$(get_svc 0)\"]" "" "Amelia - Classic Manicure $D"; then count_past=$((count_past+1)); fi
done
# Isla — mixed services
D=$(open_day_past "$(TZ=Europe/London date -d "today -3 days" +%Y-%m-%d)")
if create_admin_booking "$ISLA_ID" "$(format_london_time "$D" "$SLOT_A")" "[\"$(get_svc 3)\"]" "" "Isla - Express Mani & Pedi"; then count_past=$((count_past+1)); fi
D=$(open_day_past "$(TZ=Europe/London date -d "today -17 days" +%Y-%m-%d)")
if create_admin_booking "$ISLA_ID" "$(format_london_time "$D" "$SLOT_C")" "[\"$(get_svc 0)\",\"$(get_svc 5)\"]" "" "Isla - Classic + Nail Art"; then count_past=$((count_past+1)); fi
# Lily — gel with nail art combo
D=$(open_day_past "$(TZ=Europe/London date -d "today -10 days" +%Y-%m-%d)")
if create_admin_booking "$LILY_ID" "$(format_london_time "$D" "$SLOT_D")" "[\"$(get_svc 1)\",\"$(get_svc 5)\"]" "" "Lily - Gel + Nail Art"; then count_past=$((count_past+1)); fi
# Ava — occasional pedicure
D=$(open_day_past "$(TZ=Europe/London date -d "today -12 days" +%Y-%m-%d)")
if create_admin_booking "$AVA_ID" "$(format_london_time "$D" "$SLOT_B")" "[\"$(get_svc 2)\"]" "" "Ava - Luxury Pedicure"; then count_past=$((count_past+1)); fi
# Grace — gel client (patch test already recorded)
D=$(open_day_past "$(TZ=Europe/London date -d "today -9 days" +%Y-%m-%d)")
if create_admin_booking "$GRACE_ID" "$(format_london_time "$D" "$SLOT_A")" "[\"$(get_svc 6)\"]" "" "Grace - Gel Full Set"; then count_past=$((count_past+1)); fi
D=$(open_day_past "$(TZ=Europe/London date -d "today -23 days" +%Y-%m-%d)")
if create_admin_booking "$GRACE_ID" "$(format_london_time "$D" "$SLOT_A")" "[\"$(get_svc 7)\"]" "" "Grace - Luxury Gel Manicure"; then count_past=$((count_past+1)); fi
# Primary test user — variety of past bookings
for day_offset in 2 4 8 11 15; do
D=$(open_day_past "$(TZ=Europe/London date -d "today -$day_offset days" +%Y-%m-%d)")
SVC_IDX=$(( (day_offset % 4) ))
if create_admin_booking "$USER_USER_ID" "$(format_london_time "$D" "$SLOT_C")" "[\"$(get_svc $SVC_IDX)\"]" "" "User - Past booking $D"; then count_past=$((count_past+1)); fi
done
# Poppy / Mia — deposit-required past visits
D=$(open_day_past "$(TZ=Europe/London date -d "today -5 days" +%Y-%m-%d)")
if create_admin_booking "$POPPY_ID" "$(format_london_time "$D" "$SLOT_C")" "[\"$(get_svc 0)\"]" "" "Poppy - Past (deposit snapshotted)"; then count_past=$((count_past+1)); fi
D=$(open_day_past "$(TZ=Europe/London date -d "today -13 days" +%Y-%m-%d)")
if create_admin_booking "$MIA_ID" "$(format_london_time "$D" "$SLOT_D")" "[\"$(get_svc 2)\"]" "" "Mia - Past"; then count_past=$((count_past+1)); fi
echo "${C_GREEN}✅ Created $count_past Past Bookings${C_RESET}"
# ---------------------------------------------------------------------------
# TODAY'S BOOKINGS — all via admin (bypass 1-hour advance)
# ---------------------------------------------------------------------------
echo -e "\n${C_YELLOW}📅 Creating Today's Bookings...${C_RESET}"
count_today=0
if create_admin_booking "$EMMA_ID" "$(format_london_time "$TODAY" "$SLOT_A")" "[\"$(get_svc 1)\"]" "" "Emma - Gel Manicure (today)"; then count_today=$((count_today+1)); fi
if create_admin_booking "$SOPHIE_ID" "$(format_london_time "$TODAY" "$SLOT_B")" "[\"$(get_svc 2)\"]" "" "Sophie - Luxury Pedicure (today)"; then count_today=$((count_today+1)); fi
if create_admin_booking "$AMELIA_ID" "$(format_london_time "$TODAY" "$SLOT_C")" "[\"$(get_svc 0)\",\"$(get_svc 5)\"]" "" "Amelia - Classic + Nail Art (today)"; then count_today=$((count_today+1)); fi
if create_admin_booking "$USER_USER_ID" "$(format_london_time "$TODAY" "$SLOT_D")" "[\"$(get_svc 3)\"]" "" "User - Express Mani & Pedi (today)"; then count_today=$((count_today+1)); fi
if create_admin_booking "$ISLA_ID" "$(format_london_time "$TODAY" "$SLOT_A")" "[\"$(get_svc 1)\"]" "" "Isla - Gel Manicure (today)"; then count_today=$((count_today+1)); fi
if create_admin_booking "$GRACE_ID" "$(format_london_time "$TODAY" "$SLOT_D")" "[\"$(get_svc 6)\"]" "" "Grace - Gel Full Set (today)"; then count_today=$((count_today+1)); fi
if create_admin_booking "$LILY_ID" "$(format_london_time "$TODAY" "$SLOT_C")" "[\"$(get_svc 0)\"]" "" "Lily - Classic Manicure (today)"; then count_today=$((count_today+1)); fi
echo "${C_GREEN}✅ Created $count_today Today's Bookings${C_RESET}"
# ---------------------------------------------------------------------------
# TOMORROW'S BOOKINGS — all via admin (avoids booking_status enum bug in
# POST /bookings). We simulate the pending/notes scenario by creating 2
# bookings via user tokens (which intentionally hit the notes path) and
# leaving them unconfirmed for admin review.
# ---------------------------------------------------------------------------
echo -e "\n${C_YELLOW}📅 Creating Tomorrow's Bookings...${C_RESET}"
count_tomorrow=0
# Bulk of tomorrow's bookings — admin-created, all confirmed
if create_admin_booking "$EMMA_ID" "$(format_london_time "$TOMORROW" "$SLOT_A")" "[\"$(get_svc 1)\"]" "" "Emma - Gel Manicure (tomorrow)"; then count_tomorrow=$((count_tomorrow+1)); fi
if create_admin_booking "$SOPHIE_ID" "$(format_london_time "$TOMORROW" "$SLOT_C")" "[\"$(get_svc 2)\"]" "" "Sophie - Luxury Pedicure (tomorrow)"; then count_tomorrow=$((count_tomorrow+1)); fi
if create_admin_booking "$AMELIA_ID" "$(format_london_time "$TOMORROW" "$SLOT_D")" "[\"$(get_svc 3)\"]" "" "Amelia - Express Mani & Pedi (tomorrow)"; then count_tomorrow=$((count_tomorrow+1)); fi
if create_admin_booking "$USER_USER_ID" "$(format_london_time "$TOMORROW" "$SLOT_B")" "[\"$(get_svc 0)\"]" "" "User - Classic Manicure (tomorrow)"; then count_tomorrow=$((count_tomorrow+1)); fi
if create_admin_booking "$LILY_ID" "$(format_london_time "$TOMORROW" "$SLOT_C")" "[\"$(get_svc 2)\"]" "" "Lily - Luxury Pedicure (tomorrow)"; then count_tomorrow=$((count_tomorrow+1)); fi
# Pending bookings — admin-created (avoids enum bug), then force to pending via DB.
# Notes are included so the admin UI shows the request context.
if create_admin_booking "$ISLA_ID" "$(format_london_time "$TOMORROW" "$SLOT_E")" "[\"$(get_svc 1)\",\"$(get_svc 5)\"]" "Would love a French tip look with some floral art on the ring fingers if possible?" "Isla - Gel + Nail Art (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+=("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
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" )
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]}"
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
# 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
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
skipped_count=0
sleep 1
for ((i=0; i<${#PENDING_BOOKING_IDS[@]}; i++)); do
id="${PENDING_BOOKING_IDS[$i]}"
[[ -z "$id" ]] && continue
[[ ! "$id" =~ ^[0-9a-f-]{8,}$ ]] && continue
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 '{"service_overrides":[],"notes":""}' \
"$BASE_URL/admin/bookings/$id/confirm")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
[ "$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 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
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
echo "${C_GREEN}✅ Simulated $cancel_count cancellations${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}"
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 ..
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")
TOTAL_TESTS=${TOTAL_TESTS:-0}
PASSED_TESTS=${PASSED_TESTS:-0}
FAILED_TESTS=${FAILED_TESTS:-0}
SKIPPED_TESTS=${SKIPPED_TESTS:-0}
echo ""
if [ "$SKIPPED_TESTS" -gt 0 ] 2>/dev/null; then
echo -e "${C_YELLOW}⚠️ Tests Skipped: $SKIPPED_TESTS${C_RESET}"
fi
if [ "$FAILED_TESTS" -gt 0 ] 2>/dev/null; then
echo -e "${C_RED}❌ Tests Failed: $FAILED_TESTS/$TOTAL_TESTS failed${C_RESET}"
echo ""
echo -e "${C_RED}--- Failed Tests ---${C_RESET}"
echo "$TEST_OUTPUT" | grep "^--- FAIL" | head -20
else
echo -e "${C_GREEN}✅ All Tests Passed: $PASSED_TESTS/$TOTAL_TESTS${C_RESET}"
fi
echo ""
echo -e "${C_YELLOW}Press ENTER to restore 4-pane layout...${C_RESET}"
read -r
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..."
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