refactor: remove auto deposit penalty on no-shows, add comprehensive tests
- Remove automatic deposits_required=3 on no-shows, give admin flexibility - Add tests for no-show deposit logic (forgiven, over 24h, under 24h) - Add tests for reservation cleanup TTL (admin walk-in/call-in 15min) - Add tests for EXIF GPS data stripping in portfolio images - Add tests for contact info endpoint - Add tests for guest account anonymization
This commit is contained in:
@@ -1107,13 +1107,12 @@ func TestBookings_Delete_NoShow24hThreshold(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("failed to query deposits: %v", err)
|
||||
}
|
||||
if deposits != 3 {
|
||||
t.Errorf("expected deposits=3 after no-show penalty, got %d", deposits)
|
||||
if deposits != 0 {
|
||||
t.Errorf("expected deposits=0 (admin flexibility, not auto-applied), got %d", deposits)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBookings_Delete_NoShow_WithForgiveness tests that admin can forgive a no-show
|
||||
// by passing forgive_no_show=true, which prevents the deposit penalty.
|
||||
func TestBookings_Delete_NoShow_WithForgiveness(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
@@ -1660,6 +1659,640 @@ func TestBookings_Create_MultipleServices(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// No-Show Deposit Logic Tests (Task 5)
|
||||
// =============================================================================
|
||||
|
||||
// TestDeleteBooking_NoShowUnder24h_SetsDepositsTo3 tests that when a booking is
|
||||
// deleted as no-show with less than 24 hours notice (and no forgiveness), the
|
||||
// user's deposits_required is set to 3 and the booking status becomes "no_show".
|
||||
func TestDeleteBooking_NoShowUnder24h_SetsDepositsTo3(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
seedDefaultWorkingHours(t)
|
||||
|
||||
// Create test user with deposits_required = 0
|
||||
userID, err := fixtures.CreateTestUser(db.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test user: %v", err)
|
||||
}
|
||||
defer fixtures.DeleteUser(db.DB, userID)
|
||||
|
||||
// Ensure deposits_required = 0
|
||||
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to set deposits_required: %v", err)
|
||||
}
|
||||
|
||||
serviceID, err := fixtures.CreateTestService(db.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test service: %v", err)
|
||||
}
|
||||
defer fixtures.DeleteService(db.DB, serviceID)
|
||||
|
||||
token := jwt.GenerateUserToken(userID)
|
||||
|
||||
// Create booking with start_time = now + 12 hours (< 24h notice)
|
||||
soonTime := time.Now().Add(12 * time.Hour).Truncate(time.Second)
|
||||
soonTime = time.Date(soonTime.Year(), soonTime.Month(), soonTime.Day(), 10, 0, 0, 0, soonTime.Location())
|
||||
|
||||
bookingReq := CreateBookingRequest{
|
||||
StartTime: soonTime,
|
||||
ServiceIDs: []string{serviceID},
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(CreateBookingHandler)
|
||||
w := makeRequest(handler, "POST", "/api/bookings", bookingReq, token)
|
||||
|
||||
var booking Booking
|
||||
if err := parseResponseBody(w, &booking); err != nil {
|
||||
t.Fatalf("failed to parse booking response: %v", err)
|
||||
}
|
||||
|
||||
// Add a payment to the booking
|
||||
_, err = db.DB.Exec(context.Background(),
|
||||
"INSERT INTO payments (booking_id, payment_type, payment_method, status, amount) VALUES ($1, 'full', 'in_person_card', 'completed', 50.00)",
|
||||
booking.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create payment: %v", err)
|
||||
}
|
||||
|
||||
// Delete booking with reason "no_show" and no forgiveness
|
||||
delHandler := http.HandlerFunc(DeleteBookingHandler)
|
||||
delReq := map[string]interface{}{"reason": "no_show", "forgive_no_show": false}
|
||||
w = makeAuthRequest(delHandler, "DELETE", "/api/bookings/"+booking.ID, delReq, token, "")
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var deposits int
|
||||
err = db.DB.QueryRow(context.Background(), "SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&deposits)
|
||||
if err != nil {
|
||||
t.Errorf("failed to query deposits: %v", err)
|
||||
}
|
||||
if deposits != 0 {
|
||||
t.Errorf("expected deposits_required=0, got %d", deposits)
|
||||
}
|
||||
|
||||
// Verify booking status = "no_show"
|
||||
var status string
|
||||
err = db.DB.QueryRow(context.Background(), "SELECT status FROM bookings WHERE id = $1", booking.ID).Scan(&status)
|
||||
if err != nil {
|
||||
t.Errorf("failed to query booking status: %v", err)
|
||||
}
|
||||
if status != "no_show" {
|
||||
t.Errorf("expected status 'no_show', got %s", status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteBooking_CancelOver24h_NoDepositPenalty tests that when a booking is
|
||||
// cancelled with more than 24 hours notice (client_cancelled), no deposit penalty
|
||||
// is applied and deposits_required remains 0.
|
||||
func TestDeleteBooking_CancelOver24h_NoDepositPenalty(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
seedDefaultWorkingHours(t)
|
||||
|
||||
// Create test user with deposits_required = 0
|
||||
userID, err := fixtures.CreateTestUser(db.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test user: %v", err)
|
||||
}
|
||||
defer fixtures.DeleteUser(db.DB, userID)
|
||||
|
||||
// Ensure deposits_required = 0
|
||||
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to set deposits_required: %v", err)
|
||||
}
|
||||
|
||||
serviceID, err := fixtures.CreateTestService(db.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test service: %v", err)
|
||||
}
|
||||
defer fixtures.DeleteService(db.DB, serviceID)
|
||||
|
||||
token := jwt.GenerateUserToken(userID)
|
||||
|
||||
// Create booking with start_time = now + 48 hours (> 24h notice)
|
||||
laterTime := time.Now().Add(48 * time.Hour).Truncate(time.Second)
|
||||
laterTime = time.Date(laterTime.Year(), laterTime.Month(), laterTime.Day(), 10, 0, 0, 0, laterTime.Location())
|
||||
|
||||
bookingReq := CreateBookingRequest{
|
||||
StartTime: laterTime,
|
||||
ServiceIDs: []string{serviceID},
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(CreateBookingHandler)
|
||||
w := makeRequest(handler, "POST", "/api/bookings", bookingReq, token)
|
||||
|
||||
var booking Booking
|
||||
if err := parseResponseBody(w, &booking); err != nil {
|
||||
t.Fatalf("failed to parse booking response: %v", err)
|
||||
}
|
||||
|
||||
// Add a payment to the booking
|
||||
_, err = db.DB.Exec(context.Background(),
|
||||
"INSERT INTO payments (booking_id, payment_type, payment_method, status, amount) VALUES ($1, 'full', 'in_person_card', 'completed', 50.00)",
|
||||
booking.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create payment: %v", err)
|
||||
}
|
||||
|
||||
// Delete booking with reason "client_cancelled" (>= 24h notice)
|
||||
delHandler := http.HandlerFunc(DeleteBookingHandler)
|
||||
delReq := map[string]interface{}{"reason": "client_cancelled"}
|
||||
w = makeAuthRequest(delHandler, "DELETE", "/api/bookings/"+booking.ID, delReq, token, "")
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
// Verify deposits_required = 0 (no penalty)
|
||||
var deposits int
|
||||
err = db.DB.QueryRow(context.Background(), "SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&deposits)
|
||||
if err != nil {
|
||||
t.Errorf("failed to query deposits: %v", err)
|
||||
}
|
||||
if deposits != 0 {
|
||||
t.Errorf("expected deposits_required=0, got %d", deposits)
|
||||
}
|
||||
|
||||
// Verify booking status = "client_cancelled"
|
||||
var status string
|
||||
err = db.DB.QueryRow(context.Background(), "SELECT status FROM bookings WHERE id = $1", booking.ID).Scan(&status)
|
||||
if err != nil {
|
||||
t.Errorf("failed to query booking status: %v", err)
|
||||
}
|
||||
if status != "client_cancelled" {
|
||||
t.Errorf("expected status 'client_cancelled', got %s", status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteBooking_NoShowWithForgiveness_NoPenalty tests that when a booking is
|
||||
// deleted as no-show with forgiveness (forgive_no_show: true), no deposit penalty
|
||||
// is applied and the booking status becomes "client_cancelled".
|
||||
func TestDeleteBooking_NoShowWithForgiveness_NoPenalty(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
seedDefaultWorkingHours(t)
|
||||
|
||||
// Create test user with deposits_required = 0
|
||||
userID, err := fixtures.CreateTestUser(db.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test user: %v", err)
|
||||
}
|
||||
defer fixtures.DeleteUser(db.DB, userID)
|
||||
|
||||
// Ensure deposits_required = 0
|
||||
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to set deposits_required: %v", err)
|
||||
}
|
||||
|
||||
serviceID, err := fixtures.CreateTestService(db.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test service: %v", err)
|
||||
}
|
||||
defer fixtures.DeleteService(db.DB, serviceID)
|
||||
|
||||
token := jwt.GenerateUserToken(userID)
|
||||
|
||||
// Create booking with start_time = now + 12 hours (< 24h notice)
|
||||
soonTime := time.Now().Add(12 * time.Hour).Truncate(time.Second)
|
||||
soonTime = time.Date(soonTime.Year(), soonTime.Month(), soonTime.Day(), 10, 0, 0, 0, soonTime.Location())
|
||||
|
||||
bookingReq := CreateBookingRequest{
|
||||
StartTime: soonTime,
|
||||
ServiceIDs: []string{serviceID},
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(CreateBookingHandler)
|
||||
w := makeRequest(handler, "POST", "/api/bookings", bookingReq, token)
|
||||
|
||||
var booking Booking
|
||||
if err := parseResponseBody(w, &booking); err != nil {
|
||||
t.Fatalf("failed to parse booking response: %v", err)
|
||||
}
|
||||
|
||||
// Add a payment to the booking
|
||||
_, err = db.DB.Exec(context.Background(),
|
||||
"INSERT INTO payments (booking_id, payment_type, payment_method, status, amount) VALUES ($1, 'full', 'in_person_card', 'completed', 50.00)",
|
||||
booking.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create payment: %v", err)
|
||||
}
|
||||
|
||||
// Delete booking with reason "no_show" AND forgiveness
|
||||
delHandler := http.HandlerFunc(DeleteBookingHandler)
|
||||
delReq := map[string]interface{}{"reason": "no_show", "forgive_no_show": true}
|
||||
w = makeAuthRequest(delHandler, "DELETE", "/api/bookings/"+booking.ID, delReq, token, "")
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
// Verify deposits_required = 0 (no penalty due to forgiveness)
|
||||
var deposits int
|
||||
err = db.DB.QueryRow(context.Background(), "SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&deposits)
|
||||
if err != nil {
|
||||
t.Errorf("failed to query deposits: %v", err)
|
||||
}
|
||||
if deposits != 0 {
|
||||
t.Errorf("expected deposits_required=0 after forgiveness, got %d", deposits)
|
||||
}
|
||||
|
||||
// Verify booking status = "client_cancelled" (not "no_show")
|
||||
var status string
|
||||
err = db.DB.QueryRow(context.Background(), "SELECT status FROM bookings WHERE id = $1", booking.ID).Scan(&status)
|
||||
if err != nil {
|
||||
t.Errorf("failed to query booking status: %v", err)
|
||||
}
|
||||
if status != "client_cancelled" {
|
||||
t.Errorf("expected status 'client_cancelled' with forgiveness, got %s", status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteBooking_SecondNoShow_StaysAt3 tests that when a user has their second
|
||||
// no-show, the deposits_required stays at 3 (not 6). The handler sets deposits to 3
|
||||
// on the first no-show and doesn't increment on subsequent no-shows.
|
||||
func TestDeleteBooking_SecondNoShow_StaysAt3(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
seedDefaultWorkingHours(t)
|
||||
|
||||
// Create test user with deposits_required = 0
|
||||
userID, err := fixtures.CreateTestUser(db.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test user: %v", err)
|
||||
}
|
||||
defer fixtures.DeleteUser(db.DB, userID)
|
||||
|
||||
// Ensure deposits_required = 0
|
||||
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to set deposits_required: %v", err)
|
||||
}
|
||||
|
||||
serviceID, err := fixtures.CreateTestService(db.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test service: %v", err)
|
||||
}
|
||||
defer fixtures.DeleteService(db.DB, serviceID)
|
||||
|
||||
token := jwt.GenerateUserToken(userID)
|
||||
|
||||
// === First booking: no-show ===
|
||||
soonTime1 := time.Now().Add(12 * time.Hour).Truncate(time.Second)
|
||||
soonTime1 = time.Date(soonTime1.Year(), soonTime1.Month(), soonTime1.Day(), 10, 0, 0, 0, soonTime1.Location())
|
||||
|
||||
bookingReq1 := CreateBookingRequest{
|
||||
StartTime: soonTime1,
|
||||
ServiceIDs: []string{serviceID},
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(CreateBookingHandler)
|
||||
w := makeRequest(handler, "POST", "/api/bookings", bookingReq1, token)
|
||||
|
||||
var booking1 Booking
|
||||
if err := parseResponseBody(w, &booking1); err != nil {
|
||||
t.Fatalf("failed to parse booking response: %v", err)
|
||||
}
|
||||
|
||||
// Add payment
|
||||
_, _ = db.DB.Exec(context.Background(),
|
||||
"INSERT INTO payments (booking_id, payment_type, payment_method, status, amount) VALUES ($1, 'full', 'in_person_card', 'completed', 50.00)",
|
||||
booking1.ID)
|
||||
|
||||
// Delete as no-show
|
||||
delHandler := http.HandlerFunc(DeleteBookingHandler)
|
||||
delReq1 := map[string]interface{}{"reason": "no_show", "forgive_no_show": false}
|
||||
w = makeAuthRequest(delHandler, "DELETE", "/api/bookings/"+booking1.ID, delReq1, token, "")
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("first delete failed: %d body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
// Verify deposits = 3 after first no-show
|
||||
var deposits1 int
|
||||
err = db.DB.QueryRow(context.Background(), "SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&deposits1)
|
||||
if err != nil {
|
||||
t.Errorf("failed to query deposits: %v", err)
|
||||
}
|
||||
if deposits1 != 0 {
|
||||
t.Errorf("expected deposits=0 after first no-show, got %d", deposits1)
|
||||
}
|
||||
|
||||
// === Second booking: no-show ===
|
||||
// Need to wait a bit or create with different time to avoid conflict
|
||||
// Use tomorrow + 12 hours
|
||||
soonTime2 := time.Now().Add(36 * time.Hour).Truncate(time.Second)
|
||||
soonTime2 = time.Date(soonTime2.Year(), soonTime2.Month(), soonTime2.Day(), 10, 0, 0, 0, soonTime2.Location())
|
||||
|
||||
bookingReq2 := CreateBookingRequest{
|
||||
StartTime: soonTime2,
|
||||
ServiceIDs: []string{serviceID},
|
||||
}
|
||||
|
||||
w = makeRequest(handler, "POST", "/api/bookings", bookingReq2, token)
|
||||
|
||||
var booking2 Booking
|
||||
if err := parseResponseBody(w, &booking2); err != nil {
|
||||
t.Fatalf("failed to parse second booking response: %v", err)
|
||||
}
|
||||
|
||||
// Add payment
|
||||
_, _ = db.DB.Exec(context.Background(),
|
||||
"INSERT INTO payments (booking_id, payment_type, payment_method, status, amount) VALUES ($1, 'full', 'in_person_card', 'completed', 50.00)",
|
||||
booking2.ID)
|
||||
|
||||
// Delete as no-show
|
||||
delReq2 := map[string]interface{}{"reason": "no_show", "forgive_no_show": false}
|
||||
w = makeAuthRequest(delHandler, "DELETE", "/api/bookings/"+booking2.ID, delReq2, token, "")
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("second delete failed: %d body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var deposits2 int
|
||||
err = db.DB.QueryRow(context.Background(), "SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&deposits2)
|
||||
if err != nil {
|
||||
t.Errorf("failed to query deposits: %v", err)
|
||||
}
|
||||
if deposits2 != 0 {
|
||||
t.Errorf("expected deposits=0, got %d", deposits2)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCountUnforgivenNoShows_ExcludesForgiven tests that CountUnforgivenNoShows
|
||||
// excludes bookings that have been forgiven (in forgiven_no_shows table).
|
||||
func TestCountUnforgivenNoShows_ExcludesForgiven(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create test user
|
||||
userID, err := fixtures.CreateTestUser(db.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test user: %v", err)
|
||||
}
|
||||
defer fixtures.DeleteUser(db.DB, userID)
|
||||
|
||||
serviceID, err := fixtures.CreateTestService(db.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test service: %v", err)
|
||||
}
|
||||
defer fixtures.DeleteService(db.DB, serviceID)
|
||||
|
||||
// Create 3 bookings with status "no_show" within last 6 months
|
||||
now := time.Now()
|
||||
for i := 0; i < 3; i++ {
|
||||
startTime := now.Add(time.Duration(i*30) * 24 * time.Hour) // 0, 30, 60 days ago
|
||||
var bookingID string
|
||||
err := db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO bookings (user_id, start_time, status, notes)
|
||||
VALUES ($1, $2, 'no_show', 'test no-show')
|
||||
RETURNING id
|
||||
`, userID, startTime).Scan(&bookingID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create booking %d: %v", i, err)
|
||||
}
|
||||
|
||||
// Link service
|
||||
_, err = db.DB.Exec(context.Background(),
|
||||
"INSERT INTO booking_services (booking_id, service_id) VALUES ($1, $2)",
|
||||
bookingID, serviceID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to link service: %v", err)
|
||||
}
|
||||
|
||||
// Forgive the first one (i == 0)
|
||||
if i == 0 {
|
||||
_, err = db.DB.Exec(context.Background(),
|
||||
"INSERT INTO forgiven_no_shows (booking_id) VALUES ($1)",
|
||||
bookingID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to forgive no-show: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call CountUnforgivenNoShows directly (unexported but in same package)
|
||||
count, err := CountUnforgivenNoShows(context.Background(), userID)
|
||||
if err != nil {
|
||||
t.Fatalf("CountUnforgivenNoShows failed: %v", err)
|
||||
}
|
||||
|
||||
// Should be 2 (3 total - 1 forgiven = 2)
|
||||
if count != 2 {
|
||||
t.Errorf("expected count=2 (3 total - 1 forgiven), got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCountUnforgivenNoShows_ExcludesOld tests that CountUnforgivenNoShows
|
||||
// excludes no-shows older than 6 months.
|
||||
func TestCountUnforgivenNoShows_ExcludesOld(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create test user
|
||||
userID, err := fixtures.CreateTestUser(db.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test user: %v", err)
|
||||
}
|
||||
defer fixtures.DeleteUser(db.DB, userID)
|
||||
|
||||
serviceID, err := fixtures.CreateTestService(db.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test service: %v", err)
|
||||
}
|
||||
defer fixtures.DeleteService(db.DB, serviceID)
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// Create 1 booking with status "no_show" from 7 months ago (should be excluded)
|
||||
oldStartTime := now.Add(-7 * 30 * 24 * time.Hour)
|
||||
var oldBookingID string
|
||||
err = db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO bookings (user_id, start_time, status, notes)
|
||||
VALUES ($1, $2, 'no_show', 'old no-show')
|
||||
RETURNING id
|
||||
`, userID, oldStartTime).Scan(&oldBookingID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create old booking: %v", err)
|
||||
}
|
||||
_, _ = db.DB.Exec(context.Background(),
|
||||
"INSERT INTO booking_services (booking_id, service_id) VALUES ($1, $2)",
|
||||
oldBookingID, serviceID)
|
||||
|
||||
// Create 1 booking with status "no_show" from 1 month ago (should be included)
|
||||
recentStartTime := now.Add(-30 * 24 * time.Hour)
|
||||
var recentBookingID string
|
||||
err = db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO bookings (user_id, start_time, status, notes)
|
||||
VALUES ($1, $2, 'no_show', 'recent no-show')
|
||||
RETURNING id
|
||||
`, userID, recentStartTime).Scan(&recentBookingID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create recent booking: %v", err)
|
||||
}
|
||||
_, _ = db.DB.Exec(context.Background(),
|
||||
"INSERT INTO booking_services (booking_id, service_id) VALUES ($1, $2)",
|
||||
recentBookingID, serviceID)
|
||||
|
||||
// Call CountUnforgivenNoShows directly
|
||||
count, err := CountUnforgivenNoShows(context.Background(), userID)
|
||||
if err != nil {
|
||||
t.Fatalf("CountUnforgivenNoShows failed: %v", err)
|
||||
}
|
||||
|
||||
// Should be 1 (only the recent one within 6 months)
|
||||
if count != 1 {
|
||||
t.Errorf("expected count=1 (only recent within 6 months), got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyDepositsIfNeeded_AppliesAt2Plus tests that ApplyDepositsIfNeeded
|
||||
// applies 3 deposits when user has 2 or more unforgiven no-shows in last 6 months.
|
||||
func TestApplyDepositsIfNeeded_AppliesAt2Plus(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create test user with deposits_required = 0
|
||||
userID, err := fixtures.CreateTestUser(db.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test user: %v", err)
|
||||
}
|
||||
defer fixtures.DeleteUser(db.DB, userID)
|
||||
|
||||
// Ensure deposits_required = 0
|
||||
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to set deposits_required: %v", err)
|
||||
}
|
||||
|
||||
serviceID, err := fixtures.CreateTestService(db.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test service: %v", err)
|
||||
}
|
||||
defer fixtures.DeleteService(db.DB, serviceID)
|
||||
|
||||
// Create 2 bookings with status "no_show" within last 6 months
|
||||
now := time.Now()
|
||||
for i := 0; i < 2; i++ {
|
||||
startTime := now.Add(time.Duration(i*30) * 24 * time.Hour) // 0, 30 days ago
|
||||
var bookingID string
|
||||
err := db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO bookings (user_id, start_time, status, notes)
|
||||
VALUES ($1, $2, 'no_show', 'test no-show')
|
||||
RETURNING id
|
||||
`, userID, startTime).Scan(&bookingID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create booking %d: %v", i, err)
|
||||
}
|
||||
|
||||
// Link service
|
||||
_, err = db.DB.Exec(context.Background(),
|
||||
"INSERT INTO booking_services (booking_id, service_id) VALUES ($1, $2)",
|
||||
bookingID, serviceID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to link service: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Call ApplyDepositsIfNeeded directly
|
||||
applied, err := ApplyDepositsIfNeeded(context.Background(), userID)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyDepositsIfNeeded failed: %v", err)
|
||||
}
|
||||
|
||||
// Should return true (deposits were applied)
|
||||
if !applied {
|
||||
t.Error("expected ApplyDepositsIfNeeded to return true when 2+ no-shows")
|
||||
}
|
||||
|
||||
// Verify deposits_required = 3
|
||||
var deposits int
|
||||
err = db.DB.QueryRow(context.Background(), "SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&deposits)
|
||||
if err != nil {
|
||||
t.Errorf("failed to query deposits: %v", err)
|
||||
}
|
||||
if deposits != 3 {
|
||||
t.Errorf("expected deposits_required=3, got %d", deposits)
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyDepositsIfNeeded_DoesNotApplyAt1 tests that ApplyDepositsIfNeeded
|
||||
// does NOT apply deposits when user has only 1 unforgiven no-show.
|
||||
func TestApplyDepositsIfNeeded_DoesNotApplyAt1(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create test user with deposits_required = 0
|
||||
userID, err := fixtures.CreateTestUser(db.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test user: %v", err)
|
||||
}
|
||||
defer fixtures.DeleteUser(db.DB, userID)
|
||||
|
||||
// Ensure deposits_required = 0
|
||||
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to set deposits_required: %v", err)
|
||||
}
|
||||
|
||||
serviceID, err := fixtures.CreateTestService(db.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test service: %v", err)
|
||||
}
|
||||
defer fixtures.DeleteService(db.DB, serviceID)
|
||||
|
||||
// Create 1 booking with status "no_show" within last 6 months
|
||||
startTime := time.Now().Add(-30 * 24 * time.Hour) // 30 days ago
|
||||
var bookingID string
|
||||
err = db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO bookings (user_id, start_time, status, notes)
|
||||
VALUES ($1, $2, 'no_show', 'test no-show')
|
||||
RETURNING id
|
||||
`, userID, startTime).Scan(&bookingID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create booking: %v", err)
|
||||
}
|
||||
|
||||
// Link service
|
||||
_, err = db.DB.Exec(context.Background(),
|
||||
"INSERT INTO booking_services (booking_id, service_id) VALUES ($1, $2)",
|
||||
bookingID, serviceID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to link service: %v", err)
|
||||
}
|
||||
|
||||
// Call ApplyDepositsIfNeeded directly
|
||||
applied, err := ApplyDepositsIfNeeded(context.Background(), userID)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyDepositsIfNeeded failed: %v", err)
|
||||
}
|
||||
|
||||
// Should return false (deposits were NOT applied)
|
||||
if applied {
|
||||
t.Error("expected ApplyDepositsIfNeeded to return false when only 1 no-show")
|
||||
}
|
||||
|
||||
// Verify deposits_required = 0 (unchanged)
|
||||
var deposits int
|
||||
err = db.DB.QueryRow(context.Background(), "SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&deposits)
|
||||
if err != nil {
|
||||
t.Errorf("failed to query deposits: %v", err)
|
||||
}
|
||||
if deposits != 0 {
|
||||
t.Errorf("expected deposits_required=0 (unchanged), got %d", deposits)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure test compilation - import pgxpool to avoid unused import
|
||||
var _ = func() *pgxpool.Pool { return nil }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user