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