From 5d803bb01cac64d5af0c864b66120d8a18f70638 Mon Sep 17 00:00:00 2001 From: Sebastiaan de Schaetzen Date: Sun, 18 May 2025 08:45:05 +0200 Subject: [PATCH] Add complete allowance endpoint --- backend/api_test.go | 29 +++++++++++++++++++++ backend/db.go | 33 ++++++++++++++++++++++++ backend/main.go | 44 ++++++++++++++++++++++++++++++++ backend/migrations/1_initial.sql | 2 +- 4 files changed, 107 insertions(+), 1 deletion(-) diff --git a/backend/api_test.go b/backend/api_test.go index eff4f61..9273fa6 100644 --- a/backend/api_test.go +++ b/backend/api_test.go @@ -599,6 +599,35 @@ func TestCompleteTaskInvalidId(t *testing.T) { e.POST("/task/999/complete").Expect().Status(404) } +func TestCompleteAllowance(t *testing.T) { + e := startServer(t) + createTestTaskWithAmount(e, 100) + + // Create allowance goal + e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{ + Name: "Test Allowance 1", + Target: 100, + Weight: 50, + }).Expect().Status(201) + + // Complete the task + e.POST("/task/1/complete").Expect().Status(200) + + // Complete allowance goal + e.POST("/user/1/allowance/1/complete").Expect().Status(200) + + // Verify the allowance no longer exists + e.GET("/user/1/allowance/1").Expect().Status(404) + + // Verify history is updated + history := e.GET("/user/1/history").Expect().Status(200).JSON().Array() + history.Length().IsEqual(2) + history.Value(0).Object().Value("allowance").Number().IsEqual(100) + history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0)) + history.Value(1).Object().Value("allowance").Number().IsEqual(-100) + history.Value(1).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0)) +} + func getDelta(base time.Time, delta float64) (time.Time, time.Time) { start := base.Add(-time.Duration(delta) * time.Second) end := base.Add(time.Duration(delta) * time.Second) diff --git a/backend/db.go b/backend/db.go index 7f9feff..937be11 100644 --- a/backend/db.go +++ b/backend/db.go @@ -175,6 +175,39 @@ func (db *Db) DeleteAllowance(userId int, allowanceId int) error { return nil } +func (db *Db) CompleteAllowance(userId int, allowanceId int) error { + tx, err := db.db.Begin() + if err != nil { + return err + } + defer tx.MustRollback() + + // Get the cost of the allowance + var cost int + err = tx.Query("select balance from allowances where id = ? and user_id = ?"). + Bind(allowanceId, userId).ScanSingle(&cost) + if err != nil { + return err + } + + // Delete the allowance + err = tx.Query("delete from allowances where id = ? and user_id = ?"). + Bind(allowanceId, userId).Exec() + if err != nil { + return err + } + + // Add a history entry + err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)"). + Bind(userId, time.Now().Unix(), -cost). + Exec() + if err != nil { + return err + } + + return tx.Commit() +} + func (db *Db) UpdateUserAllowance(userId int, allowance *UpdateAllowanceRequest) error { tx, err := db.db.Begin() if err != nil { diff --git a/backend/main.go b/backend/main.go index e8567b4..512bdf3 100644 --- a/backend/main.go +++ b/backend/main.go @@ -287,6 +287,49 @@ func putUserAllowance(c *gin.Context) { c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance updated successfully"}) } +func completeAllowance(c *gin.Context) { + userIdStr := c.Param("userId") + allowanceIdStr := c.Param("allowanceId") + + userId, err := strconv.Atoi(userIdStr) + if err != nil { + log.Printf(ErrInvalidUserID+": %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID}) + return + } + + allowanceId, err := strconv.Atoi(allowanceIdStr) + if err != nil { + log.Printf("Invalid allowance ID: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid allowance ID"}) + return + } + + exists, err := db.UserExists(userId) + if err != nil { + log.Printf(ErrCheckingUserExist, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) + return + } + if !exists { + c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound}) + return + } + + err = db.CompleteAllowance(userId, allowanceId) + if errors.Is(err, mysqlite.ErrNoRows) { + c.JSON(http.StatusNotFound, gin.H{"error": "Allowance not found"}) + return + } + if err != nil { + log.Printf("Error completing allowance: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) + return + } + + c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance completed successfully"}) +} + func createTask(c *gin.Context) { var taskRequest CreateTaskRequest if err := c.ShouldBindJSON(&taskRequest); err != nil { @@ -518,6 +561,7 @@ func start(ctx context.Context, config *ServerConfig) { router.GET("/api/user/:userId/allowance/:allowanceId", getUserAllowanceById) router.DELETE("/api/user/:userId/allowance/:allowanceId", deleteUserAllowance) router.PUT("/api/user/:userId/allowance/:allowanceId", putUserAllowance) + router.POST("/api/user/:userId/allowance/:allowanceId/complete", completeAllowance) router.POST("/api/tasks", createTask) router.GET("/api/tasks", getTasks) router.GET("/api/task/:taskId", getTask) diff --git a/backend/migrations/1_initial.sql b/backend/migrations/1_initial.sql index c787888..d400ec8 100644 --- a/backend/migrations/1_initial.sql +++ b/backend/migrations/1_initial.sql @@ -2,7 +2,7 @@ create table users ( id integer primary key, name text not null, - weight real not null default 1.0, + weight real not null default 0.0, balance integer not null default 0 ) strict;