desktop/backend/types/transaction.go
package types
import (
"context"
"database/sql"
"fmt"
"math/big"
"pennyapp/backend/internal/dberror"
"pennyapp/backend/model"
"time"
"github.com/shopspring/decimal"
"github.com/volatiletech/sqlboiler/v4/queries/qm"
)
// DefaultDBTimeLayout is the time layout used in datastore.
const DefaultDBTimeLayout string = "2006-01-02 15:04:05.000"
type TransactionResponse struct {
Success bool `json:"success"`
Msg string `json:"msg"`
Data Transaction `json:"data"`
}
type TransactionsResponse struct {
Success bool `json:"success"`
Msg string `json:"msg"`
Data []Transaction `json:"data"`
}
// TransactionsTotalBalance is the total balance amount for all non-deleted
// transactions.
type TransactionsTotalBalance struct {
Success bool `json:"success"`
Msg string `json:"msg"`
Data string `json:"data"`
}
// TransactionsNetAssetsResponse is the total net assets amount for all
// non-deleted transactions.
type TransactionsNetAssetsResponse struct {
Success bool `json:"success"`
Msg string `json:"msg"`
Data string `json:"data"`
}
// NewTransactionResponse returns a default TransactionResponse. All attributes
// of TransactionResponse must be set or else the struct wont be returned
// correctly to the frontend.
func NewTransactionResponse() TransactionResponse {
t := TransactionResponse{
Success: false,
Msg: "default",
Data: Transaction{},
}
return t
}
type Transaction struct {
ID int64 `json:"id"`
Memo string `json:"memo"`
Date string `json:"date"`
Splits []Split `json:"splits"`
Amount string `json:"amount"`
Deleted bool `json:"deleted"`
Display TransactionDisplay `json:"display"`
Currency `json:"currency"`
}
type TransactionDisplay struct {
Account TransactionDisplayAccount `json:"account"`
}
type TransactionDisplayAccount struct {
Name string `json:"name"`
ID int64 `json:"id"`
}
func NewTransaction(ctx context.Context, db *sql.DB, t *model.Transaction) (Transaction, error) {
var err error
transaction := Transaction{
ID: t.ID,
Memo: t.Memo,
}
date, err := time.Parse(DefaultDBTimeLayout, t.Date)
if err != nil {
return transaction, err
}
transaction.Date = date.Format("2006-01-02")
if err = transaction.setSplits(ctx, db, t); err != nil {
return transaction, err
}
if err = transaction.setDeleted(ctx, db, t); err != nil {
return transaction, err
}
transaction.setAmount()
transaction.setDisplay()
return transaction, nil
}
func (t *Transaction) setSplits(ctx context.Context, db *sql.DB, transaction *model.Transaction) error {
splits, err := transaction.Splits(qm.Select("splits.id, splits.transactions_id, splits.account_id, splits.value_num, splits.value_denom")).All(ctx, db)
if err != nil {
return err
}
s := []Split{}
for _, ii := range splits {
account, err := model.FindAccount(ctx, db, ii.AccountID)
if err != nil {
return err
}
accountType, err := model.FindAccountType(ctx, db, account.AccountTypeID)
if err != nil {
return err
}
parentAccountType := &ParentAccountType{}
if accountType.ParentID != 0 {
pat, err := model.FindAccountType(ctx, db, accountType.ParentID)
if err != nil {
return err
}
parentAccountType = &ParentAccountType{
ID: pat.ID,
Name: pat.Name,
}
}
split := Split{
ID: ii.ID,
ValueNum: ii.ValueNum,
ValueDenom: ii.ValueDenom,
Amount: getAmountAsString(ii.ValueNum, ii.ValueDenom),
Account: Account{
ID: account.ID,
Name: account.Name,
AccountType: AccountType{
ID: accountType.ID,
Name: accountType.Name,
ParentAccountType: parentAccountType,
},
},
}
s = append(s, split)
}
t.Splits = s
return nil
}
func (t *Transaction) setDisplay() {
display := TransactionDisplay{
Account: TransactionDisplayAccount{},
}
switch len(t.Splits) {
case 1:
default:
split := t.Splits[1]
display = TransactionDisplay{
Account: TransactionDisplayAccount{
Name: split.Account.Name,
ID: split.Account.ID,
},
}
}
t.Display = display
return
}
func (t *Transaction) setAmount() {
if len(t.Splits) < 1 {
t.Amount = "0"
return
}
// TODO: work out how to show proper amount. For now I will just display
// the amount of the first split.
firstSplit := t.Splits[0]
t.Amount = getAmountAsString(firstSplit.ValueNum, firstSplit.ValueDenom)
if firstSplit.Account.AccountType.ParentAccountType.Name == "Expenses" {
t.Amount = fmt.Sprintf("-%s", t.Amount)
}
}
func (t *Transaction) setDeleted(ctx context.Context, db *sql.DB, transaction *model.Transaction) error {
transactionAttribute, err := model.TransactionsAttributes(qm.Where("transactions_id=? AND name=?", transaction.ID, "deleted")).One(ctx, db)
switch {
case dberror.IsNoRowsFound(err):
case err != nil:
return err
default:
if transactionAttribute.Value == "true" {
t.Deleted = true
}
}
return nil
}
// getAmountAsString accepts a numerator, a denominator and returns the decimal
// amount as a string.
func getAmountAsString(num, denom int64) string {
r := big.NewRat(num, denom)
f, _ := r.Float64()
return decimal.NewFromFloat(f).StringFixed(2)
}