feat: move transaction_recurring to seperate module
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m16s
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m16s
This commit is contained in:
534
internal/transaction_recurring/service.go
Normal file
534
internal/transaction_recurring/service.go
Normal file
@@ -0,0 +1,534 @@
|
||||
package transaction_recurring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
"spend-sparrow/internal/auth_types"
|
||||
"spend-sparrow/internal/core"
|
||||
"spend-sparrow/internal/service"
|
||||
"spend-sparrow/internal/treasure_chest_types"
|
||||
"spend-sparrow/internal/types"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
const (
|
||||
DECIMALS_MULTIPLIER = 100
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
Add(ctx context.Context, user *auth_types.User, transactionRecurring TransactionRecurringInput) (*TransactionRecurring, error)
|
||||
Update(ctx context.Context, user *auth_types.User, transactionRecurring TransactionRecurringInput) (*TransactionRecurring, error)
|
||||
GetAll(ctx context.Context, user *auth_types.User) ([]*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)
|
||||
Delete(ctx context.Context, user *auth_types.User, id string) error
|
||||
|
||||
GenerateTransactions(ctx context.Context) error
|
||||
}
|
||||
|
||||
type ServiceImpl struct {
|
||||
db *sqlx.DB
|
||||
clock core.Clock
|
||||
random core.Random
|
||||
transaction service.Transaction
|
||||
}
|
||||
|
||||
func NewService(db *sqlx.DB, random core.Random, clock core.Clock, transaction service.Transaction) Service {
|
||||
return ServiceImpl{
|
||||
db: db,
|
||||
clock: clock,
|
||||
random: random,
|
||||
transaction: transaction,
|
||||
}
|
||||
}
|
||||
|
||||
func (s ServiceImpl) Add(ctx context.Context,
|
||||
user *auth_types.User,
|
||||
transactionRecurringInput TransactionRecurringInput,
|
||||
) (*TransactionRecurring, error) {
|
||||
if user == nil {
|
||||
return nil, core.ErrUnauthorized
|
||||
}
|
||||
|
||||
tx, err := s.db.BeginTxx(ctx, nil)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring Add", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
transactionRecurring, err := s.validateAndEnrichTransactionRecurring(ctx, tx, nil, user.Id, transactionRecurringInput)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r, err := tx.NamedExecContext(ctx, `
|
||||
INSERT INTO "transaction_recurring" (id, user_id, interval_months,
|
||||
next_execution, party, description, account_id, treasure_chest_id, value, created_at, created_by)
|
||||
VALUES (:id, :user_id, :interval_months,
|
||||
:next_execution, :party, :description, :account_id, :treasure_chest_id, :value, :created_at, :created_by)`,
|
||||
transactionRecurring)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring Insert", r, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring Add", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transactionRecurring, nil
|
||||
}
|
||||
|
||||
func (s ServiceImpl) Update(ctx context.Context,
|
||||
user *auth_types.User,
|
||||
input TransactionRecurringInput,
|
||||
) (*TransactionRecurring, error) {
|
||||
if user == nil {
|
||||
return nil, core.ErrUnauthorized
|
||||
}
|
||||
uuid, err := uuid.Parse(input.Id)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "transactionRecurring update", "err", err)
|
||||
return nil, fmt.Errorf("could not parse Id: %w", core.ErrBadRequest)
|
||||
}
|
||||
|
||||
tx, err := s.db.BeginTxx(ctx, nil)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring Update", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
transactionRecurring := &TransactionRecurring{}
|
||||
err = tx.GetContext(ctx, transactionRecurring, `SELECT * FROM transaction_recurring WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring Update", nil, err)
|
||||
if err != nil {
|
||||
if errors.Is(err, core.ErrNotFound) {
|
||||
return nil, fmt.Errorf("transactionRecurring %v not found: %w", input.Id, core.ErrBadRequest)
|
||||
}
|
||||
return nil, core.ErrInternal
|
||||
}
|
||||
|
||||
transactionRecurring, err = s.validateAndEnrichTransactionRecurring(ctx, tx, transactionRecurring, user.Id, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r, err := tx.NamedExecContext(ctx, `
|
||||
UPDATE transaction_recurring
|
||||
SET
|
||||
interval_months = :interval_months,
|
||||
next_execution = :next_execution,
|
||||
party = :party,
|
||||
description = :description,
|
||||
account_id = :account_id,
|
||||
treasure_chest_id = :treasure_chest_id,
|
||||
value = :value,
|
||||
updated_at = :updated_at,
|
||||
updated_by = :updated_by
|
||||
WHERE id = :id
|
||||
AND user_id = :user_id`, transactionRecurring)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring Update", r, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring Update", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transactionRecurring, nil
|
||||
}
|
||||
|
||||
func (s ServiceImpl) GetAll(ctx context.Context, user *auth_types.User) ([]*TransactionRecurring, error) {
|
||||
if user == nil {
|
||||
return nil, core.ErrUnauthorized
|
||||
}
|
||||
|
||||
transactionRecurrings := make([]*TransactionRecurring, 0)
|
||||
err := s.db.SelectContext(ctx, &transactionRecurrings, `
|
||||
SELECT *
|
||||
FROM transaction_recurring
|
||||
WHERE user_id = ?
|
||||
ORDER BY created_at DESC`,
|
||||
user.Id)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring GetAll", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transactionRecurrings, nil
|
||||
}
|
||||
|
||||
func (s ServiceImpl) GetAllByAccount(ctx context.Context, user *auth_types.User, accountId string) ([]*TransactionRecurring, error) {
|
||||
if user == nil {
|
||||
return nil, core.ErrUnauthorized
|
||||
}
|
||||
|
||||
accountUuid, err := uuid.Parse(accountId)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "transactionRecurring GetAllByAccount", "err", err)
|
||||
return nil, fmt.Errorf("could not parse accountId: %w", core.ErrBadRequest)
|
||||
}
|
||||
|
||||
tx, err := s.db.BeginTxx(ctx, nil)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring GetAllByAccount", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
var rowCount int
|
||||
err = tx.GetContext(ctx, &rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, accountUuid, user.Id)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring GetAllByAccount", nil, err)
|
||||
if err != nil {
|
||||
if errors.Is(err, core.ErrNotFound) {
|
||||
return nil, fmt.Errorf("account %v not found: %w", accountId, core.ErrBadRequest)
|
||||
}
|
||||
return nil, core.ErrInternal
|
||||
}
|
||||
|
||||
transactionRecurrings := make([]*TransactionRecurring, 0)
|
||||
err = tx.SelectContext(ctx, &transactionRecurrings, `
|
||||
SELECT *
|
||||
FROM transaction_recurring
|
||||
WHERE user_id = ?
|
||||
AND account_id = ?
|
||||
ORDER BY created_at DESC`,
|
||||
user.Id, accountUuid)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring GetAll", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring GetAllByAccount", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transactionRecurrings, nil
|
||||
}
|
||||
|
||||
func (s ServiceImpl) GetAllByTreasureChest(ctx context.Context,
|
||||
user *auth_types.User,
|
||||
treasureChestId string,
|
||||
) ([]*TransactionRecurring, error) {
|
||||
if user == nil {
|
||||
return nil, core.ErrUnauthorized
|
||||
}
|
||||
|
||||
treasureChestUuid, err := uuid.Parse(treasureChestId)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "transactionRecurring GetAllByTreasureChest", "err", err)
|
||||
return nil, fmt.Errorf("could not parse treasureChestId: %w", core.ErrBadRequest)
|
||||
}
|
||||
|
||||
tx, err := s.db.BeginTxx(ctx, nil)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring GetAllByTreasureChest", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
var rowCount int
|
||||
err = tx.GetContext(ctx, &rowCount, `SELECT COUNT(*) FROM treasure_chest WHERE id = ? AND user_id = ?`, treasureChestId, user.Id)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring GetAllByTreasureChest", nil, err)
|
||||
if err != nil {
|
||||
if errors.Is(err, core.ErrNotFound) {
|
||||
return nil, fmt.Errorf("treasurechest %v not found: %w", treasureChestId, core.ErrBadRequest)
|
||||
}
|
||||
return nil, core.ErrInternal
|
||||
}
|
||||
|
||||
transactionRecurrings := make([]*TransactionRecurring, 0)
|
||||
err = tx.SelectContext(ctx, &transactionRecurrings, `
|
||||
SELECT *
|
||||
FROM transaction_recurring
|
||||
WHERE user_id = ?
|
||||
AND treasure_chest_id = ?
|
||||
ORDER BY created_at DESC`,
|
||||
user.Id, treasureChestUuid)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring GetAll", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring GetAllByTreasureChest", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transactionRecurrings, nil
|
||||
}
|
||||
|
||||
func (s ServiceImpl) Delete(ctx context.Context, user *auth_types.User, id string) error {
|
||||
if user == nil {
|
||||
return core.ErrUnauthorized
|
||||
}
|
||||
uuid, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "transactionRecurring delete", "err", err)
|
||||
return fmt.Errorf("could not parse Id: %w", core.ErrBadRequest)
|
||||
}
|
||||
|
||||
tx, err := s.db.BeginTxx(ctx, nil)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring Delete", nil, err)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
var transactionRecurring TransactionRecurring
|
||||
err = tx.GetContext(ctx, &transactionRecurring, `SELECT * FROM transaction_recurring WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring Delete", nil, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r, err := tx.ExecContext(ctx, "DELETE FROM transaction_recurring WHERE id = ? AND user_id = ?", uuid, user.Id)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring Delete", r, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring Delete", nil, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s ServiceImpl) GenerateTransactions(ctx context.Context) error {
|
||||
now := s.clock.Now()
|
||||
|
||||
tx, err := s.db.BeginTxx(ctx, nil)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring GenerateTransactions", nil, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
recurringTransactions := make([]*TransactionRecurring, 0)
|
||||
err = tx.SelectContext(ctx, &recurringTransactions, `
|
||||
SELECT * FROM transaction_recurring WHERE next_execution <= ?`,
|
||||
now)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring GenerateTransactions", nil, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, transactionRecurring := range recurringTransactions {
|
||||
user := &auth_types.User{
|
||||
Id: transactionRecurring.UserId,
|
||||
}
|
||||
transaction := types.Transaction{
|
||||
Timestamp: *transactionRecurring.NextExecution,
|
||||
Party: transactionRecurring.Party,
|
||||
Description: transactionRecurring.Description,
|
||||
|
||||
TreasureChestId: transactionRecurring.TreasureChestId,
|
||||
Value: transactionRecurring.Value,
|
||||
}
|
||||
|
||||
_, err = s.transaction.Add(ctx, tx, user, transaction)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nextExecution := transactionRecurring.NextExecution.AddDate(0, int(transactionRecurring.IntervalMonths), 0)
|
||||
r, err := tx.ExecContext(ctx, `UPDATE transaction_recurring SET next_execution = ? WHERE id = ? AND user_id = ?`,
|
||||
nextExecution, transactionRecurring.Id, user.Id)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring GenerateTransactions", r, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring GenerateTransactions", nil, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s ServiceImpl) validateAndEnrichTransactionRecurring(
|
||||
ctx context.Context,
|
||||
tx *sqlx.Tx,
|
||||
oldTransactionRecurring *TransactionRecurring,
|
||||
userId uuid.UUID,
|
||||
input TransactionRecurringInput,
|
||||
) (*TransactionRecurring, error) {
|
||||
var (
|
||||
id uuid.UUID
|
||||
accountUuid *uuid.UUID
|
||||
treasureChestUuid *uuid.UUID
|
||||
createdAt time.Time
|
||||
createdBy uuid.UUID
|
||||
updatedAt *time.Time
|
||||
updatedBy uuid.UUID
|
||||
intervalMonths int64
|
||||
|
||||
err error
|
||||
rowCount int
|
||||
)
|
||||
|
||||
if oldTransactionRecurring == nil {
|
||||
id, err = s.random.UUID(ctx)
|
||||
if err != nil {
|
||||
return nil, core.ErrInternal
|
||||
}
|
||||
createdAt = s.clock.Now()
|
||||
createdBy = userId
|
||||
} else {
|
||||
id = oldTransactionRecurring.Id
|
||||
createdAt = oldTransactionRecurring.CreatedAt
|
||||
createdBy = oldTransactionRecurring.CreatedBy
|
||||
time := s.clock.Now()
|
||||
updatedAt = &time
|
||||
updatedBy = userId
|
||||
}
|
||||
|
||||
hasAccount := false
|
||||
hasTreasureChest := false
|
||||
if input.AccountId != "" {
|
||||
temp, err := uuid.Parse(input.AccountId)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
|
||||
return nil, fmt.Errorf("could not parse accountId: %w", core.ErrBadRequest)
|
||||
}
|
||||
accountUuid = &temp
|
||||
err = tx.GetContext(ctx, &rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, accountUuid, userId)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring validate", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rowCount == 0 {
|
||||
slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
|
||||
return nil, fmt.Errorf("account not found: %w", core.ErrBadRequest)
|
||||
}
|
||||
|
||||
hasAccount = true
|
||||
}
|
||||
|
||||
if input.TreasureChestId != "" {
|
||||
temp, err := uuid.Parse(input.TreasureChestId)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
|
||||
return nil, fmt.Errorf("could not parse treasureChestId: %w", core.ErrBadRequest)
|
||||
}
|
||||
treasureChestUuid = &temp
|
||||
var treasureChest treasure_chest_types.TreasureChest
|
||||
err = tx.GetContext(ctx, &treasureChest, `SELECT * FROM treasure_chest WHERE id = ? AND user_id = ?`, treasureChestUuid, userId)
|
||||
err = core.TransformAndLogDbError(ctx, "transactionRecurring validate", nil, err)
|
||||
if err != nil {
|
||||
if errors.Is(err, core.ErrNotFound) {
|
||||
return nil, fmt.Errorf("treasure chest not found: %w", core.ErrBadRequest)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if treasureChest.ParentId == nil {
|
||||
return nil, fmt.Errorf("treasure chest is a group: %w", core.ErrBadRequest)
|
||||
}
|
||||
hasTreasureChest = true
|
||||
}
|
||||
|
||||
if !hasAccount && !hasTreasureChest {
|
||||
slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
|
||||
return nil, fmt.Errorf("either account or treasure chest is required: %w", core.ErrBadRequest)
|
||||
}
|
||||
if hasAccount && hasTreasureChest {
|
||||
slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
|
||||
return nil, fmt.Errorf("either account or treasure chest is required, not both: %w", core.ErrBadRequest)
|
||||
}
|
||||
|
||||
valueFloat, err := strconv.ParseFloat(input.Value, 64)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
|
||||
return nil, fmt.Errorf("could not parse value: %w", core.ErrBadRequest)
|
||||
}
|
||||
value := int64(math.Round(valueFloat * DECIMALS_MULTIPLIER))
|
||||
|
||||
if input.Party != "" {
|
||||
err = core.ValidateString(input.Party, "party")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if input.Description != "" {
|
||||
err = core.ValidateString(input.Description, "description")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
intervalMonths, err = strconv.ParseInt(input.IntervalMonths, 10, 0)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
|
||||
return nil, fmt.Errorf("could not parse intervalMonths: %w", core.ErrBadRequest)
|
||||
}
|
||||
if intervalMonths < 1 {
|
||||
slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
|
||||
return nil, fmt.Errorf("intervalMonths needs to be greater than 0: %w", core.ErrBadRequest)
|
||||
}
|
||||
var nextExecution *time.Time = nil
|
||||
if input.NextExecution != "" {
|
||||
t, err := time.Parse("2006-01-02", input.NextExecution)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "transaction validate", "err", err)
|
||||
return nil, fmt.Errorf("could not parse timestamp: %w", core.ErrBadRequest)
|
||||
}
|
||||
|
||||
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
|
||||
nextExecution = &t
|
||||
}
|
||||
|
||||
transactionRecurring := TransactionRecurring{
|
||||
Id: id,
|
||||
UserId: userId,
|
||||
|
||||
IntervalMonths: intervalMonths,
|
||||
NextExecution: nextExecution,
|
||||
|
||||
Party: input.Party,
|
||||
Description: input.Description,
|
||||
|
||||
AccountId: accountUuid,
|
||||
TreasureChestId: treasureChestUuid,
|
||||
Value: value,
|
||||
|
||||
CreatedAt: createdAt,
|
||||
CreatedBy: createdBy,
|
||||
UpdatedAt: updatedAt,
|
||||
UpdatedBy: &updatedBy,
|
||||
}
|
||||
|
||||
return &transactionRecurring, nil
|
||||
}
|
||||
Reference in New Issue
Block a user