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:
2026-02-24 22:17:50 +00:00
parent f59595eeec
commit c6fe9e92a7
5 changed files with 173 additions and 443 deletions
+61
View File
@@ -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, &noticeHours)
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 == "" {