Implement business logic changes: deposits, no-shows, reservations, approval workflow
CHANGES: Phase 1: Schema - Change deposits_required default from 3 to 0 for new users - Add forgiven_no_shows table to track forgiven no-show bookings Phase 2: No-Show Logic (manage.go) - CountUnforgivenNoShows(): Count unforgiven no-shows in 6-month period - ApplyDepositsIfNeeded(): Auto-apply 3 deposits if 2+ no-shows detected - ForgiveNoShowsForUser(): Clear no-shows and reset deposits on full payment Phase 3: Slot Reservation System - Add CleanupOldReservations() to delete 1h+ old reservation blockers - Call cleanup in GetAvailableHours() on each availability check - Delete existing user reservation before creating new booking Phase 4: Minimum Advance Time - Changed from 48h (deposit-only) to 1h (all users) - Now universally enforced at booking creation time Phase 5: Notes-Based Approval Workflow - If booking has notes (not empty) → status = 'pending' (needs approval) - If no notes → status = 'confirmed' (auto-approved) - Uses CASE statement in INSERT for status determination Phase 6: Late Night Lock - After 22:00, non-admin users cannot book next morning before 11:00 - Implemented in GetAvailableHours() via artificial blocker subtraction - Admin users see all times (no restriction) Phase 7: Admin Notifications - Notify admin if booking has notes OR is for same day - All qualifying bookings trigger notification for admin review VERIFICATION: ✓ Build passes: go build -tags dev ./main.go succeeds ✓ All 7 phases implemented as per dev-approved plan ✓ No breaking changes to existing schemas ✓ Backward compatible with existing booking flow
This commit is contained in:
@@ -270,6 +270,11 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
|
||||
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, ukLocation)
|
||||
end = time.Date(end.Year(), end.Month(), end.Day(), 23, 59, 59, 999999999, ukLocation)
|
||||
|
||||
// Clean up old reservations (older than 1 hour)
|
||||
if err := CleanupOldReservations(r.Context()); err != nil {
|
||||
log.Printf("Failed to cleanup old reservations: %v", err)
|
||||
}
|
||||
|
||||
// Load default hours
|
||||
defaultMap := map[int]DefaultHours{}
|
||||
defRows, _ := db.DB.Query(r.Context(), `SELECT weekday, start_time::text, end_time::text, is_open FROM working_hours`)
|
||||
@@ -450,6 +455,22 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
|
||||
if dayBlockers, ok := blockerMap[day.Date]; ok {
|
||||
day.Slots = subtractTimeSlots(day.Slots, dayBlockers)
|
||||
}
|
||||
|
||||
// Late night lock: after 22:00, block next morning 00:00-11:00 for non-admin users
|
||||
now := time.Now()
|
||||
if !isAdmin && now.Hour() >= 22 {
|
||||
// Check if this is tomorrow's date
|
||||
tomorrow := now.AddDate(0, 0, 1)
|
||||
tomorrowStr := tomorrow.Format("2006-01-02")
|
||||
if day.Date == tomorrowStr {
|
||||
// Add a fake blocker for 00:00-11:00
|
||||
lateNightBlock := TimeSlot{
|
||||
StartTime: "00:00",
|
||||
EndTime: "11:00",
|
||||
}
|
||||
day.Slots = subtractTimeSlots(day.Slots, []TimeSlot{lateNightBlock})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Admins: keep blockers visible for warning display
|
||||
if dayBlockers, ok := blockerMap[day.Date]; ok {
|
||||
|
||||
@@ -333,3 +333,15 @@ func CheckTimeBlockerOverlap(ctx context.Context, startTime, endTime time.Time)
|
||||
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
// CleanupOldReservations deletes reservations (time_blockers with RESERVATION: description prefix)
|
||||
// that are older than 1 hour.
|
||||
func CleanupOldReservations(ctx context.Context) error {
|
||||
oneHourAgo := time.Now().Add(-1 * time.Hour)
|
||||
_, err := db.DB.Exec(ctx, `
|
||||
DELETE FROM time_blockers
|
||||
WHERE description LIKE 'RESERVATION:%'
|
||||
AND created_at < $1
|
||||
`, oneHourAgo)
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user