summary history files

desktop/backend/types/tag.go
package types

import (
	"context"
	"database/sql"
	"math/big"
	"pennyapp/backend/internal/dberror"
	"pennyapp/backend/model"
	"strings"

	"github.com/shopspring/decimal"
	"github.com/volatiletech/sqlboiler/v4/queries/qm"
)

type TagResponse struct {
	Success bool   `json:"success"`
	Msg     string `json:"msg"`
	Data    Tag    `json:"data"`
}

type TagsResponse struct {
	Success bool   `json:"success"`
	Msg     string `json:"msg"`
	Data    []Tag  `json:"data"`
}

type Tag struct {
	ID          int64      `json:"id"`
	Name        string     `json:"name"`
	Description string     `json:"description"`
	Deleted     bool       `json:"deleted"`
	Regexes     []TagRegex `json:"regexes"`
	Amount      string     `json:"amount"`
}

type TagRegexResponse struct {
	Success bool     `json:"success"`
	Msg     string   `json:"msg"`
	Data    TagRegex `json:"data"`
}

type TagRegex struct {
	ID    int64  `json:"id"`
	Regex string `json:"regex"`
}

func NewTag(ctx context.Context, db *sql.DB, t *model.Tag) (Tag, error) {
	var err error
	tag := Tag{
		ID:   t.ID,
		Name: t.Name,
	}

	if err = tag.setDescription(ctx, db, t); err != nil {
		return tag, err
	}

	if err = tag.setDeleted(ctx, db, t); err != nil {
		return tag, err
	}

	if err = tag.setRegexes(ctx, db, t); err != nil {
		return tag, err
	}

	if err = tag.setAmount(ctx, db, t); err != nil {
		return tag, err
	}

	return tag, nil
}

func (t *Tag) setDescription(ctx context.Context, db *sql.DB, tag *model.Tag) error {
	attr, err := model.TagAttributes(qm.Where("tag_id=? AND name=?", tag.ID, "description")).One(ctx, db)
	switch {
	case dberror.IsNoRowsFound(err):
	case err != nil:
		return err
	default:
		t.Description = attr.Value
	}
	return nil
}

func (t *Tag) setDeleted(ctx context.Context, db *sql.DB, tag *model.Tag) error {
	attr, err := model.TagAttributes(qm.Where("tag_id=? AND name=?", tag.ID, "deleted")).One(ctx, db)
	switch {
	case dberror.IsNoRowsFound(err):
	case err != nil:
		return err
	default:
		if attr.Value == "true" {
			t.Deleted = true
		}
	}
	return nil
}

func (t *Tag) setRegexes(ctx context.Context, db *sql.DB, tag *model.Tag) error {
	regexes, err := model.TagRegexes(qm.Where("tag_id=?", tag.ID)).All(ctx, db)
	if err != nil {
		return err
	}
	t.Regexes = []TagRegex{}
	for _, regex := range regexes {
		t.Regexes = append(t.Regexes, TagRegex{ID: regex.ID, Regex: regex.Regex})
	}
	return nil
}

func (t *Tag) setAmount(ctx context.Context, db *sql.DB, tag *model.Tag) error {
	q := []qm.QueryMod{
		qm.Where("tag_transactions.tag_id=?", tag.ID),
		qm.InnerJoin("transactions on tag_transactions.transactions_id = transactions.id"),
		qm.Load("Transaction"),
		qm.Load("Transaction.Splits"),
		qm.Load("Transaction.Splits.Account"),
	}
	tagTransactions, err := model.TagTransactions(q...).All(ctx, db)
	if err != nil {
		return err
	}

	amount := float64(0)
	for _, tagTransaction := range tagTransactions {
		if tagTransaction.R == nil && tagTransaction.R.Transaction == nil {
			continue
		}
		transaction := tagTransaction.R.Transaction

		transactionDeleted, err := isTransactionDeleted(ctx, db, transaction.ID)
		if err != nil {
			return err
		}
		if transactionDeleted {
			continue
		}

		for _, split := range transaction.R.Splits {
			q = []qm.QueryMod{
				qm.Where("account.id=?", split.AccountID),
				qm.InnerJoin("account_type on account_type.id = account.account_type_id"),
				qm.Load("AccountType"),
			}
			account, err := model.Accounts(q...).One(ctx, db)
			if err != nil {
				return err
			}

			accountType, err := account.AccountType().One(ctx, db)
			if err != nil {
				return err
			}

			// Only calculate amount for splits which have an account type of
			// expense. There is a FEAT that improves Tag support by tagging
			// transactions by account type which should remove this hack.
			switch strings.ToLower(accountType.Name) {
			case "expense":
				r := big.NewRat(split.ValueNum, split.ValueDenom)
				f, _ := r.Float64()
				amount = amount + f
			default:
			}

		}
	}
	t.Amount = decimal.NewFromFloat(amount).StringFixed(2)
	return nil
}