99 lines
3.6 KiB
Go
99 lines
3.6 KiB
Go
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)
|
||
}
|