package rules import ( "time" "botpontos/internal/domain" ) // EffectiveWindow devolve a janela do usuário (exceção) ou a global (9.14). func EffectiveWindow(global domain.QueryWindow, schedule domain.WorkSchedule) (window domain.QueryWindow, custom bool) { if schedule.Window != nil { return *schedule.Window, true } return global, false } // ValidateWindow aplica as regras da seção 9.13. prefix identifica o campo // na resposta (ex.: "window." no formulário do usuário). func ValidateWindow(w domain.QueryWindow, reconciliation time.Duration, prefix string) domain.ValidationErrors { var errs domain.ValidationErrors step := int(reconciliation / time.Minute) fields := []struct { name string value domain.ClockTime }{ {"query_window_start", w.Start}, {"day_check_deadline", w.DayCheckDeadline}, {"no_points_recheck_until", w.NoPointsRecheckUntil}, {"query_window_end", w.End}, } if step > 0 { for _, f := range fields { if int(f.value)%step != 0 { errs.Add(prefix+f.name, "precisa ser múltiplo de "+formatStep(step)+" (ex.: :00, :15, :30, :45)") } } } if !(w.Start < w.DayCheckDeadline && w.DayCheckDeadline < w.NoPointsRecheckUntil && w.NoPointsRecheckUntil < w.End) { errs.Add(prefix+"query_window", "a ordem precisa ser início < confirmação < fim das reverificações < fim") } if step > 0 && int(w.DayCheckDeadline-w.Start) < step { errs.Add(prefix+"day_check_deadline", "precisa ser pelo menos "+formatStep(step)+" depois do início, para haver duas verificações") } if int(w.End-w.Start) < 4*60 { errs.Add(prefix+"query_window_end", "a janela precisa ter pelo menos 4 horas") } return errs } // ValidateSchedule aplica as regras da seção 9.8 e, se houver exceção de // janela, as da 9.13. global é a janela usada quando o usuário não tem exceção. func ValidateSchedule(s domain.WorkSchedule, global domain.QueryWindow, reconciliation time.Duration) domain.ValidationErrors { var errs domain.ValidationErrors if s.LunchMinutes < 15 || s.LunchMinutes > 180 { errs.Add("lunch_minutes", "precisa estar entre 15 e 180") } if s.DailyWorkMinutes < 60 || s.DailyWorkMinutes > 720 { errs.Add("daily_work_minutes", "precisa estar entre 60 e 720") } if s.Entrada2LeadMinutes < 0 || s.Entrada2LeadMinutes > 60 { errs.Add("entrada2_lead_minutes", "precisa estar entre 0 e 60") } if s.Saida2LeadMinutes < 0 || s.Saida2LeadMinutes > 60 { errs.Add("saida2_lead_minutes", "precisa estar entre 0 e 60") } step := int(reconciliation / time.Minute) if step > 0 && s.LunchMinutes-s.Entrada2LeadMinutes < step { errs.Add("lunch_minutes", "almoço − antecedência de entrada2 precisa ser pelo menos "+formatStep(step)+ ", para a saída do almoço ser detectada antes do lembrete") } if s.Window != nil { errs = append(errs, ValidateWindow(*s.Window, reconciliation, "window.")...) } return errs } // ScheduleFitsInterval indica se a jornada continua válida para o intervalo de // reconciliação atual (verificado na inicialização do worker, 9.8). func ScheduleFitsInterval(s domain.WorkSchedule, reconciliation time.Duration) bool { step := int(reconciliation / time.Minute) return s.LunchMinutes-s.Entrada2LeadMinutes >= step } func ValidateTimezone(name string) error { if name == "" { return domain.ValidationErrors{{Field: "timezone", Message: "obrigatório"}} } if _, err := time.LoadLocation(name); err != nil { return domain.ValidationErrors{{Field: "timezone", Message: "fuso inválido (use um nome IANA, ex.: America/Sao_Paulo)"}} } return nil } func formatStep(minutes int) string { return FormatMinutesShort(minutes) }