feat(transaction): #66 implement transactions
This commit is contained in:
28
db/error.go
28
db/error.go
@@ -1,10 +1,38 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
|
"spend-sparrow/log"
|
||||||
|
"spend-sparrow/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNotFound = errors.New("the value does not exist")
|
ErrNotFound = errors.New("the value does not exist")
|
||||||
ErrAlreadyExists = errors.New("row already exists")
|
ErrAlreadyExists = errors.New("row already exists")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TransformAndLogDbError(module string, r sql.Result, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
log.Error("%v: %v", module, err)
|
||||||
|
return types.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
if r != nil {
|
||||||
|
rows, err := r.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("%v: %v", module, err)
|
||||||
|
return types.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows == 0 {
|
||||||
|
log.Info("%v: not found", module)
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"spend-sparrow/log"
|
|
||||||
"spend-sparrow/types"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
)
|
|
||||||
|
|
||||||
// While it may be duplicated to check for userId in the database access, it serves as a security layer
|
|
||||||
type Transaction interface {
|
|
||||||
Insert(userId uuid.UUID, transaction *types.Transaction) error
|
|
||||||
Update(userId uuid.UUID, transaction *types.Transaction) error
|
|
||||||
GetAll(userId uuid.UUID) ([]*types.Transaction, error)
|
|
||||||
Get(userId uuid.UUID, id uuid.UUID) (*types.Transaction, error)
|
|
||||||
Delete(userId uuid.UUID, id uuid.UUID) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type TransactionSqlite struct {
|
|
||||||
db *sqlx.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTransactionSqlite(db *sqlx.DB) *TransactionSqlite {
|
|
||||||
return &TransactionSqlite{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db TransactionSqlite) Insert(userId uuid.UUID, transaction *types.Transaction) error {
|
|
||||||
|
|
||||||
_, err := db.db.Exec(`
|
|
||||||
INSERT INTO transaction (id, user_id, account_id, treasure_chest_id, internal, value, timestamp, note, created_at, created_by)
|
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?)`, transaction.Id, userId, transaction.AccountId, transaction.TreasureChestId, transaction.Internal, transaction.Value, transaction.Timestamp, transaction.Note, transaction.CreatedAt, transaction.CreatedBy)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("transaction Insert: %v", err)
|
|
||||||
return types.ErrInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db TransactionSqlite) Update(userId uuid.UUID, transaction *types.Transaction) error {
|
|
||||||
|
|
||||||
r, err := db.db.Exec(`
|
|
||||||
UPDATE transaction
|
|
||||||
SET
|
|
||||||
account_id = ?,
|
|
||||||
treasure_chest_id = ?,
|
|
||||||
internal = ?,
|
|
||||||
value = ?,
|
|
||||||
timestamp = ?,
|
|
||||||
note = ?,
|
|
||||||
updated_at = ?,
|
|
||||||
updated_by = ?
|
|
||||||
WHERE id = ?
|
|
||||||
AND user_id = ?`, transaction.AccountId, transaction.TreasureChestId, transaction.Internal, transaction.Value, transaction.Timestamp, transaction.UpdatedBy, transaction.Id, userId)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("transaction Update: %v", err)
|
|
||||||
return types.ErrInternal
|
|
||||||
}
|
|
||||||
rows, err := r.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("transaction Update: %v", err)
|
|
||||||
return types.ErrInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
if rows == 0 {
|
|
||||||
log.Info("transaction Update: not found")
|
|
||||||
return ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db TransactionSqlite) GetAll(userId uuid.UUID) ([]*types.Transaction, error) {
|
|
||||||
|
|
||||||
transactions := make([]*types.Transaction, 0)
|
|
||||||
err := db.db.Select(&transactions, `
|
|
||||||
SELECT
|
|
||||||
id, user_id,
|
|
||||||
account_id, treasure_chest_id, internal, value, timestamp, note,
|
|
||||||
created_at, created_by, updated_at, updated_by
|
|
||||||
FROM transaction
|
|
||||||
WHERE user_id = ?
|
|
||||||
ORDER BY name`, userId)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("transaction GetAll: %v", err)
|
|
||||||
return nil, types.ErrInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
return transactions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db TransactionSqlite) Get(userId uuid.UUID, id uuid.UUID) (*types.Transaction, error) {
|
|
||||||
|
|
||||||
transaction := &types.Transaction{}
|
|
||||||
err := db.db.Get(transaction, `
|
|
||||||
SELECT
|
|
||||||
id, user_id,
|
|
||||||
account_id, treasure_chest_id, internal, value, timestamp, note,
|
|
||||||
created_at, created_by, updated_at, updated_by
|
|
||||||
FROM transaction
|
|
||||||
WHERE user_id = ?
|
|
||||||
AND id = ?`, userId, id)
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return nil, ErrNotFound
|
|
||||||
}
|
|
||||||
log.Error("transaction Get: %v", err)
|
|
||||||
return nil, types.ErrInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
return transaction, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db TransactionSqlite) Delete(userId uuid.UUID, id uuid.UUID) error {
|
|
||||||
|
|
||||||
res, err := db.db.Exec("DELETE FROM transaction WHERE id = ? and user_id = ?", id, userId)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("transaction Delete: %v", err)
|
|
||||||
return types.ErrInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := res.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("transaction Delete: %v", err)
|
|
||||||
return types.ErrInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
if rows == 0 {
|
|
||||||
log.Info("transaction Delete: not found")
|
|
||||||
return ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -16,16 +16,20 @@ type Transaction interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TransactionImpl struct {
|
type TransactionImpl struct {
|
||||||
s service.Transaction
|
s service.Transaction
|
||||||
a service.Auth
|
account service.Account
|
||||||
r *Render
|
treasureChest service.TreasureChest
|
||||||
|
a service.Auth
|
||||||
|
r *Render
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransaction(s service.Transaction, a service.Auth, r *Render) Transaction {
|
func NewTransaction(s service.Transaction, account service.Account, treasureChest service.TreasureChest, a service.Auth, r *Render) Transaction {
|
||||||
return TransactionImpl{
|
return TransactionImpl{
|
||||||
s: s,
|
s: s,
|
||||||
a: a,
|
account: account,
|
||||||
r: r,
|
treasureChest: treasureChest,
|
||||||
|
a: a,
|
||||||
|
r: r,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +54,19 @@ func (h TransactionImpl) handleTransactionPage() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
comp := t.Transaction(transactions)
|
accounts, err := h.account.GetAll(user)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
treasureChests, err := h.treasureChest.GetAll(user)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
comp := t.Transaction(transactions, accounts, treasureChests)
|
||||||
h.r.RenderLayout(r, w, comp, user)
|
h.r.RenderLayout(r, w, comp, user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,9 +79,21 @@ func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accounts, err := h.account.GetAll(user)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
treasureChests, err := h.treasureChest.GetAll(user)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
id := r.PathValue("id")
|
id := r.PathValue("id")
|
||||||
if id == "new" {
|
if id == "new" {
|
||||||
comp := t.EditTransaction(nil)
|
comp := t.EditTransaction(nil, accounts, treasureChests)
|
||||||
h.r.Render(r, w, comp)
|
h.r.Render(r, w, comp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -78,7 +106,7 @@ func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc {
|
|||||||
|
|
||||||
var comp templ.Component
|
var comp templ.Component
|
||||||
if r.URL.Query().Get("edit") == "true" {
|
if r.URL.Query().Get("edit") == "true" {
|
||||||
comp = t.EditTransaction(transaction)
|
comp = t.EditTransaction(transaction, accounts, treasureChests)
|
||||||
} else {
|
} else {
|
||||||
comp = t.TransactionItem(transaction)
|
comp = t.TransactionItem(transaction)
|
||||||
}
|
}
|
||||||
@@ -98,16 +126,23 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc {
|
|||||||
transaction *types.Transaction
|
transaction *types.Transaction
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
id := r.PathValue("id")
|
input := types.TransactionInput{
|
||||||
name := r.FormValue("name")
|
Id: r.PathValue("id"),
|
||||||
if id == "new" {
|
AccountId: r.FormValue("account_id"),
|
||||||
transaction, err = h.s.Add(user, name)
|
TreasureChestId: r.FormValue("treasure_chest_id"),
|
||||||
|
Value: r.FormValue("value"),
|
||||||
|
Timestamp: r.FormValue("timestamp"),
|
||||||
|
Note: r.FormValue("note"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.Id == "new" {
|
||||||
|
transaction, err = h.s.Add(user, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
transaction, err = h.s.Update(user, id, name)
|
transaction, err = h.s.Update(user, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
|
|||||||
10
input.css
10
input.css
@@ -24,6 +24,16 @@ input:focus {
|
|||||||
--font-shippori: "Shippori Mincho", sans-serif;
|
--font-shippori: "Shippori Mincho", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select,
|
||||||
|
::picker(select) {
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
option {
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
::picker(select) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Button */
|
/* Button */
|
||||||
.button {
|
.button {
|
||||||
|
|||||||
3
main.go
3
main.go
@@ -117,17 +117,20 @@ func createHandler(d *sqlx.DB, serverSettings *types.Settings) http.Handler {
|
|||||||
authService := service.NewAuth(authDb, randomService, clockService, mailService, serverSettings)
|
authService := service.NewAuth(authDb, randomService, clockService, mailService, serverSettings)
|
||||||
accountService := service.NewAccount(accountDb, randomService, clockService, serverSettings)
|
accountService := service.NewAccount(accountDb, randomService, clockService, serverSettings)
|
||||||
treasureChestService := service.NewTreasureChest(treasureChestDb, randomService, clockService, serverSettings)
|
treasureChestService := service.NewTreasureChest(treasureChestDb, randomService, clockService, serverSettings)
|
||||||
|
transactionService := service.NewTransaction(d, randomService, clockService, serverSettings)
|
||||||
|
|
||||||
render := handler.NewRender()
|
render := handler.NewRender()
|
||||||
indexHandler := handler.NewIndex(authService, render)
|
indexHandler := handler.NewIndex(authService, render)
|
||||||
authHandler := handler.NewAuth(authService, render)
|
authHandler := handler.NewAuth(authService, render)
|
||||||
accountHandler := handler.NewAccount(accountService, authService, render)
|
accountHandler := handler.NewAccount(accountService, authService, render)
|
||||||
treasureChestHandler := handler.NewTreasureChest(treasureChestService, authService, render)
|
treasureChestHandler := handler.NewTreasureChest(treasureChestService, authService, render)
|
||||||
|
transactionHandler := handler.NewTransaction(transactionService, accountService, treasureChestService, authService, render)
|
||||||
|
|
||||||
indexHandler.Handle(router)
|
indexHandler.Handle(router)
|
||||||
accountHandler.Handle(router)
|
accountHandler.Handle(router)
|
||||||
treasureChestHandler.Handle(router)
|
treasureChestHandler.Handle(router)
|
||||||
authHandler.Handle(router)
|
authHandler.Handle(router)
|
||||||
|
transactionHandler.Handle(router)
|
||||||
|
|
||||||
// Serve static files (CSS, JS and images)
|
// Serve static files (CSS, JS and images)
|
||||||
router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
|
router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ CREATE TABLE account (
|
|||||||
|
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
|
|
||||||
current_balance int64 NOT NULL,
|
current_balance INTEGER NOT NULL,
|
||||||
last_transaction DATETIME,
|
last_transaction DATETIME,
|
||||||
oink_balance int64 NOT NULL,
|
oink_balance INTEGER NOT NULL,
|
||||||
|
|
||||||
created_at DATETIME NOT NULL,
|
created_at DATETIME NOT NULL,
|
||||||
created_by TEXT NOT NULL,
|
created_by TEXT NOT NULL,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ CREATE TABLE treasure_chest (
|
|||||||
|
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
|
|
||||||
current_balance int64 NOT NULL,
|
current_balance INTEGER NOT NULL,
|
||||||
|
|
||||||
created_at DATETIME NOT NULL,
|
created_at DATETIME NOT NULL,
|
||||||
created_by TEXT NOT NULL,
|
created_by TEXT NOT NULL,
|
||||||
|
|||||||
17
migration/004_transaction.up.sql
Normal file
17
migration/004_transaction.up.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
CREATE TABLE "transaction" (
|
||||||
|
id TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
timestamp DATETIME NOT NULL,
|
||||||
|
note TEXT NOT NULL,
|
||||||
|
|
||||||
|
account_id TEXT,
|
||||||
|
treasure_chest_id TEXT,
|
||||||
|
value INTEGER NOT NULL,
|
||||||
|
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
created_by TEXT NOT NULL,
|
||||||
|
updated_at DATETIME,
|
||||||
|
updated_by TEXT
|
||||||
|
) WITHOUT ROWID;
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss -i input.css -o static/css/tailwind.css --minify",
|
"build": "cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss -i input.css -o static/css/tailwind.css --minify",
|
||||||
"watch": "cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss -i input.css -o static/css/tailwind.css --watch"
|
"watch": "cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss -i input.css -o static/css/tailwind.css --watch --minify"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"spend-sparrow/types"
|
"spend-sparrow/types"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
)
|
)
|
||||||
@@ -25,87 +26,93 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Transaction interface {
|
type Transaction interface {
|
||||||
Add(user *types.User, accountId, treasureChestId, internal, value, timestamp, note string) (*types.Transaction, error)
|
Add(user *types.User, transaction types.TransactionInput) (*types.Transaction, error)
|
||||||
Update(user *types.User, id, accountId, treasureChestId, internal, value, timestamp, note string) (*types.Transaction, error)
|
Update(user *types.User, transaction types.TransactionInput) (*types.Transaction, error)
|
||||||
Get(user *types.User, id string) (*types.Transaction, error)
|
Get(user *types.User, id string) (*types.Transaction, error)
|
||||||
GetAll(user *types.User) ([]*types.Transaction, error)
|
GetAll(user *types.User) ([]*types.Transaction, error)
|
||||||
Delete(user *types.User, id string) error
|
Delete(user *types.User, id string) error
|
||||||
CanDeleteTreasureChest(user *types.User, treasureChestId uuid.UUID) (bool, error)
|
|
||||||
CanDeleteAccount(user *types.User, accountId uuid.UUID) (bool, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransactionImpl struct {
|
type TransactionImpl struct {
|
||||||
db db.Transaction
|
db *sqlx.DB
|
||||||
account Account
|
clock Clock
|
||||||
treasureChest TreasureChest
|
random Random
|
||||||
clock Clock
|
settings *types.Settings
|
||||||
random Random
|
|
||||||
settings *types.Settings
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransaction(db db.Transaction, account Account, treasureChest TreasureChest, random Random, clock Clock, settings *types.Settings) Transaction {
|
func NewTransaction(db *sqlx.DB, random Random, clock Clock, settings *types.Settings) Transaction {
|
||||||
return TransactionImpl{
|
return TransactionImpl{
|
||||||
db: db,
|
db: db,
|
||||||
account: account,
|
clock: clock,
|
||||||
treasureChest: treasureChest,
|
random: random,
|
||||||
clock: clock,
|
settings: settings,
|
||||||
random: random,
|
|
||||||
settings: settings,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionImpl) Add(user *types.User, accountId, treasureChestId, internal, value, timestamp, note string) (*types.Transaction, error) {
|
func (s TransactionImpl) Add(user *types.User, transactionInput types.TransactionInput) (*types.Transaction, error) {
|
||||||
transactionMetric.WithLabelValues("add").Inc()
|
transactionMetric.WithLabelValues("add").Inc()
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction, err := s.validateTransaction(nil, user.Id, accountId, treasureChestId, internal, value, timestamp, note)
|
transaction, err := s.validateAndEnrichTransaction(nil, user.Id, transactionInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.db.Insert(user.Id, transaction)
|
r, err := s.db.NamedExec(`
|
||||||
|
INSERT INTO "transaction" (id, user_id, account_id, treasure_chest_id, value, timestamp, note, created_at, created_by)
|
||||||
|
VALUES (:id, :user_id, :account_id, :treasure_chest_id, :value, :timestamp, :note, :created_at, :created_by)`, transaction)
|
||||||
|
err = db.TransformAndLogDbError("transaction Insert", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, types.ErrInternal
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
savedTransaction, err := s.db.Get(user.Id, transaction.Id)
|
return transaction, nil
|
||||||
if err != nil {
|
|
||||||
log.Error("transaction %v not found after insert: %v", transaction.Id, err)
|
|
||||||
return nil, types.ErrInternal
|
|
||||||
}
|
|
||||||
return savedTransaction, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionImpl) Update(user *types.User, id, accountId, treasureChestId, internal, value, timestamp, note string) (*types.Transaction, error) {
|
func (s TransactionImpl) Update(user *types.User, input types.TransactionInput) (*types.Transaction, error) {
|
||||||
transactionMetric.WithLabelValues("update").Inc()
|
transactionMetric.WithLabelValues("update").Inc()
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
uuid, err := uuid.Parse(id)
|
uuid, err := uuid.Parse(input.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("transaction update: %v", err)
|
log.Error("transaction update: %v", err)
|
||||||
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction, err := s.db.Get(user.Id, uuid)
|
var transaction *types.Transaction
|
||||||
|
err = s.db.Select(transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
||||||
|
err = db.TransformAndLogDbError("transaction Get", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == db.ErrNotFound {
|
if err == db.ErrNotFound {
|
||||||
return nil, fmt.Errorf("transaction %v not found: %w", id, ErrBadRequest)
|
return nil, fmt.Errorf("transaction %v not found: %w", input.Id, ErrBadRequest)
|
||||||
}
|
}
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction, err = s.validateTransaction(transaction, user.Id, accountId, treasureChestId, internal, value, timestamp, note)
|
transaction, err = s.validateAndEnrichTransaction(transaction, user.Id, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.db.Update(user.Id, transaction)
|
r, err := s.db.NamedExec(`
|
||||||
|
UPDATE transaction
|
||||||
|
SET
|
||||||
|
account_id = :account_id,
|
||||||
|
treasure_chest_id = :treasure_chest_id,
|
||||||
|
value = :value,
|
||||||
|
timestamp = :timestamp,
|
||||||
|
note = :note,
|
||||||
|
updated_at = :updated_at,
|
||||||
|
updated_by = :updated_by
|
||||||
|
WHERE id = :id
|
||||||
|
AND user_id = :user_id`, transaction)
|
||||||
|
err = db.TransformAndLogDbError("transaction Update", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, types.ErrInternal
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return transaction, nil
|
return transaction, nil
|
||||||
@@ -123,7 +130,9 @@ func (s TransactionImpl) Get(user *types.User, id string) (*types.Transaction, e
|
|||||||
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction, err := s.db.Get(user.Id, uuid)
|
var transaction *types.Transaction
|
||||||
|
err = s.db.Select(transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
||||||
|
err = db.TransformAndLogDbError("transaction Get", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == db.ErrNotFound {
|
if err == db.ErrNotFound {
|
||||||
return nil, fmt.Errorf("transaction %v not found: %w", id, ErrBadRequest)
|
return nil, fmt.Errorf("transaction %v not found: %w", id, ErrBadRequest)
|
||||||
@@ -139,10 +148,11 @@ func (s TransactionImpl) GetAll(user *types.User) ([]*types.Transaction, error)
|
|||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
transactions := make([]*types.Transaction, 0)
|
||||||
transactions, err := s.db.GetAll(user.Id)
|
err := s.db.Select(&transactions, `SELECT * FROM "transaction" WHERE user_id = ? ORDER BY timestamp`, user.Id)
|
||||||
|
err = db.TransformAndLogDbError("transaction GetAll", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, types.ErrInternal
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return transactions, nil
|
return transactions, nil
|
||||||
@@ -159,39 +169,28 @@ func (s TransactionImpl) Delete(user *types.User, id string) error {
|
|||||||
return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction, err := s.db.Get(user.Id, uuid)
|
r, err := s.db.Exec("DELETE FROM \"transaction\" WHERE id = ? AND user_id = ?", uuid, user.Id)
|
||||||
|
err = db.TransformAndLogDbError("transaction Delete", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == db.ErrNotFound {
|
return err
|
||||||
return fmt.Errorf("transaction %v not found: %w", id, ErrBadRequest)
|
|
||||||
}
|
|
||||||
return types.ErrInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
if transaction.UserId != user.Id {
|
|
||||||
return types.ErrUnauthorized
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.db.Delete(user.Id, transaction.Id)
|
|
||||||
if err != nil {
|
|
||||||
return types.ErrInternal
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionImpl) validateTransaction(transaction *types.Transaction, userId uuid.UUID, accountId, treasureChestId, internal, value, timestamp, note string) (*types.Transaction, error) {
|
func (s TransactionImpl) validateAndEnrichTransaction(transaction *types.Transaction, userId uuid.UUID, input types.TransactionInput) (*types.Transaction, error) {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
id uuid.UUID
|
id uuid.UUID
|
||||||
accountUuid uuid.UUID
|
accountUuid uuid.UUID
|
||||||
treasureChestUuid uuid.UUID
|
treasureChestUuid uuid.UUID
|
||||||
internalBool bool
|
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
createdBy uuid.UUID
|
createdBy uuid.UUID
|
||||||
updatedAt *time.Time
|
updatedAt *time.Time
|
||||||
updatedBy uuid.UUID
|
updatedBy uuid.UUID
|
||||||
|
|
||||||
err error
|
err error
|
||||||
|
rowCount int
|
||||||
)
|
)
|
||||||
|
|
||||||
if transaction == nil {
|
if transaction == nil {
|
||||||
@@ -210,61 +209,71 @@ func (s TransactionImpl) validateTransaction(transaction *types.Transaction, use
|
|||||||
updatedBy = userId
|
updatedBy = userId
|
||||||
}
|
}
|
||||||
|
|
||||||
if accountId != "" {
|
if input.AccountId != "" {
|
||||||
accountUuid, err = uuid.Parse(accountId)
|
accountUuid, err = uuid.Parse(input.AccountId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("transaction validate: %v", err)
|
log.Error("transaction validate: %v", err)
|
||||||
return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest)
|
return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
err = s.db.Select(&rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, accountUuid, userId)
|
||||||
|
err = db.TransformAndLogDbError("transaction validate", nil, err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if rowCount == 0 {
|
||||||
|
log.Error("transaction validate: %v", err)
|
||||||
|
return nil, fmt.Errorf("account not found: %w", ErrBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if treasureChestId != "" {
|
if input.TreasureChestId != "" {
|
||||||
treasureChestUuid, err = uuid.Parse(treasureChestId)
|
treasureChestUuid, err = uuid.Parse(input.TreasureChestId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("transaction validate: %v", err)
|
log.Error("transaction validate: %v", err)
|
||||||
return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest)
|
return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
err = s.db.Select(&rowCount, `SELECT COUNT(*) FROM treasure_chest WHERE id = ? AND user_id = ?`, accountUuid, userId)
|
||||||
|
err = db.TransformAndLogDbError("transaction validate", nil, err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if rowCount == 0 {
|
||||||
|
log.Error("transaction validate: %v", err)
|
||||||
|
return nil, fmt.Errorf("treasure chest not found: %w", ErrBadRequest)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internalBool, err = strconv.ParseBool(internal)
|
valueInt, err := strconv.ParseInt(input.Value, 10, 64)
|
||||||
if err != nil {
|
|
||||||
log.Error("transaction validate: %v", err)
|
|
||||||
return nil, fmt.Errorf("could not parse internal: %w", ErrBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
valueInt, err := strconv.ParseInt(value, 10, 64)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("transaction validate: %v", err)
|
log.Error("transaction validate: %v", err)
|
||||||
return nil, fmt.Errorf("could not parse value: %w", ErrBadRequest)
|
return nil, fmt.Errorf("could not parse value: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
timestampTime, err := time.Parse(time.RFC3339, timestamp)
|
timestampTime, err := time.Parse(time.RFC3339, input.Timestamp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("transaction validate: %v", err)
|
log.Error("transaction validate: %v", err)
|
||||||
return nil, fmt.Errorf("could not parse timestamp: %w", ErrBadRequest)
|
return nil, fmt.Errorf("could not parse timestamp: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = validateString(note)
|
err = validateString(input.Note)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
result := types.Transaction{
|
return &types.Transaction{
|
||||||
Id: id,
|
Id: id,
|
||||||
UserId: userId,
|
UserId: userId,
|
||||||
|
|
||||||
AccountId: accountUuid,
|
AccountId: &accountUuid,
|
||||||
TreasureChestId: &treasureChestUuid,
|
TreasureChestId: &treasureChestUuid,
|
||||||
Internal: internalBool,
|
|
||||||
Value: valueInt,
|
Value: valueInt,
|
||||||
Timestamp: timestampTime,
|
Timestamp: timestampTime,
|
||||||
Note: note,
|
Note: input.Note,
|
||||||
|
|
||||||
CreatedAt: createdAt,
|
CreatedAt: createdAt,
|
||||||
CreatedBy: createdBy,
|
CreatedBy: createdBy,
|
||||||
UpdatedAt: updatedAt,
|
UpdatedAt: updatedAt,
|
||||||
UpdatedBy: &updatedBy,
|
UpdatedBy: &updatedBy,
|
||||||
}
|
}, nil
|
||||||
|
|
||||||
return &result, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ func (s TreasureChestImpl) GetAll(user *types.User) ([]*types.TreasureChest, err
|
|||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
return treasureChests, nil
|
return sortTree(treasureChests), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TreasureChestImpl) Delete(user *types.User, idStr string) error {
|
func (s TreasureChestImpl) Delete(user *types.User, idStr string) error {
|
||||||
@@ -237,3 +237,37 @@ func (s TreasureChestImpl) Delete(user *types.User, idStr string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sortTree(nodes []*types.TreasureChest) []*types.TreasureChest {
|
||||||
|
|
||||||
|
var (
|
||||||
|
roots []*types.TreasureChest
|
||||||
|
result []*types.TreasureChest
|
||||||
|
)
|
||||||
|
children := make(map[uuid.UUID][]*types.TreasureChest)
|
||||||
|
|
||||||
|
for _, node := range nodes {
|
||||||
|
if node.ParentId == uuid.Nil {
|
||||||
|
roots = append(roots, node)
|
||||||
|
} else {
|
||||||
|
children[node.ParentId] = append(children[node.ParentId], node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, root := range roots {
|
||||||
|
result = append(result, root)
|
||||||
|
result = append(result, children[root.Id]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareStrings(a, b string) int {
|
||||||
|
if a < b {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if a > b {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package transaction
|
package transaction
|
||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
import "time"
|
||||||
import "spend-sparrow/template/svg"
|
import "spend-sparrow/template/svg"
|
||||||
import "spend-sparrow/types"
|
import "spend-sparrow/types"
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
templ Transaction(transactions []*types.Transaction) {
|
templ Transaction(transactions []*types.Transaction, accounts []*types.Account, treasureChests []*types.TreasureChest) {
|
||||||
<div class="max-w-6xl mt-10 mx-auto">
|
<div class="max-w-6xl mt-10 mx-auto">
|
||||||
<button
|
<button
|
||||||
hx-get="/transaction/new"
|
hx-get="/transaction/new"
|
||||||
@@ -16,6 +18,7 @@ templ Transaction(transactions []*types.Transaction) {
|
|||||||
<p class="">New Transaction</p>
|
<p class="">New Transaction</p>
|
||||||
</button>
|
</button>
|
||||||
<div id="transaction-items" class="my-6 flex flex-col items-center">
|
<div id="transaction-items" class="my-6 flex flex-col items-center">
|
||||||
|
@EditTransaction(nil, accounts, treasureChests)
|
||||||
for _, transaction := range transactions {
|
for _, transaction := range transactions {
|
||||||
@TransactionItem(transaction)
|
@TransactionItem(transaction)
|
||||||
}
|
}
|
||||||
@@ -23,22 +26,37 @@ templ Transaction(transactions []*types.Transaction) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ EditTransaction(transaction *types.Transaction) {
|
templ EditTransaction(transaction *types.Transaction, accounts []*types.Account, treasureChests []*types.TreasureChest) {
|
||||||
{{
|
{{
|
||||||
var (
|
var (
|
||||||
name string
|
timestamp time.Time
|
||||||
id string
|
note string
|
||||||
cancelUrl string
|
// accountId string
|
||||||
)
|
// treasureChestId string
|
||||||
if transaction == nil {
|
value int64
|
||||||
name = ""
|
|
||||||
id = "new"
|
id string
|
||||||
cancelUrl = "/empty"
|
cancelUrl string
|
||||||
} else {
|
)
|
||||||
name = transaction.Name
|
if transaction == nil {
|
||||||
id = transaction.Id.String()
|
timestamp = time.Now()
|
||||||
cancelUrl = "/transaction/" + id
|
note = ""
|
||||||
}
|
// accountId = ""
|
||||||
|
// treasureChestId = ""
|
||||||
|
value = 0
|
||||||
|
|
||||||
|
id = "new"
|
||||||
|
cancelUrl = "/empty"
|
||||||
|
} else {
|
||||||
|
timestamp = transaction.Timestamp
|
||||||
|
note = transaction.Note
|
||||||
|
// accountId = transaction.AccountId.String()
|
||||||
|
// treasureChestId = transaction.TreasureChestId.String()
|
||||||
|
value = transaction.Value
|
||||||
|
|
||||||
|
id = transaction.Id.String()
|
||||||
|
cancelUrl = "/transaction/" + id
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
<div id="transaction" class="border-1 border-gray-300 w-full my-4 p-4 bg-gray-50 rounded-lg">
|
<div id="transaction" class="border-1 border-gray-300 w-full my-4 p-4 bg-gray-50 rounded-lg">
|
||||||
<form
|
<form
|
||||||
@@ -47,14 +65,58 @@ templ EditTransaction(transaction *types.Transaction) {
|
|||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
class="text-xl flex justify-end gap-4 items-center"
|
class="text-xl flex justify-end gap-4 items-center"
|
||||||
>
|
>
|
||||||
<input
|
<div class="grid grid-cols-[auto_auto] items-center gap-4 mr-auto">
|
||||||
autofocus
|
<label for="name" class="text-sm text-gray-500">Transaction Date</label>
|
||||||
name="name"
|
<input
|
||||||
type="text"
|
autofocus
|
||||||
value={ name }
|
name="name"
|
||||||
placeholder="Transaction Name"
|
type="datetime-local"
|
||||||
class="mr-auto bg-white input"
|
value={ timestamp.Format("2006-01-02T15:04") }
|
||||||
/>
|
class="bg-white input"
|
||||||
|
/>
|
||||||
|
<label for="note" class="text-sm text-gray-500">Note</label>
|
||||||
|
<input
|
||||||
|
name="note"
|
||||||
|
type="text"
|
||||||
|
value={ note }
|
||||||
|
class="mr-auto bg-white input"
|
||||||
|
/>
|
||||||
|
<label for="value" class="text-sm text-gray-500">Value (€)</label>
|
||||||
|
<input
|
||||||
|
name="value"
|
||||||
|
step="0.01"
|
||||||
|
type="number"
|
||||||
|
value={ value }
|
||||||
|
class="bg-white input"
|
||||||
|
/>
|
||||||
|
<label for="account-id" class="text-sm text-gray-500">Account</label>
|
||||||
|
<select
|
||||||
|
name="account-id"
|
||||||
|
class="bg-white input"
|
||||||
|
>
|
||||||
|
<option value="">-</option>
|
||||||
|
for _, account := range accounts {
|
||||||
|
<option value={ account.Id.String() }>{ account.Name }</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<label for="treasure-chest-id" class="text-sm text-gray-500">Treasure Chest</label>
|
||||||
|
<!-- <select -->
|
||||||
|
<!-- name="treasure-chest-id" -->
|
||||||
|
<!-- class="bg-white input" -->
|
||||||
|
<!-- > -->
|
||||||
|
<select
|
||||||
|
name="treasure-chest-id"
|
||||||
|
>
|
||||||
|
<button>
|
||||||
|
<selectedcontent></selectedcontent>
|
||||||
|
</button>
|
||||||
|
<option value="">-</option>
|
||||||
|
for _, treasureChest := range treasureChests {
|
||||||
|
<option value={ treasureChest.Id.String() } class="text-4xl p-20 hover:text-8xl">{ treasureChest.Name }</option>
|
||||||
|
<!-- <option value={ treasureChest.Id.String() } class={ calculateTreasureChestClass(treasureChest) }>{ treasureChest.Name }</option> -->
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<button type="submit" class="button button-neglect px-1 flex items-center gap-2">
|
<button type="submit" class="button button-neglect px-1 flex items-center gap-2">
|
||||||
@svg.Save()
|
@svg.Save()
|
||||||
<span>
|
<span>
|
||||||
@@ -79,8 +141,8 @@ templ EditTransaction(transaction *types.Transaction) {
|
|||||||
templ TransactionItem(transaction *types.Transaction) {
|
templ TransactionItem(transaction *types.Transaction) {
|
||||||
<div id="transaction" class="border-1 border-gray-300 w-full my-4 p-4 bg-gray-50 rounded-lg">
|
<div id="transaction" class="border-1 border-gray-300 w-full my-4 p-4 bg-gray-50 rounded-lg">
|
||||||
<div class="text-xl flex justify-end gap-4">
|
<div class="text-xl flex justify-end gap-4">
|
||||||
<p class="mr-auto">{ transaction.Name }</p>
|
<p class="mr-auto">{ transaction.Timestamp.String() }</p>
|
||||||
<p class="mr-20 text-green-700">{ displayBalance(transaction.CurrentBalance) }</p>
|
<p class="mr-20 text-green-700">{ displayBalance(transaction.Value) }</p>
|
||||||
<button
|
<button
|
||||||
hx-get={ "/transaction/" + transaction.Id.String() + "?edit=true" }
|
hx-get={ "/transaction/" + transaction.Id.String() + "?edit=true" }
|
||||||
hx-target="closest #transaction"
|
hx-target="closest #transaction"
|
||||||
@@ -112,3 +174,10 @@ func displayBalance(balance int64) string {
|
|||||||
euros := float64(balance) / 100
|
euros := float64(balance) / 100
|
||||||
return fmt.Sprintf("%.2f €", euros)
|
return fmt.Sprintf("%.2f €", euros)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func calculateTreasureChestClass(treasureChest *types.TreasureChest) string {
|
||||||
|
if treasureChest.ParentId == uuid.Nil {
|
||||||
|
return "font-bold"
|
||||||
|
}
|
||||||
|
return "ml-4"
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ templ TreasureChest(treasureChests []*types.TreasureChest) {
|
|||||||
<p>New Treasure Chest</p>
|
<p>New Treasure Chest</p>
|
||||||
</button>
|
</button>
|
||||||
<div id="treasurechest-items" class="my-6 flex flex-col items-center">
|
<div id="treasurechest-items" class="my-6 flex flex-col items-center">
|
||||||
for _, treasureChest := range sortTree(treasureChests) {
|
for _, treasureChest := range treasureChests {
|
||||||
@TreasureChestItem(treasureChest)
|
@TreasureChestItem(treasureChest)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -148,37 +148,3 @@ func displayBalance(balance int64) string {
|
|||||||
euros := float64(balance) / 100
|
euros := float64(balance) / 100
|
||||||
return fmt.Sprintf("%.2f €", euros)
|
return fmt.Sprintf("%.2f €", euros)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sortTree(nodes []*types.TreasureChest) []*types.TreasureChest {
|
|
||||||
|
|
||||||
var (
|
|
||||||
roots []*types.TreasureChest
|
|
||||||
result []*types.TreasureChest
|
|
||||||
)
|
|
||||||
children := make(map[uuid.UUID][]*types.TreasureChest)
|
|
||||||
|
|
||||||
for _, node := range nodes {
|
|
||||||
if node.ParentId == uuid.Nil {
|
|
||||||
roots = append(roots, node)
|
|
||||||
} else {
|
|
||||||
children[node.ParentId] = append(children[node.ParentId], node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, root := range roots {
|
|
||||||
result = append(result, root)
|
|
||||||
result = append(result, children[root.Id]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func compareStrings(a, b string) int {
|
|
||||||
if a < b {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if a > b {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,21 +17,26 @@ type Transaction struct {
|
|||||||
Id uuid.UUID
|
Id uuid.UUID
|
||||||
UserId uuid.UUID `db:"user_id"`
|
UserId uuid.UUID `db:"user_id"`
|
||||||
|
|
||||||
AccountId uuid.UUID `db:"account_id"`
|
|
||||||
// nil indicates that the transaction is not yet associated with a piggy bank
|
|
||||||
TreasureChestId *uuid.UUID `db:"treasure_chest_id"`
|
|
||||||
|
|
||||||
// The internal transaction is amove between e.g. an account and a piggy bank to execute a savings plan
|
|
||||||
Internal bool
|
|
||||||
|
|
||||||
// The value of the transacion. Negative for outgoing and positive for incoming
|
|
||||||
Value int64
|
|
||||||
Timestamp time.Time
|
Timestamp time.Time
|
||||||
|
Note string
|
||||||
|
|
||||||
Note string
|
// account id is only nil, if the transaction is a deposit to a treasure chest
|
||||||
|
AccountId *uuid.UUID `db:"account_id"`
|
||||||
|
TreasureChestId *uuid.UUID `db:"treasure_chest_id"`
|
||||||
|
// The value of the transacion. Negative for outgoing and positive for incoming transactions.
|
||||||
|
Value int64
|
||||||
|
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
CreatedBy uuid.UUID `db:"created_by"`
|
CreatedBy uuid.UUID `db:"created_by"`
|
||||||
UpdatedAt *time.Time `db:"updated_at"`
|
UpdatedAt *time.Time `db:"updated_at"`
|
||||||
UpdatedBy *uuid.UUID `db:"updated_by"`
|
UpdatedBy *uuid.UUID `db:"updated_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TransactionInput struct {
|
||||||
|
Id string
|
||||||
|
AccountId string
|
||||||
|
TreasureChestId string
|
||||||
|
Value string
|
||||||
|
Timestamp string
|
||||||
|
Note string
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user