Projeto bot pontos
This commit is contained in:
112
backend/internal/adapters/feriados/client.go
Normal file
112
backend/internal/adapters/feriados/client.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user