Compare commits
No commits in common. "2aa58f1b651764dc3de1c8af7622b3f20e5e370c" and "5a20e76df2b7a23c617ae2e8d0d10f7a8b21b419" have entirely different histories.
2aa58f1b65
...
5a20e76df2
@ -2,11 +2,10 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gavv/httpexpect/v2"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gavv/httpexpect/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -915,88 +914,3 @@ func createTestAllowance(e *httpexpect.Expect, name string, target float64, weig
|
||||
func createTestTask(e *httpexpect.Expect) int {
|
||||
return createTestTaskWithAmount(e, 100)
|
||||
}
|
||||
|
||||
// Transfer tests
|
||||
func TestTransferSuccessful(t *testing.T) {
|
||||
e := startServer(t)
|
||||
|
||||
// Create two allowances for user 1
|
||||
createTestAllowance(e, "From Allowance", 100, 1)
|
||||
createTestAllowance(e, "To Allowance", 100, 1)
|
||||
|
||||
// Add 30 to allowance 1
|
||||
req := map[string]interface{}{"amount": 30, "description": "funds"}
|
||||
e.POST("/user/1/allowance/1/add").WithJSON(req).Expect().Status(200)
|
||||
|
||||
// Transfer 10 from 1 to 2
|
||||
transfer := map[string]interface{}{"from": 1, "to": 2, "amount": 10}
|
||||
e.POST("/transfer").WithJSON(transfer).Expect().Status(200).JSON().Object().Value("message").IsEqual("Transfer successful")
|
||||
|
||||
// Verify balances
|
||||
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
|
||||
allowances.Value(1).Object().Value("progress").Number().InDelta(20.0, 0.01)
|
||||
allowances.Value(2).Object().Value("progress").Number().InDelta(10.0, 0.01)
|
||||
}
|
||||
|
||||
func TestTransferCapsAtTarget(t *testing.T) {
|
||||
e := startServer(t)
|
||||
|
||||
// Create two allowances
|
||||
createTestAllowance(e, "From Allowance", 100, 1)
|
||||
createTestAllowance(e, "To Allowance", 5, 1)
|
||||
|
||||
// Add 10 to allowance 1
|
||||
req := map[string]interface{}{"amount": 10, "description": "funds"}
|
||||
e.POST("/user/1/allowance/1/add").WithJSON(req).Expect().Status(200)
|
||||
|
||||
// Transfer 10 from 1 to 2, but to only needs 5
|
||||
transfer := map[string]interface{}{"from": 1, "to": 2, "amount": 10}
|
||||
e.POST("/transfer").WithJSON(transfer).Expect().Status(200)
|
||||
|
||||
// Verify capped transfer
|
||||
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
|
||||
allowances.Value(1).Object().Value("progress").Number().InDelta(5.0, 0.01) // from had 10, transferred 5 -> left 5
|
||||
allowances.Value(2).Object().Value("progress").Number().InDelta(5.0, 0.01) // to reached target
|
||||
}
|
||||
|
||||
func TestTransferDifferentUsersFails(t *testing.T) {
|
||||
e := startServer(t)
|
||||
|
||||
// Create allowance for user 1 and user 2
|
||||
createTestAllowance(e, "User1 Allowance", 100, 1)
|
||||
// create for user 2
|
||||
e.POST("/user/2/allowance").WithJSON(CreateAllowanceRequest{Name: "User2 Allowance", Target: 100, Weight: 1}).Expect().Status(201)
|
||||
|
||||
// Add to user1 allowance
|
||||
req := map[string]interface{}{"amount": 10, "description": "funds"}
|
||||
e.POST("/user/1/allowance/1/add").WithJSON(req).Expect().Status(200)
|
||||
|
||||
// Attempt transfer between different users
|
||||
transfer := map[string]interface{}{"from": 1, "to": 1 /* wrong id to simulate different user's id? */}
|
||||
// To ensure different user, fetch the allowance id for user2 (it's 1 for user2 in its own context but global id will be 2)
|
||||
// Create above for user2 produced global id 2, so use that
|
||||
transfer = map[string]interface{}{"from": 1, "to": 2, "amount": 5}
|
||||
e.POST("/transfer").WithJSON(transfer).Expect().Status(400)
|
||||
}
|
||||
|
||||
func TestTransferInsufficientFunds(t *testing.T) {
|
||||
e := startServer(t)
|
||||
|
||||
// Create two allowances
|
||||
createTestAllowance(e, "From Allowance", 100, 1)
|
||||
createTestAllowance(e, "To Allowance", 100, 1)
|
||||
|
||||
// Ensure from has 0 balance
|
||||
transfer := map[string]interface{}{"from": 1, "to": 2, "amount": 10}
|
||||
resp := e.POST("/transfer").WithJSON(transfer).Expect().Status(400).JSON().Object()
|
||||
// Error text should mention insufficient funds
|
||||
resp.Value("error").String().Contains("insufficient")
|
||||
}
|
||||
|
||||
func TestTransferNotFound(t *testing.T) {
|
||||
e := startServer(t)
|
||||
|
||||
// No allowances exist yet (only user rows). Attempt transfer with non-existent IDs
|
||||
transfer := map[string]interface{}{"from": 999, "to": 1000, "amount": 1}
|
||||
e.POST("/transfer").WithJSON(transfer).Expect().Status(404)
|
||||
}
|
||||
|
@ -7,8 +7,6 @@ import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/adhocore/gronx"
|
||||
|
||||
"gitea.seeseepuff.be/seeseemelk/mysqlite"
|
||||
)
|
||||
|
||||
@ -633,74 +631,3 @@ func (db *Db) AddAllowanceAmount(userId int, allowanceId int, request AddAllowan
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// TransferAllowance transfers amount from one allowance goal to another.
|
||||
// Both allowance ids must exist and belong to the same user. The transfer
|
||||
// will not move more than the 'to' goal still needs (target - balance).
|
||||
func (db *Db) TransferAllowance(fromId int, toId int, amount float64) error {
|
||||
if fromId == toId {
|
||||
return nil
|
||||
}
|
||||
amountCents := int(math.Round(amount * 100.0))
|
||||
if amountCents <= 0 {
|
||||
return fmt.Errorf("amount must be positive")
|
||||
}
|
||||
|
||||
tx, err := db.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.MustRollback()
|
||||
|
||||
// Fetch from allowance (user_id, balance)
|
||||
var fromUserId int
|
||||
var fromBalance int
|
||||
err = tx.Query("select user_id, balance from allowances where id = ?").Bind(fromId).ScanSingle(&fromUserId, &fromBalance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fetch to allowance (user_id, target, balance)
|
||||
var toUserId int
|
||||
var toTarget int
|
||||
var toBalance int
|
||||
err = tx.Query("select user_id, target, balance from allowances where id = ?").Bind(toId).ScanSingle(&toUserId, &toTarget, &toBalance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure same owner
|
||||
if fromUserId != toUserId {
|
||||
return fmt.Errorf("allowances do not belong to the same user")
|
||||
}
|
||||
|
||||
// Calculate how much the 'to' goal still needs
|
||||
remainingTo := toTarget - toBalance
|
||||
if remainingTo <= 0 {
|
||||
// Nothing to transfer
|
||||
return fmt.Errorf("target already reached")
|
||||
}
|
||||
|
||||
// Limit transfer to what 'to' still needs
|
||||
transfer := amountCents
|
||||
if transfer > remainingTo {
|
||||
transfer = remainingTo
|
||||
}
|
||||
|
||||
// Ensure 'from' has enough balance
|
||||
if fromBalance < transfer {
|
||||
return fmt.Errorf("insufficient funds in source allowance")
|
||||
}
|
||||
|
||||
// Perform updates
|
||||
err = tx.Query("update allowances set balance = balance - ? where id = ? and user_id = ?").Bind(transfer, fromId, fromUserId).Exec()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?").Bind(transfer, toId, toUserId).Exec()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
@ -78,10 +78,3 @@ type AddAllowanceAmountRequest struct {
|
||||
Amount float64 `json:"amount"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// TransferRequest represents a request to transfer amount between two goals.
|
||||
type TransferRequest struct {
|
||||
From int `json:"from"`
|
||||
To int `json:"to"`
|
||||
Amount float64 `json:"amount"`
|
||||
}
|
||||
|
@ -4,11 +4,9 @@ go 1.24.2
|
||||
|
||||
require (
|
||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0
|
||||
github.com/adhocore/gronx v1.19.6
|
||||
github.com/gavv/httpexpect/v2 v2.17.0
|
||||
github.com/gin-contrib/cors v1.7.5
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
)
|
||||
|
||||
require (
|
||||
@ -49,6 +47,7 @@ require (
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/sanity-io/litter v1.5.8 // indirect
|
||||
github.com/sergi/go-diff v1.3.1 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
|
@ -2,8 +2,6 @@ gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 h1:aRItVfUj48fBmuec7rm/jY9KCfvHW
|
||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.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/adhocore/gronx v1.19.6 h1:5KNVcoR9ACgL9HhEqCm5QXsab/gI4QDIybTAWcXDKDc=
|
||||
github.com/adhocore/gronx v1.19.6/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
|
@ -4,15 +4,13 @@ import (
|
||||
"context"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gitea.seeseepuff.be/seeseemelk/mysqlite"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"gitea.seeseepuff.be/seeseemelk/mysqlite"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@ -438,14 +436,6 @@ func createTask(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if taskRequest.Schedule != nil {
|
||||
valid := gronx.IsValid(*taskRequest.Schedule)
|
||||
if !valid {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid cron schedule: %s", *taskRequest.Schedule)})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If assigned is not nil, check if user exists
|
||||
if taskRequest.Assigned != nil {
|
||||
exists, err := db.UserExists(*taskRequest.Assigned)
|
||||
@ -523,11 +513,6 @@ func putTask(c *gin.Context) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("Error getting task: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
|
||||
return
|
||||
}
|
||||
|
||||
err = db.UpdateTask(taskId, &taskRequest)
|
||||
if err != nil {
|
||||
|
@ -409,59 +409,6 @@ paths:
|
||||
404:
|
||||
description: The task could not be found.
|
||||
|
||||
/api/transfer:
|
||||
post:
|
||||
summary: Transfer amount between allowances
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
from:
|
||||
type: integer
|
||||
description: Source allowance ID
|
||||
to:
|
||||
type: integer
|
||||
description: Destination allowance ID
|
||||
amount:
|
||||
type: number
|
||||
format: float
|
||||
description: Amount to transfer
|
||||
required:
|
||||
- from
|
||||
- to
|
||||
- amount
|
||||
responses:
|
||||
'200':
|
||||
description: Transfer successful
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
'400':
|
||||
description: Invalid request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
'404':
|
||||
description: Allowance not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
|
||||
components:
|
||||
schemas:
|
||||
task:
|
||||
|
Loading…
x
Reference in New Issue
Block a user