feat(transaction-recurring): #100 generate transactions
Some checks failed
Build Docker Image / Build-Docker-Image (push) Failing after 4m18s
Some checks failed
Build Docker Image / Build-Docker-Image (push) Failing after 4m18s
This commit is contained in:
24
handler/middleware/generate_recurring_transactions.go
Normal file
24
handler/middleware/generate_recurring_transactions.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"spend-sparrow/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateRecurringTransactions(transactionRecurring service.TransactionRecurring) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user := GetUser(r)
|
||||||
|
if user == nil || r.Method != http.MethodGet {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_ = transactionRecurring.GenerateTransactions(user)
|
||||||
|
}()
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"spend-sparrow/handler/middleware"
|
"spend-sparrow/handler/middleware"
|
||||||
"spend-sparrow/service"
|
"spend-sparrow/service"
|
||||||
t "spend-sparrow/template/transaction"
|
t "spend-sparrow/template/transaction"
|
||||||
"spend-sparrow/types"
|
"spend-sparrow/types"
|
||||||
"spend-sparrow/utils"
|
"spend-sparrow/utils"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -137,20 +140,66 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
transaction *types.Transaction
|
id uuid.UUID
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
input := types.TransactionInput{
|
|
||||||
Id: r.PathValue("id"),
|
idStr := r.PathValue("id")
|
||||||
AccountId: r.FormValue("account-id"),
|
if idStr != "new" {
|
||||||
TreasureChestId: r.FormValue("treasure-chest-id"),
|
id, err = uuid.Parse(idStr)
|
||||||
Value: r.FormValue("value"),
|
if err != nil {
|
||||||
Timestamp: r.FormValue("timestamp"),
|
handleError(w, r, fmt.Errorf("could not parse Id: %w", service.ErrBadRequest))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accountIdStr := r.FormValue("account-id")
|
||||||
|
var accountId *uuid.UUID
|
||||||
|
if accountIdStr != "" {
|
||||||
|
i, err := uuid.Parse(accountIdStr)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, r, fmt.Errorf("could not parse account id: %w", service.ErrBadRequest))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
accountId = &i
|
||||||
|
}
|
||||||
|
|
||||||
|
treasureChestIdStr := r.FormValue("treasure-chest-id")
|
||||||
|
var treasureChestId *uuid.UUID
|
||||||
|
if treasureChestIdStr != "" {
|
||||||
|
i, err := uuid.Parse(treasureChestIdStr)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, r, fmt.Errorf("could not parse treasure chest id: %w", service.ErrBadRequest))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
treasureChestId = &i
|
||||||
|
}
|
||||||
|
|
||||||
|
valueF, err := strconv.ParseFloat(r.FormValue("value"), 64)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, r, fmt.Errorf("could not parse value: %w", service.ErrBadRequest))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value := int64(valueF * service.DECIMALS_MULTIPLIER)
|
||||||
|
|
||||||
|
timestamp, err := time.Parse("2006-01-02", r.FormValue("timestamp"))
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, r, fmt.Errorf("could not parse timestamp: %w", service.ErrBadRequest))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
input := types.Transaction{
|
||||||
|
Id: id,
|
||||||
|
AccountId: accountId,
|
||||||
|
TreasureChestId: treasureChestId,
|
||||||
|
Value: value,
|
||||||
|
Timestamp: timestamp,
|
||||||
Party: r.FormValue("party"),
|
Party: r.FormValue("party"),
|
||||||
Description: r.FormValue("description"),
|
Description: r.FormValue("description"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.Id == "new" {
|
var transaction *types.Transaction
|
||||||
|
if idStr == "new" {
|
||||||
transaction, err = h.s.Add(user, input)
|
transaction, err = h.s.Add(user, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
|
|||||||
3
main.go
3
main.go
@@ -126,7 +126,7 @@ func createHandler(d *sqlx.DB, serverSettings *types.Settings) http.Handler {
|
|||||||
accountService := service.NewAccount(d, randomService, clockService)
|
accountService := service.NewAccount(d, randomService, clockService)
|
||||||
treasureChestService := service.NewTreasureChest(d, randomService, clockService)
|
treasureChestService := service.NewTreasureChest(d, randomService, clockService)
|
||||||
transactionService := service.NewTransaction(d, randomService, clockService)
|
transactionService := service.NewTransaction(d, randomService, clockService)
|
||||||
transactionRecurringService := service.NewTransactionRecurring(d, randomService, clockService)
|
transactionRecurringService := service.NewTransactionRecurring(d, randomService, clockService, transactionService)
|
||||||
|
|
||||||
render := handler.NewRender()
|
render := handler.NewRender()
|
||||||
indexHandler := handler.NewIndex(render)
|
indexHandler := handler.NewIndex(render)
|
||||||
@@ -148,6 +148,7 @@ func createHandler(d *sqlx.DB, serverSettings *types.Settings) http.Handler {
|
|||||||
|
|
||||||
return middleware.Wrapper(
|
return middleware.Wrapper(
|
||||||
router,
|
router,
|
||||||
|
middleware.GenerateRecurringTransactions(transactionRecurringService),
|
||||||
middleware.SecurityHeaders(serverSettings),
|
middleware.SecurityHeaders(serverSettings),
|
||||||
middleware.CacheControl,
|
middleware.CacheControl,
|
||||||
middleware.CrossSiteRequestForgery(authService),
|
middleware.CrossSiteRequestForgery(authService),
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ package service
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"spend-sparrow/db"
|
"spend-sparrow/db"
|
||||||
"spend-sparrow/log"
|
"spend-sparrow/log"
|
||||||
"spend-sparrow/types"
|
"spend-sparrow/types"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
@@ -27,8 +25,8 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Transaction interface {
|
type Transaction interface {
|
||||||
Add(user *types.User, transaction types.TransactionInput) (*types.Transaction, error)
|
Add(user *types.User, transaction types.Transaction) (*types.Transaction, error)
|
||||||
Update(user *types.User, transaction types.TransactionInput) (*types.Transaction, error)
|
Update(user *types.User, transaction types.Transaction) (*types.Transaction, error)
|
||||||
Get(user *types.User, id string) (*types.Transaction, error)
|
Get(user *types.User, id string) (*types.Transaction, error)
|
||||||
GetAll(user *types.User, filter types.TransactionItemsFilter) ([]*types.Transaction, error)
|
GetAll(user *types.User, filter types.TransactionItemsFilter) ([]*types.Transaction, error)
|
||||||
Delete(user *types.User, id string) error
|
Delete(user *types.User, id string) error
|
||||||
@@ -50,7 +48,7 @@ func NewTransaction(db *sqlx.DB, random Random, clock Clock) Transaction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionImpl) Add(user *types.User, transactionInput types.TransactionInput) (*types.Transaction, error) {
|
func (s TransactionImpl) Add(user *types.User, transactionInput types.Transaction) (*types.Transaction, error) {
|
||||||
transactionMetric.WithLabelValues("add").Inc()
|
transactionMetric.WithLabelValues("add").Inc()
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
@@ -112,16 +110,11 @@ func (s TransactionImpl) Add(user *types.User, transactionInput types.Transactio
|
|||||||
return transaction, nil
|
return transaction, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionImpl) Update(user *types.User, input types.TransactionInput) (*types.Transaction, error) {
|
func (s TransactionImpl) Update(user *types.User, input types.Transaction) (*types.Transaction, error) {
|
||||||
transactionMetric.WithLabelValues("update").Inc()
|
transactionMetric.WithLabelValues("update").Inc()
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
uuid, err := uuid.Parse(input.Id)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("transaction update: %v", err)
|
|
||||||
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := s.db.Beginx()
|
tx, err := s.db.Beginx()
|
||||||
err = db.TransformAndLogDbError("transaction Update", nil, err)
|
err = db.TransformAndLogDbError("transaction Update", nil, err)
|
||||||
@@ -133,7 +126,7 @@ func (s TransactionImpl) Update(user *types.User, input types.TransactionInput)
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
transaction := &types.Transaction{}
|
transaction := &types.Transaction{}
|
||||||
err = tx.Get(transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
err = tx.Get(transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, input.Id)
|
||||||
err = db.TransformAndLogDbError("transaction Update", nil, err)
|
err = db.TransformAndLogDbError("transaction Update", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, db.ErrNotFound) {
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
@@ -440,15 +433,13 @@ func (s TransactionImpl) RecalculateBalances(user *types.User) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionImpl) validateAndEnrichTransaction(tx *sqlx.Tx, oldTransaction *types.Transaction, userId uuid.UUID, input types.TransactionInput) (*types.Transaction, error) {
|
func (s TransactionImpl) validateAndEnrichTransaction(tx *sqlx.Tx, oldTransaction *types.Transaction, userId uuid.UUID, input types.Transaction) (*types.Transaction, error) {
|
||||||
var (
|
var (
|
||||||
id uuid.UUID
|
id uuid.UUID
|
||||||
accountUuid *uuid.UUID
|
createdAt time.Time
|
||||||
treasureChestUuid *uuid.UUID
|
createdBy uuid.UUID
|
||||||
createdAt time.Time
|
updatedAt *time.Time
|
||||||
createdBy uuid.UUID
|
updatedBy uuid.UUID
|
||||||
updatedAt *time.Time
|
|
||||||
updatedBy uuid.UUID
|
|
||||||
|
|
||||||
err error
|
err error
|
||||||
rowCount int
|
rowCount int
|
||||||
@@ -470,14 +461,8 @@ func (s TransactionImpl) validateAndEnrichTransaction(tx *sqlx.Tx, oldTransactio
|
|||||||
updatedBy = userId
|
updatedBy = userId
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.AccountId != "" {
|
if input.AccountId != nil {
|
||||||
temp, err := uuid.Parse(input.AccountId)
|
err = tx.Get(&rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, input.AccountId, userId)
|
||||||
if err != nil {
|
|
||||||
log.Error("transaction validate: %v", err)
|
|
||||||
return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest)
|
|
||||||
}
|
|
||||||
accountUuid = &temp
|
|
||||||
err = tx.Get(&rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, accountUuid, userId)
|
|
||||||
err = db.TransformAndLogDbError("transaction validate", nil, err)
|
err = db.TransformAndLogDbError("transaction validate", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -488,15 +473,9 @@ func (s TransactionImpl) validateAndEnrichTransaction(tx *sqlx.Tx, oldTransactio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.TreasureChestId != "" {
|
if input.TreasureChestId != nil {
|
||||||
temp, err := uuid.Parse(input.TreasureChestId)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("transaction validate: %v", err)
|
|
||||||
return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest)
|
|
||||||
}
|
|
||||||
treasureChestUuid = &temp
|
|
||||||
var treasureChest types.TreasureChest
|
var treasureChest types.TreasureChest
|
||||||
err = tx.Get(&treasureChest, `SELECT * FROM treasure_chest WHERE id = ? AND user_id = ?`, treasureChestUuid, userId)
|
err = tx.Get(&treasureChest, `SELECT * FROM treasure_chest WHERE id = ? AND user_id = ?`, input.TreasureChestId, userId)
|
||||||
err = db.TransformAndLogDbError("transaction validate", nil, err)
|
err = db.TransformAndLogDbError("transaction validate", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, db.ErrNotFound) {
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
@@ -509,19 +488,6 @@ func (s TransactionImpl) validateAndEnrichTransaction(tx *sqlx.Tx, oldTransactio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
valueFloat, err := strconv.ParseFloat(input.Value, 64)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("transaction validate: %v", err)
|
|
||||||
return nil, fmt.Errorf("could not parse value: %w", ErrBadRequest)
|
|
||||||
}
|
|
||||||
valueInt := int64(valueFloat * DECIMALS_MULTIPLIER)
|
|
||||||
|
|
||||||
timestamp, err := time.Parse("2006-01-02", input.Timestamp)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("transaction validate: %v", err)
|
|
||||||
return nil, fmt.Errorf("could not parse timestamp: %w", ErrBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.Party != "" {
|
if input.Party != "" {
|
||||||
err = validateString(input.Party, "party")
|
err = validateString(input.Party, "party")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -539,10 +505,10 @@ func (s TransactionImpl) validateAndEnrichTransaction(tx *sqlx.Tx, oldTransactio
|
|||||||
Id: id,
|
Id: id,
|
||||||
UserId: userId,
|
UserId: userId,
|
||||||
|
|
||||||
AccountId: accountUuid,
|
AccountId: input.AccountId,
|
||||||
TreasureChestId: treasureChestUuid,
|
TreasureChestId: input.TreasureChestId,
|
||||||
Value: valueInt,
|
Value: input.Value,
|
||||||
Timestamp: timestamp,
|
Timestamp: input.Timestamp,
|
||||||
Party: input.Party,
|
Party: input.Party,
|
||||||
Description: input.Description,
|
Description: input.Description,
|
||||||
Error: nil,
|
Error: nil,
|
||||||
|
|||||||
@@ -33,19 +33,23 @@ type TransactionRecurring interface {
|
|||||||
GetAllByAccount(user *types.User, accountId string) ([]*types.TransactionRecurring, error)
|
GetAllByAccount(user *types.User, accountId string) ([]*types.TransactionRecurring, error)
|
||||||
GetAllByTreasureChest(user *types.User, treasureChestId string) ([]*types.TransactionRecurring, error)
|
GetAllByTreasureChest(user *types.User, treasureChestId string) ([]*types.TransactionRecurring, error)
|
||||||
Delete(user *types.User, id string) error
|
Delete(user *types.User, id string) error
|
||||||
|
|
||||||
|
GenerateTransactions(user *types.User) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransactionRecurringImpl struct {
|
type TransactionRecurringImpl struct {
|
||||||
db *sqlx.DB
|
db *sqlx.DB
|
||||||
clock Clock
|
clock Clock
|
||||||
random Random
|
random Random
|
||||||
|
transaction Transaction
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransactionRecurring(db *sqlx.DB, random Random, clock Clock) TransactionRecurring {
|
func NewTransactionRecurring(db *sqlx.DB, random Random, clock Clock, transaction Transaction) TransactionRecurring {
|
||||||
return TransactionRecurringImpl{
|
return TransactionRecurringImpl{
|
||||||
db: db,
|
db: db,
|
||||||
clock: clock,
|
clock: clock,
|
||||||
random: random,
|
random: random,
|
||||||
|
transaction: transaction,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,6 +330,59 @@ func (s TransactionRecurringImpl) Delete(user *types.User, id string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s TransactionRecurringImpl) GenerateTransactions(user *types.User) error {
|
||||||
|
now := s.clock.Now()
|
||||||
|
|
||||||
|
tx, err := s.db.Beginx()
|
||||||
|
err = db.TransformAndLogDbError("transactionRecurring GenerateTransactions", nil, err)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
recurringTransactions := make([]*types.TransactionRecurring, 0)
|
||||||
|
err = tx.Select(&recurringTransactions, `
|
||||||
|
SELECT * FROM transaction_recurring WHERE user_id = ? AND next_execution <= ?`,
|
||||||
|
user.Id, now)
|
||||||
|
err = db.TransformAndLogDbError("transactionRecurring GenerateTransactions", nil, err)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, transactionRecurring := range recurringTransactions {
|
||||||
|
transaction := types.Transaction{
|
||||||
|
Timestamp: *transactionRecurring.NextExecution,
|
||||||
|
Party: transactionRecurring.Party,
|
||||||
|
Description: transactionRecurring.Description,
|
||||||
|
|
||||||
|
TreasureChestId: transactionRecurring.TreasureChestId,
|
||||||
|
Value: transactionRecurring.Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.transaction.Add(user, transaction)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
nextExecution := transactionRecurring.NextExecution.AddDate(0, int(transactionRecurring.IntervalMonths), 0)
|
||||||
|
r, err := tx.Exec(`UPDATE transaction_recurring SET next_execution = ? WHERE id = ? AND user_id = ?`,
|
||||||
|
nextExecution, transactionRecurring.Id, user.Id)
|
||||||
|
err = db.TransformAndLogDbError("transactionRecurring GenerateTransactions", r, err)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit()
|
||||||
|
err = db.TransformAndLogDbError("transactionRecurring GenerateTransactions", nil, err)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
|
func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
|
||||||
tx *sqlx.Tx,
|
tx *sqlx.Tx,
|
||||||
oldTransactionRecurring *types.TransactionRecurring,
|
oldTransactionRecurring *types.TransactionRecurring,
|
||||||
|
|||||||
@@ -34,16 +34,6 @@ type Transaction struct {
|
|||||||
UpdatedBy *uuid.UUID `db:"updated_by"`
|
UpdatedBy *uuid.UUID `db:"updated_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransactionInput struct {
|
|
||||||
Id string
|
|
||||||
AccountId string
|
|
||||||
TreasureChestId string
|
|
||||||
Value string
|
|
||||||
Timestamp string
|
|
||||||
Party string
|
|
||||||
Description string
|
|
||||||
}
|
|
||||||
|
|
||||||
type TransactionItemsFilter struct {
|
type TransactionItemsFilter struct {
|
||||||
AccountId string
|
AccountId string
|
||||||
TreasureChestId string
|
TreasureChestId string
|
||||||
|
|||||||
Reference in New Issue
Block a user