feat: extract account to domain package
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m17s

This commit is contained in:
2025-12-24 07:45:44 +01:00
parent 1e61b765ae
commit f9a5a9e5f9
24 changed files with 281 additions and 284 deletions

219
internal/account/service.go Normal file
View File

@@ -0,0 +1,219 @@
package account
import (
"context"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"log/slog"
"spend-sparrow/internal/db"
"spend-sparrow/internal/service"
"spend-sparrow/internal/types"
)
type Service interface {
Add(ctx context.Context, user *types.User, name string) (*Account, error)
UpdateName(ctx context.Context, user *types.User, id string, name string) (*Account, error)
Get(ctx context.Context, user *types.User, id string) (*Account, error)
GetAll(ctx context.Context, user *types.User) ([]*Account, error)
Delete(ctx context.Context, user *types.User, id string) error
}
type ServiceImpl struct {
db *sqlx.DB
clock service.Clock
random service.Random
}
func NewServiceImpl(db *sqlx.DB, random service.Random, clock service.Clock) Service {
return ServiceImpl{
db: db,
clock: clock,
random: random,
}
}
func (s ServiceImpl) Add(ctx context.Context, user *types.User, name string) (*Account, error) {
if user == nil {
return nil, types.ErrUnauthorized
}
newId, err := s.random.UUID(ctx)
if err != nil {
return nil, types.ErrInternal
}
err = service.ValidateString(name, "name")
if err != nil {
return nil, err
}
account := &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.NamedExecContext(ctx, `
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(ctx, "account Insert", r, err)
if err != nil {
return nil, err
}
return account, nil
}
func (s ServiceImpl) UpdateName(ctx context.Context, user *types.User, id string, name string) (*Account, error) {
if user == nil {
return nil, types.ErrUnauthorized
}
err := service.ValidateString(name, "name")
if err != nil {
return nil, err
}
uuid, err := uuid.Parse(id)
if err != nil {
slog.ErrorContext(ctx, "account update", "err", err)
return nil, fmt.Errorf("could not parse Id: %w", service.ErrBadRequest)
}
tx, err := s.db.BeginTxx(ctx, nil)
err = db.TransformAndLogDbError(ctx, "account Update", nil, err)
if err != nil {
return nil, err
}
defer func() {
_ = tx.Rollback()
}()
var account Account
err = tx.GetContext(ctx, &account, `SELECT * FROM account WHERE user_id = ? AND id = ?`, user.Id, uuid)
err = db.TransformAndLogDbError(ctx, "account Update", nil, err)
if err != nil {
if errors.Is(err, db.ErrNotFound) {
return nil, fmt.Errorf("account %v not found: %w", id, service.ErrBadRequest)
}
return nil, types.ErrInternal
}
timestamp := s.clock.Now()
account.Name = name
account.UpdatedAt = &timestamp
account.UpdatedBy = &user.Id
r, err := tx.NamedExecContext(ctx, `
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(ctx, "account Update", r, err)
if err != nil {
return nil, err
}
err = tx.Commit()
err = db.TransformAndLogDbError(ctx, "account Update", nil, err)
if err != nil {
return nil, err
}
return &account, nil
}
func (s ServiceImpl) Get(ctx context.Context, user *types.User, id string) (*Account, error) {
if user == nil {
return nil, service.ErrUnauthorized
}
uuid, err := uuid.Parse(id)
if err != nil {
slog.ErrorContext(ctx, "account get", "err", err)
return nil, fmt.Errorf("could not parse Id: %w", service.ErrBadRequest)
}
var account Account
err = s.db.GetContext(ctx, &account, `
SELECT * FROM account WHERE user_id = ? AND id = ?`, user.Id, uuid)
err = db.TransformAndLogDbError(ctx, "account Get", nil, err)
if err != nil {
slog.ErrorContext(ctx, "account get", "err", err)
return nil, err
}
return &account, nil
}
func (s ServiceImpl) GetAll(ctx context.Context, user *types.User) ([]*Account, error) {
if user == nil {
return nil, service.ErrUnauthorized
}
accounts := make([]*Account, 0)
err := s.db.SelectContext(ctx, &accounts, `
SELECT * FROM account WHERE user_id = ? ORDER BY name`, user.Id)
err = db.TransformAndLogDbError(ctx, "account GetAll", nil, err)
if err != nil {
return nil, err
}
return accounts, nil
}
func (s ServiceImpl) Delete(ctx context.Context, user *types.User, id string) error {
if user == nil {
return service.ErrUnauthorized
}
uuid, err := uuid.Parse(id)
if err != nil {
slog.ErrorContext(ctx, "account delete", "err", err)
return fmt.Errorf("could not parse Id: %w", service.ErrBadRequest)
}
tx, err := s.db.BeginTxx(ctx, nil)
err = db.TransformAndLogDbError(ctx, "account Delete", nil, err)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
transactionsCount := 0
err = tx.GetContext(ctx, &transactionsCount, `SELECT COUNT(*) FROM "transaction" WHERE user_id = ? AND account_id = ?`, user.Id, uuid)
err = db.TransformAndLogDbError(ctx, "account Delete", nil, err)
if err != nil {
return err
}
if transactionsCount > 0 {
return fmt.Errorf("account has transactions, cannot delete: %w", service.ErrBadRequest)
}
res, err := tx.ExecContext(ctx, "DELETE FROM account WHERE id = ? and user_id = ?", uuid, user.Id)
err = db.TransformAndLogDbError(ctx, "account Delete", res, err)
if err != nil {
return err
}
err = tx.Commit()
err = db.TransformAndLogDbError(ctx, "account Delete", nil, err)
if err != nil {
return err
}
return nil
}