Files
spend-sparrow/service/transaction.go
Tim Wundenberg e1b5d72606
Some checks failed
Build Docker Image / Build-Docker-Image (push) Failing after 3m33s
feat(transaction): #66 implement transactions
2025-05-13 23:02:53 +02:00

271 lines
6.9 KiB
Go

package service
import (
"fmt"
"strconv"
"time"
"spend-sparrow/db"
"spend-sparrow/log"
"spend-sparrow/types"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
transactionMetric = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "spendsparrow_transaction_total",
Help: "The total of transaction operations",
},
[]string{"operation"},
)
)
type Transaction interface {
Add(user *types.User, accountId, treasureChestId, internal, value, timestamp, note string) (*types.Transaction, error)
Update(user *types.User, id, accountId, treasureChestId, internal, value, timestamp, note string) (*types.Transaction, error)
Get(user *types.User, id string) (*types.Transaction, error)
GetAll(user *types.User) ([]*types.Transaction, error)
Delete(user *types.User, id string) error
CanDeleteTreasureChest(user *types.User, treasureChestId uuid.UUID) (bool, error)
CanDeleteAccount(user *types.User, accountId uuid.UUID) (bool, error)
}
type TransactionImpl struct {
db db.Transaction
account Account
treasureChest TreasureChest
clock Clock
random Random
settings *types.Settings
}
func NewTransaction(db db.Transaction, account Account, treasureChest TreasureChest, random Random, clock Clock, settings *types.Settings) Transaction {
return TransactionImpl{
db: db,
account: account,
treasureChest: treasureChest,
clock: clock,
random: random,
settings: settings,
}
}
func (s TransactionImpl) Add(user *types.User, accountId, treasureChestId, internal, value, timestamp, note string) (*types.Transaction, error) {
transactionMetric.WithLabelValues("add").Inc()
if user == nil {
return nil, ErrUnauthorized
}
transaction, err := s.validateTransaction(nil, user.Id, accountId, treasureChestId, internal, value, timestamp, note)
if err != nil {
return nil, err
}
err = s.db.Insert(user.Id, transaction)
if err != nil {
return nil, types.ErrInternal
}
savedTransaction, err := s.db.Get(user.Id, transaction.Id)
if err != nil {
log.Error("transaction %v not found after insert: %v", transaction.Id, err)
return nil, types.ErrInternal
}
return savedTransaction, nil
}
func (s TransactionImpl) Update(user *types.User, id, accountId, treasureChestId, internal, value, timestamp, note string) (*types.Transaction, error) {
transactionMetric.WithLabelValues("update").Inc()
if user == nil {
return nil, ErrUnauthorized
}
uuid, err := uuid.Parse(id)
if err != nil {
log.Error("transaction update: %v", err)
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
}
transaction, err := s.db.Get(user.Id, uuid)
if err != nil {
if err == db.ErrNotFound {
return nil, fmt.Errorf("transaction %v not found: %w", id, ErrBadRequest)
}
return nil, types.ErrInternal
}
transaction, err = s.validateTransaction(transaction, user.Id, accountId, treasureChestId, internal, value, timestamp, note)
if err != nil {
return nil, err
}
err = s.db.Update(user.Id, transaction)
if err != nil {
return nil, types.ErrInternal
}
return transaction, nil
}
func (s TransactionImpl) Get(user *types.User, id string) (*types.Transaction, error) {
transactionMetric.WithLabelValues("get").Inc()
if user == nil {
return nil, ErrUnauthorized
}
uuid, err := uuid.Parse(id)
if err != nil {
log.Error("transaction get: %v", err)
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
}
transaction, err := s.db.Get(user.Id, uuid)
if err != nil {
if err == db.ErrNotFound {
return nil, fmt.Errorf("transaction %v not found: %w", id, ErrBadRequest)
}
return nil, types.ErrInternal
}
return transaction, nil
}
func (s TransactionImpl) GetAll(user *types.User) ([]*types.Transaction, error) {
transactionMetric.WithLabelValues("get_all").Inc()
if user == nil {
return nil, ErrUnauthorized
}
transactions, err := s.db.GetAll(user.Id)
if err != nil {
return nil, types.ErrInternal
}
return transactions, nil
}
func (s TransactionImpl) Delete(user *types.User, id string) error {
transactionMetric.WithLabelValues("delete").Inc()
if user == nil {
return ErrUnauthorized
}
uuid, err := uuid.Parse(id)
if err != nil {
log.Error("transaction delete: %v", err)
return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
}
transaction, err := s.db.Get(user.Id, uuid)
if err != nil {
if err == db.ErrNotFound {
return fmt.Errorf("transaction %v not found: %w", id, ErrBadRequest)
}
return types.ErrInternal
}
if transaction.UserId != user.Id {
return types.ErrUnauthorized
}
err = s.db.Delete(user.Id, transaction.Id)
if err != nil {
return types.ErrInternal
}
return nil
}
func (s TransactionImpl) validateTransaction(transaction *types.Transaction, userId uuid.UUID, accountId, treasureChestId, internal, value, timestamp, note string) (*types.Transaction, error) {
var (
id uuid.UUID
accountUuid uuid.UUID
treasureChestUuid uuid.UUID
internalBool bool
createdAt time.Time
createdBy uuid.UUID
updatedAt *time.Time
updatedBy uuid.UUID
err error
)
if transaction == nil {
id, err = s.random.UUID()
if err != nil {
return nil, types.ErrInternal
}
createdAt = s.clock.Now()
createdBy = userId
} else {
id = transaction.Id
createdAt = transaction.CreatedAt
createdBy = transaction.CreatedBy
time := s.clock.Now()
updatedAt = &time
updatedBy = userId
}
if accountId != "" {
accountUuid, err = uuid.Parse(accountId)
if err != nil {
log.Error("transaction validate: %v", err)
return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest)
}
}
if treasureChestId != "" {
treasureChestUuid, err = uuid.Parse(treasureChestId)
if err != nil {
log.Error("transaction validate: %v", err)
return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest)
}
}
internalBool, err = strconv.ParseBool(internal)
if err != nil {
log.Error("transaction validate: %v", err)
return nil, fmt.Errorf("could not parse internal: %w", ErrBadRequest)
}
valueInt, err := strconv.ParseInt(value, 10, 64)
if err != nil {
log.Error("transaction validate: %v", err)
return nil, fmt.Errorf("could not parse value: %w", ErrBadRequest)
}
timestampTime, err := time.Parse(time.RFC3339, timestamp)
if err != nil {
log.Error("transaction validate: %v", err)
return nil, fmt.Errorf("could not parse timestamp: %w", ErrBadRequest)
}
err = validateString(note)
if err != nil {
return nil, err
}
result := types.Transaction{
Id: id,
UserId: userId,
AccountId: accountUuid,
TreasureChestId: &treasureChestUuid,
Internal: internalBool,
Value: valueInt,
Timestamp: timestampTime,
Note: note,
CreatedAt: createdAt,
CreatedBy: createdBy,
UpdatedAt: updatedAt,
UpdatedBy: &updatedBy,
}
return &result, nil
}