74 lines
2.3 KiB
Go
74 lines
2.3 KiB
Go
// 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
|
||
}
|