feat: extract into remaining packages
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m19s
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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()) }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package types
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package utils
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package log
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package types
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -1 +0,0 @@
|
||||
package template
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
package transaction
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
@@ -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" }
|
||||
@@ -1,4 +1,4 @@
|
||||
package types
|
||||
package transaction
|
||||
|
||||
import (
|
||||
"time"
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"> per month</span>
|
||||
+ { core.FormatEuros(monthlySums[treasureChest.Id]) } <span class="text-gray-500 text-sm"> 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
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user