450 lines
13 KiB
Go
450 lines
13 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"spend-sparrow/db"
|
|
"spend-sparrow/log"
|
|
"spend-sparrow/types"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
)
|
|
|
|
var (
|
|
transactionRecurringMetric = promauto.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Name: "spendsparrow_transaction_recurring_total",
|
|
Help: "The total of transactionRecurring operations",
|
|
},
|
|
[]string{"operation"},
|
|
)
|
|
)
|
|
|
|
type TransactionRecurring interface {
|
|
Add(user *types.User, transactionRecurring types.TransactionRecurringInput) (*types.TransactionRecurring, error)
|
|
Update(user *types.User, transactionRecurring types.TransactionRecurringInput) (*types.TransactionRecurring, error)
|
|
GetAllByAccount(user *types.User, accountId string) ([]*types.TransactionRecurring, error)
|
|
GetAllByTreasureChest(user *types.User, treasureChestId string) ([]*types.TransactionRecurring, error)
|
|
Delete(user *types.User, id string) error
|
|
}
|
|
|
|
type TransactionRecurringImpl struct {
|
|
db *sqlx.DB
|
|
clock Clock
|
|
random Random
|
|
settings *types.Settings
|
|
}
|
|
|
|
func NewTransactionRecurring(db *sqlx.DB, random Random, clock Clock, settings *types.Settings) TransactionRecurring {
|
|
return TransactionRecurringImpl{
|
|
db: db,
|
|
clock: clock,
|
|
random: random,
|
|
settings: settings,
|
|
}
|
|
}
|
|
|
|
func (s TransactionRecurringImpl) Add(
|
|
user *types.User,
|
|
transactionRecurringInput types.TransactionRecurringInput) (*types.TransactionRecurring, error) {
|
|
transactionRecurringMetric.WithLabelValues("add").Inc()
|
|
|
|
if user == nil {
|
|
return nil, ErrUnauthorized
|
|
}
|
|
|
|
tx, err := s.db.Beginx()
|
|
err = db.TransformAndLogDbError("transactionRecurring Add", nil, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
_ = tx.Rollback()
|
|
}()
|
|
|
|
transactionRecurring, err := s.validateAndEnrichTransactionRecurring(tx, nil, user.Id, transactionRecurringInput)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r, err := tx.NamedExec(`
|
|
INSERT INTO "transaction_recurring" (id, user_id, interval_months,
|
|
active, party, description, account_id, treasure_chest_id, value, created_at, created_by)
|
|
VALUES (:id, :user_id, :interval_months,
|
|
:active, :party, :description, :account_id, :treasure_chest_id, :value, :created_at, :created_by)`,
|
|
transactionRecurring)
|
|
err = db.TransformAndLogDbError("transactionRecurring Insert", r, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = tx.Commit()
|
|
err = db.TransformAndLogDbError("transactionRecurring Add", nil, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return transactionRecurring, nil
|
|
}
|
|
|
|
func (s TransactionRecurringImpl) Update(
|
|
user *types.User,
|
|
input types.TransactionRecurringInput) (*types.TransactionRecurring, error) {
|
|
transactionRecurringMetric.WithLabelValues("update").Inc()
|
|
if user == nil {
|
|
return nil, ErrUnauthorized
|
|
}
|
|
uuid, err := uuid.Parse(input.Id)
|
|
if err != nil {
|
|
log.Error("transactionRecurring update: %v", err)
|
|
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
|
}
|
|
|
|
tx, err := s.db.Beginx()
|
|
err = db.TransformAndLogDbError("transactionRecurring Update", nil, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
_ = tx.Rollback()
|
|
}()
|
|
|
|
transactionRecurring := &types.TransactionRecurring{}
|
|
err = tx.Get(transactionRecurring, `SELECT * FROM transaction_recurring WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
|
err = db.TransformAndLogDbError("transactionRecurring Update", nil, err)
|
|
if err != nil {
|
|
if errors.Is(err, db.ErrNotFound) {
|
|
return nil, fmt.Errorf("transactionRecurring %v not found: %w", input.Id, ErrBadRequest)
|
|
}
|
|
return nil, types.ErrInternal
|
|
}
|
|
|
|
transactionRecurring, err = s.validateAndEnrichTransactionRecurring(tx, transactionRecurring, user.Id, input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r, err := tx.NamedExec(`
|
|
UPDATE transaction_recurring
|
|
SET
|
|
interval_months = :interval_months,
|
|
active = :active,
|
|
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 = db.TransformAndLogDbError("transactionRecurring Update", r, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = tx.Commit()
|
|
err = db.TransformAndLogDbError("transactionRecurring Update", nil, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return transactionRecurring, nil
|
|
}
|
|
|
|
func (s TransactionRecurringImpl) GetAllByAccount(user *types.User, accountId string) ([]*types.TransactionRecurring, error) {
|
|
transactionRecurringMetric.WithLabelValues("get_all_by_account").Inc()
|
|
if user == nil {
|
|
return nil, ErrUnauthorized
|
|
}
|
|
|
|
accountUuid, err := uuid.Parse(accountId)
|
|
if err != nil {
|
|
log.Error("transactionRecurring GetAllByAccount: %v", err)
|
|
return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest)
|
|
}
|
|
|
|
tx, err := s.db.Beginx()
|
|
err = db.TransformAndLogDbError("transactionRecurring GetAllByAccount", nil, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
_ = tx.Rollback()
|
|
}()
|
|
|
|
var rowCount int
|
|
err = tx.Get(&rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, accountUuid, user.Id)
|
|
err = db.TransformAndLogDbError("transactionRecurring GetAllByAccount", nil, err)
|
|
if err != nil {
|
|
if errors.Is(err, db.ErrNotFound) {
|
|
return nil, fmt.Errorf("account %v not found: %w", accountId, ErrBadRequest)
|
|
}
|
|
return nil, types.ErrInternal
|
|
}
|
|
|
|
transactionRecurrings := make([]*types.TransactionRecurring, 0)
|
|
err = tx.Select(&transactionRecurrings, `
|
|
SELECT *
|
|
FROM transaction_recurring
|
|
WHERE user_id = ?
|
|
AND account_id = ?
|
|
ORDER BY created_at DESC`,
|
|
user.Id, accountUuid)
|
|
err = db.TransformAndLogDbError("transactionRecurring GetAll", nil, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = tx.Commit()
|
|
err = db.TransformAndLogDbError("transactionRecurring GetAllByAccount", nil, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return transactionRecurrings, nil
|
|
}
|
|
|
|
func (s TransactionRecurringImpl) GetAllByTreasureChest(user *types.User, treasureChestId string) ([]*types.TransactionRecurring, error) {
|
|
transactionRecurringMetric.WithLabelValues("get_all_by_treasurechest").Inc()
|
|
if user == nil {
|
|
return nil, ErrUnauthorized
|
|
}
|
|
|
|
treasureChestUuid, err := uuid.Parse(treasureChestId)
|
|
if err != nil {
|
|
log.Error("transactionRecurring GetAllByTreasureChest: %v", err)
|
|
return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest)
|
|
}
|
|
|
|
tx, err := s.db.Beginx()
|
|
err = db.TransformAndLogDbError("transactionRecurring GetAllByTreasureChest", nil, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
_ = tx.Rollback()
|
|
}()
|
|
|
|
var rowCount int
|
|
err = tx.Get(&rowCount, `SELECT COUNT(*) FROM treasure_chest WHERE id = ? AND user_id = ?`, treasureChestId, user.Id)
|
|
err = db.TransformAndLogDbError("transactionRecurring GetAllByTreasureChest", nil, err)
|
|
if err != nil {
|
|
if errors.Is(err, db.ErrNotFound) {
|
|
return nil, fmt.Errorf("treasurechest %v not found: %w", treasureChestId, ErrBadRequest)
|
|
}
|
|
return nil, types.ErrInternal
|
|
}
|
|
|
|
transactionRecurrings := make([]*types.TransactionRecurring, 0)
|
|
err = tx.Select(&transactionRecurrings, `
|
|
SELECT *
|
|
FROM transaction_recurring
|
|
WHERE user_id = ?
|
|
AND treasure_chest_id = ?
|
|
ORDER BY created_at DESC`,
|
|
user.Id, treasureChestUuid)
|
|
err = db.TransformAndLogDbError("transactionRecurring GetAll", nil, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = tx.Commit()
|
|
err = db.TransformAndLogDbError("transactionRecurring GetAllByTreasureChest", nil, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return transactionRecurrings, nil
|
|
}
|
|
|
|
func (s TransactionRecurringImpl) Delete(user *types.User, id string) error {
|
|
transactionRecurringMetric.WithLabelValues("delete").Inc()
|
|
if user == nil {
|
|
return ErrUnauthorized
|
|
}
|
|
uuid, err := uuid.Parse(id)
|
|
if err != nil {
|
|
log.Error("transactionRecurring delete: %v", err)
|
|
return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
|
}
|
|
|
|
tx, err := s.db.Beginx()
|
|
err = db.TransformAndLogDbError("transactionRecurring Delete", nil, err)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer func() {
|
|
_ = tx.Rollback()
|
|
}()
|
|
|
|
var transactionRecurring types.TransactionRecurring
|
|
err = tx.Get(&transactionRecurring, `SELECT * FROM transaction_recurring WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
|
err = db.TransformAndLogDbError("transactionRecurring Delete", nil, err)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r, err := tx.Exec("DELETE FROM transaction_recurring WHERE id = ? AND user_id = ?", uuid, user.Id)
|
|
err = db.TransformAndLogDbError("transactionRecurring Delete", r, err)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = tx.Commit()
|
|
err = db.TransformAndLogDbError("transactionRecurring Delete", nil, err)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
|
|
tx *sqlx.Tx,
|
|
oldTransactionRecurring *types.TransactionRecurring,
|
|
userId uuid.UUID,
|
|
input types.TransactionRecurringInput) (*types.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()
|
|
if err != nil {
|
|
return nil, types.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 {
|
|
log.Error("transactionRecurring 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("transactionRecurring validate", nil, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rowCount == 0 {
|
|
log.Error("transactionRecurring validate: %v", err)
|
|
return nil, fmt.Errorf("account not found: %w", ErrBadRequest)
|
|
}
|
|
|
|
hasAccount = true
|
|
}
|
|
|
|
if input.TreasureChestId != "" {
|
|
temp, err := uuid.Parse(input.TreasureChestId)
|
|
if err != nil {
|
|
log.Error("transactionRecurring validate: %v", err)
|
|
return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest)
|
|
}
|
|
treasureChestUuid = &temp
|
|
var treasureChest types.TreasureChest
|
|
err = tx.Get(&treasureChest, `SELECT * FROM treasure_chest WHERE id = ? AND user_id = ?`, treasureChestUuid, userId)
|
|
err = db.TransformAndLogDbError("transactionRecurring validate", nil, err)
|
|
if err != nil {
|
|
if errors.Is(err, db.ErrNotFound) {
|
|
return nil, fmt.Errorf("treasure chest not found: %w", ErrBadRequest)
|
|
}
|
|
return nil, err
|
|
}
|
|
if treasureChest.ParentId == nil {
|
|
return nil, fmt.Errorf("treasure chest is a group: %w", ErrBadRequest)
|
|
}
|
|
hasTreasureChest = true
|
|
}
|
|
|
|
if !hasAccount && !hasTreasureChest {
|
|
log.Error("transactionRecurring validate: %v", err)
|
|
return nil, fmt.Errorf("either account or treasure chest is required: %w", ErrBadRequest)
|
|
}
|
|
if hasAccount && hasTreasureChest {
|
|
log.Error("transactionRecurring validate: %v", err)
|
|
return nil, fmt.Errorf("either account or treasure chest is required, not both: %w", ErrBadRequest)
|
|
}
|
|
|
|
valueFloat, err := strconv.ParseFloat(input.Value, 64)
|
|
if err != nil {
|
|
log.Error("transactionRecurring validate: %v", err)
|
|
return nil, fmt.Errorf("could not parse value: %w", ErrBadRequest)
|
|
}
|
|
valueInt := int64(valueFloat * DECIMALS_MULTIPLIER)
|
|
|
|
if input.Party != "" {
|
|
err = validateString(input.Party, "party")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if input.Description != "" {
|
|
err = validateString(input.Description, "description")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
intervalMonths, err = strconv.ParseInt(input.IntervalMonths, 10, 0)
|
|
if err != nil {
|
|
log.Error("transactionRecurring validate: %v", err)
|
|
return nil, fmt.Errorf("could not parse intervalMonths: %w", ErrBadRequest)
|
|
}
|
|
if intervalMonths < 1 {
|
|
log.Error("transactionRecurring validate: %v", err)
|
|
return nil, fmt.Errorf("intervalMonths needs to be greater than 0: %w", ErrBadRequest)
|
|
}
|
|
active := input.Active == "on"
|
|
|
|
transactionRecurring := types.TransactionRecurring{
|
|
Id: id,
|
|
UserId: userId,
|
|
|
|
IntervalMonths: intervalMonths,
|
|
Active: active,
|
|
LastExecution: nil,
|
|
|
|
Party: input.Party,
|
|
Description: input.Description,
|
|
|
|
AccountId: accountUuid,
|
|
TreasureChestId: treasureChestUuid,
|
|
Value: valueInt,
|
|
|
|
CreatedAt: createdAt,
|
|
CreatedBy: createdBy,
|
|
UpdatedAt: updatedAt,
|
|
UpdatedBy: &updatedBy,
|
|
}
|
|
|
|
return &transactionRecurring, nil
|
|
}
|