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,134 @@
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
}