api for initial widgets

This commit is contained in:
Gabe Farrell 2023-04-10 22:39:51 -04:00
parent 36f751a69f
commit 4aa8a2f822
14 changed files with 864 additions and 11 deletions

115
widgets/balance.go Normal file
View file

@ -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)
}

View file

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

173
widgets/budget.go Normal file
View file

@ -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}"))
}

55
widgets/expenses.go Normal file
View file

@ -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)
}

55
widgets/income.go Normal file
View file

@ -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)
}

29
widgets/router.go Normal file
View file

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

248
widgets/transactions.go Normal file
View file

@ -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}"))
}