// Package rules contém os cálculos determinísticos da jornada. Não conhece // banco, HTTP nem Telegram, e por isso é testado sem nenhuma dependência. package rules import ( "errors" "sort" "time" "botpontos/internal/domain" ) var ( ErrNotEnoughPoints = errors.New("pontos insuficientes para o cálculo") ErrNoRemainingWork = errors.New("tempo restante da jornada igual ou menor que zero") ) // CurrentWorkday filtra os pontos da data (AAAA-MM-DD, no fuso loc), ordena e // classifica: mais de quatro pontos é hora extra; horários repetidos ou fora // de ordem tornam o dia inválido. func CurrentWorkday(points []domain.Point, day string, loc *time.Location) domain.Workday { filtered := make([]domain.Point, 0, len(points)) for _, p := range points { if domain.LocalDay(p.OccurredAt, loc) == day { filtered = append(filtered, p) } } sort.Slice(filtered, func(i, j int) bool { return filtered[i].OccurredAt.Before(filtered[j].OccurredAt) }) workday := domain.Workday{Points: filtered} if len(filtered) > 4 { workday.Overtime = true return workday } for i := 1; i < len(filtered); i++ { if !filtered[i].OccurredAt.After(filtered[i-1].OccurredAt) { workday.Invalid = true } } return workday } // Entrada2Time: retorno previsto do almoço = saida1 + almoço. func Entrada2Time(w domain.Workday, lunchMinutes int) (time.Time, error) { if w.Count() < 2 { return time.Time{}, ErrNotEnoughPoints } return w.Exit1().Add(time.Duration(lunchMinutes) * time.Minute), nil } // Saida2Time: saída prevista = entrada2 + (jornada − (saida1 − entrada1)). func Saida2Time(w domain.Workday, dailyWorkMinutes int) (time.Time, error) { if w.Count() < 3 { return time.Time{}, ErrNotEnoughPoints } workedBeforeLunch := w.Exit1().Sub(w.Entry1()) remaining := time.Duration(dailyWorkMinutes)*time.Minute - workedBeforeLunch if remaining <= 0 { return time.Time{}, ErrNoRemainingWork } return w.Entry2().Add(remaining), nil } // ReminderAt é o horário do aviso: o horário previsto menos a antecedência. func ReminderAt(predicted time.Time, leadMinutes int) time.Time { return predicted.Add(-time.Duration(leadMinutes) * time.Minute) } // IsWeekend indica sábado ou domingo no fuso informado. func IsWeekend(t time.Time, loc *time.Location) bool { wd := t.In(loc).Weekday() return wd == time.Saturday || wd == time.Sunday }