diff --git a/backend/handlers/scheduling/default-hours.go b/backend/handlers/scheduling/default-hours.go index bdfde58..5a4a24c 100644 --- a/backend/handlers/scheduling/default-hours.go +++ b/backend/handlers/scheduling/default-hours.go @@ -275,6 +275,11 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) { log.Printf("Failed to cleanup old reservations: %v", err) } + // Anonymize stale guest accounts (6+ months after last booking) + if err := AnonymizeStaleGuestAccounts(r.Context()); err != nil { + log.Printf("Failed to anonymize stale guest accounts: %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`) @@ -456,21 +461,21 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) { 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", + // 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}) + } } - day.Slots = subtractTimeSlots(day.Slots, []TimeSlot{lateNightBlock}) - } - } } else { // Admins: keep blockers visible for warning display if dayBlockers, ok := blockerMap[day.Date]; ok { diff --git a/backend/handlers/scheduling/time-blockers.go b/backend/handlers/scheduling/time-blockers.go index 7454fa5..1036090 100644 --- a/backend/handlers/scheduling/time-blockers.go +++ b/backend/handlers/scheduling/time-blockers.go @@ -336,6 +336,8 @@ func CheckTimeBlockerOverlap(ctx context.Context, startTime, endTime time.Time) // CleanupOldReservations deletes expired reservations: // - Logged-in (RESERVATION:user): older than 1 hour // - Anonymous (RESERVATION:anon): older than 10 minutes +// - Admin walk-in (RESERVATION:admin:walkin:%): older than 10 minutes +// - Admin call-in (RESERVATION:admin:callin:%): older than 1 hour func CleanupOldReservations(ctx context.Context) error { oneHourAgo := time.Now().Add(-1 * time.Hour) tenMinutesAgo := time.Now().Add(-10 * time.Minute) @@ -344,6 +346,36 @@ func CleanupOldReservations(ctx context.Context) error { DELETE FROM time_blockers WHERE (description LIKE 'RESERVATION:user:%' AND created_at < $1) OR (description LIKE 'RESERVATION:anon:%' AND created_at < $2) - `, oneHourAgo, tenMinutesAgo) + OR (description LIKE 'RESERVATION:admin:walkin:%' AND created_at < $2) + OR (description LIKE 'RESERVATION:admin:callin:%' AND created_at < $3) + `, oneHourAgo, tenMinutesAgo, oneHourAgo) + return err +} + +// AnonymizeStaleGuestAccounts anonymizes personal data for guest accounts +// whose last booking was more than 6 months ago (UK GDPR storage limitation). +// Financial records (bookings, payments) remain intact — only PII is scrubbed. +// Active/pending bookings are excluded so the salon can still contact the guest. +func AnonymizeStaleGuestAccounts(ctx context.Context) error { + _, err := db.DB.Exec(ctx, ` + UPDATE users SET + n_first_name = 'Guest', + n_last_name = 'Anonymized', + email = 'anon-' || id || '@anon.invalid', + phone = '000000000000', + date_of_birth = '1900-01-01', + updated_at = NOW() + WHERE account_role = 'guest' + AND id NOT IN ( + SELECT user_id FROM bookings WHERE status IN ('pending', 'confirmed') + ) + AND id IN ( + SELECT user_id + FROM bookings + WHERE user_id IS NOT NULL + GROUP BY user_id + HAVING MAX(start_time) < NOW() - INTERVAL '6 months' + ) + `) return err }