135 lines
4.7 KiB
Go
135 lines
4.7 KiB
Go
package rules
|
||
|
||
import (
|
||
"time"
|
||
|
||
"botpontos/internal/domain"
|
||
)
|
||
|
||
// Tick alinha um instante ao relógio no intervalo informado (ex.: :00, :15,
|
||
// :30, :45 para 15 min). Fusos com deslocamento em horas inteiras ou em
|
||
// múltiplos de 15 min mantêm o alinhamento local.
|
||
func Tick(t time.Time, interval time.Duration) time.Time {
|
||
return t.Truncate(interval)
|
||
}
|
||
|
||
// NextTick é o próximo instante alinhado depois de t.
|
||
func NextTick(t time.Time, interval time.Duration) time.Time {
|
||
return Tick(t, interval).Add(interval)
|
||
}
|
||
|
||
// InWindow indica se t está dentro da janela de consulta do dia (inclusive nas pontas).
|
||
func InWindow(t time.Time, w domain.QueryWindow, day string, loc *time.Location) bool {
|
||
start := w.Start.On(day, loc)
|
||
end := w.End.On(day, loc)
|
||
return !t.Before(start) && !t.After(end)
|
||
}
|
||
|
||
// PlanInput reúne o que a reconciliação sabe sobre um usuário depois de uma
|
||
// consulta bem-sucedida à PontoMais.
|
||
type PlanInput struct {
|
||
Tick time.Time // instante alinhado do ciclo
|
||
Day string // AAAA-MM-DD no fuso do usuário
|
||
Location *time.Location
|
||
Window domain.QueryWindow
|
||
Workday domain.Workday
|
||
Resumed bool // o dia foi retomado manualmente (9.12)
|
||
Saida2At *time.Time // scheduled_at do lembrete de saida2, se agendado
|
||
Reconciliation time.Duration
|
||
Slow time.Duration // consultas lentas (60 min)
|
||
ExitConfirmFrom time.Duration // início da janela de confirmação antes do lembrete (20 min)
|
||
ExitConfirmTo time.Duration // fim da janela de confirmação antes do lembrete (5 min)
|
||
}
|
||
|
||
// Plan é a nova situação do dia e a próxima consulta (nil = nenhuma hoje).
|
||
type Plan struct {
|
||
Status domain.DayStatus
|
||
NextCheckAt *time.Time
|
||
}
|
||
|
||
// PlanNextCheck implementa a frequência adaptativa da seção 9.12:
|
||
// - 0 pontos antes da confirmação: próximo tick;
|
||
// - 0 pontos depois da confirmação: de hora em hora até o fim das reverificações;
|
||
// - 1 ou 2 pontos: próximo tick;
|
||
// - 3 pontos: de hora em hora + uma consulta de confirmação antes do lembrete de saida2;
|
||
// - 4 ou mais pontos: dia encerrado.
|
||
func PlanNextCheck(in PlanInput) Plan {
|
||
end := in.Window.End.On(in.Day, in.Location)
|
||
within := func(t time.Time) *time.Time {
|
||
if t.After(end) {
|
||
return nil
|
||
}
|
||
return &t
|
||
}
|
||
quick := in.Tick.Add(in.Reconciliation)
|
||
slow := in.Tick.Add(in.Slow)
|
||
|
||
w := in.Workday
|
||
switch {
|
||
case w.Overtime || w.Count() >= 4:
|
||
return Plan{Status: domain.DayFinished}
|
||
|
||
case w.Invalid:
|
||
// Dado inconsistente: continua consultando, mas devagar, para não poluir a auditoria.
|
||
status := domain.DayWorking
|
||
if w.Count() == 0 {
|
||
status = domain.DayAwaiting
|
||
}
|
||
return Plan{Status: status, NextCheckAt: within(slow)}
|
||
|
||
case w.Count() == 3:
|
||
if in.Saida2At == nil {
|
||
return Plan{Status: domain.DayWorking, NextCheckAt: within(slow)}
|
||
}
|
||
confirmStart := in.Saida2At.Add(-in.ExitConfirmFrom)
|
||
if !in.Tick.Before(confirmStart) {
|
||
// Esta já foi a consulta de confirmação (ou passou dela): nada mais a fazer hoje.
|
||
return Plan{Status: domain.DayWorking}
|
||
}
|
||
next := slow
|
||
if confirm := ConfirmTick(*in.Saida2At, in.Reconciliation, in.ExitConfirmTo); confirm.Before(next) {
|
||
next = confirm
|
||
}
|
||
return Plan{Status: domain.DayWorking, NextCheckAt: within(next)}
|
||
|
||
case w.Count() >= 1:
|
||
return Plan{Status: domain.DayWorking, NextCheckAt: within(quick)}
|
||
}
|
||
|
||
// 0 pontos.
|
||
if in.Resumed {
|
||
// Dia retomado manualmente: consulta no ritmo normal até o fim da janela.
|
||
return Plan{Status: domain.DayWorking, NextCheckAt: within(quick)}
|
||
}
|
||
deadline := in.Window.DayCheckDeadline.On(in.Day, in.Location)
|
||
if in.Tick.Before(deadline) {
|
||
return Plan{Status: domain.DayAwaiting, NextCheckAt: within(quick)}
|
||
}
|
||
recheckUntil := in.Window.NoPointsRecheckUntil.On(in.Day, in.Location)
|
||
if slow.After(recheckUntil) {
|
||
return Plan{Status: domain.DayNotWorking}
|
||
}
|
||
return Plan{Status: domain.DayNoPoints, NextCheckAt: within(slow)}
|
||
}
|
||
|
||
// ConfirmTick é o último tick antes de (lembrete − to). Com to = 5 min e
|
||
// tick de 15 min, ele cai sempre entre 20 e 5 minutos antes do lembrete.
|
||
func ConfirmTick(reminderAt time.Time, reconciliation, to time.Duration) time.Time {
|
||
return Tick(reminderAt.Add(-to).Add(-time.Nanosecond), reconciliation)
|
||
}
|
||
|
||
// ResumeNextCheck é a próxima consulta depois de um dia ser retomado: o próximo
|
||
// tick dentro da janela, ou o início da janela se ela ainda não começou.
|
||
func ResumeNextCheck(now time.Time, w domain.QueryWindow, day string, loc *time.Location, reconciliation time.Duration) *time.Time {
|
||
start := w.Start.On(day, loc)
|
||
end := w.End.On(day, loc)
|
||
next := NextTick(now, reconciliation)
|
||
if next.Before(start) {
|
||
next = start
|
||
}
|
||
if next.After(end) {
|
||
return nil
|
||
}
|
||
return &next
|
||
}
|