refactor: migrate patch test schema from service-level to dedicated tables
- Remove patch_test_duration_hours from services table - Add new patch_tests table with service_ids array, notice_duration_hours, expiry_months - Add new user_patch_tests table linking users to patch_tests with tested_at - Update services handler to check patch_tests.service_ids for eligibility - Update booking creation to validate patch test requirements (24h notice, 6mo expiry) - Update booking completion to extend patch test validity (reset tested_at) - Update admin handlers for new patch test CRUD operations - Update test fixtures and test cases for new schema - Update seeding script to create patch_tests and link to gel services
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
Reference in New Issue
Block a user