From 4aa8a2f8227aca79eddccc2f8105d9097acaea4b Mon Sep 17 00:00:00 2001 From: Gabe Farrell Date: Mon, 10 Apr 2023 22:39:51 -0400 Subject: [PATCH] api for initial widgets --- db/schemas.go | 33 +++++ main.go | 15 ++- money/currency.go | 8 ++ money/money.go | 31 +++++ routes/createAccount.go | 5 +- routes/login.go | 13 +- routes/userInfo.go | 55 ++++++++ widgets/balance.go | 115 +++++++++++++++++ widgets/balanceInternal.go | 40 ++++++ widgets/budget.go | 173 ++++++++++++++++++++++++++ widgets/expenses.go | 55 ++++++++ widgets/income.go | 55 ++++++++ widgets/router.go | 29 +++++ widgets/transactions.go | 248 +++++++++++++++++++++++++++++++++++++ 14 files changed, 864 insertions(+), 11 deletions(-) create mode 100644 db/schemas.go create mode 100644 money/currency.go create mode 100644 money/money.go create mode 100644 routes/userInfo.go create mode 100644 widgets/balance.go create mode 100644 widgets/balanceInternal.go create mode 100644 widgets/budget.go create mode 100644 widgets/expenses.go create mode 100644 widgets/income.go create mode 100644 widgets/router.go create mode 100644 widgets/transactions.go diff --git a/db/schemas.go b/db/schemas.go new file mode 100644 index 0000000..4a82d0b --- /dev/null +++ b/db/schemas.go @@ -0,0 +1,33 @@ +package db + +import ( + "github.com/jacobmveber-01839764/BudgetBuddy/money" +) + +type UserSchema struct { + Name string `json:"name" bson:"name"` + Email string `json:"email" bson:"email"` + Password string `json:"password" bson:"password"` + Session string `json:"session" bson:"session"` + Balance money.Money `json:"balance" bson:"balance"` + Budget money.Money `json:"budget" bson:"budget"` + Categories map[string]money.Money `json:"categories" bson:"categories"` + Expenses []Transaction `json:"expenses" bson:"expenses"` + Income []Transaction `json:"income" bson:"income"` + RecurringExpenses []RecurringTransaction `json:"recurring_expenses" bson:"recurring_expenses"` + RecurringIncome []RecurringTransaction `json:"recurring_income" bson:"recurring_income"` +} + +type Transaction struct { + Timestamp int64 `json:"timestamp" bson:"timestamp"` + Category string `json:"category" bson:"category"` + Amount money.Money `json:"amount" bson:"amount"` + Type string `json:"type" bson:"type"` +} + +type RecurringTransaction struct { + Transaction `json:"transaction" bson:"transaction"` + Period int `json:"period" bson:"period"` // in days + Since int64 `json:"since" bson:"since"` // unix timestamp + Until int64 `json:"until" bson:"until"` // 0 for no end date +} diff --git a/main.go b/main.go index 24b78d3..b54d359 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,15 @@ import ( "github.com/go-chi/chi/v5" "github.com/jacobmveber-01839764/BudgetBuddy/db" "github.com/jacobmveber-01839764/BudgetBuddy/routes" + "github.com/jacobmveber-01839764/BudgetBuddy/widgets" +) + +// TODO: expire transactions after one month on login +// TODO: perform recurring transactions on login +// TODO: give transactions ids so you can delete them + +const ( + _PORT = ":3030" ) func main() { @@ -20,6 +29,10 @@ func main() { r.Post("/auth/login", routes.Login) r.Post("/auth/login/session", routes.Login) r.Post("/auth/createaccount", routes.CreateAccount) + r.Get("/userinfo", routes.UserInfo) + + r.Mount("/w", widgets.Router()) - log.Fatal(http.ListenAndServe(":3030", r)) + log.Println("* Listening on " + _PORT) + log.Fatal(http.ListenAndServe(_PORT, r)) } diff --git a/money/currency.go b/money/currency.go new file mode 100644 index 0000000..842a152 --- /dev/null +++ b/money/currency.go @@ -0,0 +1,8 @@ +package money + +type Currency string + +const ( + USD Currency = "USD" + CAD = "CAD" +) diff --git a/money/money.go b/money/money.go new file mode 100644 index 0000000..250f30f --- /dev/null +++ b/money/money.go @@ -0,0 +1,31 @@ +package money + +type Money struct { + Currency Currency `json:"currency" bson:"currency"` + Whole int `json:"whole" bson:"whole"` + Decimal int `json:"decimal" bson:"decimal"` +} + +// add x to y +func Add(x, y Money) Money { + x.Decimal += y.Decimal + if x.Decimal >= 100 { + x.Whole++ + x.Decimal -= 100 + } + x.Whole += y.Whole + + return x +} + +// subtract x from y +func Subtract(x, y Money) Money { + x.Decimal = y.Decimal - x.Decimal + if x.Decimal < 0 { + x.Whole++ + x.Decimal += 100 + } + x.Whole = y.Whole - x.Whole + + return x +} diff --git a/routes/createAccount.go b/routes/createAccount.go index 967e73c..5ad5f72 100644 --- a/routes/createAccount.go +++ b/routes/createAccount.go @@ -18,6 +18,7 @@ import ( ) func CreateAccount(w http.ResponseWriter, r *http.Request) { + log.Println("* /auth/createaccount") // prepare DB err := db.Client.Ping(context.Background(), readpref.Primary()) if err != nil { @@ -26,7 +27,7 @@ func CreateAccount(w http.ResponseWriter, r *http.Request) { var userCollection = db.Client.Database("budgetbuddy").Collection("users") // var v contains POST credentials - var v UserSchema + var v db.UserSchema r.ParseForm() v.Email = r.FormValue("email") v.Password = r.FormValue("password") @@ -72,6 +73,8 @@ func CreateAccount(w http.ResponseWriter, r *http.Request) { // add the new user to the database v.Session = sessionID v.Password = string(hashedPass) + v.Balance.Currency = "USD" + v.Budget.Currency = "USD" _, err = userCollection.InsertOne(r.Context(), v) if err != nil { log.Println("* Error inserting new user") diff --git a/routes/login.go b/routes/login.go index aaea8ec..0bc6fff 100644 --- a/routes/login.go +++ b/routes/login.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "log" "net/http" "strings" @@ -16,13 +17,6 @@ import ( "golang.org/x/crypto/bcrypt" ) -type UserSchema struct { - Name string `json:"name" bson:"name"` - Email string `json:"email" bson:"email"` - Password string `json:"password" bson:"password"` - Session string `json:"session" bson:"session"` -} - type LoginResponse struct { Name string `json:"name"` Email string `json:"email"` @@ -30,6 +24,7 @@ type LoginResponse struct { } func Login(w http.ResponseWriter, r *http.Request) { + log.Println("* /auth/login") // prepare DB err := db.Client.Ping(context.Background(), readpref.Primary()) if err != nil { @@ -38,7 +33,7 @@ func Login(w http.ResponseWriter, r *http.Request) { var userCollection = db.Client.Database("budgetbuddy").Collection("users") // var v contains POST credentials - var v UserSchema + var v db.UserSchema r.ParseForm() v.Email = r.FormValue("email") v.Password = r.FormValue("password") @@ -48,7 +43,7 @@ func Login(w http.ResponseWriter, r *http.Request) { } // cmp struct will be compared with v to verify credentials - var cmp UserSchema + var cmp db.UserSchema found := userCollection.FindOne(r.Context(), bson.D{primitive.E{Key: "email", Value: strings.ToLower(v.Email)}}) if found.Err() != nil { diff --git a/routes/userInfo.go b/routes/userInfo.go new file mode 100644 index 0000000..a784b7b --- /dev/null +++ b/routes/userInfo.go @@ -0,0 +1,55 @@ +package routes + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/jacobmveber-01839764/BudgetBuddy/db" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type UserInfoResponse struct { + Name string `json:"name" bson:"name"` + Email string `json:"email" bson:"email"` +} + +func UserInfo(w http.ResponseWriter, r *http.Request) { + // get session key from request + session := r.Header.Get("x-session-key") + + // get collection handle from db + var userCollection = db.Client.Database("budgetbuddy").Collection("users") + + found := userCollection.FindOne(r.Context(), bson.D{primitive.E{Key: "session", Value: strings.ToLower(session)}}) + if found.Err() != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "{\"error\":\"session key invalid\"}") + return + } + + var user = db.UserSchema{} + + err := found.Decode(&user) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "{\"error\":\"problem decoding user\"}") + return + } + + info := UserInfoResponse{ + Name: user.Name, + Email: user.Email, + } + + ret, err := json.Marshal(info) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "{\"error\":\"problem marshalling response\"}") + return + } + + w.Write(ret) +} diff --git a/widgets/balance.go b/widgets/balance.go new file mode 100644 index 0000000..6bd06bf --- /dev/null +++ b/widgets/balance.go @@ -0,0 +1,115 @@ +package widgets + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" + + "github.com/jacobmveber-01839764/BudgetBuddy/db" + "github.com/jacobmveber-01839764/BudgetBuddy/money" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// TODO: Put user info fetching code into middleware + +type GetBalanceResponse struct { + Status int `json:"status"` + RequestID string `json:"request_id"` + Data money.Money `json:"data"` +} + +func GetBalance(w http.ResponseWriter, r *http.Request) { + // get session key from request + session := r.Header.Get("x-session-key") + + // get collection handle from db + var userCollection = db.Client.Database("budgetbuddy").Collection("users") + + var user = db.UserSchema{} + + err := userCollection.FindOne(context.Background(), bson.D{primitive.E{Key: "session", Value: session}}).Decode(&user) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "{\"error\":\"invalid session key\"}") + return + } + + response := GetBalanceResponse{ + Status: 200, + RequestID: "0", + Data: money.Money{ + Currency: user.Balance.Currency, + Whole: user.Balance.Whole, + Decimal: user.Balance.Decimal, + }, + } + ret, err := json.Marshal(response) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "{\"error\":\"problem marshalling response\"}") + return + } + + w.Write(ret) +} + +func SetBalance(w http.ResponseWriter, r *http.Request) { + // get session key from request + session := r.Header.Get("x-session-key") + + // get form values + r.ParseForm() + newWhole, err := strconv.Atoi(r.Form.Get("whole")) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "{\"error\":\"incorrect whole value\"}") + return + } + newDecimal, err := strconv.Atoi(r.Form.Get("decimal")) + if err != nil || newDecimal < 0 || newDecimal > 99 { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "{\"error\":\"incorrect decimal value\"}") + return + } + // TODO: figure out how to efficiently determing currency + newBalance := money.Money{ + Currency: "USD", + Whole: newWhole, + Decimal: newDecimal, + } + + // get collection handle from db + var userCollection = db.Client.Database("budgetbuddy").Collection("users") + + filter := bson.D{primitive.E{Key: "session", Value: session}} + opts := options.Update().SetUpsert(true) + update := bson.D{primitive.E{Key: "$set", Value: bson.D{primitive.E{Key: "balance", Value: newBalance}}}} + _, err = userCollection.UpdateOne(context.TODO(), filter, update, opts) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "{\"error\":\"invalid session key\"}") + return + } + + response := GetBalanceResponse{ + Status: 200, + RequestID: "0", + Data: money.Money{ + Currency: newBalance.Currency, + Whole: newBalance.Whole, + Decimal: newBalance.Decimal, + }, + } + ret, err := json.Marshal(response) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "{\"error\":\"problem marshalling response\"}") + return + } + + w.Write(ret) +} diff --git a/widgets/balanceInternal.go b/widgets/balanceInternal.go new file mode 100644 index 0000000..c695cf3 --- /dev/null +++ b/widgets/balanceInternal.go @@ -0,0 +1,40 @@ +package widgets + +import ( + "context" + + "github.com/jacobmveber-01839764/BudgetBuddy/db" + "github.com/jacobmveber-01839764/BudgetBuddy/money" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func addToBalance(user db.UserSchema, amount money.Money) bool { + + newBalance := money.Add(amount, user.Balance) + + // get collection handle from db + var userCollection = db.Client.Database("budgetbuddy").Collection("users") + + filter := bson.D{primitive.E{Key: "session", Value: user.Session}} + opts := options.Update().SetUpsert(true) + update := bson.D{primitive.E{Key: "$set", Value: bson.D{primitive.E{Key: "balance", Value: newBalance}}}} + _, err := userCollection.UpdateOne(context.TODO(), filter, update, opts) + return err == nil +} + +func subtractFromBalance(user db.UserSchema, amount money.Money) bool { + + // create money object to store in db + newBalance := money.Subtract(amount, user.Balance) + + // get collection handle from db + var userCollection = db.Client.Database("budgetbuddy").Collection("users") + + filter := bson.D{primitive.E{Key: "session", Value: user.Session}} + opts := options.Update().SetUpsert(true) + update := bson.D{primitive.E{Key: "$set", Value: bson.D{primitive.E{Key: "balance", Value: newBalance}}}} + _, err := userCollection.UpdateOne(context.TODO(), filter, update, opts) + return err == nil +} diff --git a/widgets/budget.go b/widgets/budget.go new file mode 100644 index 0000000..20a9c59 --- /dev/null +++ b/widgets/budget.go @@ -0,0 +1,173 @@ +package widgets + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" + + "github.com/jacobmveber-01839764/BudgetBuddy/db" + "github.com/jacobmveber-01839764/BudgetBuddy/money" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type BudgetResponse struct { + // total amount allowed to spend in a month + Budget money.Money `json:"budget"` + // total amount allowed to spend by category + BudgetCategories map[string]money.Money `json:"budget_categories"` + // categories in the budget + Categories []string `json:"categories"` + // transactions mapped to a category + Expenses map[string][]db.Transaction `json:"expenses"` +} + +func GetBudget(w http.ResponseWriter, r *http.Request) { + // get session key from request + session := r.Header.Get("x-session-key") + + // get collection handle from db + var userCollection = db.Client.Database("budgetbuddy").Collection("users") + + var user = db.UserSchema{} + + err := userCollection.FindOne(context.Background(), bson.D{primitive.E{Key: "session", Value: session}}).Decode(&user) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "{\"error\":\"invalid session key\"}") + return + } + var response BudgetResponse + + response.Budget = user.Budget + response.BudgetCategories = user.Categories + cats := make([]string, len(user.Categories)) + i := 0 + for k := range user.Categories { + cats[i] = k + i++ + } + response.Categories = cats + response.Expenses = make(map[string][]db.Transaction) + for _, e := range user.Expenses { + if response.Expenses[e.Category] == nil { + response.Expenses[e.Category] = make([]db.Transaction, 0) + } + response.Expenses[e.Category] = append(response.Expenses[e.Category], e) + } + + ret, err := json.Marshal(response) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "{\"error\":\"problem marshalling response\"}") + return + } + + w.Write(ret) + +} +func SetCategoryBudget(w http.ResponseWriter, r *http.Request) { + // get session key from request + session := r.Header.Get("x-session-key") + + // get collection handle from db + var userCollection = db.Client.Database("budgetbuddy").Collection("users") + + var user = db.UserSchema{} + + err := userCollection.FindOne(context.Background(), bson.D{primitive.E{Key: "session", Value: session}}).Decode(&user) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "{\"error\":\"invalid session key\"}") + return + } + + // get form values + r.ParseForm() + cat := r.FormValue("category") + if cat == "" { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "{\"error\":\"category must be specified\"}") + return + } + newWhole, err := strconv.Atoi(r.Form.Get("whole")) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "{\"error\":\"incorrect whole value\"}") + return + } + newDecimal, err := strconv.Atoi(r.Form.Get("decimal")) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "{\"error\":\"incorrect decimal value\"}") + return + } + // TODO: figure out how to efficiently determing currency + newBudget := money.Money{ + Currency: money.Currency(r.FormValue("currency")), + Whole: newWhole, + Decimal: newDecimal, + } + + if user.Categories == nil { + user.Categories = make(map[string]money.Money) + } + + user.Categories[cat] = newBudget + + filter := bson.D{primitive.E{Key: "session", Value: session}} + opts := options.Update().SetUpsert(true) + update := bson.D{primitive.E{Key: "$set", Value: bson.D{primitive.E{Key: "categories", Value: user.Categories}}}} + _, err = userCollection.UpdateOne(context.TODO(), filter, update, opts) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "{\"error\":\"invalid session key\"}") + return + } + + w.Write([]byte("{\"status\":200}")) +} + +func SetBudget(w http.ResponseWriter, r *http.Request) { + // get session key from request + session := r.Header.Get("x-session-key") + + // get form values + r.ParseForm() + newWhole, err := strconv.Atoi(r.Form.Get("whole")) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "{\"error\":\"incorrect whole value\"}") + return + } + newDecimal, err := strconv.Atoi(r.Form.Get("decimal")) + if err != nil || newDecimal < 0 || newDecimal > 99 { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "{\"error\":\"incorrect decimal value\"}") + return + } + // TODO: figure out how to efficiently determing currency + newBudget := money.Money{ + Currency: money.Currency(r.FormValue("currency")), + Whole: newWhole, + Decimal: newDecimal, + } + + // get collection handle from db + var userCollection = db.Client.Database("budgetbuddy").Collection("users") + + filter := bson.D{primitive.E{Key: "session", Value: session}} + opts := options.Update().SetUpsert(true) + update := bson.D{primitive.E{Key: "$set", Value: bson.D{primitive.E{Key: "budget", Value: newBudget}}}} + _, err = userCollection.UpdateOne(context.TODO(), filter, update, opts) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "{\"error\":\"invalid session key\"}") + return + } + + w.Write([]byte("{\"status\":200}")) +} diff --git a/widgets/expenses.go b/widgets/expenses.go new file mode 100644 index 0000000..279fe2f --- /dev/null +++ b/widgets/expenses.go @@ -0,0 +1,55 @@ +package widgets + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/jacobmveber-01839764/BudgetBuddy/db" + "github.com/jacobmveber-01839764/BudgetBuddy/money" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func GetMonthExpenses(w http.ResponseWriter, r *http.Request) { + // get session key from request + session := r.Header.Get("x-session-key") + + // get collection handle from db + var userCollection = db.Client.Database("budgetbuddy").Collection("users") + + var user = db.UserSchema{} + + err := userCollection.FindOne(context.Background(), bson.D{primitive.E{Key: "session", Value: session}}).Decode(&user) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "{\"error\":\"invalid session key\"}") + return + } + + if user.Expenses == nil { + user.Expenses = make([]db.Transaction, 0) + } + + total := money.Money{} + total.Currency = user.Balance.Currency + + for i := 0; i < len(user.Expenses); i++ { + // stop if/when we get past a month ago + if user.Expenses[i].Timestamp < time.Now().Add(-30*24*time.Hour).Unix() { + break + } + total = money.Add(total, user.Expenses[i].Amount) + } + + ret, err := json.Marshal(total) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "{\"error\":\"problem marshalling response\"}") + return + } + + w.Write(ret) +} diff --git a/widgets/income.go b/widgets/income.go new file mode 100644 index 0000000..95cbd91 --- /dev/null +++ b/widgets/income.go @@ -0,0 +1,55 @@ +package widgets + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/jacobmveber-01839764/BudgetBuddy/db" + "github.com/jacobmveber-01839764/BudgetBuddy/money" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func GetMonthIncome(w http.ResponseWriter, r *http.Request) { + // get session key from request + session := r.Header.Get("x-session-key") + + // get collection handle from db + var userCollection = db.Client.Database("budgetbuddy").Collection("users") + + var user = db.UserSchema{} + + err := userCollection.FindOne(context.Background(), bson.D{primitive.E{Key: "session", Value: session}}).Decode(&user) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "{\"error\":\"invalid session key\"}") + return + } + + if user.Income == nil { + user.Income = make([]db.Transaction, 0) + } + + total := money.Money{} + total.Currency = user.Balance.Currency + + for i := 0; i < len(user.Income); i++ { + // stop if/when we get past a month ago + if user.Income[i].Timestamp < time.Now().Add(-30*24*time.Hour).Unix() { + break + } + total = money.Add(total, user.Income[i].Amount) + } + + ret, err := json.Marshal(total) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "{\"error\":\"problem marshalling response\"}") + return + } + + w.Write(ret) +} diff --git a/widgets/router.go b/widgets/router.go new file mode 100644 index 0000000..cdfed92 --- /dev/null +++ b/widgets/router.go @@ -0,0 +1,29 @@ +package widgets + +import "github.com/go-chi/chi/v5" + +func Router() *chi.Mux { + r := chi.NewRouter() + + // balance widget + r.Get("/balance", GetBalance) + r.Post("/balance", SetBalance) + + // transaction widget + r.Get("/transactions/recent", GetRecentTransactions) + r.Post("/transactions", NewTransaction) + r.Post("/transactions/recurring", NewRecurring) + + // budget widget + r.Get("/budget", GetBudget) + r.Post("/budget", SetBudget) + r.Post("/budget/categories", SetCategoryBudget) + + // expenses + r.Get("/expenses/month", GetMonthExpenses) + + // income + r.Get("/income/month", GetMonthIncome) + + return r +} diff --git a/widgets/transactions.go b/widgets/transactions.go new file mode 100644 index 0000000..7095332 --- /dev/null +++ b/widgets/transactions.go @@ -0,0 +1,248 @@ +package widgets + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/jacobmveber-01839764/BudgetBuddy/db" + "github.com/jacobmveber-01839764/BudgetBuddy/money" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const ( + RecentTransactionAmount = 10 +) + +type RecentTransactionsResponse struct { + Status int `json:"status"` + Transactions []db.Transaction `json:"transactions"` +} + +func GetRecentTransactions(w http.ResponseWriter, r *http.Request) { + // get session key from request + session := r.Header.Get("x-session-key") + + // get collection handle from db + var userCollection = db.Client.Database("budgetbuddy").Collection("users") + + var user = db.UserSchema{} + + err := userCollection.FindOne(context.Background(), bson.D{primitive.E{Key: "session", Value: session}}).Decode(&user) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "{\"error\":\"invalid session key\"}") + return + } + + var transactionA []db.Transaction + + i := 0 + j := 0 + for i+j < RecentTransactionAmount { + // if we are out of transactions, return + if i >= len(user.Expenses) && j >= len(user.Income) { + break + } else if i > len(user.Expenses)-1 { // if we are out of expenses, just use income + transactionA = append(transactionA, user.Income[j]) + j++ + } else if j > len(user.Income)-1 { // if we are out of income, just use expenses + transactionA = append(transactionA, user.Expenses[i]) + i++ + } else if user.Expenses[i].Timestamp > user.Income[j].Timestamp { + transactionA = append(transactionA, user.Expenses[i]) + i++ + } else { + transactionA = append(transactionA, user.Income[j]) + j++ + } + } + + response := RecentTransactionsResponse{ + Status: 200, + Transactions: transactionA, + } + + ret, err := json.Marshal(response) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "{\"error\":\"problem marshalling response\"}") + } + + w.Write(ret) +} + +func NewTransaction(w http.ResponseWriter, r *http.Request) { + // get session key from request + session := r.Header.Get("x-session-key") + + // get collection handle from db + var userCollection = db.Client.Database("budgetbuddy").Collection("users") + + var user = db.UserSchema{} + + err := userCollection.FindOne(context.Background(), bson.D{primitive.E{Key: "session", Value: session}}).Decode(&user) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "{\"error\":\"invalid session key\"}") + return + } + + r.ParseForm() + + whole, err := strconv.Atoi(r.Form.Get("whole")) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "{\"error\":\"incorrect whole value\"}") + return + } + decimal, err := strconv.Atoi(r.Form.Get("decimal")) + if err != nil || decimal < 0 || decimal > 99 { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "{\"error\":\"incorrect decimal value\"}") + return + } + + var cat string + + if r.FormValue("category") == "" { + cat = "uncategorized" + } else { + cat = r.FormValue("category") + } + + newT := db.Transaction{ + Timestamp: time.Now().Unix(), + Category: cat, + Amount: money.Money{ + Currency: money.Currency(r.FormValue("currency")), + Whole: whole, + Decimal: decimal, + }, + Type: r.FormValue("type"), + } + + var newArr []db.Transaction + var success bool + if r.FormValue("type") == "income" { + newArr = append(user.Income, newT) + success = addToBalance(user, newT.Amount) + } else if r.FormValue("type") == "expenses" { + newArr = append(user.Expenses, newT) + success = subtractFromBalance(user, newT.Amount) + } else { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "{\"error\":\"invalid transaction type - only income or expenses are allowed\"}") + return + } + + if !success { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "{\"error\":\"unable to update balance\"}") + return + } + + // push the new transaction to db + filter := bson.D{primitive.E{Key: "session", Value: user.Session}} + opts := options.Update().SetUpsert(true) + update := bson.D{primitive.E{Key: "$set", Value: bson.D{primitive.E{Key: r.FormValue("type"), Value: newArr}}}} + userCollection.UpdateOne(context.TODO(), filter, update, opts) + + w.Write([]byte("{\"status\": 200}")) +} + +func NewRecurring(w http.ResponseWriter, r *http.Request) { + // get session key from request + session := r.Header.Get("x-session-key") + + // get collection handle from db + var userCollection = db.Client.Database("budgetbuddy").Collection("users") + + var user = db.UserSchema{} + + err := userCollection.FindOne(context.Background(), bson.D{primitive.E{Key: "session", Value: session}}).Decode(&user) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "{\"error\":\"invalid session key\"}") + return + } + + r.ParseForm() + + whole, err := strconv.Atoi(r.Form.Get("whole")) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "{\"error\":\"incorrect whole value\"}") + return + } + decimal, err := strconv.Atoi(r.Form.Get("decimal")) + if err != nil || decimal < 0 || decimal > 99 { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "{\"error\":\"incorrect decimal value\"}") + return + } + period, err := strconv.Atoi(r.Form.Get("period")) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "{\"error\":\"period must be specified\"}") + return + } + + var cat string + + if r.FormValue("category") == "" { + cat = "uncategorized" + } + + newT := db.Transaction{ + Timestamp: time.Now().Unix(), + Category: cat, + Amount: money.Money{ + Currency: money.Currency(r.FormValue("currency")), + Whole: whole, + Decimal: decimal, + }, + Type: r.FormValue("type"), + } + + newR := db.RecurringTransaction{ + Transaction: newT, + Period: period, + Since: time.Now().Unix(), + Until: int64(0), + } + + var newArr []db.RecurringTransaction + var success bool + if r.FormValue("type") == "income" { + newArr = append(user.RecurringIncome, newR) + success = addToBalance(user, newT.Amount) + } else if r.FormValue("type") == "expenses" { + newArr = append(user.RecurringExpenses, newR) + success = subtractFromBalance(user, newT.Amount) + } else { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "{\"error\":\"invalid transaction type - only income or expenses are allowed\"}") + return + } + + if !success { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "{\"error\":\"unable to update balance\"}") + return + } + + // push the new transaction to db + filter := bson.D{primitive.E{Key: "session", Value: user.Session}} + opts := options.Update().SetUpsert(true) + update := bson.D{primitive.E{Key: "$set", Value: bson.D{primitive.E{Key: r.FormValue("type"), Value: newArr}}}} + userCollection.UpdateOne(context.TODO(), filter, update, opts) + + w.Write([]byte("{\"status\": 200}")) + +}