feat: extract into remaining packages
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m19s

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.
This commit is contained in:
2025-12-31 22:24:21 +01:00
parent 6de8d8fb10
commit 1be46780bb
29 changed files with 230 additions and 251 deletions

View File

@@ -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
}

View File

@@ -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) {
<div class="max-w-6xl mt-10 mx-auto">
@@ -82,9 +84,9 @@ templ accountItem(account *Account) {
<div class="text-xl flex justify-end gap-4">
<p class="mr-auto">{ account.Name }</p>
if account.CurrentBalance < 0 {
<p class="mr-20 text-red-700">{ types.FormatEuros(account.CurrentBalance) }</p>
<p class="mr-20 text-red-700">{ core.FormatEuros(account.CurrentBalance) }</p>
} else {
<p class="mr-20 text-green-700">{ types.FormatEuros(account.CurrentBalance) }</p>
<p class="mr-20 text-green-700">{ core.FormatEuros(account.CurrentBalance) }</p>
}
<a
href={ templ.URL("/transaction?account-id=" + account.Id.String()) }

View File

@@ -8,7 +8,6 @@ import (
"spend-sparrow/internal/auth_types"
"spend-sparrow/internal/authentication/template"
"spend-sparrow/internal/core"
"spend-sparrow/internal/utils"
"time"
)
@@ -62,9 +61,9 @@ func (handler HandlerImpl) handleSignInPage() http.HandlerFunc {
user := core.GetUser(r)
if user != nil {
if !user.EmailVerified {
utils.DoRedirect(w, r, "/auth/verify")
core.DoRedirect(w, r, "/auth/verify")
} else {
utils.DoRedirect(w, r, "/")
core.DoRedirect(w, r, "/")
}
return
}
@@ -79,7 +78,7 @@ func (handler HandlerImpl) handleSignIn() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
core.UpdateSpan(r)
user, err := utils.WaitMinimumTime(securityWaitDuration, func() (*auth_types.User, error) {
user, err := core.WaitMinimumTime(securityWaitDuration, func() (*auth_types.User, error) {
session := core.GetSession(r)
email := r.FormValue("email")
password := r.FormValue("password")
@@ -97,17 +96,17 @@ func (handler HandlerImpl) handleSignIn() http.HandlerFunc {
if err != nil {
if errors.Is(err, ErrInvalidCredentials) {
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Invalid email or password", http.StatusUnauthorized)
core.TriggerToastWithStatus(r.Context(), w, r, "error", "Invalid email or password", http.StatusUnauthorized)
} else {
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "An error occurred", http.StatusInternalServerError)
core.TriggerToastWithStatus(r.Context(), w, r, "error", "An error occurred", http.StatusInternalServerError)
}
return
}
if user.EmailVerified {
utils.DoRedirect(w, r, "/")
core.DoRedirect(w, r, "/")
} else {
utils.DoRedirect(w, r, "/auth/verify")
core.DoRedirect(w, r, "/auth/verify")
}
}
}
@@ -120,9 +119,9 @@ func (handler HandlerImpl) handleSignUpPage() http.HandlerFunc {
if user != nil {
if !user.EmailVerified {
utils.DoRedirect(w, r, "/auth/verify")
core.DoRedirect(w, r, "/auth/verify")
} else {
utils.DoRedirect(w, r, "/")
core.DoRedirect(w, r, "/")
}
return
}
@@ -138,12 +137,12 @@ func (handler HandlerImpl) handleSignUpVerifyPage() http.HandlerFunc {
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
core.DoRedirect(w, r, "/auth/signin")
return
}
if user.EmailVerified {
utils.DoRedirect(w, r, "/")
core.DoRedirect(w, r, "/")
return
}
@@ -158,7 +157,7 @@ func (handler HandlerImpl) handleVerifyResendComp() http.HandlerFunc {
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
core.DoRedirect(w, r, "/auth/signin")
return
}
@@ -200,7 +199,7 @@ func (handler HandlerImpl) handleSignUp() http.HandlerFunc {
var email = r.FormValue("email")
var password = r.FormValue("password")
_, err := utils.WaitMinimumTime(securityWaitDuration, func() (any, error) {
_, err := core.WaitMinimumTime(securityWaitDuration, func() (any, error) {
slog.InfoContext(r.Context(), "signing up", "email", email)
user, err := handler.service.SignUp(r.Context(), email, password)
if err != nil {
@@ -215,19 +214,19 @@ func (handler HandlerImpl) handleSignUp() http.HandlerFunc {
if err != nil {
switch {
case errors.Is(err, core.ErrInternal):
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "An error occurred", http.StatusInternalServerError)
core.TriggerToastWithStatus(r.Context(), w, r, "error", "An error occurred", http.StatusInternalServerError)
return
case errors.Is(err, ErrInvalidEmail):
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "The email provided is invalid", http.StatusBadRequest)
core.TriggerToastWithStatus(r.Context(), w, r, "error", "The email provided is invalid", http.StatusBadRequest)
return
case errors.Is(err, ErrInvalidPassword):
utils.TriggerToastWithStatus(r.Context(), w, r, "error", ErrInvalidPassword.Error(), http.StatusBadRequest)
core.TriggerToastWithStatus(r.Context(), w, r, "error", ErrInvalidPassword.Error(), http.StatusBadRequest)
return
}
// If err is "service.ErrAccountExists", then just continue
}
utils.TriggerToastWithStatus(r.Context(), w, r, "success", "An activation link has been send to your email", http.StatusOK)
core.TriggerToastWithStatus(r.Context(), w, r, "success", "An activation link has been send to your email", http.StatusOK)
}
}
@@ -256,7 +255,7 @@ func (handler HandlerImpl) handleSignOut() http.HandlerFunc {
}
http.SetCookie(w, &c)
utils.DoRedirect(w, r, "/")
core.DoRedirect(w, r, "/")
}
}
@@ -266,7 +265,7 @@ func (handler HandlerImpl) handleDeleteAccountPage() http.HandlerFunc {
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
core.DoRedirect(w, r, "/auth/signin")
return
}
@@ -281,7 +280,7 @@ func (handler HandlerImpl) handleDeleteAccountComp() http.HandlerFunc {
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
core.DoRedirect(w, r, "/auth/signin")
return
}
@@ -290,14 +289,14 @@ func (handler HandlerImpl) handleDeleteAccountComp() http.HandlerFunc {
err := handler.service.DeleteAccount(r.Context(), user, password)
if err != nil {
if errors.Is(err, ErrInvalidCredentials) {
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Password not correct", http.StatusBadRequest)
core.TriggerToastWithStatus(r.Context(), w, r, "error", "Password not correct", http.StatusBadRequest)
} else {
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Internal Server Error", http.StatusInternalServerError)
core.TriggerToastWithStatus(r.Context(), w, r, "error", "Internal Server Error", http.StatusInternalServerError)
}
return
}
utils.DoRedirect(w, r, "/")
core.DoRedirect(w, r, "/")
}
}
@@ -310,7 +309,7 @@ func (handler HandlerImpl) handleChangePasswordPage() http.HandlerFunc {
user := core.GetUser(r)
if user == nil && !isPasswordReset {
utils.DoRedirect(w, r, "/auth/signin")
core.DoRedirect(w, r, "/auth/signin")
return
}
@@ -326,7 +325,7 @@ func (handler HandlerImpl) handleChangePasswordComp() http.HandlerFunc {
session := core.GetSession(r)
user := core.GetUser(r)
if session == nil || user == nil {
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Unathorized", http.StatusUnauthorized)
core.TriggerToastWithStatus(r.Context(), w, r, "error", "Unathorized", http.StatusUnauthorized)
return
}
@@ -335,11 +334,11 @@ func (handler HandlerImpl) handleChangePasswordComp() http.HandlerFunc {
err := handler.service.ChangePassword(r.Context(), user, session.Id, currPass, newPass)
if err != nil {
utils.TriggerToastWithStatus(r.Context(), w, r, "error", err.Error(), http.StatusBadRequest)
core.TriggerToastWithStatus(r.Context(), w, r, "error", err.Error(), http.StatusBadRequest)
return
}
utils.TriggerToastWithStatus(r.Context(), w, r, "success", "Password changed", http.StatusOK)
core.TriggerToastWithStatus(r.Context(), w, r, "success", "Password changed", http.StatusOK)
}
}
@@ -349,7 +348,7 @@ func (handler HandlerImpl) handleForgotPasswordPage() http.HandlerFunc {
user := core.GetUser(r)
if user != nil {
utils.DoRedirect(w, r, "/")
core.DoRedirect(w, r, "/")
return
}
@@ -364,19 +363,19 @@ func (handler HandlerImpl) handleForgotPasswordComp() http.HandlerFunc {
email := r.FormValue("email")
if email == "" {
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Please enter an email", http.StatusBadRequest)
core.TriggerToastWithStatus(r.Context(), w, r, "error", "Please enter an email", http.StatusBadRequest)
return
}
_, err := utils.WaitMinimumTime(securityWaitDuration, func() (any, error) {
_, err := core.WaitMinimumTime(securityWaitDuration, func() (any, error) {
err := handler.service.SendForgotPasswordMail(r.Context(), email)
return nil, err
})
if err != nil {
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Internal Server Error", http.StatusInternalServerError)
core.TriggerToastWithStatus(r.Context(), w, r, "error", "Internal Server Error", http.StatusInternalServerError)
} else {
utils.TriggerToastWithStatus(r.Context(), w, r, "info", "If the address exists, an email has been sent.", http.StatusOK)
core.TriggerToastWithStatus(r.Context(), w, r, "info", "If the address exists, an email has been sent.", http.StatusOK)
}
}
}
@@ -388,7 +387,7 @@ func (handler HandlerImpl) handleForgotPasswordResponseComp() http.HandlerFunc {
pageUrl, err := url.Parse(r.Header.Get("Hx-Current-Url"))
if err != nil {
slog.ErrorContext(r.Context(), "Could not get current URL", "err", err)
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Internal Server Error", http.StatusInternalServerError)
core.TriggerToastWithStatus(r.Context(), w, r, "error", "Internal Server Error", http.StatusInternalServerError)
return
}
@@ -397,9 +396,9 @@ func (handler HandlerImpl) handleForgotPasswordResponseComp() http.HandlerFunc {
err = handler.service.ForgotPassword(r.Context(), token, newPass)
if err != nil {
utils.TriggerToastWithStatus(r.Context(), w, r, "error", err.Error(), http.StatusBadRequest)
core.TriggerToastWithStatus(r.Context(), w, r, "error", err.Error(), http.StatusBadRequest)
} else {
utils.TriggerToastWithStatus(r.Context(), w, r, "success", "Password changed", http.StatusOK)
core.TriggerToastWithStatus(r.Context(), w, r, "success", "Password changed", http.StatusOK)
}
}
}

View File

@@ -9,7 +9,6 @@ import (
"spend-sparrow/internal/auth_types"
"spend-sparrow/internal/core"
mailTemplate "spend-sparrow/internal/template/mail"
"spend-sparrow/internal/types"
"strings"
"time"
@@ -54,10 +53,10 @@ type ServiceImpl struct {
random core.Random
clock core.Clock
mail core.Mail
serverSettings *types.Settings
serverSettings *core.Settings
}
func NewService(db Db, random core.Random, clock core.Clock, mail core.Mail, serverSettings *types.Settings) *ServiceImpl {
func NewService(db Db, random core.Random, clock core.Clock, mail core.Mail, serverSettings *core.Settings) *ServiceImpl {
return &ServiceImpl{
db: db,
random: random,

View File

@@ -6,7 +6,6 @@ import (
"errors"
"log/slog"
"net/http"
"spend-sparrow/internal/utils"
"strings"
)
@@ -48,17 +47,17 @@ func TransformAndLogDbError(ctx context.Context, module string, r sql.Result, er
func HandleError(w http.ResponseWriter, r *http.Request, err error) {
switch {
case errors.Is(err, ErrUnauthorized):
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "You are not autorized to perform this operation.", http.StatusUnauthorized)
TriggerToastWithStatus(r.Context(), w, r, "error", "You are not autorized to perform this operation.", http.StatusUnauthorized)
return
case errors.Is(err, ErrBadRequest):
utils.TriggerToastWithStatus(r.Context(), w, r, "error", extractErrorMessage(err), http.StatusBadRequest)
TriggerToastWithStatus(r.Context(), w, r, "error", extractErrorMessage(err), http.StatusBadRequest)
return
case errors.Is(err, ErrNotFound):
utils.TriggerToastWithStatus(r.Context(), w, r, "error", extractErrorMessage(err), http.StatusNotFound)
TriggerToastWithStatus(r.Context(), w, r, "error", extractErrorMessage(err), http.StatusNotFound)
return
}
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Internal Server Error", http.StatusInternalServerError)
TriggerToastWithStatus(r.Context(), w, r, "error", "Internal Server Error", http.StatusInternalServerError)
}
func extractErrorMessage(err error) string {

View File

@@ -1,4 +1,4 @@
package types
package core
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package utils
package core
import (
"context"

View File

@@ -1,4 +1,4 @@
package log
package core
import (
"context"

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"log/slog"
"net/smtp"
"spend-sparrow/internal/types"
)
type Mail interface {
@@ -14,10 +13,10 @@ type Mail interface {
}
type MailImpl struct {
server *types.Settings
server *Settings
}
func NewMail(server *types.Settings) MailImpl {
func NewMail(server *Settings) MailImpl {
return MailImpl{server: server}
}

View File

@@ -1,4 +1,4 @@
package types
package core
import (
"context"

View File

@@ -5,9 +5,7 @@ import (
"log/slog"
"net/http"
"spend-sparrow/internal/core"
"spend-sparrow/internal/dashboard/template"
"spend-sparrow/internal/treasure_chest"
"spend-sparrow/internal/utils"
"strings"
"time"
@@ -45,7 +43,7 @@ func (handler HandlerImpl) handleDashboard() http.HandlerFunc {
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
core.DoRedirect(w, r, "/auth/signin")
return
}
@@ -55,7 +53,7 @@ func (handler HandlerImpl) handleDashboard() http.HandlerFunc {
return
}
comp := template.Dashboard(treasureChests)
comp := DashboardComp(treasureChests)
handler.r.RenderLayoutWithStatus(r, w, comp, user, http.StatusOK)
}
}

View File

@@ -4,9 +4,9 @@ import (
"context"
"spend-sparrow/internal/auth_types"
"spend-sparrow/internal/core"
"spend-sparrow/internal/transaction"
"spend-sparrow/internal/treasure_chest"
"spend-sparrow/internal/treasure_chest_types"
"spend-sparrow/internal/types"
"time"
"github.com/google/uuid"
@@ -31,7 +31,7 @@ func (s Service) MainChart(
return nil, core.ErrUnauthorized
}
transactions := make([]types.Transaction, 0)
transactions := make([]transaction.Transaction, 0)
err := s.db.SelectContext(ctx, &transactions, `
SELECT *
FROM "transaction"
@@ -130,7 +130,7 @@ func (s Service) TreasureChest(
return nil, core.ErrUnauthorized
}
transactions := make([]types.Transaction, 0)
transactions := make([]transaction.Transaction, 0)
err := s.db.SelectContext(ctx, &transactions, `
SELECT *
FROM "transaction"

View File

@@ -1,8 +1,8 @@
package template
package dashboard
import "spend-sparrow/internal/treasure_chest_types"
templ Dashboard(treasureChests []*treasure_chest_types.TreasureChest) {
templ DashboardComp(treasureChests []*treasure_chest_types.TreasureChest) {
<div class="mt-10 h-full">
<div id="main-chart" class="h-96 mt-10"></div>
<div id="treasure-chests" class="h-96 mt-10"></div>

View File

@@ -1 +0,0 @@
package template

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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")

View File

@@ -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()

View File

@@ -1 +0,0 @@
package transaction

View File

@@ -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

View File

@@ -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 {

View File

@@ -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) {
<div class="max-w-6xl mt-10 mx-auto">
<div class="flex items-center gap-4">
<form
@@ -91,7 +91,7 @@ templ Transaction(items templ.Component, filter types.TransactionItemsFilter, ac
</div>
}
templ TransactionItems(transactions []*types.Transaction, accounts, treasureChests map[uuid.UUID]string) {
templ TransactionItems(transactions []*Transaction, accounts, treasureChests map[uuid.UUID]string) {
<div id="transaction-items" class="my-6">
for _, transaction := range transactions {
@TransactionItem(transaction, accounts, treasureChests)
@@ -99,7 +99,7 @@ templ TransactionItems(transactions []*types.Transaction, accounts, treasureChes
</div>
}
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
</div>
}
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
</p>
</div>
if transaction.Value < 0 {
<p class="mr-8 min-w-22 text-right text-red-700">{ types.FormatEuros(transaction.Value) }</p>
<p class="mr-8 min-w-22 text-right text-red-700">{ core.FormatEuros(transaction.Value) }</p>
} else {
<p class="mr-8 w-22 text-right text-green-700">{ types.FormatEuros(transaction.Value) }</p>
<p class="mr-8 w-22 text-right text-green-700">{ core.FormatEuros(transaction.Value) }</p>
}
<button
hx-get={ "/transaction/" + transaction.Id.String() + "?edit=true" }

View File

@@ -1,4 +1,4 @@
package types
package transaction
import (
"time"

View File

@@ -4,7 +4,6 @@ import (
"net/http"
"spend-sparrow/internal/auth_types"
"spend-sparrow/internal/core"
"spend-sparrow/internal/utils"
)
type Handler interface {
@@ -35,7 +34,7 @@ func (h HandlerImpl) handleTransactionRecurringItemComp() http.HandlerFunc {
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
core.DoRedirect(w, r, "/auth/signin")
return
}
@@ -52,7 +51,7 @@ func (h HandlerImpl) handleUpdateTransactionRecurring() http.HandlerFunc {
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
core.DoRedirect(w, r, "/auth/signin")
return
}
@@ -91,7 +90,7 @@ func (h HandlerImpl) handleDeleteTransactionRecurring() http.HandlerFunc {
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
core.DoRedirect(w, r, "/auth/signin")
return
}
@@ -113,7 +112,7 @@ func (h HandlerImpl) renderItems(w http.ResponseWriter, r *http.Request, user *a
var transactionsRecurring []*TransactionRecurring
var err error
if accountId == "" && treasureChestId == "" {
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Please select an account or treasure chest", http.StatusBadRequest)
core.TriggerToastWithStatus(r.Context(), w, r, "error", "Please select an account or treasure chest", http.StatusBadRequest)
}
if accountId != "" {
transactionsRecurring, err = h.s.GetAllByAccount(r.Context(), user, accountId)

View File

@@ -8,9 +8,7 @@ import (
"math"
"spend-sparrow/internal/auth_types"
"spend-sparrow/internal/core"
"spend-sparrow/internal/service"
"spend-sparrow/internal/treasure_chest_types"
"spend-sparrow/internal/types"
"strconv"
"time"
@@ -29,23 +27,19 @@ type Service interface {
GetAllByAccount(ctx context.Context, user *auth_types.User, accountId string) ([]*TransactionRecurring, error)
GetAllByTreasureChest(ctx context.Context, user *auth_types.User, treasureChestId string) ([]*TransactionRecurring, error)
Delete(ctx context.Context, user *auth_types.User, id string) error
GenerateTransactions(ctx context.Context) error
}
type ServiceImpl struct {
db *sqlx.DB
clock core.Clock
random core.Random
transaction service.Transaction
db *sqlx.DB
clock core.Clock
random core.Random
}
func NewService(db *sqlx.DB, random core.Random, clock core.Clock, transaction service.Transaction) Service {
func NewService(db *sqlx.DB, random core.Random, clock core.Clock) Service {
return ServiceImpl{
db: db,
clock: clock,
random: random,
transaction: transaction,
db: db,
clock: clock,
random: random,
}
}
@@ -324,62 +318,6 @@ func (s ServiceImpl) Delete(ctx context.Context, user *auth_types.User, id strin
return nil
}
func (s ServiceImpl) GenerateTransactions(ctx context.Context) error {
now := s.clock.Now()
tx, err := s.db.BeginTxx(ctx, nil)
err = core.TransformAndLogDbError(ctx, "transactionRecurring GenerateTransactions", nil, err)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
recurringTransactions := make([]*TransactionRecurring, 0)
err = tx.SelectContext(ctx, &recurringTransactions, `
SELECT * FROM transaction_recurring WHERE next_execution <= ?`,
now)
err = core.TransformAndLogDbError(ctx, "transactionRecurring GenerateTransactions", nil, err)
if err != nil {
return err
}
for _, transactionRecurring := range recurringTransactions {
user := &auth_types.User{
Id: transactionRecurring.UserId,
}
transaction := types.Transaction{
Timestamp: *transactionRecurring.NextExecution,
Party: transactionRecurring.Party,
Description: transactionRecurring.Description,
TreasureChestId: transactionRecurring.TreasureChestId,
Value: transactionRecurring.Value,
}
_, err = s.transaction.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, "transactionRecurring GenerateTransactions", r, err)
if err != nil {
return err
}
}
err = tx.Commit()
err = core.TransformAndLogDbError(ctx, "transactionRecurring GenerateTransactions", nil, err)
if err != nil {
return err
}
return nil
}
func (s ServiceImpl) validateAndEnrichTransactionRecurring(
ctx context.Context,
tx *sqlx.Tx,

View File

@@ -1,9 +1,11 @@
package transaction_recurring
import "fmt"
import "time"
import "spend-sparrow/internal/template/svg"
import "spend-sparrow/internal/types"
import (
"fmt"
"spend-sparrow/internal/core"
"spend-sparrow/internal/template/svg"
"time"
)
templ TransactionRecurringItems(transactionsRecurring []*TransactionRecurring, editId, accountId, treasureChestId string) {
<!-- Don't use table, because embedded forms are only valid for cells -->
@@ -53,9 +55,9 @@ templ TransactionRecurringItem(transactionRecurring *TransactionRecurring, accou
Every <span class="text-xl">{ transactionRecurring.IntervalMonths }</span> month(s)
</p>
if transactionRecurring.Value < 0 {
<p class="text-right text-red-700">{ types.FormatEuros(transactionRecurring.Value) }</p>
<p class="text-right text-red-700">{ core.FormatEuros(transactionRecurring.Value) }</p>
} else {
<p class="text-right text-green-700">{ types.FormatEuros(transactionRecurring.Value) }</p>
<p class="text-right text-green-700">{ core.FormatEuros(transactionRecurring.Value) }</p>
}
<div class="flex gap-2">
<button

View File

@@ -5,7 +5,6 @@ import (
"spend-sparrow/internal/core"
"spend-sparrow/internal/transaction_recurring"
"spend-sparrow/internal/treasure_chest_types"
"spend-sparrow/internal/utils"
"github.com/a-h/templ"
"github.com/google/uuid"
@@ -42,7 +41,7 @@ func (h HandlerImpl) handleHandlerPage() http.HandlerFunc {
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
core.DoRedirect(w, r, "/auth/signin")
return
}
@@ -71,7 +70,7 @@ func (h HandlerImpl) handleHandlerItemComp() http.HandlerFunc {
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
core.DoRedirect(w, r, "/auth/signin")
return
}
@@ -118,7 +117,7 @@ func (h HandlerImpl) handleUpdateHandler() http.HandlerFunc {
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
core.DoRedirect(w, r, "/auth/signin")
return
}
@@ -163,7 +162,7 @@ func (h HandlerImpl) handleDeleteHandler() http.HandlerFunc {
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
core.DoRedirect(w, r, "/auth/signin")
return
}

View File

@@ -2,9 +2,9 @@ package treasure_chest
import (
"github.com/google/uuid"
"spend-sparrow/internal/core"
"spend-sparrow/internal/template/svg"
"spend-sparrow/internal/treasure_chest_types"
"spend-sparrow/internal/types"
)
templ TreasureChestComp(treasureChests []*treasure_chest_types.TreasureChest, monthlySums map[uuid.UUID]int64) {
@@ -134,14 +134,14 @@ templ TreasureChestItem(treasureChest *treasure_chest_types.TreasureChest, month
<p class="mr-auto">{ treasureChest.Name }</p>
<p class="mr-20 text-gray-600">
if treasureChest.ParentId != nil {
+ { types.FormatEuros(monthlySums[treasureChest.Id]) } <span class="text-gray-500 text-sm">&nbsp;per month</span>
+ { core.FormatEuros(monthlySums[treasureChest.Id]) } <span class="text-gray-500 text-sm">&nbsp;per month</span>
}
</p>
if treasureChest.ParentId != nil {
if treasureChest.CurrentBalance < 0 {
<p class="mr-20 min-w-20 text-right text-red-700">{ types.FormatEuros(treasureChest.CurrentBalance) }</p>
<p class="mr-20 min-w-20 text-right text-red-700">{ core.FormatEuros(treasureChest.CurrentBalance) }</p>
} else {
<p class="mr-20 min-w-20 text-right text-green-700">{ types.FormatEuros(treasureChest.CurrentBalance) }</p>
<p class="mr-20 min-w-20 text-right text-green-700">{ core.FormatEuros(treasureChest.CurrentBalance) }</p>
}
}
<a

View File

@@ -5,7 +5,6 @@ import (
"spend-sparrow/internal/auth_types"
"spend-sparrow/internal/authentication"
"spend-sparrow/internal/core"
"spend-sparrow/internal/types"
"spend-sparrow/mocks"
"strings"
"testing"
@@ -18,7 +17,7 @@ import (
)
var (
settings = types.Settings{
settings = core.Settings{
Port: "",
BaseUrl: "",
Environment: "test",