summary history files

desktop/backend/services/account_service.go
package services

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"pennyapp/backend/defaultresources"
	"pennyapp/backend/internal/dberror"
	"pennyapp/backend/logwrap"
	"pennyapp/backend/model"
	"pennyapp/backend/types"
	"strings"

	"github.com/volatiletech/sqlboiler/v4/boil"
	"github.com/volatiletech/sqlboiler/v4/queries/qm"
)

var (
	ErrAccountNotFound          = errors.New("account not found")
	ErrAccountAttributeNotFound = errors.New("account attribute not found")
)

type accountService struct {
	ctx              context.Context
	db               *sql.DB
	logger           *logwrap.LogWrap
	defaultResources defaultresources.DefaultResources
}

var account *accountService

func Account() *accountService {
	account = &accountService{}
	return account
}

func (a *accountService) Start(ctx context.Context, db *sql.DB, logger *logwrap.LogWrap, defaultResources defaultresources.DefaultResources) {
	a.ctx = ctx
	a.db = db
	a.logger = logger
	a.defaultResources = defaultResources
}

func (a *accountService) getChildAccountType(name string) (model.AccountType, error) {
	var accountType model.AccountType
	var err error

	childAccountTypes, err := a.getAllChildAccountTypes()
	if err != nil {
		return accountType, err
	}

	for _, i := range childAccountTypes {
		if strings.ToLower(i.Name) == strings.ToLower(name) {
			accountType = i
			break
		}
	}

	if (model.AccountType{}) == accountType {
		return accountType, fmt.Errorf("failed to find child account type: %s", name)
	}

	return accountType, nil
}

func (a *accountService) getAllChildAccountTypes() ([]model.AccountType, error) {
	var childAccountTypes []model.AccountType
	var err error
	var q []qm.QueryMod

	q = []qm.QueryMod{
		qm.Where("name=?", "AccountType"),
		qm.Where("parent_id=0"),
	}
	grandParentAccountType, err := model.AccountTypes(q...).One(a.ctx, a.db)
	if err != nil {
		return childAccountTypes, err
	}

	q = []qm.QueryMod{
		qm.Where("parent_id=?", grandParentAccountType.ID),
	}
	parentAccountTypes, err := model.AccountTypes(q...).All(a.ctx, a.db)
	if err != nil {
		return childAccountTypes, err
	}

	for _, parentAccountType := range parentAccountTypes {
		q = []qm.QueryMod{
			qm.Where("parent_id=?", parentAccountType.ID),
		}
		c, err := model.AccountTypes(q...).All(a.ctx, a.db)
		if err != nil {
			return childAccountTypes, err
		}

		for _, childAccountType := range c {
			childAccountTypes = append(childAccountTypes, *childAccountType)
		}
	}

	return childAccountTypes, nil
}

func (a *accountService) GetChildAccountTypes() types.JSResp {
	var resp types.JSResp

	childAccountTypes, err := a.getAllChildAccountTypes()
	if err != nil {
		resp.Msg = "Failed to get child account types"
		a.logger.Error(resp.Msg)
		return resp
	}

	data := []types.AccountType{}
	for _, childAccountType := range childAccountTypes {
		accountType := types.AccountType{
			ID:   childAccountType.ID,
			Name: childAccountType.Name,
		}
		data = append(data, accountType)
	}

	resp.Success = true
	resp.Data = data

	return resp
}

func (a *accountService) UpdateAccount(accountID int64, name, description string, accountTypeID int64) types.JSResp {
	var resp types.JSResp

	tx, err := a.db.BeginTx(a.ctx, nil)
	if err != nil {
		resp.Msg = err.Error()
		a.logger.Error(resp.Msg)
		return resp
	}
	defer tx.Rollback()

	account, err := model.FindAccount(a.ctx, tx, accountID)
	if err != nil {
		resp.Msg = err.Error()
		a.logger.Error(resp.Msg)
		return resp
	}
	account.Name = name
	account.AccountTypeID = accountTypeID

	_, err = account.Update(a.ctx, tx, boil.Whitelist("name", "account_type_id"))
	if err != nil {
		resp.Msg = err.Error()
		a.logger.Error(resp.Msg)
		return resp
	}

	accountType, err := model.FindAccountType(a.ctx, tx, accountTypeID)
	if err != nil {
		resp.Msg = err.Error()
		a.logger.Error(resp.Msg)
		return resp
	}
	account.AccountTypeID = accountType.ID

	accountAttribute := model.AccountAttribute{
		ID:        0,
		AccountID: account.ID,
		Name:      "description",
		Value:     description,
	}
	if err := accountAttribute.Upsert(a.ctx, tx, true, []string{"account_id", "name"}, boil.Whitelist("value"), boil.Infer()); err != nil {
		resp.Msg = err.Error()
		a.logger.Error(resp.Msg)
		return resp
	}

	if err := tx.Commit(); err != nil {
		resp.Msg = err.Error()
		a.logger.Error(resp.Msg)
		return resp
	}

	resp.Success = true
	resp.Msg = "Account updated"

	return resp
}

