diff --git a/handler/transaction.go b/handler/transaction.go
index f0714f8..8bb27a0 100644
--- a/handler/transaction.go
+++ b/handler/transaction.go
@@ -3,12 +3,14 @@ package handler
import (
"net/http"
"spend-sparrow/handler/middleware"
+ "spend-sparrow/log"
"spend-sparrow/service"
t "spend-sparrow/template/transaction"
"spend-sparrow/types"
"spend-sparrow/utils"
"github.com/a-h/templ"
+ "github.com/google/uuid"
)
type Transaction interface {
@@ -85,7 +87,9 @@ func (h TransactionImpl) handleTransactionPage() http.HandlerFunc {
return
}
- comp := t.Transaction(transactions, accounts, treasureChests)
+ accountMap, treasureChestMap := h.getTransactionData(accounts, treasureChests)
+
+ comp := t.Transaction(transactions, accountMap, treasureChestMap)
h.r.RenderLayout(r, w, comp, user)
}
}
@@ -127,7 +131,8 @@ func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc {
if r.URL.Query().Get("edit") == "true" {
comp = t.EditTransaction(transaction, accounts, treasureChests)
} else {
- comp = t.TransactionItem(transaction)
+ accountMap, treasureChestMap := h.getTransactionData(accounts, treasureChests)
+ comp = t.TransactionItem(transaction, accountMap, treasureChestMap)
}
h.r.Render(r, w, comp)
}
@@ -169,7 +174,20 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc {
}
}
- comp := t.TransactionItem(transaction)
+ 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
+ }
+
+ accountMap, treasureChestMap := h.getTransactionData(accounts, treasureChests)
+ comp := t.TransactionItem(transaction, accountMap, treasureChestMap)
h.r.Render(r, w, comp)
}
}
@@ -191,3 +209,22 @@ func (h TransactionImpl) handleDeleteTransaction() http.HandlerFunc {
}
}
}
+
+func (h TransactionImpl) getTransactionData(accounts []*types.Account, treasureChests []*types.TreasureChest) (map[uuid.UUID]string, map[uuid.UUID]string) {
+ accountMap := make(map[uuid.UUID]string, 0)
+ for _, account := range accounts {
+ accountMap[account.Id] = account.Name
+ }
+ treasureChestMap := make(map[uuid.UUID]string, 0)
+ root := ""
+ for _, treasureChest := range treasureChests {
+ if treasureChest.ParentId == uuid.Nil {
+ root = treasureChest.Name + " > "
+ treasureChestMap[treasureChest.Id] = treasureChest.Name
+ } else {
+ treasureChestMap[treasureChest.Id] = root + treasureChest.Name
+ }
+ }
+ log.Info("treasureChestMap: %v", treasureChestMap)
+ return accountMap, treasureChestMap
+}
diff --git a/migration/005_transaction_add_error.up.sql b/migration/005_transaction_add_error.up.sql
new file mode 100644
index 0000000..11fd775
--- /dev/null
+++ b/migration/005_transaction_add_error.up.sql
@@ -0,0 +1,2 @@
+
+ALTER TABLE "transaction" ADD COLUMN error TEXT;
diff --git a/service/transaction.go b/service/transaction.go
index 3a4b5de..69b60d7 100644
--- a/service/transaction.go
+++ b/service/transaction.go
@@ -64,8 +64,8 @@ func (s TransactionImpl) Add(user *types.User, transactionInput types.Transactio
}
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)
+ INSERT INTO "transaction" (id, user_id, account_id, treasure_chest_id, value, timestamp, note, error, created_at, created_by)
+ VALUES (:id, :user_id, :account_id, :treasure_chest_id, :value, :timestamp, :note, :error, :created_at, :created_by)`, transaction)
err = db.TransformAndLogDbError("transaction Insert", r, err)
if err != nil {
return nil, err
@@ -141,6 +141,7 @@ func (s TransactionImpl) Update(user *types.User, input types.TransactionInput)
value = :value,
timestamp = :timestamp,
note = :note,
+ error = :error,
updated_at = :updated_at,
updated_by = :updated_by
WHERE id = :id
@@ -184,7 +185,7 @@ func (s TransactionImpl) GetAll(user *types.User) ([]*types.Transaction, error)
return nil, ErrUnauthorized
}
transactions := make([]*types.Transaction, 0)
- err := s.db.Select(&transactions, `SELECT * FROM "transaction" WHERE user_id = ? ORDER BY timestamp`, user.Id)
+ err := s.db.Select(&transactions, `SELECT * FROM "transaction" WHERE user_id = ? ORDER BY timestamp DESC`, user.Id)
err = db.TransformAndLogDbError("transaction GetAll", nil, err)
if err != nil {
return nil, err
@@ -223,119 +224,6 @@ func (s TransactionImpl) Delete(user *types.User, id string) error {
return nil
}
-func (s TransactionImpl) validateAndEnrichTransaction(transaction *types.Transaction, userId uuid.UUID, input types.TransactionInput) (*types.Transaction, error) {
-
- var (
- id uuid.UUID
- accountUuid *uuid.UUID
- treasureChestUuid *uuid.UUID
- createdAt time.Time
- createdBy uuid.UUID
- updatedAt *time.Time
- updatedBy uuid.UUID
-
- err error
- rowCount int
- )
-
- if transaction == nil {
- id, err = s.random.UUID()
- if err != nil {
- return nil, types.ErrInternal
- }
- createdAt = s.clock.Now()
- createdBy = userId
- } else {
- id = transaction.Id
- createdAt = transaction.CreatedAt
- createdBy = transaction.CreatedBy
- time := s.clock.Now()
- updatedAt = &time
- updatedBy = userId
- }
-
- if input.AccountId != "" {
- temp, err := uuid.Parse(input.AccountId)
- if err != nil {
- log.Error("transaction validate: %v", err)
- return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest)
- }
- accountUuid = &temp
- err = s.db.Get(&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 input.TreasureChestId != "" {
- temp, err := uuid.Parse(input.TreasureChestId)
- if err != nil {
- log.Error("transaction validate: %v", err)
- return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest)
- }
- treasureChestUuid = &temp
- err = s.db.Get(&rowCount, `SELECT COUNT(*) FROM treasure_chest WHERE id = ? AND user_id = ?`, treasureChestUuid, 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)
- }
- }
-
- valueFloat, err := strconv.ParseFloat(input.Value, 64)
- if err != nil {
- log.Error("transaction validate: %v", err)
- return nil, fmt.Errorf("could not parse value: %w", ErrBadRequest)
- }
- valueInt := int64(valueFloat * 100)
-
- timestampTime, err := time.Parse("2006-01-02T15:04", input.Timestamp)
- if err != nil {
- log.Error("transaction validate: %v", err)
- return nil, fmt.Errorf("could not parse timestamp: %w", ErrBadRequest)
- }
-
- if input.TimezoneOffsetMinutes != "" {
- timezoneOffsetMinutes, err := strconv.Atoi(input.TimezoneOffsetMinutes)
- if err != nil {
- return nil, fmt.Errorf("could not parse timezone offset: %w", ErrBadRequest)
- }
- timestampTime = timestampTime.Add(time.Duration(-1*timezoneOffsetMinutes) * time.Minute)
- }
-
- if input.Note != "" {
- err = validateString(input.Note, "note")
- if err != nil {
- return nil, err
- }
- }
-
- return &types.Transaction{
- Id: id,
- UserId: userId,
-
- AccountId: accountUuid,
- TreasureChestId: treasureChestUuid,
- Value: valueInt,
- Timestamp: timestampTime,
- Note: input.Note,
-
- CreatedAt: createdAt,
- CreatedBy: createdBy,
- UpdatedAt: updatedAt,
- UpdatedBy: &updatedBy,
- }, nil
-}
-
func (s TransactionImpl) RecalculateBalances(user *types.User) error {
transactionMetric.WithLabelValues("recalculate").Inc()
if user == nil {
@@ -404,3 +292,132 @@ func (s TransactionImpl) RecalculateBalances(user *types.User) error {
return nil
}
+
+func (s TransactionImpl) validateAndEnrichTransaction(oldTransaction *types.Transaction, userId uuid.UUID, input types.TransactionInput) (*types.Transaction, error) {
+
+ var (
+ id uuid.UUID
+ accountUuid *uuid.UUID
+ treasureChestUuid *uuid.UUID
+ createdAt time.Time
+ createdBy uuid.UUID
+ updatedAt *time.Time
+ updatedBy uuid.UUID
+
+ err error
+ rowCount int
+ )
+
+ if oldTransaction == nil {
+ id, err = s.random.UUID()
+ if err != nil {
+ return nil, types.ErrInternal
+ }
+ createdAt = s.clock.Now()
+ createdBy = userId
+ } else {
+ id = oldTransaction.Id
+ createdAt = oldTransaction.CreatedAt
+ createdBy = oldTransaction.CreatedBy
+ time := s.clock.Now()
+ updatedAt = &time
+ updatedBy = userId
+ }
+
+ if input.AccountId != "" {
+ temp, err := uuid.Parse(input.AccountId)
+ if err != nil {
+ log.Error("transaction validate: %v", err)
+ return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest)
+ }
+ accountUuid = &temp
+ err = s.db.Get(&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 input.TreasureChestId != "" {
+ temp, err := uuid.Parse(input.TreasureChestId)
+ if err != nil {
+ log.Error("transaction validate: %v", err)
+ return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest)
+ }
+ treasureChestUuid = &temp
+ err = s.db.Get(&rowCount, `SELECT COUNT(*) FROM treasure_chest WHERE id = ? AND user_id = ?`, treasureChestUuid, 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)
+ }
+ }
+
+ valueFloat, err := strconv.ParseFloat(input.Value, 64)
+ if err != nil {
+ log.Error("transaction validate: %v", err)
+ return nil, fmt.Errorf("could not parse value: %w", ErrBadRequest)
+ }
+ valueInt := int64(valueFloat * 100)
+
+ timestampTime, err := time.Parse("2006-01-02T15:04", input.Timestamp)
+ if err != nil {
+ log.Error("transaction validate: %v", err)
+ return nil, fmt.Errorf("could not parse timestamp: %w", ErrBadRequest)
+ }
+
+ if input.TimezoneOffsetMinutes != "" {
+ timezoneOffsetMinutes, err := strconv.Atoi(input.TimezoneOffsetMinutes)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse timezone offset: %w", ErrBadRequest)
+ }
+ timestampTime = timestampTime.Add(time.Duration(-1*timezoneOffsetMinutes) * time.Minute)
+ }
+
+ if input.Note != "" {
+ err = validateString(input.Note, "note")
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ transaction := types.Transaction{
+ Id: id,
+ UserId: userId,
+
+ AccountId: accountUuid,
+ TreasureChestId: treasureChestUuid,
+ Value: valueInt,
+ Timestamp: timestampTime,
+ Note: input.Note,
+
+ CreatedAt: createdAt,
+ CreatedBy: createdBy,
+ UpdatedAt: updatedAt,
+ UpdatedBy: &updatedBy,
+ }
+ error := getErrors(transaction)
+ if error != "" {
+ transaction.Error = &error
+ }
+ return &transaction, nil
+}
+
+func getErrors(transaction types.Transaction) string {
+ if transaction.Value < 0 {
+ // panic("unimplemented")
+ } else if transaction.Value > 0 {
+ // panic("unimplemented")
+ } else {
+ return "\"value\" needs to be positive or negative"
+ }
+ return ""
+}
diff --git a/template/svg/default.templ b/template/svg/default.templ
index b008873..587dfe3 100644
--- a/template/svg/default.templ
+++ b/template/svg/default.templ
@@ -35,3 +35,15 @@ templ Cancel() {
{ transaction.Timestamp.String() }
-{ displayBalance(transaction.Value)+" €" }
- - +templ TransactionItem(transaction *types.Transaction, accounts, treasureChests map[uuid.UUID]string) { + {{ + background := "bg-gray-50" + if transaction.Error != nil { + background = "bg-yellow-50" + } + }} +{ transaction.Timestamp.String() }
++ if transaction.AccountId != nil { + { accounts[*transaction.AccountId] } + } else { + + } +
++ if transaction.TreasureChestId != nil { + { treasureChests[*transaction.TreasureChestId] } + } else { + + } +
+{ displayBalance(transaction.Value)+" €" }
+ +