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 }