Projeto bot pontos

This commit is contained in:
2026-10-23 22:31:02 -03:00
parent 29614855ff
commit b35e1a7d69
116 changed files with 14523 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
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)
}