Files
bot-pontos/backend/internal/rules/validation.go
2026-10-23 22:31:02 -03:00

99 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}