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"
|
"github.com/a-h/templ"
|
||||||
"net/http"
|
"net/http"
|
||||||
"spend-sparrow/internal/core"
|
"spend-sparrow/internal/core"
|
||||||
"spend-sparrow/internal/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
@@ -32,7 +31,7 @@ func (h Handler) handleAccountPage() http.HandlerFunc {
|
|||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +52,7 @@ func (h Handler) handleAccountItemComp() http.HandlerFunc {
|
|||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +85,7 @@ func (h Handler) handleUpdateAccount() http.HandlerFunc {
|
|||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +120,7 @@ func (h Handler) handleDeleteAccount() http.HandlerFunc {
|
|||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package account
|
package account
|
||||||
|
|
||||||
import "spend-sparrow/internal/template/svg"
|
import (
|
||||||
import "spend-sparrow/internal/types"
|
"spend-sparrow/internal/core"
|
||||||
|
"spend-sparrow/internal/template/svg"
|
||||||
|
)
|
||||||
|
|
||||||
templ template(accounts []*Account) {
|
templ template(accounts []*Account) {
|
||||||
<div class="max-w-6xl mt-10 mx-auto">
|
<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">
|
<div class="text-xl flex justify-end gap-4">
|
||||||
<p class="mr-auto">{ account.Name }</p>
|
<p class="mr-auto">{ account.Name }</p>
|
||||||
if account.CurrentBalance < 0 {
|
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 {
|
} 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
|
<a
|
||||||
href={ templ.URL("/transaction?account-id=" + account.Id.String()) }
|
href={ templ.URL("/transaction?account-id=" + account.Id.String()) }
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"spend-sparrow/internal/auth_types"
|
"spend-sparrow/internal/auth_types"
|
||||||
"spend-sparrow/internal/authentication/template"
|
"spend-sparrow/internal/authentication/template"
|
||||||
"spend-sparrow/internal/core"
|
"spend-sparrow/internal/core"
|
||||||
"spend-sparrow/internal/utils"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -62,9 +61,9 @@ func (handler HandlerImpl) handleSignInPage() http.HandlerFunc {
|
|||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user != nil {
|
if user != nil {
|
||||||
if !user.EmailVerified {
|
if !user.EmailVerified {
|
||||||
utils.DoRedirect(w, r, "/auth/verify")
|
core.DoRedirect(w, r, "/auth/verify")
|
||||||
} else {
|
} else {
|
||||||
utils.DoRedirect(w, r, "/")
|
core.DoRedirect(w, r, "/")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -79,7 +78,7 @@ func (handler HandlerImpl) handleSignIn() http.HandlerFunc {
|
|||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
core.UpdateSpan(r)
|
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)
|
session := core.GetSession(r)
|
||||||
email := r.FormValue("email")
|
email := r.FormValue("email")
|
||||||
password := r.FormValue("password")
|
password := r.FormValue("password")
|
||||||
@@ -97,17 +96,17 @@ func (handler HandlerImpl) handleSignIn() http.HandlerFunc {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ErrInvalidCredentials) {
|
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 {
|
} 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.EmailVerified {
|
if user.EmailVerified {
|
||||||
utils.DoRedirect(w, r, "/")
|
core.DoRedirect(w, r, "/")
|
||||||
} else {
|
} 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 != nil {
|
||||||
if !user.EmailVerified {
|
if !user.EmailVerified {
|
||||||
utils.DoRedirect(w, r, "/auth/verify")
|
core.DoRedirect(w, r, "/auth/verify")
|
||||||
} else {
|
} else {
|
||||||
utils.DoRedirect(w, r, "/")
|
core.DoRedirect(w, r, "/")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -138,12 +137,12 @@ func (handler HandlerImpl) handleSignUpVerifyPage() http.HandlerFunc {
|
|||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.EmailVerified {
|
if user.EmailVerified {
|
||||||
utils.DoRedirect(w, r, "/")
|
core.DoRedirect(w, r, "/")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +157,7 @@ func (handler HandlerImpl) handleVerifyResendComp() http.HandlerFunc {
|
|||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +199,7 @@ func (handler HandlerImpl) handleSignUp() http.HandlerFunc {
|
|||||||
var email = r.FormValue("email")
|
var email = r.FormValue("email")
|
||||||
var password = r.FormValue("password")
|
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)
|
slog.InfoContext(r.Context(), "signing up", "email", email)
|
||||||
user, err := handler.service.SignUp(r.Context(), email, password)
|
user, err := handler.service.SignUp(r.Context(), email, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -215,19 +214,19 @@ func (handler HandlerImpl) handleSignUp() http.HandlerFunc {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, core.ErrInternal):
|
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
|
return
|
||||||
case errors.Is(err, ErrInvalidEmail):
|
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
|
return
|
||||||
case errors.Is(err, ErrInvalidPassword):
|
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
|
return
|
||||||
}
|
}
|
||||||
// If err is "service.ErrAccountExists", then just continue
|
// 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)
|
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)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +280,7 @@ func (handler HandlerImpl) handleDeleteAccountComp() http.HandlerFunc {
|
|||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,14 +289,14 @@ func (handler HandlerImpl) handleDeleteAccountComp() http.HandlerFunc {
|
|||||||
err := handler.service.DeleteAccount(r.Context(), user, password)
|
err := handler.service.DeleteAccount(r.Context(), user, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ErrInvalidCredentials) {
|
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 {
|
} 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.DoRedirect(w, r, "/")
|
core.DoRedirect(w, r, "/")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +309,7 @@ func (handler HandlerImpl) handleChangePasswordPage() http.HandlerFunc {
|
|||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
|
|
||||||
if user == nil && !isPasswordReset {
|
if user == nil && !isPasswordReset {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +325,7 @@ func (handler HandlerImpl) handleChangePasswordComp() http.HandlerFunc {
|
|||||||
session := core.GetSession(r)
|
session := core.GetSession(r)
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if session == nil || user == nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,11 +334,11 @@ func (handler HandlerImpl) handleChangePasswordComp() http.HandlerFunc {
|
|||||||
|
|
||||||
err := handler.service.ChangePassword(r.Context(), user, session.Id, currPass, newPass)
|
err := handler.service.ChangePassword(r.Context(), user, session.Id, currPass, newPass)
|
||||||
if err != nil {
|
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
|
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)
|
user := core.GetUser(r)
|
||||||
if user != nil {
|
if user != nil {
|
||||||
utils.DoRedirect(w, r, "/")
|
core.DoRedirect(w, r, "/")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,19 +363,19 @@ func (handler HandlerImpl) handleForgotPasswordComp() http.HandlerFunc {
|
|||||||
|
|
||||||
email := r.FormValue("email")
|
email := r.FormValue("email")
|
||||||
if 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := utils.WaitMinimumTime(securityWaitDuration, func() (any, error) {
|
_, err := core.WaitMinimumTime(securityWaitDuration, func() (any, error) {
|
||||||
err := handler.service.SendForgotPasswordMail(r.Context(), email)
|
err := handler.service.SendForgotPasswordMail(r.Context(), email)
|
||||||
return nil, err
|
return nil, err
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
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 {
|
} 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"))
|
pageUrl, err := url.Parse(r.Header.Get("Hx-Current-Url"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(r.Context(), "Could not get current URL", "err", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,9 +396,9 @@ func (handler HandlerImpl) handleForgotPasswordResponseComp() http.HandlerFunc {
|
|||||||
|
|
||||||
err = handler.service.ForgotPassword(r.Context(), token, newPass)
|
err = handler.service.ForgotPassword(r.Context(), token, newPass)
|
||||||
if err != nil {
|
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 {
|
} 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/auth_types"
|
||||||
"spend-sparrow/internal/core"
|
"spend-sparrow/internal/core"
|
||||||
mailTemplate "spend-sparrow/internal/template/mail"
|
mailTemplate "spend-sparrow/internal/template/mail"
|
||||||
"spend-sparrow/internal/types"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -54,10 +53,10 @@ type ServiceImpl struct {
|
|||||||
random core.Random
|
random core.Random
|
||||||
clock core.Clock
|
clock core.Clock
|
||||||
mail core.Mail
|
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{
|
return &ServiceImpl{
|
||||||
db: db,
|
db: db,
|
||||||
random: random,
|
random: random,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"spend-sparrow/internal/utils"
|
|
||||||
"strings"
|
"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) {
|
func HandleError(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, ErrUnauthorized):
|
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
|
return
|
||||||
case errors.Is(err, ErrBadRequest):
|
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
|
return
|
||||||
case errors.Is(err, ErrNotFound):
|
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
|
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 {
|
func extractErrorMessage(err error) string {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package types
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package utils
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package log
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
"spend-sparrow/internal/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Mail interface {
|
type Mail interface {
|
||||||
@@ -14,10 +13,10 @@ type Mail interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MailImpl struct {
|
type MailImpl struct {
|
||||||
server *types.Settings
|
server *Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMail(server *types.Settings) MailImpl {
|
func NewMail(server *Settings) MailImpl {
|
||||||
return MailImpl{server: server}
|
return MailImpl{server: server}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package types
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -5,9 +5,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"spend-sparrow/internal/core"
|
"spend-sparrow/internal/core"
|
||||||
"spend-sparrow/internal/dashboard/template"
|
|
||||||
"spend-sparrow/internal/treasure_chest"
|
"spend-sparrow/internal/treasure_chest"
|
||||||
"spend-sparrow/internal/utils"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -45,7 +43,7 @@ func (handler HandlerImpl) handleDashboard() http.HandlerFunc {
|
|||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +53,7 @@ func (handler HandlerImpl) handleDashboard() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
comp := template.Dashboard(treasureChests)
|
comp := DashboardComp(treasureChests)
|
||||||
handler.r.RenderLayoutWithStatus(r, w, comp, user, http.StatusOK)
|
handler.r.RenderLayoutWithStatus(r, w, comp, user, http.StatusOK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"spend-sparrow/internal/auth_types"
|
"spend-sparrow/internal/auth_types"
|
||||||
"spend-sparrow/internal/core"
|
"spend-sparrow/internal/core"
|
||||||
|
"spend-sparrow/internal/transaction"
|
||||||
"spend-sparrow/internal/treasure_chest"
|
"spend-sparrow/internal/treasure_chest"
|
||||||
"spend-sparrow/internal/treasure_chest_types"
|
"spend-sparrow/internal/treasure_chest_types"
|
||||||
"spend-sparrow/internal/types"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -31,7 +31,7 @@ func (s Service) MainChart(
|
|||||||
return nil, core.ErrUnauthorized
|
return nil, core.ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
transactions := make([]types.Transaction, 0)
|
transactions := make([]transaction.Transaction, 0)
|
||||||
err := s.db.SelectContext(ctx, &transactions, `
|
err := s.db.SelectContext(ctx, &transactions, `
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM "transaction"
|
FROM "transaction"
|
||||||
@@ -130,7 +130,7 @@ func (s Service) TreasureChest(
|
|||||||
return nil, core.ErrUnauthorized
|
return nil, core.ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
transactions := make([]types.Transaction, 0)
|
transactions := make([]transaction.Transaction, 0)
|
||||||
err := s.db.SelectContext(ctx, &transactions, `
|
err := s.db.SelectContext(ctx, &transactions, `
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM "transaction"
|
FROM "transaction"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package template
|
package dashboard
|
||||||
|
|
||||||
import "spend-sparrow/internal/treasure_chest_types"
|
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 class="mt-10 h-full">
|
||||||
<div id="main-chart" class="h-96 mt-10"></div>
|
<div id="main-chart" class="h-96 mt-10"></div>
|
||||||
<div id="treasure-chests" 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/dashboard"
|
||||||
"spend-sparrow/internal/handler"
|
"spend-sparrow/internal/handler"
|
||||||
"spend-sparrow/internal/handler/middleware"
|
"spend-sparrow/internal/handler/middleware"
|
||||||
"spend-sparrow/internal/log"
|
"spend-sparrow/internal/transaction"
|
||||||
"spend-sparrow/internal/service"
|
|
||||||
"spend-sparrow/internal/transaction_recurring"
|
"spend-sparrow/internal/transaction_recurring"
|
||||||
"spend-sparrow/internal/treasure_chest"
|
"spend-sparrow/internal/treasure_chest"
|
||||||
"spend-sparrow/internal/types"
|
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"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)
|
ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
otelEnabled := types.IsOtelEnabled(env)
|
otelEnabled := core.IsOtelEnabled(env)
|
||||||
if otelEnabled {
|
if otelEnabled {
|
||||||
// use context.Background(), otherwise the shutdown can't be called, as the context is already cancelled
|
// use context.Background(), otherwise the shutdown can't be called, as the context is already cancelled
|
||||||
otelShutdown, err := setupOTelSDK(context.Background())
|
otelShutdown, err := setupOTelSDK(context.Background())
|
||||||
@@ -48,13 +46,13 @@ func Run(ctx context.Context, database *sqlx.DB, migrationsPrefix string, env fu
|
|||||||
cancel()
|
cancel()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
slog.SetDefault(log.NewLogPropagator())
|
slog.SetDefault(core.NewLogPropagator())
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.InfoContext(ctx, "Starting server...")
|
slog.InfoContext(ctx, "Starting server...")
|
||||||
|
|
||||||
// init server settings
|
// init server settings
|
||||||
serverSettings, err := types.NewSettingsFromEnv(ctx, env)
|
serverSettings, err := core.NewSettingsFromEnv(ctx, env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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()
|
var router = http.NewServeMux()
|
||||||
|
|
||||||
authDb := authentication.NewDbSqlite(d)
|
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)
|
authService := authentication.NewService(authDb, randomService, clockService, mailService, serverSettings)
|
||||||
accountService := account.NewServiceImpl(d, randomService, clockService)
|
accountService := account.NewServiceImpl(d, randomService, clockService)
|
||||||
treasureChestService := treasure_chest.NewService(d, randomService, clockService)
|
treasureChestService := treasure_chest.NewService(d, randomService, clockService)
|
||||||
transactionService := service.NewTransaction(d, randomService, clockService)
|
transactionService := transaction.NewService(d, randomService, clockService)
|
||||||
transactionRecurringService := transaction_recurring.NewService(d, randomService, clockService, transactionService)
|
transactionRecurringService := transaction_recurring.NewService(d, randomService, clockService)
|
||||||
dashboardService := dashboard.NewService(d)
|
dashboardService := dashboard.NewService(d)
|
||||||
|
|
||||||
render := core.NewRender()
|
render := core.NewRender()
|
||||||
@@ -129,10 +127,10 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *
|
|||||||
authHandler := authentication.NewHandler(authService, render)
|
authHandler := authentication.NewHandler(authService, render)
|
||||||
accountHandler := account.NewHandler(accountService, render)
|
accountHandler := account.NewHandler(accountService, render)
|
||||||
treasureChestHandler := treasure_chest.NewHandler(treasureChestService, transactionRecurringService, 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)
|
transactionRecurringHandler := transaction_recurring.NewHandler(transactionRecurringService, render)
|
||||||
|
|
||||||
go dailyTaskTimer(ctx, transactionRecurringService, authService)
|
go dailyTaskTimer(ctx, transactionService, authService)
|
||||||
|
|
||||||
indexHandler.Handle(router)
|
indexHandler.Handle(router)
|
||||||
dashboardHandler.Handle(router)
|
dashboardHandler.Handle(router)
|
||||||
@@ -160,8 +158,8 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *
|
|||||||
return wrapper
|
return wrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
func dailyTaskTimer(ctx context.Context, transactionRecurring transaction_recurring.Service, auth authentication.Service) {
|
func dailyTaskTimer(ctx context.Context, transaction transaction.Service, auth authentication.Service) {
|
||||||
runDailyTasks(ctx, transactionRecurring, auth)
|
runDailyTasks(ctx, transaction, auth)
|
||||||
ticker := time.NewTicker(24 * time.Hour)
|
ticker := time.NewTicker(24 * time.Hour)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
@@ -170,13 +168,13 @@ func dailyTaskTimer(ctx context.Context, transactionRecurring transaction_recurr
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
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")
|
slog.InfoContext(ctx, "Running daily tasks")
|
||||||
_ = transactionRecurring.GenerateTransactions(ctx)
|
_ = transaction.GenerateRecurringTransactions(ctx)
|
||||||
_ = auth.CleanupSessionsAndTokens(ctx)
|
_ = auth.CleanupSessionsAndTokens(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"spend-sparrow/internal/authentication"
|
"spend-sparrow/internal/authentication"
|
||||||
"spend-sparrow/internal/core"
|
"spend-sparrow/internal/core"
|
||||||
"spend-sparrow/internal/utils"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -52,7 +51,7 @@ func CrossSiteRequestForgery(auth authentication.Service) func(http.Handler) htt
|
|||||||
if session == nil || csrfToken == "" || !auth.IsCsrfTokenValid(ctx, csrfToken, session.Id) {
|
if session == nil || csrfToken == "" || !auth.IsCsrfTokenValid(ctx, csrfToken, session.Id) {
|
||||||
slog.InfoContext(ctx, "CSRF-Token not correct", "token", csrfToken)
|
slog.InfoContext(ctx, "CSRF-Token not correct", "token", csrfToken)
|
||||||
if r.Header.Get("Hx-Request") == "true" {
|
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 {
|
} else {
|
||||||
http.Error(w, "CSRF-Token not correct", http.StatusBadRequest)
|
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)
|
token, err := auth.GetCsrfToken(ctx, session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if r.Header.Get("Hx-Request") == "true" {
|
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 {
|
} else {
|
||||||
http.Error(w, "Could not generate CSRF Token", http.StatusBadRequest)
|
http.Error(w, "Could not generate CSRF Token", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"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 func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"spend-sparrow/internal/core"
|
"spend-sparrow/internal/core"
|
||||||
"spend-sparrow/internal/template"
|
"spend-sparrow/internal/template"
|
||||||
"spend-sparrow/internal/utils"
|
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
)
|
)
|
||||||
@@ -36,7 +35,7 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc {
|
|||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
|
|
||||||
htmx := utils.IsHtmx(r)
|
htmx := core.IsHtmx(r)
|
||||||
|
|
||||||
var comp templ.Component
|
var comp templ.Component
|
||||||
|
|
||||||
@@ -46,7 +45,7 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc {
|
|||||||
status = http.StatusNotFound
|
status = http.StatusNotFound
|
||||||
} else {
|
} else {
|
||||||
if user != nil {
|
if user != nil {
|
||||||
utils.DoRedirect(w, r, "/dashboard")
|
core.DoRedirect(w, r, "/dashboard")
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
comp = template.Index()
|
comp = template.Index()
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
package transaction
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package handler
|
package transaction
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -6,12 +6,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"spend-sparrow/internal/account"
|
"spend-sparrow/internal/account"
|
||||||
"spend-sparrow/internal/core"
|
"spend-sparrow/internal/core"
|
||||||
"spend-sparrow/internal/service"
|
|
||||||
t "spend-sparrow/internal/template/transaction"
|
|
||||||
"spend-sparrow/internal/treasure_chest"
|
"spend-sparrow/internal/treasure_chest"
|
||||||
"spend-sparrow/internal/treasure_chest_types"
|
"spend-sparrow/internal/treasure_chest_types"
|
||||||
"spend-sparrow/internal/types"
|
|
||||||
"spend-sparrow/internal/utils"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -23,19 +19,19 @@ const (
|
|||||||
DECIMALS_MULTIPLIER = 100
|
DECIMALS_MULTIPLIER = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
type Transaction interface {
|
type Handler interface {
|
||||||
Handle(router *http.ServeMux)
|
Handle(router *http.ServeMux)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransactionImpl struct {
|
type HandlerImpl struct {
|
||||||
s service.Transaction
|
s Service
|
||||||
account account.Service
|
account account.Service
|
||||||
treasureChest treasure_chest.Service
|
treasureChest treasure_chest.Service
|
||||||
r *core.Render
|
r *core.Render
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransaction(s service.Transaction, account account.Service, treasureChest treasure_chest.Service, r *core.Render) Transaction {
|
func NewHandler(s Service, account account.Service, treasureChest treasure_chest.Service, r *core.Render) Handler {
|
||||||
return TransactionImpl{
|
return HandlerImpl{
|
||||||
s: s,
|
s: s,
|
||||||
account: account,
|
account: account,
|
||||||
treasureChest: treasureChest,
|
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", h.handleTransactionPage())
|
||||||
r.Handle("GET /transaction/{id}", h.handleTransactionItemComp())
|
r.Handle("GET /transaction/{id}", h.handleTransactionItemComp())
|
||||||
r.Handle("POST /transaction/{id}", h.handleUpdateTransaction())
|
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())
|
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) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
core.UpdateSpan(r)
|
core.UpdateSpan(r)
|
||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
filter := types.TransactionItemsFilter{
|
filter := TransactionItemsFilter{
|
||||||
AccountId: r.URL.Query().Get("account-id"),
|
AccountId: r.URL.Query().Get("account-id"),
|
||||||
TreasureChestId: r.URL.Query().Get("treasure-chest-id"),
|
TreasureChestId: r.URL.Query().Get("treasure-chest-id"),
|
||||||
Error: r.URL.Query().Get("error"),
|
Error: r.URL.Query().Get("error"),
|
||||||
@@ -88,23 +84,23 @@ func (h TransactionImpl) handleTransactionPage() http.HandlerFunc {
|
|||||||
|
|
||||||
accountMap, treasureChestMap := h.getTransactionData(accounts, treasureChests)
|
accountMap, treasureChestMap := h.getTransactionData(accounts, treasureChests)
|
||||||
|
|
||||||
items := t.TransactionItems(transactions, accountMap, treasureChestMap)
|
items := TransactionItems(transactions, accountMap, treasureChestMap)
|
||||||
if utils.IsHtmx(r) {
|
if core.IsHtmx(r) {
|
||||||
h.r.Render(r, w, items)
|
h.r.Render(r, w, items)
|
||||||
} else {
|
} else {
|
||||||
comp := t.Transaction(items, filter, accounts, treasureChests)
|
comp := TransactionComp(items, filter, accounts, treasureChests)
|
||||||
h.r.RenderLayout(r, w, comp, user)
|
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) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
core.UpdateSpan(r)
|
core.UpdateSpan(r)
|
||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +118,7 @@ func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc {
|
|||||||
|
|
||||||
id := r.PathValue("id")
|
id := r.PathValue("id")
|
||||||
if id == "new" {
|
if id == "new" {
|
||||||
comp := t.EditTransaction(nil, accounts, treasureChests)
|
comp := EditTransaction(nil, accounts, treasureChests)
|
||||||
h.r.Render(r, w, comp)
|
h.r.Render(r, w, comp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -135,22 +131,22 @@ func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc {
|
|||||||
|
|
||||||
var comp templ.Component
|
var comp templ.Component
|
||||||
if r.URL.Query().Get("edit") == "true" {
|
if r.URL.Query().Get("edit") == "true" {
|
||||||
comp = t.EditTransaction(transaction, accounts, treasureChests)
|
comp = EditTransaction(transaction, accounts, treasureChests)
|
||||||
} else {
|
} else {
|
||||||
accountMap, treasureChestMap := h.getTransactionData(accounts, treasureChests)
|
accountMap, treasureChestMap := h.getTransactionData(accounts, treasureChests)
|
||||||
comp = t.TransactionItem(transaction, accountMap, treasureChestMap)
|
comp = TransactionItem(transaction, accountMap, treasureChestMap)
|
||||||
}
|
}
|
||||||
h.r.Render(r, w, comp)
|
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) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
core.UpdateSpan(r)
|
core.UpdateSpan(r)
|
||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +199,7 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
input := types.Transaction{
|
input := Transaction{
|
||||||
Id: id,
|
Id: id,
|
||||||
AccountId: accountId,
|
AccountId: accountId,
|
||||||
TreasureChestId: treasureChestId,
|
TreasureChestId: treasureChestId,
|
||||||
@@ -213,7 +209,7 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc {
|
|||||||
Description: r.FormValue("description"),
|
Description: r.FormValue("description"),
|
||||||
}
|
}
|
||||||
|
|
||||||
var transaction *types.Transaction
|
var transaction *Transaction
|
||||||
if idStr == "new" {
|
if idStr == "new" {
|
||||||
transaction, err = h.s.Add(r.Context(), nil, user, input)
|
transaction, err = h.s.Add(r.Context(), nil, user, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -241,18 +237,18 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
accountMap, treasureChestMap := h.getTransactionData(accounts, treasureChests)
|
accountMap, treasureChestMap := h.getTransactionData(accounts, treasureChests)
|
||||||
comp := t.TransactionItem(transaction, accountMap, treasureChestMap)
|
comp := TransactionItem(transaction, accountMap, treasureChestMap)
|
||||||
h.r.Render(r, w, comp)
|
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) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
core.UpdateSpan(r)
|
core.UpdateSpan(r)
|
||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,17 +258,17 @@ func (h TransactionImpl) handleRecalculate() http.HandlerFunc {
|
|||||||
return
|
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) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
core.UpdateSpan(r)
|
core.UpdateSpan(r)
|
||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
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)
|
accountMap := make(map[uuid.UUID]string, 0)
|
||||||
for _, account := range accounts {
|
for _, account := range accounts {
|
||||||
accountMap[account.Id] = account.Name
|
accountMap[account.Id] = account.Name
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package service
|
package transaction
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"spend-sparrow/internal/auth_types"
|
"spend-sparrow/internal/auth_types"
|
||||||
"spend-sparrow/internal/core"
|
"spend-sparrow/internal/core"
|
||||||
|
"spend-sparrow/internal/transaction_recurring"
|
||||||
"spend-sparrow/internal/treasure_chest_types"
|
"spend-sparrow/internal/treasure_chest_types"
|
||||||
"spend-sparrow/internal/types"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -18,31 +18,32 @@ import (
|
|||||||
|
|
||||||
const page_size = 25
|
const page_size = 25
|
||||||
|
|
||||||
type Transaction interface {
|
type Service interface {
|
||||||
Add(ctx context.Context, tx *sqlx.Tx, user *auth_types.User, transaction types.Transaction) (*types.Transaction, error)
|
Add(ctx context.Context, tx *sqlx.Tx, user *auth_types.User, transaction Transaction) (*Transaction, error)
|
||||||
Update(ctx context.Context, user *auth_types.User, transaction types.Transaction) (*types.Transaction, error)
|
Update(ctx context.Context, user *auth_types.User, transaction Transaction) (*Transaction, error)
|
||||||
Get(ctx context.Context, user *auth_types.User, id string) (*types.Transaction, error)
|
Get(ctx context.Context, user *auth_types.User, id string) (*Transaction, error)
|
||||||
GetAll(ctx context.Context, user *auth_types.User, filter types.TransactionItemsFilter) ([]*types.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
|
Delete(ctx context.Context, user *auth_types.User, id string) error
|
||||||
|
|
||||||
RecalculateBalances(ctx context.Context, user *auth_types.User) error
|
RecalculateBalances(ctx context.Context, user *auth_types.User) error
|
||||||
|
GenerateRecurringTransactions(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransactionImpl struct {
|
type ServiceImpl struct {
|
||||||
db *sqlx.DB
|
db *sqlx.DB
|
||||||
clock core.Clock
|
clock core.Clock
|
||||||
random core.Random
|
random core.Random
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransaction(db *sqlx.DB, random core.Random, clock core.Clock) Transaction {
|
func NewService(db *sqlx.DB, random core.Random, clock core.Clock) Service {
|
||||||
return TransactionImpl{
|
return ServiceImpl{
|
||||||
db: db,
|
db: db,
|
||||||
clock: clock,
|
clock: clock,
|
||||||
random: random,
|
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 {
|
if user == nil {
|
||||||
return nil, core.ErrUnauthorized
|
return nil, core.ErrUnauthorized
|
||||||
}
|
}
|
||||||
@@ -109,7 +110,7 @@ func (s TransactionImpl) Add(ctx context.Context, tx *sqlx.Tx, user *auth_types.
|
|||||||
return transaction, nil
|
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 {
|
if user == nil {
|
||||||
return nil, core.ErrUnauthorized
|
return nil, core.ErrUnauthorized
|
||||||
}
|
}
|
||||||
@@ -123,7 +124,7 @@ func (s TransactionImpl) Update(ctx context.Context, user *auth_types.User, inpu
|
|||||||
_ = tx.Rollback()
|
_ = 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 = tx.GetContext(ctx, transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, input.Id)
|
||||||
err = core.TransformAndLogDbError(ctx, "transaction Update", nil, err)
|
err = core.TransformAndLogDbError(ctx, "transaction Update", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -208,7 +209,7 @@ func (s TransactionImpl) Update(ctx context.Context, user *auth_types.User, inpu
|
|||||||
return transaction, nil
|
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 {
|
if user == nil {
|
||||||
return nil, core.ErrUnauthorized
|
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)
|
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 = s.db.GetContext(ctx, &transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
||||||
err = core.TransformAndLogDbError(ctx, "transaction Get", nil, err)
|
err = core.TransformAndLogDbError(ctx, "transaction Get", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -231,7 +232,7 @@ func (s TransactionImpl) Get(ctx context.Context, user *auth_types.User, id stri
|
|||||||
return &transaction, nil
|
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 {
|
if user == nil {
|
||||||
return nil, core.ErrUnauthorized
|
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, `
|
err = s.db.SelectContext(ctx, &transactions, `
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM "transaction"
|
FROM "transaction"
|
||||||
@@ -279,7 +280,7 @@ func (s TransactionImpl) GetAll(ctx context.Context, user *auth_types.User, filt
|
|||||||
return transactions, nil
|
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 {
|
if user == nil {
|
||||||
return core.ErrUnauthorized
|
return core.ErrUnauthorized
|
||||||
}
|
}
|
||||||
@@ -298,7 +299,7 @@ func (s TransactionImpl) Delete(ctx context.Context, user *auth_types.User, id s
|
|||||||
_ = tx.Rollback()
|
_ = 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 = tx.GetContext(ctx, &transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
||||||
err = core.TransformAndLogDbError(ctx, "transaction Delete", nil, err)
|
err = core.TransformAndLogDbError(ctx, "transaction Delete", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -344,7 +345,7 @@ func (s TransactionImpl) Delete(ctx context.Context, user *auth_types.User, id s
|
|||||||
return nil
|
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 {
|
if user == nil {
|
||||||
return core.ErrUnauthorized
|
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() {
|
for rows.Next() {
|
||||||
err = rows.StructScan(&transaction)
|
err = rows.StructScan(&transaction)
|
||||||
err = core.TransformAndLogDbError(ctx, "transaction RecalculateBalances", nil, err)
|
err = core.TransformAndLogDbError(ctx, "transaction RecalculateBalances", nil, err)
|
||||||
@@ -445,7 +446,63 @@ func (s TransactionImpl) RecalculateBalances(ctx context.Context, user *auth_typ
|
|||||||
return nil
|
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 (
|
var (
|
||||||
id uuid.UUID
|
id uuid.UUID
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
@@ -513,7 +570,7 @@ func (s TransactionImpl) validateAndEnrichTransaction(ctx context.Context, tx *s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction := types.Transaction{
|
transaction := Transaction{
|
||||||
Id: id,
|
Id: id,
|
||||||
UserId: userId,
|
UserId: userId,
|
||||||
|
|
||||||
@@ -536,7 +593,7 @@ func (s TransactionImpl) validateAndEnrichTransaction(ctx context.Context, tx *s
|
|||||||
return &transaction, nil
|
return &transaction, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionImpl) updateErrors(t *types.Transaction) {
|
func (s ServiceImpl) updateErrors(t *Transaction) {
|
||||||
errorStr := ""
|
errorStr := ""
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
@@ -4,13 +4,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"spend-sparrow/internal/account"
|
"spend-sparrow/internal/account"
|
||||||
|
"spend-sparrow/internal/core"
|
||||||
"spend-sparrow/internal/template/svg"
|
"spend-sparrow/internal/template/svg"
|
||||||
"spend-sparrow/internal/treasure_chest_types"
|
"spend-sparrow/internal/treasure_chest_types"
|
||||||
"spend-sparrow/internal/types"
|
|
||||||
"time"
|
"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="max-w-6xl mt-10 mx-auto">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<form
|
<form
|
||||||
@@ -91,7 +91,7 @@ templ Transaction(items templ.Component, filter types.TransactionItemsFilter, ac
|
|||||||
</div>
|
</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">
|
<div id="transaction-items" class="my-6">
|
||||||
for _, transaction := range transactions {
|
for _, transaction := range transactions {
|
||||||
@TransactionItem(transaction, accounts, treasureChests)
|
@TransactionItem(transaction, accounts, treasureChests)
|
||||||
@@ -99,7 +99,7 @@ templ TransactionItems(transactions []*types.Transaction, accounts, treasureChes
|
|||||||
</div>
|
</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 (
|
var (
|
||||||
timestamp time.Time
|
timestamp time.Time
|
||||||
@@ -223,7 +223,7 @@ templ EditTransaction(transaction *types.Transaction, accounts []*account.Accoun
|
|||||||
</div>
|
</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"
|
background := "bg-gray-50"
|
||||||
if transaction.Error != nil {
|
if transaction.Error != nil {
|
||||||
@@ -276,9 +276,9 @@ templ TransactionItem(transaction *types.Transaction, accounts, treasureChests m
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
if transaction.Value < 0 {
|
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 {
|
} 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
|
<button
|
||||||
hx-get={ "/transaction/" + transaction.Id.String() + "?edit=true" }
|
hx-get={ "/transaction/" + transaction.Id.String() + "?edit=true" }
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package types
|
package transaction
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"spend-sparrow/internal/auth_types"
|
"spend-sparrow/internal/auth_types"
|
||||||
"spend-sparrow/internal/core"
|
"spend-sparrow/internal/core"
|
||||||
"spend-sparrow/internal/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler interface {
|
type Handler interface {
|
||||||
@@ -35,7 +34,7 @@ func (h HandlerImpl) handleTransactionRecurringItemComp() http.HandlerFunc {
|
|||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +51,7 @@ func (h HandlerImpl) handleUpdateTransactionRecurring() http.HandlerFunc {
|
|||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +90,7 @@ func (h HandlerImpl) handleDeleteTransactionRecurring() http.HandlerFunc {
|
|||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +112,7 @@ func (h HandlerImpl) renderItems(w http.ResponseWriter, r *http.Request, user *a
|
|||||||
var transactionsRecurring []*TransactionRecurring
|
var transactionsRecurring []*TransactionRecurring
|
||||||
var err error
|
var err error
|
||||||
if accountId == "" && treasureChestId == "" {
|
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 != "" {
|
if accountId != "" {
|
||||||
transactionsRecurring, err = h.s.GetAllByAccount(r.Context(), user, accountId)
|
transactionsRecurring, err = h.s.GetAllByAccount(r.Context(), user, accountId)
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"spend-sparrow/internal/auth_types"
|
"spend-sparrow/internal/auth_types"
|
||||||
"spend-sparrow/internal/core"
|
"spend-sparrow/internal/core"
|
||||||
"spend-sparrow/internal/service"
|
|
||||||
"spend-sparrow/internal/treasure_chest_types"
|
"spend-sparrow/internal/treasure_chest_types"
|
||||||
"spend-sparrow/internal/types"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -29,23 +27,19 @@ type Service interface {
|
|||||||
GetAllByAccount(ctx context.Context, user *auth_types.User, accountId string) ([]*TransactionRecurring, error)
|
GetAllByAccount(ctx context.Context, user *auth_types.User, accountId string) ([]*TransactionRecurring, error)
|
||||||
GetAllByTreasureChest(ctx context.Context, user *auth_types.User, treasureChestId 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
|
Delete(ctx context.Context, user *auth_types.User, id string) error
|
||||||
|
|
||||||
GenerateTransactions(ctx context.Context) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServiceImpl struct {
|
type ServiceImpl struct {
|
||||||
db *sqlx.DB
|
db *sqlx.DB
|
||||||
clock core.Clock
|
clock core.Clock
|
||||||
random core.Random
|
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{
|
return ServiceImpl{
|
||||||
db: db,
|
db: db,
|
||||||
clock: clock,
|
clock: clock,
|
||||||
random: random,
|
random: random,
|
||||||
transaction: transaction,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,62 +318,6 @@ func (s ServiceImpl) Delete(ctx context.Context, user *auth_types.User, id strin
|
|||||||
return nil
|
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(
|
func (s ServiceImpl) validateAndEnrichTransactionRecurring(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
tx *sqlx.Tx,
|
tx *sqlx.Tx,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package transaction_recurring
|
package transaction_recurring
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
import "time"
|
"fmt"
|
||||||
import "spend-sparrow/internal/template/svg"
|
"spend-sparrow/internal/core"
|
||||||
import "spend-sparrow/internal/types"
|
"spend-sparrow/internal/template/svg"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
templ TransactionRecurringItems(transactionsRecurring []*TransactionRecurring, editId, accountId, treasureChestId string) {
|
templ TransactionRecurringItems(transactionsRecurring []*TransactionRecurring, editId, accountId, treasureChestId string) {
|
||||||
<!-- Don't use table, because embedded forms are only valid for cells -->
|
<!-- 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)
|
Every <span class="text-xl">{ transactionRecurring.IntervalMonths }</span> month(s)
|
||||||
</p>
|
</p>
|
||||||
if transactionRecurring.Value < 0 {
|
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 {
|
} 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">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"spend-sparrow/internal/core"
|
"spend-sparrow/internal/core"
|
||||||
"spend-sparrow/internal/transaction_recurring"
|
"spend-sparrow/internal/transaction_recurring"
|
||||||
"spend-sparrow/internal/treasure_chest_types"
|
"spend-sparrow/internal/treasure_chest_types"
|
||||||
"spend-sparrow/internal/utils"
|
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -42,7 +41,7 @@ func (h HandlerImpl) handleHandlerPage() http.HandlerFunc {
|
|||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +70,7 @@ func (h HandlerImpl) handleHandlerItemComp() http.HandlerFunc {
|
|||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +117,7 @@ func (h HandlerImpl) handleUpdateHandler() http.HandlerFunc {
|
|||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +162,7 @@ func (h HandlerImpl) handleDeleteHandler() http.HandlerFunc {
|
|||||||
|
|
||||||
user := core.GetUser(r)
|
user := core.GetUser(r)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package treasure_chest
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"spend-sparrow/internal/core"
|
||||||
"spend-sparrow/internal/template/svg"
|
"spend-sparrow/internal/template/svg"
|
||||||
"spend-sparrow/internal/treasure_chest_types"
|
"spend-sparrow/internal/treasure_chest_types"
|
||||||
"spend-sparrow/internal/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
templ TreasureChestComp(treasureChests []*treasure_chest_types.TreasureChest, monthlySums map[uuid.UUID]int64) {
|
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-auto">{ treasureChest.Name }</p>
|
||||||
<p class="mr-20 text-gray-600">
|
<p class="mr-20 text-gray-600">
|
||||||
if treasureChest.ParentId != nil {
|
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>
|
</p>
|
||||||
if treasureChest.ParentId != nil {
|
if treasureChest.ParentId != nil {
|
||||||
if treasureChest.CurrentBalance < 0 {
|
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 {
|
} 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
|
<a
|
||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"spend-sparrow/internal/auth_types"
|
"spend-sparrow/internal/auth_types"
|
||||||
"spend-sparrow/internal/authentication"
|
"spend-sparrow/internal/authentication"
|
||||||
"spend-sparrow/internal/core"
|
"spend-sparrow/internal/core"
|
||||||
"spend-sparrow/internal/types"
|
|
||||||
"spend-sparrow/mocks"
|
"spend-sparrow/mocks"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -18,7 +17,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
settings = types.Settings{
|
settings = core.Settings{
|
||||||
Port: "",
|
Port: "",
|
||||||
BaseUrl: "",
|
BaseUrl: "",
|
||||||
Environment: "test",
|
Environment: "test",
|
||||||
|
|||||||
Reference in New Issue
Block a user