func (a *accountService) createAccount(name, description string, accountTypeID int64) (model.Account, error) {
	var err error
	var account model.Account

	accountType, err := model.AccountTypes(qm.Where("id=?", accountTypeID)).One(a.ctx, a.db)
	if err != nil {
		return account, err
	}

	account = model.Account{
		Name:          name,
		EntityID:      a.defaultResources.EntityID(),
		AccountTypeID: accountType.ID,
		ParentID:      a.defaultResources.GrandParentAccountID(),
	}

	if err := account.Upsert(a.ctx, a.db, true, []string{"name", "parent_id", "entity_id", "account_type_id"}, boil.Whitelist("name"), boil.Infer()); err != nil {
		return account, err
	}

	if len(description) != 0 {
		accountAttribute := model.AccountAttribute{
			AccountID: account.ID,
			Name:      "description",
			Value:     description,
		}
		if err := accountAttribute.Insert(a.ctx, a.db, boil.Infer()); err != nil {
			return account, err
		}
	}

	return account, nil
}

func (a *accountService) deleteAccountAttribute(accountID int64, name string) (int64, error) {
	return model.AccountAttributes(qm.Where("account_id=? AND name=?", accountID, name)).DeleteAll(a.ctx, a.db)
}

func (a *accountService) upsertAccountAttribute(accountID int64, name, value string) (model.AccountAttribute, error) {
	accountAttribute := model.AccountAttribute{
		ID:        0,
		AccountID: accountID,
		Name:      name,
		Value:     value,
	}
	if err := accountAttribute.Upsert(a.ctx, a.db, true, []string{"account_id", "name"}, boil.Whitelist("value"), boil.Infer()); err != nil {
		return accountAttribute, err
	}
	return accountAttribute, nil
}

func (a *accountService) UndeleteAccount(accountID int64) types.AccountResponse {
	resp := types.NewAccountResponse()
	account, err := model.FindAccount(a.ctx, a.db, accountID)
	if err != nil {
		resp.Msg = fmt.Sprintf("Unable to find account: %s", err.Error())
		a.logger.Error(resp.Msg)
		return resp
	}

	if _, err := a.deleteAccountAttribute(account.ID, "deleted"); err != nil {
		resp.Msg = fmt.Sprintf("Failed to undelete account: %s", err.Error())
		a.logger.Error(resp.Msg)
		return resp
	}

	resp.Msg = "Account undeleted"
	resp.Success = true

	return resp
}

func (a *accountService) DeleteAccount(accountID int64) types.AccountResponse {
	resp := types.NewAccountResponse()
	account, err := model.FindAccount(a.ctx, a.db, accountID)
	if err != nil {
		resp.Msg = fmt.Sprintf("Unable to find account: %s", err.Error())
		a.logger.Error(resp.Msg)
		return resp
	}

	splitsExist, err := model.Splits(qm.Where("account_id=?", accountID)).Exists(a.ctx, a.db)
	switch {
	case err != nil:
		resp.Msg = err.Error()
		a.logger.Error(resp.Msg)
		return resp
	case splitsExist:
		resp.Msg = "Account still assigned to transactions"
		return resp
	}

	if _, err := a.upsertAccountAttribute(account.ID, "deleted", "true"); err != nil {
		resp.Msg = fmt.Sprintf("Failed to delete account: %s", err.Error())
		a.logger.Error(resp.Msg)
		return resp
	}

	resp.Msg = "Account deleted"
	resp.Success = true

	return resp
}

