diff --git a/backend/api_test.go b/backend/api_test.go index 2fc70a1..6afd361 100644 --- a/backend/api_test.go +++ b/backend/api_test.go @@ -248,7 +248,16 @@ func TestCreateTask(t *testing.T) { // Verify the response has an 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 assignedUserId := 1 @@ -265,7 +274,7 @@ func TestCreateTask(t *testing.T) { JSON().Object() responseWithUser.ContainsKey("id") - responseWithUser.Value("id").Number().NotEqual(taskId) // Ensure different ID + responseWithUser.Value("id").Number().IsEqual(2) } func TestDeleteTask(t *testing.T) { @@ -345,12 +354,12 @@ func TestGetTaskWhenNoTasks(t *testing.T) { result.Length().IsEqual(0) } -func createTestTask(e *httpexpect.Expect) { +func createTestTask(e *httpexpect.Expect) int { requestBody := map[string]interface{}{ "name": "Test Task", "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) { @@ -522,36 +531,39 @@ func TestPutAllowanceById(t *testing.T) { func TestCompleteTask(t *testing.T) { e := startServer(t) - createTestTask(e) + taskId := createTestTask(e) - // Create a task - requestBody := map[string]interface{}{ - "name": "Test Task", - "reward": 100, - } - e.POST("/tasks").WithJSON(requestBody).Expect().Status(201) + 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(200) + }).Expect().Status(201) e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{ Name: "Test Allowance 1", Target: 1000, Weight: 25, - }).Expect().Status(200) + }).Expect().Status(201) e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{ Weight: 25, - }) + }).Expect().Status(200) // Complete the task - e.POST("/task/1/complete").Expect().Status(200) + e.POST("/task/" + strconv.Itoa(taskId) + "/complete").Expect().Status(200) // Verify the task is marked as completed - result := e.GET("/task/1").Expect().Status(200).JSON().Object() - result.Value("completed").Boolean().IsTrue() + 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) { diff --git a/backend/db.go b/backend/db.go index 31ba5e3..6b3c221 100644 --- a/backend/db.go +++ b/backend/db.go @@ -71,7 +71,14 @@ func (db *Db) GetUserAllowances(userId int) ([]Allowance, error) { allowances := make([]Allowance, 0) 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) { allowance := Allowance{} 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) { 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). ScanSingle(&allowance.ID, &allowance.Name, &allowance.Target, &allowance.Progress, &allowance.Weight) if err != nil { @@ -305,6 +312,65 @@ func (db *Db) UpdateTask(id int, task *CreateTaskRequest) error { 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 { tx, err := db.db.Begin() if err != nil { diff --git a/backend/go.mod b/backend/go.mod index f231ba2..8ce55ec 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -3,7 +3,7 @@ module allowance_planner go 1.24.2 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/gin-contrib/cors v1.7.5 github.com/gin-gonic/gin v1.10.0 diff --git a/backend/go.sum b/backend/go.sum index d07840c..2e7fa76 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,5 +1,7 @@ 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.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/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= diff --git a/backend/main.go b/backend/main.go index ab99b33..53eec79 100644 --- a/backend/main.go +++ b/backend/main.go @@ -414,6 +414,29 @@ func deleteTask(c *gin.Context) { 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) { userIdStr := c.Param("userId") userId, err := strconv.Atoi(userIdStr) @@ -495,6 +518,7 @@ func start(ctx context.Context, config *ServerConfig) { router.GET("/api/task/:taskId", getTask) router.PUT("/api/task/:taskId", putTask) router.DELETE("/api/task/:taskId", deleteTask) + router.POST("/api/task/:taskId/complete", completeTask) srv := &http.Server{ Addr: config.Addr, diff --git a/backend/migrations/1_initial.sql b/backend/migrations/1_initial.sql index abf29fe..c787888 100644 --- a/backend/migrations/1_initial.sql +++ b/backend/migrations/1_initial.sql @@ -20,7 +20,7 @@ create table allowances user_id integer not null, name text not null, target integer not null, - progress integer not null default 0, + balance integer not null default 0, weight real not null );