summary history files

internal/cli/report.go
package cli

import (
	"gt/internal/render"
	"gt/internal/report"
	"gt/internal/store"
	"time"

	"github.com/spf13/cobra"
)

func reportCmd(cli *cli) *cobra.Command {
	var cmd = &cobra.Command{
		Use: "report",
	}
	cmd.AddCommand(incomeExpenseReportCmd(cli))
	return cmd
}

type AccountType string

const (
	AccountTypeLiability AccountType = "LIABILITY"
	AccountTypeExpense   AccountType = "EXPENSE"
	AccountTypeIncome    AccountType = "INCOME"
)

func incomeExpenseReportCmd(cli *cli) *cobra.Command {
	var flags struct {
		sortBy    string
		startDate string
		endDate   string
	}

	var cmd = &cobra.Command{
		Use:   "income-expense [flags]",
		Short: "Income Expense Report",
		Example: `
* Print report between 2 dates:

  $ gt report income-expense --start-date 2026-01-01 --end-date 2026-01-31

* Print report sorting by 'value':

  $ gt report income-expense --sort-by value

`,
		RunE: func(cmd *cobra.Command, args []string) error {
			var err error
			s := store.NewStore(cli.db)
			transactionQuery := store.NewTransactionQuery().
				Where("transactions.post_date > ?", flags.startDate).
				Where("transactions.post_date < ?", flags.endDate)
			transactions, err := s.Transactions.All(cmd.Context(), transactionQuery)
			if err != nil {
				return err
			}

			p := report.IncomeExpenseReport{
				Income:  []*report.AccountItem{},
				Expense: []*report.AccountItem{},
			}

			for _, t := range transactions {
				for _, s := range t.Splits {
					switch s.Account.AccountType {
					case string(AccountTypeExpense):
						exists := false
						for _, i := range p.Expense {
							if i.Account.GUID == s.Account.GUID {
								i.ValueNum = i.ValueNum + s.ValueNum
								exists = true
								break
							}
						}
						if !exists {
							item := &report.AccountItem{
								Account:    *s.Account,
								ValueNum:   s.ValueNum,
								ValueDenom: s.ValueDenom,
							}
							p.Expense = append(p.Expense, item)
						}
					case string(AccountTypeIncome):
						exists := false
						for _, i := range p.Income {
							if i.Account.GUID == s.Account.GUID {
								i.ValueNum = i.ValueNum + s.ValueNum
								exists = true
								break
							}
						}
						if !exists {
							item := &report.AccountItem{
								Account:    *s.Account,
								ValueNum:   s.ValueNum,
								ValueDenom: s.ValueDenom,
							}
							p.Income = append(p.Income, item)
						}
					}
				}
			}

			r, err := render.New("table")
			if err != nil {
				return err
			}

			renderOpts := []render.RendererOptsFunc{render.WithAccountShortName(true), render.WithSortBy(flags.sortBy)}
			return r.Render(cmd.OutOrStdout(), p, renderOpts...)
		},
	}
	cmd.Flags().StringVar(&flags.sortBy, "sort-by", "name", "Sort report items by name or value")
	cmd.Flags().StringVar(&flags.startDate, "start-date", time.Now().AddDate(0, 0, -31).Format("2006-01-02 15:04:05"), "Start date of the report")
	cmd.Flags().StringVar(&flags.endDate, "end-date", time.Now().Format("2006-01-02 15:04:05"), "End date of the report")
	return cmd
}