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,112 @@
// Package feriados consulta a API feriadosapi.com, endpoint por cidade
// (GET /feriados/cidade/{ibge}?ano=AAAA), que devolve os feriados nacionais e
// os municipais do ano.
package feriados
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"botpontos/internal/domain"
)
// apiDateLayout é o formato das datas devolvidas pela API (dd/mm/aaaa).
const apiDateLayout = "02/01/2006"
type Client struct {
http *http.Client
baseURL string
apiKey string
location string // código IBGE da cidade
}
func NewClient(baseURL, apiKey, location string) *Client {
return &Client{
http: &http.Client{Timeout: 10 * time.Second},
baseURL: strings.TrimRight(strings.TrimSpace(baseURL), "/"),
apiKey: strings.TrimSpace(apiKey),
location: strings.TrimSpace(location),
}
}
func (c *Client) Configured() bool {
return c.baseURL != "" && c.apiKey != "" && c.location != ""
}
type response struct {
Feriados []struct {
Data string `json:"data"`
Nome string `json:"nome"`
} `json:"feriados"`
}
// FetchYear devolve os feriados do ano para a cidade configurada.
func (c *Client) FetchYear(ctx context.Context, year int) ([]domain.Holiday, error) {
if !c.Configured() {
return nil, domain.ErrNotConfigured
}
if _, err := strconv.Atoi(c.location); err != nil {
return nil, fmt.Errorf("BP_HOLIDAY_LOCATION deve ser o código IBGE da cidade (ex.: 3106200), não %q", c.location)
}
endpoint, err := url.Parse(c.baseURL + "/feriados/cidade/" + url.PathEscape(c.location))
if err != nil {
return nil, fmt.Errorf("URL da API de feriados: %w", err)
}
query := endpoint.Query()
query.Set("ano", strconv.Itoa(year))
endpoint.RawQuery = query.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+c.apiKey)
resp, err := c.http.Do(req)
if err != nil {
var urlErr *url.Error
if errors.As(err, &urlErr) {
err = urlErr.Err
}
return nil, fmt.Errorf("consultar API de feriados: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return nil, fmt.Errorf("API de feriados retornou HTTP %d", resp.StatusCode)
}
var payload response
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, fmt.Errorf("resposta da API de feriados ilegível: %w", err)
}
found := map[string]string{}
for _, item := range payload.Feriados {
parsed, err := time.Parse(apiDateLayout, strings.TrimSpace(item.Data))
if err != nil || parsed.Year() != year {
continue
}
name := strings.TrimSpace(item.Nome)
if name == "" {
name = "Feriado"
}
found[parsed.Format(domain.DateLayout)] = name
}
if len(found) == 0 {
return nil, fmt.Errorf("a API de feriados não retornou datas para %d", year)
}
holidays := make([]domain.Holiday, 0, len(found))
for day, name := range found {
holidays = append(holidays, domain.Holiday{Day: day, Name: name, Source: domain.HolidaySourceAPI})
}
return holidays, nil
}

View File

@@ -0,0 +1,52 @@
package feriados
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestFetchYear(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/feriados/cidade/3106200" || r.URL.Query().Get("ano") != "2026" {
t.Errorf("requisição inesperada: %s", r.URL)
}
if r.Header.Get("Authorization") != "Bearer chave" {
t.Errorf("Authorization = %q", r.Header.Get("Authorization"))
}
_, _ = w.Write([]byte(`{"feriados":[
{"data":"01/01/2026","nome":"Ano Novo","tipo":"NACIONAL"},
{"data":"15/08/2026","nome":"Assunção de Nossa Senhora","tipo":"MUNICIPAL"},
{"data":"31/12/2025","nome":"Outro ano"},
{"data":"lixo","nome":"Inválido"}]}`))
}))
defer srv.Close()
got, err := NewClient(srv.URL, "chave", "3106200").FetchYear(context.Background(), 2026)
if err != nil {
t.Fatal(err)
}
days := map[string]string{}
for _, h := range got {
days[h.Day] = h.Name
}
if len(days) != 2 || days["2026-01-01"] != "Ano Novo" || days["2026-08-15"] != "Assunção de Nossa Senhora" {
t.Fatalf("feriados = %v", days)
}
}
func TestFetchYearErrors(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
defer srv.Close()
if _, err := NewClient(srv.URL, "chave", "3106200").FetchYear(context.Background(), 2026); err == nil || !strings.Contains(err.Error(), "401") {
t.Fatalf("esperava HTTP 401, veio %v", err)
}
if _, err := NewClient(srv.URL, "chave", "MG-belo-horizonte").FetchYear(context.Background(), 2026); err == nil || !strings.Contains(err.Error(), "IBGE") {
t.Fatalf("esperava erro de código IBGE, veio %v", err)
}
}