// SplitRels is where relationship names are stored.
var SplitRels = struct {
- Account string
- Transaction string
- SplitsAttributes string
+ Account string
+ Transaction string
}{
- Account: "Account",
- Transaction: "Transaction",
- SplitsAttributes: "SplitsAttributes",
+ Account: "Account",
+ Transaction: "Transaction",
}
// splitR is where relationships are stored.
type splitR struct {
- Account *Account `boil:"Account" json:"Account" toml:"Account" yaml:"Account"`
- Transaction *Transaction `boil:"Transaction" json:"Transaction" toml:"Transaction" yaml:"Transaction"`
- SplitsAttributes SplitsAttributeSlice `boil:"SplitsAttributes" json:"SplitsAttributes" toml:"SplitsAttributes" yaml:"SplitsAttributes"`
+ Account *Account `boil:"Account" json:"Account" toml:"Account" yaml:"Account"`
+ Transaction *Transaction `boil:"Transaction" json:"Transaction" toml:"Transaction" yaml:"Transaction"`
}
// NewStruct creates a new relationship struct
return r.Transaction
}
-func (r *splitR) GetSplitsAttributes() SplitsAttributeSlice {
- if r == nil {
- return nil
- }
- return r.SplitsAttributes
-}
-
// splitL is where Load methods for each relationship are stored.
type splitL struct{}
return Transactions(queryMods...)
}
-// SplitsAttributes retrieves all the splits_attribute's SplitsAttributes with an executor.
-func (o *Split) SplitsAttributes(mods ...qm.QueryMod) splitsAttributeQuery {
- var queryMods []qm.QueryMod
- if len(mods) != 0 {
- queryMods = append(queryMods, mods...)
- }
-
- queryMods = append(queryMods,
- qm.Where("\"splits_attributes\".\"splits_id\"=?", o.ID),
- )
-
- return SplitsAttributes(queryMods...)
-}
-
// LoadAccount allows an eager lookup of values, cached into the
// loaded structs of the objects. This is for an N-1 relationship.
func (splitL) LoadAccount(ctx context.Context, e boil.ContextExecutor, singular bool, maybeSplit interface{}, mods queries.Applicator) error {
return nil
}
-// LoadSplitsAttributes allows an eager lookup of values, cached into the
-// loaded structs of the objects. This is for a 1-M or N-M relationship.
-func (splitL) LoadSplitsAttributes(ctx context.Context, e boil.ContextExecutor, singular bool, maybeSplit interface{}, mods queries.Applicator) error {
- var slice []*Split
- var object *Split
-
- if singular {
- var ok bool
- object, ok = maybeSplit.(*Split)
- if !ok {
- object = new(Split)
- ok = queries.SetFromEmbeddedStruct(&object, &maybeSplit)
- if !ok {
- return errors.New(fmt.Sprintf("failed to set %T from embedded struct %T", object, maybeSplit))
- }
- }
- } else {
- s, ok := maybeSplit.(*[]*Split)
- if ok {
- slice = *s
- } else {
- ok = queries.SetFromEmbeddedStruct(&slice, maybeSplit)
- if !ok {
- return errors.New(fmt.Sprintf("failed to set %T from embedded struct %T", slice, maybeSplit))
- }
- }
- }
-
- args := make([]interface{}, 0, 1)
- if singular {
- if object.R == nil {
- object.R = &splitR{}
- }
- args = append(args, object.ID)
- } else {
- Outer:
- for _, obj := range slice {
- if obj.R == nil {
- obj.R = &splitR{}
- }
-
- for _, a := range args {
- if a == obj.ID {
- continue Outer
- }
- }
-
- args = append(args, obj.ID)
- }
- }
-
- if len(args) == 0 {
- return nil
- }
-
- query := NewQuery(
- qm.From(`splits_attributes`),
- qm.WhereIn(`splits_attributes.splits_id in ?`, args...),
- )
- if mods != nil {
- mods.Apply(query)
- }
-
- results, err := query.QueryContext(ctx, e)
- if err != nil {
- return errors.Wrap(err, "failed to eager load splits_attributes")
- }
-
- var resultSlice []*SplitsAttribute
- if err = queries.Bind(results, &resultSlice); err != nil {
- return errors.Wrap(err, "failed to bind eager loaded slice splits_attributes")
- }
-
- if err = results.Close(); err != nil {
- return errors.Wrap(err, "failed to close results in eager load on splits_attributes")
- }
- if err = results.Err(); err != nil {
- return errors.Wrap(err, "error occurred during iteration of eager loaded relations for splits_attributes")
- }
-
- if singular {
- object.R.SplitsAttributes = resultSlice
- for _, foreign := range resultSlice {
- if foreign.R == nil {
- foreign.R = &splitsAttributeR{}
- }
- foreign.R.Split = object
- }
- return nil
- }
-
- for _, foreign := range resultSlice {
- for _, local := range slice {
- if local.ID == foreign.SplitsID {
- local.R.SplitsAttributes = append(local.R.SplitsAttributes, foreign)
- if foreign.R == nil {
- foreign.R = &splitsAttributeR{}
- }
- foreign.R.Split = local
- break
- }
- }
- }
-
- return nil
-}
-
// SetAccount of the split to the related item.
// Sets o.R.Account to related.
// Adds o to related.R.Splits.
return nil
}
-// AddSplitsAttributes adds the given related objects to the existing relationships
-// of the split, optionally inserting them as new records.
-// Appends related to o.R.SplitsAttributes.
-// Sets related.R.Split appropriately.
-func (o *Split) AddSplitsAttributes(ctx context.Context, exec boil.ContextExecutor, insert bool, related ...*SplitsAttribute) error {
- var err error
- for _, rel := range related {
- if insert {
- rel.SplitsID = o.ID
- if err = rel.Insert(ctx, exec, boil.Infer()); err != nil {
- return errors.Wrap(err, "failed to insert into foreign table")
- }
- } else {
- updateQuery := fmt.Sprintf(
- "UPDATE \"splits_attributes\" SET %s WHERE %s",
- strmangle.SetParamNames("\"", "\"", 0, []string{"splits_id"}),
- strmangle.WhereClause("\"", "\"", 0, splitsAttributePrimaryKeyColumns),
- )
- values := []interface{}{o.ID, rel.ID}
-
- if boil.IsDebug(ctx) {
- writer := boil.DebugWriterFrom(ctx)
- fmt.Fprintln(writer, updateQuery)
- fmt.Fprintln(writer, values)
- }
- if _, err = exec.ExecContext(ctx, updateQuery, values...); err != nil {
- return errors.Wrap(err, "failed to update foreign table")
- }
-
- rel.SplitsID = o.ID
- }
- }
-
- if o.R == nil {
- o.R = &splitR{
- SplitsAttributes: related,
- }
- } else {
- o.R.SplitsAttributes = append(o.R.SplitsAttributes, related...)
- }
-
- for _, rel := range related {
- if rel.R == nil {
- rel.R = &splitsAttributeR{
- Split: o,
- }
- } else {
- rel.R.Split = o
- }
- }
- return nil
-}
-
// Splits retrieves all the records using an executor.
func Splits(mods ...qm.QueryMod) splitQuery {
mods = append(mods, qm.From("\"splits\""))
// SplitsAttributeRels is where relationship names are stored.
var SplitsAttributeRels = struct {
- Split string
-}{
- Split: "Split",
-}
+}{}
// splitsAttributeR is where relationships are stored.
type splitsAttributeR struct {
- Split *Split `boil:"Split" json:"Split" toml:"Split" yaml:"Split"`
}
// NewStruct creates a new relationship struct
return &splitsAttributeR{}
}
-func (r *splitsAttributeR) GetSplit() *Split {
- if r == nil {
- return nil
- }
- return r.Split
-}
-
// splitsAttributeL is where Load methods for each relationship are stored.
type splitsAttributeL struct{}
return count > 0, nil
}
-// Split pointed to by the foreign key.
-func (o *SplitsAttribute) Split(mods ...qm.QueryMod) splitQuery {
- queryMods := []qm.QueryMod{
- qm.Where("\"id\" = ?", o.SplitsID),
- }
-
- queryMods = append(queryMods, mods...)
-
- return Splits(queryMods...)
-}
-
-// LoadSplit allows an eager lookup of values, cached into the
-// loaded structs of the objects. This is for an N-1 relationship.
-func (splitsAttributeL) LoadSplit(ctx context.Context, e boil.ContextExecutor, singular bool, maybeSplitsAttribute interface{}, mods queries.Applicator) error {
- var slice []*SplitsAttribute
- var object *SplitsAttribute
-
- if singular {
- var ok bool
- object, ok = maybeSplitsAttribute.(*SplitsAttribute)
- if !ok {
- object = new(SplitsAttribute)
- ok = queries.SetFromEmbeddedStruct(&object, &maybeSplitsAttribute)
- if !ok {
- return errors.New(fmt.Sprintf("failed to set %T from embedded struct %T", object, maybeSplitsAttribute))
- }
- }
- } else {
- s, ok := maybeSplitsAttribute.(*[]*SplitsAttribute)
- if ok {
- slice = *s
- } else {
- ok = queries.SetFromEmbeddedStruct(&slice, maybeSplitsAttribute)
- if !ok {
- return errors.New(fmt.Sprintf("failed to set %T from embedded struct %T", slice, maybeSplitsAttribute))
- }
- }
- }
-
- args := make([]interface{}, 0, 1)
- if singular {
- if object.R == nil {
- object.R = &splitsAttributeR{}
- }
- args = append(args, object.SplitsID)
-
- } else {
- Outer:
- for _, obj := range slice {
- if obj.R == nil {
- obj.R = &splitsAttributeR{}
- }
-
- for _, a := range args {
- if a == obj.SplitsID {
- continue Outer
- }
- }
-
- args = append(args, obj.SplitsID)
-
- }
- }
-
- if len(args) == 0 {
- return nil
- }
-
- query := NewQuery(
- qm.From(`splits`),
- qm.WhereIn(`splits.id in ?`, args...),
- )
- if mods != nil {
- mods.Apply(query)
- }
-
- results, err := query.QueryContext(ctx, e)
- if err != nil {
- return errors.Wrap(err, "failed to eager load Split")
- }
-
- var resultSlice []*Split
- if err = queries.Bind(results, &resultSlice); err != nil {
- return errors.Wrap(err, "failed to bind eager loaded slice Split")
- }
-
- if err = results.Close(); err != nil {
- return errors.Wrap(err, "failed to close results of eager load for splits")
- }
- if err = results.Err(); err != nil {
- return errors.Wrap(err, "error occurred during iteration of eager loaded relations for splits")
- }
-
- if len(resultSlice) == 0 {
- return nil
- }
-
- if singular {
- foreign := resultSlice[0]
- object.R.Split = foreign
- if foreign.R == nil {
- foreign.R = &splitR{}
- }
- foreign.R.SplitsAttributes = append(foreign.R.SplitsAttributes, object)
- return nil
- }
-
- for _, local := range slice {
- for _, foreign := range resultSlice {
- if local.SplitsID == foreign.ID {
- local.R.Split = foreign
- if foreign.R == nil {
- foreign.R = &splitR{}
- }
- foreign.R.SplitsAttributes = append(foreign.R.SplitsAttributes, local)
- break
- }
- }
- }
-
- return nil
-}
-
-// SetSplit of the splitsAttribute to the related item.
-// Sets o.R.Split to related.
-// Adds o to related.R.SplitsAttributes.
-func (o *SplitsAttribute) SetSplit(ctx context.Context, exec boil.ContextExecutor, insert bool, related *Split) error {
- var err error
- if insert {
- if err = related.Insert(ctx, exec, boil.Infer()); err != nil {
- return errors.Wrap(err, "failed to insert into foreign table")
- }
- }
-
- updateQuery := fmt.Sprintf(
- "UPDATE \"splits_attributes\" SET %s WHERE %s",
- strmangle.SetParamNames("\"", "\"", 0, []string{"splits_id"}),
- strmangle.WhereClause("\"", "\"", 0, splitsAttributePrimaryKeyColumns),
- )
- values := []interface{}{related.ID, o.ID}
-
- if boil.IsDebug(ctx) {
- writer := boil.DebugWriterFrom(ctx)
- fmt.Fprintln(writer, updateQuery)
- fmt.Fprintln(writer, values)
- }
- if _, err = exec.ExecContext(ctx, updateQuery, values...); err != nil {
- return errors.Wrap(err, "failed to update local table")
- }
-
- o.SplitsID = related.ID
- if o.R == nil {
- o.R = &splitsAttributeR{
- Split: related,
- }
- } else {
- o.R.Split = related
- }
-
- if related.R == nil {
- related.R = &splitR{
- SplitsAttributes: SplitsAttributeSlice{o},
- }
- } else {
- related.R.SplitsAttributes = append(related.R.SplitsAttributes, o)
- }
-
- return nil
-}
-
// SplitsAttributes retrieves all the records using an executor.
func SplitsAttributes(mods ...qm.QueryMod) splitsAttributeQuery {
mods = append(mods, qm.From("\"splits_attributes\""))
a.defaultResources = defaultResources
a.AccountMatchRequestC = make(chan AccountMatchRequest)
- go func() { a.matcher(a.ctx, a.db, a.logger) }()
+ go func() { a.matcher(a.ctx, a.db, a.AccountMatchRequestC, a.logger) }()
}
-func (a *accountMatchService) matcher(ctx context.Context, db *sql.DB, logger *logwrap.LogWrap) {
+func (a *accountMatchService) matcher(ctx context.Context, db *sql.DB, c chan AccountMatchRequest, logger *logwrap.LogWrap) {
var q []qm.QueryMod
- for range a.AccountMatchRequestC {
+ for range c {
logger.Info(fmt.Sprintf("received new account match request"))
- accountMatches, err := model.AccountMatches().All(ctx, db)
- if err != nil {
- logger.Error("Failed to find Account Matches")
- }
-
q = []qm.QueryMod{
+ qm.InnerJoin("splits on splits.transactions_id = transactions.id"),
+ qm.InnerJoin("account on splits.account_id = account.id"),
qm.Load("Splits"),
qm.Load("Splits.Account"),
}
continue
}
+ q = []qm.QueryMod{
+ qm.InnerJoin("account_match_regex on account_match_regex.account_match_id = account_match.id"),
+ qm.Load("AccountMatchRegexes"),
+ }
+ accountMatches, err := model.AccountMatches(q...).All(ctx, db)
+ if err != nil {
+ logger.Error("Failed to find any Account Matches")
+ continue
+ }
+
for _, accountMatch := range accountMatches {
- attr, err := model.AccountMatchAttributes(qm.Where("account_match_id=? AND name=?", accountMatch.ID, "deleted")).One(ctx, db)
- switch {
- case dberror.IsNoRowsFound(err):
- case err != nil:
+ attrDeleted, err := model.AccountMatchAttributes(qm.Where("account_match_id=? AND name=? AND value=?", accountMatch.ID, "deleted", "true")).Exists(ctx, db)
+ if err != nil {
logger.Error(err.Error())
continue
- default:
- if attr.Value == "true" {
- continue
- }
+ }
+ if attrDeleted {
+ continue
}
- accountMatchRegexes, err := model.AccountMatchRegexes(qm.Where("account_match_id=?", accountMatch.ID)).All(ctx, db)
- if err != nil {
- logger.Error(err.Error())
+ if accountMatch.R == nil || len(accountMatch.R.AccountMatchRegexes) == 0 {
continue
}
continue
}
+ if transaction.R == nil || len(transaction.R.Splits) == 0 {
+ continue
+ }
+
+ match, err := a.transactionMemoMatchesAccountMatchRegex(transaction.Memo, accountMatch.R.AccountMatchRegexes)
+ if err != nil {
+ logger.Error(err.Error())
+ continue
+ }
+ if !match {
+ continue
+ }
+
+ // Check if one of the splits is assigned to the source account
+ // of the account match. If it is, an account match should be
+ // performed on all splits for this transaction.
+ transactionMatchesAccountMatchSourceAccount := false
for _, split := range transaction.R.Splits {
if accountMatch.SourceAccountID == split.R.Account.ID {
- exists, err := model.Splits(qm.Where("transactions_id=? AND id!=?", split.TransactionsID, split.ID)).Exists(ctx, db)
- if err != nil {
- logger.Error(err.Error())
- continue
- }
- if exists {
- continue
- }
-
- for _, regex := range accountMatchRegexes {
- r, err := regexp.Compile(fmt.Sprintf("(?i)%s", regex.Regex))
- if err != nil {
- logger.Error(fmt.Sprintf("regexp compile %d: %s", regex.ID, err.Error()))
- continue
- }
-
- result := r.FindStringSubmatch(transaction.Memo)
-
- valueNum := split.ValueNum
- if split.ValueNum < 0 {
- valueNum = split.ValueNum * -1
- }
-
- if len(result) > 0 {
- s := model.Split{
- TransactionsID: transaction.ID,
- AccountID: accountMatch.DestinationAccountID,
- ValueNum: valueNum,
- ValueDenom: split.ValueDenom,
- }
- if err := s.Insert(ctx, db, boil.Infer()); err != nil {
- logger.Error(err.Error())
- continue
- }
- }
- }
+ transactionMatchesAccountMatchSourceAccount = true
+ break
+ }
+ }
+ if !transactionMatchesAccountMatchSourceAccount {
+ continue
+ }
+
+ for _, split := range transaction.R.Splits {
+ // If split matches source account id, skip this split.
+ // Account match will run on the remaining splits.
+ if accountMatch.SourceAccountID == split.R.Account.ID {
+ continue
+ }
+
+ // If split matches destination account id, skip this
+ // split. The split has already been assigned to the
+ // account.
+ if accountMatch.DestinationAccountID == split.R.Account.ID {
+ continue
+ }
+
+ if _, err := a.updateSplitIfImbalanced(ctx, db, split, accountMatch.DestinationAccountID); err != nil {
+ logger.Error(fmt.Sprintf("failed to update split: %d", split.ID))
+ continue
}
}
}
resp.Msg = "Account Match undeleted"
return resp
}
+
+// updateSplitIfImbalanced accepts a pointer transaction and accountID. It
+// finds any splits which have an account "Imbalanced" and updates that split
+// account id with the accountID. If split is updated, true is returned else
+// false.
+func (a *accountMatchService) updateSplitIfImbalanced(ctx context.Context, db *sql.DB, split *model.Split, accountID int64) (bool, error) {
+ account, err := model.Accounts(qm.Where("id=?", split.AccountID)).One(ctx, db)
+ if err != nil {
+ return false, err
+ }
+ if account.Name == "Imbalanced" {
+ split.AccountID = accountID
+ if _, err := split.Update(ctx, db, boil.Infer()); err != nil {
+ return false, err
+ }
+ return true, nil
+ }
+ return false, nil
+}
+
+func (a *accountMatchService) transactionMemoMatchesAccountMatchRegex(memo string, accountMatches model.AccountMatchRegexSlice) (bool, error) {
+ for _, regex := range accountMatches {
+ r, err := regexp.Compile(fmt.Sprintf("(?i)%s", regex.Regex))
+ if err != nil {
+ return false, err
+ }
+
+ result := r.FindStringSubmatch(memo)
+ if len(result) != 0 {
+ return true, nil
+ }
+ }
+ return false, nil
+}
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
if err != nil {
return err
}
- _, err = model.Accounts(qm.Where("name=? AND parent_id=0 AND entity_id=?", "Account", defaultEntity.ID)).One(ctx, d.DB)
+
+ grandParentAccountType, err := d.getGrantParentAccountType()
if err != nil {
- switch {
- case dberror.IsNoRowsFound(err):
- grandParentAccountType, err := d.getGrantParentAccountType()
- if err != nil {
- return err
- }
+ return err
+ }
- grandParentAccount := &model.Account{
- Name: "Account",
- EntityID: defaultEntity.ID,
- AccountTypeID: grandParentAccountType.ID,
- ParentID: int64(0),
- }
- if err := grandParentAccount.Insert(ctx, d.DB, boil.Infer()); err != nil {
- return err
+ grandParentAccount, err := model.Accounts(qm.Where("name=? AND parent_id=0 AND entity_id=? AND account_type_id=?", "Account", defaultEntity.ID, grandParentAccountType.ID)).One(ctx, d.DB)
+ switch {
+ case dberror.IsNoRowsFound(err):
+ grandParentAccount = &model.Account{
+ Name: "Account",
+ EntityID: defaultEntity.ID,
+ AccountTypeID: grandParentAccountType.ID,
+ ParentID: int64(0),
+ }
+ if err := grandParentAccount.Insert(ctx, d.DB, boil.Infer()); err != nil {
+ return err
+ }
+ case err != nil:
+ return fmt.Errorf("failed to insert grand parent account: %w", err)
+ }
+
+ assetAccountType, err := model.AccountTypes(qm.Where("name=?", "Asset")).One(ctx, d.DB)
+ if err != nil {
+ return err
+ }
+
+ accounts := []model.Account{
+ {Name: "Imbalanced", ParentID: grandParentAccount.ID, EntityID: defaultEntity.ID, AccountTypeID: assetAccountType.ID},
+ }
+ for _, account := range accounts {
+ if err := account.Insert(ctx, d.DB, boil.Infer()); err != nil {
+ switch {
+ case dberror.IsUniqueConstraint(err):
+ continue
+ default:
+ return fmt.Errorf("failed to insert account: %w", err)
}
- default:
- return fmt.Errorf("failed to insert grand parent account: %w", err)
}
}
}
}
+ imbalancedAccount, err := model.Accounts(qm.Where("name=?", "Imbalanced")).One(t.ctx, t.db)
+ if err != nil {
+ return err
+ }
+
splits := []model.Split{
{
TransactionsID: transaction.ID,
ValueNum: i.TrnAmt.Num().Int64(),
ValueDenom: i.TrnAmt.Denom().Int64(),
},
+ {
+ TransactionsID: transaction.ID,
+ AccountID: imbalancedAccount.ID,
+ ValueNum: -i.TrnAmt.Num().Int64(),
+ ValueDenom: -i.TrnAmt.Denom().Int64(),
+ },
}
for _, ii := range splits {
if err := ii.Insert(t.ctx, t.db, boil.Infer()); err != nil {
return err
}
+ tx, err := t.db.BeginTx(t.ctx, nil)
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback()
+
+ imbalancedAccount, err := model.Accounts(qm.Where("name=?", "Imbalanced")).One(t.ctx, tx)
+ if err != nil {
+ return err
+ }
+
for _, i := range stmt.BankTranList.Transactions {
transaction := model.Transaction{
EntityID: t.defaultResources.EntityID(),
v1Hash := hashers.NewTransactionHashV1()
transactionHash := v1Hash.Hash(transaction)
- hashType, err := model.TransactionsHashTypes(qm.Where("name=?", v1Hash.String())).One(t.ctx, t.db)
+ hashType, err := model.TransactionsHashTypes(qm.Where("name=?", v1Hash.String())).One(t.ctx, tx)
if err != nil {
return err
}
qm.Where("hash=?", fmt.Sprintf("%x", transactionHash.Sum(nil))),
qm.Where("transactions_hash_type_id=?", hashType.ID),
}
- exists, err := model.TransactionsHashes(q...).Exists(t.ctx, t.db)
+ exists, err := model.TransactionsHashes(q...).Exists(t.ctx, tx)
if err != nil {
return err
}
continue
}
- if err := transaction.Insert(t.ctx, t.db, boil.Infer()); err != nil {
+ if err := transaction.Insert(t.ctx, tx, boil.Infer()); err != nil {
return err
}
TransactionsHashTypeID: hashType.ID,
Hash: fmt.Sprintf("%x", transactionHash.Sum(nil)),
}
- if err := th.Insert(t.ctx, t.db, boil.Infer()); err != nil {
+ if err := th.Insert(t.ctx, tx, boil.Infer()); err != nil {
return err
}
}
for _, ii := range transactionAttributes {
- if err := ii.Insert(t.ctx, t.db, boil.Infer()); err != nil {
+ if err := ii.Upsert(t.ctx, tx, true, []string{"transactions_id", "name"}, boil.Whitelist("name"), boil.Infer()); err != nil {
return err
}
}
ValueNum: i.TrnAmt.Num().Int64(),
ValueDenom: i.TrnAmt.Denom().Int64(),
},
+ {
+ TransactionsID: transaction.ID,
+ AccountID: imbalancedAccount.ID,
+ ValueNum: -i.TrnAmt.Num().Int64(),
+ ValueDenom: i.TrnAmt.Denom().Int64(),
+ },
}
for _, ii := range splits {
- if err := ii.Insert(t.ctx, t.db, boil.Infer()); err != nil {
+ if err := ii.Insert(t.ctx, tx, boil.Infer()); err != nil {
return err
}
}
+
+ }
+
+ if err := tx.Commit(); err != nil {
+ return err
}
+
return nil
}
"database/sql"
"encoding/base64"
"fmt"
+ "math/big"
"pennyapp/backend/logwrap"
"pennyapp/backend/model"
"pennyapp/backend/types"
"strings"
"time"
+ "github.com/shopspring/decimal"
"github.com/volatiletech/sqlboiler/v4/boil"
"github.com/volatiletech/sqlboiler/v4/queries/qm"
)
return resp
}
+func (t *transactionService) transactionExistsInSlice(transaction *model.Transaction, transactions []types.Transaction) bool {
+ for _, i := range transactions {
+ if i.ID == transaction.ID {
+ return true
+ }
+ }
+ return false
+}
+
func (t *transactionService) GetTransactionsAccount(id int64) types.TransactionsResponse {
var resp types.TransactionsResponse
var q []qm.QueryMod
qm.Where("account.id=?", id),
qm.InnerJoin("splits on splits.transactions_id = transactions.id"),
qm.InnerJoin("account on account.id = splits.account_id"),
+ qm.OrderBy("transactions.date DESC"),
qm.Load("Splits"),
qm.Load("Splits.Account"),
}
resp.Data = []types.Transaction{}
for _, i := range transactions {
+
+ if exists := t.transactionExistsInSlice(i, resp.Data); exists {
+ continue
+ }
+
transaction, err := types.NewTransaction(t.ctx, t.db, i)
if err != nil {
resp.Msg = err.Error()
return resp
}
+func (t *transactionService) GetTotalBalance() types.TransactionsNetAssetsResponse {
+ var resp types.TransactionsNetAssetsResponse
+ var err error
+
+ transactions, err := model.Transactions().All(t.ctx, t.db)
+ if err != nil {
+ resp.Msg = err.Error()
+ t.logger.Error(resp.Msg)
+ return resp
+ }
+
+ imbalancedAccount, err := model.Accounts(qm.Where("name=?", "Imbalanced")).One(t.ctx, t.db)
+ if err != nil {
+ resp.Msg = err.Error()
+ t.logger.Error(resp.Msg)
+ return resp
+ }
+
+ amount := float64(0)
+ for _, i := range transactions {
+ transaction, err := types.NewTransaction(t.ctx, t.db, i)
+ if err != nil {
+ resp.Msg = err.Error()
+ t.logger.Error(resp.Msg)
+ return resp
+ }
+ if transaction.Deleted == true {
+ continue
+ }
+
+ for _, split := range transaction.Splits {
+ if split.Account.ID != imbalancedAccount.ID {
+ continue
+ }
+ r := big.NewRat(split.ValueNum, split.ValueDenom)
+ f, _ := r.Float64()
+ amount = amount + f
+ t.logger.Debug(fmt.Sprintf("%#+v, %#+v", split, amount))
+ }
+ }
+ resp.Data = decimal.NewFromFloat(amount).StringFixed(2)
+ resp.Success = true
+ return resp
+}
+
func (t *transactionService) GetTransactions() types.TransactionsResponse {
var resp types.TransactionsResponse
var err error
"math/big"
"pennyapp/backend/internal/dberror"
"pennyapp/backend/model"
+ "strings"
"github.com/shopspring/decimal"
"github.com/volatiletech/sqlboiler/v4/queries/qm"
return err
}
+ accountType, err := model.AccountTypes(qm.Where("id=?", account.AccountTypeID)).One(ctx, db)
+ if err != nil {
+ return err
+ }
+
amount := float64(0)
for _, split := range splits {
+ transactionDeleted, err := isTransactionDeleted(ctx, db, split.TransactionsID)
+ if err != nil {
+ return err
+ }
+ if transactionDeleted {
+ continue
+ }
+
r := big.NewRat(split.ValueNum, split.ValueDenom)
f, _ := r.Float64()
+
+ switch strings.ToLower(accountType.Name) {
+ case "liability", "income":
+ f = -f
+ }
+
amount = amount + f
}
+
a.Amount = decimal.NewFromFloat(amount).StringFixed(2)
return nil
}
"math/big"
"pennyapp/backend/internal/dberror"
"pennyapp/backend/model"
+ "strings"
"github.com/shopspring/decimal"
"github.com/volatiletech/sqlboiler/v4/queries/qm"
qm.Load("Transaction"),
qm.Load("Transaction.Splits"),
qm.Load("Transaction.Splits.Account"),
- qm.Load("Transaction.Splits.Account.AccountType"),
}
tagTransactions, err := model.TagTransactions(q...).All(ctx, db)
if err != 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 {
- r := big.NewRat(split.ValueNum, split.ValueDenom)
- f, _ := r.Float64()
- amount = amount + f
+ 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)
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.
package types
+import (
+ "context"
+ "database/sql"
+ "pennyapp/backend/internal/dberror"
+ "pennyapp/backend/model"
+
+ "github.com/volatiletech/sqlboiler/v4/queries/qm"
+)
+
type JSResp struct {
Success bool `json:"success"`
Msg string `json:"msg"`
ID int64 `json:"id"`
Name string `json:"name"`
}
+
+// isTransactionDeleted accepts a transaction id and returns true if it is marked
+// as deleted.
+func isTransactionDeleted(ctx context.Context, db *sql.DB, transactionsID int64) (bool, error) {
+ var resp bool
+ transactionAttribute, err := model.TransactionsAttributes(qm.Where("transactions_id=? AND name=?", transactionsID, "deleted")).One(ctx, db)
+ switch {
+ case dberror.IsNoRowsFound(err):
+ case err != nil:
+ return resp, err
+ default:
+ if transactionAttribute.Value == "true" {
+ resp = true
+ }
+ }
+ return resp, nil
+}
+PRAGMA foreign_keys=off;
+
+ALTER TABLE splits RENAME TO splits_1;
+
+CREATE TABLE IF NOT EXISTS splits
+(
+ id INTEGER NOT NULL PRIMARY KEY,
+ transactions_id INTEGER NOT NULL,
+ account_id INTEGER NOT NULL,
+ value_num BIGINT NOT NULL,
+ value_denom BIGINT NOT NULL,
+ FOREIGN KEY(transactions_id) REFERENCES transactions(id),
+ FOREIGN KEY(account_id) REFERENCES account(id)
+);
+
+INSERT INTO splits SELECT * FROM splits_1;
+
+DROP TABLE splits_1;
+
+PRAGMA foreign_keys=on;
// 000008_comments.up.sql
// 000009_attachments.down.sql
// 000009_attachments.up.sql
+// 000010_doubleentry.down.sql
+// 000010_doubleentry.up.sql
// migrations.go
// migrations_test.go
package migrations
return nil, err
}
- info := bindataFileInfo{name: "000008_comments.down.sql", size: 67, mode: os.FileMode(420), modTime: time.Unix(1723236041, 0)}
+ info := bindataFileInfo{name: "000008_comments.down.sql", size: 67, mode: os.FileMode(420), modTime: time.Unix(1723934530, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
return nil, err
}
- info := bindataFileInfo{name: "000008_comments.up.sql", size: 398, mode: os.FileMode(420), modTime: time.Unix(1723236041, 0)}
+ info := bindataFileInfo{name: "000008_comments.up.sql", size: 398, mode: os.FileMode(420), modTime: time.Unix(1723934530, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
return nil, err
}
- info := bindataFileInfo{name: "000009_attachments.down.sql", size: 79, mode: os.FileMode(420), modTime: time.Unix(1723324013, 0)}
+ info := bindataFileInfo{name: "000009_attachments.down.sql", size: 79, mode: os.FileMode(420), modTime: time.Unix(1723934551, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
return nil, err
}
- info := bindataFileInfo{name: "000009_attachments.up.sql", size: 540, mode: os.FileMode(420), modTime: time.Unix(1723374891, 0)}
+ info := bindataFileInfo{name: "000009_attachments.up.sql", size: 540, mode: os.FileMode(420), modTime: time.Unix(1723934551, 0)}
+ a := &asset{bytes: bytes, info: info}
+ return a, nil
+}
+
+var __000010_doubleentryDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00")
+
+func _000010_doubleentryDownSqlBytes() ([]byte, error) {
+ return bindataRead(
+ __000010_doubleentryDownSql,
+ "000010_doubleentry.down.sql",
+ )
+}
+
+func _000010_doubleentryDownSql() (*asset, error) {
+ bytes, err := _000010_doubleentryDownSqlBytes()
+ if err != nil {
+ return nil, err
+ }
+
+ info := bindataFileInfo{name: "000010_doubleentry.down.sql", size: 0, mode: os.FileMode(420), modTime: time.Unix(1724476098, 0)}
+ a := &asset{bytes: bytes, info: info}
+ return a, nil
+}
+
+var __000010_doubleentryUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x74\x91\xcd\x4e\x84\x40\x10\x84\xef\xfd\x14\x7d\x04\xe3\xc5\x33\xf1\x30\x8b\x0d\x99\x08\x03\x69\xda\xc4\x3d\x11\xc2\x8f\x21\xae\x83\xd9\x01\x13\xdf\xde\x80\x18\xc5\x65\xaf\x35\x5f\x25\xdf\x74\xe5\xac\xe2\x54\x61\x37\x9c\xdb\xfe\xc5\x96\xaf\xed\xa7\xbb\x1f\xba\x2e\x00\x50\x89\x10\xa3\xa8\x43\x42\xe8\xde\x4f\xfd\xe8\x90\xc9\xa8\x94\x50\xb2\x35\x28\xef\x02\x80\x90\x49\x09\xad\xa0\x8e\xd0\x64\x82\xf4\xac\x0b\x29\x56\x0a\x3c\x40\x44\xec\x1b\xd4\x46\x28\x26\x5e\x10\xf3\x94\x24\x98\xb3\x4e\x15\x1f\xf1\x91\x8e\xb7\x0b\x34\x9e\x2b\xeb\xaa\x7a\xec\x07\xeb\xca\x9d\xc6\x37\x55\xd5\xf5\x30\xd9\xf1\x3a\xf0\x51\x9d\xa6\xb6\xb4\xd3\x1b\x1e\x74\xac\x8d\xec\x3e\x37\xad\x1d\xae\x00\x51\xc6\xa4\x63\x33\x7b\x79\xff\x94\x7c\x64\x8a\x88\xc9\x84\x54\x6c\x74\xbd\xbe\xf1\x2f\xdb\xbf\xaa\x9b\xe2\x1a\xcf\x1d\xf0\x03\x00\x6d\x0a\x62\x99\x7f\xf3\x73\x5b\x2c\x28\xa1\x50\xf0\x06\x23\xce\xd2\xbf\x07\x7f\xe0\x2c\xdf\xec\xb2\xa4\xbb\x43\xda\x00\xbe\x02\x00\x00\xff\xff\xe8\x95\x40\x5c\xe1\x01\x00\x00")
+
+func _000010_doubleentryUpSqlBytes() ([]byte, error) {
+ return bindataRead(
+ __000010_doubleentryUpSql,
+ "000010_doubleentry.up.sql",
+ )
+}
+
+func _000010_doubleentryUpSql() (*asset, error) {
+ bytes, err := _000010_doubleentryUpSqlBytes()
+ if err != nil {
+ return nil, err
+ }
+
+ info := bindataFileInfo{name: "000010_doubleentry.up.sql", size: 481, mode: os.FileMode(420), modTime: time.Unix(1724476935, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
"000008_comments.up.sql": _000008_commentsUpSql,
"000009_attachments.down.sql": _000009_attachmentsDownSql,
"000009_attachments.up.sql": _000009_attachmentsUpSql,
+ "000010_doubleentry.down.sql": _000010_doubleentryDownSql,
+ "000010_doubleentry.up.sql": _000010_doubleentryUpSql,
"migrations.go": migrationsGo,
"migrations_test.go": migrations_testGo,
}
"000008_comments.up.sql": &bintree{_000008_commentsUpSql, map[string]*bintree{}},
"000009_attachments.down.sql": &bintree{_000009_attachmentsDownSql, map[string]*bintree{}},
"000009_attachments.up.sql": &bintree{_000009_attachmentsUpSql, map[string]*bintree{}},
+ "000010_doubleentry.down.sql": &bintree{_000010_doubleentryDownSql, map[string]*bintree{}},
+ "000010_doubleentry.up.sql": &bintree{_000010_doubleentryUpSql, map[string]*bintree{}},
"migrations.go": &bintree{migrationsGo, map[string]*bintree{}},
"migrations_test.go": &bintree{migrations_testGo, map[string]*bintree{}},
}}
</div>
<!-- <div v-for="(split, index) in splits" :key="index" v-show="splits.length >= 2"> -->
- <div class="row gx-3 mb-3" v-show="splits[0].amount != '0.00'">
+ <div class="row gx-3 mb-3" v-show="splits[0].amount != '0.00' && splits[0].account.id != null">
<!-- Form Group (increase account)-->
<div class="col-md-6">
<label class="small mb-1" for="inputFirstName">Account</label>
<div class="mb-3">
<select class="form-select" aria-label="Type" v-model="splits[1].account.id">
<option selected="" disabled="">Select an account:</option>
- <!-- <option v-for="account in accounts.filter(account => account.account_type.name === 'Current Liability')" :value="account.id">{{ account.name }}</option> -->
<option v-for="account in accounts" :value="account.id">{{ account.name }}</option>
</select>
</div>
<!-- </div> -->
<div class="row gx-3 jjmb-3">
- <div class="col-md-6" v-show="splits[1].amount != '0.00' && splits[0].amount != '0.00'">
+ <div class="col-md-6" v-show="splits[1].amount != '0.00' && splits[0].amount != '0.00' && splits[0].account.id != null && splits[1].account.id != null && (splits[0].amount + splits[1].amount == 0)">
<button class="btn btn-primary" type="submit">Create</button>
</div>
</div>
}
const { success, msg, data } = await UpdateSplit(split.id, split.account_id, Number(split.amount))
+ console.log("DEBUG1: " + JSON.stringify(split))
if (!success) {
$message.error(msg)
return
<script>
import { ref } from 'vue';
-import { GetTransactions, GetTransactionsAccount, GetTransactionsTag } from 'wailsjs/go/services/transactionService.js'
+import { GetTransactions, GetTransactionsAccount, GetTransactionsTag, GetTotalBalance } from 'wailsjs/go/services/transactionService.js'
+import { GetAccountByName } from 'wailsjs/go/services/accountService.js'
import { useRouter } from 'vue-router';
export default {
searchValue: ref(),
searchFields: ['memo', 'amount'],
transactions: [],
+ totalBalance: ref(),
+ imbalancedAccountID: ref(),
headers: [
{
text: 'Date',
() => this.$route.params,
() => {
this.getTransactions()
+ this.getTotalBalance()
+ this.getImbalancedAccountID()
},
{ immediate: true },
)
},
methods: {
+ async getImbalancedAccountID() {
+ const getAccountByNameResp = await GetAccountByName("Imbalanced")
+ if (!getAccountByNameResp.success) {
+ return
+ }
+ this.imbalancedAccountID = getAccountByNameResp.data.id
+ },
+ async getTotalBalance() {
+ const getTotalBalanceResp = await GetTotalBalance()
+ if (!getTotalBalanceResp.success) {
+ $message.error(getTotalBalanceResp.msg)
+ return
+ }
+ this.totalBalance = getTotalBalanceResp.data
+ },
async getTransactions() {
let transactions = [];
</EasyDataTable>
</div>
</div>
+
+ <div class="col-md-6">
+ <label class="small mb-1">Total Balance:</label>
+ <label class="small mb-1"><router-link :to="{ name: 'get-account', params: { id: this.imbalancedAccountID }}">{{ totalBalance }}</router-link></label>
+ </div>
</div>
</div>
<!-- /.container-fluid -->
+
</div>
<!-- End of Main Content -->
return a;
}
}
+ export class TransactionsNetAssetsResponse {
+ success: boolean;
+ msg: string;
+ data: string;
+
+ static createFrom(source: any = {}) {
+ return new TransactionsNetAssetsResponse(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.success = source["success"];
+ this.msg = source["msg"];
+ this.data = source["data"];
+ }
+ }
export class TransactionsResponse {
success: boolean;
msg: string;
export function GetAccount(arg1:number):Promise<types.AccountResponse>;
+export function GetAccountByName(arg1:string):Promise<types.AccountResponse>;
+
export function GetAccounts():Promise<types.JSResp>;
export function GetChildAccountTypes():Promise<types.JSResp>;
return window['go']['services']['accountService']['GetAccount'](arg1);
}
+export function GetAccountByName(arg1) {
+ return window['go']['services']['accountService']['GetAccountByName'](arg1);
+}
+
export function GetAccounts() {
return window['go']['services']['accountService']['GetAccounts']();
}
export function DeleteTransaction(arg1:number):Promise<types.TransactionResponse>;
+export function GetTotalBalance():Promise<types.TransactionsNetAssetsResponse>;
+
export function GetTransaction(arg1:number):Promise<types.TransactionResponse>;
export function GetTransactions():Promise<types.TransactionsResponse>;
return window['go']['services']['transactionService']['DeleteTransaction'](arg1);
}
+export function GetTotalBalance() {
+ return window['go']['services']['transactionService']['GetTotalBalance']();
+}
+
export function GetTransaction(arg1) {
return window['go']['services']['transactionService']['GetTransaction'](arg1);
}
[sqlite3]
dbname = "./.pennyapp.db"
-blacklist = ["schema_migrations"]
+blacklist = ["schema_migrations", "splits_1"]