feat(transaction): #66 implement transactions
This commit was merged in pull request #72.
This commit is contained in:
292
service/transaction.go
Normal file
292
service/transaction.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"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 (
|
||||
transactionMetric = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "spendsparrow_transaction_total",
|
||||
Help: "The total of transaction operations",
|
||||
},
|
||||
[]string{"operation"},
|
||||
)
|
||||
)
|
||||
|
||||
type Transaction interface {
|
||||
Add(user *types.User, transaction types.TransactionInput) (*types.Transaction, error)
|
||||
Update(user *types.User, transaction types.TransactionInput) (*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
|
||||
}
|
||||
|
||||
type TransactionImpl struct {
|
||||
db *sqlx.DB
|
||||
clock Clock
|
||||
random Random
|
||||
settings *types.Settings
|
||||
}
|
||||
|
||||
func NewTransaction(db *sqlx.DB, random Random, clock Clock, settings *types.Settings) Transaction {
|
||||
return TransactionImpl{
|
||||
db: db,
|
||||
clock: clock,
|
||||
random: random,
|
||||
settings: settings,
|
||||
}
|
||||
}
|
||||
|
||||
func (s TransactionImpl) Add(user *types.User, transactionInput types.TransactionInput) (*types.Transaction, error) {
|
||||
transactionMetric.WithLabelValues("add").Inc()
|
||||
|
||||
if user == nil {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
transaction, err := s.validateAndEnrichTransaction(nil, user.Id, transactionInput)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r, err := s.db.NamedExec(`
|
||||
INSERT INTO "transaction" (id, user_id, account_id, treasure_chest_id, value, timestamp, note, created_at, created_by)
|
||||
VALUES (:id, :user_id, :account_id, :treasure_chest_id, :value, :timestamp, :note, :created_at, :created_by)`, transaction)
|
||||
err = db.TransformAndLogDbError("transaction Insert", r, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transaction, nil
|
||||
}
|
||||
|
||||
func (s TransactionImpl) Update(user *types.User, input types.TransactionInput) (*types.Transaction, error) {
|
||||
transactionMetric.WithLabelValues("update").Inc()
|
||||
if user == nil {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
uuid, err := uuid.Parse(input.Id)
|
||||
if err != nil {
|
||||
log.Error("transaction update: %v", err)
|
||||
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||
}
|
||||
|
||||
transaction := &types.Transaction{}
|
||||
err = s.db.Get(transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
||||
err = db.TransformAndLogDbError("transaction Update", nil, err)
|
||||
if err != nil {
|
||||
if err == db.ErrNotFound {
|
||||
return nil, fmt.Errorf("transaction %v not found: %w", input.Id, ErrBadRequest)
|
||||
}
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
transaction, err = s.validateAndEnrichTransaction(transaction, user.Id, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r, err := s.db.NamedExec(`
|
||||
UPDATE "transaction"
|
||||
SET
|
||||
account_id = :account_id,
|
||||
treasure_chest_id = :treasure_chest_id,
|
||||
value = :value,
|
||||
timestamp = :timestamp,
|
||||
note = :note,
|
||||
updated_at = :updated_at,
|
||||
updated_by = :updated_by
|
||||
WHERE id = :id
|
||||
AND user_id = :user_id`, transaction)
|
||||
err = db.TransformAndLogDbError("transaction Update", r, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
var transaction types.Transaction
|
||||
err = s.db.Get(&transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
||||
err = db.TransformAndLogDbError("transaction Get", nil, err)
|
||||
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 := make([]*types.Transaction, 0)
|
||||
err := s.db.Select(&transactions, `SELECT * FROM "transaction" WHERE user_id = ? ORDER BY timestamp`, user.Id)
|
||||
err = db.TransformAndLogDbError("transaction GetAll", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
r, err := s.db.Exec("DELETE FROM \"transaction\" WHERE id = ? AND user_id = ?", uuid, user.Id)
|
||||
err = db.TransformAndLogDbError("transaction Delete", r, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s TransactionImpl) validateAndEnrichTransaction(transaction *types.Transaction, userId uuid.UUID, input types.TransactionInput) (*types.Transaction, error) {
|
||||
|
||||
var (
|
||||
id uuid.UUID
|
||||
accountUuid *uuid.UUID
|
||||
treasureChestUuid *uuid.UUID
|
||||
createdAt time.Time
|
||||
createdBy uuid.UUID
|
||||
updatedAt *time.Time
|
||||
updatedBy uuid.UUID
|
||||
|
||||
err error
|
||||
rowCount int
|
||||
)
|
||||
|
||||
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 input.AccountId != "" {
|
||||
temp, err := uuid.Parse(input.AccountId)
|
||||
if err != nil {
|
||||
log.Error("transaction validate: %v", err)
|
||||
return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest)
|
||||
}
|
||||
accountUuid = &temp
|
||||
err = s.db.Get(&rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, accountUuid, userId)
|
||||
err = db.TransformAndLogDbError("transaction validate", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rowCount == 0 {
|
||||
log.Error("transaction validate: %v", err)
|
||||
return nil, fmt.Errorf("account not found: %w", ErrBadRequest)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if input.TreasureChestId != "" {
|
||||
temp, err := uuid.Parse(input.TreasureChestId)
|
||||
if err != nil {
|
||||
log.Error("transaction validate: %v", err)
|
||||
return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest)
|
||||
}
|
||||
treasureChestUuid = &temp
|
||||
err = s.db.Get(&rowCount, `SELECT COUNT(*) FROM treasure_chest WHERE id = ? AND user_id = ?`, treasureChestUuid, userId)
|
||||
err = db.TransformAndLogDbError("transaction validate", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rowCount == 0 {
|
||||
log.Error("transaction validate: %v", err)
|
||||
return nil, fmt.Errorf("treasure chest not found: %w", ErrBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
valueFloat, err := strconv.ParseFloat(input.Value, 64)
|
||||
if err != nil {
|
||||
log.Error("transaction validate: %v", err)
|
||||
return nil, fmt.Errorf("could not parse value: %w", ErrBadRequest)
|
||||
}
|
||||
valueInt := int64(valueFloat * 100)
|
||||
|
||||
timestampTime, err := time.Parse("2006-01-02T15:04", input.Timestamp)
|
||||
if err != nil {
|
||||
log.Error("transaction validate: %v", err)
|
||||
return nil, fmt.Errorf("could not parse timestamp: %w", ErrBadRequest)
|
||||
}
|
||||
|
||||
if input.TimezoneOffsetMinutes != "" {
|
||||
timezoneOffsetMinutes, err := strconv.Atoi(input.TimezoneOffsetMinutes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse timezone offset: %w", ErrBadRequest)
|
||||
}
|
||||
timestampTime = timestampTime.Add(time.Duration(-1*timezoneOffsetMinutes) * time.Minute)
|
||||
}
|
||||
|
||||
if input.Note != "" {
|
||||
err = validateString(input.Note, "note")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &types.Transaction{
|
||||
Id: id,
|
||||
UserId: userId,
|
||||
|
||||
AccountId: accountUuid,
|
||||
TreasureChestId: treasureChestUuid,
|
||||
Value: valueInt,
|
||||
Timestamp: timestampTime,
|
||||
Note: input.Note,
|
||||
|
||||
CreatedAt: createdAt,
|
||||
CreatedBy: createdBy,
|
||||
UpdatedAt: updatedAt,
|
||||
UpdatedBy: &updatedBy,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user