Files
spend-sparrow/service/account.go
Tim Wundenberg 1e78b40c3b
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m15s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m26s
feat(transaction): #66 add delete constraints
2025-05-16 12:44:24 +02:00

212 lines
5.1 KiB
Go

package service
import (
"fmt"
"regexp"
"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 (
safeInputRegex = regexp.MustCompile(`^[a-zA-Z0-9äöüß -]+$`)
accountMetric = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "spendsparrow_account_total",
Help: "The total of account operations",
},
[]string{"operation"},
)
)
type Account interface {
Add(user *types.User, name string) (*types.Account, error)
UpdateName(user *types.User, id string, name string) (*types.Account, error)
Get(user *types.User, id string) (*types.Account, error)
GetAll(user *types.User) ([]*types.Account, error)
Delete(user *types.User, id string) error
}
type AccountImpl struct {
db *sqlx.DB
clock Clock
random Random
settings *types.Settings
}
func NewAccount(db *sqlx.DB, random Random, clock Clock, settings *types.Settings) Account {
return AccountImpl{
db: db,
clock: clock,
random: random,
settings: settings,
}
}
func (s AccountImpl) Add(user *types.User, name string) (*types.Account, error) {
accountMetric.WithLabelValues("add").Inc()
if user == nil {
return nil, ErrUnauthorized
}
newId, err := s.random.UUID()
if err != nil {
return nil, types.ErrInternal
}
err = validateString(name, "name")
if err != nil {
return nil, err
}
account := &types.Account{
Id: newId,
UserId: user.Id,
Name: name,
CurrentBalance: 0,
LastTransaction: nil,
OinkBalance: 0,
CreatedAt: s.clock.Now(),
CreatedBy: user.Id,
UpdatedAt: nil,
UpdatedBy: nil,
}
r, err := s.db.NamedExec(`
INSERT INTO account (id, user_id, name, current_balance, oink_balance, created_at, created_by)
VALUES (:id, :user_id, :name, :current_balance, :oink_balance, :created_at, :created_by)`, account)
err = db.TransformAndLogDbError("account Insert", r, err)
if err != nil {
return nil, err
}
return account, nil
}
func (s AccountImpl) UpdateName(user *types.User, id string, name string) (*types.Account, error) {
accountMetric.WithLabelValues("update").Inc()
if user == nil {
return nil, ErrUnauthorized
}
err := validateString(name, "name")
if err != nil {
return nil, err
}
uuid, err := uuid.Parse(id)
if err != nil {
log.Error("account update: %v", err)
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
}
var account types.Account
err = s.db.Get(&account, `SELECT * FROM account WHERE user_id = ? AND id = ?`, user.Id, uuid)
err = db.TransformAndLogDbError("account Update", nil, err)
if err != nil {
if err == db.ErrNotFound {
return nil, fmt.Errorf("account %v not found: %w", id, ErrBadRequest)
}
return nil, types.ErrInternal
}
timestamp := s.clock.Now()
account.Name = name
account.UpdatedAt = &timestamp
account.UpdatedBy = &user.Id
r, err := s.db.NamedExec(`
UPDATE account
SET
name = :name,
updated_at = :updated_at,
updated_by = :updated_by
WHERE id = :id
AND user_id = :user_id`, account)
err = db.TransformAndLogDbError("account Update", r, err)
if err != nil {
return nil, err
}
return &account, nil
}
func (s AccountImpl) Get(user *types.User, id string) (*types.Account, error) {
accountMetric.WithLabelValues("get").Inc()
if user == nil {
return nil, ErrUnauthorized
}
uuid, err := uuid.Parse(id)
if err != nil {
log.Error("account get: %v", err)
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
}
account := &types.Account{}
err = s.db.Get(account, `
SELECT * FROM account WHERE user_id = ? AND id = ?`, user.Id, uuid)
err = db.TransformAndLogDbError("account Get", nil, err)
if err != nil {
log.Error("account get: %v", err)
return nil, err
}
return account, nil
}
func (s AccountImpl) GetAll(user *types.User) ([]*types.Account, error) {
accountMetric.WithLabelValues("get_all").Inc()
if user == nil {
return nil, ErrUnauthorized
}
accounts := make([]*types.Account, 0)
err := s.db.Select(&accounts, `
SELECT * FROM account WHERE user_id = ? ORDER BY name`, user.Id)
err = db.TransformAndLogDbError("account GetAll", nil, err)
if err != nil {
return nil, err
}
return accounts, nil
}
func (s AccountImpl) Delete(user *types.User, id string) error {
accountMetric.WithLabelValues("delete").Inc()
if user == nil {
return ErrUnauthorized
}
uuid, err := uuid.Parse(id)
if err != nil {
log.Error("account delete: %v", err)
return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
}
transactionsCount := 0
err = s.db.Get(&transactionsCount, `SELECT COUNT(*) FROM "transaction" WHERE user_id = ? AND account_id = ?`, user.Id, uuid)
err = db.TransformAndLogDbError("account Delete", nil, err)
if err != nil {
return err
}
if transactionsCount > 0 {
return fmt.Errorf("account has transactions, cannot delete: %w", ErrBadRequest)
}
res, err := s.db.Exec("DELETE FROM account WHERE id = ? and user_id = ?", uuid, user.Id)
err = db.TransformAndLogDbError("account Delete", res, err)
if err != nil {
return err
}
return nil
}