func (a *accountService) CreateAccount(name, description string, accountTypeID int64) types.AccountResponse {
	resp := types.NewAccountResponse()

	if len(name) == 0 {
		resp.Msg = "Name must be not be empty"
		return resp
	}

	if accountTypeID == 0 {
		resp.Msg = "Account Type must not be empty"
		return resp
	}

	account, err := a.createAccount(name, description, accountTypeID)
	switch {
	case dberror.IsUniqueConstraint(err):
		resp.Msg = "Account already exists"
		return resp
	case err != nil:
		resp.Msg = fmt.Sprintf("Failed to create Account: %s", err.Error())
		a.logger.Error(resp.Msg)
		return resp
	}

	// Check if the account was previously deleted and if it was, remove the account attribute delete.
	accountAttribute, err := model.AccountAttributes(qm.Where("account_id=? AND name=?", account.ID, "deleted")).One(a.ctx, a.db)
	switch {
	case dberror.IsNoRowsFound(err):
	case err != nil:
		resp.Msg = err.Error()
		a.logger.Error(resp.Msg)
		return resp
	default:
		if _, err := accountAttribute.Delete(a.ctx, a.db); err != nil {
			resp.Msg = err.Error()
			a.logger.Error(resp.Msg)
			return resp
		}
	}

	resp.Data, err = types.NewAccount(a.ctx, a.db, &account)
	if err != nil {
		resp.Msg = err.Error()
		a.logger.Error(resp.Msg)
		return resp
	}

	resp.Msg = "Account created"
	resp.Success = true
	return resp
}

func (a *accountService) getAccount(id int64) (*model.Account, error) {
	q := []qm.QueryMod{
		qm.Where("account.id=?", id),
		qm.Where("account.entity_id=?", a.defaultResources.EntityID()),
		qm.InnerJoin("account_type on account_type.id = account.account_type_id"),
		qm.Load("AccountType"),
	}
	return model.Accounts(q...).One(a.ctx, a.db)
}

func (a *accountService) GetAccountByName(name string) types.AccountResponse {
	var resp types.AccountResponse
	q := []qm.QueryMod{
		qm.Where("account.name=?", name),
		qm.InnerJoin("account_type on account_type.id = account.account_type_id"),
		qm.Load("AccountType"),
	}
	account, err := model.Accounts(q...).One(a.ctx, a.db)
	if err != nil {
		resp.Msg = err.Error()
		a.logger.Error(resp.Msg)
		return resp
	}
	resp.Data, err = types.NewAccount(a.ctx, a.db, account)
	if err != nil {
		resp.Msg = err.Error()
		a.logger.Error(resp.Msg)
		return resp
	}
	resp.Success = true
	return resp
}

func (a *accountService) GetAccount(id int64) types.AccountResponse {
	var err error

	resp := types.NewAccountResponse()

	account, err := a.getAccount(id)
	switch {
	case dberror.IsNoRowsFound(err):
		resp.Msg = "Account not found"
		a.logger.Error(resp.Msg)
		return resp
	case err != nil:
		resp.Msg = fmt.Sprintf("Failed to find account: %s", err.Error())
		a.logger.Error(resp.Msg)
		return resp
	}

	resp.Data, err = types.NewAccount(a.ctx, a.db, account)
	if err != nil {
		resp.Msg = err.Error()
		a.logger.Error(resp.Msg)
		return resp
	}

	resp.Success = true

	return resp
}

func (a *accountService) GetAccounts() types.JSResp {
	var err error
	var resp types.JSResp
	var q []qm.QueryMod

	q = []qm.QueryMod{
		qm.Where("account.entity_id=?", a.defaultResources.EntityID()),
		qm.Where("account.parent_id=?", a.defaultResources.GrandParentAccountID()),
		qm.InnerJoin("account_type on account_type.id = account.account_type_id"),
		qm.Load("AccountType"),
	}

	accounts, err := model.Accounts(q...).All(a.ctx, a.db)
	if err != nil {
		resp.Msg = err.Error()
		a.logger.Error(resp.Msg)
		return resp
	}

	data := []types.Account{}
	for _, account := range accounts {
		d, err := types.NewAccount(a.ctx, a.db, account)
		if err != nil {
			resp.Msg = err.Error()
			a.logger.Error(resp.Msg)
			return resp
		}

		if d.Deleted == true {
			continue
		}

		data = append(data, d)
	}

	resp.Success = true
	resp.Data = data

	return resp
}