Files
bot-pontos/backend/internal/adapters/feriados/client.go
2026-10-23 22:31:02 -03:00

113 lines
3.1 KiB
Go

// 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
}