package service import ( "context" "encoding/json" "fmt" "net/http" "time" "financeiro-carvalho/internal/model" "financeiro-carvalho/internal/repository" ) const bcbCDIURL = "https://api.bcb.gov.br/dados/serie/bcdata.sgs.12/dados?formato=json&dataInicial=%s&dataFinal=%s" type CDIYieldService struct { accountRepo repository.AccountRepoWithYield txRepo repository.ManualTransactionRepository client *http.Client } func NewCDIYieldService(accountRepo repository.AccountRepoWithYield, txRepo repository.ManualTransactionRepository) *CDIYieldService { return &CDIYieldService{ accountRepo: accountRepo, txRepo: txRepo, client: &http.Client{Timeout: 10 * time.Second}, } } // ApplyYield fetches CDI rates and creates a yield transaction for the account if applicable. func (s *CDIYieldService) ApplyYield(ctx context.Context, a *model.Account) error { if a.YieldType != "cdi" { return nil } // Determine the start date (day after last yield) yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02") startDate := a.CreatedAt[:10] // default: account creation date if a.LastYieldDate != nil && *a.LastYieldDate != "" { startDate = *a.LastYieldDate // start is exclusive: add one day t, err := time.Parse("2006-01-02", startDate) if err != nil { return nil } startDate = t.AddDate(0, 0, 1).Format("2006-01-02") } if startDate > yesterday { // nothing to calculate yet return nil } rates, err := s.fetchCDIRates(startDate, yesterday) if err != nil { // BCB API unreachable — skip silently; will retry next load return nil } if len(rates) == 0 { return nil } // Fill gaps (weekends/holidays) using last known rate rateMap := make(map[string]float64, len(rates)) for _, r := range rates { rateMap[r.date] = r.value } compound := 1.0 lastRate := rates[0].value start, _ := time.Parse("2006-01-02", startDate) end, _ := time.Parse("2006-01-02", yesterday) for d := start; !d.After(end); d = d.AddDate(0, 0, 1) { key := d.Format("2006-01-02") if r, ok := rateMap[key]; ok { lastRate = r } compound *= 1 + lastRate/100 } cdiPct := a.CDIPercentage if cdiPct <= 0 { cdiPct = 100 } yieldAmount := a.Balance * (compound - 1) * (cdiPct / 100) if yieldAmount <= 0 { return nil } period := fmt.Sprintf("%s → %s", startDate, yesterday) tx := model.Transaction{ Date: time.Now().Format("2006-01-02"), Amount: yieldAmount, Description: fmt.Sprintf("Rendimento CDI — %s", period), Type: "income", Source: "yield", AccountID: &a.ID, } if _, err := s.txRepo.Create(ctx, tx); err != nil { return err } return s.accountRepo.UpdateLastYieldDate(ctx, a.ID, yesterday) } type bcbEntry struct { date string value float64 } func (s *CDIYieldService) fetchCDIRates(startDate, endDate string) ([]bcbEntry, error) { // BCB API uses DD/MM/YYYY format start, _ := time.Parse("2006-01-02", startDate) end, _ := time.Parse("2006-01-02", endDate) url := fmt.Sprintf(bcbCDIURL, start.Format("02/01/2006"), end.Format("02/01/2006")) resp, err := s.client.Get(url) if err != nil { return nil, err } defer resp.Body.Close() var raw []struct { Data string `json:"data"` Valor string `json:"valor"` } if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil { return nil, err } var out []bcbEntry for _, r := range raw { // BCB date format: DD/MM/YYYY → convert to YYYY-MM-DD t, err := time.Parse("02/01/2006", r.Data) if err != nil { continue } var v float64 fmt.Sscanf(r.Valor, "%f", &v) out = append(out, bcbEntry{date: t.Format("2006-01-02"), value: v}) } return out, nil }