From 1be46780bb1b776f00972b3705a127bee071fda3 Mon Sep 17 00:00:00 2001 From: Tim Wundenberg Date: Wed, 31 Dec 2025 22:24:21 +0100 Subject: [PATCH] feat: extract into remaining packages There has been a cyclic dependency. transaction -> treasure_chest -> transaction_recurring -> transaction This has been temporarily solved by moving the GenerateTransactions function into the transaction package. In the future, this function has to be rewritten to use a proper Service insteas of direct DB access or replaced with a different system entirely. --- internal/account/handler.go | 9 +- internal/account/template.templ | 10 +- internal/authentication/handler.go | 71 ++++++------ internal/authentication/service.go | 5 +- internal/core/error.go | 9 +- internal/{types => core}/format.go | 2 +- internal/{utils => core}/http.go | 2 +- internal/{log/default.go => core/log.go} | 2 +- internal/core/mail.go | 5 +- internal/{types => core}/settings.go | 2 +- internal/dashboard/handler.go | 6 +- internal/dashboard/service.go | 6 +- .../dashboard.templ => template.templ} | 4 +- internal/dashboard/template/default.go | 1 - internal/default.go | 30 +++-- .../middleware/cross_site_request_forgery.go | 5 +- .../handler/middleware/security_headers.go | 4 +- internal/handler/root_and_404.go | 5 +- internal/template/transaction/default.go | 1 - .../transaction.go => transaction/handler.go} | 62 +++++------ .../transaction.go => transaction/service.go} | 105 ++++++++++++++---- .../template.templ} | 14 +-- .../transaction.go => transaction/types.go} | 2 +- internal/transaction_recurring/handler.go | 9 +- internal/transaction_recurring/service.go | 76 ++----------- ...saction_recurring.templ => template.templ} | 14 ++- internal/treasure_chest/handler.go | 9 +- .../{treasure_chest.templ => template.templ} | 8 +- test/auth_test.go | 3 +- 29 files changed, 230 insertions(+), 251 deletions(-) rename internal/{types => core}/format.go (97%) rename internal/{utils => core}/http.go (98%) rename internal/{log/default.go => core/log.go} (98%) rename internal/{types => core}/settings.go (99%) rename internal/dashboard/{template/dashboard.templ => template.templ} (90%) delete mode 100644 internal/dashboard/template/default.go delete mode 100644 internal/template/transaction/default.go rename internal/{handler/transaction.go => transaction/handler.go} (76%) rename internal/{service/transaction.go => transaction/service.go} (80%) rename internal/{template/transaction/transaction.templ => transaction/template.templ} (92%) rename internal/{types/transaction.go => transaction/types.go} (98%) rename internal/transaction_recurring/{transaction_recurring.templ => template.templ} (94%) rename internal/treasure_chest/{treasure_chest.templ => template.templ} (93%) diff --git a/internal/account/handler.go b/internal/account/handler.go index a00e048..362db47 100644 --- a/internal/account/handler.go +++ b/internal/account/handler.go @@ -4,7 +4,6 @@ import ( "github.com/a-h/templ" "net/http" "spend-sparrow/internal/core" - "spend-sparrow/internal/utils" ) type Handler struct { @@ -32,7 +31,7 @@ func (h Handler) handleAccountPage() http.HandlerFunc { user := core.GetUser(r) if user == nil { - utils.DoRedirect(w, r, "/auth/signin") + core.DoRedirect(w, r, "/auth/signin") return } @@ -53,7 +52,7 @@ func (h Handler) handleAccountItemComp() http.HandlerFunc { user := core.GetUser(r) if user == nil { - utils.DoRedirect(w, r, "/auth/signin") + core.DoRedirect(w, r, "/auth/signin") return } @@ -86,7 +85,7 @@ func (h Handler) handleUpdateAccount() http.HandlerFunc { user := core.GetUser(r) if user == nil { - utils.DoRedirect(w, r, "/auth/signin") + core.DoRedirect(w, r, "/auth/signin") return } @@ -121,7 +120,7 @@ func (h Handler) handleDeleteAccount() http.HandlerFunc { user := core.GetUser(r) if user == nil { - utils.DoRedirect(w, r, "/auth/signin") + core.DoRedirect(w, r, "/auth/signin") return } diff --git a/internal/account/template.templ b/internal/account/template.templ index b9267d1..b5dbd9a 100644 --- a/internal/account/template.templ +++ b/internal/account/template.templ @@ -1,7 +1,9 @@ package account -import "spend-sparrow/internal/template/svg" -import "spend-sparrow/internal/types" +import ( + "spend-sparrow/internal/core" + "spend-sparrow/internal/template/svg" +) templ template(accounts []*Account) {
@@ -82,9 +84,9 @@ templ accountItem(account *Account) {

{ account.Name }

if account.CurrentBalance < 0 { -

{ types.FormatEuros(account.CurrentBalance) }

+

{ core.FormatEuros(account.CurrentBalance) }

} else { -

{ types.FormatEuros(account.CurrentBalance) }

+

{ core.FormatEuros(account.CurrentBalance) }

}
diff --git a/internal/dashboard/template/default.go b/internal/dashboard/template/default.go deleted file mode 100644 index 38cdfe4..0000000 --- a/internal/dashboard/template/default.go +++ /dev/null @@ -1 +0,0 @@ -package template diff --git a/internal/default.go b/internal/default.go index 1d7de57..c3049d2 100644 --- a/internal/default.go +++ b/internal/default.go @@ -13,11 +13,9 @@ import ( "spend-sparrow/internal/dashboard" "spend-sparrow/internal/handler" "spend-sparrow/internal/handler/middleware" - "spend-sparrow/internal/log" - "spend-sparrow/internal/service" + "spend-sparrow/internal/transaction" "spend-sparrow/internal/transaction_recurring" "spend-sparrow/internal/treasure_chest" - "spend-sparrow/internal/types" "sync" "syscall" "time" @@ -31,7 +29,7 @@ func Run(ctx context.Context, database *sqlx.DB, migrationsPrefix string, env fu ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) defer cancel() - otelEnabled := types.IsOtelEnabled(env) + otelEnabled := core.IsOtelEnabled(env) if otelEnabled { // use context.Background(), otherwise the shutdown can't be called, as the context is already cancelled otelShutdown, err := setupOTelSDK(context.Background()) @@ -48,13 +46,13 @@ func Run(ctx context.Context, database *sqlx.DB, migrationsPrefix string, env fu cancel() }() - slog.SetDefault(log.NewLogPropagator()) + slog.SetDefault(core.NewLogPropagator()) } slog.InfoContext(ctx, "Starting server...") // init server settings - serverSettings, err := types.NewSettingsFromEnv(ctx, env) + serverSettings, err := core.NewSettingsFromEnv(ctx, env) if err != nil { return err } @@ -107,7 +105,7 @@ func shutdownServer(ctx context.Context, s *http.Server, wg *sync.WaitGroup) { } } -func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *types.Settings) http.Handler { +func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *core.Settings) http.Handler { var router = http.NewServeMux() authDb := authentication.NewDbSqlite(d) @@ -119,8 +117,8 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings * authService := authentication.NewService(authDb, randomService, clockService, mailService, serverSettings) accountService := account.NewServiceImpl(d, randomService, clockService) treasureChestService := treasure_chest.NewService(d, randomService, clockService) - transactionService := service.NewTransaction(d, randomService, clockService) - transactionRecurringService := transaction_recurring.NewService(d, randomService, clockService, transactionService) + transactionService := transaction.NewService(d, randomService, clockService) + transactionRecurringService := transaction_recurring.NewService(d, randomService, clockService) dashboardService := dashboard.NewService(d) render := core.NewRender() @@ -129,10 +127,10 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings * authHandler := authentication.NewHandler(authService, render) accountHandler := account.NewHandler(accountService, render) treasureChestHandler := treasure_chest.NewHandler(treasureChestService, transactionRecurringService, render) - transactionHandler := handler.NewTransaction(transactionService, accountService, treasureChestService, render) + transactionHandler := transaction.NewHandler(transactionService, accountService, treasureChestService, render) transactionRecurringHandler := transaction_recurring.NewHandler(transactionRecurringService, render) - go dailyTaskTimer(ctx, transactionRecurringService, authService) + go dailyTaskTimer(ctx, transactionService, authService) indexHandler.Handle(router) dashboardHandler.Handle(router) @@ -160,8 +158,8 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings * return wrapper } -func dailyTaskTimer(ctx context.Context, transactionRecurring transaction_recurring.Service, auth authentication.Service) { - runDailyTasks(ctx, transactionRecurring, auth) +func dailyTaskTimer(ctx context.Context, transaction transaction.Service, auth authentication.Service) { + runDailyTasks(ctx, transaction, auth) ticker := time.NewTicker(24 * time.Hour) defer ticker.Stop() @@ -170,13 +168,13 @@ func dailyTaskTimer(ctx context.Context, transactionRecurring transaction_recurr case <-ctx.Done(): return case <-ticker.C: - runDailyTasks(ctx, transactionRecurring, auth) + runDailyTasks(ctx, transaction, auth) } } } -func runDailyTasks(ctx context.Context, transactionRecurring transaction_recurring.Service, auth authentication.Service) { +func runDailyTasks(ctx context.Context, transaction transaction.Service, auth authentication.Service) { slog.InfoContext(ctx, "Running daily tasks") - _ = transactionRecurring.GenerateTransactions(ctx) + _ = transaction.GenerateRecurringTransactions(ctx) _ = auth.CleanupSessionsAndTokens(ctx) } diff --git a/internal/handler/middleware/cross_site_request_forgery.go b/internal/handler/middleware/cross_site_request_forgery.go index 407ab2c..91e4cb0 100644 --- a/internal/handler/middleware/cross_site_request_forgery.go +++ b/internal/handler/middleware/cross_site_request_forgery.go @@ -5,7 +5,6 @@ import ( "net/http" "spend-sparrow/internal/authentication" "spend-sparrow/internal/core" - "spend-sparrow/internal/utils" "strings" ) @@ -52,7 +51,7 @@ func CrossSiteRequestForgery(auth authentication.Service) func(http.Handler) htt if session == nil || csrfToken == "" || !auth.IsCsrfTokenValid(ctx, csrfToken, session.Id) { slog.InfoContext(ctx, "CSRF-Token not correct", "token", csrfToken) if r.Header.Get("Hx-Request") == "true" { - utils.TriggerToastWithStatus(ctx, w, r, "error", "CSRF-Token not correct", http.StatusBadRequest) + core.TriggerToastWithStatus(ctx, w, r, "error", "CSRF-Token not correct", http.StatusBadRequest) } else { http.Error(w, "CSRF-Token not correct", http.StatusBadRequest) } @@ -63,7 +62,7 @@ func CrossSiteRequestForgery(auth authentication.Service) func(http.Handler) htt token, err := auth.GetCsrfToken(ctx, session) if err != nil { if r.Header.Get("Hx-Request") == "true" { - utils.TriggerToastWithStatus(ctx, w, r, "error", "Could not generate CSRF Token", http.StatusBadRequest) + core.TriggerToastWithStatus(ctx, w, r, "error", "Could not generate CSRF Token", http.StatusBadRequest) } else { http.Error(w, "Could not generate CSRF Token", http.StatusBadRequest) } diff --git a/internal/handler/middleware/security_headers.go b/internal/handler/middleware/security_headers.go index 4ea4c59..699f710 100644 --- a/internal/handler/middleware/security_headers.go +++ b/internal/handler/middleware/security_headers.go @@ -2,10 +2,10 @@ package middleware import ( "net/http" - "spend-sparrow/internal/types" + "spend-sparrow/internal/core" ) -func SecurityHeaders(serverSettings *types.Settings) func(http.Handler) http.Handler { +func SecurityHeaders(serverSettings *core.Settings) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff") diff --git a/internal/handler/root_and_404.go b/internal/handler/root_and_404.go index e09b6e4..1a8958c 100644 --- a/internal/handler/root_and_404.go +++ b/internal/handler/root_and_404.go @@ -4,7 +4,6 @@ import ( "net/http" "spend-sparrow/internal/core" "spend-sparrow/internal/template" - "spend-sparrow/internal/utils" "github.com/a-h/templ" ) @@ -36,7 +35,7 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc { user := core.GetUser(r) - htmx := utils.IsHtmx(r) + htmx := core.IsHtmx(r) var comp templ.Component @@ -46,7 +45,7 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc { status = http.StatusNotFound } else { if user != nil { - utils.DoRedirect(w, r, "/dashboard") + core.DoRedirect(w, r, "/dashboard") return } else { comp = template.Index() diff --git a/internal/template/transaction/default.go b/internal/template/transaction/default.go deleted file mode 100644 index 0619207..0000000 --- a/internal/template/transaction/default.go +++ /dev/null @@ -1 +0,0 @@ -package transaction diff --git a/internal/handler/transaction.go b/internal/transaction/handler.go similarity index 76% rename from internal/handler/transaction.go rename to internal/transaction/handler.go index 3c351b6..678bce9 100644 --- a/internal/handler/transaction.go +++ b/internal/transaction/handler.go @@ -1,4 +1,4 @@ -package handler +package transaction import ( "fmt" @@ -6,12 +6,8 @@ import ( "net/http" "spend-sparrow/internal/account" "spend-sparrow/internal/core" - "spend-sparrow/internal/service" - t "spend-sparrow/internal/template/transaction" "spend-sparrow/internal/treasure_chest" "spend-sparrow/internal/treasure_chest_types" - "spend-sparrow/internal/types" - "spend-sparrow/internal/utils" "strconv" "time" @@ -23,19 +19,19 @@ const ( DECIMALS_MULTIPLIER = 100 ) -type Transaction interface { +type Handler interface { Handle(router *http.ServeMux) } -type TransactionImpl struct { - s service.Transaction +type HandlerImpl struct { + s Service account account.Service treasureChest treasure_chest.Service r *core.Render } -func NewTransaction(s service.Transaction, account account.Service, treasureChest treasure_chest.Service, r *core.Render) Transaction { - return TransactionImpl{ +func NewHandler(s Service, account account.Service, treasureChest treasure_chest.Service, r *core.Render) Handler { + return HandlerImpl{ s: s, account: account, treasureChest: treasureChest, @@ -43,7 +39,7 @@ func NewTransaction(s service.Transaction, account account.Service, treasureChes } } -func (h TransactionImpl) Handle(r *http.ServeMux) { +func (h HandlerImpl) Handle(r *http.ServeMux) { r.Handle("GET /transaction", h.handleTransactionPage()) r.Handle("GET /transaction/{id}", h.handleTransactionItemComp()) r.Handle("POST /transaction/{id}", h.handleUpdateTransaction()) @@ -51,17 +47,17 @@ func (h TransactionImpl) Handle(r *http.ServeMux) { r.Handle("DELETE /transaction/{id}", h.handleDeleteTransaction()) } -func (h TransactionImpl) handleTransactionPage() http.HandlerFunc { +func (h HandlerImpl) handleTransactionPage() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { core.UpdateSpan(r) user := core.GetUser(r) if user == nil { - utils.DoRedirect(w, r, "/auth/signin") + core.DoRedirect(w, r, "/auth/signin") return } - filter := types.TransactionItemsFilter{ + filter := TransactionItemsFilter{ AccountId: r.URL.Query().Get("account-id"), TreasureChestId: r.URL.Query().Get("treasure-chest-id"), Error: r.URL.Query().Get("error"), @@ -88,23 +84,23 @@ func (h TransactionImpl) handleTransactionPage() http.HandlerFunc { accountMap, treasureChestMap := h.getTransactionData(accounts, treasureChests) - items := t.TransactionItems(transactions, accountMap, treasureChestMap) - if utils.IsHtmx(r) { + items := TransactionItems(transactions, accountMap, treasureChestMap) + if core.IsHtmx(r) { h.r.Render(r, w, items) } else { - comp := t.Transaction(items, filter, accounts, treasureChests) + comp := TransactionComp(items, filter, accounts, treasureChests) h.r.RenderLayout(r, w, comp, user) } } } -func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc { +func (h HandlerImpl) handleTransactionItemComp() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { core.UpdateSpan(r) user := core.GetUser(r) if user == nil { - utils.DoRedirect(w, r, "/auth/signin") + core.DoRedirect(w, r, "/auth/signin") return } @@ -122,7 +118,7 @@ func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc { id := r.PathValue("id") if id == "new" { - comp := t.EditTransaction(nil, accounts, treasureChests) + comp := EditTransaction(nil, accounts, treasureChests) h.r.Render(r, w, comp) return } @@ -135,22 +131,22 @@ func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc { var comp templ.Component if r.URL.Query().Get("edit") == "true" { - comp = t.EditTransaction(transaction, accounts, treasureChests) + comp = EditTransaction(transaction, accounts, treasureChests) } else { accountMap, treasureChestMap := h.getTransactionData(accounts, treasureChests) - comp = t.TransactionItem(transaction, accountMap, treasureChestMap) + comp = TransactionItem(transaction, accountMap, treasureChestMap) } h.r.Render(r, w, comp) } } -func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc { +func (h HandlerImpl) handleUpdateTransaction() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { core.UpdateSpan(r) user := core.GetUser(r) if user == nil { - utils.DoRedirect(w, r, "/auth/signin") + core.DoRedirect(w, r, "/auth/signin") return } @@ -203,7 +199,7 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc { return } - input := types.Transaction{ + input := Transaction{ Id: id, AccountId: accountId, TreasureChestId: treasureChestId, @@ -213,7 +209,7 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc { Description: r.FormValue("description"), } - var transaction *types.Transaction + var transaction *Transaction if idStr == "new" { transaction, err = h.s.Add(r.Context(), nil, user, input) if err != nil { @@ -241,18 +237,18 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc { } accountMap, treasureChestMap := h.getTransactionData(accounts, treasureChests) - comp := t.TransactionItem(transaction, accountMap, treasureChestMap) + comp := TransactionItem(transaction, accountMap, treasureChestMap) h.r.Render(r, w, comp) } } -func (h TransactionImpl) handleRecalculate() http.HandlerFunc { +func (h HandlerImpl) handleRecalculate() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { core.UpdateSpan(r) user := core.GetUser(r) if user == nil { - utils.DoRedirect(w, r, "/auth/signin") + core.DoRedirect(w, r, "/auth/signin") return } @@ -262,17 +258,17 @@ func (h TransactionImpl) handleRecalculate() http.HandlerFunc { return } - utils.TriggerToastWithStatus(r.Context(), w, r, "success", "Balances recalculated, please refresh", http.StatusOK) + core.TriggerToastWithStatus(r.Context(), w, r, "success", "Balances recalculated, please refresh", http.StatusOK) } } -func (h TransactionImpl) handleDeleteTransaction() http.HandlerFunc { +func (h HandlerImpl) handleDeleteTransaction() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { core.UpdateSpan(r) user := core.GetUser(r) if user == nil { - utils.DoRedirect(w, r, "/auth/signin") + core.DoRedirect(w, r, "/auth/signin") return } @@ -286,7 +282,7 @@ func (h TransactionImpl) handleDeleteTransaction() http.HandlerFunc { } } -func (h TransactionImpl) getTransactionData(accounts []*account.Account, treasureChests []*treasure_chest_types.TreasureChest) (map[uuid.UUID]string, map[uuid.UUID]string) { +func (h HandlerImpl) getTransactionData(accounts []*account.Account, treasureChests []*treasure_chest_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 diff --git a/internal/service/transaction.go b/internal/transaction/service.go similarity index 80% rename from internal/service/transaction.go rename to internal/transaction/service.go index ecf7784..123b5b7 100644 --- a/internal/service/transaction.go +++ b/internal/transaction/service.go @@ -1,4 +1,4 @@ -package service +package transaction import ( "context" @@ -7,8 +7,8 @@ import ( "log/slog" "spend-sparrow/internal/auth_types" "spend-sparrow/internal/core" + "spend-sparrow/internal/transaction_recurring" "spend-sparrow/internal/treasure_chest_types" - "spend-sparrow/internal/types" "strconv" "time" @@ -18,31 +18,32 @@ import ( const page_size = 25 -type Transaction interface { - Add(ctx context.Context, tx *sqlx.Tx, user *auth_types.User, transaction types.Transaction) (*types.Transaction, error) - Update(ctx context.Context, user *auth_types.User, transaction types.Transaction) (*types.Transaction, error) - Get(ctx context.Context, user *auth_types.User, id string) (*types.Transaction, error) - GetAll(ctx context.Context, user *auth_types.User, filter types.TransactionItemsFilter) ([]*types.Transaction, error) +type Service interface { + Add(ctx context.Context, tx *sqlx.Tx, user *auth_types.User, transaction Transaction) (*Transaction, error) + Update(ctx context.Context, user *auth_types.User, transaction Transaction) (*Transaction, error) + Get(ctx context.Context, user *auth_types.User, id string) (*Transaction, error) + GetAll(ctx context.Context, user *auth_types.User, filter TransactionItemsFilter) ([]*Transaction, error) Delete(ctx context.Context, user *auth_types.User, id string) error RecalculateBalances(ctx context.Context, user *auth_types.User) error + GenerateRecurringTransactions(ctx context.Context) error } -type TransactionImpl struct { +type ServiceImpl struct { db *sqlx.DB clock core.Clock random core.Random } -func NewTransaction(db *sqlx.DB, random core.Random, clock core.Clock) Transaction { - return TransactionImpl{ +func NewService(db *sqlx.DB, random core.Random, clock core.Clock) Service { + return ServiceImpl{ db: db, clock: clock, random: random, } } -func (s TransactionImpl) Add(ctx context.Context, tx *sqlx.Tx, user *auth_types.User, transactionInput types.Transaction) (*types.Transaction, error) { +func (s ServiceImpl) Add(ctx context.Context, tx *sqlx.Tx, user *auth_types.User, transactionInput Transaction) (*Transaction, error) { if user == nil { return nil, core.ErrUnauthorized } @@ -109,7 +110,7 @@ func (s TransactionImpl) Add(ctx context.Context, tx *sqlx.Tx, user *auth_types. return transaction, nil } -func (s TransactionImpl) Update(ctx context.Context, user *auth_types.User, input types.Transaction) (*types.Transaction, error) { +func (s ServiceImpl) Update(ctx context.Context, user *auth_types.User, input Transaction) (*Transaction, error) { if user == nil { return nil, core.ErrUnauthorized } @@ -123,7 +124,7 @@ func (s TransactionImpl) Update(ctx context.Context, user *auth_types.User, inpu _ = tx.Rollback() }() - transaction := &types.Transaction{} + transaction := &Transaction{} err = tx.GetContext(ctx, transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, input.Id) err = core.TransformAndLogDbError(ctx, "transaction Update", nil, err) if err != nil { @@ -208,7 +209,7 @@ func (s TransactionImpl) Update(ctx context.Context, user *auth_types.User, inpu return transaction, nil } -func (s TransactionImpl) Get(ctx context.Context, user *auth_types.User, id string) (*types.Transaction, error) { +func (s ServiceImpl) Get(ctx context.Context, user *auth_types.User, id string) (*Transaction, error) { if user == nil { return nil, core.ErrUnauthorized } @@ -218,7 +219,7 @@ func (s TransactionImpl) Get(ctx context.Context, user *auth_types.User, id stri return nil, fmt.Errorf("could not parse Id: %w", core.ErrBadRequest) } - var transaction types.Transaction + var transaction Transaction err = s.db.GetContext(ctx, &transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, uuid) err = core.TransformAndLogDbError(ctx, "transaction Get", nil, err) if err != nil { @@ -231,7 +232,7 @@ func (s TransactionImpl) Get(ctx context.Context, user *auth_types.User, id stri return &transaction, nil } -func (s TransactionImpl) GetAll(ctx context.Context, user *auth_types.User, filter types.TransactionItemsFilter) ([]*types.Transaction, error) { +func (s ServiceImpl) GetAll(ctx context.Context, user *auth_types.User, filter TransactionItemsFilter) ([]*Transaction, error) { if user == nil { return nil, core.ErrUnauthorized } @@ -251,7 +252,7 @@ func (s TransactionImpl) GetAll(ctx context.Context, user *auth_types.User, filt } } - transactions := make([]*types.Transaction, 0) + transactions := make([]*Transaction, 0) err = s.db.SelectContext(ctx, &transactions, ` SELECT * FROM "transaction" @@ -279,7 +280,7 @@ func (s TransactionImpl) GetAll(ctx context.Context, user *auth_types.User, filt return transactions, nil } -func (s TransactionImpl) Delete(ctx context.Context, user *auth_types.User, id string) error { +func (s ServiceImpl) Delete(ctx context.Context, user *auth_types.User, id string) error { if user == nil { return core.ErrUnauthorized } @@ -298,7 +299,7 @@ func (s TransactionImpl) Delete(ctx context.Context, user *auth_types.User, id s _ = tx.Rollback() }() - var transaction types.Transaction + var transaction Transaction err = tx.GetContext(ctx, &transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, uuid) err = core.TransformAndLogDbError(ctx, "transaction Delete", nil, err) if err != nil { @@ -344,7 +345,7 @@ func (s TransactionImpl) Delete(ctx context.Context, user *auth_types.User, id s return nil } -func (s TransactionImpl) RecalculateBalances(ctx context.Context, user *auth_types.User) error { +func (s ServiceImpl) RecalculateBalances(ctx context.Context, user *auth_types.User) error { if user == nil { return core.ErrUnauthorized } @@ -391,7 +392,7 @@ func (s TransactionImpl) RecalculateBalances(ctx context.Context, user *auth_typ } }() - var transaction types.Transaction + var transaction Transaction for rows.Next() { err = rows.StructScan(&transaction) err = core.TransformAndLogDbError(ctx, "transaction RecalculateBalances", nil, err) @@ -445,7 +446,63 @@ func (s TransactionImpl) RecalculateBalances(ctx context.Context, user *auth_typ return nil } -func (s TransactionImpl) validateAndEnrichTransaction(ctx context.Context, tx *sqlx.Tx, oldTransaction *types.Transaction, userId uuid.UUID, input types.Transaction) (*types.Transaction, error) { +func (s ServiceImpl) GenerateRecurringTransactions(ctx context.Context) error { + now := s.clock.Now() + + tx, err := s.db.BeginTxx(ctx, nil) + err = core.TransformAndLogDbError(ctx, "transaction GenerateRecurringTransactions", nil, err) + if err != nil { + return err + } + defer func() { + _ = tx.Rollback() + }() + + recurringTransactions := make([]*transaction_recurring.TransactionRecurring, 0) + err = tx.SelectContext(ctx, &recurringTransactions, ` + SELECT * FROM transaction_recurring WHERE next_execution <= ?`, + now) + err = core.TransformAndLogDbError(ctx, "transaction GenerateRecurringTransactions", nil, err) + if err != nil { + return err + } + + for _, transactionRecurring := range recurringTransactions { + user := &auth_types.User{ + Id: transactionRecurring.UserId, + } + transaction := Transaction{ + Timestamp: *transactionRecurring.NextExecution, + Party: transactionRecurring.Party, + Description: transactionRecurring.Description, + + TreasureChestId: transactionRecurring.TreasureChestId, + Value: transactionRecurring.Value, + } + + _, err = s.Add(ctx, tx, user, transaction) + if err != nil { + return err + } + + nextExecution := transactionRecurring.NextExecution.AddDate(0, int(transactionRecurring.IntervalMonths), 0) + r, err := tx.ExecContext(ctx, `UPDATE transaction_recurring SET next_execution = ? WHERE id = ? AND user_id = ?`, + nextExecution, transactionRecurring.Id, user.Id) + err = core.TransformAndLogDbError(ctx, "transaction GenerateRecurringTransactions", r, err) + if err != nil { + return err + } + } + + err = tx.Commit() + err = core.TransformAndLogDbError(ctx, "transaction GenerateRecurringTransactions", nil, err) + if err != nil { + return err + } + return nil +} + +func (s ServiceImpl) validateAndEnrichTransaction(ctx context.Context, tx *sqlx.Tx, oldTransaction *Transaction, userId uuid.UUID, input Transaction) (*Transaction, error) { var ( id uuid.UUID createdAt time.Time @@ -513,7 +570,7 @@ func (s TransactionImpl) validateAndEnrichTransaction(ctx context.Context, tx *s } } - transaction := types.Transaction{ + transaction := Transaction{ Id: id, UserId: userId, @@ -536,7 +593,7 @@ func (s TransactionImpl) validateAndEnrichTransaction(ctx context.Context, tx *s return &transaction, nil } -func (s TransactionImpl) updateErrors(t *types.Transaction) { +func (s ServiceImpl) updateErrors(t *Transaction) { errorStr := "" switch { diff --git a/internal/template/transaction/transaction.templ b/internal/transaction/template.templ similarity index 92% rename from internal/template/transaction/transaction.templ rename to internal/transaction/template.templ index a5a1a34..2daeae7 100644 --- a/internal/template/transaction/transaction.templ +++ b/internal/transaction/template.templ @@ -4,13 +4,13 @@ import ( "fmt" "github.com/google/uuid" "spend-sparrow/internal/account" + "spend-sparrow/internal/core" "spend-sparrow/internal/template/svg" "spend-sparrow/internal/treasure_chest_types" - "spend-sparrow/internal/types" "time" ) -templ Transaction(items templ.Component, filter types.TransactionItemsFilter, accounts []*account.Account, treasureChests []*treasure_chest_types.TreasureChest) { +templ TransactionComp(items templ.Component, filter TransactionItemsFilter, accounts []*account.Account, treasureChests []*treasure_chest_types.TreasureChest) {
} -templ TransactionItems(transactions []*types.Transaction, accounts, treasureChests map[uuid.UUID]string) { +templ TransactionItems(transactions []*Transaction, accounts, treasureChests map[uuid.UUID]string) {
for _, transaction := range transactions { @TransactionItem(transaction, accounts, treasureChests) @@ -99,7 +99,7 @@ templ TransactionItems(transactions []*types.Transaction, accounts, treasureChes
} -templ EditTransaction(transaction *types.Transaction, accounts []*account.Account, treasureChests []*treasure_chest_types.TreasureChest) { +templ EditTransaction(transaction *Transaction, accounts []*account.Account, treasureChests []*treasure_chest_types.TreasureChest) { {{ var ( timestamp time.Time @@ -223,7 +223,7 @@ templ EditTransaction(transaction *types.Transaction, accounts []*account.Accoun
} -templ TransactionItem(transaction *types.Transaction, accounts, treasureChests map[uuid.UUID]string) { +templ TransactionItem(transaction *Transaction, accounts, treasureChests map[uuid.UUID]string) { {{ background := "bg-gray-50" if transaction.Error != nil { @@ -276,9 +276,9 @@ templ TransactionItem(transaction *types.Transaction, accounts, treasureChests m

if transaction.Value < 0 { -

{ types.FormatEuros(transaction.Value) }

+

{ core.FormatEuros(transaction.Value) }

} else { -

{ types.FormatEuros(transaction.Value) }

+

{ core.FormatEuros(transaction.Value) }

}