Some checks failed
Build Docker Image / Build-Docker-Image (push) Failing after 3m33s
271 lines
6.9 KiB
Go
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
|
|
}
|