package service import ( "context" "errors" "fmt" "log/slog" "spend-sparrow/internal/db" "spend-sparrow/internal/types" "github.com/google/uuid" "github.com/jmoiron/sqlx" ) type Account interface { Add(ctx context.Context, user *types.User, name string) (*types.Account, error) UpdateName(ctx context.Context, user *types.User, id string, name string) (*types.Account, error) Get(ctx context.Context, user *types.User, id string) (*types.Account, error) GetAll(ctx context.Context, user *types.User) ([]*types.Account, error) Delete(ctx context.Context, user *types.User, id string) error } type AccountImpl struct { db *sqlx.DB clock Clock random Random } func NewAccount(db *sqlx.DB, random Random, clock Clock) Account { return AccountImpl{ db: db, clock: clock, random: random, } } func (s AccountImpl) Add(ctx context.Context, user *types.User, name string) (*types.Account, error) { 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.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("account Insert", r, err) if err != nil { return nil, err } return account, nil } func (s AccountImpl) UpdateName(ctx context.Context, user *types.User, id string, name string) (*types.Account, error) { if user == nil { return nil, ErrUnauthorized } err := validateString(name, "name") if err != nil { return nil, err } uuid, err := uuid.Parse(id) if err != nil { slog.Error("account update", "err", err) return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) } tx, err := s.db.BeginTxx(ctx, nil) err = db.TransformAndLogDbError("account Update", nil, err) if err != nil { return nil, err } defer func() { _ = tx.Rollback() }() var account types.Account err = tx.GetContext(ctx, &account, `SELECT * FROM account WHERE user_id = ? AND id = ?`, user.Id, uuid) err = db.TransformAndLogDbError("account Update", nil, err) if err != nil { if errors.Is(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 = ×tamp 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("account Update", r, err) if err != nil { return nil, err } err = tx.Commit() err = db.TransformAndLogDbError("account Update", nil, err) if err != nil { return nil, err } return &account, nil } func (s AccountImpl) Get(ctx context.Context, user *types.User, id string) (*types.Account, error) { if user == nil { return nil, ErrUnauthorized } uuid, err := uuid.Parse(id) if err != nil { slog.Error("account get", "err", err) return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) } var account types.Account err = s.db.GetContext(ctx, &account, ` SELECT * FROM account WHERE user_id = ? AND id = ?`, user.Id, uuid) err = db.TransformAndLogDbError("account Get", nil, err) if err != nil { slog.Error("account get", "err", err) return nil, err } return &account, nil } func (s AccountImpl) GetAll(ctx context.Context, user *types.User) ([]*types.Account, error) { if user == nil { return nil, ErrUnauthorized } accounts := make([]*types.Account, 0) err := s.db.SelectContext(ctx, &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(ctx context.Context, user *types.User, id string) error { if user == nil { return ErrUnauthorized } uuid, err := uuid.Parse(id) if err != nil { slog.Error("account delete", "err", err) return fmt.Errorf("could not parse Id: %w", ErrBadRequest) } tx, err := s.db.BeginTxx(ctx, nil) err = db.TransformAndLogDbError("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("account Delete", nil, err) if err != nil { return err } if transactionsCount > 0 { return fmt.Errorf("account has transactions, cannot delete: %w", ErrBadRequest) } res, err := tx.ExecContext(ctx, "DELETE FROM account WHERE id = ? and user_id = ?", uuid, user.Id) err = db.TransformAndLogDbError("account Delete", res, err) if err != nil { return err } err = tx.Commit() err = db.TransformAndLogDbError("account Delete", nil, err) if err != nil { return err } return nil }