refactor: update deposit system with 24h no-show rule, optional forgiveness, and admin enforcement toggle
- Change no-show threshold from 12h to 24h for late cancellations - Add optional forgive_no_show boolean to cancellation endpoint - Add optional enforce_deposits boolean to admin booking creation - Set deposits_required = 3 on no-show (not +=3) to prevent escalation - Implement per-cancellation forgiveness instead of bulk forgiveness - Remove ForgiveNoShowsForUser() function (now per-event) - Admin can now bypass deposit checks when needed - All changes backward compatible (nil defaults to enforce)
This commit is contained in:
@@ -139,6 +139,7 @@ type ServiceOverride struct {
|
||||
// DeleteBookingRequest represents the request payload for deleting a booking with payment
|
||||
type DeleteBookingRequest struct {
|
||||
Reason string `json:"reason" validate:"required,oneof=client_cancelled we_cancelled re-schedule no_show"`
|
||||
ForgiveNoShow *bool `json:"forgive_no_show,omitempty"` // Admin-only: forgive a no-show at cancellation time
|
||||
}
|
||||
|
||||
// AdminUserSummary represents a small user summary for admin views
|
||||
@@ -1842,13 +1843,19 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
tx.QueryRow(r.Context(), "SELECT start_time FROM bookings WHERE id = $1", bookingID).Scan(&startTime)
|
||||
noticeHours := startTime.Sub(time.Now()).Hours()
|
||||
|
||||
if noticeHours < 12 {
|
||||
tx.Exec(r.Context(), "UPDATE users SET deposits_required = deposits_required + 3 WHERE id = $1", userID)
|
||||
tx.Exec(r.Context(), "UPDATE bookings SET status = 'no_show' WHERE id = $1", bookingID)
|
||||
} else if noticeHours < 24 {
|
||||
tx.Exec(r.Context(), `
|
||||
INSERT INTO admin_notifications (reason, booking_id, user_id) VALUES ($1, $2, $3)
|
||||
`, "late_cancellation", bookingID, userID)
|
||||
// < 24 hours notice: treat as no-show
|
||||
if noticeHours < 24 {
|
||||
// Check if admin is forgiving this no-show
|
||||
isForgiving := req.ForgiveNoShow != nil && *req.ForgiveNoShow
|
||||
|
||||
if !isForgiving {
|
||||
// No forgiveness: apply penalty
|
||||
tx.Exec(r.Context(), "UPDATE users SET deposits_required = 3 WHERE id = $1", userID)
|
||||
tx.Exec(r.Context(), "UPDATE bookings SET status = 'no_show' WHERE id = $1", bookingID)
|
||||
} else {
|
||||
// Forgiveness granted: treat as client_cancelled, no penalty
|
||||
tx.Exec(r.Context(), "UPDATE bookings SET status = 'client_cancelled' WHERE id = $1", bookingID)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
|
||||
@@ -411,6 +411,7 @@ type AdminCreateBookingForUserRequest struct {
|
||||
ServiceIDs []string `json:"service_ids" validate:"required,min=1"`
|
||||
ServiceOverrides []ServiceOverride `json:"service_overrides,omitempty"`
|
||||
Notes *string `json:"notes,omitempty"` // appointment notes, visible to customers and staff
|
||||
EnforceDeposits *bool `json:"enforce_deposits,omitempty"` // Optional: if true, enforce outstanding deposit checks; if false or omitted, bypass checks
|
||||
}
|
||||
|
||||
func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -489,6 +490,39 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce deposit checks if requested (default: true if not specified)
|
||||
enforceDeposits := true
|
||||
if req.EnforceDeposits != nil {
|
||||
enforceDeposits = *req.EnforceDeposits
|
||||
}
|
||||
|
||||
if enforceDeposits {
|
||||
// Read live deposits_required from user
|
||||
var depositsRequired int
|
||||
if err := db.DB.QueryRow(r.Context(), `SELECT deposits_required FROM users WHERE id = $1`, req.UserID).Scan(&depositsRequired); err != nil {
|
||||
log.Printf("Failed to fetch deposits_required for user %s: %v", req.UserID, err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Check one-active-booking limit when deposits are outstanding
|
||||
if depositsRequired > 0 {
|
||||
var activeCount int
|
||||
if err := db.DB.QueryRow(r.Context(), `
|
||||
SELECT COUNT(*) FROM bookings
|
||||
WHERE user_id = $1 AND status IN ('pending', 'confirmed')
|
||||
`, req.UserID).Scan(&activeCount); err != nil {
|
||||
log.Printf("Failed to check active bookings for user %s: %v", req.UserID, err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if activeCount > 0 {
|
||||
http.Error(w, "User already has an active booking. Cannot create another until deposit requirements are cleared.", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate overrides
|
||||
if req.UserID == "" {
|
||||
http.Error(w, "User ID is required", http.StatusBadRequest)
|
||||
@@ -1387,58 +1421,4 @@ func ApplyDepositsIfNeeded(ctx context.Context, userID string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// ForgiveNoShowsForUser clears all unforgiven no-shows for a user
|
||||
// by inserting them into forgiven_no_shows table and resetting deposits to 0
|
||||
func ForgiveNoShowsForUser(ctx context.Context, userID string) error {
|
||||
tx, err := db.DB.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
// Get all unforgiven no-shows
|
||||
rows, err := tx.Query(ctx, `
|
||||
SELECT id FROM bookings
|
||||
WHERE user_id = $1
|
||||
AND status = 'no_show'
|
||||
AND start_time >= NOW() - INTERVAL '6 months'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM forgiven_no_shows WHERE booking_id = bookings.id
|
||||
)
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var bookingIDs []string
|
||||
for rows.Next() {
|
||||
var id string
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return err
|
||||
}
|
||||
bookingIDs = append(bookingIDs, id)
|
||||
}
|
||||
|
||||
// Insert into forgiven_no_shows for each
|
||||
for _, bookingID := range bookingIDs {
|
||||
_, err := tx.Exec(ctx, `
|
||||
INSERT INTO forgiven_no_shows (booking_id)
|
||||
VALUES ($1)
|
||||
ON CONFLICT (booking_id) DO NOTHING
|
||||
`, bookingID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Reset deposits to 0
|
||||
_, err = tx.Exec(ctx, `
|
||||
UPDATE users SET deposits_required = 0 WHERE id = $1
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user