Compare commits

...

4 Commits

7 changed files with 178 additions and 11 deletions

View File

@ -1,2 +1,9 @@
# Allowance Planner 2000 # Allowance Planner 2000
An improved Allowance Planner app. An improved Allowance Planner app.
## Running backend
In order to run the backend, go to the `backend directory and run:
```bash
$ go run .
```

View File

@ -248,7 +248,16 @@ func TestCreateTask(t *testing.T) {
// Verify the response has an ID // Verify the response has an ID
response.ContainsKey("id") response.ContainsKey("id")
taskId := response.Value("id").Number().Raw() response.Value("id").Number().IsEqual(1)
e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)
// Get task
result := e.GET("/task/1").Expect().Status(200).JSON().Object()
result.Value("id").IsEqual(1)
result.Value("name").IsEqual("Test Task")
result.Value("reward").IsEqual(100)
result.Value("assigned").IsNull()
// Create a new task with assigned user // Create a new task with assigned user
assignedUserId := 1 assignedUserId := 1
@ -265,7 +274,7 @@ func TestCreateTask(t *testing.T) {
JSON().Object() JSON().Object()
responseWithUser.ContainsKey("id") responseWithUser.ContainsKey("id")
responseWithUser.Value("id").Number().NotEqual(taskId) // Ensure different ID responseWithUser.Value("id").Number().IsEqual(2)
} }
func TestDeleteTask(t *testing.T) { func TestDeleteTask(t *testing.T) {
@ -345,12 +354,12 @@ func TestGetTaskWhenNoTasks(t *testing.T) {
result.Length().IsEqual(0) result.Length().IsEqual(0)
} }
func createTestTask(e *httpexpect.Expect) { func createTestTask(e *httpexpect.Expect) int {
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"name": "Test Task", "name": "Test Task",
"reward": 100, "reward": 100,
} }
e.POST("/tasks").WithJSON(requestBody).Expect().Status(201) return int(e.POST("/tasks").WithJSON(requestBody).Expect().Status(201).JSON().Object().Value("id").Number().Raw())
} }
func TestGetTasksWhenTasks(t *testing.T) { func TestGetTasksWhenTasks(t *testing.T) {
@ -520,6 +529,43 @@ func TestPutAllowanceById(t *testing.T) {
result.Value("weight").IsEqual(15) result.Value("weight").IsEqual(15)
} }
func TestCompleteTask(t *testing.T) {
e := startServer(t)
taskId := createTestTask(e)
e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)
// Create two allowance goals
e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
Name: "Test Allowance 1",
Target: 1000,
Weight: 50,
}).Expect().Status(201)
e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
Name: "Test Allowance 1",
Target: 1000,
Weight: 25,
}).Expect().Status(201)
e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{
Weight: 25,
}).Expect().Status(200)
// Complete the task
e.POST("/task/" + strconv.Itoa(taskId) + "/complete").Expect().Status(200)
// Verify the task is marked as completed
e.GET("/task/" + strconv.Itoa(taskId)).Expect().Status(404)
// Verify the allowances are updated
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Length().IsEqual(3)
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().IsEqual(50)
allowances.Value(0).Object().Value("id").Number().IsEqual(1)
allowances.Value(1).Object().Value("progress").Number().IsEqual(25)
allowances.Value(0).Object().Value("id").Number().IsEqual(2)
allowances.Value(2).Object().Value("progress").Number().IsEqual(26)
}
func getDelta(base time.Time, delta float64) (time.Time, time.Time) { func getDelta(base time.Time, delta float64) (time.Time, time.Time) {
start := base.Add(-time.Duration(delta) * time.Second) start := base.Add(-time.Duration(delta) * time.Second)
end := base.Add(time.Duration(delta) * time.Second) end := base.Add(time.Duration(delta) * time.Second)

View File

@ -71,7 +71,14 @@ func (db *Db) GetUserAllowances(userId int) ([]Allowance, error) {
allowances := make([]Allowance, 0) allowances := make([]Allowance, 0)
var err error var err error
for row := range db.db.Query("select id, name, target, progress, weight from allowances where user_id = ?"). totalAllowance := Allowance{}
err = db.db.Query("select balance, weight from users where id = ?").Bind(userId).ScanSingle(&totalAllowance.Progress, &totalAllowance.Weight)
if err != nil {
return nil, err
}
allowances = append(allowances, totalAllowance)
for row := range db.db.Query("select id, name, target, balance, weight from allowances where user_id = ?").
Bind(userId).Range(&err) { Bind(userId).Range(&err) {
allowance := Allowance{} allowance := Allowance{}
err = row.Scan(&allowance.ID, &allowance.Name, &allowance.Target, &allowance.Progress, &allowance.Weight) err = row.Scan(&allowance.ID, &allowance.Name, &allowance.Target, &allowance.Progress, &allowance.Weight)
@ -88,7 +95,7 @@ func (db *Db) GetUserAllowances(userId int) ([]Allowance, error) {
func (db *Db) GetUserAllowanceById(userId int, allowanceId int) (*Allowance, error) { func (db *Db) GetUserAllowanceById(userId int, allowanceId int) (*Allowance, error) {
allowance := &Allowance{} allowance := &Allowance{}
err := db.db.Query("select id, name, target, progress, weight from allowances where user_id = ? and id = ?"). err := db.db.Query("select id, name, target, balance, weight from allowances where user_id = ? and id = ?").
Bind(userId, allowanceId). Bind(userId, allowanceId).
ScanSingle(&allowance.ID, &allowance.Name, &allowance.Target, &allowance.Progress, &allowance.Weight) ScanSingle(&allowance.ID, &allowance.Name, &allowance.Target, &allowance.Progress, &allowance.Weight)
if err != nil { if err != nil {
@ -114,7 +121,7 @@ func (db *Db) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (in
defer tx.MustRollback() defer tx.MustRollback()
// Insert the new allowance // Insert the new allowance
err = tx.Query("insert into allowances (user_id, name, target, progress, weight) values (?, ?, ?, 0, ?)"). err = tx.Query("insert into allowances (user_id, name, target, weight) values (?, ?, ?, ?)").
Bind(userId, allowance.Name, allowance.Target, allowance.Weight). Bind(userId, allowance.Name, allowance.Target, allowance.Weight).
Exec() Exec()
@ -160,6 +167,22 @@ func (db *Db) DeleteAllowance(userId int, allowanceId int) error {
return nil return nil
} }
func (db *Db) UpdateUserAllowance(userId int, allowance *UpdateAllowanceRequest) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
err = tx.Query("update users set weight=? where id = ?").
Bind(allowance.Weight, userId).
Exec()
if err != nil {
return err
}
return tx.Commit()
}
func (db *Db) UpdateAllowance(userId int, allowanceId int, allowance *UpdateAllowanceRequest) error { func (db *Db) UpdateAllowance(userId int, allowanceId int, allowance *UpdateAllowanceRequest) error {
// Check if the allowance exists for the user // Check if the allowance exists for the user
count := 0 count := 0
@ -289,6 +312,65 @@ func (db *Db) UpdateTask(id int, task *CreateTaskRequest) error {
return tx.Commit() return tx.Commit()
} }
func (db *Db) CompleteTask(taskId int) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
var reward int
err = tx.Query("select reward from tasks where id = ?").Bind(taskId).ScanSingle(&reward)
if err != nil {
return err
}
for userRow := range tx.Query("select id, weight from users").Range(&err) {
var userId int
var userWeight float64
err = userRow.Scan(&userId, &userWeight)
if err != nil {
return err
}
var sumOfWeights float64
err = tx.Query("select sum(weight) from allowances where user_id = ? and weight > 0").Bind(userId).ScanSingle(&sumOfWeights)
for allowanceRow := range tx.Query("select id, weight from allowances where user_id = ? and weight > 0").Bind(userId).Range(&err) {
var allowanceId int
var allowanceWeight float64
err = allowanceRow.Scan(&allowanceId, &allowanceWeight)
if err != nil {
return err
}
// Calculate the amount to add to the allowance
amount := int((allowanceWeight / sumOfWeights) * float64(reward))
err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?").
Bind(amount, allowanceId, userId).Exec()
if err != nil {
return err
}
reward -= amount
}
// Add the remaining reward to the user
err = tx.Query("update users set balance = balance + ? where id = ?").
Bind(reward, userId).Exec()
if err != nil {
return err
}
}
if err != nil {
return err
}
// Remove the task
err = tx.Query("delete from tasks where id = ?").Bind(taskId).Exec()
return tx.Commit()
}
func (db *Db) AddHistory(userId int, allowance *PostHistory) error { func (db *Db) AddHistory(userId int, allowance *PostHistory) error {
tx, err := db.db.Begin() tx, err := db.db.Begin()
if err != nil { if err != nil {

View File

@ -3,7 +3,7 @@ module allowance_planner
go 1.24.2 go 1.24.2
require ( require (
gitea.seeseepuff.be/seeseemelk/mysqlite v0.12.0 gitea.seeseepuff.be/seeseemelk/mysqlite v0.13.0
github.com/gavv/httpexpect/v2 v2.17.0 github.com/gavv/httpexpect/v2 v2.17.0
github.com/gin-contrib/cors v1.7.5 github.com/gin-contrib/cors v1.7.5
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0

View File

@ -1,5 +1,7 @@
gitea.seeseepuff.be/seeseemelk/mysqlite v0.12.0 h1:kl0VFgvm52UKxJhZpf1hvucxZdOoXY50g/VmzsWH+/8= gitea.seeseepuff.be/seeseemelk/mysqlite v0.12.0 h1:kl0VFgvm52UKxJhZpf1hvucxZdOoXY50g/VmzsWH+/8=
gitea.seeseepuff.be/seeseemelk/mysqlite v0.12.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE= gitea.seeseepuff.be/seeseemelk/mysqlite v0.12.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
gitea.seeseepuff.be/seeseemelk/mysqlite v0.13.0 h1:nqSXu5i5fHB1rrx/kfi8Phn/J6eFa2yh02FiGc9U1yg=
gitea.seeseepuff.be/seeseemelk/mysqlite v0.13.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4= github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4=
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=

View File

@ -268,7 +268,11 @@ func putUserAllowance(c *gin.Context) {
return return
} }
err = db.UpdateAllowance(userId, allowanceId, &allowanceRequest) if allowanceId == 0 {
err = db.UpdateUserAllowance(userId, &allowanceRequest)
} else {
err = db.UpdateAllowance(userId, allowanceId, &allowanceRequest)
}
if err != nil { if err != nil {
log.Printf("Error updating allowance: %v", err) log.Printf("Error updating allowance: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
@ -410,6 +414,29 @@ func deleteTask(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Task deleted successfully"}) c.JSON(http.StatusOK, gin.H{"message": "Task deleted successfully"})
} }
func completeTask(c *gin.Context) {
taskIdStr := c.Param("taskId")
taskId, err := strconv.Atoi(taskIdStr)
if err != nil {
log.Printf("Invalid task ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
return
}
err = db.CompleteTask(taskId)
if errors.Is(err, mysqlite.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
return
}
if err != nil {
log.Printf("Error completing task: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Task completed successfully"})
}
func postHistory(c *gin.Context) { func postHistory(c *gin.Context) {
userIdStr := c.Param("userId") userIdStr := c.Param("userId")
userId, err := strconv.Atoi(userIdStr) userId, err := strconv.Atoi(userIdStr)
@ -491,6 +518,7 @@ func start(ctx context.Context, config *ServerConfig) {
router.GET("/api/task/:taskId", getTask) router.GET("/api/task/:taskId", getTask)
router.PUT("/api/task/:taskId", putTask) router.PUT("/api/task/:taskId", putTask)
router.DELETE("/api/task/:taskId", deleteTask) router.DELETE("/api/task/:taskId", deleteTask)
router.POST("/api/task/:taskId/complete", completeTask)
srv := &http.Server{ srv := &http.Server{
Addr: config.Addr, Addr: config.Addr,

View File

@ -1,7 +1,9 @@
create table users create table users
( (
id integer primary key, id integer primary key,
name text not null name text not null,
weight real not null default 1.0,
balance integer not null default 0
) strict; ) strict;
create table history create table history
@ -18,7 +20,7 @@ create table allowances
user_id integer not null, user_id integer not null,
name text not null, name text not null,
target integer not null, target integer not null,
progress integer not null, balance integer not null default 0,
weight real not null weight real not null
); );