Refactor patch test system and add booking edit requests

- Replace patch_test_duration_hours on services with separate
  patch_tests table
- Add user_patch_tests table to track user patch test records
- Add booking edit request system: users can request time changes
- Add admin handlers to list, approve, and reject edit requests
- Add validation to prevent editing completed/cancelled bookings
- Add overlap and closed-day checks for booking edits
This commit is contained in:
2026-02-24 17:23:28 +00:00
parent 89d848ee72
commit f59595eeec
11 changed files with 906 additions and 255 deletions
+20 -22
View File
@@ -30,12 +30,11 @@ func TestAdminServices_Create(t *testing.T) {
handler := http.HandlerFunc(services.CreateServiceHandler)
createReq := services.CreateServiceRequest{
Name: "Test Manicure",
Description: stringPtr("A test manicure service"),
Price: 35.00,
DurationMinutes: 45,
PatchTestDurationHours: 0,
MinimumAgeRequired: 16,
Name: "Test Manicure",
Description: stringPtr("A test manicure service"),
Price: 35.00,
DurationMinutes: 45,
MinimumAgeRequired: 16,
}
w := makeAdminRequest(handler, "POST", "/api/admin/services", createReq)
@@ -66,11 +65,11 @@ func TestAdminServices_List(t *testing.T) {
// Insert test services
_, err := db.DB.Exec(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES
('Manicure', 'Basic manicure', 25.00, 30, true, 0, 0),
('Pedicure', 'Basic pedicure', 30.00, 45, false, 0, 0),
('Gel Polish', 'Gel polish service', 40.00, 60, true, 48, 16)
('Manicure', 'Basic manicure', 25.00, 30, true, 0),
('Pedicure', 'Basic pedicure', 30.00, 45, false, 0),
('Gel Polish', 'Gel polish service', 40.00, 60, true, 16)
`)
if err != nil {
t.Fatalf("failed to create services: %v", err)
@@ -116,8 +115,8 @@ func TestAdminServices_Toggle(t *testing.T) {
// Create a service
var serviceID string
err := db.DB.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES ('Test Service', 'A test service', 50.00, 60, true, 0, 16)
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES ('Test Service', 'A test service', 50.00, 60, true, 16)
RETURNING id
`).Scan(&serviceID)
if err != nil {
@@ -164,8 +163,8 @@ func TestAdminServices_Delete(t *testing.T) {
// Create a service
var serviceID string
err := db.DB.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES ('Test Service', 'A test service', 50.00, 60, true, 0, 16)
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES ('Test Service', 'A test service', 50.00, 60, true, 16)
RETURNING id
`).Scan(&serviceID)
if err != nil {
@@ -206,12 +205,11 @@ func TestAdminServices_NonAdmin(t *testing.T) {
// Test CREATE - should get 403 when using middleware
createHandler := mw.RequireAdmin(http.HandlerFunc(services.CreateServiceHandler))
createReq := services.CreateServiceRequest{
Name: "Test Service",
Description: stringPtr("Test"),
Price: 50.00,
DurationMinutes: 60,
PatchTestDurationHours: 0,
MinimumAgeRequired: 16,
Name: "Test Service",
Description: stringPtr("Test"),
Price: 50.00,
DurationMinutes: 60,
MinimumAgeRequired: 16,
}
w := makeUserRequest(createHandler, "POST", "/api/admin/services", createReq)
if w.Code != http.StatusForbidden {
@@ -228,8 +226,8 @@ func TestAdminServices_NonAdmin(t *testing.T) {
// Test TOGGLE - should get 403 when using middleware
var serviceID string
err = db.DB.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES ('Test Service', 'A test service', 50.00, 60, true, 0, 16)
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES ('Test Service', 'A test service', 50.00, 60, true, 16)
RETURNING id
`).Scan(&serviceID)
if err != nil {
+83 -35
View File
@@ -117,17 +117,45 @@ func TestAdminUsers_PatchTests_Eligible(t *testing.T) {
// Create services - some with patch test, some without
_, err = db.DB.Exec(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES
('Basic Manicure', 'Basic manicure', 25.00, 30, true, 0, 0),
('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 48, 16),
('Luxury Gel Manicure', 'Luxury gel', 55.00, 75, true, 48, 16),
('Inactive Service', 'Inactive', 30.00, 30, false, 48, 16)
('Basic Manicure', 'Basic manicure', 25.00, 30, true, 0),
('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 16),
('Luxury Gel Manicure', 'Luxury gel', 55.00, 75, true, 16),
('Inactive Service', 'Inactive', 30.00, 30, false, 16)
`)
if err != nil {
t.Fatalf("failed to create services: %v", err)
}
// Get service IDs for patch test services
var gelPolishID, luxuryGelID string
err = db.DB.QueryRow(context.Background(), "SELECT id FROM services WHERE name = 'Gel Polish Full Set'").Scan(&gelPolishID)
if err != nil {
t.Fatalf("failed to get gel polish service ID: %v", err)
}
err = db.DB.QueryRow(context.Background(), "SELECT id FROM services WHERE name = 'Luxury Gel Manicure'").Scan(&luxuryGelID)
if err != nil {
t.Fatalf("failed to get luxury gel service ID: %v", err)
}
// Create patch tests that link to these services
_, err = db.DB.Exec(context.Background(), `
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
VALUES ('Gel Allergy Test', 'Patch test for gel products', 24, 6, $1)
`, []string{gelPolishID})
if err != nil {
t.Fatalf("failed to create patch test for gel polish: %v", err)
}
_, err = db.DB.Exec(context.Background(), `
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
VALUES ('Luxury Gel Test', 'Patch test for luxury gel', 24, 6, $1)
`, []string{luxuryGelID})
if err != nil {
t.Fatalf("failed to create patch test for luxury gel: %v", err)
}
handler := http.HandlerFunc(user.GetEligiblePatchTestServicesHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID+"/patch-tests/eligible", nil)
@@ -140,7 +168,7 @@ func TestAdminUsers_PatchTests_Eligible(t *testing.T) {
t.Fatalf("failed to unmarshal response: %v", err)
}
// Should return 2 services (the two with patch test duration > 0 that are active)
// Should return 2 services (the two with patch tests that are active)
if len(response) != 2 {
t.Errorf("expected 2 eligible services, got %d. body: %s", len(response), w.Body.String())
}
@@ -161,11 +189,11 @@ func TestAdminUsers_PatchTests_Eligible_WithExisting(t *testing.T) {
t.Fatalf("failed to create user: %v", err)
}
// Create services with patch test
// Create services
var serviceID1, serviceID2 string
err = db.DB.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES ('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 48, 16)
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES ('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 16)
RETURNING id
`).Scan(&serviceID1)
if err != nil {
@@ -173,19 +201,38 @@ func TestAdminUsers_PatchTests_Eligible_WithExisting(t *testing.T) {
}
err = db.DB.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES ('Luxury Gel Manicure', 'Luxury gel', 55.00, 75, true, 48, 16)
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES ('Luxury Gel Manicure', 'Luxury gel', 55.00, 75, true, 16)
RETURNING id
`).Scan(&serviceID2)
if err != nil {
t.Fatalf("failed to create service 2: %v", err)
}
// Add one patch test for the user
// Create patch tests
var patchTestID1 string
err = db.DB.QueryRow(context.Background(), `
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
VALUES ('Gel Allergy Test', 'Patch test for gel products', 24, 6, $1)
RETURNING id
`, []string{serviceID1}).Scan(&patchTestID1)
if err != nil {
t.Fatalf("failed to create patch test 1: %v", err)
}
_, err = db.DB.Exec(context.Background(), `
INSERT INTO user_service_patch_tests (user_id, service_id, last_time)
VALUES ($1, $2, NOW())
`, userID, serviceID1)
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
VALUES ('Luxury Gel Test', 'Patch test for luxury gel', 24, 6, $1)
`, []string{serviceID2})
if err != nil {
t.Fatalf("failed to create patch test 2: %v", err)
}
// Add one patch test for the user (valid - within expiry)
_, err = db.DB.Exec(context.Background(), `
INSERT INTO user_patch_tests (user_id, patch_test_id, tested_at)
VALUES ($1, $2, NOW() - INTERVAL '2 months')
`, userID, patchTestID1)
if err != nil {
t.Fatalf("failed to add patch test: %v", err)
}
@@ -227,20 +274,31 @@ func TestAdminUsers_AddPatchTest(t *testing.T) {
t.Fatalf("failed to create user: %v", err)
}
// Create service with patch test
// Create a service
var serviceID string
err = db.DB.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES ('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 48, 16)
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES ('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 16)
RETURNING id
`).Scan(&serviceID)
if err != nil {
t.Fatalf("failed to create service: %v", err)
}
// Create a patch test that links to this service
var patchTestID string
err = db.DB.QueryRow(context.Background(), `
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
VALUES ('Gel Allergy Test', 'Patch test for gel products', 24, 6, $1)
RETURNING id
`, []string{serviceID}).Scan(&patchTestID)
if err != nil {
t.Fatalf("failed to create patch test: %v", err)
}
handler := http.HandlerFunc(user.AddPatchTestHandler)
reqBody := user.AddPatchTestRequest{ServiceID: serviceID}
reqBody := user.AddPatchTestRequest{PatchTestID: patchTestID}
w := makeAdminRequest(handler, "POST", "/api/admin/users/"+userID+"/patch-tests", reqBody)
if w.Code != http.StatusCreated {
@@ -250,8 +308,8 @@ func TestAdminUsers_AddPatchTest(t *testing.T) {
// Verify patch test was added
var count int
err = db.DB.QueryRow(context.Background(), `
SELECT COUNT(*) FROM user_service_patch_tests WHERE user_id = $1 AND service_id = $2
`, userID, serviceID).Scan(&count)
SELECT COUNT(*) FROM user_patch_tests WHERE user_id = $1 AND patch_test_id = $2
`, userID, patchTestID).Scan(&count)
if err != nil {
t.Fatalf("failed to check patch test: %v", err)
}
@@ -261,7 +319,7 @@ func TestAdminUsers_AddPatchTest(t *testing.T) {
}
}
func TestAdminUsers_AddPatchTest_InvalidService(t *testing.T) {
func TestAdminUsers_AddPatchTest_InvalidPatchTest(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
@@ -276,20 +334,10 @@ func TestAdminUsers_AddPatchTest_InvalidService(t *testing.T) {
t.Fatalf("failed to create user: %v", err)
}
// Create service without patch test requirement
var serviceID string
err = db.DB.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES ('Basic Manicure', 'Basic manicure', 25.00, 30, true, 0, 0)
RETURNING id
`).Scan(&serviceID)
if err != nil {
t.Fatalf("failed to create service: %v", err)
}
handler := http.HandlerFunc(user.AddPatchTestHandler)
reqBody := user.AddPatchTestRequest{ServiceID: serviceID}
// Try to add a non-existent patch test
reqBody := user.AddPatchTestRequest{PatchTestID: "nonexist123"}
w := makeAdminRequest(handler, "POST", "/api/admin/users/"+userID+"/patch-tests", reqBody)
if w.Code != http.StatusBadRequest {
@@ -344,7 +392,7 @@ func TestAdminUsers_NonAdmin(t *testing.T) {
// Test add patch test - should get 403 when using middleware
addHandler := mw.RequireAdmin(http.HandlerFunc(user.AddPatchTestHandler))
w = makeUserRequest(addHandler, "POST", "/api/admin/users/"+targetUserID+"/patch-tests", map[string]string{"service_id": "some-service-id"})
w = makeUserRequest(addHandler, "POST", "/api/admin/users/"+targetUserID+"/patch-tests", map[string]string{"patch_test_id": "some-test-id"})
if w.Code != http.StatusForbidden {
t.Errorf("ADD: expected status 403, got %d", w.Code)
}