From 80f5b39961950d76cad450b3a8143028cac3ff3e Mon Sep 17 00:00:00 2001 From: TheWanderingCrow Date: Wed, 12 Nov 2025 09:59:32 -0500 Subject: [PATCH] feat!: Finalize code --- src/main.go | 121 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 116 insertions(+), 5 deletions(-) diff --git a/src/main.go b/src/main.go index ce6b430..192b04a 100644 --- a/src/main.go +++ b/src/main.go @@ -1,10 +1,17 @@ package main import ( + "bytes" + "encoding/json" + "fmt" "github.com/joho/godotenv" + "io" "log" "net/http" + "net/smtp" "os" + "strings" + "time" ) type BudgetClient struct { @@ -14,6 +21,41 @@ type BudgetClient struct { fullUrl string } +type BudgetMonthsResponse struct { + Data struct { + Month string `json:"month"` + IncomeAvailable int `json:"incomeAvailable"` + LastMonthOverspent int `json:"lastMonthOverspent"` + ForNextMonth int `json:"forNextMonth"` + TotalBudgeted int `json:"totalBudgeted"` + ToBudget int `json:"toBudget"` + FromLastMonth int `json:"fromLastMonth"` + TotalIncome int `json:"totalIncome"` + TotalSpent int `json:"totalSpent"` + TotalBalance int `json:"totalBalance"` + CategoryGroups []struct { + ID string `json:"id"` + Name string `json:"name"` + IsIncome bool `json:"is_income"` + Hidden bool `json:"hidden"` + Budgeted int `json:"budgeted"` + Spent int `json:"spent"` + Balance int `json:"balance"` + Categories []struct { + ID string `json:"id"` + Name string `json:"name"` + IsIncome bool `json:"is_income"` + Hidden bool `json:"hidden"` + GroupID string `json:"group_id"` + Budgeted int `json:"budgeted"` + Spent int `json:"spent"` + Balance int `json:"balance"` + Carryover bool `json:"carryover"` + } `json:"categories"` + } `json:"categoryGroups"` + } `json:"data"` +} + func CreateBudgetClient(baseUrl string, apiKey string, syncId string) *BudgetClient { fullUrl := baseUrl + "/v1/budgets/" + syncId client := BudgetClient{baseUrl, apiKey, syncId, fullUrl} @@ -25,7 +67,7 @@ func CreateBudgetClient(baseUrl string, apiKey string, syncId string) *BudgetCli // method is POST, GET, ect // route is the route to call with a leading / ex. /accounts/banksync // headers is a map of strings expecting "header" "value" -func (b BudgetClient) callApi(method string, route string, headers map[string]string) bool { +func (b BudgetClient) callApi(method string, route string, headers map[string]string) *http.Response { var httpClient http.Client req, err := http.NewRequest(method, b.fullUrl+route, http.NoBody) if err != nil { @@ -39,12 +81,35 @@ func (b BudgetClient) callApi(method string, route string, headers map[string]st if err != nil { log.Fatal(err) } - log.Println(resp) + return resp +} + +// Triggers a bank sync, returns true if successful +func (b BudgetClient) BankSync() bool { + resp := b.callApi("POST", "/accounts/banksync", nil) return resp.StatusCode == http.StatusOK } -func (b BudgetClient) BankSync() bool { - return b.callApi("POST", "/accounts/banksync", nil) +func (b BudgetClient) GetBudgetMonths() *BudgetMonthsResponse { + currentTime := time.Now() + year, month := currentTime.Year(), int(currentTime.Month()) + budgetMonth := fmt.Sprintf("%v-%v", year, month) + resp := b.callApi("GET", "/months/"+budgetMonth, nil) + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + log.Fatal("GetBudgetAmounts failed with: " + string(resp.Status)) + } + var data bytes.Buffer + + _, err := io.Copy(&data, resp.Body) + if err != nil { + log.Println(err) + } + + var budgetMonths BudgetMonthsResponse + err = json.Unmarshal(data.Bytes(), &budgetMonths) + return &budgetMonths + } func main() { @@ -57,6 +122,52 @@ func main() { var baseUrl = os.Getenv("BASE_URL") var apiKey = os.Getenv("API_KEY") var syncId = os.Getenv("SYNC_ID") + var smtpUsername = os.Getenv("SMTP_USERNAME") + var smtpPassword = os.Getenv("SMTP_PASSWORD") + var smtpHost = os.Getenv("SMTP_HOST") + var smtpRecipients = os.Getenv("SMTP_RECIPIENTS") client := CreateBudgetClient(baseUrl, apiKey, syncId) - client.BankSync() + //if !client.BankSync() { + // log.Println("Bank Sync failed, information may not be up to date") + //} + budgetMonths := client.GetBudgetMonths() + + subject := "Subject: Budget Report\n" + mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" + const bodyTemplate = ` +

Good day,

+

Your unallocated budget total as of %s: $%v

+ +

Category breakdown:

+ ` + var categories strings.Builder + + for _, v := range budgetMonths.Data.CategoryGroups { + for _, v2 := range v.Categories { + if !v2.IsIncome { + bal := float32(v2.Balance) + bal /= 100 + fmt.Fprintf(&categories, "

%v: $%v

", v2.Name, bal) + } + } + } + categories.Write([]byte("

This is an automated report generated by actualbudget-report")) + + currentTime := time.Now() + toBudget := float32(budgetMonths.Data.ToBudget) + toBudget /= 100 + body := fmt.Sprintf(bodyTemplate, currentTime.Format(time.RFC850), toBudget) + message := []byte(subject + mime + body + categories.String()) + + var auth smtp.Auth + if os.Getenv("ENVIRONMENT") == "dev" { + auth = nil + } else { + auth = smtp.PlainAuth("", smtpUsername, smtpPassword, smtpHost) + } + err := smtp.SendMail(smtpHost, auth, smtpUsername, []string{smtpRecipients}, []byte(message)) + if err != nil { + log.Fatal(err) + } + }